commit aeae9c05a99d201dffc43b9150889fa14376d710 Author: agent.develop Date: Thu Aug 27 21:35:44 2026 +0800 feat(world): 世界模式 world 模块初始化提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b3d32c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +*.egg-info/ +__pycache__/ +*.pyc +*.swp +*.swo +wwwroot/world/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9da0e28 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# world 模块 + +世界定义与管理(根模块,平台入口域)。 + +## 功能 +- 世界表 `world` 的 CRUD(list/get/create/update/delete) +- 世界列表查询接口(供下拉/菜单) +- 创建世界自动生成 code、写入 created_at + +## 数据表 +- `world`:世界表(见 `models/world.json`) + - 主键 `id`(str32) + - 唯一索引 `idx_world_code`(code) + - 字典:world_type、status(appcodes_kv) + +## 目录结构 +- `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(最先开发,无业务依赖)。 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..36ef2f9 --- /dev/null +++ b/__init__.py @@ -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", +] diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..4392a11 --- /dev/null +++ b/init/data.json @@ -0,0 +1,21 @@ +{ + "appcodes": [ + { + "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": "停用"} + ] + } + ] +} diff --git a/json/world.json b/json/world.json new file mode 100644 index 0000000..ba6e116 --- /dev/null +++ b/json/world.json @@ -0,0 +1,25 @@ +{ + "tblname": "world", + "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"], + "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')}}" + } + } +} \ No newline at end of file diff --git a/models/world.json b/models/world.json new file mode 100644 index 0000000..435ff3e --- /dev/null +++ b/models/world.json @@ -0,0 +1,28 @@ +{ + "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'"} + ] +} \ No newline at end of file diff --git a/models/world_member.json b/models/world_member.json new file mode 100644 index 0000000..6c7adfa --- /dev/null +++ b/models/world_member.json @@ -0,0 +1 @@ +world_member 表定义。 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e07d01a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "world" +version = "1.0.0" +requires-python = ">=3.8" +dependencies = ["sqlor", "bricks_for_python"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["world*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..a20a0c9 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""world 模块 RBAC 路径注册(显式路径,禁止通配符)。""" +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")): + 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(): + 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") + + +if __name__ == "__main__": + main() diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..32ef5fc --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,54 @@ +--- +name: world +description: world 模块技能文档——世界定义与管理(根模块,平台入口域)。提供 world 表 CRUD、列表查询接口、创建自动编码,通过 load_world() 挂载。 +--- + +# world 模块 + +## 概述 +世界定义与管理,是平台的根模块/入口域,无业务依赖,最先开发。依赖 appbase(appcodes 字典)、rbac(权限)。 + +## 数据模型 + +### 表 `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 | 更新时间 | + +- 主键:`["id"]` +- 唯一索引:`idx_world_code(code)` +- 字典:world_type → appcodes_kv(parentid='world_type')、status → appcodes_kv(parentid='world_status') + +## 关键接口 + +### 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`:字典下拉 + +## 陷阱 +- 库名禁止硬编码,统一 `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` + +## 依赖 +- appbase(appcodes 字典)、rbac(权限) diff --git a/world/__init__.py b/world/__init__.py new file mode 100644 index 0000000..c4685d2 --- /dev/null +++ b/world/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +"""world 模块包入口。""" +from .init 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", +] diff --git a/world/init.py b/world/init.py new file mode 100644 index 0000000..c608b0f --- /dev/null +++ b/world/init.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +"""world 模块初始化与业务函数。 + +通过 load_world() 将函数注册到 ServerEnv,供 .ui/.dspy 直接调用。 +库名统一从宿主应用获取,禁止硬编码 DBNAME。 +""" +from appPublic.uniqueID import getID +from appPublic.timeUtils import curDateString, timestampstr +from sqlor.dbpools import DBPools + + +def _get_dbname(): + """取库名(禁止硬编码 DBNAME)。""" + from ahserver 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 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 + return env diff --git a/wwwroot/api/create_world.dspy b/wwwroot/api/create_world.dspy new file mode 100644 index 0000000..a49a8c8 --- /dev/null +++ b/wwwroot/api/create_world.dspy @@ -0,0 +1,6 @@ +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")} diff --git a/wwwroot/api/get_search_status.dspy b/wwwroot/api/get_search_status.dspy new file mode 100644 index 0000000..0ed05c7 --- /dev/null +++ b/wwwroot/api/get_search_status.dspy @@ -0,0 +1,8 @@ +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] +except Exception as e: + debug(f"get_search_status error: {e}") +return result diff --git a/wwwroot/api/get_search_world_type.dspy b/wwwroot/api/get_search_world_type.dspy new file mode 100644 index 0000000..20c2f4c --- /dev/null +++ b/wwwroot/api/get_search_world_type.dspy @@ -0,0 +1,8 @@ +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] +except Exception as e: + debug(f"get_search_world_type error: {e}") +return result diff --git a/wwwroot/api/list_worlds.dspy b/wwwroot/api/list_worlds.dspy new file mode 100644 index 0000000..e6671b1 --- /dev/null +++ b/wwwroot/api/list_worlds.dspy @@ -0,0 +1,2 @@ +worlds = await list_worlds({}) +return {"status": 0, "data": worlds} diff --git a/wwwroot/api/world_delete.dspy b/wwwroot/api/world_delete.dspy new file mode 100644 index 0000000..984eaef --- /dev/null +++ b/wwwroot/api/world_delete.dspy @@ -0,0 +1,3 @@ +ns = dict(params_kw or {}) +await delete_world(ns) +return {"success": True} diff --git a/wwwroot/api/world_update.dspy b/wwwroot/api/world_update.dspy new file mode 100644 index 0000000..a1ea0e3 --- /dev/null +++ b/wwwroot/api/world_update.dspy @@ -0,0 +1,6 @@ +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} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..a11c6bd --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +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')}}"}, "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"}} + ] +}