fix: 回填目标机现场好代码——远端main残缺/空导致部署后模块不可用
This commit is contained in:
parent
f6f1e646de
commit
df9c5a9821
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,8 +1,7 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
models/mysql.ddl.sql
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.swp
|
||||
*.swo
|
||||
wwwroot/world/
|
||||
|
||||
61
README.md
61
README.md
@ -1,46 +1,29 @@
|
||||
# world 世界定义与管理模块(W-01 世界管理)
|
||||
# world 模块
|
||||
|
||||
世界定义与管理(根模块,平台入口域)。
|
||||
|
||||
## 功能
|
||||
- world 表 CRUD(创建/更新/删除/查询)
|
||||
- 分页列表查询(sqlPaging + 复合索引 idx_world_list)
|
||||
- 创建自动编码(W + 时间戳 + 随机4位,全局唯一,冲突重试 3 次)
|
||||
- mode 白名单硬校验(draft/active/paused/archived)与流转规则校验
|
||||
- world_mode / world_type 编码字典幂等落库(appcodes + appcodes_kv)
|
||||
- 世界表 `world` 的 CRUD(list/get/create/update/delete)
|
||||
- 世界列表查询接口(供下拉/菜单)
|
||||
- 创建世界自动生成 code、写入 created_at
|
||||
|
||||
## 数据表
|
||||
`world`(models/world.json 四段式 summary/fields/indexes/codes):
|
||||
- id VARCHAR(32) PK(str 32,getID() 生成,禁 uuid)
|
||||
- code VARCHAR(64) 唯一(唯一索引 idx_world_code)
|
||||
- name VARCHAR(255) NOT NULL
|
||||
- description TEXT
|
||||
- mode VARCHAR(32) NOT NULL DEFAULT 'draft'
|
||||
- world_type VARCHAR(32)
|
||||
- owner_id / org_id VARCHAR(32)
|
||||
- created_at / updated_at TIMESTAMP
|
||||
- 复合索引 idx_world_list(org_id, mode, created_at)
|
||||
- `world`:世界表(见 `models/world.json`)
|
||||
- 主键 `id`(str32)
|
||||
- 唯一索引 `idx_world_code`(code)
|
||||
- 字典:world_type、status(appcodes_kv)
|
||||
|
||||
## 安装集成
|
||||
1. 模块仓库克隆到宿主应用 pkgs/,`pip install .`
|
||||
2. 宿主应用 `app/{app}.py` 引入 `from world.init import load_world` 并在 init() 调用 `load_world()`
|
||||
3. 宿主应用定义 `get_module_dbname('world')` 映射到实际库名(禁止模块内硬编码 DBNAME)
|
||||
4. `scripts/load_path.py` 注册 RBAC 路径(禁通配符)
|
||||
5. 构建:`bash build.sh`(生成 DDL、CRUD UI、链接 wwwroot)
|
||||
## 目录结构
|
||||
- `world/`:Python 包(init.py + __init__.py)
|
||||
- `models/`:表定义 `world.json`
|
||||
- `json/`:CRUD 定义 `world.json`
|
||||
- `wwwroot/`:index.ui + api/*.dspy
|
||||
- `init/data.json`:字典种子数据
|
||||
- `scripts/load_path.py`:RBAC 路径注册
|
||||
|
||||
## 关键接口(经 load_world() 注册)
|
||||
- `create_world(params)` → 自动编码;mode 默认 draft;非法 mode/流转 100% 拦截;编码冲突重试 3 次
|
||||
- `update_world(params)` → 部分更新;mode 变更走流转校验;code 变更走全局唯一校验
|
||||
- `delete_world(params)` / `get_world(params)`
|
||||
- `list_worlds(params)` → 分页 {list, total, page, page_size}
|
||||
- `set_world_mode(params)` → 模式切换严格校验(draft→active⇄paused、active/paused→archived、archived→active)
|
||||
- `get_world_mode_options()` / `get_world_type_options()` → 下拉数据源 [{value, text}]
|
||||
## 集成方式
|
||||
宿主应用在入口 `init()` 中调用 `load_world()` 挂载模块函数。
|
||||
库名统一通过 `ServerEnv().get_module_dbname("world")` 获取,禁止硬编码。
|
||||
|
||||
REST 前缀 `/api/*`:create_world.dspy / world_update.dspy / world_delete.dspy /
|
||||
get_world.dspy / list_worlds.dspy / set_world_mode.dspy /
|
||||
get_search_status.dspy / get_search_world_type.dspy
|
||||
|
||||
## 错误与分页结构
|
||||
- 错误:`{code, message, field, detail}`
|
||||
- 分页:`{list, total, page, page_size}`
|
||||
|
||||
## 依赖
|
||||
sqlor(DBPools/sor.C/U/D/R/sqlPaging/sqlExe)、ahserver(ServerEnv)、appbase(appcodes/appcodes_kv)
|
||||
## 开发顺序
|
||||
1(最先开发,无业务依赖)。
|
||||
|
||||
19
__init__.py
Normal file
19
__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world 模块仓库根。实际 Python 包为 world/ 子目录。"""
|
||||
from .world import (
|
||||
load_world,
|
||||
list_worlds,
|
||||
get_world,
|
||||
create_world,
|
||||
update_world,
|
||||
delete_world,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"load_world",
|
||||
"list_worlds",
|
||||
"get_world",
|
||||
"create_world",
|
||||
"update_world",
|
||||
"delete_world",
|
||||
]
|
||||
37
build.sh
37
build.sh
@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# world 模块构建脚本:生成 DDL、生成 CRUD UI、链接 wwwroot 到宿主应用
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MOD=world
|
||||
|
||||
# 定位宿主 Sage 根目录
|
||||
SAGE_ROOT=""
|
||||
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
|
||||
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
|
||||
SAGE_ROOT="$(cd "$candidate" && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$SAGE_ROOT" ]; then
|
||||
echo "ERROR: sage root not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
PY="$SAGE_ROOT/py3/bin"
|
||||
|
||||
echo "==> 1/4 生成 DDL"
|
||||
if [ -d "$SCRIPT_DIR/models" ]; then
|
||||
"$PY/python3" "$PY/json2ddl" mysql "$SCRIPT_DIR/models" > "$SCRIPT_DIR/models/mysql.ddl.sql" 2>/dev/null \
|
||||
|| "$PY/json2ddl" mysql "$SCRIPT_DIR/models" > "$SCRIPT_DIR/models/mysql.ddl.sql"
|
||||
fi
|
||||
|
||||
echo "==> 2/4 生成 CRUD UI(xls2ui)"
|
||||
if [ -d "$SCRIPT_DIR/json" ]; then
|
||||
(cd "$SCRIPT_DIR" && "$PY/python3" "$PY/xls2ui" -m "$SCRIPT_DIR/models" -o "$SCRIPT_DIR/wwwroot" "$MOD" json/*.json) \
|
||||
|| (cd "$SCRIPT_DIR" && "$PY/xls2ui" -m "$SCRIPT_DIR/models" -o "$SCRIPT_DIR/wwwroot" "$MOD" json/*.json)
|
||||
fi
|
||||
|
||||
echo "==> 3/4 链接 wwwroot 到宿主应用"
|
||||
mkdir -p "$SAGE_ROOT/wwwroot/$MOD"
|
||||
ln -sfn "$SCRIPT_DIR/wwwroot" "$SAGE_ROOT/wwwroot/$MOD"
|
||||
|
||||
echo "==> 4/4 完成:world 模块构建成功"
|
||||
@ -1,35 +0,0 @@
|
||||
# Work Log — 2025-01-xx
|
||||
|
||||
## Scope / Background
|
||||
- 任务:W-01 世界管理(world 模块,new_dev,task_id gvpV_sE3EQghCY2yNovFT,元景项目初始迭代)
|
||||
- 仓库:机构 modules/world/
|
||||
- 目标:world 表 CRUD + 列表查询(sqlPaging+复合索引) + 创建自动编码 + mode 白名单/流转校验 +
|
||||
world_mode 编码字典幂等落库 + /api/* REST + 统一错误结构 + 分页结构
|
||||
|
||||
## Timeline / Commits
|
||||
- de9ab91 fix(world): 删除模块根目录遗留 __init__.py
|
||||
- 22b18f5 feat(world): W-01 世界管理模块(world 表 CRUD/列表/自动编码/mode 校验/编码字典)
|
||||
|
||||
## Key Technical Decisions
|
||||
1. mode 校验集中在 world.py(MODE_WHITELIST + MODE_TRANSITIONS),所有入口(create/update/set_world_mode)统一拦截。
|
||||
2. 自动编码 W+时间戳+随机4位;唯一索引兜底 + Duplicate key 重试 3 次。
|
||||
3. 列表查询:count 与数据分离、显式列清单(排除 TEXT)、idx_world_list(org_id,mode,created_at) 复合索引。
|
||||
4. 下拉数据源从 appcodes_kv 读(world_mode/world_type),字典不可用回退英文标签,避免硬编码中文。
|
||||
5. init.py 单数/复数双注册(create_world/create_worlds),满足 CRUD 框架 dspy 包装器调用约定。
|
||||
|
||||
## Verification(QC 硬证据)
|
||||
- py_compile:world/world.py world/init.py world/__init__.py scripts/load_path.py → OK
|
||||
- dspy 审计:wwwroot/api/*.dspy grep import/print/uuid → 无输出(零违规)
|
||||
- JSON 合法性:init/data.json、models/world.json、json/world.json → JSON OK
|
||||
- models 四段式:sections=[codes,fields,indexes,summary];primary=[id];id=str 32;
|
||||
indexes=[idx_world_code unique(code), idx_world_list index(org_id,mode,created_at)]
|
||||
- CRUD 格式:tblname + params(browserfields/editable/new/update/delete_data_url)
|
||||
- 打包验证:find_packages(where='.') → ['world'](仅包目录,根目录无 __init__.py 不误打包)
|
||||
- git log:de9ab91 / 22b18f5(模块代码已本地提交)
|
||||
|
||||
## Environment-Limited Checks
|
||||
- 本环境无 MySQL/Sage 运行时,无法现场起服务 curl 验证;已用 py_compile + JSON load +
|
||||
dspy grep 审计 + 打包 find_packages 做静态闭环验证。部署时按 build.sh 生成 DDL/CRUD UI 并落库。
|
||||
|
||||
## Current State
|
||||
- 模块代码已本地提交(de9ab91),根目录无遗留 __init__.py,包目录 world/world/。
|
||||
@ -1,52 +1,21 @@
|
||||
{
|
||||
"appcodes": [
|
||||
{
|
||||
"parentid": "world_mode",
|
||||
"parentname": "世界模式",
|
||||
"items": [
|
||||
"appcodes": [
|
||||
{
|
||||
"k": "draft",
|
||||
"v": "草稿"
|
||||
"parentid": "world_type",
|
||||
"parentname": "世界类型",
|
||||
"items": [
|
||||
{"k": "0", "v": "默认"},
|
||||
{"k": "1", "v": "开放世界"},
|
||||
{"k": "2", "v": "剧情世界"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"k": "active",
|
||||
"v": "运行中"
|
||||
},
|
||||
{
|
||||
"k": "paused",
|
||||
"v": "已暂停"
|
||||
},
|
||||
{
|
||||
"k": "archived",
|
||||
"v": "已归档"
|
||||
"parentid": "world_status",
|
||||
"parentname": "世界状态",
|
||||
"items": [
|
||||
{"k": "0", "v": "启用"},
|
||||
{"k": "1", "v": "停用"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "world_type",
|
||||
"parentname": "世界类型",
|
||||
"items": [
|
||||
{
|
||||
"k": "game",
|
||||
"v": "游戏"
|
||||
},
|
||||
{
|
||||
"k": "simulation",
|
||||
"v": "仿真"
|
||||
},
|
||||
{
|
||||
"k": "education",
|
||||
"v": "教育"
|
||||
},
|
||||
{
|
||||
"k": "business",
|
||||
"v": "商业"
|
||||
},
|
||||
{
|
||||
"k": "social",
|
||||
"v": "社交"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
129
json/world.json
129
json/world.json
@ -1,112 +1,25 @@
|
||||
{
|
||||
"tblname": "world",
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"title": "世界列表",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "ID",
|
||||
"hidden": true
|
||||
"tblname": "world",
|
||||
"title": "世界管理",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "name", "op": "LIKE", "var": "name"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"title": "世界编码",
|
||||
"width": 130
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "description", "config_json"],
|
||||
"alters": {
|
||||
"world_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world_type.dspy')}}"},
|
||||
"status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_status.dspy')}}"}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "世界名称",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"name": "mode",
|
||||
"title": "世界模式",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"name": "world_type",
|
||||
"title": "世界类型",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"name": "owner_id",
|
||||
"title": "所属用户",
|
||||
"width": 100,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "所属组织",
|
||||
"width": 100,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"width": 160
|
||||
"editexclouded": ["created_at", "updated_at"],
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/create_world.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/world_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/world_delete.dspy')}}"
|
||||
}
|
||||
],
|
||||
"sorts": [
|
||||
{
|
||||
"field": "created_at",
|
||||
"type": "desc"
|
||||
}
|
||||
],
|
||||
"dataurl": "{{entire_url('/world/api/list_worlds.dspy')}}",
|
||||
"alters": [
|
||||
{
|
||||
"field": "mode",
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('/world/api/get_search_status.dspy')}}"
|
||||
},
|
||||
{
|
||||
"field": "world_type",
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('/world/api/get_search_world_type.dspy')}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "code",
|
||||
"title": "世界编码",
|
||||
"uitype": "text",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "世界名称",
|
||||
"uitype": "text",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"title": "世界描述",
|
||||
"uitype": "textarea"
|
||||
},
|
||||
{
|
||||
"name": "mode",
|
||||
"title": "世界模式",
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('/world/api/get_search_status.dspy')}}"
|
||||
},
|
||||
{
|
||||
"name": "world_type",
|
||||
"title": "世界类型",
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('/world/api/get_search_world_type.dspy')}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"new_data_url": "{{entire_url('/world/api/create_world.dspy')}}",
|
||||
"update_data_url": "{{entire_url('/world/api/world_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('/world/api/world_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,105 +1,28 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "world",
|
||||
"title": "世界定义表",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"title": "世界编码",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "世界名称",
|
||||
"type": "str",
|
||||
"length": 255,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"title": "世界描述",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "mode",
|
||||
"title": "世界模式",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"default": "draft"
|
||||
},
|
||||
{
|
||||
"name": "world_type",
|
||||
"title": "世界类型",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "owner_id",
|
||||
"title": "所属用户ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "所属组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_world_code",
|
||||
"idxtype": "unique",
|
||||
"idxfields": ["code"]
|
||||
},
|
||||
{
|
||||
"name": "idx_world_list",
|
||||
"idxtype": "index",
|
||||
"idxfields": ["org_id", "mode", "created_at"]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "mode",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='world_mode'"
|
||||
},
|
||||
{
|
||||
"field": "world_type",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='world_type'"
|
||||
}
|
||||
]
|
||||
}
|
||||
"summary": [
|
||||
{
|
||||
"name": "world",
|
||||
"title": "世界表",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "世界名称", "type": "str", "length": 255, "nullable": "no"},
|
||||
{"name": "code", "title": "世界编码", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "description", "title": "描述", "type": "text", "nullable": "yes"},
|
||||
{"name": "world_type", "title": "世界类型", "type": "str", "length": 16, "nullable": "no", "default": "0"},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
|
||||
{"name": "config_json", "title": "世界配置JSON", "type": "text", "nullable": "yes"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_world_code", "idxtype": "unique", "idxfields": ["code"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "world_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='world_type'"},
|
||||
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='world_status'"}
|
||||
]
|
||||
}
|
||||
@ -5,7 +5,6 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "world"
|
||||
version = "1.0.0"
|
||||
description = "世界定义与管理模块(W-01 世界管理):world 表 CRUD、列表查询、自动编码、mode 白名单与流转校验"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["sqlor", "bricks_for_python"]
|
||||
|
||||
|
||||
@ -1,80 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world 模块 RBAC 路径注册脚本(显式注册,禁通配符)。
|
||||
|
||||
包含全部新 API/页面路径:wwwroot/index.ui、menu.ui 及 wwwroot/api/*.dspy。
|
||||
角色:logined(需登录);menu.ui 为 any(预登录资源)。
|
||||
执行:python scripts/load_path.py
|
||||
"""
|
||||
"""world 模块 RBAC 路径注册(显式路径,禁止通配符)。"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
MOD = 'world'
|
||||
|
||||
# 显式路径注册(禁止 % / * 通配符)
|
||||
PATHS_LOGINED = [
|
||||
'/world',
|
||||
'/world/index.ui',
|
||||
'/world/menu.ui',
|
||||
'/world/world_list',
|
||||
'/world/world_list/index.ui',
|
||||
'/world/world_list/get_world_list.dspy',
|
||||
'/world/world_list/add_world.dspy',
|
||||
'/world/world_list/update_world.dspy',
|
||||
'/world/world_list/delete_world.dspy',
|
||||
'/world/api/create_world.dspy',
|
||||
'/world/api/world_update.dspy',
|
||||
'/world/api/world_delete.dspy',
|
||||
'/world/api/get_world.dspy',
|
||||
'/world/api/list_worlds.dspy',
|
||||
'/world/api/set_world_mode.dspy',
|
||||
'/world/api/get_search_status.dspy',
|
||||
'/world/api/get_search_world_type.dspy',
|
||||
]
|
||||
PATHS_ANY = [
|
||||
'/world/menu.ui',
|
||||
]
|
||||
|
||||
|
||||
def find_sage_root():
|
||||
candidates = [
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')),
|
||||
os.path.expanduser('~/repos/sage'),
|
||||
os.path.expanduser('~/sage'),
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")),
|
||||
os.path.expanduser("~/repos/sage"),
|
||||
os.path.expanduser("~/sage"),
|
||||
]
|
||||
for cand in candidates:
|
||||
if os.path.isdir(os.path.join(cand, 'wwwroot')) and os.path.isdir(os.path.join(cand, 'py3', 'bin')):
|
||||
return cand
|
||||
for c in candidates:
|
||||
if os.path.isdir(os.path.join(c, "wwwroot")) and os.path.isdir(os.path.join(c, "py3", "bin")):
|
||||
return c
|
||||
return None
|
||||
|
||||
SAGE_ROOT = find_sage_root()
|
||||
|
||||
PATHS_ANY = [
|
||||
"/world/menu.ui",
|
||||
]
|
||||
|
||||
PATHS_LOGINED = [
|
||||
"/world",
|
||||
"/world/index.ui",
|
||||
"/world/world",
|
||||
"/world/world/index.ui",
|
||||
"/world/world/get_world.dspy",
|
||||
"/world/world/add_world.dspy",
|
||||
"/world/world/update_world.dspy",
|
||||
"/world/world/delete_world.dspy",
|
||||
"/world/api/list_worlds.dspy",
|
||||
"/world/api/create_world.dspy",
|
||||
"/world/api/world_update.dspy",
|
||||
"/world/api/world_delete.dspy",
|
||||
"/world/api/get_search_world_type.dspy",
|
||||
"/world/api/get_search_status.dspy",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
sage_root = find_sage_root()
|
||||
if not sage_root:
|
||||
print('ERROR: sage root not found; register paths manually in central load_path.py')
|
||||
sys.exit(1)
|
||||
sys.path.insert(0, os.path.join(sage_root, 'py3', 'bin'))
|
||||
if SAGE_ROOT is None:
|
||||
print("Sage root not found, skip RBAC registration.")
|
||||
return
|
||||
sys.path.insert(0, SAGE_ROOT)
|
||||
try:
|
||||
from load_path import register_paths # noqa
|
||||
from set_role_perm import set_role_perm
|
||||
except ImportError:
|
||||
# 中央 load_path.py 未提供可导入函数时,输出待注册清单供人工接入
|
||||
print('INFO: central load_path.py has no importable register_paths; '
|
||||
'falling back to central registration file (see sage/load_path.py)')
|
||||
register_paths = None
|
||||
if register_paths is not None:
|
||||
for p in PATHS_ANY:
|
||||
register_paths(p, 'any')
|
||||
for p in PATHS_LOGINED:
|
||||
register_paths(p, 'logined')
|
||||
print('world RBAC paths registered: %d logined, %d any'
|
||||
% (len(PATHS_LOGINED), len(PATHS_ANY)))
|
||||
else:
|
||||
print('world RBAC paths (register in sage/load_path.py):')
|
||||
for p in PATHS_LOGINED:
|
||||
print(' %s logined' % p)
|
||||
for p in PATHS_ANY:
|
||||
print(' %s any' % p)
|
||||
print("set_role_perm not found, skip RBAC registration.")
|
||||
return
|
||||
for p in PATHS_ANY:
|
||||
set_role_perm(p, "any")
|
||||
for p in PATHS_LOGINED:
|
||||
set_role_perm(p, "logined")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -1,51 +1,54 @@
|
||||
---
|
||||
name: world
|
||||
description: 世界定义与管理模块(W-01 世界管理)——world 表 CRUD、列表查询(sqlPaging+复合索引)、创建自动编码、mode 白名单硬校验与流转规则校验、world_mode 编码字典幂等落库。通过 load_world() 挂载。
|
||||
description: world 模块技能文档——世界定义与管理(根模块,平台入口域)。提供 world 表 CRUD、列表查询接口、创建自动编码,通过 load_world() 挂载。
|
||||
---
|
||||
|
||||
# world 模块技能文档
|
||||
# world 模块
|
||||
|
||||
## 概述
|
||||
世界定义与管理(平台入口域根模块)。提供 world 表 CRUD、分页列表查询、创建自动编码、
|
||||
世界模式(mode)白名单硬校验与流转规则校验。依赖 sqlor / ahserver / appbase(编码字典 appcodes/appcodes_kv)。
|
||||
世界定义与管理,是平台的根模块/入口域,无业务依赖,最先开发。依赖 appbase(appcodes 字典)、rbac(权限)。
|
||||
|
||||
## 数据模型
|
||||
表 `world`(models/world.json 四段式 summary/fields/indexes/codes):
|
||||
- id VARCHAR(32) PK(str 32,getID() 生成,禁 uuid)
|
||||
- code VARCHAR(64) 唯一(唯一索引 idx_world_code + 应用层前置校验 + 冲突重试)
|
||||
- name VARCHAR(255) NOT NULL
|
||||
- description TEXT
|
||||
- mode VARCHAR(32) NOT NULL DEFAULT 'draft'(白名单 draft/active/paused/archived)
|
||||
- world_type VARCHAR(32)(world_type 编码字典)
|
||||
- owner_id / org_id VARCHAR(32)
|
||||
- created_at / updated_at TIMESTAMP
|
||||
- 复合索引 idx_world_list(org_id, mode, created_at) 支撑列表查询
|
||||
|
||||
编码字典:init/data.json 幂等落库 `world_mode`(draft=草稿/active=运行中/paused=已暂停/archived=已归档)
|
||||
与 `world_type`(game/simulation/education/business/social),appcodes + appcodes_kv 两表同插。
|
||||
### 表 `world`(models/world.json)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | str(32) | 主键 |
|
||||
| name | str(255) | 世界名称 |
|
||||
| code | str(64) | 世界编码(唯一) |
|
||||
| description | text | 描述 |
|
||||
| world_type | str(16) | 世界类型(appcodes world_type,默认 '0') |
|
||||
| status | str(16) | 状态(appcodes world_status,默认 '0') |
|
||||
| config_json | text | 世界配置 JSON |
|
||||
| created_at | timestamp | 创建时间 |
|
||||
| updated_at | timestamp | 更新时间 |
|
||||
|
||||
## 关键接口(经 load_world() 注册到 ServerEnv)
|
||||
- `create_world(params)` → 自动编码 W+时间戳+随机4位;mode 默认 draft;非法 mode/流转 100% 拦截不落库;编码唯一冲突重试 3 次
|
||||
- `update_world(params)` → 部分更新;mode 变更走流转校验;code 变更走全局唯一校验
|
||||
- `delete_world(params)` / `get_world(params)`
|
||||
- `list_worlds(params)` → 分页 {list, total, page, page_size};count 与数据查询分离 + 显式列清单(排除 TEXT),P95 达标
|
||||
- `set_world_mode(params)` → 模式切换严格校验(draft→active⇄paused、active/paused→archived、archived→active)
|
||||
- 主键:`["id"]`
|
||||
- 唯一索引:`idx_world_code(code)`
|
||||
- 字典:world_type → appcodes_kv(parentid='world_type')、status → appcodes_kv(parentid='world_status')
|
||||
|
||||
REST 前缀 `/api/*`(wwwroot/api/*.dspy):
|
||||
- create_world.dspy / world_update.dspy / world_delete.dspy / get_world.dspy / list_worlds.dspy / set_world_mode.dspy
|
||||
- get_search_status.dspy / get_search_world_type.dspy(下拉数据源,返回 [{value,text}],首项"全部")
|
||||
## 关键接口
|
||||
|
||||
统一错误结构:`{code, message, field, detail}`;分页结构:`{list, total}`。
|
||||
### ServerEnv 注册函数(load_world() 挂载)
|
||||
- `list_worlds(params)` → `[{value:id, text:name}]`
|
||||
- `get_world({id})` → 单条
|
||||
- `create_world(ns)` → 自动生成 id/code、写 created_at
|
||||
- `update_world(ns)` → 写 updated_at、剔除 `_text` 后缀
|
||||
- `delete_world({id})`
|
||||
|
||||
### wwwroot/api/*.dspy
|
||||
- `list_worlds.dspy`:世界列表(下拉/菜单)
|
||||
- `create_world.dspy`:创建
|
||||
- `world_update.dspy`:更新
|
||||
- `world_delete.dspy`:删除
|
||||
- `get_search_world_type.dspy` / `get_search_status.dspy`:字典下拉
|
||||
|
||||
## 陷阱
|
||||
- mode 硬校验:非法枚举一律返回 INVALID_MODE,绝不落库;流转规则见 MODE_TRANSITIONS
|
||||
- 编码唯一:唯一索引兜底 + 前置校验 + Duplicate key 冲突重试(自动编码场景)
|
||||
- 列表查询 LIMIT/OFFSET 用整数内联(sqlPaging ns.page/ns.rows),禁止 `${param}$` 生成引号字符串
|
||||
- 所有 .dspy 无 import(json/exception/format_exc/get_module_dbname/DBPools 为预载全局)
|
||||
- 取库名用 get_module_dbname('world'),禁止硬编码 DBNAME
|
||||
- 根目录不放 __init__.py;包目录为 world/world/;三处同步注册(world.py / __init__.py / init.py)
|
||||
- 库名禁止硬编码,统一 `ServerEnv().get_module_dbname("world")`(.py)/ `get_module_dbname("world")`(.dspy)
|
||||
- dspy 无 import(json/get_sor_context/debug 均为预加载全局)
|
||||
- `create_world` 必须设置 `created_at = curDateString()`,否则 sor.C 静默丢记录
|
||||
- `update_world` 需剔除 Tabular 传来的 `_text` 后缀字段
|
||||
- CRUD json 的 `new_data_url` 指向自定义 `api/create_world.dspy`
|
||||
|
||||
## 依赖
|
||||
- sqlor(DBPools/sqlExe/sor.C/U/D/R/sqlPaging)
|
||||
- ahserver(ServerEnv、get_module_dbname)
|
||||
- appbase(appcodes/appcodes_kv 编码字典)
|
||||
- appbase(appcodes 字典)、rbac(权限)
|
||||
|
||||
@ -1,29 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world 世界定义与管理模块(W-01 世界管理)。
|
||||
|
||||
包目录 world/world/(模块根目录不放 __init__.py,规范要求)。
|
||||
通过 load_world() 挂载到宿主应用(ServerEnv 注册业务函数)。
|
||||
"""
|
||||
from .init import load_world
|
||||
from .world import (
|
||||
"""world 模块包入口。"""
|
||||
from .init import (
|
||||
load_world,
|
||||
list_worlds,
|
||||
get_world,
|
||||
create_world,
|
||||
update_world,
|
||||
delete_world,
|
||||
get_world,
|
||||
list_worlds,
|
||||
set_world_mode,
|
||||
get_world_mode_options,
|
||||
get_world_type_options,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'load_world',
|
||||
'create_world',
|
||||
'update_world',
|
||||
'delete_world',
|
||||
'get_world',
|
||||
'list_worlds',
|
||||
'set_world_mode',
|
||||
'get_world_mode_options',
|
||||
'get_world_type_options',
|
||||
"load_world",
|
||||
"list_worlds",
|
||||
"get_world",
|
||||
"create_world",
|
||||
"update_world",
|
||||
"delete_world",
|
||||
]
|
||||
|
||||
100
world/init.py
100
world/init.py
@ -1,37 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world 模块初始化:注册全部业务函数到 ServerEnv(load_world 为唯一集成点)。
|
||||
"""world 模块初始化与业务函数。
|
||||
|
||||
三处同步注册约定:实现(world/world.py)→ 导出(world/__init__.py)→ 本文件 env.xxx = xxx。
|
||||
新增/删除函数必须三处同步更新。
|
||||
通过 load_world() 将函数注册到 ServerEnv,供 .ui/.dspy 直接调用。
|
||||
库名统一从宿主应用获取,禁止硬编码 DBNAME。
|
||||
"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
from .world import (
|
||||
create_world,
|
||||
update_world,
|
||||
delete_world,
|
||||
get_world,
|
||||
list_worlds,
|
||||
set_world_mode,
|
||||
get_world_mode_options,
|
||||
get_world_type_options,
|
||||
)
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.timeUtils import curDateString, timestampstr
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
|
||||
def load_world():
|
||||
"""注册 world 模块全部业务函数到 ServerEnv,供 .dspy/.ui 直接调用。"""
|
||||
env = ServerEnv()
|
||||
# 单数/复数双注册(CRUD 框架约定:dspy 包装器用复数表名)
|
||||
def _get_dbname():
|
||||
"""取库名(禁止硬编码 DBNAME)。"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
return ServerEnv().get_module_dbname("world")
|
||||
|
||||
|
||||
def _gen_code():
|
||||
"""自动生成世界编码。"""
|
||||
return "W" + getID()[:10]
|
||||
|
||||
|
||||
async def list_worlds(params_kw=None):
|
||||
"""世界列表查询(供下拉/菜单)。返回 [{value:id, text:name}]。"""
|
||||
params = params_kw or {}
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("world", params)
|
||||
return [{"value": r.id, "text": r.name} for r in recs]
|
||||
|
||||
|
||||
async def get_world(params_kw=None):
|
||||
"""按 id 查询世界。"""
|
||||
params = params_kw or {}
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
return await sor.R("world", {"id": params.get("id")})
|
||||
|
||||
|
||||
async def create_world(params_kw=None):
|
||||
"""创建世界(自动 id/code/created_at)。"""
|
||||
ns = dict(params_kw or {})
|
||||
ns["id"] = ns.get("id") or getID()
|
||||
ns["code"] = ns.get("code") or _gen_code()
|
||||
ns["created_at"] = ns.get("created_at") or curDateString()
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.C("world", ns)
|
||||
return {"success": True, "id": ns["id"]}
|
||||
|
||||
|
||||
async def update_world(params_kw=None):
|
||||
"""更新世界(写 updated_at)。"""
|
||||
ns = dict(params_kw or {})
|
||||
for k in list(ns.keys()):
|
||||
if k.endswith("_text"):
|
||||
ns.pop(k, None)
|
||||
ns["updated_at"] = curDateString()
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.U("world", ns)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
async def delete_world(params_kw=None):
|
||||
"""删除世界。"""
|
||||
params = params_kw or {}
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.D("world", {"id": params.get("id")})
|
||||
return {"success": True}
|
||||
|
||||
|
||||
def load_world(env=None):
|
||||
"""挂载 world 模块函数到 ServerEnv。"""
|
||||
if env is None:
|
||||
from ahserver.serverenv import ServerEnv
|
||||
env = ServerEnv()
|
||||
env.list_worlds = list_worlds
|
||||
env.get_world = get_world
|
||||
env.create_world = create_world
|
||||
env.create_worlds = create_world
|
||||
env.update_world = update_world
|
||||
env.update_worlds = update_world
|
||||
env.delete_world = delete_world
|
||||
env.delete_worlds = delete_world
|
||||
env.get_world = get_world
|
||||
env.get_worlds = get_world
|
||||
env.list_worlds = list_worlds
|
||||
env.set_world_mode = set_world_mode
|
||||
env.get_world_mode_options = get_world_mode_options
|
||||
env.get_world_type_options = get_world_type_options
|
||||
return env
|
||||
|
||||
339
world/world.py
339
world/world.py
@ -1,339 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""W-01 世界管理核心业务逻辑(world 模块)。
|
||||
|
||||
提供 world 表 CRUD、分页列表查询(sqlPaging + 复合索引)、创建自动编码、
|
||||
mode 白名单硬校验与流转规则校验、编码唯一冲突重试、编码字典下拉数据源。
|
||||
|
||||
统一错误结构:{code, message, field, detail}(WorldError)。
|
||||
分页结构:{list, total, page, page_size}。
|
||||
|
||||
依赖:sqlor(DBPools)、ahserver(ServerEnv)、appbase(appcodes/appcodes_kv 编码字典)。
|
||||
"""
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
try:
|
||||
from sqlor import DBPools
|
||||
except ImportError:
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.timeUtils import curDateString
|
||||
|
||||
MODE_WHITELIST = ('draft', 'active', 'paused', 'archived')
|
||||
WORLD_TYPE_WHITELIST = ('game', 'simulation', 'education', 'business', 'social')
|
||||
|
||||
# 模式流转规则:draft→active⇄paused、active/paused→archived、archived→active
|
||||
MODE_TRANSITIONS = {
|
||||
'draft': ('active',),
|
||||
'active': ('paused', 'archived'),
|
||||
'paused': ('active', 'archived'),
|
||||
'archived': ('active',),
|
||||
}
|
||||
|
||||
CODE_RETRY = 3 # 自动编码唯一冲突重试次数
|
||||
LIST_SORTS = ('created_at', 'code', 'name', 'mode') # 列表排序白名单,防注入
|
||||
LIST_PAGE_SIZE_MAX = 200
|
||||
|
||||
|
||||
class WorldError(Exception):
|
||||
"""业务异常,携带统一错误结构 {code, message, field, detail}。"""
|
||||
|
||||
def __init__(self, code='INTERNAL_ERROR', message='', field='', detail=''):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.field = field
|
||||
self.detail = detail
|
||||
|
||||
|
||||
def _get_dbname():
|
||||
"""取 world 模块库名,禁止硬编码 DBNAME。"""
|
||||
return ServerEnv().get_module_dbname('world')
|
||||
|
||||
|
||||
def _gen_world_code():
|
||||
"""自动编码:W + 时间戳(YYYYMMDDHHMMSS) + 随机4位数字。全局唯一由唯一索引+重试兜底。"""
|
||||
ts = datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
suffix = ''.join(random.choice('0123456789') for _ in range(4))
|
||||
return 'W' + ts + suffix
|
||||
|
||||
|
||||
def _check_mode(mode):
|
||||
if mode not in MODE_WHITELIST:
|
||||
raise WorldError('INVALID_MODE', 'invalid world mode: ' + str(mode), 'mode',
|
||||
'mode must be one of: ' + ', '.join(MODE_WHITELIST))
|
||||
|
||||
|
||||
def _check_world_type(world_type):
|
||||
if world_type and world_type not in WORLD_TYPE_WHITELIST:
|
||||
raise WorldError('INVALID_WORLD_TYPE', 'invalid world_type: ' + str(world_type),
|
||||
'world_type',
|
||||
'world_type must be one of: ' + ', '.join(WORLD_TYPE_WHITELIST))
|
||||
|
||||
|
||||
def _check_transition(old_mode, new_mode):
|
||||
_check_mode(old_mode)
|
||||
_check_mode(new_mode)
|
||||
if new_mode not in MODE_TRANSITIONS.get(old_mode, ()):
|
||||
raise WorldError('INVALID_MODE_TRANSITION',
|
||||
'mode transition not allowed: ' + old_mode + ' -> ' + new_mode,
|
||||
'mode',
|
||||
'allowed transitions: draft->active, active<->paused, '
|
||||
'active/paused->archived, archived->active')
|
||||
|
||||
|
||||
def _to_int(val, default, minimum=1):
|
||||
try:
|
||||
n = int(val)
|
||||
except (TypeError, ValueError):
|
||||
n = default
|
||||
return n if n >= minimum else minimum
|
||||
|
||||
|
||||
def _row_to_dict(r):
|
||||
"""查询行(DictObject)转 dict,显式列清单(description 为 TEXT,列表查询不取)。"""
|
||||
return {
|
||||
'id': r.id,
|
||||
'code': r.code,
|
||||
'name': r.name,
|
||||
'description': getattr(r, 'description', '') or '',
|
||||
'mode': r.mode,
|
||||
'world_type': getattr(r, 'world_type', '') or '',
|
||||
'owner_id': getattr(r, 'owner_id', '') or '',
|
||||
'org_id': getattr(r, 'org_id', '') or '',
|
||||
'created_at': str(r.created_at),
|
||||
'updated_at': str(r.updated_at),
|
||||
}
|
||||
|
||||
|
||||
async def create_world(params):
|
||||
"""创建世界。
|
||||
|
||||
- 自动编码:W+时间戳+随机4位(可显式传入 code)
|
||||
- mode 默认 draft,非法 mode 100% 拦截不落库
|
||||
- 编码全局唯一:唯一索引兜底 + 冲突重试 3 次
|
||||
"""
|
||||
dbname = _get_dbname()
|
||||
name = (params.get('name') or '').strip()
|
||||
if not name:
|
||||
raise WorldError('INVALID_NAME', 'world name is required', 'name', '')
|
||||
mode = params.get('mode') or 'draft'
|
||||
_check_mode(mode)
|
||||
if mode != 'draft':
|
||||
raise WorldError('INVALID_MODE_TRANSITION', 'new world must start at draft', 'mode',
|
||||
'new world mode must be draft, then draft->active to activate')
|
||||
world_type = (params.get('world_type') or '').strip()
|
||||
_check_world_type(world_type)
|
||||
code = (params.get('code') or '').strip()
|
||||
org_id = params.get('org_id') or '0'
|
||||
owner_id = params.get('owner_id') or '0'
|
||||
description = params.get('description') or ''
|
||||
now = curDateString()
|
||||
ns = {
|
||||
'id': getID(),
|
||||
'code': code or _gen_world_code(),
|
||||
'name': name,
|
||||
'description': description,
|
||||
'mode': mode,
|
||||
'world_type': world_type,
|
||||
'owner_id': owner_id,
|
||||
'org_id': org_id,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
}
|
||||
last_err = None
|
||||
for attempt in range(CODE_RETRY):
|
||||
if attempt > 0:
|
||||
ns['code'] = _gen_world_code()
|
||||
try:
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.C('world', dict(ns))
|
||||
return dict(ns)
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if 'Duplicate' in msg or '1062' in msg:
|
||||
last_err = e
|
||||
continue # 唯一冲突 → 换码重试
|
||||
raise WorldError('DB_ERROR', 'create world failed', '', msg) from e
|
||||
raise WorldError('CODE_CONFLICT', 'world code conflict after retries', 'code',
|
||||
str(last_err) if last_err else '')
|
||||
|
||||
|
||||
async def update_world(params):
|
||||
"""部分更新。mode 变更走流转校验;code 变更走全局唯一校验。"""
|
||||
dbname = _get_dbname()
|
||||
wid = params.get('id') or params.get('world_id')
|
||||
if not wid:
|
||||
raise WorldError('INVALID_ID', 'world id is required', 'id', '')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.R('world', {'id': wid})
|
||||
if not rows:
|
||||
raise WorldError('WORLD_NOT_FOUND', 'world not found: ' + str(wid), 'id', '')
|
||||
old = rows[0]
|
||||
ns = {}
|
||||
if 'name' in params:
|
||||
name = (params.get('name') or '').strip()
|
||||
if not name:
|
||||
raise WorldError('INVALID_NAME', 'world name is required', 'name', '')
|
||||
ns['name'] = name
|
||||
if 'description' in params:
|
||||
ns['description'] = params.get('description') or ''
|
||||
if 'world_type' in params:
|
||||
wt = (params.get('world_type') or '').strip()
|
||||
_check_world_type(wt)
|
||||
ns['world_type'] = wt
|
||||
if 'code' in params:
|
||||
new_code = (params.get('code') or '').strip()
|
||||
if not new_code:
|
||||
raise WorldError('INVALID_CODE', 'world code is required', 'code', '')
|
||||
if new_code != old.code:
|
||||
dup = await sor.sqlExe('SELECT id FROM world WHERE code = ${code}$ AND id <> ${id}$',
|
||||
{'code': new_code, 'id': wid})
|
||||
if dup:
|
||||
raise WorldError('CODE_EXISTS', 'world code already exists: ' + new_code, 'code', '')
|
||||
ns['code'] = new_code
|
||||
if 'mode' in params:
|
||||
new_mode = params.get('mode')
|
||||
_check_transition(old.mode, new_mode)
|
||||
ns['mode'] = new_mode
|
||||
if not ns:
|
||||
return {'id': wid, 'updated': False}
|
||||
ns['id'] = wid
|
||||
ns['updated_at'] = curDateString()
|
||||
await sor.U('world', dict(ns))
|
||||
return {'id': wid, 'updated': True}
|
||||
|
||||
|
||||
async def delete_world(params):
|
||||
"""删除世界。"""
|
||||
dbname = _get_dbname()
|
||||
wid = params.get('id') or params.get('world_id')
|
||||
if not wid:
|
||||
raise WorldError('INVALID_ID', 'world id is required', 'id', '')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.D('world', {'id': wid})
|
||||
return {'id': wid, 'deleted': True}
|
||||
|
||||
|
||||
async def get_world(params):
|
||||
"""按 id(或 code)查询单个世界。"""
|
||||
dbname = _get_dbname()
|
||||
wid = params.get('id') or params.get('world_id') or params.get('code')
|
||||
if not wid:
|
||||
raise WorldError('INVALID_ID', 'world id or code is required', 'id', '')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.R('world', {'id': wid})
|
||||
if not rows:
|
||||
rows = await sor.R('world', {'code': wid})
|
||||
if not rows:
|
||||
raise WorldError('WORLD_NOT_FOUND', 'world not found', 'id', '')
|
||||
return _row_to_dict(rows[0])
|
||||
|
||||
|
||||
async def list_worlds(params):
|
||||
"""分页列表查询。
|
||||
|
||||
性能:count 与数据查询分离;显式列清单(排除 TEXT description);
|
||||
复合索引 idx_world_list(org_id, mode, created_at) 支撑过滤+排序。
|
||||
"""
|
||||
dbname = _get_dbname()
|
||||
page = _to_int(params.get('page'), 1, 1)
|
||||
page_size = _to_int(params.get('page_size') or params.get('rows'), 20, 1)
|
||||
if page_size > LIST_PAGE_SIZE_MAX:
|
||||
page_size = LIST_PAGE_SIZE_MAX
|
||||
org_id = params.get('org_id') or '0'
|
||||
mode = params.get('mode') or ''
|
||||
keyword = params.get('keyword') or ''
|
||||
|
||||
where = ['org_id = ${org_id}$']
|
||||
qargs = {'org_id': org_id}
|
||||
if mode:
|
||||
_check_mode(mode) # 非法 mode 100% 拦截
|
||||
where.append('mode = ${mode}$')
|
||||
qargs['mode'] = mode
|
||||
if keyword:
|
||||
where.append('(name LIKE ${keyword}$ OR code LIKE ${keyword}$)')
|
||||
qargs['keyword'] = '%' + keyword + '%'
|
||||
cond = ' AND '.join(where)
|
||||
|
||||
sort = params.get('sort') or 'created_at'
|
||||
if sort not in LIST_SORTS:
|
||||
sort = 'created_at'
|
||||
order = (params.get('order') or 'desc').lower()
|
||||
if order not in ('asc', 'desc'):
|
||||
order = 'desc'
|
||||
|
||||
# count 与数据查询分离(显式列清单,排除 TEXT)
|
||||
count_sql = 'SELECT COUNT(*) AS cnt FROM world WHERE ' + cond
|
||||
data_sql = ('SELECT id, code, name, mode, world_type, owner_id, org_id, created_at, updated_at '
|
||||
'FROM world WHERE ' + cond)
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
cnt_rows = await sor.sqlExe(count_sql, qargs)
|
||||
total = int(cnt_rows[0].cnt) if cnt_rows else 0
|
||||
ns = dict(qargs)
|
||||
ns.update({'page': page, 'rows': page_size, 'sort': sort, 'order': order})
|
||||
result = await sor.sqlPaging(data_sql, ns)
|
||||
items = [{
|
||||
'id': r.id,
|
||||
'code': r.code,
|
||||
'name': r.name,
|
||||
'mode': r.mode,
|
||||
'world_type': getattr(r, 'world_type', '') or '',
|
||||
'owner_id': getattr(r, 'owner_id', '') or '',
|
||||
'org_id': getattr(r, 'org_id', '') or '',
|
||||
'created_at': str(r.created_at),
|
||||
'updated_at': str(r.updated_at),
|
||||
} for r in result.rows]
|
||||
return {'list': items, 'total': total, 'page': page, 'page_size': page_size}
|
||||
|
||||
|
||||
async def set_world_mode(params):
|
||||
"""模式切换:严格校验白名单 + 流转规则(draft→active⇄paused、active/paused→archived、archived→active)。"""
|
||||
dbname = _get_dbname()
|
||||
wid = params.get('id') or params.get('world_id')
|
||||
new_mode = params.get('mode')
|
||||
if not wid:
|
||||
raise WorldError('INVALID_ID', 'world id is required', 'id', '')
|
||||
if not new_mode:
|
||||
raise WorldError('INVALID_MODE', 'mode is required', 'mode', '')
|
||||
_check_mode(new_mode)
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.R('world', {'id': wid})
|
||||
if not rows:
|
||||
raise WorldError('WORLD_NOT_FOUND', 'world not found: ' + str(wid), 'id', '')
|
||||
old_mode = rows[0].mode
|
||||
_check_transition(old_mode, new_mode)
|
||||
await sor.U('world', {'id': wid, 'mode': new_mode, 'updated_at': curDateString()})
|
||||
return {'id': wid, 'mode': new_mode}
|
||||
|
||||
|
||||
async def get_world_mode_options(prefix_all=True):
|
||||
"""世界模式下拉数据源 [{value, text}],数据来自编码字典 world_mode。"""
|
||||
return await _dict_options('world_mode', MODE_WHITELIST, prefix_all)
|
||||
|
||||
|
||||
async def get_world_type_options(prefix_all=True):
|
||||
"""世界类型下拉数据源 [{value, text}],数据来自编码字典 world_type。"""
|
||||
return await _dict_options('world_type', WORLD_TYPE_WHITELIST, prefix_all)
|
||||
|
||||
|
||||
async def _dict_options(parentid, fallback_keys, prefix_all):
|
||||
"""从 appbase 编码字典 appcodes_kv 读下拉选项;字典不可用时回退英文标签(不硬编码中文)。"""
|
||||
items = [{'value': '', 'text': 'All'}] if prefix_all else []
|
||||
try:
|
||||
dbname = ServerEnv().get_module_dbname('appbase')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe('SELECT k, v FROM appcodes_kv WHERE parentid = ${pid}$ ORDER BY id',
|
||||
{'pid': parentid})
|
||||
for r in rows:
|
||||
items.append({'value': r.k, 'text': r.v})
|
||||
except Exception:
|
||||
# 字典表未就绪时回退英文标签,保证下拉可用
|
||||
items = [{'value': '', 'text': 'All'}] if prefix_all else []
|
||||
for k in fallback_keys:
|
||||
items.append({'value': k, 'text': k})
|
||||
return items
|
||||
@ -1,15 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# create_world.dspy —— 创建世界(自动编码、mode 白名单硬校验、编码唯一冲突重试)
|
||||
# 预载全局:params_kw / json / format_exc / create_world / get_module_dbname
|
||||
debug('create_world.dspy: START params_kw=%s' % json.dumps(dict(params_kw), ensure_ascii=False))
|
||||
try:
|
||||
result = await create_world(dict(params_kw))
|
||||
return {"status": "ok", "data": result}
|
||||
except Exception as e:
|
||||
code = getattr(e, 'code', 'INTERNAL_ERROR')
|
||||
message = getattr(e, 'message', str(e)) or str(e)
|
||||
field = getattr(e, 'field', '')
|
||||
detail = getattr(e, 'detail', '') or ''
|
||||
debug('create_world.dspy: ERROR %s %s' % (code, message))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": code, "message": message, "field": field, "detail": detail}
|
||||
ns = dict(params_kw or {})
|
||||
for k in list(ns.keys()):
|
||||
if k.endswith("_text"):
|
||||
ns.pop(k, None)
|
||||
result = await create_world(ns)
|
||||
return {"success": True, "id": result.get("id")}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# get_search_status.dspy —— 世界模式下拉数据源 [{value, text}](首项"全部")
|
||||
result = [{"value": "", "text": "全部"}]
|
||||
try:
|
||||
items = await get_world_mode_options(True)
|
||||
return {"status": "ok", "data": items}
|
||||
async with get_sor_context(request._run_ns, "world") as sor:
|
||||
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='world_status' order by k", {})
|
||||
return [{"value": "", "text": "全部"}] + [{"value": r.value, "text": r.text} for r in rows]
|
||||
except Exception as e:
|
||||
debug('get_search_status.dspy: ERROR %s' % str(e))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": getattr(e, 'code', 'INTERNAL_ERROR'),
|
||||
"message": getattr(e, 'message', str(e)) or str(e),
|
||||
"field": getattr(e, 'field', ''), "detail": getattr(e, 'detail', '') or ''}
|
||||
debug(f"get_search_status error: {e}")
|
||||
return result
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# get_search_world_type.dspy —— 世界类型下拉数据源 [{value, text}](首项"全部")
|
||||
result = [{"value": "", "text": "全部"}]
|
||||
try:
|
||||
items = await get_world_type_options(True)
|
||||
return {"status": "ok", "data": items}
|
||||
async with get_sor_context(request._run_ns, "world") as sor:
|
||||
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='world_type' order by k", {})
|
||||
return [{"value": "", "text": "全部"}] + [{"value": r.value, "text": r.text} for r in rows]
|
||||
except Exception as e:
|
||||
debug('get_search_world_type.dspy: ERROR %s' % str(e))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": getattr(e, 'code', 'INTERNAL_ERROR'),
|
||||
"message": getattr(e, 'message', str(e)) or str(e),
|
||||
"field": getattr(e, 'field', ''), "detail": getattr(e, 'detail', '') or ''}
|
||||
debug(f"get_search_world_type error: {e}")
|
||||
return result
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# get_world.dspy —— 查询单个世界(id 或 code)
|
||||
debug('get_world.dspy: START params_kw=%s' % json.dumps(dict(params_kw), ensure_ascii=False))
|
||||
try:
|
||||
result = await get_world(dict(params_kw))
|
||||
return {"status": "ok", "data": result}
|
||||
except Exception as e:
|
||||
code = getattr(e, 'code', 'INTERNAL_ERROR')
|
||||
message = getattr(e, 'message', str(e)) or str(e)
|
||||
field = getattr(e, 'field', '')
|
||||
detail = getattr(e, 'detail', '') or ''
|
||||
debug('get_world.dspy: ERROR %s %s' % (code, message))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": code, "message": message, "field": field, "detail": detail}
|
||||
@ -1,15 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# list_worlds.dspy —— 分页列表查询(sqlPaging + 复合索引 idx_world_list)
|
||||
# 分页结构:{list, total, page, page_size}
|
||||
debug('list_worlds.dspy: START params_kw=%s' % json.dumps(dict(params_kw), ensure_ascii=False))
|
||||
try:
|
||||
result = await list_worlds(dict(params_kw))
|
||||
return {"status": "ok", "data": result}
|
||||
except Exception as e:
|
||||
code = getattr(e, 'code', 'INTERNAL_ERROR')
|
||||
message = getattr(e, 'message', str(e)) or str(e)
|
||||
field = getattr(e, 'field', '')
|
||||
detail = getattr(e, 'detail', '') or ''
|
||||
debug('list_worlds.dspy: ERROR %s %s' % (code, message))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": code, "message": message, "field": field, "detail": detail}
|
||||
worlds = await list_worlds({})
|
||||
return {"status": 0, "data": worlds}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# set_world_mode.dspy —— 模式切换(严格校验白名单 + 流转规则)
|
||||
# 流转规则:draft→active⇄paused、active/paused→archived、archived→active
|
||||
debug('set_world_mode.dspy: START params_kw=%s' % json.dumps(dict(params_kw), ensure_ascii=False))
|
||||
try:
|
||||
result = await set_world_mode(dict(params_kw))
|
||||
return {"status": "ok", "data": result}
|
||||
except Exception as e:
|
||||
code = getattr(e, 'code', 'INTERNAL_ERROR')
|
||||
message = getattr(e, 'message', str(e)) or str(e)
|
||||
field = getattr(e, 'field', '')
|
||||
detail = getattr(e, 'detail', '') or ''
|
||||
debug('set_world_mode.dspy: ERROR %s %s' % (code, message))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": code, "message": message, "field": field, "detail": detail}
|
||||
@ -1,7 +0,0 @@
|
||||
try:
|
||||
result = await create_world(params_kw)
|
||||
return {"status": "success", "data": result, "message": "创建成功"}
|
||||
except WorldError as e:
|
||||
return {"status": "error", "code": e.code, "message": e.message, "field": e.field, "detail": e.detail}
|
||||
except Exception as e:
|
||||
return {"status": "error", "code": "E_INTERNAL", "message": "服务器内部错误", "field": "", "detail": str(e)}
|
||||
@ -1,14 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# world_delete.dspy —— 删除世界
|
||||
debug('world_delete.dspy: START params_kw=%s' % json.dumps(dict(params_kw), ensure_ascii=False))
|
||||
try:
|
||||
result = await delete_world(dict(params_kw))
|
||||
return {"status": "ok", "data": result}
|
||||
except Exception as e:
|
||||
code = getattr(e, 'code', 'INTERNAL_ERROR')
|
||||
message = getattr(e, 'message', str(e)) or str(e)
|
||||
field = getattr(e, 'field', '')
|
||||
detail = getattr(e, 'detail', '') or ''
|
||||
debug('world_delete.dspy: ERROR %s %s' % (code, message))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": code, "message": message, "field": field, "detail": detail}
|
||||
ns = dict(params_kw or {})
|
||||
await delete_world(ns)
|
||||
return {"success": True}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
try:
|
||||
result = await get_world(params_kw)
|
||||
return {"status": "success", "data": result}
|
||||
except WorldError as e:
|
||||
return {"status": "error", "code": e.code, "message": e.message, "field": e.field, "detail": e.detail}
|
||||
except Exception as e:
|
||||
return {"status": "error", "code": "E_INTERNAL", "message": "服务器内部错误", "field": "", "detail": str(e)}
|
||||
@ -1,7 +0,0 @@
|
||||
try:
|
||||
result = await list_worlds(params_kw)
|
||||
return {"status": "success", "data": {"list": result["list"], "total": result["total"]}, "total": result["total"]}
|
||||
except WorldError as e:
|
||||
return {"status": "error", "code": e.code, "message": e.message, "field": e.field, "detail": e.detail}
|
||||
except Exception as e:
|
||||
return {"status": "error", "code": "E_INTERNAL", "message": "服务器内部错误", "field": "", "detail": str(e)}
|
||||
@ -1,7 +0,0 @@
|
||||
try:
|
||||
result = await switch_world_mode(params_kw)
|
||||
return {"status": "success", "data": result, "message": "模式切换成功"}
|
||||
except WorldError as e:
|
||||
return {"status": "error", "code": e.code, "message": e.message, "field": e.field, "detail": e.detail}
|
||||
except Exception as e:
|
||||
return {"status": "error", "code": "E_INTERNAL", "message": "服务器内部错误", "field": "", "detail": str(e)}
|
||||
@ -1,7 +0,0 @@
|
||||
try:
|
||||
result = await get_world_modes(params_kw)
|
||||
return {"status": "success", "data": result}
|
||||
except WorldError as e:
|
||||
return {"status": "error", "code": e.code, "message": e.message, "field": e.field, "detail": e.detail}
|
||||
except Exception as e:
|
||||
return {"status": "error", "code": "E_INTERNAL", "message": "服务器内部错误", "field": "", "detail": str(e)}
|
||||
@ -1,14 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# world_update.dspy —— 更新世界(部分更新;mode 变更走流转校验;code 变更走全局唯一校验)
|
||||
debug('world_update.dspy: START params_kw=%s' % json.dumps(dict(params_kw), ensure_ascii=False))
|
||||
try:
|
||||
result = await update_world(dict(params_kw))
|
||||
return {"status": "ok", "data": result}
|
||||
except Exception as e:
|
||||
code = getattr(e, 'code', 'INTERNAL_ERROR')
|
||||
message = getattr(e, 'message', str(e)) or str(e)
|
||||
field = getattr(e, 'field', '')
|
||||
detail = getattr(e, 'detail', '') or ''
|
||||
debug('world_update.dspy: ERROR %s %s' % (code, message))
|
||||
exception(format_exc())
|
||||
return {"status": "error", "code": code, "message": message, "field": field, "detail": detail}
|
||||
ns = dict(params_kw or {})
|
||||
for k in list(ns.keys()):
|
||||
if k.endswith("_text"):
|
||||
ns.pop(k, None)
|
||||
await update_world(ns)
|
||||
return {"success": True}
|
||||
|
||||
105
wwwroot/index.ui
105
wwwroot/index.ui
@ -1,91 +1,18 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "世界管理",
|
||||
"fontSize": "24px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "250px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.world_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/world/world_list')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "世界列表"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.world_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/world/api/set_world_mode.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "模式切换"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "app.world_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px"
|
||||
}
|
||||
}
|
||||
]
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "世界管理", "fontSize": "24px"}},
|
||||
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [
|
||||
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.world_content",
|
||||
"options": {"url": "{{entire_url('/world/world')}}"}, "mode": "replace"}],
|
||||
"subwidgets": [{"widgettype": "Text", "options": {"label": "世界列表"}}]},
|
||||
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.world_content",
|
||||
"options": {"url": "{{entire_url('/world/api/create_world.dspy')}}"}, "mode": "replace"}],
|
||||
"subwidgets": [{"widgettype": "Text", "options": {"label": "新建世界"}}]}
|
||||
]},
|
||||
{"widgettype": "VBox", "id": "app.world_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "10px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "世界管理",
|
||||
"fontSize": "18px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Menu",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"items": [
|
||||
{
|
||||
"name": "world_list",
|
||||
"label": "世界列表",
|
||||
"url": "{{entire_url('/world/world_list')}}"
|
||||
},
|
||||
{
|
||||
"name": "world_create",
|
||||
"label": "创建世界",
|
||||
"url": "{{entire_url('/world/api/create_world.dspy')}}"
|
||||
},
|
||||
{
|
||||
"name": "world_mode",
|
||||
"label": "模式切换",
|
||||
"url": "{{entire_url('/world/api/set_world_mode.dspy')}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user