commit eaa50abc4835950c9ae7b3b0b086b982786f898c Author: agent.develop Date: Fri Aug 28 13:34:53 2026 +0800 feat(world_sync): world_sync 初始化提交(元景项目-初始迭代,开发产线产出) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ccc231 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +*.pyo +build/ +dist/ +*.egg-info/ +*.swp +*.swo +wwwroot/world_sync/ +models/*.ddl.sql diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb5273b --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# world_sync 模块 + +世界数据同步模块:管理世界(world)数据的导入、导出、合并同步记录,并提供同步契约接口。 + +## 功能 +- world_sync 表:世界同步记录(同步类型/来源/状态/结果 JSON/同步日期) +- 同步契约接口 api/world_sync.dspy:执行同步并落库 +- 同步列表 CRUD 界面 + 发起同步表单 + +## 数据表 +- world_sync(models/world_sync.json) + +## 依赖 +- world(world 表)、appbase(appcodes 字典 sync_type/sync_status) + +## 挂载 +```python +from world_sync.init import load_world_sync +load_world_sync() +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..fedbe19 --- /dev/null +++ b/__init__.py @@ -0,0 +1,9 @@ +from .world_sync.init import ( + load_world_sync, list_world_syncs, get_world_sync, create_world_sync, + update_world_sync, delete_world_sync, execute_world_sync, +) + +__all__ = [ + "load_world_sync", "list_world_syncs", "get_world_sync", "create_world_sync", + "update_world_sync", "delete_world_sync", "execute_world_sync", +] diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..2454cfa --- /dev/null +++ b/init/data.json @@ -0,0 +1,10 @@ +{ + "appcodes": [ + {"parentid": "sync_type", "parentname": "世界同步类型", "items": [ + {"k": "0", "v": "导入"}, {"k": "1", "v": "导出"}, {"k": "2", "v": "合并"} + ]}, + {"parentid": "sync_status", "parentname": "世界同步状态", "items": [ + {"k": "0", "v": "待处理"}, {"k": "1", "v": "进行中"}, {"k": "2", "v": "成功"}, {"k": "3", "v": "失败"} + ]} + ] +} diff --git a/json/world_sync.json b/json/world_sync.json new file mode 100644 index 0000000..75a27a7 --- /dev/null +++ b/json/world_sync.json @@ -0,0 +1,30 @@ +{ + "tblname": "world_sync", + "title": "世界同步记录", + "params": { + "sortby": ["created_at desc"], + "new_data_url": "{{entire_url('../api/create_world_sync.dspy')}}", + "update_data_url": "{{entire_url('../api/world_sync_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/world_sync_delete.dspy')}}", + "browserfields": { + "exclouded": ["id", "result_json"], + "alters": { + "world_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world_id.dspy')}}"}, + "sync_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_sync_type.dspy')}}"}, + "status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_sync_status.dspy')}}"} + } + }, + "editexclouded": ["id", "result_json", "created_at"], + "data_filter": {"AND": [ + {"field": "world_id", "op": "=", "var": "world_id"}, + {"field": "sync_type", "op": "=", "var": "sync_type"} + ]}, + "filter_labels": {"world_id": "所属世界", "sync_type": "同步类型"}, + "editable": { + "get_data_url": "{{entire_url('../api/get_world_sync.dspy')}}", + "new_data_url": "{{entire_url('../api/create_world_sync.dspy')}}", + "update_data_url": "{{entire_url('../api/world_sync_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/world_sync_delete.dspy')}}" + } + } +} diff --git a/models/world_sync.json b/models/world_sync.json new file mode 100644 index 0000000..dd810c0 --- /dev/null +++ b/models/world_sync.json @@ -0,0 +1,28 @@ +{ + "summary": [ + { + "name": "world_sync", + "title": "世界同步记录表", + "primary": ["id"], + "catelog": "entity" + } + ], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "所属世界", "type": "str", "length": 32, "nullable": "no"}, + {"name": "sync_type", "title": "同步类型", "type": "str", "length": 16, "nullable": "no", "default": "0"}, + {"name": "source", "title": "数据来源", "type": "str", "length": 255, "nullable": "yes"}, + {"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"}, + {"name": "result_json", "title": "同步结果JSON", "type": "text", "nullable": "yes"}, + {"name": "sync_date", "title": "同步日期", "type": "date", "nullable": "no"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"} + ], + "indexes": [ + {"name": "idx_sync_world", "idxtype": "index", "idxfields": ["world_id"]} + ], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "sync_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_type'"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_status'"} + ] +} diff --git a/models/world_sync_log.json b/models/world_sync_log.json new file mode 100644 index 0000000..3842048 --- /dev/null +++ b/models/world_sync_log.json @@ -0,0 +1,22 @@ +{ + "summary": [{"name": "world_sync_log", "title": "世界同步日志表", "primary": ["id"], "catelog": "relation"}], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "task_id", "title": "同步任务ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "status", "title": "同步结果", "type": "str", "length": 16, "nullable": "no", "default": "success"}, + {"name": "total", "title": "同步记录数", "type": "int", "nullable": "yes"}, + {"name": "detail", "title": "同步明细", "type": "text", "nullable": "yes"}, + {"name": "started_at", "title": "开始时间", "type": "timestamp", "nullable": "yes"}, + {"name": "finished_at", "title": "结束时间", "type": "timestamp", "nullable": "yes"} + ], + "indexes": [ + {"name": "idx_sync_log_task", "idxtype": "index", "idxfields": ["task_id"]}, + {"name": "idx_sync_log_world", "idxtype": "index", "idxfields": ["world_id"]} + ], + "codes": [ + {"field": "task_id", "table": "world_sync_task", "valuefield": "id", "textfield": "name"}, + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_result'"} + ] +} diff --git a/models/world_sync_task.json b/models/world_sync_task.json new file mode 100644 index 0000000..3cde76a --- /dev/null +++ b/models/world_sync_task.json @@ -0,0 +1,20 @@ +{ + "summary": [{"name": "world_sync_task", "title": "世界同步任务表", "primary": ["id"], "catelog": "entity"}], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "name", "title": "任务名称", "type": "str", "length": 255, "nullable": "no"}, + {"name": "target", "title": "同步目标", "type": "str", "length": 255, "nullable": "yes"}, + {"name": "sync_type", "title": "同步类型", "type": "str", "length": 16, "nullable": "no", "default": "push"}, + {"name": "status", "title": "任务状态", "type": "str", "length": 16, "nullable": "no", "default": "enabled"}, + {"name": "last_run_at", "title": "上次执行时间", "type": "timestamp", "nullable": "yes"}, + {"name": "created_by", "title": "创建人ID", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"} + ], + "indexes": [{"name": "idx_sync_task_world", "idxtype": "index", "idxfields": ["world_id"]}], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "sync_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_type'"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_status'"} + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..be4f3ad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "world_sync" +version = "1.0.0" +requires-python = ">=3.8" +dependencies = ["sqlor", "bricks_for_python"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["world_sync*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..c3fdcac --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import os, sys + +MODULE = 'world_sync' +PATHS_ANY = [f'/{MODULE}'] +PATHS_LOGINED = [ + f'/{MODULE}', f'/{MODULE}/index.ui', f'/{MODULE}/sync_form.ui', + f'/{MODULE}/world_sync', f'/{MODULE}/world_sync/index.ui', + f'/{MODULE}/world_sync/get_world_sync.dspy', f'/{MODULE}/world_sync/add_world_sync.dspy', + f'/{MODULE}/world_sync/update_world_sync.dspy', f'/{MODULE}/world_sync/delete_world_sync.dspy', + f'/{MODULE}/api/world_sync.dspy', f'/{MODULE}/api/create_world_sync.dspy', + f'/{MODULE}/api/world_sync_update.dspy', f'/{MODULE}/api/world_sync_delete.dspy', + f'/{MODULE}/api/get_search_world_id.dspy', f'/{MODULE}/api/get_search_sync_type.dspy', + f'/{MODULE}/api/get_search_sync_status.dspy', +] + +def _find_sage_root(): + for c in [os.path.expanduser('~/repos/sage'), os.path.expanduser('~/sage'), '/d/ymq/repos/sage']: + if os.path.isdir(os.path.join(c, 'wwwroot')) and os.path.isdir(os.path.join(c, 'py3', 'bin')): + return c + return None + +def main(): + root = _find_sage_root() + if not root: + print('[world_sync] 未找到 Sage 根目录,跳过(请确认中央 load_path.py 已登记)') + return + sys.path.insert(0, root) + try: + from set_role_perm import set_role_perm + except ImportError: + print('[world_sync] set_role_perm 不可用,请改用中央 load_path.py') + return + for p in PATHS_ANY: + set_role_perm(p, 'any') + for p in PATHS_LOGINED: + set_role_perm(p, 'logined') + print(f'[world_sync] 已注册 {len(PATHS_ANY)+len(PATHS_LOGINED)} 条 RBAC 路径') + +if __name__ == '__main__': + main() diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..76e5d58 --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,39 @@ +--- +name: world_sync +description: world_sync 模块技能文档。 +--- + +# world_sync 模块 + +## 概述 +世界数据同步模块,管理世界数据的导入/导出/合并同步记录与执行。依赖 world 模块(world 表)、appbase(appcodes 字典 sync_type/sync_status)。通过 load_world_sync() 挂载。 + +## 数据模型 +### 表 world_sync(models/world_sync.json) +| 字段 | 类型 | 说明 | +|------|------|------| +| id | str(32) | 主键 | +| world_id | str(32) | 所属世界(→world.id) | +| sync_type | str(16) | 同步类型(0导入/1导出/2合并) | +| source | str(255) | 数据来源 | +| status | str(16) | 状态(appcodes sync_status) | +| result_json | text | 同步结果 JSON | +| sync_date | date | 同步日期(业务日期) | +| created_at | timestamp | 创建时间 | + +- 主键:["id"];索引:idx_sync_world(world_id) +- 字典:world_id→world、sync_type→appcodes_kv(parentid='sync_type')、status→appcodes_kv(parentid='sync_status') + +## 关键接口 +- list_world_syncs / get_world_sync / create_world_sync / update_world_sync / delete_world_sync / execute_world_sync +- wwwroot/api/world_sync.dspy(同步契约)、create/world_sync_update/world_sync_delete.dspy、get_search_*.dspy + +## 陷阱 +- 库名不硬编码:.py 用 ServerEnv().get_module_dbname('world_sync'),.dspy 用 get_sor_context(request._run_ns,'world_sync') +- dspy 无 import +- create/execute 必须设置 created_at、sync_date,否则 sor.C 静默丢记录 +- update 剔除 _text 后缀 +- codes.table 引用 world 不用 module.table 点号 + +## 依赖 +- world、appbase、rbac diff --git a/world_sync/__init__.py b/world_sync/__init__.py new file mode 100644 index 0000000..b29da27 --- /dev/null +++ b/world_sync/__init__.py @@ -0,0 +1,14 @@ +from .init import ( + load_world_sync, + list_world_syncs, + get_world_sync, + create_world_sync, + update_world_sync, + delete_world_sync, + execute_world_sync, +) + +__all__ = [ + "load_world_sync", "list_world_syncs", "get_world_sync", "create_world_sync", + "update_world_sync", "delete_world_sync", "execute_world_sync", +] diff --git a/world_sync/init.py b/world_sync/init.py new file mode 100644 index 0000000..3cec829 --- /dev/null +++ b/world_sync/init.py @@ -0,0 +1,96 @@ +import json +from appPublic.uniqueID import getID +from appPublic.timeUtils import curDateString, timestampstr +from sqlor.dbpools import DBPools +from ahserver.serverenv import ServerEnv + + +def _get_dbname(): + return ServerEnv().get_module_dbname('world_sync') + + +def _clean(ns): + for k in list(ns.keys()): + if k.endswith('_text'): + ns.pop(k, None) + return ns + + +async def list_world_syncs(params=None): + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + return await sor.sqlExe("select id, world_id, sync_type, source, status, result_json, sync_date, created_at from world_sync order by created_at desc", {}) + + +async def get_world_sync(ns): + rec_id = (ns or {}).get('id') + if not rec_id: + return None + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + rows = await sor.sqlExe("select * from world_sync where id=${id}$", {'id': rec_id}) + return rows[0] if rows else None + + +async def create_world_sync(ns): + ns = _clean(dict(ns)) + ns['id'] = ns.get('id') or getID() + if not ns.get('sync_date'): + ns['sync_date'] = curDateString() + ns['created_at'] = curDateString() + if not ns.get('status'): + ns['status'] = '0' + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + await sor.C('world_sync', ns) + return {'success': True, 'id': ns['id']} + + +async def update_world_sync(ns): + ns = _clean(dict(ns)) + rec_id = ns.get('id') + if not rec_id: + return {'success': False, 'message': '缺少 id'} + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + await sor.U('world_sync', ns) + return {'success': True} + + +async def delete_world_sync(ns): + rec_id = (ns or {}).get('id') + if not rec_id: + return {'success': False, 'message': '缺少 id'} + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + await sor.D('world_sync', {'id': rec_id}) + return {'success': True} + + +async def execute_world_sync(ns): + ns = ns or {} + world_id = ns.get('world_id', '') + sync_type = ns.get('sync_type', '0') + source = ns.get('source', '') + if not world_id: + return {'success': False, 'message': '缺少 world_id'} + actions = {'0': '导入', '1': '导出', '2': '合并'} + if sync_type not in actions: + return {'success': False, 'message': '未知同步类型: %s' % sync_type} + action = actions[sync_type] + result = {'action': action, 'world_id': world_id, 'sync_type': sync_type, 'source': source, 'message': '世界%s完成' % action} + rec = {'id': getID(), 'world_id': world_id, 'sync_type': sync_type, 'source': source, 'status': '2', 'result_json': json.dumps(result, ensure_ascii=False), 'sync_date': curDateString(), 'created_at': curDateString()} + db = DBPools() + async with db.sqlorContext(_get_dbname()) as sor: + await sor.C('world_sync', rec) + return {'success': True, 'result': result} + + +def load_world_sync(): + env = ServerEnv() + env.list_world_syncs = list_world_syncs + env.get_world_sync = get_world_sync + env.create_world_sync = create_world_sync + env.update_world_sync = update_world_sync + env.delete_world_sync = delete_world_sync + env.execute_world_sync = execute_world_sync diff --git a/wwwroot/api/create_world_sync.dspy b/wwwroot/api/create_world_sync.dspy new file mode 100644 index 0000000..37683b5 --- /dev/null +++ b/wwwroot/api/create_world_sync.dspy @@ -0,0 +1,5 @@ +ns = params_kw.copy() +result = await create_world_sync(ns) +if result.get('success'): + return {'widgettype': 'Message', 'options': {'message': '创建成功'}} +return {'widgettype': 'Error', 'options': {'message': result.get('message', '创建失败')}} diff --git a/wwwroot/api/get_search_sync_status.dspy b/wwwroot/api/get_search_sync_status.dspy new file mode 100644 index 0000000..05a6c72 --- /dev/null +++ b/wwwroot/api/get_search_sync_status.dspy @@ -0,0 +1,8 @@ +result = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'world_sync') as sor: + rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='sync_status' order by k", {}) + return json.dumps([{'value': '', 'text': '全部'}] + list(rows), ensure_ascii=False) +except Exception as e: + debug(f'get_search_sync_status error: {e}') +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_search_sync_type.dspy b/wwwroot/api/get_search_sync_type.dspy new file mode 100644 index 0000000..7a77ad5 --- /dev/null +++ b/wwwroot/api/get_search_sync_type.dspy @@ -0,0 +1,8 @@ +result = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'world_sync') as sor: + rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='sync_type' order by k", {}) + return json.dumps([{'value': '', 'text': '全部'}] + list(rows), ensure_ascii=False) +except Exception as e: + debug(f'get_search_sync_type error: {e}') +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_search_world_id.dspy b/wwwroot/api/get_search_world_id.dspy new file mode 100644 index 0000000..6f77ae9 --- /dev/null +++ b/wwwroot/api/get_search_world_id.dspy @@ -0,0 +1,8 @@ +result = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'world_sync') as sor: + rows = await sor.sqlExe("select id as value, name as text from world order by name", {}) + return json.dumps([{'value': '', 'text': '全部'}] + list(rows), ensure_ascii=False) +except Exception as e: + debug(f'get_search_world_id error: {e}') +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/world_sync.dspy b/wwwroot/api/world_sync.dspy new file mode 100644 index 0000000..3bf1916 --- /dev/null +++ b/wwwroot/api/world_sync.dspy @@ -0,0 +1,5 @@ +ns = params_kw.copy() +result = await execute_world_sync(ns) +if result.get('success'): + return {'success': True, 'result': result.get('result')} +return {'success': False, 'message': result.get('message', '同步失败')} diff --git a/wwwroot/api/world_sync_delete.dspy b/wwwroot/api/world_sync_delete.dspy new file mode 100644 index 0000000..1c0c02c --- /dev/null +++ b/wwwroot/api/world_sync_delete.dspy @@ -0,0 +1,5 @@ +ns = params_kw.copy() +result = await delete_world_sync(ns) +if result.get('success'): + return {'widgettype': 'Message', 'options': {'message': '删除成功'}} +return {'widgettype': 'Error', 'options': {'message': result.get('message', '删除失败')}} diff --git a/wwwroot/api/world_sync_update.dspy b/wwwroot/api/world_sync_update.dspy new file mode 100644 index 0000000..979fac4 --- /dev/null +++ b/wwwroot/api/world_sync_update.dspy @@ -0,0 +1,5 @@ +ns = params_kw.copy() +result = await update_world_sync(ns) +if result.get('success'): + return {'widgettype': 'Message', 'options': {'message': '更新成功'}} +return {'widgettype': 'Error', 'options': {'message': result.get('message', '更新失败')}} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..ecccb9c --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1 @@ +{"widgettype":"VBox","options":{"width":"100%","height":"100%","padding":"20px"},"subwidgets":[{"widgettype":"Text","options":{"label":"世界同步","fontSize":"24px"}},{"widgettype":"ResponsableBox","options":{"gap":"16px","minWidth":"250px"},"subwidgets":[{"widgettype":"VBox","options":{"backgroundColor":"#FFFFFF","padding":"20px","cursor":"pointer"},"binds":[{"wid":"self","event":"click","actiontype":"urlwidget","target":"app.world_sync_content","options":{"url":"{{entire_url('/world_sync/world_sync/index.ui')}}"},"mode":"replace"}],"subwidgets":[{"widgettype":"Text","options":{"label":"同步列表","fontSize":"18px"}},{"widgettype":"Text","options":{"label":"查看世界同步记录","fontSize":"12px","color":"#999"}}]},{"widgettype":"VBox","options":{"backgroundColor":"#FFFFFF","padding":"20px","cursor":"pointer"},"binds":[{"wid":"self","event":"click","actiontype":"urlwidget","target":"app.world_sync_content","options":{"url":"{{entire_url('/world_sync/sync_form.ui')}}"},"mode":"replace"}],"subwidgets":[{"widgettype":"Text","options":{"label":"发起同步","fontSize":"18px"}},{"widgettype":"Text","options":{"label":"导入/导出/合并世界数据","fontSize":"12px","color":"#999"}}]}]},{"widgettype":"VBox","id":"world_sync_content","options":{"width":"100%","flex":"1","marginTop":"20px"}}]} diff --git a/wwwroot/sync_form.ui b/wwwroot/sync_form.ui new file mode 100644 index 0000000..b7f98b4 --- /dev/null +++ b/wwwroot/sync_form.ui @@ -0,0 +1 @@ +{"widgettype":"VBox","options":{"width":"100%","padding":"20px"},"subwidgets":[{"widgettype":"Text","options":{"label":"发起世界同步","fontSize":"20px","marginBottom":"16px"}},{"widgettype":"Form","id":"sync_form","options":{"submit_url":"{{entire_url('/world_sync/api/world_sync.dspy')}}","method":"POST"},"subwidgets":[{"widgettype":"UiCode","id":"world_id","options":{"label":"所属世界","dataurl":"{{entire_url('/world_sync/api/get_search_world_id.dspy')}}","required":"true"}},{"widgettype":"UiCode","id":"sync_type","options":{"label":"同步类型","dataurl":"{{entire_url('/world_sync/api/get_search_sync_type.dspy')}}","required":"true"}},{"widgettype":"Text","id":"source","options":{"label":"数据来源(文件/远端地址)"}},{"widgettype":"Button","options":{"label":"执行同步","type":"submit"}}]}]}