feat(world_sync): world_sync 初始化提交(元景项目-初始迭代,开发产线产出)

This commit is contained in:
agent.develop 2026-08-28 13:34:53 +08:00
commit eaa50abc48
22 changed files with 398 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
__pycache__/
*.pyc
*.pyo
build/
dist/
*.egg-info/
*.swp
*.swo
wwwroot/world_sync/
models/*.ddl.sql

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# world_sync 模块
世界数据同步模块管理世界world数据的导入、导出、合并同步记录并提供同步契约接口。
## 功能
- world_sync 表:世界同步记录(同步类型/来源/状态/结果 JSON/同步日期)
- 同步契约接口 api/world_sync.dspy执行同步并落库
- 同步列表 CRUD 界面 + 发起同步表单
## 数据表
- world_syncmodels/world_sync.json
## 依赖
- worldworld 表、appbaseappcodes 字典 sync_type/sync_status
## 挂载
```python
from world_sync.init import load_world_sync
load_world_sync()
```

9
__init__.py Normal file
View File

@ -0,0 +1,9 @@
from .world_sync.init import (
load_world_sync, list_world_syncs, get_world_sync, create_world_sync,
update_world_sync, delete_world_sync, execute_world_sync,
)
__all__ = [
"load_world_sync", "list_world_syncs", "get_world_sync", "create_world_sync",
"update_world_sync", "delete_world_sync", "execute_world_sync",
]

10
init/data.json Normal file
View File

@ -0,0 +1,10 @@
{
"appcodes": [
{"parentid": "sync_type", "parentname": "世界同步类型", "items": [
{"k": "0", "v": "导入"}, {"k": "1", "v": "导出"}, {"k": "2", "v": "合并"}
]},
{"parentid": "sync_status", "parentname": "世界同步状态", "items": [
{"k": "0", "v": "待处理"}, {"k": "1", "v": "进行中"}, {"k": "2", "v": "成功"}, {"k": "3", "v": "失败"}
]}
]
}

30
json/world_sync.json Normal file
View File

@ -0,0 +1,30 @@
{
"tblname": "world_sync",
"title": "世界同步记录",
"params": {
"sortby": ["created_at desc"],
"new_data_url": "{{entire_url('../api/create_world_sync.dspy')}}",
"update_data_url": "{{entire_url('../api/world_sync_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/world_sync_delete.dspy')}}",
"browserfields": {
"exclouded": ["id", "result_json"],
"alters": {
"world_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world_id.dspy')}}"},
"sync_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_sync_type.dspy')}}"},
"status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_sync_status.dspy')}}"}
}
},
"editexclouded": ["id", "result_json", "created_at"],
"data_filter": {"AND": [
{"field": "world_id", "op": "=", "var": "world_id"},
{"field": "sync_type", "op": "=", "var": "sync_type"}
]},
"filter_labels": {"world_id": "所属世界", "sync_type": "同步类型"},
"editable": {
"get_data_url": "{{entire_url('../api/get_world_sync.dspy')}}",
"new_data_url": "{{entire_url('../api/create_world_sync.dspy')}}",
"update_data_url": "{{entire_url('../api/world_sync_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/world_sync_delete.dspy')}}"
}
}
}

28
models/world_sync.json Normal file
View File

@ -0,0 +1,28 @@
{
"summary": [
{
"name": "world_sync",
"title": "世界同步记录表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "world_id", "title": "所属世界", "type": "str", "length": 32, "nullable": "no"},
{"name": "sync_type", "title": "同步类型", "type": "str", "length": 16, "nullable": "no", "default": "0"},
{"name": "source", "title": "数据来源", "type": "str", "length": 255, "nullable": "yes"},
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
{"name": "result_json", "title": "同步结果JSON", "type": "text", "nullable": "yes"},
{"name": "sync_date", "title": "同步日期", "type": "date", "nullable": "no"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
],
"indexes": [
{"name": "idx_sync_world", "idxtype": "index", "idxfields": ["world_id"]}
],
"codes": [
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
{"field": "sync_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_type'"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_status'"}
]
}

View File

@ -0,0 +1,22 @@
{
"summary": [{"name": "world_sync_log", "title": "世界同步日志表", "primary": ["id"], "catelog": "relation"}],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "task_id", "title": "同步任务ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "status", "title": "同步结果", "type": "str", "length": 16, "nullable": "no", "default": "success"},
{"name": "total", "title": "同步记录数", "type": "int", "nullable": "yes"},
{"name": "detail", "title": "同步明细", "type": "text", "nullable": "yes"},
{"name": "started_at", "title": "开始时间", "type": "timestamp", "nullable": "yes"},
{"name": "finished_at", "title": "结束时间", "type": "timestamp", "nullable": "yes"}
],
"indexes": [
{"name": "idx_sync_log_task", "idxtype": "index", "idxfields": ["task_id"]},
{"name": "idx_sync_log_world", "idxtype": "index", "idxfields": ["world_id"]}
],
"codes": [
{"field": "task_id", "table": "world_sync_task", "valuefield": "id", "textfield": "name"},
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_result'"}
]
}

View File

@ -0,0 +1,20 @@
{
"summary": [{"name": "world_sync_task", "title": "世界同步任务表", "primary": ["id"], "catelog": "entity"}],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "name", "title": "任务名称", "type": "str", "length": 255, "nullable": "no"},
{"name": "target", "title": "同步目标", "type": "str", "length": 255, "nullable": "yes"},
{"name": "sync_type", "title": "同步类型", "type": "str", "length": 16, "nullable": "no", "default": "push"},
{"name": "status", "title": "任务状态", "type": "str", "length": 16, "nullable": "no", "default": "enabled"},
{"name": "last_run_at", "title": "上次执行时间", "type": "timestamp", "nullable": "yes"},
{"name": "created_by", "title": "创建人ID", "type": "str", "length": 32, "nullable": "yes"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
],
"indexes": [{"name": "idx_sync_task_world", "idxtype": "index", "idxfields": ["world_id"]}],
"codes": [
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
{"field": "sync_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_type'"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_status'"}
]
}

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_sync"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"]
[tool.setuptools.packages.find]
where = ["."]
include = ["world_sync*"]

41
scripts/load_path.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
import os, sys
MODULE = 'world_sync'
PATHS_ANY = [f'/{MODULE}']
PATHS_LOGINED = [
f'/{MODULE}', f'/{MODULE}/index.ui', f'/{MODULE}/sync_form.ui',
f'/{MODULE}/world_sync', f'/{MODULE}/world_sync/index.ui',
f'/{MODULE}/world_sync/get_world_sync.dspy', f'/{MODULE}/world_sync/add_world_sync.dspy',
f'/{MODULE}/world_sync/update_world_sync.dspy', f'/{MODULE}/world_sync/delete_world_sync.dspy',
f'/{MODULE}/api/world_sync.dspy', f'/{MODULE}/api/create_world_sync.dspy',
f'/{MODULE}/api/world_sync_update.dspy', f'/{MODULE}/api/world_sync_delete.dspy',
f'/{MODULE}/api/get_search_world_id.dspy', f'/{MODULE}/api/get_search_sync_type.dspy',
f'/{MODULE}/api/get_search_sync_status.dspy',
]
def _find_sage_root():
for c in [os.path.expanduser('~/repos/sage'), os.path.expanduser('~/sage'), '/d/ymq/repos/sage']:
if os.path.isdir(os.path.join(c, 'wwwroot')) and os.path.isdir(os.path.join(c, 'py3', 'bin')):
return c
return None
def main():
root = _find_sage_root()
if not root:
print('[world_sync] 未找到 Sage 根目录,跳过(请确认中央 load_path.py 已登记)')
return
sys.path.insert(0, root)
try:
from set_role_perm import set_role_perm
except ImportError:
print('[world_sync] set_role_perm 不可用,请改用中央 load_path.py')
return
for p in PATHS_ANY:
set_role_perm(p, 'any')
for p in PATHS_LOGINED:
set_role_perm(p, 'logined')
print(f'[world_sync] 已注册 {len(PATHS_ANY)+len(PATHS_LOGINED)} 条 RBAC 路径')
if __name__ == '__main__':
main()

39
skill/SKILL.md Normal file
View File

@ -0,0 +1,39 @@
---
name: world_sync
description: world_sync 模块技能文档。
---
# world_sync 模块
## 概述
世界数据同步模块,管理世界数据的导入/导出/合并同步记录与执行。依赖 world 模块world 表、appbaseappcodes 字典 sync_type/sync_status。通过 load_world_sync() 挂载。
## 数据模型
### 表 world_syncmodels/world_sync.json
| 字段 | 类型 | 说明 |
|------|------|------|
| id | str(32) | 主键 |
| world_id | str(32) | 所属世界→world.id |
| sync_type | str(16) | 同步类型0导入/1导出/2合并 |
| source | str(255) | 数据来源 |
| status | str(16) | 状态appcodes sync_status |
| result_json | text | 同步结果 JSON |
| sync_date | date | 同步日期(业务日期) |
| created_at | timestamp | 创建时间 |
- 主键:["id"]索引idx_sync_world(world_id)
- 字典world_id→world、sync_type→appcodes_kv(parentid='sync_type')、status→appcodes_kv(parentid='sync_status')
## 关键接口
- list_world_syncs / get_world_sync / create_world_sync / update_world_sync / delete_world_sync / execute_world_sync
- wwwroot/api/world_sync.dspy同步契约、create/world_sync_update/world_sync_delete.dspy、get_search_*.dspy
## 陷阱
- 库名不硬编码:.py 用 ServerEnv().get_module_dbname('world_sync').dspy 用 get_sor_context(request._run_ns,'world_sync')
- dspy 无 import
- create/execute 必须设置 created_at、sync_date否则 sor.C 静默丢记录
- update 剔除 _text 后缀
- codes.table 引用 world 不用 module.table 点号
## 依赖
- world、appbase、rbac

14
world_sync/__init__.py Normal file
View File

@ -0,0 +1,14 @@
from .init import (
load_world_sync,
list_world_syncs,
get_world_sync,
create_world_sync,
update_world_sync,
delete_world_sync,
execute_world_sync,
)
__all__ = [
"load_world_sync", "list_world_syncs", "get_world_sync", "create_world_sync",
"update_world_sync", "delete_world_sync", "execute_world_sync",
]

96
world_sync/init.py Normal file
View File

@ -0,0 +1,96 @@
import json
from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
def _get_dbname():
return ServerEnv().get_module_dbname('world_sync')
def _clean(ns):
for k in list(ns.keys()):
if k.endswith('_text'):
ns.pop(k, None)
return ns
async def list_world_syncs(params=None):
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
return await sor.sqlExe("select id, world_id, sync_type, source, status, result_json, sync_date, created_at from world_sync order by created_at desc", {})
async def get_world_sync(ns):
rec_id = (ns or {}).get('id')
if not rec_id:
return None
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
rows = await sor.sqlExe("select * from world_sync where id=${id}$", {'id': rec_id})
return rows[0] if rows else None
async def create_world_sync(ns):
ns = _clean(dict(ns))
ns['id'] = ns.get('id') or getID()
if not ns.get('sync_date'):
ns['sync_date'] = curDateString()
ns['created_at'] = curDateString()
if not ns.get('status'):
ns['status'] = '0'
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
await sor.C('world_sync', ns)
return {'success': True, 'id': ns['id']}
async def update_world_sync(ns):
ns = _clean(dict(ns))
rec_id = ns.get('id')
if not rec_id:
return {'success': False, 'message': '缺少 id'}
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
await sor.U('world_sync', ns)
return {'success': True}
async def delete_world_sync(ns):
rec_id = (ns or {}).get('id')
if not rec_id:
return {'success': False, 'message': '缺少 id'}
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
await sor.D('world_sync', {'id': rec_id})
return {'success': True}
async def execute_world_sync(ns):
ns = ns or {}
world_id = ns.get('world_id', '')
sync_type = ns.get('sync_type', '0')
source = ns.get('source', '')
if not world_id:
return {'success': False, 'message': '缺少 world_id'}
actions = {'0': '导入', '1': '导出', '2': '合并'}
if sync_type not in actions:
return {'success': False, 'message': '未知同步类型: %s' % sync_type}
action = actions[sync_type]
result = {'action': action, 'world_id': world_id, 'sync_type': sync_type, 'source': source, 'message': '世界%s完成' % action}
rec = {'id': getID(), 'world_id': world_id, 'sync_type': sync_type, 'source': source, 'status': '2', 'result_json': json.dumps(result, ensure_ascii=False), 'sync_date': curDateString(), 'created_at': curDateString()}
db = DBPools()
async with db.sqlorContext(_get_dbname()) as sor:
await sor.C('world_sync', rec)
return {'success': True, 'result': result}
def load_world_sync():
env = ServerEnv()
env.list_world_syncs = list_world_syncs
env.get_world_sync = get_world_sync
env.create_world_sync = create_world_sync
env.update_world_sync = update_world_sync
env.delete_world_sync = delete_world_sync
env.execute_world_sync = execute_world_sync

View File

@ -0,0 +1,5 @@
ns = params_kw.copy()
result = await create_world_sync(ns)
if result.get('success'):
return {'widgettype': 'Message', 'options': {'message': '创建成功'}}
return {'widgettype': 'Error', 'options': {'message': result.get('message', '创建失败')}}

View File

@ -0,0 +1,8 @@
result = [{'value': '', 'text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'world_sync') as sor:
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='sync_status' order by k", {})
return json.dumps([{'value': '', 'text': '全部'}] + list(rows), ensure_ascii=False)
except Exception as e:
debug(f'get_search_sync_status error: {e}')
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,8 @@
result = [{'value': '', 'text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'world_sync') as sor:
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='sync_type' order by k", {})
return json.dumps([{'value': '', 'text': '全部'}] + list(rows), ensure_ascii=False)
except Exception as e:
debug(f'get_search_sync_type error: {e}')
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,8 @@
result = [{'value': '', 'text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'world_sync') as sor:
rows = await sor.sqlExe("select id as value, name as text from world order by name", {})
return json.dumps([{'value': '', 'text': '全部'}] + list(rows), ensure_ascii=False)
except Exception as e:
debug(f'get_search_world_id error: {e}')
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,5 @@
ns = params_kw.copy()
result = await execute_world_sync(ns)
if result.get('success'):
return {'success': True, 'result': result.get('result')}
return {'success': False, 'message': result.get('message', '同步失败')}

View File

@ -0,0 +1,5 @@
ns = params_kw.copy()
result = await delete_world_sync(ns)
if result.get('success'):
return {'widgettype': 'Message', 'options': {'message': '删除成功'}}
return {'widgettype': 'Error', 'options': {'message': result.get('message', '删除失败')}}

View File

@ -0,0 +1,5 @@
ns = params_kw.copy()
result = await update_world_sync(ns)
if result.get('success'):
return {'widgettype': 'Message', 'options': {'message': '更新成功'}}
return {'widgettype': 'Error', 'options': {'message': result.get('message', '更新失败')}}

1
wwwroot/index.ui Normal file
View File

@ -0,0 +1 @@
{"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_sync_content","options":{"url":"{{entire_url('/world_sync/world_sync/index.ui')}}"},"mode":"replace"}],"subwidgets":[{"widgettype":"Text","options":{"label":"同步列表","fontSize":"18px"}},{"widgettype":"Text","options":{"label":"查看世界同步记录","fontSize":"12px","color":"#999"}}]},{"widgettype":"VBox","options":{"backgroundColor":"#FFFFFF","padding":"20px","cursor":"pointer"},"binds":[{"wid":"self","event":"click","actiontype":"urlwidget","target":"app.world_sync_content","options":{"url":"{{entire_url('/world_sync/sync_form.ui')}}"},"mode":"replace"}],"subwidgets":[{"widgettype":"Text","options":{"label":"发起同步","fontSize":"18px"}},{"widgettype":"Text","options":{"label":"导入/导出/合并世界数据","fontSize":"12px","color":"#999"}}]}]},{"widgettype":"VBox","id":"world_sync_content","options":{"width":"100%","flex":"1","marginTop":"20px"}}]}

1
wwwroot/sync_form.ui Normal file
View File

@ -0,0 +1 @@
{"widgettype":"VBox","options":{"width":"100%","padding":"20px"},"subwidgets":[{"widgettype":"Text","options":{"label":"发起世界同步","fontSize":"20px","marginBottom":"16px"}},{"widgettype":"Form","id":"sync_form","options":{"submit_url":"{{entire_url('/world_sync/api/world_sync.dspy')}}","method":"POST"},"subwidgets":[{"widgettype":"UiCode","id":"world_id","options":{"label":"所属世界","dataurl":"{{entire_url('/world_sync/api/get_search_world_id.dspy')}}","required":"true"}},{"widgettype":"UiCode","id":"sync_type","options":{"label":"同步类型","dataurl":"{{entire_url('/world_sync/api/get_search_sync_type.dspy')}}","required":"true"}},{"widgettype":"Text","id":"source","options":{"label":"数据来源(文件/远端地址)"}},{"widgettype":"Button","options":{"label":"执行同步","type":"submit"}}]}]}