feat(world): 世界模式 world 模块初始化提交
This commit is contained in:
commit
aeae9c05a9
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
build/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.swp
|
||||
*.swo
|
||||
wwwroot/world/
|
||||
29
README.md
Normal file
29
README.md
Normal file
@ -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(最先开发,无业务依赖)。
|
||||
19
__init__.py
Normal file
19
__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world 模块仓库根。实际 Python 包为 world/ 子目录。"""
|
||||
from .world import (
|
||||
load_world,
|
||||
list_worlds,
|
||||
get_world,
|
||||
create_world,
|
||||
update_world,
|
||||
delete_world,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"load_world",
|
||||
"list_worlds",
|
||||
"get_world",
|
||||
"create_world",
|
||||
"update_world",
|
||||
"delete_world",
|
||||
]
|
||||
21
init/data.json
Normal file
21
init/data.json
Normal 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
25
json/world.json
Normal 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
28
models/world.json
Normal 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
1
models/world_member.json
Normal file
@ -0,0 +1 @@
|
||||
world_member 表定义。
|
||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal 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
59
scripts/load_path.py
Normal 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
54
skill/SKILL.md
Normal file
@ -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(权限)
|
||||
19
world/__init__.py
Normal file
19
world/__init__.py
Normal 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
87
world/init.py
Normal 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
|
||||
6
wwwroot/api/create_world.dspy
Normal file
6
wwwroot/api/create_world.dspy
Normal 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")}
|
||||
8
wwwroot/api/get_search_status.dspy
Normal file
8
wwwroot/api/get_search_status.dspy
Normal 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
|
||||
8
wwwroot/api/get_search_world_type.dspy
Normal file
8
wwwroot/api/get_search_world_type.dspy
Normal 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
|
||||
2
wwwroot/api/list_worlds.dspy
Normal file
2
wwwroot/api/list_worlds.dspy
Normal file
@ -0,0 +1,2 @@
|
||||
worlds = await list_worlds({})
|
||||
return {"status": 0, "data": worlds}
|
||||
3
wwwroot/api/world_delete.dspy
Normal file
3
wwwroot/api/world_delete.dspy
Normal file
@ -0,0 +1,3 @@
|
||||
ns = dict(params_kw or {})
|
||||
await delete_world(ns)
|
||||
return {"success": True}
|
||||
6
wwwroot/api/world_update.dspy
Normal file
6
wwwroot/api/world_update.dspy
Normal 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
18
wwwroot/index.ui
Normal 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"}}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user