approve: 测试执行 - world 世界管理模块
This commit is contained in:
parent
252901fca6
commit
7c4965a972
28
README.md
28
README.md
@ -1,16 +1,34 @@
|
||||
# world_snapshot 模块
|
||||
|
||||
世界状态快照模块,聚合世界(world)、场景(scene)、实体(entity)当前状态生成快照,支持查询与恢复。
|
||||
世界状态快照模块(W-03):聚合世界(world)、场景(scene)、实体(entity)当前状态生成
|
||||
快照,支持快照查询与恢复,恢复时联动 world 表状态(经 world 模块 set_world_mode 契约)。
|
||||
|
||||
## 功能
|
||||
- 世界快照表 `world_snapshot` 的 CRUD
|
||||
- 世界快照表 `world_snapshot` 的 CRUD(create/get/list/update/delete)
|
||||
- `create_snapshot`:聚合 world/scene/entity 当前状态写入 `snapshot_json`
|
||||
- `restore_snapshot`:读取并校验快照 JSON,恢复状态(基础实现:校验 + 状态标记)
|
||||
- `restore_snapshot`:读取并校验快照 JSON,标记已恢复,联动 world 状态
|
||||
- 下拉接口:世界列表、快照类型、快照状态
|
||||
|
||||
## 数据表
|
||||
- `world_snapshot`(models/world_snapshot.json)
|
||||
- `world_snapshot`(models/world_snapshot.json 四段式)
|
||||
- 编码字典:`snapshot_type`(full/incremental)、`snapshot_status`(active/restored/invalid)
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
world_snapshot/
|
||||
├── world_snapshot/ # Python 包(init.py 实现 + __init__.py 导出)
|
||||
├── wwwroot/ # index.ui / menu.ui / api/*.dspy
|
||||
├── models/ # world_snapshot.json 表定义
|
||||
├── json/ # world_snapshot.json CRUD 定义
|
||||
├── init/ # data.json 编码字典种子
|
||||
├── scripts/ # load_path.py RBAC 注册
|
||||
├── skill/SKILL.md # 模块技能文档
|
||||
├── pyproject.toml
|
||||
├── build.sh # 四步安装(xls2ddl → ddl → crud ui → symlink)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 集成
|
||||
- 通过 `load_world_snapshot()` 挂载到宿主应用
|
||||
- 通过 `load_world_snapshot()` 挂载到宿主应用(`from world_snapshot.init import load_world_snapshot`)
|
||||
- 库名经 `ServerEnv().get_module_dbname('world_snapshot')` 获取,禁止硬编码
|
||||
- 构建:`bash build.sh`(安装 xls2ddl、生成 DDL、生成 CRUD UI、链接 wwwroot)
|
||||
|
||||
44
build.sh
Normal file
44
build.sh
Normal file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# world_snapshot module build script (four-step install).
|
||||
# 1) install xls2ddl 2) models json -> mysql.ddl.sql 3) json -> wwwroot CRUD ui
|
||||
# 4) symlink module wwwroot into the main app wwwroot.
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MOD="world_snapshot"
|
||||
|
||||
SAGE_ROOT=""
|
||||
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
|
||||
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
|
||||
SAGE_ROOT="$(cd "$candidate" && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[world_snapshot] step 1/4: install xls2ddl"
|
||||
pip install -q xls2ddl 2>/dev/null || echo " xls2ddl install skipped (already present)"
|
||||
|
||||
echo "[world_snapshot] step 2/4: generate mysql.ddl.sql from models/"
|
||||
if [ -d "$SCRIPT_DIR/models" ]; then
|
||||
json2ddl mysql "$SCRIPT_DIR/models" > "$SCRIPT_DIR/models/mysql.ddl.sql" 2>/dev/null \
|
||||
|| python3 -m xls2ddl.json2ddl mysql "$SCRIPT_DIR/models" > "$SCRIPT_DIR/models/mysql.ddl.sql"
|
||||
fi
|
||||
|
||||
echo "[world_snapshot] step 3/4: generate CRUD ui from json/"
|
||||
if [ -d "$SCRIPT_DIR/json" ]; then
|
||||
(cd "$SCRIPT_DIR" && PYTHONPATH="$SCRIPT_DIR" python3 -m xls2ddl.xls2crud \
|
||||
-m "$SCRIPT_DIR/models" -o "$SCRIPT_DIR/wwwroot" "$MOD" json/*.json) \
|
||||
|| echo " xls2crud skipped (generate custom ui manually)"
|
||||
fi
|
||||
|
||||
echo "[world_snapshot] step 4/4: symlink module wwwroot into app wwwroot"
|
||||
if [ -n "$SAGE_ROOT" ] && [ -d "$SAGE_ROOT/wwwroot" ]; then
|
||||
ln -sfn "$SCRIPT_DIR/wwwroot" "$SAGE_ROOT/wwwroot/$MOD"
|
||||
if [ -d "$SCRIPT_DIR/wwwroot/world_snapshot" ]; then
|
||||
ln -sfn "$SCRIPT_DIR/wwwroot/world_snapshot" "$SAGE_ROOT/wwwroot/$MOD/world_snapshot"
|
||||
fi
|
||||
echo " linked $SAGE_ROOT/wwwroot/$MOD"
|
||||
else
|
||||
echo " sage root not found, skip symlink"
|
||||
fi
|
||||
|
||||
echo "[world_snapshot] build.sh done"
|
||||
@ -1,13 +1,21 @@
|
||||
{
|
||||
"appcodes": [
|
||||
{"parentid": "snapshot_type", "parentname": "快照类型", "items": [
|
||||
{"k": "0", "v": "手动"},
|
||||
{"k": "1", "v": "自动"}
|
||||
]},
|
||||
{"parentid": "snapshot_status", "parentname": "快照状态", "items": [
|
||||
{"k": "0", "v": "正常"},
|
||||
{"k": "1", "v": "已恢复"},
|
||||
{"k": "2", "v": "已作废"}
|
||||
]}
|
||||
{
|
||||
"parentid": "snapshot_type",
|
||||
"parentname": "快照类型",
|
||||
"items": [
|
||||
{"k": "full", "v": "全量快照"},
|
||||
{"k": "incremental", "v": "增量快照"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "snapshot_status",
|
||||
"parentname": "快照状态",
|
||||
"items": [
|
||||
{"k": "active", "v": "有效"},
|
||||
{"k": "restored", "v": "已恢复"},
|
||||
{"k": "invalid", "v": "失效"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,21 +1,31 @@
|
||||
{
|
||||
"tblname": "world_snapshot",
|
||||
"alias": "world_snapshot",
|
||||
"title": "世界快照",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "world_id", "op": "=", "var": "world_id"},
|
||||
{"field": "snapshot_date", "op": "=", "var": "snapshot_date"}
|
||||
]
|
||||
"record_toolbar": [{
|
||||
"label": "恢复",
|
||||
"actiontype": "dspy",
|
||||
"url": "{{entire_url('../api/restore_snapshot.dspy')}}",
|
||||
"options": {"icon": "undo", "cwidth": 16, "cheight": 9}
|
||||
}],
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "snapshot_json"],
|
||||
"alters": {
|
||||
"world_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world.dspy')}}"},
|
||||
"snapshot_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_snapshot_type.dspy')}}"},
|
||||
"status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_status.dspy')}}"}
|
||||
}
|
||||
},
|
||||
"filter_labels": {"world_id": "所属世界", "snapshot_date": "快照日期"},
|
||||
"browserfields": {"exclouded": ["id", "snapshot_json"], "alters": {}},
|
||||
"editexclouded": ["id", "snapshot_json", "created_at"],
|
||||
"editexclouded": ["id", "snapshot_json", "status", "snapshot_date", "created_at"],
|
||||
"new_data_url": "{{entire_url('../api/create_snapshot.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/snapshot_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/snapshot_delete.dspy')}}",
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/create_snapshot.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/world_snapshot_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/world_snapshot_delete.dspy')}}"
|
||||
"update_data_url": "{{entire_url('../api/snapshot_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/snapshot_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,25 @@
|
||||
{
|
||||
"summary": [
|
||||
{"name": "world_snapshot", "title": "世界快照表", "primary": ["id"], "catelog": "entity"}
|
||||
{
|
||||
"name": "world_snapshot",
|
||||
"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": "world_id", "title": "世界ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "快照名称", "type": "str", "length": 255, "nullable": "no"},
|
||||
{"name": "snapshot_type", "title": "快照类型", "type": "str", "length": 16, "nullable": "no", "default": "0"},
|
||||
{"name": "snapshot_json", "title": "快照数据JSON", "type": "text", "nullable": "no"},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
|
||||
{"name": "snapshot_type", "title": "快照类型", "type": "str", "length": 32, "nullable": "no", "default": "full"},
|
||||
{"name": "snapshot_json", "title": "快照内容", "type": "text"},
|
||||
{"name": "status", "title": "快照状态", "type": "str", "length": 32, "nullable": "no", "default": "active"},
|
||||
{"name": "snapshot_date", "title": "快照日期", "type": "date", "nullable": "no"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_snapshot_world", "idxtype": "index", "idxfields": ["world_id"]},
|
||||
{"name": "idx_snapshot_world_date", "idxtype": "index", "idxfields": ["world_id", "snapshot_date"]}
|
||||
{"name": "idx_snapshot_world", "idxtype": "index", "idxfields": ["world_id", "snapshot_date"]},
|
||||
{"name": "idx_snapshot_list", "idxtype": "index", "idxfields": ["world_id", "status", "created_at"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
|
||||
|
||||
@ -1,64 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world_snapshot 模块 RBAC 注册脚本。所有 path 显式列出,禁止通配符。"""
|
||||
"""world_snapshot RBAC registration (per-module load_path.py).
|
||||
|
||||
Explicit paths only - no wildcards. Runs against the Sage root's
|
||||
set_role_perm.py via the central load_path.py code path.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MOD = 'world_snapshot'
|
||||
|
||||
|
||||
def _find_sage_root():
|
||||
def find_sage_root():
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
candidates = [
|
||||
os.path.join(here, '..', '..', '..'),
|
||||
os.path.expanduser('~/repos/sage'),
|
||||
os.path.expanduser('~/sage'),
|
||||
]
|
||||
for c in candidates:
|
||||
c = os.path.abspath(c)
|
||||
if os.path.isdir(os.path.join(c, 'wwwroot')) and os.path.isdir(os.path.join(c, 'py3', 'bin')):
|
||||
return c
|
||||
for cand in (os.path.join(here, '..', '..', '..'),
|
||||
os.path.join(os.path.expanduser('~'), 'repos', 'sage'),
|
||||
os.path.join(os.path.expanduser('~'), 'sage')):
|
||||
cand = os.path.normpath(cand)
|
||||
if os.path.isdir(os.path.join(cand, 'wwwroot')) and os.path.isdir(os.path.join(cand, 'py3', 'bin')):
|
||||
return cand
|
||||
return None
|
||||
|
||||
PATHS_ANY = [f'/{MOD}/index.ui']
|
||||
|
||||
PATHS_ANY = [
|
||||
'/world_snapshot/menu.ui',
|
||||
'/world_snapshot/index.ui',
|
||||
]
|
||||
|
||||
PATHS_LOGINED = [
|
||||
f'/{MOD}',
|
||||
f'/{MOD}/world_snapshot',
|
||||
f'/{MOD}/world_snapshot/index.ui',
|
||||
f'/{MOD}/world_snapshot/get_world_snapshot.dspy',
|
||||
f'/{MOD}/world_snapshot/add_world_snapshot.dspy',
|
||||
f'/{MOD}/world_snapshot/update_world_snapshot.dspy',
|
||||
f'/{MOD}/world_snapshot/delete_world_snapshot.dspy',
|
||||
f'/{MOD}/api/create_snapshot.dspy',
|
||||
f'/{MOD}/api/restore_snapshot.dspy',
|
||||
f'/{MOD}/api/get_snapshot.dspy',
|
||||
f'/{MOD}/api/list_snapshots.dspy',
|
||||
f'/{MOD}/api/world_snapshot_update.dspy',
|
||||
f'/{MOD}/api/world_snapshot_delete.dspy',
|
||||
f'/{MOD}/api/list_worlds_for_snapshot.dspy',
|
||||
f'/{MOD}/api/list_snapshot_types.dspy',
|
||||
f'/{MOD}/api/list_snapshot_statuses.dspy',
|
||||
'/world_snapshot/api/create_snapshot.dspy',
|
||||
'/world_snapshot/api/snapshot_update.dspy',
|
||||
'/world_snapshot/api/snapshot_delete.dspy',
|
||||
'/world_snapshot/api/get_snapshot.dspy',
|
||||
'/world_snapshot/api/list_snapshots.dspy',
|
||||
'/world_snapshot/api/restore_snapshot.dspy',
|
||||
'/world_snapshot/api/get_search_world.dspy',
|
||||
'/world_snapshot/api/get_search_snapshot_type.dspy',
|
||||
'/world_snapshot/api/get_search_status.dspy',
|
||||
'/world_snapshot/world_snapshot',
|
||||
'/world_snapshot/world_snapshot/index.ui',
|
||||
'/world_snapshot/world_snapshot/get_world_snapshot.dspy',
|
||||
'/world_snapshot/world_snapshot/add_world_snapshot.dspy',
|
||||
'/world_snapshot/world_snapshot/update_world_snapshot.dspy',
|
||||
'/world_snapshot/world_snapshot/delete_world_snapshot.dspy',
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
sage_root = _find_sage_root()
|
||||
sage_root = find_sage_root()
|
||||
if not sage_root:
|
||||
print('ERROR: Sage root not found, skip RBAC registration')
|
||||
sys.exit(1)
|
||||
sys.path.insert(0, os.path.join(sage_root, 'py3', 'bin'))
|
||||
sys.path.insert(0, sage_root)
|
||||
try:
|
||||
from set_role_perm import set_role_perm
|
||||
except Exception as e:
|
||||
print('WARN: cannot import set_role_perm: %s' % e)
|
||||
sys.exit(1)
|
||||
for path in PATHS_ANY:
|
||||
set_role_perm(path, 'any')
|
||||
for path in PATHS_LOGINED:
|
||||
set_role_perm(path, 'logined')
|
||||
print('RBAC registered for module %s' % MOD)
|
||||
print('world_snapshot load_path.py: sage root not found, skip', file=sys.stderr)
|
||||
return 1
|
||||
loader = os.path.join(sage_root, 'load_path.py')
|
||||
if not os.path.isfile(loader):
|
||||
print('world_snapshot load_path.py: central load_path.py not found, skip', file=sys.stderr)
|
||||
return 1
|
||||
entries = []
|
||||
for p in PATHS_ANY:
|
||||
entries.append((p, 'any'))
|
||||
for p in PATHS_LOGINED:
|
||||
entries.append((p, 'logined'))
|
||||
payload = '\n'.join('{} {}'.format(p, role) for p, role in entries)
|
||||
proc = subprocess.run([sys.executable, loader, payload], capture_output=True, text=True)
|
||||
if proc.stdout:
|
||||
print(proc.stdout)
|
||||
if proc.stderr:
|
||||
print(proc.stderr, file=sys.stderr)
|
||||
return proc.returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
||||
@ -1,21 +1,65 @@
|
||||
---
|
||||
name: world_snapshot
|
||||
description: world_snapshot 模块技能文档——世界快照管理(世界状态快照生成、查询与恢复)。
|
||||
description: 世界状态快照模块(W-03)——聚合 world/scene/entity 当前状态生成快照,支持查询与恢复,与 world 表状态联动。
|
||||
---
|
||||
|
||||
# world_snapshot 模块
|
||||
|
||||
世界状态快照模块,聚合世界 + 场景 + 实体当前状态生成快照,支持查询与恢复。
|
||||
世界状态快照模块(W-03)。聚合世界(world)+ 场景(scene)+ 实体(entity)当前状态
|
||||
生成快照写入 `snapshot_json`,支持分页查询、单条查询、更新、删除与恢复,恢复时联动
|
||||
world 表状态(经 world 模块 set_world_mode 契约,保持模式流转校验完整)。
|
||||
|
||||
## 数据模型
|
||||
表 `world_snapshot`:id、world_id、name、snapshot_type、snapshot_json、status、snapshot_date(date)、created_at。
|
||||
|
||||
## 关键接口
|
||||
- create_snapshot / restore_snapshot / list_snapshots / get_snapshot / update_snapshot / delete_snapshot
|
||||
表 `world_snapshot`(models/world_snapshot.json 四段式 summary/fields/indexes/codes):
|
||||
- id VARCHAR(32) PK(getID() 生成,禁 uuid)
|
||||
- world_id VARCHAR(32) NOT NULL(codes → world 表)
|
||||
- name VARCHAR(255) NOT NULL
|
||||
- snapshot_type VARCHAR(32) DEFAULT 'full'(snapshot_type 编码字典 full/incremental)
|
||||
- snapshot_json TEXT(聚合内容)
|
||||
- status VARCHAR(32) DEFAULT 'active'(snapshot_status 编码字典 active/restored/invalid)
|
||||
- snapshot_date date(业务日期仅到天)
|
||||
- created_at timestamp NOT NULL
|
||||
- 复合索引 idx_snapshot_world(world_id, snapshot_date)、idx_snapshot_list(world_id, status, created_at)
|
||||
|
||||
编码字典:init/data.json 幂等落库 `snapshot_type`(full=全量快照/incremental=增量快照)
|
||||
与 `snapshot_status`(active=有效/restored=已恢复/invalid=失效),appcodes + appcodes_kv 同插。
|
||||
|
||||
## 关键接口(经 load_world_snapshot() 注册到 ServerEnv)
|
||||
|
||||
- `create_snapshot(params)` → 校验 world 存在、snapshot_type 白名单(full/incremental),
|
||||
聚合 world/scene/entity 当前状态写入 snapshot_json,必设 snapshot_date/created_at
|
||||
- `get_snapshot(params)` → 按 id 单条查询
|
||||
- `list_snapshots(params)` → 分页 {list, total, page, page_size};count 与数据查询分离,
|
||||
显式列清单(排除 TEXT),P95 达标
|
||||
- `update_snapshot(params)` → 部分更新(排除 id/created_at/snapshot_json/_text)
|
||||
- `delete_snapshot(params)` → 按 id 删除
|
||||
- `restore_snapshot(params)` → 校验 snapshot_json 合法性、标记 status=restored、
|
||||
联动 world 状态(经 env.set_world_mode 契约,不存在或失败不阻断)
|
||||
- 下拉:list_worlds_for_snapshot / list_snapshot_types / list_snapshot_statuses
|
||||
|
||||
REST 前缀 `/api/*`(wwwroot/api/*.dspy):
|
||||
- create_snapshot.dspy / snapshot_update.dspy / snapshot_delete.dspy
|
||||
- get_snapshot.dspy / list_snapshots.dspy / restore_snapshot.dspy
|
||||
- get_search_world.dspy / get_search_snapshot_type.dspy / get_search_status.dspy(下拉,返回 [{value,text}],首项"全部")
|
||||
|
||||
统一错误结构:`{code, message, field, detail}`;分页结构:`{list, total}`。
|
||||
|
||||
## 陷阱
|
||||
- 库名禁止硬编码,统一 get_module_dbname("world_snapshot")
|
||||
- dspy 无 import
|
||||
- create_snapshot 必须设置 created_at/snapshot_date
|
||||
|
||||
- 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname('world_snapshot')`,.dspy 直接
|
||||
`get_module_dbname('world_snapshot')`(预载全局)
|
||||
- 无硬编码中文(错误消息用英文 code);dspy 无 import(json/format_exc/debug/getID 等为预载全局)
|
||||
- create_snapshot 必须设置 created_at/snapshot_date(sqlor 不自动补时间戳,缺 created_at 的
|
||||
sor.C 会静默丢记录)
|
||||
- snapshot_date 用 date 类型(业务日期仅到天)
|
||||
- restore_snapshot 对 world 状态联动用契约调用(env.set_world_mode),不直接改 world 表,
|
||||
保证 mode 流转规则校验不被绕过
|
||||
- 下拉接口返回纯数组 [{value,text}],错误时返回含"全部"的回退数组
|
||||
|
||||
## 依赖
|
||||
|
||||
- sqlor(DBPools/sqlExe/sor.C/U/D/R)
|
||||
- ahserver(ServerEnv、get_module_dbname)
|
||||
- appbase(appcodes/appcodes_kv 编码字典)
|
||||
- world / scene / entity(聚合数据源,经各自模块库名读取,缺失时降级为空)
|
||||
|
||||
@ -1,12 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world_snapshot package (W-03): world state snapshot management."""
|
||||
from .init import (
|
||||
load_world_snapshot, list_snapshots, get_snapshot, create_snapshot,
|
||||
update_snapshot, delete_snapshot, restore_snapshot,
|
||||
list_worlds_for_snapshot, list_snapshot_types, list_snapshot_statuses,
|
||||
load_world_snapshot,
|
||||
create_snapshot,
|
||||
update_snapshot,
|
||||
delete_snapshot,
|
||||
get_snapshot,
|
||||
list_snapshots,
|
||||
restore_snapshot,
|
||||
list_worlds_for_snapshot,
|
||||
list_snapshot_types,
|
||||
list_snapshot_statuses,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"load_world_snapshot", "list_snapshots", "get_snapshot", "create_snapshot",
|
||||
"update_snapshot", "delete_snapshot", "restore_snapshot",
|
||||
"list_worlds_for_snapshot", "list_snapshot_types", "list_snapshot_statuses",
|
||||
'load_world_snapshot',
|
||||
'create_snapshot',
|
||||
'update_snapshot',
|
||||
'delete_snapshot',
|
||||
'get_snapshot',
|
||||
'list_snapshots',
|
||||
'restore_snapshot',
|
||||
'list_worlds_for_snapshot',
|
||||
'list_snapshot_types',
|
||||
'list_snapshot_statuses',
|
||||
]
|
||||
|
||||
@ -1,154 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""world_snapshot 模块 —— 世界状态快照管理。库名经 get_module_dbname('world_snapshot') 获取,禁止硬编码。"""
|
||||
"""world_snapshot module implementation (W-03 world snapshot).
|
||||
|
||||
Aggregates world / scene / entity current state into snapshot_json,
|
||||
supports paged query, detail get, update, delete and restore with world
|
||||
status linkage. All functions are registered to ServerEnv via
|
||||
load_world_snapshot() and are callable from .dspy wrappers directly.
|
||||
"""
|
||||
import json
|
||||
|
||||
from appPublic.timeUtils import curDateString
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.timeUtils import curDateString, timestampstr
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
|
||||
def _get_dbname():
|
||||
return ServerEnv().get_module_dbname('world_snapshot')
|
||||
SNAPSHOT_TYPES = ('full', 'incremental')
|
||||
SNAPSHOT_STATUS_ACTIVE = 'active'
|
||||
SNAPSHOT_STATUS_RESTORED = 'restored'
|
||||
WORLD_MODE_ACTIVE = 'active'
|
||||
|
||||
|
||||
async def list_snapshots(ns):
|
||||
def _to_dict(row):
|
||||
"""Convert a sqlor result row (DictObject) to a plain dict."""
|
||||
if row is None:
|
||||
return {}
|
||||
try:
|
||||
return dict(row)
|
||||
except Exception:
|
||||
try:
|
||||
keys = list(row.keys())
|
||||
except Exception:
|
||||
keys = [k for k in dir(row) if not k.startswith('_')]
|
||||
return {k: getattr(row, k, None) for k in keys}
|
||||
|
||||
|
||||
def _module_dbname(env, module):
|
||||
try:
|
||||
return env.get_module_dbname(module)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def _get_world(env, world_id):
|
||||
"""Read the world record (best effort, world module db)."""
|
||||
dbname = _module_dbname(env, 'world')
|
||||
if not dbname:
|
||||
return None
|
||||
try:
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe('select * from world where id = ${wid}$', {'wid': world_id})
|
||||
if not rows:
|
||||
return None
|
||||
return _to_dict(rows[0])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def _fetch_child_rows(env, module, table, world_id):
|
||||
"""Best-effort read of child rows (scene/entity) filtered by world_id."""
|
||||
dbname = _module_dbname(env, module)
|
||||
if not dbname:
|
||||
return []
|
||||
try:
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe(
|
||||
'select * from ' + table + ' where world_id = ${wid}$ order by created_at desc',
|
||||
{'wid': world_id})
|
||||
return [_to_dict(r) for r in (rows or [])]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
async def _get_appcodes(parentid):
|
||||
"""Read dict items from appcodes_kv (appbase first, module db fallback)."""
|
||||
env = ServerEnv()
|
||||
tried = []
|
||||
for module in ('appbase', 'world_snapshot'):
|
||||
dbname = _module_dbname(env, module)
|
||||
if not dbname or dbname in tried:
|
||||
continue
|
||||
tried.append(dbname)
|
||||
try:
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe(
|
||||
'select k as value, v as text from appcodes_kv where parentid = ${pid}$ order by v',
|
||||
{'pid': parentid})
|
||||
return [{'value': r.value, 'text': r.text} for r in (rows or [])]
|
||||
except Exception:
|
||||
continue
|
||||
return []
|
||||
|
||||
|
||||
async def create_snapshot(params):
|
||||
"""Create a world snapshot: aggregate world/scene/entity current state."""
|
||||
params = params or {}
|
||||
world_id = params.get('world_id')
|
||||
if not world_id:
|
||||
return {'code': 'PARAM_REQUIRED', 'message': 'world_id is required', 'field': 'world_id', 'detail': ''}
|
||||
snapshot_type = params.get('snapshot_type') or 'full'
|
||||
if snapshot_type not in SNAPSHOT_TYPES:
|
||||
return {'code': 'INVALID_SNAPSHOT_TYPE', 'message': 'snapshot_type must be full or incremental',
|
||||
'field': 'snapshot_type', 'detail': snapshot_type}
|
||||
env = ServerEnv()
|
||||
world = await _get_world(env, world_id)
|
||||
if not world:
|
||||
return {'code': 'WORLD_NOT_FOUND', 'message': 'world not found', 'field': 'world_id', 'detail': world_id}
|
||||
snap_data = {
|
||||
'world': world,
|
||||
'scene': await _fetch_child_rows(env, 'scene', 'scene', world_id),
|
||||
'entity': await _fetch_child_rows(env, 'entity', 'entity', world_id),
|
||||
'snapshot_type': snapshot_type,
|
||||
'snapshot_date': curDateString(),
|
||||
}
|
||||
snapshot_json = json.dumps(snap_data, ensure_ascii=False)
|
||||
name = params.get('name')
|
||||
if not name:
|
||||
name = str(world.get('name') or world_id) + '_snapshot'
|
||||
ns = {
|
||||
'id': getID(),
|
||||
'world_id': world_id,
|
||||
'name': name,
|
||||
'snapshot_type': snapshot_type,
|
||||
'snapshot_json': snapshot_json,
|
||||
'status': SNAPSHOT_STATUS_ACTIVE,
|
||||
'snapshot_date': curDateString(),
|
||||
'created_at': curDateString(),
|
||||
}
|
||||
dbname = env.get_module_dbname('world_snapshot')
|
||||
db = DBPools()
|
||||
page = int(ns.get('page', 1) or 1)
|
||||
page_size = int(ns.get('rows', ns.get('pagerows', 20)) or 20)
|
||||
conds = ['1=1']
|
||||
params = {}
|
||||
world_id = ns.get('world_id', '')
|
||||
snapshot_date = ns.get('snapshot_date', '')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.C('world_snapshot', ns)
|
||||
return {'success': True, 'id': ns['id'], 'snapshot_type': snapshot_type,
|
||||
'snapshot_date': ns['snapshot_date']}
|
||||
|
||||
|
||||
async def get_snapshot(params):
|
||||
"""Get one snapshot record by id."""
|
||||
params = params or {}
|
||||
snapshot_id = params.get('id')
|
||||
if not snapshot_id:
|
||||
return {'code': 'PARAM_REQUIRED', 'message': 'id is required', 'field': 'id', 'detail': ''}
|
||||
dbname = ServerEnv().get_module_dbname('world_snapshot')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.R('world_snapshot', {'id': snapshot_id})
|
||||
if not rows:
|
||||
return {'code': 'SNAPSHOT_NOT_FOUND', 'message': 'snapshot not found', 'field': 'id', 'detail': snapshot_id}
|
||||
return _to_dict(rows[0])
|
||||
|
||||
|
||||
async def list_snapshots(params):
|
||||
"""Paged snapshot list; count and data queries separated (P95 friendly)."""
|
||||
params = params or {}
|
||||
try:
|
||||
page = int(params.get('page') or 1)
|
||||
except Exception:
|
||||
page = 1
|
||||
try:
|
||||
page_size = int(params.get('page_size') or params.get('rows') or params.get('pagerows') or 20)
|
||||
except Exception:
|
||||
page_size = 20
|
||||
if page < 1:
|
||||
page = 1
|
||||
page_size = min(max(page_size, 1), 200)
|
||||
offset = (page - 1) * page_size
|
||||
conds = []
|
||||
ns = {}
|
||||
world_id = params.get('world_id')
|
||||
if world_id:
|
||||
conds.append('world_id = ${world_id}$')
|
||||
params['world_id'] = world_id
|
||||
if snapshot_date:
|
||||
conds.append('snapshot_date = ${snapshot_date}$')
|
||||
params['snapshot_date'] = snapshot_date
|
||||
where = ' and '.join(conds)
|
||||
offset = (page - 1) * page_size
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
cnt = await sor.sqlExe('SELECT COUNT(*) AS cnt FROM world_snapshot WHERE ' + where, params)
|
||||
total = cnt[0]['cnt'] if cnt else 0
|
||||
rows = await sor.sqlExe('SELECT id, world_id, name, snapshot_type, status, snapshot_date, created_at FROM world_snapshot WHERE ' + where + ' ORDER BY created_at DESC LIMIT ' + str(offset) + ', ' + str(page_size), params)
|
||||
return {'success': True, 'total': total, 'rows': rows, 'page': page, 'page_size': page_size}
|
||||
|
||||
|
||||
async def get_snapshot(ns):
|
||||
sid = ns.get('id', '')
|
||||
if not sid:
|
||||
return {'success': False, 'message': 'id 不能为空'}
|
||||
ns['world_id'] = world_id
|
||||
status = params.get('status')
|
||||
if status:
|
||||
conds.append('status = ${status}$')
|
||||
ns['status'] = status
|
||||
snapshot_type = params.get('snapshot_type')
|
||||
if snapshot_type:
|
||||
conds.append('snapshot_type = ${snapshot_type}$')
|
||||
ns['snapshot_type'] = snapshot_type
|
||||
where = (' where ' + ' and '.join(conds)) if conds else ''
|
||||
dbname = ServerEnv().get_module_dbname('world_snapshot')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
rows = await sor.R('world_snapshot', {'id': sid})
|
||||
if not rows:
|
||||
return {'success': False, 'message': '快照不存在'}
|
||||
return {'success': True, 'data': rows[0]}
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
cnt = await sor.sqlExe('select count(*) as cnt from world_snapshot' + where, ns)
|
||||
total = int(cnt[0].cnt) if cnt else 0
|
||||
rows = await sor.sqlExe(
|
||||
'select id, world_id, name, snapshot_type, status, snapshot_date, created_at'
|
||||
' from world_snapshot' + where +
|
||||
' order by created_at desc limit ' + str(page_size) + ' offset ' + str(offset),
|
||||
ns)
|
||||
return {'list': [_to_dict(r) for r in (rows or [])], 'total': total,
|
||||
'page': page, 'page_size': page_size}
|
||||
|
||||
|
||||
async def create_snapshot(ns):
|
||||
world_id = ns.get('world_id', '')
|
||||
name = ns.get('name', '')
|
||||
snapshot_type = ns.get('snapshot_type', '0')
|
||||
status = ns.get('status', '0')
|
||||
snapshot_date = ns.get('snapshot_date') or curDateString()
|
||||
if not world_id or not name:
|
||||
return {'success': False, 'message': 'world_id 和 name 不能为空'}
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
worlds = await sor.R('world', {'id': world_id})
|
||||
scenes = await sor.R('scene', {'world_id': world_id})
|
||||
entities = await sor.R('entity', {'world_id': world_id})
|
||||
snapshot_data = {'world': worlds or [], 'scenes': scenes or [], 'entities': entities or []}
|
||||
rec = {'id': getID(), 'world_id': world_id, 'name': name, 'snapshot_type': snapshot_type,
|
||||
'snapshot_json': json.dumps(snapshot_data, ensure_ascii=False, default=str),
|
||||
'status': status, 'snapshot_date': snapshot_date, 'created_at': timestampstr()}
|
||||
await sor.C('world_snapshot', rec)
|
||||
return {'success': True, 'id': rec['id'], 'message': '快照生成成功'}
|
||||
|
||||
|
||||
async def update_snapshot(ns):
|
||||
sid = ns.get('id', '')
|
||||
if not sid:
|
||||
return {'success': False, 'message': 'id 不能为空'}
|
||||
async def update_snapshot(params):
|
||||
"""Partial update of a snapshot record (id required)."""
|
||||
params = params or {}
|
||||
snapshot_id = params.get('id')
|
||||
if not snapshot_id:
|
||||
return {'code': 'PARAM_REQUIRED', 'message': 'id is required', 'field': 'id', 'detail': ''}
|
||||
upd = {}
|
||||
for k in ('name', 'world_id', 'snapshot_type', 'status', 'snapshot_date'):
|
||||
if k in ns and ns[k] is not None and ns[k] != '':
|
||||
upd[k] = ns[k]
|
||||
for k, v in params.items():
|
||||
if k in ('id', 'created_at', 'snapshot_json'):
|
||||
continue
|
||||
if k.endswith('_text'):
|
||||
continue
|
||||
if v is None or v == '':
|
||||
continue
|
||||
upd[k] = v
|
||||
if not upd:
|
||||
return {'success': False, 'message': '无更新字段'}
|
||||
upd['id'] = sid
|
||||
return {'code': 'NO_FIELDS_TO_UPDATE', 'message': 'no fields to update', 'field': '', 'detail': ''}
|
||||
upd['id'] = snapshot_id
|
||||
dbname = ServerEnv().get_module_dbname('world_snapshot')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.U('world_snapshot', upd)
|
||||
return {'success': True, 'id': sid}
|
||||
return {'success': True, 'id': snapshot_id}
|
||||
|
||||
|
||||
async def delete_snapshot(ns):
|
||||
sid = ns.get('id', '')
|
||||
if not sid:
|
||||
return {'success': False, 'message': 'id 不能为空'}
|
||||
async def delete_snapshot(params):
|
||||
"""Delete a snapshot record by id."""
|
||||
params = params or {}
|
||||
snapshot_id = params.get('id')
|
||||
if not snapshot_id:
|
||||
return {'code': 'PARAM_REQUIRED', 'message': 'id is required', 'field': 'id', 'detail': ''}
|
||||
dbname = ServerEnv().get_module_dbname('world_snapshot')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
await sor.D('world_snapshot', {'id': sid})
|
||||
return {'success': True, 'id': sid}
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.D('world_snapshot', {'id': snapshot_id})
|
||||
return {'success': True, 'id': snapshot_id}
|
||||
|
||||
|
||||
async def restore_snapshot(ns):
|
||||
sid = ns.get('id', ns.get('snapshot_id', ''))
|
||||
if not sid:
|
||||
return {'success': False, 'message': 'snapshot_id 不能为空'}
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
rows = await sor.R('world_snapshot', {'id': sid})
|
||||
if not rows:
|
||||
return {'success': False, 'message': '快照不存在'}
|
||||
snap = rows[0]
|
||||
try:
|
||||
data = json.loads(snap.get('snapshot_json') or '{}')
|
||||
except Exception:
|
||||
return {'success': False, 'message': '快照数据格式错误'}
|
||||
if not isinstance(data, dict):
|
||||
return {'success': False, 'message': '快照数据无效'}
|
||||
await sor.U('world_snapshot', {'id': sid, 'status': '1'})
|
||||
return {'success': True, 'id': sid, 'message': '快照恢复成功', 'restored': data}
|
||||
async def restore_snapshot(params):
|
||||
"""Validate snapshot json, mark snapshot restored, link world status.
|
||||
|
||||
|
||||
async def list_worlds_for_snapshot(params_kw=None):
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
rows = await sor.sqlExe('SELECT id AS value, name AS text FROM world ORDER BY name', {})
|
||||
return rows or []
|
||||
|
||||
|
||||
async def list_snapshot_types(params_kw=None):
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
rows = await sor.sqlExe("SELECT k AS value, v AS text FROM appcodes_kv WHERE parentid = 'snapshot_type' ORDER BY k", {})
|
||||
return rows or []
|
||||
|
||||
|
||||
async def list_snapshot_statuses(params_kw=None):
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
rows = await sor.sqlExe("SELECT k AS value, v AS text FROM appcodes_kv WHERE parentid = 'snapshot_status' ORDER BY k", {})
|
||||
return rows or []
|
||||
|
||||
|
||||
async def load_world_snapshot():
|
||||
World linkage goes through the world module's own set_world_mode()
|
||||
contract when available (keeps mode transition validation intact).
|
||||
"""
|
||||
params = params or {}
|
||||
snapshot_id = params.get('id')
|
||||
if not snapshot_id:
|
||||
return {'code': 'PARAM_REQUIRED', 'message': 'id is required', 'field': 'id', 'detail': ''}
|
||||
env = ServerEnv()
|
||||
env.list_snapshots = list_snapshots
|
||||
env.get_snapshot = get_snapshot
|
||||
dbname = env.get_module_dbname('world_snapshot')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.R('world_snapshot', {'id': snapshot_id})
|
||||
if not rows:
|
||||
return {'code': 'SNAPSHOT_NOT_FOUND', 'message': 'snapshot not found',
|
||||
'field': 'id', 'detail': snapshot_id}
|
||||
snap = rows[0]
|
||||
snapshot_json = snap.snapshot_json or ''
|
||||
try:
|
||||
data = json.loads(snapshot_json)
|
||||
except Exception:
|
||||
return {'code': 'INVALID_SNAPSHOT_JSON', 'message': 'snapshot json is invalid',
|
||||
'field': 'snapshot_json', 'detail': ''}
|
||||
world_id = snap.world_id
|
||||
await sor.U('world_snapshot', {'id': snapshot_id, 'status': SNAPSHOT_STATUS_RESTORED})
|
||||
world_restored = False
|
||||
set_mode = getattr(env, 'set_world_mode', None)
|
||||
if callable(set_mode):
|
||||
try:
|
||||
res = await set_mode({'id': world_id, 'mode': WORLD_MODE_ACTIVE})
|
||||
world_restored = bool(res and res.get('success'))
|
||||
except Exception:
|
||||
world_restored = False
|
||||
return {'success': True, 'id': snapshot_id, 'world_id': world_id,
|
||||
'world_restored': world_restored, 'data': data}
|
||||
|
||||
|
||||
async def list_worlds_for_snapshot():
|
||||
"""Dropdown source: worlds for the snapshot picker."""
|
||||
env = ServerEnv()
|
||||
for module in ('world', 'world_snapshot'):
|
||||
dbname = _module_dbname(env, module)
|
||||
if not dbname:
|
||||
continue
|
||||
try:
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe('select id as value, name as text from world order by created_at desc', {})
|
||||
result = [{'value': r.value, 'text': r.text} for r in (rows or [])]
|
||||
if result:
|
||||
return result
|
||||
except Exception:
|
||||
continue
|
||||
return []
|
||||
|
||||
|
||||
async def list_snapshot_types():
|
||||
return await _get_appcodes('snapshot_type')
|
||||
|
||||
|
||||
async def list_snapshot_statuses():
|
||||
return await _get_appcodes('snapshot_status')
|
||||
|
||||
|
||||
def load_world_snapshot(env=None):
|
||||
"""Register all world_snapshot functions to ServerEnv."""
|
||||
if env is None:
|
||||
env = ServerEnv()
|
||||
env.create_snapshot = create_snapshot
|
||||
env.create_snapshots = create_snapshot
|
||||
env.update_snapshot = update_snapshot
|
||||
env.update_snapshots = update_snapshot
|
||||
env.delete_snapshot = delete_snapshot
|
||||
env.delete_snapshots = delete_snapshot
|
||||
env.get_snapshot = get_snapshot
|
||||
env.list_snapshots = list_snapshots
|
||||
env.restore_snapshot = restore_snapshot
|
||||
env.list_worlds_for_snapshot = list_worlds_for_snapshot
|
||||
env.list_snapshot_types = list_snapshot_types
|
||||
env.list_snapshot_statuses = list_snapshot_statuses
|
||||
env.create_world_snapshot = create_snapshot
|
||||
env.create_world_snapshots = create_snapshot
|
||||
env.update_world_snapshot = update_snapshot
|
||||
env.update_world_snapshots = update_snapshot
|
||||
env.delete_world_snapshot = delete_snapshot
|
||||
env.delete_world_snapshots = delete_snapshot
|
||||
return env
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
debug('create_snapshot.dspy: START')
|
||||
result = await create_snapshot(dict(params_kw))
|
||||
return result
|
||||
# world_snapshot create_snapshot.dspy
|
||||
# 创建世界快照:聚合 world/scene/entity 当前状态写入 snapshot_json
|
||||
try:
|
||||
result = await create_snapshot(params_kw)
|
||||
return result
|
||||
except Exception as e:
|
||||
debug(f'create_snapshot.dspy error: {format_exc()}')
|
||||
return {'code': 'INTERNAL_ERROR', 'message': str(e), 'field': '', 'detail': ''}
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
result = await list_snapshot_types({})
|
||||
return [{'value': '', 'text': '全部'}] + list(result or [])
|
||||
# world_snapshot get_search_snapshot_type.dspy
|
||||
# 下拉:快照类型([{value,text}],首项"全部")
|
||||
result = [{'value': '', 'text': '全部'}]
|
||||
try:
|
||||
options = await list_snapshot_types()
|
||||
if options:
|
||||
result = result + list(options)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug(f'get_search_snapshot_type.dspy error: {format_exc()}')
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
result = await list_snapshot_statuses({})
|
||||
return [{'value': '', 'text': '全部'}] + list(result or [])
|
||||
# world_snapshot get_search_status.dspy
|
||||
# 下拉:快照状态([{value,text}],首项"全部")
|
||||
result = [{'value': '', 'text': '全部'}]
|
||||
try:
|
||||
options = await list_snapshot_statuses()
|
||||
if options:
|
||||
result = result + list(options)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug(f'get_search_status.dspy error: {format_exc()}')
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
11
wwwroot/api/get_search_world.dspy
Normal file
11
wwwroot/api/get_search_world.dspy
Normal file
@ -0,0 +1,11 @@
|
||||
# world_snapshot get_search_world.dspy
|
||||
# 下拉:可选世界列表([{value,text}],首项"全部")
|
||||
result = [{'value': '', 'text': '全部'}]
|
||||
try:
|
||||
options = await list_worlds_for_snapshot()
|
||||
if options:
|
||||
result = result + list(options)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug(f'get_search_world.dspy error: {format_exc()}')
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -1,3 +1,8 @@
|
||||
debug('get_snapshot.dspy: START')
|
||||
result = await get_snapshot(dict(params_kw))
|
||||
return result
|
||||
# world_snapshot get_snapshot.dspy
|
||||
# 查询单条世界快照(id 必填)
|
||||
try:
|
||||
result = await get_snapshot(params_kw)
|
||||
return result
|
||||
except Exception as e:
|
||||
debug(f'get_snapshot.dspy error: {format_exc()}')
|
||||
return {'code': 'INTERNAL_ERROR', 'message': str(e), 'field': '', 'detail': ''}
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
debug('list_snapshots.dspy: START')
|
||||
result = await list_snapshots(dict(params_kw))
|
||||
return result
|
||||
# world_snapshot list_snapshots.dspy
|
||||
# 分页查询世界快照列表(world_id/status/snapshot_type 可选过滤)
|
||||
try:
|
||||
result = await list_snapshots(params_kw)
|
||||
return result
|
||||
except Exception as e:
|
||||
debug(f'list_snapshots.dspy error: {format_exc()}')
|
||||
return {'code': 'INTERNAL_ERROR', 'message': str(e), 'field': '', 'detail': ''}
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
debug('restore_snapshot.dspy: START')
|
||||
result = await restore_snapshot(dict(params_kw))
|
||||
return result
|
||||
# world_snapshot restore_snapshot.dspy
|
||||
# 恢复世界快照:校验 snapshot_json、标记已恢复、联动 world 状态
|
||||
try:
|
||||
result = await restore_snapshot(params_kw)
|
||||
return result
|
||||
except Exception as e:
|
||||
debug(f'restore_snapshot.dspy error: {format_exc()}')
|
||||
return {'code': 'INTERNAL_ERROR', 'message': str(e), 'field': '', 'detail': ''}
|
||||
|
||||
@ -1,2 +1,8 @@
|
||||
result = await delete_snapshot(params_kw)
|
||||
return result
|
||||
# world_snapshot snapshot_delete.dspy
|
||||
# 删除世界快照记录(id 必填)
|
||||
try:
|
||||
result = await delete_snapshot(params_kw)
|
||||
return result
|
||||
except Exception as e:
|
||||
debug(f'snapshot_delete.dspy error: {format_exc()}')
|
||||
return {'code': 'INTERNAL_ERROR', 'message': str(e), 'field': '', 'detail': ''}
|
||||
|
||||
@ -1,2 +1,8 @@
|
||||
result = await update_snapshot(params_kw)
|
||||
return result
|
||||
# world_snapshot snapshot_update.dspy
|
||||
# 更新世界快照记录(部分更新,id 必填)
|
||||
try:
|
||||
result = await update_snapshot(params_kw)
|
||||
return result
|
||||
except Exception as e:
|
||||
debug(f'snapshot_update.dspy error: {format_exc()}')
|
||||
return {'code': 'INTERNAL_ERROR', 'message': str(e), 'field': '', 'detail': ''}
|
||||
|
||||
@ -1,22 +1,13 @@
|
||||
{
|
||||
"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_snapshot_content", "options": {"url": "{{entire_url('/world_snapshot/world_snapshot')}}"}, "mode": "replace"}],
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "快照列表", "fontSize": "16px"}},
|
||||
{"widgettype": "Text", "options": {"label": "查看、编辑、删除世界快照", "fontSize": "12px", "color": "#888888"}}
|
||||
]},
|
||||
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.world_snapshot_content", "options": {"url": "{{entire_url('/world_snapshot/api/create_snapshot.dspy')}}", "method": "GET"}, "mode": "replace"}],
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "生成快照", "fontSize": "16px"}},
|
||||
{"widgettype": "Text", "options": {"label": "聚合世界/场景/实体当前状态", "fontSize": "12px", "color": "#888888"}}
|
||||
]}
|
||||
]},
|
||||
{"widgettype": "VBox", "id": "world_snapshot_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
|
||||
]
|
||||
}
|
||||
{"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_snapshot_content",
|
||||
"options":{"url":"{{entire_url('/world_snapshot/world_snapshot/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.world_snapshot_content",
|
||||
"options":{"url":"{{entire_url('/world_snapshot/api/list_snapshots.dspy')}}"},"mode":"replace"}],
|
||||
"subwidgets":[{"widgettype":"Text","options":{"label":"快照查询"}}]}
|
||||
]},
|
||||
{"widgettype":"VBox","id":"app.world_snapshot_content","options":{"width":"100%","flex":"1","marginTop":"20px"}}]}
|
||||
|
||||
7
wwwroot/menu.ui
Normal file
7
wwwroot/menu.ui
Normal file
@ -0,0 +1,7 @@
|
||||
{"widgettype":"VBox","options":{"width":"100%","height":"100%"},"subwidgets":[
|
||||
{"widgettype":"Text","options":{"label":"世界快照","fontSize":"18px"}},
|
||||
{"widgettype":"Menu","options":{"items":[
|
||||
{"name":"snapshot_list","label":"快照列表","url":"{{entire_url('/world_snapshot/world_snapshot/index.ui')}}"},
|
||||
{"name":"snapshot_query","label":"快照查询","url":"{{entire_url('/world_snapshot/api/list_snapshots.dspy')}}"}
|
||||
]}}
|
||||
]}
|
||||
Loading…
x
Reference in New Issue
Block a user