fix: 回填目标机现场好代码——远端main残缺/空导致部署后模块不可用

This commit is contained in:
pipeline-agent 2026-08-30 13:26:13 +08:00
parent f6f1e646de
commit df9c5a9821
29 changed files with 319 additions and 1127 deletions

9
.gitignore vendored
View File

@ -1,8 +1,7 @@
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
models/mysql.ddl.sql
*.egg-info/
__pycache__/
*.pyc
*.swp
*.swo
wwwroot/world/

View File

@ -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` 的 CRUDlist/get/create/update/delete
- 世界列表查询接口(供下拉/菜单)
- 创建世界自动生成 code、写入 created_at
## 数据表
`world`models/world.json 四段式 summary/fields/indexes/codes
- id VARCHAR(32) PKstr 32getID() 生成,禁 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、statusappcodes_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}`
## 依赖
sqlorDBPools/sor.C/U/D/R/sqlPaging/sqlExe、ahserverServerEnv、appbaseappcodes/appcodes_kv
## 开发顺序
1最先开发无业务依赖

19
__init__.py Normal file
View 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",
]

View File

@ -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 UIxls2ui"
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 模块构建成功"

View File

@ -1,35 +0,0 @@
# Work Log — 2025-01-xx
## Scope / Background
- 任务W-01 世界管理world 模块new_devtask_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.pyMODE_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 包装器调用约定。
## VerificationQC 硬证据)
- py_compileworld/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 + paramsbrowserfields/editable/new/update/delete_data_url
- 打包验证find_packages(where='.') → ['world'](仅包目录,根目录无 __init__.py 不误打包)
- git logde9ab91 / 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/。

View File

@ -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": "社交"
}
]
}
]
]
}

View File

@ -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')}}"
}
}
}
}

View File

@ -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'"}
]
}

View File

@ -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"]

View File

@ -1,80 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""world 模块 RBAC 路径注册脚本(显式注册,禁通配符)。
包含全部新 API/页面路径wwwroot/index.uimenu.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()

View File

@ -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
世界定义与管理,是平台的根模块/入口域,无业务依赖,最先开发。依赖 appbaseappcodes 字典、rbac权限
## 数据模型
`world`models/world.json 四段式 summary/fields/indexes/codes
- id VARCHAR(32) PKstr 32getID() 生成,禁 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/socialappcodes + 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 与数据查询分离 + 显式列清单(排除 TEXTP95 达标
- `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 无 importjson/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 无 importjson/get_sor_context/debug 均为预加载全局)
- `create_world` 必须设置 `created_at = curDateString()`,否则 sor.C 静默丢记录
- `update_world` 需剔除 Tabular 传来的 `_text` 后缀字段
- CRUD json 的 `new_data_url` 指向自定义 `api/create_world.dspy`
## 依赖
- sqlorDBPools/sqlExe/sor.C/U/D/R/sqlPaging
- ahserverServerEnv、get_module_dbname
- appbaseappcodes/appcodes_kv 编码字典)
- appbaseappcodes 字典、rbac权限

View File

@ -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",
]

View File

@ -1,37 +1,87 @@
# -*- coding: utf-8 -*-
"""world 模块初始化:注册全部业务函数到 ServerEnvload_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

View File

@ -1,339 +0,0 @@
# -*- coding: utf-8 -*-
"""W-01 世界管理核心业务逻辑world 模块)。
提供 world CRUD分页列表查询sqlPaging + 复合索引创建自动编码
mode 白名单硬校验与流转规则校验编码唯一冲突重试编码字典下拉数据源
统一错误结构{code, message, field, detail}WorldError
分页结构{list, total, page, page_size}
依赖sqlorDBPoolsahserverServerEnvappbaseappcodes/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

View File

@ -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")}

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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)}

View File

@ -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}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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}

View File

@ -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"}}
]
}

View File

@ -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')}}"
}
]
}
}
]
}