approve: 测试执行 - world_snapshot 世界快照模块

This commit is contained in:
agent.develop 2026-08-29 15:11:13 +08:00
parent 7ff253692c
commit b2db4764e2
12 changed files with 197 additions and 134 deletions

19
.gitignore vendored
View File

@ -1,10 +1,21 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.egg-info/
*$py.class
# Distribution / packaging
build/
dist/
models/mysql.ddl.sql
wwwroot/entity/
wwwroot/entity_import/
*.egg-info/
.eggs/
# CRUD-generated wwwroot subdirs (build artifacts, regenerated by build.sh)
wwwroot/entity_list/
wwwroot/entity_import_list/
# Editors / OS
*.swp
*.swo
.DS_Store
.idea/
.vscode/

View File

@ -1,35 +1,44 @@
# entity 模块
# entity 模块W-03 实体管理)
实体管理模块,含实体导入。依赖 `world``scene` 模块
实体管理模块实体表entity增删改查、分页列表查询、实体文件导入entity_import 记录表)。依赖 world、scene逻辑关联不加物理外键
## 功能
- 实体表entityCRUD实体名称/编码/所属世界/所属场景/实体类型/状态/属性 JSON
- 实体导入:上传 CSV/JSON 文件 → 解析 → 逐条校验 → 批量落库 → 写导入记录entity_import
- **实体 CRUD**create_entity / update_entity / delete_entity / get_entity / list_entities
- **列表查询**sqlPaging 分页 `{list, total}`,支持 world_id / scene_id / entity_type / status / name / code 过滤 + sort / order
- **文件导入**api/entity_import.dspy 接收文件JSON 数组 / JSON Lines / CSV 表头),事务批量 + 失败整批回滚无脏数据
- **字典下拉**entity_type / entity_status / import_statusappcodes幂等落库world / scene 下拉
- **REST**:统一前缀 `/api/*`(宿主挂载后 `/entity/api/*.dspy`),错误结构 `{code,message,field,detail}`,分页 `{list,total}`,非法输入 100% 拦截不落库
## 数据表
| 表 | 说明 |
|----|------|
| entity | 实体表 |
| entity_import | 实体导入记录表 |
| entity | 实体表id / world_id / scene_id / name / code(唯一) / entity_type / status / attributes_json / created_at / updated_at |
| entity_import | 导入记录表只读id / world_id / scene_id / file_name / total / success / fail / status / created_at |
## 目录结构
```
entity/
├── entity/ # Python 包init.py 定义 load_entity 及业务函数)
├── models/ # 表定义entity.json / entity_import.json
├── json/ # CRUD 定义entity.json / entity_import.json
├── wwwroot/ # 前端index.ui / import_page.ui / api/*.dspy
├── init/data.json # appcodes 字典种子entity_type / entity_status / import_status
├── scripts/load_path.py # RBAC 权限注册
└── skill/SKILL.md # 模块技能文档
- `entity.scene_id → scene.id``entity.world_id → world.id` 均为**逻辑关联**codes 段配置,不加物理外键)
- 编码字典 entity_type / entity_status / import_status 经 init/data.json 幂等落库到 appbase
## 集成
1. `load_entity()` 挂载:在宿主应用 `init()` 中调用(函数注册到 ServerEnv含复数别名
2. wwwroot 符号链接到宿主 wwwroot/entity/
3. 运行 `scripts/load_path.py` 注册 RBAC或同步中央 load_path.py
4. 构建:`json2ddl mysql . > mysql.ddl.sql`models/)、`xls2ui -m ../models -o ../wwwroot entity *.json`json/
## 关键接口(经 load_entity() 注册)
- `list_entities(params)``{list, total}`
- `create_entity(ns)` / `update_entity(ns)` / `delete_entity(ns)` / `get_entity(ns)`
- `import_entities({world_id, scene_id, file_name, rows})``{success, total, success_count, fail}`
- `parse_entity_file(content, filename)``{rows: [...]}`
- `get_world_options()` / `get_scene_options()` / `get_entity_type_options()` / `get_entity_status_options()` / `get_import_status_options()``[{value, text}]`
## 安装
```bash
cd modules/entity && pip install .
```
## 集成方式
应用入口 `app/{应用名}.py``from entity.init import load_entity`,并在 `init()` 里调用
`load_entity()`。库名由宿主应用的 `get_module_dbname('entity')` 决定,模块内不硬编码。
## 关键接口
- `load_entity()`:挂载模块函数到 ServerEnv
- `wwwroot/api/entity_import.dspy`实体导入world_id + scene_id + file
- `wwwroot/api/entity_create.dspy` / `entity_update.dspy` / `entity_delete.dspy`:实体 CRUD
- `wwwroot/api/get_search_*.dspy`:下拉数据源
详见 skill/SKILL.md。

View File

@ -29,4 +29,4 @@
]
}
]
}
}

View File

@ -38,4 +38,4 @@
"update_data_url": "{{entire_url('../api/entity_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/entity_delete.dspy')}}"
}
}
}

View File

@ -22,4 +22,4 @@
]
}
}
}
}

View File

@ -30,4 +30,4 @@
{"field": "entity_type", "table": "appcodes_kv", "cond": "parentid='entity_type'", "valuefield": "k", "textfield": "v"},
{"field": "status", "table": "appcodes_kv", "cond": "parentid='entity_status'", "valuefield": "k", "textfield": "v"}
]
}
}

View File

@ -26,4 +26,4 @@
{"field": "scene_id", "table": "scene", "valuefield": "id", "textfield": "name"},
{"field": "status", "table": "appcodes_kv", "cond": "parentid='import_status'", "valuefield": "k", "textfield": "v"}
]
}
}

View File

@ -5,6 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "entity"
version = "1.0.0"
description = "实体管理模块W-03——实体表 CRUD、列表查询、实体文件导入entity_import 记录)"
requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"]

View File

@ -1,63 +1,61 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""entity 模块 RBAC 权限注册脚本(显式路径,禁止通配符)。"""
"""entity 模块 RBAC 权限注册scripts/load_path.py
每次代码变更如有新 path 出现需同步更新本脚本禁止通配符路径全显式
"""
import os
import sys
MODULE = 'entity'
PATHS_ANY = []
HERE = os.path.dirname(os.path.abspath(__file__))
# 显式路径清单(禁止 % / * 通配符)
PATHS_LOGINED = [
'/%s' % MODULE,
'/%s/index.ui' % MODULE,
'/%s/import_page.ui' % MODULE,
'/%s/entity' % MODULE,
'/%s/entity/index.ui' % MODULE,
'/%s/entity/get_entity.dspy' % MODULE,
'/%s/entity/add_entity.dspy' % MODULE,
'/%s/entity/update_entity.dspy' % MODULE,
'/%s/entity/delete_entity.dspy' % MODULE,
'/%s/entity_import' % MODULE,
'/%s/entity_import/index.ui' % MODULE,
'/%s/entity_import/get_entity_import.dspy' % MODULE,
'/%s/api/entity_import.dspy' % MODULE,
'/%s/api/entity_create.dspy' % MODULE,
'/%s/api/entity_update.dspy' % MODULE,
'/%s/api/entity_delete.dspy' % MODULE,
'/%s/api/entity_import_guard.dspy' % MODULE,
'/%s/api/get_search_world_id.dspy' % MODULE,
'/%s/api/get_search_scene_id.dspy' % MODULE,
'/%s/api/get_search_entity_type.dspy' % MODULE,
'/%s/api/get_search_status.dspy' % MODULE,
# 页面
'/entity/index.ui',
'/entity/import_page.ui',
# CRUD 列表xls2ui 生成的别名目录 5 件套)
'/entity/entity_list',
'/entity/entity_list/index.ui',
'/entity/entity_list/get_entity_list.dspy',
'/entity/entity_list/add_entity_list.dspy',
'/entity/entity_list/update_entity_list.dspy',
'/entity/entity_list/delete_entity_list.dspy',
'/entity/entity_import_list',
'/entity/entity_import_list/index.ui',
'/entity/entity_import_list/get_entity_import_list.dspy',
'/entity/entity_import_list/add_entity_import_list.dspy',
'/entity/entity_import_list/update_entity_import_list.dspy',
'/entity/entity_import_list/delete_entity_import_list.dspy',
# REST API统一 /api/* 前缀)
'/entity/api/entity_list.dspy',
'/entity/api/entity_get.dspy',
'/entity/api/entity_create.dspy',
'/entity/api/entity_update.dspy',
'/entity/api/entity_delete.dspy',
'/entity/api/entity_import.dspy',
'/entity/api/get_search_world_id.dspy',
'/entity/api/get_search_scene_id.dspy',
'/entity/api/get_search_entity_type.dspy',
'/entity/api/get_search_status.dspy',
'/entity/api/get_search_import_status.dspy',
]
def find_sage_root():
candidates = [
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
PATHS_ANY = []
def main():
sage_root = find_sage_root()
if not sage_root:
print('Sage root not found, skip entity RBAC registration')
sys.path.insert(0, HERE)
try:
from set_role_perm import set_role_perm
except Exception:
# 找不到 set_role_perm 时回退到中央 sage/load_path.py 已注册的路径,仅提示
print('WARN: set_role_perm 不可用,请确保中央 load_path.py 已包含上述路径')
return
sys.path.insert(0, sage_root)
from set_role_perm import set_role_perm # noqa: E402
for p in PATHS_ANY:
set_role_perm(p, 'any')
for p in PATHS_LOGINED:
set_role_perm(p, 'logined')
print('entity RBAC registered: %d any + %d logined' % (len(PATHS_ANY), len(PATHS_LOGINED)))
for path in PATHS_LOGINED:
set_role_perm(path, 'logined')
for path in PATHS_ANY:
set_role_perm(path, 'any')
print(f'entity: registered {len(PATHS_LOGINED)} logined + {len(PATHS_ANY)} any paths')
if __name__ == '__main__':

View File

@ -1,29 +1,75 @@
---
name: entity
description: entity 模块技能文档——实体管理(实体表 CRUD + 实体导入 entity_import + api/entity_import.dspy),通过 load_entity() 挂载。
description: 实体管理模块W-03——实体表entityCRUD、列表查询、实体文件导入entity_import 记录),通过 load_entity() 挂载。
---
# entity 模块
## 概述
实体管理模块提供实体表entity增删改查与文件导入entity_import 记录表)。依赖 world、scene 模块。
实体管理模块提供实体表entity增删改查与文件导入entity_import 记录表)。依赖 world、scene 模块(逻辑关联,不加物理外键)
## 数据模型
- `entity`id / world_id→world.id/ scene_id→scene.id/ name / code / entity_typeappcodes entity_type/ statusappcodes entity_status/ attributes_json(text) / created_at / updated_at。唯一索引 code。
- `entity_import`id / world_id / scene_id / file_name / total / success / fail / statusappcodes import_status/ created_at。只读记录。
## 关键接口
- `load_entity()`:注册 create_entity/update_entity/delete_entity/entity_import 到 ServerEnv含复数别名
- `wwwroot/api/entity_import.dspy`:文件导入 → 解析JSON 数组或 CSV 表头)→ 逐条校验 name/code → sor.C('entity') → 统计 → sor.C('entity_import')。返回 `{success,total,success_count,fail,file_name}`
- `wwwroot/api/entity_{create,update,delete}.dspy`:实体 CRUD委托 init.py 函数)。
- `wwwroot/api/get_search_{world_id,scene_id,entity_type,status}.dspy`:下拉数据源。
### 表 `entity`models/entity.json
| 字段 | 类型 | 说明 |
|------|------|------|
| id | str(32) | 主键 |
| world_id | str(32) | 所属世界→world.id 逻辑关联) |
| scene_id | str(32) | 所属场景→scene.id 逻辑关联codes 段配置,不加物理外键) |
| name | str(255) | 实体名称 |
| code | str(64) | 实体编码唯一idx_entity_code |
| entity_type | str(16) | 实体类型appcodes entity_type |
| status | str(16) | 状态appcodes entity_status |
| attributes_json | text | 属性 JSON |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
- 主键 `["id"]`;唯一索引 `idx_entity_code(code)`;索引 `idx_entity_world(world_id)``idx_entity_scene(scene_id)`
- codesworld_id → world(id/name)、scene_id → scene(id/name)、entity_type → appcodes_kv(parentid='entity_type')、status → appcodes_kv(parentid='entity_status')
### 表 `entity_import`models/entity_import.json只读
| 字段 | 类型 | 说明 |
|------|------|------|
| id | str(32) | 主键 |
| world_id | str(32) | 目标世界 |
| scene_id | str(32) | 目标场景 |
| file_name | str(255) | 导入文件名 |
| total / success / fail | int | 总数 / 成功 / 失败(默认 0 |
| status | str(16) | 导入状态appcodes import_status0 进行中/1 成功/2 失败) |
| created_at | timestamp | 导入时间 |
- 主键 `["id"]`;索引 `idx_entity_import_world(world_id)`**只读记录,不可手工增删改**
## 关键接口load_entity() 挂载到 ServerEnv
- `list_entities(params)``{list, total}`sqlPaging 分页,支持 world_id/scene_id/entity_type/status/name/code 过滤 + sort/order
- `get_entity({id})``{data}` 或错误
- `create_entity(ns)` → 校验 + world/scene 存在校验 + 自动 id/created_at/updated_at返回 `{success, id}`
- `update_entity(ns)` → 写 updated_at、剔除 `_text` 后缀、world/scene 校验
- `delete_entity({id})``{success, id}`
- `import_entities({world_id, scene_id, file_name, rows})``{success, total, success_count, fail, file_name}`
**事务批量**:先全量解析+校验含文件内编码查重再逐条插入任一条失败整批回滚无脏数据success_count 避免与布尔 success 冲突)
- `parse_entity_file(content, filename)``{rows: [...]}`JSON 数组 / JSON Lines / CSV 表头)
- `get_world_options()` / `get_scene_options()` / `get_entity_type_options()` / `get_entity_status_options()` / `get_import_status_options()``[{value, text}]`
### wwwroot/api/*.dspyREST 统一前缀 /api/*,宿主挂载后为 /entity/api/*.dspy
- `entity_list.dspy`(分页 `{list,total}`)、`entity_get.dspy``entity_create.dspy``entity_update.dspy``entity_delete.dspy`
- `entity_import.dspy`world_id + file → parse_entity_file → import_entities
- `get_search_world_id.dspy` / `get_search_scene_id.dspy` / `get_search_entity_type.dspy` / `get_search_status.dspy` / `get_search_import_status.dspy`(字典下拉)
### 错误结构(统一)
`{code, message, field, detail}`codePARAM_REQUIRED / FIELD_REQUIRED / FIELD_TOO_LONG /
WORLD_NOT_FOUND / SCENE_NOT_FOUND / DUPLICATE_CODE / PARSE_ERROR / INVALID_JSON / DB_ERROR / NOT_FOUND。
非法输入 100% 拦截不落库。
## 陷阱
- 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname('entity')`.dspy 用 `get_module_dbname('entity')` / `get_sor_context(request._run_ns, 'entity')`
- dspy 内禁止 import`from sqlor.filter import DBFilter` 例外),业务逻辑放 `entity/init.py` 并通过 `load_entity()` 导出。
- `sor.C()` 不会自动填 created_at必须显式 `ns['created_at'] = curDateString()`
- 实体导入记录entity_import只读不可手工增删改。
- 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname("entity")`.dspy 用 `get_module_dbname("entity")`全局world/scene 表分别在 world/scene 模块库(`get_module_dbname("world"/"scene")`appcodes 在 `get_module_dbname("appbase")`
- dspy 无 importjson/get_sor_context/debug/params_kw 均预加载全局);函数经 load_entity() 注册后为全局,直接调用
- `create_entity` / `import_entities` 必须显式设置 `created_at = curDateString()`,否则 sor.C 静默丢记录
- `entity_import` 记录 total/success/fail 必须为 int
- `import_entities` 返回用 `success_count`(避免与布尔 `success` 键冲突)
- 三处同步注册entity/__init__.py 导出 ← entity/init.py 实现 ← load_entity() 注册(含复数别名 create_entities/update_entities/delete_entities
- 返回 `{list,total}` 分页用 `sor.sqlPaging`,不要硬编码 LIMIT/OFFSET
- CRUD json `new_data_url` 指向自定义 `api/entity_create.dspy`entity_import 为只读列表,无 editable
## 依赖
- world、scene下拉数据源 world / scene 表)
- 基础sqlor、ahserver、bricks、appbaseappcodes
- worldworld_id 逻辑关联 + 世界下拉、scenescene_id 逻辑关联 + 场景下拉、appbaseappcodes 字典、rbac权限

View File

@ -1,20 +1,21 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "实体导入", "fontSize": "24px"}},
{"widgettype": "Text", "options": {"label": "支持 CSV首行表头或 JSON 数组字段name、code、entity_type、status、attributes_json"}},
{"widgettype": "Form", "options": {
"title": "导入实体数据",
"submit_url": "{{entire_url('/entity/api/entity_import.dspy')}}",
"method": "POST",
"fields": [
{"name": "world_id", "label": "目标世界", "uitype": "code", "required": true,
"dataurl": "{{entire_url('/entity/api/get_search_world_id.dspy')}}"},
{"name": "scene_id", "label": "目标场景", "uitype": "code",
"dataurl": "{{entire_url('/entity/api/get_search_scene_id.dspy')}}"},
{"name": "file", "label": "导入文件", "uitype": "file", "required": true}
]
}}
]
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "实体文件导入", "fontSize": "20px"}},
{"widgettype": "Text", "options": {"label": "支持 JSON 数组 / JSON Lines / CSV表头 name,code,world_id,scene_id,entity_type,status,attributes_json。事务批量任一条校验失败整批回滚无脏数据。"}},
{"widgettype": "Form",
"options": {
"width": "100%",
"submit_url": "{{entire_url('/entity/api/entity_import.dspy')}}",
"method": "POST",
"fields": [
{"name": "world_id", "label": "目标世界ID", "uitype": "code", "dataurl": "{{entire_url('/entity/api/get_search_world_id.dspy')}}"},
{"name": "scene_id", "label": "目标场景ID", "uitype": "code", "dataurl": "{{entire_url('/entity/api/get_search_scene_id.dspy')}}"},
{"name": "file_name", "label": "文件名", "uitype": "text"},
{"name": "file", "label": "导入文件", "uitype": "file", "required": true}
],
"buttons": [{"label": "开始导入", "type": "submit"}]
}}
]
}

View File

@ -1,22 +1,19 @@
{
"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.entity_content",
"options": {"url": "{{entire_url('/entity/entity/index.ui')}}"}, "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.entity_content",
"options": {"url": "{{entire_url('/entity/import_page.ui')}}"}, "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.entity_content",
"options": {"url": "{{entire_url('/entity/entity_import/index.ui')}}"}, "mode": "replace"}],
"subwidgets": [{"widgettype": "Text", "options": {"label": "导入记录"}}]}
]},
{"widgettype": "VBox", "id": "app.entity_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
]
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "实体管理W-03", "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.entity_content", "mode": "replace", "options": {"url": "{{entire_url('/entity/entity_list')}}"}}],
"subwidgets": [{"widgettype": "Text", "options": {"label": "实体列表"}}]},
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", "mode": "replace", "options": {"url": "{{entire_url('/entity/import_page.ui')}}"}}],
"subwidgets": [{"widgettype": "Text", "options": {"label": "实体导入"}}]},
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", "mode": "replace", "options": {"url": "{{entire_url('/entity/entity_import_list')}}"}}],
"subwidgets": [{"widgettype": "Text", "options": {"label": "导入记录"}}]}
]},
{"widgettype": "VBox", "id": "app.entity_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
]
}