--- name: drag description: 拖拽编程画布模块(W-09)——可视化块编排、连线、参数校验、编译为 script_engine 可执行脚本,提供画布/模板 CRUD 与 block_defs/graph_save/graph_compile/graph_validate/graph_publish 接口,通过 load_drag() 挂载。 --- # drag 拖拽编程画布模块 元景 W-09:把事件/逻辑/动作/变量/表达式块拖拽连线成流程图,校验后编译为 script_engine 可执行 Python 脚本(script_type=0)。宿主应用:scense(/d/scense/scense_app)。 ## 架构 ``` bricks 前端 (editor.ui + drag-canvas.js + drag-editor.js) │ block_defs / graph_save / graph_compile / graph_validate / graph_publish ▼ drag 模块 (init.py 挂载) ├── blocks.py 块定义 JSON(categories + blocks,唯一事实源) ├── validator.py 图校验(无入口/孤立/环形/参数/变量引用) └── compiler.py 图 → Python 脚本(变量提升/函数区/事件分支/控制流) ▼ drag_graph / drag_template 表(drag 库)+ script_engine 发布 ``` ## 数据模型 - `drag_graph`:id(32 PK)、name(100)、description(255)、blocks(text JSON)、 connections(text JSON)、content(text 编译产物)、status(16, drag_status 0草稿/1启用)、 created_at、updated_at。索引 idx_drag_graph_name/status。 - `drag_template`:id、name、category(32, 通用/移动/弹球/计时)、description、 blocks(text)、connections(text)、created_at、updated_at。 - 编码:drag_status(0 草稿 / 1 启用)→ appcodes(init/data.json Format B)。 ## 块契约(block JSON) ```json {"type":"action_move","category":"action","label":"移动","icon":"fa fa-arrows-alt", "params":[{"name":"entity","label":"目标实体","uitype":"entity","required":true}, {"name":"x","label":"X 位移","uitype":"number","default":"0"}], "ports":{"inputs":[{"name":"in","label":"上一步"}], "outputs":[{"name":"out","label":"下一步"}]}} ``` - 事件块(category=event)无输入端口 = 天然入口,不能作为连线目标。 - 分叉/并行块用 `dynamic_outputs={"param":"branch_count","prefix":"branch"}` 声明动态端口。 - 参数 uitype:text/number/select/boolean/entity/expression/variable/textarea。 - 表达式参数支持 `@变量名` 引用(validator 校验已创建变量)。 ## 关键端点(/drag/api/*.dspy,宿主挂载后 /drag/api/*.dspy) | 端点 | 方法 | 入参 | 返回 | |------|------|------|------| | block_defs.dspy | GET | - | {categories, blocks} | | graph_save.dspy | POST | {id?, name, blocks, connections} | {success, id, valid, errors, content} | | graph_list.dspy | GET | {page, rows, name?, status?} | {list, total} | | graph_get.dspy | GET | {id} | {data:{blocks, connections, content}} | | graph_delete.dspy | POST | {id} | {success} | | graph_compile.dspy | POST | {name?, blocks, connections} | {success, content, entry_points} | | graph_validate.dspy | POST | {blocks, connections} | {valid, errors:[{code,message,block_id,field}]} | | graph_publish.dspy | POST | {id, script_name?} | {success, script_id, script_name} | | template_save/list/delete/use.dspy | - | - | - | | get_search_entity.dspy | GET | - | [{value,text}] 实体下拉 | ## 错误码 PARAM_REQUIRED / PARAM_TYPE / PARAM_RANGE / PARAM_OPTION / INVALID_IDENTIFIER / VAR_UNDEFINED / FUNC_UNDEFINED / BLOCK_UNKNOWN_TYPE / NO_ENTRY / ORPHAN_BLOCK / UNREACHABLE_BLOCK / CONN_BLOCK_NOT_FOUND / CONN_PORT_NOT_FOUND / CONN_DUPLICATE / CONN_SELF_LOOP / CONN_CYCLE / EVENT_AS_TARGET / NOT_FOUND / DB_ERROR / COMPILE_ERROR ## 模块陷阱 - 取库名:.py 用 `ServerEnv().get_module_dbname('drag')`;.dspy 直接 `get_module_dbname('drag')`。 - dspy 无 import/print/uuid;显式 return;helper 在 init.py;三处同步注册 (drag/__init__.py 导出 ← drag/init.py 实现 ← load_drag() env.xxx 注册)。 - sor.C/U/D/R + sqlPaging;created_at/updated_at 必须显式设置。 - CRUD 复数别名 create_drag_graphs 等已注册(CRUD 框架约定)。 - 编译产物仅白名单语法(赋值/def/for/while/if-else/函数调用/注释), 动作函数(move/rotate/scale/set_property/play_animation/play_sound/show_hide/camera/wait) 需宿主演绎层注册到 script_engine 白名单。 - 前端 .js/.css 必须在 load_path.py 注册 any 角色,否则 403。 - 修改 blocks.py 后前端自动通过 block_defs.dspy 拿到新定义,无需改前端。 ## 验收口径(W-09a~W-09au) - 拖块连块存画布:graph_save 落库 drag_graph,blocks/connections JSON 往返一致。 - 编译产出可执行脚本:graph_compile 返回 Python 源码,无校验错误时可被 script_engine 执行。 - 校验能报孤立块/无入口:graph_validate 对孤立块报 ORPHAN_BLOCK、无事件块报 NO_ENTRY。 - 正反用例按 feature-granularity-and-testing:每功能点至少 1 正例 + 1 反例。