feat(world): 世界模式 world 模块初始化提交

This commit is contained in:
agent.develop 2026-08-27 21:35:44 +08:00
commit aeae9c05a9
19 changed files with 413 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
build/
*.egg-info/
__pycache__/
*.pyc
*.swp
*.swo
wwwroot/world/

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# world 模块
世界定义与管理(根模块,平台入口域)。
## 功能
- 世界表 `world` 的 CRUDlist/get/create/update/delete
- 世界列表查询接口(供下拉/菜单)
- 创建世界自动生成 code、写入 created_at
## 数据表
- `world`:世界表(见 `models/world.json`
- 主键 `id`str32
- 唯一索引 `idx_world_code`code
- 字典world_type、statusappcodes_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最先开发无业务依赖

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

21
init/data.json Normal file
View File

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

25
json/world.json Normal file
View File

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

28
models/world.json Normal file
View File

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

1
models/world_member.json Normal file
View File

@ -0,0 +1 @@
world_member

13
pyproject.toml Normal file
View File

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

59
scripts/load_path.py Normal file
View File

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

54
skill/SKILL.md Normal file
View File

@ -0,0 +1,54 @@
---
name: world
description: world 模块技能文档——世界定义与管理(根模块,平台入口域)。提供 world 表 CRUD、列表查询接口、创建自动编码通过 load_world() 挂载。
---
# world 模块
## 概述
世界定义与管理,是平台的根模块/入口域,无业务依赖,最先开发。依赖 appbaseappcodes 字典、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 无 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`
## 依赖
- appbaseappcodes 字典、rbac权限

19
world/__init__.py Normal file
View File

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

87
world/init.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
worlds = await list_worlds({})
return {"status": 0, "data": worlds}

View File

@ -0,0 +1,3 @@
ns = dict(params_kw or {})
await delete_world(ns)
return {"success": True}

View File

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

18
wwwroot/index.ui Normal file
View File

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