commit 5d94bb451c5ea0404cfdf05ccde10de8cb406dcf Author: agent.develop Date: Fri Aug 28 13:34:51 2026 +0800 feat(world_snapshot): world_snapshot 初始化提交(元景项目-初始迭代,开发产线产出) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9520326 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +build/ +*.egg-info/ +dist/ +wwwroot/world_snapshot/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..660e337 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# world_snapshot 模块 + +世界状态快照模块,聚合世界(world)、场景(scene)、实体(entity)当前状态生成快照,支持查询与恢复。 + +## 功能 +- 世界快照表 `world_snapshot` 的 CRUD +- `create_snapshot`:聚合 world/scene/entity 当前状态写入 `snapshot_json` +- `restore_snapshot`:读取并校验快照 JSON,恢复状态(基础实现:校验 + 状态标记) +- 下拉接口:世界列表、快照类型、快照状态 + +## 数据表 +- `world_snapshot`(models/world_snapshot.json) + +## 集成 +- 通过 `load_world_snapshot()` 挂载到宿主应用 +- 库名经 `ServerEnv().get_module_dbname('world_snapshot')` 获取,禁止硬编码 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a4b3f20 --- /dev/null +++ b/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +仓库根目录兼容 shim。 + +pip 打包(pyproject.toml include = ["world_snapshot*"])只安装 world_snapshot/ 包, +实现源码位于 world_snapshot/init.py。本文件仅在开发环境(modules/ 在 sys.path 上) +把 import world_snapshot 转发到真正的子包,避免与同名子包产生二义性。 +""" +from .world_snapshot import * # noqa: F401,F403 +from .world_snapshot import __all__ # noqa: F401 diff --git a/init.py b/init.py new file mode 100644 index 0000000..c6b1358 --- /dev/null +++ b/init.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +仓库根目录兼容 shim:真实实现位于 world_snapshot/init.py。 + +宿主应用集成方式(二选一): + from world_snapshot.init import load_world_snapshot # pip 安装后 + from world_snapshot import load_world_snapshot # 包 __init__ 已 re-export +""" +from .world_snapshot.init import * # noqa: F401,F403 +from .world_snapshot.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, +) diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..637ce0a --- /dev/null +++ b/init/data.json @@ -0,0 +1,13 @@ +{ + "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": "已作废"} + ]} + ] +} diff --git a/json/world_snapshot.json b/json/world_snapshot.json new file mode 100644 index 0000000..4f7acff --- /dev/null +++ b/json/world_snapshot.json @@ -0,0 +1,21 @@ +{ + "tblname": "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"} + ] + }, + "filter_labels": {"world_id": "所属世界", "snapshot_date": "快照日期"}, + "browserfields": {"exclouded": ["id", "snapshot_json"], "alters": {}}, + "editexclouded": ["id", "snapshot_json", "created_at"], + "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')}}" + } + } +} diff --git a/models/world_snapshot.json b/models/world_snapshot.json new file mode 100644 index 0000000..7ce3d21 --- /dev/null +++ b/models/world_snapshot.json @@ -0,0 +1,24 @@ +{ + "summary": [ + {"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": "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_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"]} + ], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"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'"} + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d4c6b1d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "world_snapshot" +version = "1.0.0" +requires-python = ">=3.8" +dependencies = ["sqlor", "bricks_for_python"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["world_snapshot*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..69ab459 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""world_snapshot 模块 RBAC 注册脚本。所有 path 显式列出,禁止通配符。""" +import os +import sys + +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 = [ + 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', +] + + +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: + set_role_perm(path, 'logined') + print('RBAC registered for module %s' % MOD) + + +if __name__ == '__main__': + main() diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..362b793 --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,21 @@ +--- +name: world_snapshot +description: world_snapshot 模块技能文档——世界快照管理(世界状态快照生成、查询与恢复)。 +--- + +# world_snapshot 模块 + +世界状态快照模块,聚合世界 + 场景 + 实体当前状态生成快照,支持查询与恢复。 + +## 数据模型 +表 `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 +- 下拉:list_worlds_for_snapshot / list_snapshot_types / list_snapshot_statuses + +## 陷阱 +- 库名禁止硬编码,统一 get_module_dbname("world_snapshot") +- dspy 无 import +- create_snapshot 必须设置 created_at/snapshot_date +- snapshot_date 用 date 类型(业务日期仅到天) diff --git a/world_snapshot/__init__.py b/world_snapshot/__init__.py new file mode 100644 index 0000000..1c75dbd --- /dev/null +++ b/world_snapshot/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +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, +) + +__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", +] diff --git a/world_snapshot/init.py b/world_snapshot/init.py new file mode 100644 index 0000000..2343456 --- /dev/null +++ b/world_snapshot/init.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +"""world_snapshot 模块 —— 世界状态快照管理。库名经 get_module_dbname('world_snapshot') 获取,禁止硬编码。""" +import json +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') + + +async def list_snapshots(ns): + 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', '') + 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 不能为空'} + 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 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 不能为空'} + 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] + if not upd: + return {'success': False, 'message': '无更新字段'} + upd['id'] = sid + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + await sor.U('world_snapshot', upd) + return {'success': True, 'id': sid} + + +async def delete_snapshot(ns): + sid = ns.get('id', '') + if not sid: + return {'success': False, 'message': 'id 不能为空'} + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + await sor.D('world_snapshot', {'id': sid}) + return {'success': True, 'id': sid} + + +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 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(): + env = ServerEnv() + 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.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 diff --git a/wwwroot/api/create_snapshot.dspy b/wwwroot/api/create_snapshot.dspy new file mode 100644 index 0000000..56f4c85 --- /dev/null +++ b/wwwroot/api/create_snapshot.dspy @@ -0,0 +1,3 @@ +debug('create_snapshot.dspy: START') +result = await create_snapshot(dict(params_kw)) +return result diff --git a/wwwroot/api/get_search_snapshot_type.dspy b/wwwroot/api/get_search_snapshot_type.dspy new file mode 100644 index 0000000..2bfa0cc --- /dev/null +++ b/wwwroot/api/get_search_snapshot_type.dspy @@ -0,0 +1,2 @@ +result = await list_snapshot_types({}) +return [{'value': '', 'text': '全部'}] + list(result or []) diff --git a/wwwroot/api/get_search_status.dspy b/wwwroot/api/get_search_status.dspy new file mode 100644 index 0000000..71aad63 --- /dev/null +++ b/wwwroot/api/get_search_status.dspy @@ -0,0 +1,2 @@ +result = await list_snapshot_statuses({}) +return [{'value': '', 'text': '全部'}] + list(result or []) diff --git a/wwwroot/api/get_search_world_id.dspy b/wwwroot/api/get_search_world_id.dspy new file mode 100644 index 0000000..12d56a0 --- /dev/null +++ b/wwwroot/api/get_search_world_id.dspy @@ -0,0 +1,2 @@ +result = await list_worlds_for_snapshot({}) +return [{'value': '', 'text': '全部'}] + list(result or []) diff --git a/wwwroot/api/get_snapshot.dspy b/wwwroot/api/get_snapshot.dspy new file mode 100644 index 0000000..8122335 --- /dev/null +++ b/wwwroot/api/get_snapshot.dspy @@ -0,0 +1,3 @@ +debug('get_snapshot.dspy: START') +result = await get_snapshot(dict(params_kw)) +return result diff --git a/wwwroot/api/list_snapshot_statuses.dspy b/wwwroot/api/list_snapshot_statuses.dspy new file mode 100644 index 0000000..e2c922b --- /dev/null +++ b/wwwroot/api/list_snapshot_statuses.dspy @@ -0,0 +1,3 @@ +debug('list_snapshot_statuses.dspy: START') +result = await list_snapshot_statuses() +return result diff --git a/wwwroot/api/list_snapshot_types.dspy b/wwwroot/api/list_snapshot_types.dspy new file mode 100644 index 0000000..c703cf1 --- /dev/null +++ b/wwwroot/api/list_snapshot_types.dspy @@ -0,0 +1,3 @@ +debug('list_snapshot_types.dspy: START') +result = await list_snapshot_types() +return result diff --git a/wwwroot/api/list_snapshots.dspy b/wwwroot/api/list_snapshots.dspy new file mode 100644 index 0000000..b095a69 --- /dev/null +++ b/wwwroot/api/list_snapshots.dspy @@ -0,0 +1,3 @@ +debug('list_snapshots.dspy: START') +result = await list_snapshots(dict(params_kw)) +return result diff --git a/wwwroot/api/list_worlds_for_snapshot.dspy b/wwwroot/api/list_worlds_for_snapshot.dspy new file mode 100644 index 0000000..0229c92 --- /dev/null +++ b/wwwroot/api/list_worlds_for_snapshot.dspy @@ -0,0 +1,3 @@ +debug('list_worlds_for_snapshot.dspy: START') +result = await list_worlds_for_snapshot() +return result diff --git a/wwwroot/api/restore_snapshot.dspy b/wwwroot/api/restore_snapshot.dspy new file mode 100644 index 0000000..1ed2f64 --- /dev/null +++ b/wwwroot/api/restore_snapshot.dspy @@ -0,0 +1,3 @@ +debug('restore_snapshot.dspy: START') +result = await restore_snapshot(dict(params_kw)) +return result diff --git a/wwwroot/api/snapshot_delete.dspy b/wwwroot/api/snapshot_delete.dspy new file mode 100644 index 0000000..79248f7 --- /dev/null +++ b/wwwroot/api/snapshot_delete.dspy @@ -0,0 +1,2 @@ +result = await delete_snapshot(params_kw) +return result diff --git a/wwwroot/api/snapshot_update.dspy b/wwwroot/api/snapshot_update.dspy new file mode 100644 index 0000000..b2970ac --- /dev/null +++ b/wwwroot/api/snapshot_update.dspy @@ -0,0 +1,2 @@ +result = await update_snapshot(params_kw) +return result diff --git a/wwwroot/api/world_snapshot_delete.dspy b/wwwroot/api/world_snapshot_delete.dspy new file mode 100644 index 0000000..7c0fd13 --- /dev/null +++ b/wwwroot/api/world_snapshot_delete.dspy @@ -0,0 +1,3 @@ +debug('world_snapshot_delete.dspy: START') +result = await delete_snapshot(dict(params_kw)) +return result diff --git a/wwwroot/api/world_snapshot_update.dspy b/wwwroot/api/world_snapshot_update.dspy new file mode 100644 index 0000000..a7824f2 --- /dev/null +++ b/wwwroot/api/world_snapshot_update.dspy @@ -0,0 +1,7 @@ +debug('world_snapshot_update.dspy: START') +ns = dict(params_kw) +for k in list(ns.keys()): + if k.endswith('_text'): + ns.pop(k, None) +result = await update_snapshot(ns) +return result diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..b387e10 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,22 @@ +{ + "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"}} + ] +}