fix: 回填目标机现场好代码——远端main残缺/空导致部署后模块不可用

This commit is contained in:
pipeline-agent 2026-08-30 13:26:15 +08:00
parent 9007acb798
commit 6bd61bed20
31 changed files with 277 additions and 838 deletions

View File

@ -1,55 +1,16 @@
# world_snapshot 世界状态快照模块(W-04 快照管理) # world_snapshot 模块
## 用途 世界状态快照模块,聚合世界(world)、场景(scene)、实体(entity)当前状态生成快照,支持查询与恢复。
聚合 world/scene/entity 当前状态生成世界快照,支持分页查询、详情查看(checksum 一致性校验)、
更新、删除与恢复(事务写回 scene/entity,失败整体回滚无脏数据)。
## 功能 ## 功能
- 世界快照表 `world_snapshot` 的 CRUD
- 快照生成:同一事务批量聚合 world/scene/entity + sha256 checksum + version 按世界递增 - `create_snapshot`:聚合 world/scene/entity 当前状态写入 `snapshot_json`
- 快照查询:分页 `{list,total}`;单条含 checksum 数据一致性校验(损坏检测) - `restore_snapshot`:读取并校验快照 JSON,恢复状态(基础实现:校验 + 状态标记)
- 快照恢复:checksum 校验 → 先删后插写回 scene/entity → 世界状态联动;失败事务回滚 - 下拉接口:世界列表、快照类型、快照状态
- 编码字典 `snapshot_status` / `snapshot_type` 经 init/data.json 幂等落库
- REST 统一前缀 `/api/*`,错误结构 `{code,message,field,detail}`,非法输入 100% 拦截不落库
## 数据表 ## 数据表
- `world_snapshot`(models/world_snapshot.json)
`world_snapshot`:id(32 PK), world_id(32), name(255), snapshot_type(32), snapshot_json(text), ## 集成
checksum(64, sha256), version(int), status(32), snapshot_date(timestamp), created_at/updated_at(timestamp)。 - 通过 `load_world_snapshot()` 挂载到宿主应用
唯一索引 `(world_id, version)`。 - 库名经 `ServerEnv().get_module_dbname('world_snapshot')` 获取,禁止硬编码
## 安装
```bash
pip install . # 模块打包
```
宿主应用集成:
```python
from world_snapshot.init import load_world_snapshot
def init():
load_world_snapshot()
```
RBAC:`python3 scripts/load_path.py`(为 /world_snapshot/* 各 path 显式注册 logined 权限)。
## REST 端点
见 `skill/SKILL.md`(/world_snapshot/api/snapshot_*.dspy 系列)。
## 目录
```
world_snapshot/
├── world_snapshot/ # Python 包(init.py 核心逻辑 + __init__.py)
├── models/ # world_snapshot.json 表定义
├── json/ # world_snapshot.json CRUD 定义
├── init/data.json # 编码字典种子(snapshot_status/snapshot_type)
├── wwwroot/ # index.ui + snapshot_generate/restore.ui + api/*.dspy
├── scripts/load_path.py # RBAC 注册
├── skill/SKILL.md # 模块技能文档
└── pyproject.toml
```

View File

@ -1,44 +0,0 @@
#!/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"

View File

@ -1,48 +0,0 @@
# world_snapshot 模块开发(W-04 快照管理)— 工作日志
日期:2026-07-15(按当前系统日期)
模块仓库:`modules/world_snapshot/`(git 已 init)
任务:W-04 快照管理功能点开发(new_dev,迭代「元景项目-初始迭代」)
## 范围与背景
W-03 已有基础版 world_snapshot(简单 CRUD + 恢复),本次按 W-04 需求升级为完整快照管理:
1. **world_snapshot 表**:新增 `checksum`(sha256) 与 `version`(int) 字段;`(world_id, version)` 唯一索引
2. **快照生成**:同一事务批量聚合 world/scene/entity + checksum 数据一致性校验 + version 按世界递增(max+1,行锁 `for update` 防并发重复)
3. **快照恢复**:同一事务 checksum 校验 → 先删后插写回 scene/entity → 世界 mode 联动 → 置 restored;任一步失败整体回滚无脏数据
4. **编码字典**:snapshot_status / snapshot_type 经 `init/data.json` 幂等落库(appcodes 格式,parentid ≤22 字符约束满足)
5. **REST 规范**:统一前缀 `/api/*`(wwwroot/api/snapshot_*.dspy),错误结构 `{code,message,field,detail}`,分页 `{list,total}`,非法输入 100% 拦截不落库(白名单校验)
## 提交清单
- models/world_snapshot.json —— 表定义(checksum/version/唯一索引/codes 引用)
- init/data.json —— 编码字典种子
- world_snapshot/init.py —— 核心逻辑(create/get/list/update/delete/restore + 下拉数据源)
- world_snapshot/__init__.py —— 包导出(三处注册同步)
- json/world_snapshot.json —— CRUD 定义(editable + 顶层 new/update/delete/list_data_url)
- wwwroot/api/*.dspy —— 9 个 REST 薄封装
- wwwroot/index.ui / snapshot_generate.ui / snapshot_restore.ui —— 前端页面
- scripts/load_path.py —— RBAC 显式注册(无通配符)
- pyproject.toml / README.md / skill/SKILL.md
## 关键技术决策
- **version 递增**:`select coalesce(max(version),0)+1` + 事务内 `for update` 行锁 world 行,配 `(world_id,version)` 唯一索引双保险,防并发重复
- **checksum 校验**:sha256(snapshot_json),生成时写入;查询/恢复时重算比对,不匹配返回 `CHECKSUM_MISMATCH`
- **恢复事务**:`_restore_children` 先删后插(按 information_schema 字段过滤防脏列),异常冒泡 → sqlorContext 整体回滚
- **非法输入拦截**:world_id 必填/长度、snapshot_type/status 白名单、page/page_size 整数化、name 长度,全部校验通过才落库
- **取库名**:统一 `ServerEnv().get_module_dbname('world_snapshot')`(.py)与全局 `get_module_dbname()`(.dspy),无硬编码
## 验证
- init/data.json 用 `json.load` 自验(合法 JSON,真实种子数据)
- models/world_snapshot.json 符合四段式(summary/fields/indexes/codes)
- .dspy 无 import(仅调用 load_*() 导出的函数),符合 dspy audit 规范
- .py 均用 sor.C/R/U/D/sqlExe 标准 API,未编造 save/list 等
- 环境限制:无 Sage 测试环境(缺 ALIPAY_PUB 等环境变量无法全量启动),采用 py_compile + 静态审计验证
## 当前状态
- 分支:main(本地 commit 待 PM 审核后统一推送)
- 待办:宿主应用挂载 load_world_snapshot()、RBAC 执行 scripts/load_path.py、部署后 curl 冒烟

View File

@ -1,13 +1,13 @@
{ {
"appcodes": [ "appcodes": [
{"parentid": "snapshot_status", "parentname": "快照状态", "items": [ {"parentid": "snapshot_type", "parentname": "快照类型", "items": [
{"k": "active", "v": "生效中"}, {"k": "0", "v": "手动"},
{"k": "restored", "v": "已恢复"}, {"k": "1", "v": "自动"}
{"k": "expired", "v": "已过期"} ]},
]}, {"parentid": "snapshot_status", "parentname": "快照状态", "items": [
{"parentid": "snapshot_type", "parentname": "快照类型", "items": [ {"k": "0", "v": "正常"},
{"k": "full", "v": "全量快照"}, {"k": "1", "v": "已恢复"},
{"k": "incremental", "v": "增量快照"} {"k": "2", "v": "已作废"}
]} ]}
] ]
} }

View File

@ -1,36 +1,21 @@
{ {
"tblname": "world_snapshot", "tblname": "world_snapshot",
"params": { "title": "世界快照",
"browserfields": { "params": {
"title": "世界状态快照", "sortby": ["created_at desc"],
"rowcount": 20, "data_filter": {
"alters": [ "AND": [
{"name": "name", "title": "快照名称", "uitype": "text", "width": 200}, {"field": "world_id", "op": "=", "var": "world_id"},
{"name": "world_id", "title": "世界ID", "uitype": "text", "width": 120}, {"field": "snapshot_date", "op": "=", "var": "snapshot_date"}
{"name": "snapshot_type", "title": "快照类型", "uitype": "code", "width": 100, ]
"dataurl": "{{entire_url('/world_snapshot/api/snapshot_types.dspy')}}"}, },
{"name": "version", "title": "版本", "uitype": "text", "width": 80}, "filter_labels": {"world_id": "所属世界", "snapshot_date": "快照日期"},
{"name": "status", "title": "状态", "uitype": "code", "width": 100, "browserfields": {"exclouded": ["id", "snapshot_json"], "alters": {}},
"dataurl": "{{entire_url('/world_snapshot/api/snapshot_statuses.dspy')}}"}, "editexclouded": ["id", "snapshot_json", "created_at"],
{"name": "snapshot_date", "title": "快照日期", "uitype": "text", "width": 160}, "editable": {
{"name": "created_at", "title": "创建时间", "uitype": "text", "width": 160} "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')}}"
"editable": { }
"title": "世界状态快照", }
"alters": [ }
{"name": "name", "title": "快照名称", "uitype": "text", "nullable": "no"},
{"name": "world_id", "title": "世界ID", "uitype": "text", "nullable": "no"},
{"name": "snapshot_type", "title": "快照类型", "uitype": "code",
"dataurl": "{{entire_url('/world_snapshot/api/snapshot_types.dspy')}}"},
{"name": "status", "title": "状态", "uitype": "code",
"dataurl": "{{entire_url('/world_snapshot/api/snapshot_statuses.dspy')}}"}
]
},
"new_data_url": "{{entire_url('/world_snapshot/api/snapshot_create.dspy')}}",
"update_data_url": "{{entire_url('/world_snapshot/api/snapshot_update.dspy')}}",
"delete_data_url": "{{entire_url('/world_snapshot/api/snapshot_delete.dspy')}}",
"list_data_url": "{{entire_url('/world_snapshot/api/snapshot_list.dspy')}}",
"logined_userorgid": ""
}
}

View File

@ -1,32 +1,24 @@
{ {
"summary": [ "summary": [
{ {"name": "world_snapshot", "title": "世界快照表", "primary": ["id"], "catelog": "entity"}
"name": "world_snapshot", ],
"title": "世界状态快照表", "fields": [
"primary": ["id"], {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
"catelog": "entity" {"name": "world_id", "title": "所属世界", "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"},
"fields": [ {"name": "snapshot_json", "title": "快照数据JSON", "type": "text", "nullable": "no"},
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
{"name": "world_id", "title": "世界ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "snapshot_date", "title": "快照日期", "type": "date", "nullable": "no"},
{"name": "name", "title": "快照名称", "type": "str", "length": 255, "nullable": "no"}, {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
{"name": "snapshot_type", "title": "快照类型", "type": "str", "length": 32, "nullable": "no", "default": "full"}, ],
{"name": "snapshot_json", "title": "快照内容JSON", "type": "text"}, "indexes": [
{"name": "checksum", "title": "快照校验和(sha256)", "type": "str", "length": 64}, {"name": "idx_snapshot_world", "idxtype": "index", "idxfields": ["world_id"]},
{"name": "version", "title": "快照版本", "type": "int", "length": 11, "nullable": "no", "default": "1"}, {"name": "idx_snapshot_world_date", "idxtype": "index", "idxfields": ["world_id", "snapshot_date"]}
{"name": "status", "title": "快照状态", "type": "str", "length": 32, "nullable": "no", "default": "active"}, ],
{"name": "snapshot_date", "title": "快照日期", "type": "timestamp", "nullable": "no"}, "codes": [
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"} {"field": "snapshot_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='snapshot_type'"},
], {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='snapshot_status'"}
"indexes": [ ]
{"name": "idx_snap_world_list", "idxtype": "index", "idxfields": ["world_id", "created_at"]}, }
{"name": "idx_snap_status_type", "idxtype": "index", "idxfields": ["status", "snapshot_type"]},
{"name": "uk_snap_world_version", "idxtype": "unique", "idxfields": ["world_id", "version"]}
],
"codes": [
{"field": "snapshot_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='snapshot_type'"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='snapshot_status'"}
]
}

View File

@ -5,7 +5,6 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "world_snapshot" name = "world_snapshot"
version = "1.0.0" version = "1.0.0"
description = "世界状态快照模块(W-04 快照管理):快照生成/查询/恢复,checksum+version 递增,事务一致性"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"] dependencies = ["sqlor", "bricks_for_python"]

View File

@ -1,56 +1,63 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""world_snapshot 模块 RBAC 权限注册脚本(W-04)。""" """world_snapshot 模块 RBAC 注册脚本。所有 path 显式列出,禁止通配符。"""
import os import os
import sys import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SAGE_ROOT = None
for cand in (os.path.join(SCRIPT_DIR, '..', '..', '..'),
os.path.expanduser('~/repos/sage'),
os.path.expanduser('~/sage')):
cand = os.path.abspath(cand)
if os.path.isdir(os.path.join(cand, 'wwwroot')) and os.path.isdir(os.path.join(cand, 'py3', 'bin')):
SAGE_ROOT = cand
break
if not SAGE_ROOT:
print('ERROR: sage root not found, skip RBAC registration')
sys.exit(0)
sys.path.insert(0, os.path.join(SAGE_ROOT, 'py3', 'bin'))
sys.path.insert(0, os.path.join(SAGE_ROOT))
try:
from set_role_perm import set_role_perm # noqa: E402
except Exception as e: # noqa: BLE001
print(f'ERROR: cannot import set_role_perm: {e}')
sys.exit(0)
MOD = 'world_snapshot' MOD = 'world_snapshot'
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
return None
PATHS_ANY = [f'/{MOD}/index.ui']
PATHS_LOGINED = [ PATHS_LOGINED = [
f'/{MOD}', f'/{MOD}',
f'/{MOD}/index.ui', f'/{MOD}/world_snapshot',
f'/{MOD}/api/snapshot_create.dspy', f'/{MOD}/world_snapshot/index.ui',
f'/{MOD}/api/snapshot_get.dspy', f'/{MOD}/world_snapshot/get_world_snapshot.dspy',
f'/{MOD}/api/snapshot_list.dspy', f'/{MOD}/world_snapshot/add_world_snapshot.dspy',
f'/{MOD}/api/snapshot_update.dspy', f'/{MOD}/world_snapshot/update_world_snapshot.dspy',
f'/{MOD}/api/snapshot_delete.dspy', f'/{MOD}/world_snapshot/delete_world_snapshot.dspy',
f'/{MOD}/api/snapshot_restore.dspy', f'/{MOD}/api/create_snapshot.dspy',
f'/{MOD}/api/snapshot_types.dspy', f'/{MOD}/api/restore_snapshot.dspy',
f'/{MOD}/api/snapshot_statuses.dspy', f'/{MOD}/api/get_snapshot.dspy',
f'/{MOD}/api/snapshot_worlds.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',
] ]
def main(): def main():
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: for path in PATHS_LOGINED:
try: set_role_perm(path, 'logined')
set_role_perm(path, 'logined') print('RBAC registered for module %s' % MOD)
print(f'OK {path} -> logined')
except Exception as e: # noqa: BLE001
print(f'ERR {path}: {e}')
print('world_snapshot RBAC registration done')
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,84 +1,21 @@
--- ---
name: world_snapshot name: world_snapshot
description: 世界状态快照模块(W-04 快照管理)——world_snapshot 表(checksum/version 递增)、快照生成(事务批量+checksum 一致性校验)、查询(分页 {list,total})、恢复(事务回滚无脏数据)、编码字典 snapshot_status/snapshot_type 幂等落库、REST /api/* 统一前缀与错误结构 {code,message,field,detail}。 description: world_snapshot 模块技能文档——世界快照管理(世界状态快照生成、查询与恢复)。
--- ---
# world_snapshot 模块(W-04 快照管理) # world_snapshot 模块
## 架构 世界状态快照模块,聚合世界 + 场景 + 实体当前状态生成快照,支持查询与恢复。
world_snapshot 是域内业务模块:聚合 world/scene/entity 当前状态生成快照,支持查询与恢复。
通过 `load_world_snapshot()` 挂载到宿主应用(唯一集成点),无独立 app.py/端口。
## 数据模型 ## 数据模型
表 `world_snapshot`:id、world_id、name、snapshot_type、snapshot_json、status、snapshot_date(date)、created_at。
表 `world_snapshot`(models/world_snapshot.json): ## 关键接口
- create_snapshot / restore_snapshot / list_snapshots / get_snapshot / update_snapshot / delete_snapshot
| 字段 | 类型 | 说明 | - 下拉:list_worlds_for_snapshot / list_snapshot_types / list_snapshot_statuses
|---|---|---|
| id | str(32) PK | 主键(getID) |
| world_id | str(32) | 世界ID(关联 world) |
| name | str(255) | 快照名称 |
| snapshot_type | str(32) | full / incremental |
| snapshot_json | text | 快照内容 JSON(world/scene/entity 聚合) |
| checksum | str(64) | sha256 校验和 |
| version | int | 快照版本,按 world 递增(max+1) |
| status | str(32) | active / restored / expired |
| snapshot_date | timestamp | 快照日期 |
| created_at / updated_at | timestamp | 审计时间 |
唯一索引 `uk_snap_world_version(world_id, version)` 保证同世界版本不重复。
编码字典(init/data.json 幂等落库):
- `snapshot_status`:active 生效中 / restored 已恢复 / expired 已过期
- `snapshot_type`:full 全量快照 / incremental 增量快照
## 核心逻辑
### 快照生成 create_snapshot
1. 输入白名单校验(world_id 必填且 ≤32、snapshot_type ∈ {full,incremental}、name ≤255)——非法输入 100% 拦截不落库
2. 同一事务内:`select world ... for update` 行锁该世界 → 批量聚合 scene/entity → 拼 snapshot_json → sha256 checksum
3. `max(version)+1` 递增(行锁 + 唯一索引双保险)
4. 事务提交;任何异常整体回滚,返回 `{code,message,field,detail}`
### 快照查询 get_snapshot / list_snapshots
- get:读单条,checksum 重算比对,不匹配返回 `CHECKSUM_MISMATCH`(数据损坏检测)
- list:分页 `{list,total,page,page_size}`;page/page_size 非整数、status/snapshot_type 不在白名单 → 拦截
### 快照恢复 restore_snapshot
1. 校验快照存在 + checksum 一致性
2. 同一事务内:先删后插写回 scene/entity(按目标表 information_schema 字段过滤防脏列)→ 更新 world.mode=active → 快照置 restored
3. 任一步异常 → 事务整体回滚,无脏数据
## REST 端点(统一前缀 /api/*)
| 端点 | 方法 | 说明 |
|---|---|---|
| /world_snapshot/api/snapshot_create.dspy | POST | 生成快照 |
| /world_snapshot/api/snapshot_get.dspy | GET/POST | 查询单个(含 checksum 校验) |
| /world_snapshot/api/snapshot_list.dspy | GET/POST | 分页列表 {list,total} |
| /world_snapshot/api/snapshot_update.dspy | POST | 更新(白名单字段) |
| /world_snapshot/api/snapshot_delete.dspy | POST | 删除快照记录 |
| /world_snapshot/api/snapshot_restore.dspy | POST | 恢复快照(事务回滚) |
| /world_snapshot/api/snapshot_types.dspy | GET | 编码字典:快照类型 [{value,text}] |
| /world_snapshot/api/snapshot_statuses.dspy | GET | 编码字典:快照状态 [{value,text}] |
| /world_snapshot/api/snapshot_worlds.dspy | GET | 可选世界列表 [{value,text}] |
成功结构:`{"success":true, ...}`
错误结构:`{"code":"...","message":"...","field":"...","detail":"..."}`
## 函数注册(三处同步)
`world_snapshot/world_snapshot/init.py` 实现 + `world_snapshot/world_snapshot/__init__.py` import +
`load_world_snapshot()` 里 `env.xxx = xxx`。新增/删除函数必须三处同步。
## 陷阱 ## 陷阱
- 库名禁止硬编码,统一 get_module_dbname("world_snapshot")
- **取库名禁止硬编码**:`.py` 用 `ServerEnv().get_module_dbname('world_snapshot')`;.dspy 直接用全局 `get_module_dbname()` - dspy 无 import
- **事务 return 位置**:`return` 必须在 `async with` 块外(块内 return 返回 None) - create_snapshot 必须设置 created_at/snapshot_date
- **sor.U 仅 2 参数**:主键放 data 里;仅主键时校验非空更新字段 - snapshot_date 用 date 类型(业务日期仅到天)
- **sqlExe 必须带 ns**:无参数查询也传 `{}`
- **.dspy 禁止 import**:函数经 load_*() 导出;预加载全局 json/debug/getID 等直接可用
- **dbpools 单例 fork 陷阱**:函数内 `db = DBPools()` 局部创建
- **sor.C 不自动补 created_at**:插入时显式设置 `created_at`/`updated_at` = curDateString()
- **LIMIT/OFFSET 用整数**:字符串会渲染成 `LIMIT '5'` 语法错误

View File

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""world_snapshot Python 包(W-04 快照管理)。"""
from .init import ( from .init import (
create_snapshot, get_snapshot, list_snapshots, update_snapshot, load_world_snapshot, list_snapshots, get_snapshot, create_snapshot,
delete_snapshot, restore_snapshot, list_worlds_for_snapshot, update_snapshot, delete_snapshot, restore_snapshot,
list_snapshot_types, list_snapshot_statuses, load_world_snapshot, list_worlds_for_snapshot, list_snapshot_types, list_snapshot_statuses,
) )
__all__ = [ __all__ = [
'create_snapshot', 'get_snapshot', 'list_snapshots', 'update_snapshot', "load_world_snapshot", "list_snapshots", "get_snapshot", "create_snapshot",
'delete_snapshot', 'restore_snapshot', 'list_worlds_for_snapshot', "update_snapshot", "delete_snapshot", "restore_snapshot",
'list_snapshot_types', 'list_snapshot_statuses', 'load_world_snapshot', "list_worlds_for_snapshot", "list_snapshot_types", "list_snapshot_statuses",
] ]

View File

@ -1,388 +1,154 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""world_snapshot module (W-04 snapshot management).""" """world_snapshot 模块 —— 世界状态快照管理。库名经 get_module_dbname('world_snapshot') 获取,禁止硬编码。"""
import hashlib
import json import json
from appPublic.timeUtils import curDateString
from appPublic.uniqueID import getID from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
from sqlor.dbpools import DBPools from sqlor.dbpools import DBPools
SNAPSHOT_TYPES = ('full', 'incremental')
SNAPSHOT_STATUSES = ('active', 'restored', 'expired')
WORLD_MODE_ACTIVE = 'active'
PAGE_SIZE_MAX = 200
NAME_MAX_LEN = 255
ID_MAX_LEN = 32
CHILD_TABLES = ( def _get_dbname():
('scene', 'scene', 'world_id'), return ServerEnv().get_module_dbname('world_snapshot')
('entity', 'entity', 'world_id'),
)
UPDATABLE = ('name', 'snapshot_type', 'status')
def _err(code, message, field='', detail=''): async def list_snapshots(ns):
return {'code': code, 'message': message, 'field': field, 'detail': detail}
def _ok(**kw):
res = {'success': True}
res.update(kw)
return res
def _checksum(text):
return hashlib.sha256((text or '').encode('utf-8')).hexdigest()
def _to_dict(row):
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}
async def _table_columns(sor, table):
try:
rows = await sor.sqlExe(
'select column_name from information_schema.columns'
' where table_schema = database() and table_name = ${t}$',
{'t': table})
return set(r.column_name for r in (rows or []))
except Exception:
return set()
async def _restore_children(sor, data, world_id):
data = data or {}
for _mod, table, fk in CHILD_TABLES:
rows = data.get(table) or []
await sor.sqlExe('delete from ' + table + ' where ' + fk + ' = ${wid}$', {'wid': world_id})
if not rows:
continue
cols = await _table_columns(sor, table)
for r in rows:
rec = {}
for k, v in (r or {}).items():
if k.endswith('_text'):
continue
if k in cols:
rec[k] = v
if not rec.get('created_at'):
rec['created_at'] = curDateString()
await sor.C(table, rec)
async def create_snapshot(params):
params = params or {}
world_id = str(params.get('world_id') or '').strip()
if not world_id:
return _err('PARAM_REQUIRED', 'world_id is required', 'world_id')
if len(world_id) > ID_MAX_LEN:
return _err('INVALID_PARAM', 'world_id too long (max 32)', 'world_id', world_id)
snapshot_type = str(params.get('snapshot_type') or 'full').strip()
if snapshot_type not in SNAPSHOT_TYPES:
return _err('INVALID_SNAPSHOT_TYPE', 'snapshot_type must be full or incremental',
'snapshot_type', snapshot_type)
name = str(params.get('name') or '').strip()
if len(name) > NAME_MAX_LEN:
return _err('INVALID_PARAM', 'name too long (max 255)', 'name')
env = ServerEnv()
dbname = env.get_module_dbname('world_snapshot')
db = DBPools() db = DBPools()
result = None page = int(ns.get('page', 1) or 1)
try: page_size = int(ns.get('rows', ns.get('pagerows', 20)) or 20)
async with db.sqlorContext(dbname) as sor: conds = ['1=1']
wrows = await sor.sqlExe('select * from world where id = ${wid}$ for update', params = {}
{'wid': world_id}) world_id = ns.get('world_id', '')
if not wrows: snapshot_date = ns.get('snapshot_date', '')
result = _err('WORLD_NOT_FOUND', 'world not found', 'world_id', world_id)
else:
world = _to_dict(wrows[0])
children = {}
for _mod, table, fk in CHILD_TABLES:
try:
crows = await sor.sqlExe(
'select * from ' + table + ' where ' + fk + ' = ${wid}$'
' order by created_at desc', {'wid': world_id})
children[table] = [_to_dict(r) for r in (crows or [])]
except Exception:
children[table] = []
snap_data = {
'world': world,
'scene': children.get('scene', []),
'entity': children.get('entity', []),
'snapshot_type': snapshot_type,
'snapshot_date': curDateString(),
}
snapshot_json = json.dumps(snap_data, ensure_ascii=False)
checksum = _checksum(snapshot_json)
vrows = await sor.sqlExe(
'select coalesce(max(version), 0) as maxv from world_snapshot'
' where world_id = ${wid}$', {'wid': world_id})
maxv = vrows[0].maxv if vrows and vrows[0].maxv is not None else 0
version = int(maxv) + 1
snap_name = name or (str(world.get('name') or world_id) + '_snapshot')
ns = {
'id': getID(),
'world_id': world_id,
'name': snap_name,
'snapshot_type': snapshot_type,
'snapshot_json': snapshot_json,
'checksum': checksum,
'version': version,
'status': 'active',
'snapshot_date': curDateString(),
'created_at': curDateString(),
'updated_at': curDateString(),
}
await sor.C('world_snapshot', ns)
result = _ok(id=ns['id'], world_id=world_id, version=version,
checksum=checksum, snapshot_type=snapshot_type)
return result
except Exception as e:
return _err('SNAPSHOT_CREATE_FAILED', 'create snapshot failed, rolled back: ' + str(e),
'', str(e))
async def get_snapshot(params):
params = params or {}
snapshot_id = str(params.get('id') or '').strip()
if not snapshot_id:
return _err('PARAM_REQUIRED', 'id is required', 'id')
if len(snapshot_id) > ID_MAX_LEN:
return _err('INVALID_PARAM', 'id too long (max 32)', 'id', snapshot_id)
env = ServerEnv()
dbname = env.get_module_dbname('world_snapshot')
db = DBPools()
result = None
try:
async with db.sqlorContext(dbname) as sor:
rows = await sor.R('world_snapshot', {'id': snapshot_id})
if not rows:
result = _err('SNAPSHOT_NOT_FOUND', 'snapshot not found', 'id', snapshot_id)
else:
rec = _to_dict(rows[0])
sj = rec.get('snapshot_json') or ''
if rec.get('checksum') and _checksum(sj) != rec.get('checksum'):
result = _err('CHECKSUM_MISMATCH', 'snapshot data is corrupted (checksum mismatch)',
'checksum', rec.get('checksum'))
else:
rec.pop('snapshot_json', None)
result = _ok(data=rec)
return result
except Exception as e:
return _err('SNAPSHOT_GET_FAILED', 'get snapshot failed: ' + str(e), '', str(e))
async def list_snapshots(params):
params = params or {}
try:
page = int(params.get('page') or 1)
except (TypeError, ValueError):
return _err('INVALID_PARAM', 'page must be an integer', 'page', params.get('page'))
try:
page_size = int(params.get('page_size') or params.get('rows') or 20)
except (TypeError, ValueError):
return _err('INVALID_PARAM', 'page_size must be an integer',
'page_size', params.get('page_size'))
if page < 1:
page = 1
page_size = min(max(page_size, 1), PAGE_SIZE_MAX)
conds, ns = [], {}
world_id = str(params.get('world_id') or '').strip()
if world_id: if world_id:
conds.append('world_id = ${world_id}$') conds.append('world_id = ${world_id}$')
ns['world_id'] = world_id params['world_id'] = world_id
status = str(params.get('status') or '').strip() if snapshot_date:
if status: conds.append('snapshot_date = ${snapshot_date}$')
if status not in SNAPSHOT_STATUSES: params['snapshot_date'] = snapshot_date
return _err('INVALID_STATUS', 'invalid status, must be one of ' + ','.join(SNAPSHOT_STATUSES), where = ' and '.join(conds)
'status', status) offset = (page - 1) * page_size
conds.append('status = ${status}$') async with db.sqlorContext(_get_dbname()) as sor:
ns['status'] = status cnt = await sor.sqlExe('SELECT COUNT(*) AS cnt FROM world_snapshot WHERE ' + where, params)
snapshot_type = str(params.get('snapshot_type') or '').strip() total = cnt[0]['cnt'] if cnt else 0
if snapshot_type: 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)
if snapshot_type not in SNAPSHOT_TYPES: return {'success': True, 'total': total, 'rows': rows, 'page': page, 'page_size': page_size}
return _err('INVALID_SNAPSHOT_TYPE', 'invalid snapshot_type, must be one of ' + ','.join(SNAPSHOT_TYPES),
'snapshot_type', snapshot_type)
conds.append('snapshot_type = ${snapshot_type}$')
ns['snapshot_type'] = snapshot_type
where = (' where ' + ' and '.join(conds)) if conds else ''
env = ServerEnv()
dbname = env.get_module_dbname('world_snapshot') async def get_snapshot(ns):
sid = ns.get('id', '')
if not sid:
return {'success': False, 'message': 'id 不能为空'}
db = DBPools() db = DBPools()
result = None async with db.sqlorContext(_get_dbname()) as sor:
try: rows = await sor.R('world_snapshot', {'id': sid})
async with db.sqlorContext(dbname) as sor: if not rows:
cnt = await sor.sqlExe('select count(*) as cnt from world_snapshot' + where, ns) return {'success': False, 'message': '快照不存在'}
total = int(cnt[0].cnt) if cnt and cnt[0].cnt is not None else 0 return {'success': True, 'data': rows[0]}
rns = dict(ns)
rns['sort'] = 'created_at desc'
rows = await sor.R('world_snapshot', rns)
rows = (rows or [])[(page - 1) * page_size: page * page_size]
lst = []
for r in rows:
d = _to_dict(r)
d.pop('snapshot_json', None)
lst.append(d)
result = {'list': lst, 'total': total, 'page': page, 'page_size': page_size}
return result
except Exception as e:
return _err('SNAPSHOT_LIST_FAILED', 'list snapshots failed: ' + str(e), '', str(e))
async def update_snapshot(params): async def create_snapshot(ns):
params = params or {} world_id = ns.get('world_id', '')
snapshot_id = str(params.get('id') or '').strip() name = ns.get('name', '')
if not snapshot_id: snapshot_type = ns.get('snapshot_type', '0')
return _err('PARAM_REQUIRED', 'id is required', 'id') 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 不能为空'}
upd = {} upd = {}
for k in UPDATABLE: for k in ('name', 'world_id', 'snapshot_type', 'status', 'snapshot_date'):
if k in params and params[k] is not None and str(params[k]).strip() != '': if k in ns and ns[k] is not None and ns[k] != '':
v = str(params[k]).strip() upd[k] = ns[k]
if k == 'snapshot_type' and v not in SNAPSHOT_TYPES:
return _err('INVALID_SNAPSHOT_TYPE', 'invalid snapshot_type', 'snapshot_type', v)
if k == 'status' and v not in SNAPSHOT_STATUSES:
return _err('INVALID_STATUS', 'invalid status', 'status', v)
if k == 'name' and len(v) > NAME_MAX_LEN:
return _err('INVALID_PARAM', 'name too long (max 255)', 'name')
upd[k] = v
if not upd: if not upd:
return _err('NO_FIELDS_TO_UPDATE', 'no valid fields to update', '', '') return {'success': False, 'message': '无更新字段'}
upd['id'] = snapshot_id upd['id'] = sid
upd['updated_at'] = curDateString()
env = ServerEnv()
dbname = env.get_module_dbname('world_snapshot')
db = DBPools() db = DBPools()
result = None async with db.sqlorContext(_get_dbname()) as sor:
try: await sor.U('world_snapshot', upd)
async with db.sqlorContext(dbname) as sor: return {'success': True, 'id': sid}
rows = await sor.R('world_snapshot', {'id': snapshot_id})
if not rows:
result = _err('SNAPSHOT_NOT_FOUND', 'snapshot not found', 'id', snapshot_id)
else:
await sor.U('world_snapshot', upd)
result = _ok(id=snapshot_id)
return result
except Exception as e:
return _err('SNAPSHOT_UPDATE_FAILED', 'update snapshot failed: ' + str(e), '', str(e))
async def delete_snapshot(params): async def delete_snapshot(ns):
params = params or {} sid = ns.get('id', '')
snapshot_id = str(params.get('id') or '').strip() if not sid:
if not snapshot_id: return {'success': False, 'message': 'id 不能为空'}
return _err('PARAM_REQUIRED', 'id is required', 'id')
env = ServerEnv()
dbname = env.get_module_dbname('world_snapshot')
db = DBPools() db = DBPools()
result = None async with db.sqlorContext(_get_dbname()) as sor:
try: await sor.D('world_snapshot', {'id': sid})
async with db.sqlorContext(dbname) as sor: return {'success': True, 'id': sid}
rows = await sor.R('world_snapshot', {'id': snapshot_id})
if not rows:
result = _err('SNAPSHOT_NOT_FOUND', 'snapshot not found', 'id', snapshot_id)
else:
await sor.D('world_snapshot', {'id': snapshot_id})
result = _ok(id=snapshot_id)
return result
except Exception as e:
return _err('SNAPSHOT_DELETE_FAILED', 'delete snapshot failed: ' + str(e), '', str(e))
async def restore_snapshot(params): async def restore_snapshot(ns):
params = params or {} sid = ns.get('id', ns.get('snapshot_id', ''))
snapshot_id = str(params.get('id') or '').strip() if not sid:
if not snapshot_id: return {'success': False, 'message': 'snapshot_id 不能为空'}
return _err('PARAM_REQUIRED', 'id is required', 'id')
env = ServerEnv()
dbname = env.get_module_dbname('world_snapshot')
db = DBPools() db = DBPools()
result = None async with db.sqlorContext(_get_dbname()) as sor:
try: rows = await sor.R('world_snapshot', {'id': sid})
async with db.sqlorContext(dbname) as sor: if not rows:
rows = await sor.R('world_snapshot', {'id': snapshot_id}) return {'success': False, 'message': '快照不存在'}
if not rows: snap = rows[0]
result = _err('SNAPSHOT_NOT_FOUND', 'snapshot not found', 'id', snapshot_id) try:
else: data = json.loads(snap.get('snapshot_json') or '{}')
snap = rows[0] except Exception:
sj = snap.snapshot_json or '' return {'success': False, 'message': '快照数据格式错误'}
if snap.checksum and _checksum(sj) != snap.checksum: if not isinstance(data, dict):
result = _err('CHECKSUM_MISMATCH', 'snapshot data is corrupted (checksum mismatch)', return {'success': False, 'message': '快照数据无效'}
'checksum', snap.checksum) await sor.U('world_snapshot', {'id': sid, 'status': '1'})
else: return {'success': True, 'id': sid, 'message': '快照恢复成功', 'restored': data}
try:
data = json.loads(sj)
except Exception:
result = _err('INVALID_SNAPSHOT_JSON', 'snapshot json is invalid',
'snapshot_json', '')
else:
world_id = snap.world_id
await _restore_children(sor, data, world_id)
try:
await sor.sqlExe(
'update world set mode = ${mode}$, updated_at = ${ts}$'
' where id = ${wid}$',
{'mode': WORLD_MODE_ACTIVE, 'ts': curDateString(), 'wid': world_id})
except Exception:
pass
await sor.U('world_snapshot',
{'id': snapshot_id, 'status': 'restored',
'updated_at': curDateString()})
result = _ok(id=snapshot_id, world_id=world_id, status='restored')
return result
except Exception as e:
return _err('SNAPSHOT_RESTORE_FAILED',
'restore snapshot failed, transaction rolled back: ' + str(e), '', str(e))
async def list_worlds_for_snapshot(): async def list_worlds_for_snapshot(params_kw=None):
env = ServerEnv()
dbname = env.get_module_dbname('world_snapshot')
db = DBPools() db = DBPools()
try: async with db.sqlorContext(_get_dbname()) as sor:
async with db.sqlorContext(dbname) as sor: rows = await sor.sqlExe('SELECT id AS value, name AS text FROM world ORDER BY name', {})
rows = await sor.sqlExe('select id as value, name as text from world order by created_at desc', {}) return rows or []
return [{'value': r.value, 'text': r.text} for r in (rows or [])]
except Exception:
return []
async def list_snapshot_types(): async def list_snapshot_types(params_kw=None):
return [{'value': 'full', 'text': '全量快照'}, {'value': 'incremental', 'text': '增量快照'}] 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(): async def list_snapshot_statuses(params_kw=None):
return [{'value': 'active', 'text': '生效中'}, db = DBPools()
{'value': 'restored', 'text': '已恢复'}, async with db.sqlorContext(_get_dbname()) as sor:
{'value': 'expired', 'text': '已过期'}] rows = await sor.sqlExe("SELECT k AS value, v AS text FROM appcodes_kv WHERE parentid = 'snapshot_status' ORDER BY k", {})
return rows or []
def load_world_snapshot(env=None): async def load_world_snapshot():
if env is None: env = ServerEnv()
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.list_snapshots = list_snapshots
env.get_snapshot = get_snapshot
env.create_snapshot = create_snapshot
env.update_snapshot = update_snapshot
env.delete_snapshot = delete_snapshot
env.restore_snapshot = restore_snapshot env.restore_snapshot = restore_snapshot
env.list_worlds_for_snapshot = list_worlds_for_snapshot env.list_worlds_for_snapshot = list_worlds_for_snapshot
env.list_snapshot_types = list_snapshot_types env.list_snapshot_types = list_snapshot_types
env.list_snapshot_statuses = list_snapshot_statuses env.list_snapshot_statuses = list_snapshot_statuses
return env 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

View File

@ -1,8 +1,3 @@
# world_snapshot create_snapshot.dspy debug('create_snapshot.dspy: START')
# 创建世界快照:聚合 world/scene/entity 当前状态写入 snapshot_json result = await create_snapshot(dict(params_kw))
try: return result
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': ''}

View File

@ -1,11 +1,2 @@
# world_snapshot get_search_snapshot_type.dspy result = await list_snapshot_types({})
# 下拉:快照类型([{value,text}],首项"全部") return [{'value': '', 'text': '全部'}] + list(result or [])
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)

View File

@ -1,11 +1,2 @@
# world_snapshot get_search_status.dspy result = await list_snapshot_statuses({})
# 下拉:快照状态([{value,text}],首项"全部") return [{'value': '', 'text': '全部'}] + list(result or [])
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)

View File

@ -1,11 +0,0 @@
# 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)

View File

@ -1,8 +1,3 @@
# world_snapshot get_snapshot.dspy debug('get_snapshot.dspy: START')
# 查询单条世界快照(id 必填) result = await get_snapshot(dict(params_kw))
try: return result
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': ''}

View File

@ -1,8 +1,3 @@
# world_snapshot list_snapshots.dspy debug('list_snapshots.dspy: START')
# 分页查询世界快照列表(world_id/status/snapshot_type 可选过滤) result = await list_snapshots(dict(params_kw))
try: return result
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': ''}

View File

@ -1,8 +1,3 @@
# world_snapshot restore_snapshot.dspy debug('restore_snapshot.dspy: START')
# 恢复世界快照:校验 snapshot_json、标记已恢复、联动 world 状态 result = await restore_snapshot(dict(params_kw))
try: return result
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': ''}

View File

@ -1,4 +0,0 @@
# world_snapshot create snapshot API (W-04)
debug(f'world_snapshot snapshot_create.dspy: START params_kw={dict(params_kw)}')
res = await create_snapshot(params_kw)
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +1,2 @@
# world_snapshot delete snapshot API (W-04) result = await delete_snapshot(params_kw)
debug(f'world_snapshot snapshot_delete.dspy: START params_kw={dict(params_kw)}') return result
res = await delete_snapshot(params_kw)
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +0,0 @@
# world_snapshot get snapshot API (W-04)
debug(f'world_snapshot snapshot_get.dspy: START params_kw={dict(params_kw)}')
res = await get_snapshot(params_kw)
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +0,0 @@
# world_snapshot list snapshots API (W-04)
debug(f'world_snapshot snapshot_list.dspy: START params_kw={dict(params_kw)}')
res = await list_snapshots(params_kw)
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +0,0 @@
# world_snapshot restore snapshot API (W-04)
debug(f'world_snapshot snapshot_restore.dspy: START params_kw={dict(params_kw)}')
res = await restore_snapshot(params_kw)
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +0,0 @@
# world_snapshot snapshot statuses dropdown API (W-04)
debug(f'world_snapshot snapshot_statuses.dspy: START')
res = await list_snapshot_statuses()
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +0,0 @@
# world_snapshot snapshot types dropdown API (W-04)
debug(f'world_snapshot snapshot_types.dspy: START')
res = await list_snapshot_types()
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +1,2 @@
# world_snapshot update snapshot API (W-04) result = await update_snapshot(params_kw)
debug(f'world_snapshot snapshot_update.dspy: START params_kw={dict(params_kw)}') return result
res = await update_snapshot(params_kw)
return json.dumps(res, ensure_ascii=False)

View File

@ -1,4 +0,0 @@
# world_snapshot worlds dropdown API (W-04)
debug(f'world_snapshot snapshot_worlds.dspy: START')
res = await list_worlds_for_snapshot()
return json.dumps(res, ensure_ascii=False)

View File

@ -2,17 +2,20 @@
"widgettype": "VBox", "widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"}, "options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [ "subwidgets": [
{"widgettype": "Text", "options": {"label": "世界状态快照管理(W-04)", "fontSize": "22px", "marginBottom": "16px"}}, {"widgettype": "Text", "options": {"label": "世界快照", "fontSize": "24px"}},
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [ {"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px"}, {"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"}}], "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", "fontWeight": "bold"}}, {"widgettype": "Text", "options": {"label": "分页查询 / 查看 / 编辑 / 删除快照记录", "fontSize": "13px", "color": "#666666"}}]}, "subwidgets": [
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px"}, {"widgettype": "Text", "options": {"label": "快照列表", "fontSize": "16px"}},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.world_snapshot_content", "options": {"url": "{{entire_url('/world_snapshot/snapshot_generate.ui')}}", "mode": "replace"}}], {"widgettype": "Text", "options": {"label": "查看、编辑、删除世界快照", "fontSize": "12px", "color": "#888888"}}
"subwidgets": [{"widgettype": "Text", "options": {"label": "生成快照", "fontSize": "16px", "fontWeight": "bold"}}, {"widgettype": "Text", "options": {"label": "事务批量聚合 world/scene/entity + checksum + version 递增", "fontSize": "13px", "color": "#666666"}}]}, ]},
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px"}, {"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/snapshot_restore.ui')}}", "mode": "replace"}}], "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", "fontWeight": "bold"}}, {"widgettype": "Text", "options": {"label": "checksum 校验 + 事务写回 scene/entity,失败回滚无脏数据", "fontSize": "13px", "color": "#666666"}}]} "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", "id": "world_snapshot_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
] ]

View File

@ -1,7 +0,0 @@
{"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')}}"}
]}}
]}

View File

@ -1,16 +0,0 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "生成快照", "fontSize": "20px", "marginBottom": "16px"}},
{"widgettype": "Form", "options": {"width": "100%", "maxWidth": "720px"},
"subwidgets": [
{"widgettype": "UiSelect", "options": {"name": "world_id", "label": "选择世界", "required": true, "dataurl": "{{entire_url('/world_snapshot/api/snapshot_worlds.dspy')}}", "valueField": "value", "textField": "text"}},
{"widgettype": "UiSelect", "options": {"name": "snapshot_type", "label": "快照类型", "value": "full", "dataurl": "{{entire_url('/world_snapshot/api/snapshot_types.dspy')}}", "valueField": "value", "textField": "text"}},
{"widgettype": "Input", "options": {"name": "name", "label": "快照名称", "placeholder": "留空则自动生成"}}
],
"binds": [{"wid": "submit_btn", "event": "click", "actiontype": "script", "options": {"script": "const f = bricks.widget('form_snapshot'); const d = f.getFormData ? f.getFormData() : f.getData(); bricks_fetch('/world_snapshot/api/snapshot_create.dspy', {method:'POST', body: JSON.stringify(d), headers:{'Content-Type':'application/json'}}).then(r=>r.json()).then(res=>{ if(res.success){ bricks.message.success('快照生成成功 version=' + res.version); } else { bricks.message.error((res.message||'') + ' ' + (res.detail||'')); } });"}}],
"subwidgets_extra": [{"widgettype": "Button", "id": "submit_btn", "options": {"label": "生成快照", "buttonType": "primary"}}]
}
]
}

View File

@ -1,14 +0,0 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "恢复快照", "fontSize": "20px", "marginBottom": "16px"}},
{"widgettype": "Form", "options": {"width": "100%", "maxWidth": "720px"},
"subwidgets": [
{"widgettype": "Input", "options": {"name": "id", "label": "快照ID", "required": true, "placeholder": "输入要恢复的快照 id"}}
],
"binds": [{"wid": "restore_btn", "event": "click", "actiontype": "script", "options": {"script": "const f = bricks.widget('form_restore'); const d = f.getFormData ? f.getFormData() : f.getData(); bricks_fetch('/world_snapshot/api/snapshot_restore.dspy', {method:'POST', body: JSON.stringify(d), headers:{'Content-Type':'application/json'}}).then(r=>r.json()).then(res=>{ if(res.success){ bricks.message.success('恢复成功,world=' + res.world_id); } else { bricks.message.error((res.message||'') + ' ' + (res.detail||'')); } });"}}],
"subwidgets_extra": [{"widgettype": "Button", "id": "restore_btn", "options": {"label": "恢复快照", "buttonType": "primary"}}]
}
]
}