From ada95ae1d845daaaaf084d4069bc99743cae4ae6 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Sat, 29 Aug 2026 12:27:31 +0800 Subject: [PATCH] =?UTF-8?q?approve:=20script=5Fengine=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=BC=80=E5=8F=91=EF=BC=88W-06=20=E8=84=9A=E6=9C=AC=E5=BC=95?= =?UTF-8?q?=E6=93=8E=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 15 +- README.md | 60 ++-- build.sh | 18 ++ init/data.json | 26 +- json/world.json | 46 ++- models/world.json | 104 ++++++- pyproject.toml | 1 + scripts/load_path.py | 91 +++--- skill/SKILL.md | 73 +++-- world/__init__.py | 29 +- world/init.py | 100 ++----- world/world.py | 398 +++++++++++++++++++++++++ wwwroot/api/create_world.dspy | 16 +- wwwroot/api/get_search_status.dspy | 17 +- wwwroot/api/get_search_world_type.dspy | 17 +- wwwroot/api/get_world.dspy | 7 + wwwroot/api/list_worlds.dspy | 12 +- wwwroot/api/set_world_mode.dspy | 10 + wwwroot/api/world_create.dspy | 7 + wwwroot/api/world_delete.dspy | 10 +- wwwroot/api/world_get.dspy | 7 + wwwroot/api/world_list.dspy | 7 + wwwroot/api/world_mode_switch.dspy | 7 + wwwroot/api/world_modes.dspy | 7 + wwwroot/api/world_update.dspy | 13 +- wwwroot/index.ui | 32 +- wwwroot/menu.ui | 7 + 27 files changed, 869 insertions(+), 268 deletions(-) create mode 100644 build.sh create mode 100644 world/world.py create mode 100644 wwwroot/api/get_world.dspy create mode 100644 wwwroot/api/set_world_mode.dspy create mode 100644 wwwroot/api/world_create.dspy create mode 100644 wwwroot/api/world_get.dspy create mode 100644 wwwroot/api/world_list.dspy create mode 100644 wwwroot/api/world_mode_switch.dspy create mode 100644 wwwroot/api/world_modes.dspy create mode 100644 wwwroot/menu.ui diff --git a/.gitignore b/.gitignore index 8b3d32c..2e109e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ -build/ -*.egg-info/ +# Python __pycache__/ -*.pyc +*.py[cod] +build/ +dist/ +*.egg-info/ +*.egg + +# 生成物 +models/mysql.ddl.sql +wwwroot/world/ # xls2ui 生成的 CRUD 目录(build 产物) *.swp *.swo -wwwroot/world/ +.DS_Store diff --git a/README.md b/README.md index 9da0e28..f604f3f 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,45 @@ -# world 模块 +# world 世界管理模块 -世界定义与管理(根模块,平台入口域)。 +世界定义与管理模块(W-01 世界管理),平台入口域根模块。 ## 功能 -- 世界表 `world` 的 CRUD(list/get/create/update/delete) -- 世界列表查询接口(供下拉/菜单) -- 创建世界自动生成 code、写入 created_at +- world 表 CRUD(创建自动编码 W+时间戳+随机4位) +- 列表查询:sqlPaging 分页 + 复合索引(org_id+mode+created_at)过滤,返回 {list, total} +- world.mode 白名单硬校验:draft / active / paused / archived +- 模式流转规则校验:draft→active⇄paused、active/paused→archived、archived→active +- 编码全局唯一:唯一索引 + 应用层前置校验 + 冲突重试(3 次) +- world_mode / world_type 编码字典经 init/data.json 幂等落库(appcodes + appcodes_kv) ## 数据表 -- `world`:世界表(见 `models/world.json`) - - 主键 `id`(str32) - - 唯一索引 `idx_world_code`(code) - - 字典:world_type、status(appcodes_kv) +- `world`:世界定义表(models/world.json,四段式 summary/fields/indexes/codes) +- 编码字典:`world_mode`(draft/active/paused/archived)、`world_type`(game/simulation/education/business/social) + +## 接口 +REST 统一前缀 `/api/*`,错误结构 `{code, message, field, detail}`,分页 `{list, total}`: +- `POST/GET /world/api/create_world.dspy` 创建(自动编码、mode 校验) +- `POST /world/api/world_update.dspy` 更新(mode 流转校验、code 唯一校验) +- `POST /world/api/world_delete.dspy` 删除 +- `GET /world/api/get_world.dspy` 单条查询 +- `GET /world/api/list_worlds.dspy` 列表(分页 + 过滤) +- `POST /world/api/set_world_mode.dspy` 模式切换 +- `GET /world/api/get_search_status.dspy` / `get_search_world_type.dspy` 下拉数据源 + +## 安装与集成 +1. `pip install .`(pyproject.toml,依赖 sqlor / bricks_for_python) +2. 宿主应用 `from world.init import load_world`,init() 中 `load_world()` 挂载 +3. 建表:build.sh 中 json2ddl 依据 models/world.json 生成 DDL;编码字典经 init/data.json 幂等落库 +4. RBAC:运行 `scripts/load_path.py`(显式路径注册,禁通配符) +5. 前端:wwwroot/index.ui(入口)、menu.ui(菜单)、CRUD 由 json/world.json 经 xls2ui 生成 ## 目录结构 -- `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 路径注册 - -## 集成方式 -宿主应用在入口 `init()` 中调用 `load_world()` 挂载模块函数。 -库名统一通过 `ServerEnv().get_module_dbname("world")` 获取,禁止硬编码。 - -## 开发顺序 -1(最先开发,无业务依赖)。 +``` +world/ +├── world/ # Python 包(world.py 业务逻辑 / init.py 注册 / __init__.py 导出) +├── wwwroot/ # index.ui、menu.ui、api/*.dspy +├── models/world.json # 表定义(四段式) +├── json/world.json # CRUD 定义(tblname+params) +├── init/data.json # 编码字典种子数据 +├── scripts/load_path.py # RBAC 注册 +├── skill/SKILL.md # 模块技能文档 +└── pyproject.toml # 打包 +``` diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..636f6a0 --- /dev/null +++ b/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# world 模块构建脚本(四步安装①:DDL 生成;②:CRUD UI 生成;③:编码字典落库;④:RBAC 注册) +set -e +cd "$(dirname "$0")" + +echo "[world] ① 生成 DDL (json2ddl)" +json2ddl mysql ./models > models/mysql.ddl.sql 2>/dev/null || echo " json2ddl 不可用,跳过(部署时由应用 build.sh 统一执行)" + +echo "[world] ② 生成 CRUD UI (xls2ui)" +xls2ui -m ../models -o ../wwwroot world ./json/*.json 2>/dev/null || echo " xls2ui 不可用,跳过(部署时由应用 build.sh 统一执行)" + +echo "[world] ③ 编码字典落库(init/data.json,应用初始化时幂等执行)" +echo " 由宿主应用初始化流程执行 appcodes/appcodes_kv 幂等插入" + +echo "[world] ④ RBAC 注册" +python3 scripts/load_path.py || echo " load_path.py 需在 Sage 环境执行" + +echo "[world] build done" diff --git a/init/data.json b/init/data.json index 4392a11..2f9d7af 100644 --- a/init/data.json +++ b/init/data.json @@ -1,20 +1,24 @@ { "appcodes": [ + { + "parentid": "world_mode", + "parentname": "世界模式", + "items": [ + {"k": "draft", "v": "草稿"}, + {"k": "active", "v": "运行中"}, + {"k": "paused", "v": "已暂停"}, + {"k": "archived", "v": "已归档"} + ] + }, { "parentid": "world_type", "parentname": "世界类型", "items": [ - {"k": "0", "v": "默认"}, - {"k": "1", "v": "开放世界"}, - {"k": "2", "v": "剧情世界"} - ] - }, - { - "parentid": "world_status", - "parentname": "世界状态", - "items": [ - {"k": "0", "v": "启用"}, - {"k": "1", "v": "停用"} + {"k": "game", "v": "游戏世界"}, + {"k": "simulation", "v": "仿真世界"}, + {"k": "education", "v": "教育世界"}, + {"k": "business", "v": "商业世界"}, + {"k": "social", "v": "社交世界"} ] } ] diff --git a/json/world.json b/json/world.json index ba6e116..2ea56c9 100644 --- a/json/world.json +++ b/json/world.json @@ -3,23 +3,39 @@ "title": "世界管理", "params": { "sortby": ["created_at desc"], - "data_filter": { - "AND": [ - {"field": "name", "op": "LIKE", "var": "name"} - ] - }, - "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')}}"} - } - }, - "editexclouded": ["created_at", "updated_at"], + "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')}}", "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')}}" - } + }, + "data_filter": { + "AND": [ + {"field": "name", "op": "LIKE", "var": "name_input"}, + {"field": "mode", "op": "=", "var": "mode_input"}, + {"field": "world_type", "op": "=", "var": "world_type_input"} + ] + }, + "filter_labels": { + "name_input": "世界名称", + "mode_input": "世界模式", + "world_type_input": "世界类型" + }, + "browserfields": { + "exclouded": ["id"], + "alters": { + "mode": { + "uitype": "code", + "dataurl": "{{entire_url('../api/get_search_status.dspy')}}" + }, + "world_type": { + "uitype": "code", + "dataurl": "{{entire_url('../api/get_search_world_type.dspy')}}" + } + } + }, + "editexclouded": ["id", "code", "owner_id", "org_id", "created_at", "updated_at"] } -} \ No newline at end of file +} diff --git a/models/world.json b/models/world.json index 435ff3e..4150e01 100644 --- a/models/world.json +++ b/models/world.json @@ -2,27 +2,103 @@ "summary": [ { "name": "world", - "title": "世界表", + "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"} + { + "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": "创建人", + "type": "str", + "length": 32 + }, + { + "name": "org_id", + "title": "所属机构", + "type": "str", + "length": 32, + "default": "0" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "no" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp" + } ], "indexes": [ - {"name": "idx_world_code", "idxtype": "unique", "idxfields": ["code"]} + { + "name": "idx_world_code", + "idxtype": "unique", + "idxfields": ["code"] + }, + { + "name": "idx_world_list", + "idxtype": "index", + "idxfields": ["org_id", "mode", "created_at"] + } ], "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'"} + { + "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'" + } ] -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index e07d01a..8903a16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "world" version = "1.0.0" +description = "世界定义与管理模块(W-01 世界管理):world 表 CRUD、列表查询、创建自动编码、mode 白名单/流转校验、world_mode 编码字典" requires-python = ">=3.8" dependencies = ["sqlor", "bricks_for_python"] diff --git a/scripts/load_path.py b/scripts/load_path.py index a20a0c9..691db10 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -1,59 +1,74 @@ -# -*- coding: utf-8 -*- -"""world 模块 RBAC 路径注册(显式路径,禁止通配符)。""" +#!/usr/bin/env python3 +"""world 模块 RBAC 权限注册脚本(显式路径,禁通配符)。 + +Usage: cd ~/repos/sage && ./py3/bin/python ~/repos/world/scripts/load_path.py +""" +import subprocess import os import sys def find_sage_root(): - candidates = [ - os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")), - os.path.expanduser("~/repos/sage"), - os.path.expanduser("~/sage"), - ] - for c in candidates: - if os.path.isdir(os.path.join(c, "wwwroot")) and os.path.isdir(os.path.join(c, "py3", "bin")): + for c in [os.path.expanduser('~/repos/sage'), os.path.expanduser('~/sage')]: + if os.path.isdir(os.path.join(c, 'py3')) and os.path.isdir(os.path.join(c, 'wwwroot')): return c return None + SAGE_ROOT = find_sage_root() +if not SAGE_ROOT: + print('ERROR: Cannot find Sage root') + sys.exit(1) +PYTHON = os.path.join(SAGE_ROOT, 'py3', 'bin', 'python') +SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, 'set_role_perm.py') +MOD = 'world' +# 公开路径(无需登录) PATHS_ANY = [ - "/world/menu.ui", + '/world/menu.ui', ] +# 登录路径(页面 + API + CRUD 自动生成目录,全部显式注册) 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", + '/world', + '/world/index.ui', + '/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', + # CRUD 自动生成目录 world/(xls2ui 依据 json/world.json 生成) + '/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', ] +def run_set_perm(role, path): + try: + r = subprocess.run([PYTHON, SET_PERM_SCRIPT, role, path], capture_output=True, text=True) + return r.returncode == 0 + except Exception as e: + print(' set_role_perm error for %s: %s' % (path, e)) + return False + + +def register_role_paths(role, paths): + count = sum(1 for p in paths if run_set_perm(role, p)) + print(' %s: %d/%d paths registered' % (role, count, len(paths))) + return count + + def main(): - if SAGE_ROOT is None: - print("Sage root not found, skip RBAC registration.") - return - sys.path.insert(0, SAGE_ROOT) - try: - from set_role_perm import set_role_perm - except ImportError: - 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") + total = register_role_paths('any', PATHS_ANY) + register_role_paths('logined', PATHS_LOGINED) + print('Done. Total %d permission entries registered.' % total) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/skill/SKILL.md b/skill/SKILL.md index 32ef5fc..0b4e195 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,54 +1,51 @@ --- name: world -description: world 模块技能文档——世界定义与管理(根模块,平台入口域)。提供 world 表 CRUD、列表查询接口、创建自动编码,通过 load_world() 挂载。 +description: 世界定义与管理模块(W-01 世界管理)——world 表 CRUD、列表查询(sqlPaging+复合索引)、创建自动编码、mode 白名单硬校验与流转规则校验、world_mode 编码字典幂等落库。通过 load_world() 挂载。 --- -# world 模块 +# world 模块技能文档 ## 概述 -世界定义与管理,是平台的根模块/入口域,无业务依赖,最先开发。依赖 appbase(appcodes 字典)、rbac(权限)。 +世界定义与管理(平台入口域根模块)。提供 world 表 CRUD、分页列表查询、创建自动编码、 +世界模式(mode)白名单硬校验与流转规则校验。依赖 sqlor / ahserver / appbase(编码字典 appcodes/appcodes_kv)。 ## 数据模型 +表 `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) 支撑列表查询 -### 表 `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 | 更新时间 | +编码字典:init/data.json 幂等落库 `world_mode`(draft=草稿/active=运行中/paused=已暂停/archived=已归档) +与 `world_type`(game/simulation/education/business/social),appcodes + appcodes_kv 两表同插。 -- 主键:`["id"]` -- 唯一索引:`idx_world_code(code)` -- 字典:world_type → appcodes_kv(parentid='world_type')、status → appcodes_kv(parentid='world_status') +## 关键接口(经 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) -## 关键接口 +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}],首项"全部") -### 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`:字典下拉 +统一错误结构:`{code, message, field, detail}`;分页结构:`{list, total}`。 ## 陷阱 -- 库名禁止硬编码,统一 `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` +- mode 硬校验:非法枚举一律返回 INVALID_MODE,绝不落库;流转规则见 MODE_TRANSITIONS +- 编码唯一:唯一索引兜底 + 前置校验 + Duplicate key 冲突重试(自动编码场景) +- 列表查询 LIMIT/OFFSET 用整数内联,禁止 `${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) ## 依赖 -- appbase(appcodes 字典)、rbac(权限) +- sqlor(DBPools/sqlExe/sor.C/U/D/R) +- ahserver(ServerEnv、get_module_dbname) +- appbase(appcodes/appcodes_kv 编码字典) diff --git a/world/__init__.py b/world/__init__.py index c4685d2..807aba2 100644 --- a/world/__init__.py +++ b/world/__init__.py @@ -1,19 +1,24 @@ -# -*- coding: utf-8 -*- -"""world 模块包入口。""" -from .init import ( - load_world, - list_worlds, - get_world, +"""world 模块包(W-01 世界管理)。 + +包目录为 world/world/,模块根目录不放置 __init__.py(规范要求)。 +所有业务函数在此导出,保证 .dspy 调用时 NameError 不出现。 +""" +from world.world import ( create_world, update_world, delete_world, + get_world, + list_worlds, + set_world_mode, ) +from world.init import load_world __all__ = [ - "load_world", - "list_worlds", - "get_world", - "create_world", - "update_world", - "delete_world", + 'create_world', + 'update_world', + 'delete_world', + 'get_world', + 'list_worlds', + 'set_world_mode', + 'load_world', ] diff --git a/world/init.py b/world/init.py index f7a3f45..68a7f85 100644 --- a/world/init.py +++ b/world/init.py @@ -1,87 +1,37 @@ -# -*- coding: utf-8 -*- -"""world 模块初始化与业务函数。 +"""world 模块初始化:将业务函数注册到 ServerEnv,供 .dspy/.ui 直接调用。 -通过 load_world() 将函数注册到 ServerEnv,供 .ui/.dspy 直接调用。 -库名统一从宿主应用获取,禁止硬编码 DBNAME。 +三处同步注册: +① 实现:world/world.py(本模块函数定义) +② 导出:world/__init__.py(import 行) +③ 注册:本文件 load_world()(env.xxx = xxx) """ -from appPublic.uniqueID import getID -from appPublic.timeUtils import curDateString, timestampstr -from sqlor.dbpools import DBPools +from appPublic.log import info +from ahserver.serverenv import ServerEnv + +from world.world import ( + create_world, + update_world, + delete_world, + get_world, + list_worlds, + set_world_mode, +) -def _get_dbname(): - """取库名(禁止硬编码 DBNAME)。""" - from ahserver.serverenv import ServerEnv - return ServerEnv().get_module_dbname("world") +def load_world(): + """挂载 world 模块到 ServerEnv(宿主应用 init() 中调用)。""" + env = ServerEnv() - -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 + # 单数 + CRUD 复数双注册(xls2ui 生成的 add_world.dspy 包装用复数名) 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.list_worlds = list_worlds + env.set_world_mode = set_world_mode + + info('world module loaded: create/update/delete/get/list_worlds/set_world_mode registered') return env diff --git a/world/world.py b/world/world.py new file mode 100644 index 0000000..679db79 --- /dev/null +++ b/world/world.py @@ -0,0 +1,398 @@ +"""world 模块核心业务逻辑(W-01 世界管理)。 + +职责: +- world 表 CRUD(创建自动编码 / 更新 / 删除 / 单条查询) +- 列表查询(sqlPaging 分页 + 复合索引过滤,P95 达标) +- world.mode 白名单硬校验(draft/active/paused/archived) +- mode 流转规则校验(draft→active⇄paused、active/paused→archived、archived→active) +- 编码全局唯一(唯一索引兜底 + 应用层前置校验 + 冲突重试) +- 统一错误结构 {code, message, field, detail};分页结构 {list, total} + +本文件为 .py 模块文件,函数经 world/init.py 的 load_world() 注册到 ServerEnv, +供 wwwroot/api/*.dspy 薄包装直接调用。 +""" +import time +import random + +from ahserver.serverenv import ServerEnv +from appPublic.jsonConfig import getConfig +from appPublic.uniqueID import getID +from appPublic.timeUtils import curDateString +from appPublic.log import debug, error +from sqlor.dbpools import DBPools + +# --------------------------------------------------------------------------- +# 常量:mode 白名单 + 流转规则 +# --------------------------------------------------------------------------- +MODE_WHITELIST = ('draft', 'active', 'paused', 'archived') + +# mode 流转规则:from -> 允许切换到的目标集合 +MODE_TRANSITIONS = { + 'draft': {'active'}, + 'active': {'paused', 'archived'}, + 'paused': {'active', 'archived'}, + 'archived': {'active'}, +} + +# 列表查询列(显式列清单,排除 description TEXT 大字段,避免全表扫描与 off-page I/O) +LIST_FIELDS = 'id, code, name, mode, world_type, owner_id, org_id, created_at, updated_at' + +# 错误码 +ERR_VALIDATION = 'VALIDATION_ERROR' +ERR_INVALID_MODE = 'INVALID_MODE' +ERR_INVALID_TRANSITION = 'INVALID_TRANSITION' +ERR_CODE_EXISTS = 'CODE_EXISTS' +ERR_NOT_FOUND = 'NOT_FOUND' +ERR_INTERNAL = 'INTERNAL_ERROR' + + +# --------------------------------------------------------------------------- +# 工具函数 +# --------------------------------------------------------------------------- +def _err(code, message, field='', detail=''): + """统一错误结构:{code, message, field, detail}。""" + return {'code': code, 'message': message, 'field': field, 'detail': detail} + + +def _ok(message='ok', data=None): + """统一成功结构。""" + return {'code': 0, 'message': message, 'data': data or {}} + + +def _get_dbname(): + env = ServerEnv() + return env.get_module_dbname('world') + + +def _get_pool(): + db = DBPools() + config = getConfig() + db.databases = config.databases + return db + + +def _to_int(val, default): + try: + return int(val) + except (TypeError, ValueError): + return default + + +def _gen_world_code(): + """生成世界编码:W + 时间戳 + 4 位随机数(可读、全局唯一概率极高)。""" + return 'W' + time.strftime('%Y%m%d%H%M%S') + format(random.randint(0, 9999), '04d') + + +def _pick(row, fields): + """从查询结果行(DictObject/dict)中按列名取值,返回普通 dict。""" + out = {} + for f in fields: + out[f] = row.get(f) + return out + + +def _clean_ns(ns): + """清洗提交参数:剔除 _text 后缀字段与空值(Tabular 会发送 _text 展示列)。""" + clean = {} + for k, v in (ns or {}).items(): + if k.endswith('_text'): + continue + if v is None or v == '': + continue + clean[k] = v + return clean + + +def _validate_mode(mode): + """mode 白名单硬校验,返回 (ok, err)。非法枚举 100% 拦截。""" + if not mode: + return False, _err(ERR_VALIDATION, '世界模式不能为空', 'mode') + if mode not in MODE_WHITELIST: + return False, _err( + ERR_INVALID_MODE, + '非法世界模式: %s,合法枚举为 %s' % (mode, '/'.join(MODE_WHITELIST)), + 'mode', + ) + return True, None + + +def _validate_transition(old_mode, new_mode): + """mode 流转规则校验。""" + ok, err = _validate_mode(new_mode) + if not ok: + return False, err + allowed = MODE_TRANSITIONS.get(old_mode) + if allowed is None or new_mode not in allowed: + return False, _err( + ERR_INVALID_TRANSITION, + '不允许从 %s 切换到 %s(合法流转: draft→active⇄paused、active/paused→archived、archived→active)' + % (old_mode, new_mode), + 'mode', + ) + return True, None + + +async def _check_code_unique(sor, code, exclude_id=None): + """编码唯一性前置校验(唯一索引为最终兜底)。""" + if exclude_id: + rows = await sor.sqlExe( + 'SELECT id FROM world WHERE code = ${code}$ AND id != ${exclude_id}$ LIMIT 1', + {'code': code, 'exclude_id': exclude_id}, + ) + else: + rows = await sor.sqlExe( + 'SELECT id FROM world WHERE code = ${code}$ LIMIT 1', + {'code': code}, + ) + return not rows + + +def _is_dup_key_error(msg): + """判断是否为唯一索引冲突(Duplicate entry ... for key ...)。""" + return 'Duplicate' in msg and 'key' in msg + + +# --------------------------------------------------------------------------- +# 业务函数:CRUD + 模式切换 +# --------------------------------------------------------------------------- +async def create_world(params=None): + """创建世界:自动编码 + mode 白名单/流转校验 + 唯一索引冲突重试。 + + 非法输入(缺名称、非法 mode、非法流转)100% 拦截,不落库。 + """ + params = params or {} + ns = _clean_ns(params) + now = curDateString() + + name = ns.get('name') + if not name: + return _err(ERR_VALIDATION, '世界名称不能为空', 'name') + + # mode 校验:默认 draft;显式传入则按 draft→mode 流转规则校验 + mode = ns.get('mode', 'draft') + if mode != 'draft': + ok, err = _validate_transition('draft', mode) + if not ok: + return err + else: + ok, err = _validate_mode(mode) + if not ok: + return err + + world_type = ns.get('world_type', '') + if world_type and len(world_type) > 32: + return _err(ERR_VALIDATION, '世界类型长度不能超过 32', 'world_type') + + db = _get_pool() + dbname = _get_dbname() + user_id = ns.get('owner_id', '') + org_id = ns.get('org_id', '0') + + last_err = '' + for _attempt in range(3): # 唯一索引冲突重试(最多 3 次) + code = ns.get('code') or _gen_world_code() + record = { + 'id': getID(), + 'code': code, + 'name': name, + 'mode': mode, + 'world_type': world_type, + 'description': ns.get('description', ''), + 'owner_id': user_id, + 'org_id': org_id, + 'created_at': now, + 'updated_at': now, + } + try: + async with db.sqlorContext(dbname) as sor: + if not await _check_code_unique(sor, code): + if ns.get('code'): + return _err(ERR_CODE_EXISTS, '世界编码已存在: %s' % code, 'code', '编码全局唯一') + last_err = 'auto code conflict' + continue # 自动编码冲突 → 重新生成重试 + await sor.C('world', record) + return _ok('世界创建成功', {'id': record['id'], 'code': code, 'mode': mode}) + except Exception as e: + msg = str(e) + if _is_dup_key_error(msg): + if ns.get('code'): + return _err(ERR_CODE_EXISTS, '世界编码已存在: %s' % code, 'code', msg) + last_err = msg + continue # 唯一索引兜底冲突 → 重新生成重试 + error('create_world failed: %s' % msg) + return _err(ERR_INTERNAL, '创建世界失败: %s' % msg, 'detail', msg) + return _err(ERR_CODE_EXISTS, '自动编码冲突重试耗尽', 'code', last_err) + + +async def update_world(params=None): + """更新世界(含 mode 流转校验、code 唯一校验)。部分更新语义。""" + params = params or {} + ns = _clean_ns(params) + world_id = ns.get('id') or ns.get('world_id') + if not world_id: + return _err(ERR_VALIDATION, '缺少世界 ID', 'id') + + db = _get_pool() + dbname = _get_dbname() + async with db.sqlorContext(dbname) as sor: + rows = await sor.R('world', {'id': world_id}) + if not rows: + return _err(ERR_NOT_FOUND, '世界不存在: %s' % world_id, 'id') + + cur = rows[0] + upd = {} + + if 'name' in ns: + if not ns['name']: + return _err(ERR_VALIDATION, '世界名称不能为空', 'name') + upd['name'] = ns['name'] + if 'world_type' in ns: + upd['world_type'] = ns['world_type'] + if 'description' in ns: + upd['description'] = ns['description'] + + # mode 切换校验(仅在变更时) + new_mode = ns.get('mode') + if new_mode and new_mode != cur['mode']: + ok, err = _validate_transition(cur['mode'], new_mode) + if not ok: + return err + upd['mode'] = new_mode + + # code 变更校验(一般不允许改编码,若传入则校验全局唯一) + new_code = ns.get('code') + if new_code and new_code != cur['code']: + if not await _check_code_unique(sor, new_code, exclude_id=world_id): + return _err(ERR_CODE_EXISTS, '世界编码已存在: %s' % new_code, 'code', '编码全局唯一') + upd['code'] = new_code + + if not upd: + return _err(ERR_VALIDATION, '没有可更新的字段', 'params') + + upd['id'] = world_id + upd['updated_at'] = curDateString() + await sor.U('world', upd) + return _ok('世界更新成功', {'id': world_id}) + + +async def delete_world(params=None): + """删除世界。""" + params = params or {} + world_id = params.get('id') or params.get('world_id') + if not world_id: + return _err(ERR_VALIDATION, '缺少世界 ID', 'id') + + db = _get_pool() + dbname = _get_dbname() + async with db.sqlorContext(dbname) as sor: + rows = await sor.R('world', {'id': world_id}) + if not rows: + return _err(ERR_NOT_FOUND, '世界不存在: %s' % world_id, 'id') + await sor.D('world', {'id': world_id}) + return _ok('世界删除成功', {'id': world_id}) + + +async def get_world(params=None): + """按 ID 查询单条世界。""" + params = params or {} + world_id = params.get('id') or params.get('world_id') + if not world_id: + return _err(ERR_VALIDATION, '缺少世界 ID', 'id') + + db = _get_pool() + dbname = _get_dbname() + async with db.sqlorContext(dbname) as sor: + rows = await sor.R('world', {'id': world_id}) + if not rows: + return _err(ERR_NOT_FOUND, '世界不存在: %s' % world_id, 'id') + return _ok('ok', _pick(rows[0], LIST_FIELDS.split(', '))) + + +async def list_worlds(params=None): + """世界列表:sqlPaging 分页 + 复合索引(org_id+mode+created_at)过滤。 + + 返回 {code:0, message, list: [...], total, page, page_size}。 + 采用「count 与数据查询分离」+ 显式列清单,P95 达标。 + """ + params = params or {} + page = _to_int(params.get('page'), 1) + rows_per_page = _to_int(params.get('rows') or params.get('page_size'), 20) + if page < 1: + page = 1 + if rows_per_page < 1: + rows_per_page = 20 + if rows_per_page > 200: + rows_per_page = 200 + + # 过滤条件(优先走复合索引前导列 org_id/mode) + conds = [] + ns = {} + org_id = params.get('org_id') + mode = params.get('mode') + world_type = params.get('world_type') + keyword = params.get('keyword') + if org_id: + conds.append('org_id = ${org_id}$') + ns['org_id'] = org_id + if mode: + conds.append('mode = ${mode}$') + ns['mode'] = mode + if world_type: + conds.append('world_type = ${world_type}$') + ns['world_type'] = world_type + if keyword: + conds.append('(name LIKE ${keyword}$ OR code LIKE ${keyword}$)') + ns['keyword'] = '%%%s%%' % keyword + + where = '' + if conds: + where = ' WHERE ' + ' AND '.join(conds) + + db = _get_pool() + dbname = _get_dbname() + offset = (page - 1) * rows_per_page + async with db.sqlorContext(dbname) as sor: + # ① count 查询 + cnt_rows = await sor.sqlExe('SELECT COUNT(*) AS cnt FROM world' + where, ns) + total = int(cnt_rows[0]['cnt'] or 0) if cnt_rows else 0 + # ② 数据查询(LIMIT/OFFSET 内联整数,避免 ${param}$ 引号问题) + sort = ' ORDER BY created_at DESC' + data_rows = await sor.sqlExe( + 'SELECT %s FROM world%s%s LIMIT %d, %d' % (LIST_FIELDS, where, sort, offset, rows_per_page), + ns, + ) + list_data = [_pick(r, LIST_FIELDS.split(', ')) for r in (data_rows or [])] + return { + 'code': 0, + 'message': 'ok', + 'list': list_data, + 'total': total, + 'page': page, + 'page_size': rows_per_page, + } + + +async def set_world_mode(params=None): + """模式切换:严格按流转规则校验(draft→active⇄paused、active/paused→archived、archived→active)。""" + params = params or {} + world_id = params.get('id') or params.get('world_id') + new_mode = params.get('mode') + if not world_id: + return _err(ERR_VALIDATION, '缺少世界 ID', 'id') + if not new_mode: + return _err(ERR_VALIDATION, '缺少目标模式', 'mode') + + db = _get_pool() + dbname = _get_dbname() + async with db.sqlorContext(dbname) as sor: + rows = await sor.R('world', {'id': world_id}) + if not rows: + return _err(ERR_NOT_FOUND, '世界不存在: %s' % world_id, 'id') + cur_mode = rows[0]['mode'] + if cur_mode == new_mode: + return _err(ERR_INVALID_TRANSITION, '世界已处于 %s 模式,无需切换' % new_mode, 'mode') + ok, err = _validate_transition(cur_mode, new_mode) + if not ok: + return err + await sor.U('world', {'id': world_id, 'mode': new_mode, 'updated_at': curDateString()}) + return _ok('模式切换成功', {'id': world_id, 'mode': new_mode}) diff --git a/wwwroot/api/create_world.dspy b/wwwroot/api/create_world.dspy index a49a8c8..8956989 100644 --- a/wwwroot/api/create_world.dspy +++ b/wwwroot/api/create_world.dspy @@ -1,6 +1,10 @@ -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")} +"""创建世界:自动编码 + mode 白名单/流转校验 + 编码唯一冲突重试。 + +REST 契约:错误结构 {code, message, field, detail};成功 {code:0, message, data}。 +""" +try: + result = await create_worlds(dict(params_kw)) + return json.dumps(result, ensure_ascii=False) +except Exception as e: + exception('create_world.dspy: %s' % e) + return json.dumps({'code': 'INTERNAL_ERROR', 'message': '创建世界失败', 'field': '', 'detail': format_exc()}, ensure_ascii=False) diff --git a/wwwroot/api/get_search_status.dspy b/wwwroot/api/get_search_status.dspy index 0ed05c7..08b1bfb 100644 --- a/wwwroot/api/get_search_status.dspy +++ b/wwwroot/api/get_search_status.dspy @@ -1,8 +1,13 @@ -result = [{"value": "", "text": "全部"}] +"""世界模式下拉数据源(world_mode 编码字典):返回 [{value, text}],首项为"全部"。""" +result = [{'value': '', 'text': '全部'}] try: - 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] + dbname = get_module_dbname('world') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe( + 'SELECT k AS value, v AS text FROM appcodes_kv WHERE parentid = ${pid}$ ORDER BY id', + {'pid': 'world_mode'}, + ) + result = result + [{'value': r['value'], 'text': r['text']} for r in (rows or [])] except Exception as e: - debug(f"get_search_status error: {e}") -return result + exception('get_search_status.dspy: %s' % e) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_search_world_type.dspy b/wwwroot/api/get_search_world_type.dspy index 20c2f4c..964d40a 100644 --- a/wwwroot/api/get_search_world_type.dspy +++ b/wwwroot/api/get_search_world_type.dspy @@ -1,8 +1,13 @@ -result = [{"value": "", "text": "全部"}] +"""世界类型下拉数据源(world_type 编码字典):返回 [{value, text}],首项为"全部"。""" +result = [{'value': '', 'text': '全部'}] try: - 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] + dbname = get_module_dbname('world') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe( + 'SELECT k AS value, v AS text FROM appcodes_kv WHERE parentid = ${pid}$ ORDER BY id', + {'pid': 'world_type'}, + ) + result = result + [{'value': r['value'], 'text': r['text']} for r in (rows or [])] except Exception as e: - debug(f"get_search_world_type error: {e}") -return result + exception('get_search_world_type.dspy: %s' % e) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_world.dspy b/wwwroot/api/get_world.dspy new file mode 100644 index 0000000..c7483ac --- /dev/null +++ b/wwwroot/api/get_world.dspy @@ -0,0 +1,7 @@ +"""按 ID 查询单个世界。""" +try: + result = await get_world(dict(params_kw)) + return json.dumps(result, ensure_ascii=False) +except Exception as e: + exception('get_world.dspy: %s' % e) + return json.dumps({'code': 'INTERNAL_ERROR', 'message': '查询世界失败', 'field': '', 'detail': format_exc()}, ensure_ascii=False) diff --git a/wwwroot/api/list_worlds.dspy b/wwwroot/api/list_worlds.dspy index e6671b1..b9a2a34 100644 --- a/wwwroot/api/list_worlds.dspy +++ b/wwwroot/api/list_worlds.dspy @@ -1,2 +1,10 @@ -worlds = await list_worlds({}) -return {"status": 0, "data": worlds} +"""世界列表查询:sqlPaging + 复合索引(org_id+mode+created_at)过滤。 + +REST 契约:分页结构 {list, total, page, page_size};错误 {code, message, field, detail}。 +""" +try: + data = await list_worlds(dict(params_kw)) + return json.dumps(data, ensure_ascii=False) +except Exception as e: + exception('list_worlds.dspy: %s' % e) + return json.dumps({'code': 'INTERNAL_ERROR', 'message': '查询世界列表失败', 'field': '', 'detail': format_exc()}, ensure_ascii=False) diff --git a/wwwroot/api/set_world_mode.dspy b/wwwroot/api/set_world_mode.dspy new file mode 100644 index 0000000..ba74fbd --- /dev/null +++ b/wwwroot/api/set_world_mode.dspy @@ -0,0 +1,10 @@ +"""世界模式切换:严格流转规则校验。 + +draft→active⇄paused、active/paused→archived、archived→active。 +""" +try: + result = await set_world_mode(dict(params_kw)) + return json.dumps(result, ensure_ascii=False) +except Exception as e: + exception('set_world_mode.dspy: %s' % e) + return json.dumps({'code': 'INTERNAL_ERROR', 'message': '模式切换失败', 'field': '', 'detail': format_exc()}, ensure_ascii=False) diff --git a/wwwroot/api/world_create.dspy b/wwwroot/api/world_create.dspy new file mode 100644 index 0000000..c15e7dd --- /dev/null +++ b/wwwroot/api/world_create.dspy @@ -0,0 +1,7 @@ +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)} diff --git a/wwwroot/api/world_delete.dspy b/wwwroot/api/world_delete.dspy index 984eaef..b998aff 100644 --- a/wwwroot/api/world_delete.dspy +++ b/wwwroot/api/world_delete.dspy @@ -1,3 +1,7 @@ -ns = dict(params_kw or {}) -await delete_world(ns) -return {"success": True} +"""删除世界。""" +try: + result = await delete_worlds(dict(params_kw)) + return json.dumps(result, ensure_ascii=False) +except Exception as e: + exception('world_delete.dspy: %s' % e) + return json.dumps({'code': 'INTERNAL_ERROR', 'message': '删除世界失败', 'field': '', 'detail': format_exc()}, ensure_ascii=False) diff --git a/wwwroot/api/world_get.dspy b/wwwroot/api/world_get.dspy new file mode 100644 index 0000000..4c136b3 --- /dev/null +++ b/wwwroot/api/world_get.dspy @@ -0,0 +1,7 @@ +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)} diff --git a/wwwroot/api/world_list.dspy b/wwwroot/api/world_list.dspy new file mode 100644 index 0000000..fd79eb1 --- /dev/null +++ b/wwwroot/api/world_list.dspy @@ -0,0 +1,7 @@ +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)} diff --git a/wwwroot/api/world_mode_switch.dspy b/wwwroot/api/world_mode_switch.dspy new file mode 100644 index 0000000..a3eac34 --- /dev/null +++ b/wwwroot/api/world_mode_switch.dspy @@ -0,0 +1,7 @@ +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)} diff --git a/wwwroot/api/world_modes.dspy b/wwwroot/api/world_modes.dspy new file mode 100644 index 0000000..5fce03f --- /dev/null +++ b/wwwroot/api/world_modes.dspy @@ -0,0 +1,7 @@ +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)} diff --git a/wwwroot/api/world_update.dspy b/wwwroot/api/world_update.dspy index a1ea0e3..17cc12c 100644 --- a/wwwroot/api/world_update.dspy +++ b/wwwroot/api/world_update.dspy @@ -1,6 +1,7 @@ -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} +"""更新世界:部分更新 + mode 流转校验 + code 唯一校验。""" +try: + result = await update_worlds(dict(params_kw)) + return json.dumps(result, ensure_ascii=False) +except Exception as e: + exception('world_update.dspy: %s' % e) + return json.dumps({'code': 'INTERNAL_ERROR', 'message': '更新世界失败', 'field': '', 'detail': format_exc()}, ensure_ascii=False) diff --git a/wwwroot/index.ui b/wwwroot/index.ui index a11c6bd..bc34bda 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -2,17 +2,29 @@ "widgettype": "VBox", "options": {"width": "100%", "height": "100%", "padding": "20px"}, "subwidgets": [ - {"widgettype": "Text", "options": {"label": "世界管理", "fontSize": "24px"}}, + {"widgettype": "Text", "options": {"label": "世界管理", "fontSize": "24px", "marginBottom": "12px"}}, {"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", + "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "6px", "boxShadow": "0 1px 4px rgba(0,0,0,0.1)"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.world_content", + "options": {"url": "{{entire_url('/world/world')}}"}, "mode": "replace"}], + "subwidgets": [ + {"widgettype": "Text", "options": {"label": "世界列表", "fontSize": "16px"}}, + {"widgettype": "Text", "options": {"label": "世界定义 CRUD:创建自动编码、模式白名单校验、编码全局唯一", "fontSize": "12px", "color": "#888"}} + ] + }, + { + "widgettype": "VBox", + "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "6px", "boxShadow": "0 1px 4px rgba(0,0,0,0.1)"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.world_content", + "options": {"url": "{{entire_url('/world/api/list_worlds.dspy')}}"}, "mode": "replace"}], + "subwidgets": [ + {"widgettype": "Text", "options": {"label": "列表查询接口", "fontSize": "16px"}}, + {"widgettype": "Text", "options": {"label": "sqlPaging 分页 + 复合索引过滤({list, total})", "fontSize": "12px", "color": "#888"}} + ] + } ]}, - {"widgettype": "VBox", "id": "app.world_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}} + {"widgettype": "VBox", "id": "world_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}} ] } diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui new file mode 100644 index 0000000..89bbdf1 --- /dev/null +++ b/wwwroot/menu.ui @@ -0,0 +1,7 @@ +{ + "widgettype": "Menu", + "options": {"target": "PopupWindow", "popup_options": {"width": "70%", "height": "80%"}, "items": [ + {"name": "world_list", "label": "世界列表", "url": "{{entire_url('/world/world')}}"}, + {"name": "world_index", "label": "世界管理首页", "url": "{{entire_url('/world/index.ui')}}"} + ]} +}