8 类图手工编辑功能
目标:让 arch / flow / sm / erd / archd / flowd / seqd / laned 8 类图具备类似页面(GrapesJS)的手工编辑能力 —— 直接改文字、选中节点/连线、发给 agent 修改。先设计,先讨论,不动代码。
0. 现状摘要(摸底结论,先对齐事实再设计)
| 项 | 现状 |
|---|---|
| 8 类图归属 | 建模域(DOMAINS.build)下:arch / flow / sm / erd / archd / flowd / seqd / laned(workbench-panel.js:2286-2293) |
| arch/flow/sm 渲染 | diagram.js React 组件直接渲染 SVG(in-proc,非 iframe) |
| erd/archd/flowd/seqd/laned 渲染 | 各自 renderer(mermaid-renderer / flowchart-renderer / swimlane-renderer / tech-arch-renderer 等)生成完整 HTML,落盘到 proto-projects.json.charts[type].html,iframe srcdoc 显示 |
| arch/flow/sm 选中 | 已有 protoSel state + 样式面板(节点/连线 fill/stroke/strokeWidth/rx/dash)+ "对话修改" 按钮(chip 注入到 DSH 输入框) |
| arch/flow/sm 缺失 | 文字直接编辑(contenteditable)、节点/连线增删、拖拽、撤销栈、纯键盘操作 |
| 5 类 ChartView | 完全无选中、无编辑,只有 hover 高亮 + 工具栏(主题/PNG/SVG/重播) |
| 数据源 | arch/flow/sm ↔ protoDiagram.modules/relations + stateMachine.states/transitions;5 类 ChartView ↔ buildChartJson 实时从 pages/dataModel/ends/stateMachine 生成 + proto.charts.save 落盘 |
| 瞄准 → AI 链路 | pick.js(页面 GrapesJS iframe 用) → aiInject(msg) → findComposer()/setComposerValue() 注入到 DSH 对话输入框。架构图已有同款:选中 → "对话修改"按钮 → setComposerValue(ctx) |
| 数据回流 | arch/flow/sm 走 proto.setDiagram({diagram, keepMeta:true})(workbench-panel.js:6680);5 类 ChartView 走 proto.charts.save |
| 撤销栈 | 没有(页面 GrapesJS 自带,但图完全没) |
关键发现:arch/flow/sm 已有"半套"编辑能力(样式 + AI 协作),缺的是文字 + 增删 + 撤销;5 类 ChartView 是"零编辑"。
1. 设计目标
- 文字即点即改 —— 单击文字进入编辑态,blur/Enter 提交。
- 选中节点/连线显示操作面板 —— 删除、改样式、复制、改文字(已经有样式的补"改名"和"删除")。
- 选中后能发给 agent —— 复用现有
aiInject,把"节点上下文 + 用户一句话"塞进 DSH 输入框。 - 节点/连线增删 + 拖拽 —— 双击空白新增节点;拖节点改位置(arch);拖连线端点改连接。
- 撤销栈 —— Ctrl/Cmd+Z / Shift+Z;最起码每图 50 步。
- 数据回流 —— 编辑后写回
proto-projects.json,刷新后保留。
不做的(明确边界)
- 不做 "所见即所得的跨图联动"(改 ER 字段自动同步到状态机 —— 这是业务规则层,应该由 agent 改,不该由人手同步)。
- 不做 "图布局引擎升级"(保留 arch 的斜向对角线、flow 的斜向、sm 的状态机布局 —— 自动布局不在此范围)。
- 不做 "多人协同 / 冲突合并"(单用户编辑,自动保存最后写的赢)。
- 不做 "插件系统支持第三方自定义节点形状"(节点形状按各图类型内置固定集)。
- 不做 "图与页面原型双向跳转的自动生成"(已有 link/relatedPages 单向关联,不做反向)。
2. 8 类图的可编辑对象矩阵
为什么分两类:arch/flow/sm 是 in-proc React,erd/archd/flowd/seqd/laned 是 iframe srcdoc —— 编辑实现路径根本不同(一个是改 React props,一个是改 iframe DOM + postMessage 回传)。下文称前者为内核图、后者为渲染图表。
2.1 内核图(arch / flow / sm)
| 图 | 节点对象 | 节点编辑 | 连线对象 | 连线编辑 | 特殊 |
|---|---|---|---|---|---|
| arch | module(业务模块) | 改名、改 desc、改 relatedPages、删/增 | relation(跨模块数据流) | 改 label、删、增(同层关系才画,跨层由 parent 表达) | 拖动节点改位置(斜向对角线布局 → 自由拖拽后要重新对齐?见难点 1) |
| flow | page(页面节点) | 改名、删/增(增会自动跳到页面编辑) | data-goto 边 | 改 label、删、增 | 节点 click → 打开页面(已有 onNodeClick) |
| sm | state(状态) | 改名、删/增、改关联 pageId | transition(迁移) | 改 trigger / guard、删、增 | 节点 click → 跳到关联 page(已有) |
2.2 渲染图表(erd / archd / flowd / seqd / laned)
| 图 | 节点对象 | 节点编辑 | 连线对象 | 连线编辑 | 特殊 |
|---|---|---|---|---|---|
| erd | table(含字段行) | 改 table 名、改字段名、改字段类型、增删字段、改字段注释、删表 | fk 关系 | 删(FK 由字段自动推导,不能手动增) | 改字段类型会触发"PK/FK 标记重算" |
| archd | module | 同 arch 的 module 编辑 | link(跨模块数据流) | 同 arch | 数据源是 protoDiagram(同 arch),但渲染走 Archify |
| flowd | node(业务步骤) | 改名、kind(process/decision/start/end)、删/增 | edge | 改 label、改 style(solid/dashed)、删、增 | 节点 type 已限定(start/end/process/decision) |
| seqd | actor(参与者) | 改名、删/增 | message | 改 label、删、增 | actors 归一化到 L3(修改会触发重归一) |
| laned | step(泳道步骤) | 改名、改 kind、删/增、跨泳道拖动 | connection(跨泳道连线) | 改 label、改 style、删、增 | 跨泳道拖动会触发 lane 归属变化 |
3. 通用编辑模式(四层叠加)
每一类图的编辑 UI 都由这四层叠加组成,缺哪一层看具体图的复杂度。
┌────────────────────────────────────────────────────────┐
│ L1 直接编辑(文字即点即改) │
│ - 单击文字 → contenteditable → blur 提交 → 写回 spec │
├────────────────────────────────────────────────────────┤
│ L2 选中态(高亮 + 操作面板) │
│ - 点节点/连线 → store.protoSel / chartSel │
│ - 浮动面板:改名/删/改样式/复制/AI 协作 │
├────────────────────────────────────────────────────────┤
│ L3 增删拖拽 │
│ - 工具栏按钮 / 右键菜单 / 双击空白 │
│ - 拖动节点改位置 / 拖连线端点改连接 │
├────────────────────────────────────────────────────────┤
│ L4 协作回传(chip → AI) │
│ - 复用 aiInject → 选中上下文 + 用户一句话进 DSH │
│ - agent 修改后调 proto.setDiagram / proto.charts.save │
└────────────────────────────────────────────────────────┘
统一交互规则(所有图通用)
- 单击 节点/连线 → L2 选中
- 双击 文字 → L1 编辑;双击空白 → L3 新增(按图类型给默认值)
- 拖动 节点(非 arch)→ L3 移动位置
- Delete/Backspace 选中时 → 删除(确认 toast:3 秒可撤销)
- Cmd/Ctrl+Z / Shift+Z → 撤销/重做
- Esc → 取消选中 / 取消编辑
- Cmd/Ctrl+D 选中节点 → 复制(同 parent 下加副本)
4. 内核图(arch / flow / sm)的具体设计
4.1 文字即点即改(L1)
实现思路:双击节点文字 → 把当前 元素切换成 contenteditable ,blur/Enter 提交。问题是 diagram.js 当前用 SVG (不是 HTML),改文字后 SVG 不会自动重新布局。两种处理:
- 方案 A:编辑期换成 HTML
,提交时读 innerText 回到 SVG,调用diagram.js的 layout() 重算位置。 - 方案 B(推荐):始终用 SVG
但通过 textContent 直接改(SVG是可以 contenteditable 的,只是浏览器支持参差)。Firefox 支持差,需要 polyfill。
建议:方案 A,更稳。
4.2 选中态面板增强(L2)
当前已有样式面板(fill/stroke/dash),补:
- 改名输入框(text input) —— 改 module.name / page.name / state.name
- 删除按钮 —— 二次确认(3 秒 toast 可撤销)
- 复制按钮 —— arch/flow 用,sm 不用(迁移是状态的边,复制状态会破坏迁移 from/to)
- 关联页面跳转(flow/sm 已有,arch 用 module.relatedPages 多选)
- "对话修改"按钮 —— 已有,复用 + 改 chip 文本("【体系结构·订单模块】desc:处理订单生命周期 ……"
4.3 增删拖拽(L3)
新增节点:
- arch/flow:双击空白 → 弹小输入框("模块名:___")→ 创建 module/page,自动加 parent(arch 取当前选中节点的 parent,flow parent=null)
- sm:双击空白 → 弹"新状态名 + 初始/中间/终态"三选一 → 创建 state
新增连线:
- 模式:选中源节点 → 出现"拉连线"手柄(节点边缘 8 向)→ 拖到目标节点 → 弹"label 输入" → 创建
- 备选:arch/flow 不一定需要手动拉(可以从 agent 一句话生成)
拖动节点(仅 arch):
- 鼠标按下 → 进入拖拽态 → 实时更新 layout.x/layout.y
- arch 的 layout() 是对角线递归布局,加一个
overridemap 存用户自由位置,layout() 优先用 override - 拖动结束 → 持久化到
protoDiagram.modules[i].layoutOverride = {x, y}
删除节点:
- 弹 toast:"已删除模块 X,3 秒内可撤销" + 撤销按钮
- 删除同时清理下游引用:
- arch:删除 module → 删除所有 relations 里 from/to 指向它的 + 子 module 的 parent 改为 null(或上一个有效 parent)
- sm:删除 state → 删除所有 transitions 里 from/to 指向它的
4.4 撤销栈(L4 撤销/重做)
栈结构:每张图一个 undoStack: Array<{spec, label}> + redoStack: Array<...>,每次写回 store 时推一条,容量 50。
关键决策:
- 撤销粒度:一次操作一次入栈(连续拖动 mousemove 期间不入栈,mouseup 时入栈一条)
- 跨图撤销:不做(,每次写回 store 时推一条,容量 50。
关键决策:
- 撤销粒度:一次操作一次入栈(连续拖动 mousemove 期间不入栈,mouseup 时入栈一条)
- 跨图撤销:不做(撤销只作用于当前打开的图;arch 撤销不影响 sm)
- 自动保存与撤销的关系:自动保存是"把当前 store 写回 proto-projects.json",不入撤销栈(因为这是持久化不是编辑)
4.5 数据通路
用户操作 → setProtoSel(...) / 直接改 React state ↓ 入撤销栈 (label='改名:订单模块 → 订单') ↓ 触发 proto.setDiagram({diagram, keepMeta:true}) ↓ 服务端更新 proto-projects.json.diagram ↓ loadProto() 重拉 → arch/flow/sm 用新 spec 重渲染
5. 渲染图表(erd/archd/flowd/seqd/laned)的具体设计
最大难点:这 5 类图当前是 iframe srcdoc,编辑事件必须在 iframe 内响应,但状态在 workbench React 里。
5.1 双向通信架构
workbench (parent) iframe (child) ┌────────────────────┐ ┌──────────────────────┐ │ React state │ │ mermaid/flowchart/ │ │ chartSel / chartOp │ ──postMessage──▶│ swimlane HTML │ │ chartUndoStack │ ◀──postMessage──│ DOM 事件 │ │ chartToolbar │ │ 选中/双击/拖拽/Delete│ └────────────────────┘ └──────────────────────┘ │ │ └─────── proto.charts.load RPC ◀────────┘ (编辑后 save → 重 load → 重渲染)5.2 通信协议(postMessage)
// parent → child { kind: 'init', chartType, editMode: true, sel: {type,id} } { kind: 'applyOp', op: { type: 'renameNode', id: '...', newName: '...' } } { kind: 'applyStyle', target: {type,id}, style: {...} } { kind: 'highlight', target: {type,id} } { kind: 'reload' } // 重 load 后通知 child 重新挂事件 // child → parent { kind: 'ready' } { kind: 'pick', target: {type, id, ...meta} } // 选中元素 { kind: 'editText', target: {type,id}, newText } // 文字编辑完成 { kind: 'requestOp', op: {...} } // 子端无法处理的 op(如删除字段)→ 父端调 RPC5.3 mermaid 注入编辑能力(以 ER 为例)
mermaid erDiagram 生成的 SVG 节点是
含+ 字段行。需要:- 给每个节点/字段/关系附加
data-node-id/data-field-name/data-rel-id - child 脚本:监听 iframe 内
dblclick/click/keydown,派发到 parent - child 脚本:监听 parent 的
init消息,给当前选中的元素加.selectedclass(描边)
关键:mermaid@11 的渲染输出 id 不可控(自动生成
entity-id-xxx),所以编辑态必须重新渲染后从 spec 反查(拿spec.nodes[].name反查 SVG text)。见难点 2。5.4 erd / archd / flowd / seqd / laned 各自编辑对象
通用编辑协议用一份子端脚本(
src/client/chart-edit-runtime.js),5 类图各自有"节点/连线识别规则":图 节点识别 连线识别 文字节点 erd SVG 含 classentityBox,data-node-id = table.namemermaid 路径 + spec.relations 反查 含 classentityLabel(字段行也是)archd Archify 渲染产物(不是 mermaid),结构不同,需要读 Archify DOM 结构 Archify link 路径 Archify card label flowd flowchart-renderer.js 生成的 已经有 data-idtext 内容 seqd mermaid sequenceDiagram 的 消息线 消息 text laned swimlane-renderer.js 生成的 已经完美!node label 好消息:flowd / laned 的渲染产物已经带了
data-id,编辑识别最简单。erd / seqd 是 mermaid,结构最复杂(要反查)。archd 是 Archify,最不可控(见难点 3)。5.5 数据通路
用户在 iframe 内编辑 ↓ (postMessage) parent React 收到 op ↓ 校验(op 是否合法 / 是否破坏引用完整性) ↓ 更新 spec(nodes/edges) ↓ proto.charts.save({type, html, spec, ir}) ← 关键:html 重生(renderer 重跑) ↓ 落盘成功 → 重 load + 通知 iframe reload双轨数据:
- spec/ir(结构化)—— 编辑后必须更新 spec/ir
- html(可视化)—— spec 变了 html 必须重生成;renderer 重跑 5-50ms 之内
5.6 数据源头冲突(难点 4)
5 类 ChartView 的 spec 是
buildChartJson实时生成的(pages/diagram/stateMachine/dataModel)。如果用户改了 ER 图的字段名(比如totalSpent→totalSpend),下游会乱:stateMachine里如果有totalSpent引用 → 失效dataModel.entities[].fields[].type = 'ref:X'里如果有 → 失效proto.charts.load之后 buildChartJson 会用新字段名重新生成,覆盖用户编辑
解决:
- 优先级:用户编辑 > 自动推断。即编辑后的 spec 落盘后,buildChartJson 应该 detect 到"已有手工编辑的 spec",不再覆盖字段内容。
- 实现:spec 里加
userEdited: true标记,buildChartJson 检测到就跳过该字段。 - 更彻底:把 5 类 ChartView 的"真数据源"从 buildChartJson 改为"落盘的 spec"(buildChartJson 只在没 spec 时生成默认)。
5.7 撤销栈
5 类 ChartView 的撤销栈在 parent(workbench React),不进 iframe。每条 op 入栈,重做时反向 op。
6. 关键技术难点与对策
难点 1:arch 拖动节点 vs 斜向对角线布局冲突
问题:arch 的 layout() 是递归对角线布局,节点位置是算出来的。用户拖动一个节点到任意位置后,其他节点的位置是跟着 layout() 还是跟着被拖节点?
对策:
- 引入
layoutOverride: { x, y }字段(per module)。layout() 优先用 override,没有 override 的走对角线递归。 - 拖动时只更新当前节点的 override;其他节点保持 layout() 结果。
- 提供"对齐模式"按钮:点一下清除所有 override,回到纯 layout()。
- 拖动视觉:被拖节点脱离布局层(z-index 提升 + 阴影),其他节点不跟。
难点 2:mermaid 渲染产物的元素 id 不可控
问题:mermaid@11 erDiagram 渲染出来的
没有可读 id,只能通过 textContent 反查表名/字段名。对策:
- 编辑态重新渲染:编辑完成时不要局部改 DOM,整体重渲染一次(renderer 重跑 5-50ms 可接受)。
- 渲染后挂事件:renderer 输出后调一遍
attachChartEditHandlers(iframe, spec),用textContent === spec.nodes[i].name反查节点。 - 节点 text 模糊匹配:表名可能有空格(
Customer { ... }→),匹配时Customer text.textContent.trim() === name。 - 字段行:字段行格式是
type name "comment",多行 text,要逐行 parse 后用field.name反查。
难点 3:archd(Archify)渲染产物不可控
问题:Archify 是第三方库(
/proto-vendor/archify/),生成的 SVG DOM 结构稳定但文档稀缺,可能版本变动。对策:
- 不直接编辑 Archify 产物:archd 的"编辑"通过反向同步到
protoDiagram.modules/relations实现 —— 用户改 archd 的 label → 更新 module.name → 重渲染 Archify。 - 检测 Archify 版本:在 archd 子端脚本里检测 Archify 的 window 全局对象版本号,写到 spec 里,渲染时兼容。
- 降级方案:archd 不可编辑时显示"请到 arch 视图编辑",点击跳 arch。
难点 4:buildChartJson 自动推断 vs 用户编辑
问题:见 5.6。
对策(已在 5.6 详述):spec 加
userEdited标记,buildChartJson 检测后跳过。难点 5:删除的级联清理
问题:删 arch 模块 → 所有 related relations 要删 + 子模块 parent 改 null;删 sm 状态 → 所有 related transitions 要删;删 ER 表 → 所有 FK 字段要从其他表移除(否则 references break)。
对策:
- 单一函数
cascadeDelete(spec, type, id),按类型走对应分支 - 删除前展示"将影响 X 个 relations / Y 个字段",二次确认
- undoStack 同时记录被删除的对象(不是只记"删除"事件),恢复时全量还原
难点 6:iframe srcdoc 与 parent 双向通信的可靠性
问题:iframe 重渲染时 srcDoc 变化会销毁所有子端脚本和事件监听,每次保存后都要 re-attach。
对策:
- iframe
onload触发 → child postMessageready→ parent 回init+sel+highlight状态 - child init 后 50ms 内挂事件(给 mermaid 渲染留时间)
- 如果 init 没收到(child 早于 parent 监听),child 定时重发
ready5 次(200ms 间隔) - parent 在 proto.charts.save 成功后先等 100ms(让 child 重渲染)再发
reload
难点 7:撤销栈的存储成本
问题:每张图 50 步 undoStack,每步存整个 spec/IR —— 8 张图 × 50 步 × 几 KB = 几 MB,全在内存。
对策:
- undoStack 存差量(delta op)而不是完整 spec —— 反向 op 就是 redo 起点
- 限制每张图 50 步、跨图共享一个 LRU cache(最近用的 4 张图保留,全用的 GC)
- 自动保存时清空 undoStack(持久化状态被认为是 checkpoint)
难点 8:键盘快捷键冲突
问题:Delete / Cmd+Z / Esc 等和 DSH 全局快捷键冲突。
对策:
- 仅在"图视图聚焦"时(鼠标在图容器内 / 选中态)响应这些快捷键
- 用
event.target判断:如果事件冒泡到 DSH 顶栏(Composer),不响应 - 提供"快捷键说明"小图标,点击展开
7. 与现有"瞄准 → 对话"机制的协同
场景 现有机制 新机制 选中节点后想让 AI 改 "对话修改"按钮 → aiInject 注入 保留。新增"快捷键 Cmd+I" 直接打开对话并注入。 选中节点改样式 样式面板手动改 保留。新增"AI 改样式"按钮 → aiInject 注入"【module】改成红色填充"。 整图让 AI 重画 工具栏"AI 补全"按钮 保留。注意:用户手工编辑过的图,AI 重画前应提示"将丢失 23 个手工修改,是否继续?" 冲突点:AI 重画会清空手工编辑。解决:手工编辑过的 spec 加
userEdited: true+ 数量计数;AI 重画前若userEdited && count > 0→ 弹确认。
8. 分阶段落地(建议)
Phase 主题 估时 Phase 1 内核图文字编辑 + 改名 + 删除 + 撤销栈 1 周 Phase 2 内核图增删拖拽 1 周 Phase 3 5 类 ChartView 编辑基础 2 周 Phase 4 5 类 ChartView 编辑增强 2 周 Phase 5 协作 AI 1 周 总估时:~7 周(一人)或 4 周(两人并行)。
注:v0.2 后续对阶段做合并——见第二部分"分阶段实施计划",Phase 1 实际拆为 P1.0(探路)+ P1.A + P1.B 三步。
9. 风险与开放问题
9.1 风险清单
风险 影响 缓解 Archify DOM 变动 archd 编辑失效 检测 Archify 版本 + 降级跳 arch mermaid 升级破坏 SVG 结构 erd / seqd 编辑失效 锁定 mermaid@11 + 编辑挂事件用通用反查 iframe 性能(5 个 ChartView 都有) 工作台卡顿 按需渲染(ChartView 展开才挂 iframe srcDoc) 撤销栈内存膨胀 OOM 差量存储 + LRU 多用户协同编辑冲突 数据丢失 单用户场景不做;多用户场景强制 read-only 9.2 开放问题(需要确认)
- arch 的 layoutOverride 是否需要跨用户/跨会话同步? —— 默认是;如果想同步,加到
protoDiagram.modules[i].layoutOverride。 - sm 的 guard 表达式能否在手工编辑里写? —— 涉及语法解析,建议先不支持(让 agent 写)。
- chart 内编辑的字段名改后,是否要联动更新
stateMachine里ref:X? —— 默认不动(人工改后让 agent 跟进)。 - 撤销栈的"全图重画"是否回退到重画前? —— 是,但要在重画前确认。
- 图表内的"AI 协作"上下文是否包含完整 spec? —— 不含(太大),只含选中元素的 spec + 全图缩略 JSON。
10. 测试用例(验收清单)
每类图至少 3 条:
内核图(arch/flow/sm)
- 双击节点文字 → 进入编辑 → 改字 → blur 提交 → 节点显示新字 + store 更新
- 选中节点 → Delete → toast 弹出 → 3 秒内点撤销 → 节点恢复
- 选中节点 → Cmd+I → DSH 输入框出现【节点上下文】
5 类 ChartView
- 工具栏打开"编辑"开关 → 双击表格字段 → 编辑 → 提交 → spec 更新 + iframe reload
- 选中 FK 连线 → Delete → 删除成功 + 其他表的 FK 字段标记被清理
- 编辑 ER 表 → 改字段名 → 关闭再打开 → 改动保留(userEdited 防被 buildChartJson 覆盖)
11. 已确认决策(review 后定稿)
决策点 选项 arch 拖动节点的位置同步? 决策 不同步。 layoutOverride仅本会话内存,刷页面/换用户后还原对角线布局。sm 的 guard 表达式手工可编辑吗? 决策 不支持。transition 面板只改 trigger/label,guard 字段只读,复杂 guard 让 agent 写。 改 ER 字段名是否联动修复引用? 决策 不联动。改完提示"如有引用请让 AI 跟进",不自动改 stateMachine/ref/页面 HTML。 AI 全图重画走撤销栈吗? 决策 重画入撤销栈。"AI 补全"作为普通 undo 项可 Cmd+Z 退回,栈按 50 步上限 + 差量存储避免膨胀。 AI 协作上下文大小? 决策 选中元素 + 上下游 1 跳 + 全图缩略 JSON(节点 id+name + 连线 from/to)。预估 1-10KB chip。 实施含义(Phase 1 必须遵守)
- layoutOverride 实现为 workbench store 的
protoLayoutOverrides: {[moduleId]: {x,y}}—— 不写到protoDiagram.modules[i],不持久化。刷新后丢失。 - sm 的 guard 输入框 disabled,hover 显示"复杂条件请让 AI 添加"。
- ER 字段改名后弹一个小提示(不阻塞):"字段名
X → Y已保存。如有引用该字段的 ref/状态机/页面,请让 AI 跟进。" - AI 重画 op 写入
chartUndoStack(差量),大小按完整 spec 估算 ≤ 50KB/op × 50 = 2.5MB 上限,可接受。 - chip payload 模板:
{selected: {type,id,name,style,neighbors:[{id,name,relation}]}, sketch: {nodes:[{id,name}], edges:[{from,to,label}]}}—— 总大小 1-10KB。
附录 A:现有 anchor 资源
- 8 类图视图定义:
workbench-panel.js:2282-2303 - buildChartJson:
workbench-panel.js:2334-2577 - protoSel 现有面板:
workbench-panel.js:6524-6680 - aiInject:
workbench-panel.js:4559-4570+panel.js:43-110 - 渲染器:
mermaid-renderer.js/flowchart-renderer.js/swimlane-renderer.js/tech-arch-renderer.js - diagram.js(arch/flow/sm 内核):
src/client/diagram.js
附录 B:编辑事件字典
type Op = | { type: 'renameNode'; id: string; newName: string } | { type: 'deleteNode'; id: string; cascade?: boolean } | { type: 'addNode'; node: SpecNode; parent?: string; position?: {x:number,y:number} } | { type: 'moveNode'; id: string; position: {x:number,y:number} } // arch only | { type: 'duplicateNode'; id: string } | { type: 'renameEdge'; edgeIndex: number; newLabel: string } | { type: 'deleteEdge'; edgeIndex: number } | { type: 'addEdge'; from: string; to: string; label?: string; style?: 'solid'|'dashed' } | { type: 'setStyle'; target: {type:'node'|'edge'; id?:string; edgeIndex?:number}; style: Record} | { type: 'setEdgeEndpoints'; edgeIndex: number; from?: string; to?: string }; // sm only
分阶段实施计划
总目标:8 类图手工编辑能力。先 Phase 1(内核图),再后续 phase(5 类 ChartView)。
切分原则:水平切分(按能力分阶段)—— 每阶段交付一项可独立验收的能力,避免过早铺满多个图导致返工。
v0.2 更新:基于项目能力盘点后调整——
- 14 项可复用点已梳理(详见第 12 节)
- P1.0 三个 POC 草稿设计同步调整(参考
vendor/pdfjs/CommandManager、复用editor.UndoManagerAPI 思路) - 撤销栈手写轻量版(op-based + pointer 索引),保持零新依赖
- P1 分 3 阶段:P1.0 → P1.A → P1.B,不估时,走完为准
总览
阶段 主题 验收一句话 P1.0 技术探路(3 个 POC) SVG 文字编辑 / 撤销 op 序列化 / Cmd+Z 拦截 三项技术验证全过 P1.A 文字编辑 + 改名 + 删除 双击改字 / 选中改 name / 删除带 3 秒撤销 P1.B 撤销栈 + 数据通路 + 回归 Cmd+Z 逐步回退 5 步;sm guard 只读;smoke test 7 条全过 P2 双击空白新增 + 工具栏 双击空白能新建节点;arch 新节点走 layoutOverride P3 arch 拖动节点 + arch 选中节点 Delete + cascadeDelete arch 节点可拖,跨用户不保留;删除清理子节点/relations P4 ChartView 编辑基础(flowd/laned)+ postMessage 协议 编辑开关开启后双击元素能改字;删除节点 P5 ChartView 编辑增强(erd/seqd/archd)+ 撤销栈 mermaid 渲染反查可改;userEdited 标记防被覆盖 P6 AI 协作(Cmd+I 注入、AI 改样式)+ 保护 选中节点 Cmd+I 注入 chip;AI 重画入栈可逆 P1.0 技术探路(3 个 POC)
放在
tests/diagram-edit-poc/(已有 POC 目录)。POC 1:
poc-text-edit.html目的:验证 SVG
↔切换的视觉对齐。输入:单个 arch 节点(含 module 名 + desc),双击切 foreignObject。
验收:
- 字体、字号、字色、行高与原 SVG text 一致
- foreignObject 宽度自适应(长节点名截断/换行行为合理)
- 粘贴富文本被剥离(监听 paste → text/plain)
- Esc 取消、Enter 提交、blur 提交
POC 2:
poc-undo-deltas.mjs目的:验证 op-based 撤销栈的差量 op 序列化模式(参考 vendor/pdfjs CommandManager)。
输入:5 种 op(renameNode / deleteNode / addEdge / deleteEdge / setStyle),定义 applyOp + inverseOp 纯函数。
验收:
- 10 条 unit test:applyOp 后再 apply inverseOp 必回到原状态
- 连续 5 次 op → undo × 5 → 回到原状态;redo × 5 → 回到操作后
- setStyle op 含"合并策略"(连续改 color picker 500ms 内合并为一条)
- 容量 51 时最早一条不进栈(LRU)
POC 3:
poc-keyboard-grab.html目的:验证 Cmd+Z 在 React + iframe 同框时的拦截可行性。
输入:模拟 workbench + GrapesJS iframe 同框布局。
验收:
- 全局
document.addEventListener('keydown')能捕获到 Cmd+Z - 用
e.target.closest('.proto-stylepanel')判定图视图聚焦(与editor-page.js:364同款) - 在 GrapesJS iframe 内聚焦时,不触发图编辑器的 Cmd+Z(让 GrapesJS 自己处理)
- 测试 vite/preview 模式下双向都不会被吞
P1.0 完成定义
三个 POC 全部跑通 + 结论文档化(
docs/design/poc-results.md)→ 你 review → 决定是否调方案 → 才进 P1.A。P1.A 文字编辑 + 改名 + 删除
做什么:
- 双击 arch/flow/sm 节点文字 → 切到 ...
- blur / Enter 提交 → 读 textContent → 写回 store
- Esc 取消(恢复原值)
- 仅改文字层;节点位置/样式不动
- 现有样式面板顶部加改名 input(双向绑定 protoSel.name ↔ store,实时改)
- 加删除按钮(红色 ghost):点击 → 弹 toast(3 秒可撤销)→ 确认删除
- 删除逻辑(仅删除,不含 cascade):arch 删 module / flow 删 page / sm 删 state(不动 relations 和子模块;P1.B 不清理下游)
- sm 单独写一个简化面板(guard 字段 disabled)
不做:cascade 清理 / 撤销栈 / 复制节点 / 新增节点
DoD:
- 双击 arch 任意模块节点 → 进入编辑态(输入框出现,光标就位)
- 改字 → Enter 或 blur → 节点显示新字,store 更新
- 改字中途按 Esc → 节点恢复原值,store 不变
- 三种图都能用
- 视觉:编辑态输入框样式与节点样式协调(边框/字号对齐)
关键文件:
src/client/diagram.js(Diagram 组件)src/client/workbench-panel.js(样式面板扩展、删除 toast、sm 简化面板)src/client/styles.js(编辑态 + toast + 按钮样式)
P1.B 撤销栈 + 数据通路 + 整体回归
做什么:
- 新增 store:
chartUndo: {[view]: Array<{op, label, ts}>}+chartRedo: {...} - 容量 50/view,存差量(delta op)而非完整 spec(决策 7)
- Cmd/Ctrl+Z / Shift+Z 全局监听,仅图视图聚焦时响应(决策 8)
- 自动保存时清空 redoStack(持久化是 checkpoint)
- AI 重画入 undoStack(决策 4)
- sm 的 guard 输入框 disabled(决策 2),hover 显示 tooltip "复杂条件请让 AI 添加"
- 数据通路验证:arch/flow 编辑 →
proto.setDiagram;sm 编辑 →proto.setStateMachine - RPC 失败处理:toast 错误,不入 undoStack
不做:跨图撤销 / 自动保存不入栈 / 真实 cascade
DoD:
- 连改 5 次 → 按 Cmd+Z 逐步回退 → 最终状态 = 改前
- Cmd+Z 后按 Cmd+Shift+Z → 重做
- 容量 51 次操作后,最早一次不进栈(LRU)
- 自动保存一次后 redoStack 清空
- 在页面编辑器(GrapesJS)聚焦时按 Cmd+Z 不响应(不影响 DSH 全局)
- sm 的 guard 输入框 disabled,hover 有提示
- 编辑后关闭该图再打开 → 改动保留
- 7 条 smoke test 全部通过
P2 双击空白新增 + 工具栏
做什么:
- 双击 arch/flow/sm 空白 → 弹小输入框("模块名:___") → Enter 确认 → 创建
- arch 新节点的 parent 默认取当前选中节点的 parent;flow parent=null;sm 标 kind(start/process/end)
- 工具栏加"+ 新增节点"按钮(同双击空白的入口)
DoD:双击空白 → 输入框 → 提交 → 新节点出现 + 自动选中;工具栏按钮可用
P3 arch 拖动 + 选中节点 Delete + cascadeDelete
做什么:
- arch 节点拖动:
mousedown→ 拖拽态 → mousemove 实时更新位置 → mouseup 入栈 - 引入
protoLayoutOverrides: {[moduleId]: {x,y}}(仅本会话内存,不持久化——决策 1) - arch 选中节点 Delete 键删除 + cascade:删 module → 删所有 relations 里指向它的 + 子 module parent 改 null
- 删除前展示"将影响 N 个引用",二次确认
DoD:arch 节点可拖,刷新页面后还原对角线布局;删除 arch 模块,所有相关 relations 自动消失
P4 ChartView 编辑基础
做什么:
- 实现 postMessage 协议(parent ↔ iframe)
- child 脚本:监听 dblclick/click/keydown 派发到 parent
- 工具栏加"编辑"开关(默认关,开则进入编辑态)
- 优先做 flowd / laned(已有 data-id,识别最简单)
DoD:开启编辑 → 双击 flowd 节点能改字 → 提交 → spec + html 都更新;选中 flowd 节点 → Delete → 节点消失;iframe 重渲染后事件能再次挂上
P5 ChartView 编辑增强
做什么:
- erd / seqd:mermaid 渲染反查(textContent === spec.nodes[i].name)
- archd:通过 Archify 渲染产物编辑,反向同步到 protoDiagram
- ChartView 撤销栈(5 类共用一个栈,按 chart type 分)
- spec 加
userEdited: true标记,buildChartJson 检测后跳过
DoD:erd 改字段名 → 关闭再打开 → 改动保留(不被 buildChartJson 覆盖);mermaid 升级不影响编辑能力(反查容错)
P6 AI 协作
做什么:
- Cmd+I 快捷键:选中节点 → 注入 chip 到 DSH 输入框(payload 包含决策 5:选中 + 1 跳邻居 + 全图缩略)
- AI 改样式按钮(节点/连线面板)
- AI 重画("AI 补全"按钮)前提示:"图已有 N 个手工修改,是否继续?"
- 重画入 undoStack(决策 4)
DoD:选中节点按 Cmd+I → DSH 输入框出现含上下文的 chip;AI 重画可 Cmd+Z 退回;userEdited 图被 AI 重画前有提示
切分原则说明
我选水平切分(按能力分阶段)而不是垂直切分(按图分阶段):
维度 水平切分(采用) 垂直切分(备选) 每阶段交付 一项能力在 8 类图通用 一个图端到端 阶段数 6 个 phase × 4 子阶段 ≈ 10 个里程碑 8 个 phase × 每个图 复用度 高(编辑能力代码一次写好,铺图) 低(每图重写) 早期可见性 中(要看 X 类图 + 能力) 高(一图完整可用) 风险 中(能力与图正交矩阵,铺开时可能漏) 高(返工成本高) 采用水平的理由:P1 阶段本身就只覆盖 arch/flow/sm 三张图(in-proc React),已经能在每个里程碑看到 arch 的进展;进入 P4-P5 才铺 ChartView,那时垂直切分会更合适(一张张铺)。
12. 项目可复用能力盘点(v0.2 增补)
✅ 直接复用(10 项)
- 选中态 store:
protoSel: {type,id,index,name,style,link}—— P1.A 改名 + 删除直接复用。 - 样式面板:
workbench-panel.js:6524-6680的proto-stylepanel—— P1.A 改名 input 加顶部、删除按钮加底部。 - AI 协作 chip:
aiInject+findComposer/setComposerValue(已存在)—— P6 Cmd+I 直接调。 - proto.setDiagram RPC:已有
keepMeta: true模式 —— arch/flow 编辑后直接调。 - GrapesJS UndoManager 参考:
editor.UndoManager.undo()API 形状 —— P1.B 撤销栈 op 列表 + pointer 思路。 - postMessage 协议样板:
proto-edit:command {cmd:'undo'/'redo'}(editor-page.js:364)+proto-edit:init/pages(workbench-panel.js:8598)—— P4 ChartView 编辑直接扩展命名空间。 - proto-anno:update/mode 协议:
workbench-panel.js:5664-5707—— postMessage 模式参考。 - toast / protoMsg:删除撤销 toast、RPC 失败 toast 直接用
store.protoMsg+setProtoMsg。 - CSS 主题/样式系统:编辑态样式加进
styles.js。 - vendor/pdfjs CommandManager:
vendor/amis/thirds/pdfjs-dist有完整 op-based undo/redo(带cmd / undo / post / overwriteIfSameType / keepUndo)—— P1.B 撤销栈设计的参考实现。
⚠️ 部分有、需要适配(4 项)
- SVG 文本编辑:项目内无先例,自己实现。
- svg/foreignObject 切换:项目内无先例,P1.0 验证。
- 撤销栈数据模型:项目内
charts._history(落盘历史,20 条上限)是落盘历史,与运行时撤销栈职责不同——需独立 store 字段chartUndo/chartRedo。 - SVG 拖拽:diagram.js 现有
selectable/onSelectprops,加draggableprop 即可。
❌ 完全没、需自实现(2 项)
- 撤销/重做库:手写轻量 op-based 栈(差量 op + pointer 索引 + 合并策略),保持零新依赖。
- mermaid 反查 / 编辑能力:P1.0 POC 验证(反查 textContent 是无依赖方案)。
💡 关键避坑
lib/index.js/src/client/*.js是手写源码,lib/client.js是构建产物。改源码后必须跑node build.mjs才生效(ER 图 / 泳道图打包时踩过)。- store 模式是
React.useState + store.subscribe自研,不是 zustand/redux。撤销栈直接放 store 字段。 - sm 节点样式面板没渲染(
protoSel.type === 'node' && protoView !== 'sm'限制)—— P1.A 给 sm 加"改名 + 删除"需单独写一个 sm 面板。 charts._history≠ 撤销栈:前者管"已发版本",后者管"当前编辑会话"。
14. P1 阶段执行清单(最终版)
P1.0 探路(目标:1 天内跑完 3 个 POC)
- 创建
tests/diagram-edit-poc/poc-text-edit.html+tests/diagram-edit-poc/visual-diff.mjs(jsdom 截图 + pixel-diff 对比切换前后像素差 ≤ 2%) - 创建
tests/diagram-edit-poc/poc-undo-deltas.mjs(纯 nodeli> - 跑通三个 POC,写
docs/design/poc-results.md结论 - 暂停:等用户 review POC 结果
P1.A 文字编辑 + 改名 + 删除
- diagram.js:双击节点文字 → 切 foreignObject → blur/Enter 提交
- diagram.js:删除
protoSel节点的 textNode 在编辑态(双击其他文字前) - workbench-panel.js:样式面板顶部加
绑定 protoSel.name(arch/flow/sm 三图) - workbench-panel.js:样式面板底部加"删除"按钮(红色 ghost) + 3 秒 toast 撤销
- workbench-panel.js:sm 单独写一个简化面板(guard 字段 disabled)
- styles.js:编辑态 + toast + 按钮样式
- 暂停:等用户 review
P1.B 撤销栈 + 数据通路 + 回归
- workbench-panel.js:store 加
chartUndo: {[view]: Array<{op,label,ts}>}+chartRedo: {...}字段 - workbench-panel.js:
commitEdit(view, payload)统一函数(arch/flow/sm 各自 RPC 路径) - workbench-panel.js:Cmd+Z / Shift+Z 全局 keydown 监听(仅图视图聚焦时响应)
- workbench-panel.js:自动保存时清空 redoStack
- workbench-panel.js:RPC 失败 toast + undoStack 那条弹出
- tests/diagram-edit-poc/smoke-test.mjs:7 条 smoke test 全过
- 完成:P1 收尾,进 P2
15. 启动检查清单(开始 P1.0 前)
- 当前分支是干净的(git status)
-
node build.mjs已跑过且成功(基线) -
cd packages/proto-workspace && pwd确认在包目录 - 工作台能正常打开 arch/flow/sm 图(基线可视)