跳转至

P1.UH-subgraph-ports-and-dblclick-fix · 子图端口推导 + 双击展开 hotfix(ADR-08 §议题④ 验收失败修复)

Worker:ClaudeA · 前端 (frontend_vue3) / 预计:1.5d / 优先级:P2(最复杂 · 但用户已建好骨架)/ 状态:dispatched 隔离:🧵 文件隔离(同 worktree 同 branch · 与同期 ClaudeA 两 hotfix 文件正交)


🔍 触发与解锁链

触发 状态 影响
用户验收 ADR-08 议题④ 失败 ✅ 2026-05-31 11:01 原话"子图永远无法展开 · 双击是编辑、单击无用 · 没生成输入输出端口 · 无法连线"
parent zombie P1.U-subgraph-canvas ✅ af945e2 子图画布 modal + 3 入口骨架已落 · 但核心交互 + 推导算法有 bug
schema 已就位 P1.U-subgraph-schema-extend ✅ e93ef27 types/subgraph.ts SubgraphDefinition / SubgraphPort / SubgraphNodeInstance 已落

→ 纯前端 · 不修改 schema · 仅修推导算法 + 交互行为


任务定义

修复 ADR-08 议题④ 子图两大用户实测问题: - ① 子图永远无法展开:双击应进入子图编辑 · 实测"双击是编辑、单击无用"(用户原话表述 confused · 实际可能是双击进入子图画布失败 · 仅触发了 PropertyPanel 编辑) - ② 没生成输入输出端口:多选封装为子图后 · SubgraphNodeInstance 没有 inputPorts/outputPorts · 无法与父画布其他 module 连线

核心范围: 1. 修 useSubgraph.tscreateSubgraphFromSelection 推导算法:正确识别外→内连线为 inputPorts、内→外连线为 outputPorts 2. 修 SubgraphNodeInstance 在父画布的端口渲染:推导出的 inputPorts/outputPorts 必须在节点四周显示为可连接的端口 3. 修双击行为:双击 SubgraphNodeInstance → 切换到子图画布(面包屑 Main / SubgraphA)· 不是触发 PropertyPanel 4. 修单击行为:单击 SubgraphNodeInstance → 选中 + 高亮(同其他 module) 5. e2e 真值:多选 3 module + 1 connection → 封装 → SubgraphNodeInstance 出现端口 · 双击 → 子图画布打开


完整 prompt(直接复制粘贴 worker 终端)

[U-thread]   P1.UH-subgraph-ports-and-dblclick-fix(alias: P1.U-subgraph-ports-and-dblclick-fix)
[部门]       前端 (frontend_vue3) · 推荐 skill: vuejs-typescript-best-practices
[Worker CWD] d:/work/25_claude/workspace/AlgoDepartment/04_development/
[Occupies]   P1.K-xilink-canvas(写 · 双击+单击+端口渲染) + P1.K-xilink-toolbar(read · 入口 1 不动) + P1.K-module-library(read)
[隔离]       🧵 文件隔离 · 仅写:
             - frontend_vue3/src/stages/xilink/composables/useSubgraph.ts(修推导算法)
             - frontend_vue3/src/stages/xilink/SubgraphCanvas.vue(修交互)
             - frontend_vue3/src/stages/xilink/SubgraphBreadcrumb.vue(若需)
             - frontend_vue3/src/components/canvas/(画布层 dblclick handler · 路径以现状为准)
             - frontend_vue3/tests/* 新增
             与同期 P1.UH-link-error-bottom-fix(bottom/*)+ P1.UH-xitest-meter-share-fix(drawers/*)文件正交
[优先级]     P2 · 1.5d · ADR-08 议题④ 用户验收失败修复(最复杂 · 但骨架已落)
[ADR]        d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/ADR/ADR-AIOS-08-xilink-stage-ux.md(必读 §2.5 子图三入口 + 双击编辑 + 端口推导算法)
[业务行为契约引用] ADR-08 §2.5 + 验收清单 C4-6~C4-12 · 用户原话"子图永远无法展开 + 没生成端口 · 无法连线"
[参考文档](绝对路径)
  - ADR-08:d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/ADR/ADR-AIOS-08-xilink-stage-ux.md(§2.5 子图三入口决议 · 含端口推导描述 · 双击编辑路径)
  - 验收清单:d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/ADR/ADR-AIOS-08-acceptance-checklist.md(议题④ C4-6~C4-12)
  - parent zombie:d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/prompts/done/P1.U-subgraph-canvas--af945e2.md(看 step 3 useSubgraph 推导算法实装 · step 4 三入口交互)
  - schema 就位:d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/prompts/done/P1.U-subgraph-schema-extend--e93ef27.md(SubgraphPort 8 字段 schema)
  - 现状必读:
    · frontend_vue3/src/stages/xilink/composables/useSubgraph.ts(全文 · 重点 createSubgraphFromSelection 推导算法)
    · frontend_vue3/src/stages/xilink/SubgraphCanvas.vue(全文 · 重点 dblclick / click handler)
    · frontend_vue3/src/types/subgraph.ts(SubgraphDefinition / SubgraphPort / SubgraphNodeInstance)
    · frontend_vue3/src/components/canvas/(画布主组件 · 找 ModuleNode 渲染处 · 端口渲染逻辑 · dblclick 监听)
    · frontend_vue3/src/stores/linkStore.ts(找 selectNode / handleNodeClick · 看单击/双击行为)

【背景】
ADR-08 议题④ parent zombie `af945e2` 完成了子图画布 modal + 3 入口 + cyclic 检查骨架 · 但用户 2026-05-31 11:01 实测两大问题:
- 原话"建立子图有按钮 · 也有(多选)变为子图 · 但是有两个主要问题:这个子图永远无法展开 · 双击是编辑、单击无用;子图没有生成输入输出端口 · 无法进行连线"

🚨 **真值核查 4 候选根因**(本 fork 第一步必须 grep 验证):
- ① **推导算法 bug**:createSubgraphFromSelection 没正确识别外/内连线 · inputPorts/outputPorts 数组为空
- ② **端口渲染缺失**:SubgraphNodeInstance 在父画布渲染时没把 inputPorts/outputPorts 渲染成可视端口
- ③ **双击 handler 错位**:dblclick 触发了 PropertyPanel 编辑(普通 module 行为)而不是 openSubgraphEditor
- ④ **节点类型识别错**:画布层判断 node.type === 'subgraph' 时分支错 · 走了普通 module 路径

**本任务核心**(1.5d · 修推导 + 端口 + 双击 + 单击 4 处):
- ✅ 修推导算法:外部连线(target 在 selectedNodes · source 不在)= inputPorts;内部连线(source 在 selectedNodes · target 不在)= outputPorts;每个 port 含 internalMapping 指向真实内部 module/port
- ✅ 端口渲染:SubgraphNodeInstance 节点四周显示推导出的 ports(左侧 inputPorts · 右侧 outputPorts · 同普通 module)· 可被父画布 edge 拖拽连接
- ✅ 修双击:dblclick subgraph 类型节点 → openSubgraphEditor(切到子图画布 · 面包屑 Main / SubgraphA)· 不触发 PropertyPanel
- ✅ 修单击:click subgraph 类型节点 → 高亮选中(普通 module 同款行为)· emit selectNode
- ✅ e2e 真值:多选 3 module + 1 connection → 封装 → SubgraphNodeInstance 显示端口 · 拖端口连接成功 · 双击进入子图

【执行步骤】

Step 1 · 真值核查(必跑 · 0.2d)
- git status / git pull origin xistudio --no-rebase
- cat frontend_vue3/src/stages/xilink/composables/useSubgraph.ts(全文 · 找 createSubgraphFromSelection)
- cat frontend_vue3/src/stages/xilink/SubgraphCanvas.vue(全文 · 找 click/dblclick handler)
- ls frontend_vue3/src/components/canvas/(看画布组件结构)
- grep "node.type === 'subgraph'\|isSubgraph\|SubgraphNodeInstance" frontend_vue3/src/(看画布层是否识别子图节点类型)
- grep "openSubgraphEditor\|enterSubgraph" frontend_vue3/src/(看双击切换是否实装)
- grep "createSubgraphFromSelection" frontend_vue3/src/(看推导算法实装位置)
- 输出真值核查到 commit message body:
  · 候选 ① 推导算法现状:返 N 项 inputPorts / M 项 outputPorts(N/M 应 > 0)
  · 候选 ② 端口渲染:画布层是否识别 subgraph 类型并渲染端口
  · 候选 ③ 双击 handler:是否调 openSubgraphEditor
  · 候选 ④ 节点类型分支:画布是否走 subgraph 路径
  · **锁定 4 候选中真断点 · 给出修复路径**

Step 2 · 修推导算法(0.4d · 高概率根因 ①)
- 编辑 frontend_vue3/src/stages/xilink/composables/useSubgraph.ts
- 找 createSubgraphFromSelection(selectedNodeIds, edges) 函数
- 算法重写(确保正确):
  ```typescript
  const selectedSet = new Set(selectedNodeIds)
  const internalEdges: Edge[] = []
  const inputPorts: SubgraphPort[] = []
  const outputPorts: SubgraphPort[] = []

  for (const edge of edges) {
    const sourceIn = selectedSet.has(edge.sourceModuleId)
    const targetIn = selectedSet.has(edge.targetModuleId)

    if (sourceIn && targetIn) {
      // 内部连线 · 保留
      internalEdges.push(edge)
    } else if (!sourceIn && targetIn) {
      // 外→内 · target 在 selected · source 在外 → inputPort
      inputPorts.push({
        id: `subgraph-in-${edge.targetModuleId}-${edge.targetPortId}`,
        label: `Input (${edge.targetModuleId}.${edge.targetPortId})`,
        type: 'audio', // 默认 audio · 后续可根据 module 端口类型推断
        internalMapping: {
          moduleId: edge.targetModuleId,
          portId: edge.targetPortId
        }
      })
    } else if (sourceIn && !targetIn) {
      // 内→外 · source 在 selected · target 在外 → outputPort
      outputPorts.push({
        id: `subgraph-out-${edge.sourceModuleId}-${edge.sourcePortId}`,
        label: `Output (${edge.sourceModuleId}.${edge.sourcePortId})`,
        type: 'audio',
        internalMapping: {
          moduleId: edge.sourceModuleId,
          portId: edge.sourcePortId
        }
      })
    }
    // 全外:不属于本子图 · 忽略
  }

  return {
    nodes: [...selectedNodeIds.map(id => linkStore.modules.find(m => m.id === id)!)],
    edges: internalEdges,
    inputPorts: dedupePorts(inputPorts), // 去重(同一端口可能被多条边引用)
    outputPorts: dedupePorts(outputPorts),
    // ... 其他字段
  } as SubgraphDefinition
  ```
- 加 dedupePorts 工具函数(去重)
- 不动 cyclic 检查 / saveSubgraph / openSubgraphEditor 等其他函数

Step 3 · 修端口渲染(0.3d · 高概率根因 ②)
- 找画布层渲染 ModuleNode 的组件(grep "ModuleNode\|<NodeView" frontend_vue3/src/components/canvas/)
- 加分支:若 node.type === 'subgraph' 或 node 有 subgraphDefId → 走 SubgraphNodeView 渲染(可能要新建)
- SubgraphNodeView 渲染逻辑:
  · 根据 node.subgraphDefId 找到对应 SubgraphDefinition
  · 左侧渲染 inputPorts(同普通 module 输入端口样式)
  · 右侧渲染 outputPorts(同普通 module 输出端口样式)
  · 端口 id 必须能被父画布 edge 系统识别(用 SubgraphPort.id 作为 portId)
  · 节点标题显示 SubgraphDefinition.name
  · 双击图标暗示(eg. 角落小图标"展开")
- 若现有画布无类型分发机制 · 在 ModuleNode.vue 顶部加 v-if subgraph 类型走子模板

Step 4 · 修双击行为(0.2d · 中概率根因 ③④)
- 找画布 dblclick handler(grep "@dblclick\|onDblClick" frontend_vue3/src/components/canvas/)
- 修分支:
  · 若 node.type === 'subgraph' 或 node.subgraphDefId → call useSubgraph.openSubgraphEditor(node.subgraphDefId)
  · 否则 → 现有行为(打开 PropertyPanel)
- openSubgraphEditor 应:emit 'enter-subgraph' 事件 → 父级切换到 SubgraphCanvas modal · 面包屑显示 Main / <子图名>

Step 5 · 修单击行为(0.1d · 中概率根因)
- 找画布 click handler
- 确保 subgraph 类型节点单击 → emit selectNode(同普通 module 高亮选中)
- 若 parent 实装把 subgraph 节点单击当作 noop · 改为正常 selectNode 行为

Step 6 · 面包屑导航修复(0.1d)
- 编辑 frontend_vue3/src/stages/xilink/SubgraphBreadcrumb.vue(若需)
- 确保:
  · 主画布:面包屑显示 "Main"
  · 进入子图:显示 "Main / <子图名>"
  · 点击 "Main" 返回主画布
- 复用现有路由 / store state(不重复造)

Step 7 · vitest 单测(0.15d)
- 新增 frontend_vue3/tests/composables/useSubgraph-ports-derive.test.ts:
  · case 1:3 module(A→B→C)+ 选中 [B] · 推导:inputPorts=[B.in←A.out] · outputPorts=[B.out→C.in]
  · case 2:复杂 4 module(A→B,C→B,B→D)+ 选中 [B] · 推导:inputPorts=[B.in←A.out, B.in←C.out](去重)· outputPorts=[B.out→D.in]
  · case 3:全选 [A,B,C,D]:inputPorts/outputPorts 都为空(全内部 · 无外部连接)
  · case 4:孤立选中 · 无任何 edges:inputPorts/outputPorts 都为空
- 新增 frontend_vue3/tests/components/SubgraphCanvas-interaction.test.ts:
  · case 1:渲染 SubgraphNodeInstance · 端口可见(数量 = inputPorts + outputPorts)
  · case 2:click → emit selectNode
  · case 3:dblclick → emit enter-subgraph 事件 · 含 subgraphDefId
  · case 4:面包屑 mount 时显示 "Main"
  · case 5:进入子图后面包屑显示 "Main / <名称>"

Step 8 · build + test + 手动 e2e(0.2d)
- cd frontend_vue3
- npm run typecheck(零错误)
- npm run test:unit(基线 +9 用例)
- 手动 e2e:
  · npm run dev
  · 浏览器进 P1-xilink stage
  · 加 4 module(source → eq → comp → sink)+ 3 connection
  · 选中 eq + comp(2 个)→ 右键 "封装为子图" → 父画布替换为 1 SubgraphNodeInstance
  · ✅ SubgraphNodeInstance 显示 1 输入端口(连 source.out)+ 1 输出端口(连 sink.in)
  · ✅ 拖端口与其他 module 连接 · 成功
  · ✅ 双击 SubgraphNodeInstance → 子图画布打开 · 面包屑 "Main / <子图名>" · 内含 eq + comp + 内部连线
  · ✅ 单击 SubgraphNodeInstance → 高亮选中(同普通 module)

Step 9 · commit + push(0.05d)
- git add frontend_vue3/src/stages/xilink/composables/useSubgraph.ts \
          frontend_vue3/src/stages/xilink/SubgraphCanvas.vue \
          frontend_vue3/src/stages/xilink/SubgraphBreadcrumb.vue \
          frontend_vue3/src/components/canvas/<改动文件> \
          frontend_vue3/tests/composables/useSubgraph-ports-derive.test.ts \
          frontend_vue3/tests/components/SubgraphCanvas-interaction.test.ts
- git commit subject + trailer 见下
- git push origin xistudio

【验收】

形式合规:
- [ ] npm run typecheck 零错误
- [ ] npm run test:unit 全绿(基线 +9 用例)
- [ ] 仅修动 5-7 文件(useSubgraph + SubgraphCanvas + Breadcrumb + canvas + 2 tests)
- [ ] 不动 types/subgraph.ts schema(parent zombie e93ef27 已落)
- [ ] 不动 stages/xilink/bottom/* / drawers/*(同期 hotfix 区域)
- [ ] 不动 stages/xitest/* / stages/xitune/*

业务行为契约(端到端真值 · 必跑):
- [ ] 多选 3 module + 1 connection → 封装 · SubgraphNodeInstance 显示推导出的 inputPorts + outputPorts
- [ ] 推导正确性:外→内连线 = inputPorts(数量准)· 内→外连线 = outputPorts(数量准)
- [ ] SubgraphNodeInstance 端口可被父画布 edge 拖拽连接(成功画线)
- [ ] 双击 SubgraphNodeInstance → 切换到子图画布 · 面包屑 "Main / <名称>"
- [ ] 单击 SubgraphNodeInstance → 高亮选中(同其他 module)
- [ ] 子图画布内可见原 selectedNodes + 内部连线
- [ ] 点击面包屑 "Main" → 返回父画布
- [ ] 用户回测 ADR-08 验收清单 C4-6~C4-12

【commit】
subject:`fix(P1.UH-subgraph-ports-and-dblclick-fix): port derivation + dblclick enter + click select · ADR-08 §议题④ hotfix · user acceptance C4-* fix`

trailer(必须精确):
[step=9/9] [pid=P1] [uid=UH-subgraph-ports-and-dblclick-fix] [occupies=P1.K-xilink-canvas+P1.K-xilink-toolbar(read)+P1.K-module-library(read)]
[files=frontend_vue3/src/stages/xilink/composables/useSubgraph.ts, frontend_vue3/src/stages/xilink/SubgraphCanvas.vue, frontend_vue3/src/stages/xilink/SubgraphBreadcrumb.vue, frontend_vue3/src/components/canvas/<具体文件>, frontend_vue3/tests/composables/useSubgraph-ports-derive.test.ts, frontend_vue3/tests/components/SubgraphCanvas-interaction.test.ts]
[isolation] file(同 worktree 同 branch · 与同期 P1 两 hotfix 文件正交)
[derived_from] ADR-AIOS-08 §议题④ 验收失败 · parent_zombie af945e2
[truth-check] 4 候选根因实测:① 推导=<bug/正确>,② 端口渲染=<缺/有>,③ 双击=<错位/正>,④ 类型分支=<错/正>
[acceptance] 多选 3 module 封装 → 端口推导正 + 双击进入子图 · 用户重跑 C4-6~C4-12

【禁止】
1. ❌ 不动 types/subgraph.ts schema(parent zombie e93ef27 已落)
2. ❌ 不动 stages/xilink/bottom/* / drawers/*(同期 hotfix 区域)
3. ❌ 不动 stages/xitest/* / stages/xitune/*
4. ❌ 不破坏 cyclic 检查(parent zombie 已落 · 仅修推导 + 交互)
5. ❌ 不修 ModuleLibrary "我的子图" 分类(parent zombie 已落 · 不动)
6. ❌ 不修 Toolbar "新建子图" 按钮(入口 1 已落 · 不动)
7. ❌ 不引入复杂图算法库(naive O(N*E) 推导即可 · MVP 限 2 层嵌套)
8. ❌ 不跳真值核查(Step 1 必须 grep 4 候选根因 · commit message 报告)
9. ❌ 不跳手动 e2e(必须浏览器实测多选封装 + 双击场景)
10. ❌ 不省略三元组 trailer + truth-check 报告

解锁链(本任务 zombie 后)

  • ✅ 用户重跑 ADR-08 验收清单 §议题④ C4-6~C4-12
  • ✅ 子图功能真业务可用 · 端口推导 + 双击展开 + 单击选中
  • ✅ ADR §议题④ 全部交互闭环(C4-1~C4-17 含本 fork 修复的 6 项核心)

风险评估

风险 缓解
⚠️ 推导算法对复杂场景(N→1, 1→N, fan-out)端口去重不准 Step 7 case 2 显式覆盖 4 module 复杂连接 · dedupePorts 用 Map 去重
⚠️ 端口渲染需新建 SubgraphNodeView 组件(若现状无类型分发) Step 3 优先在现有 ModuleNode 加 v-if 分支(避免新建组件)· 若不行再新组件
⚠️ dblclick 在某些画布库(eg. Vue Flow)有内置行为冲突 Step 4 grep 现有 dblclick listener · 用 stopPropagation / 优先级处理
⚠️ 与 P1.UH-link-error-bottom-fix 在 stage 顶层文件改动可能冲突 文件正交:本 fork 改 stages/xilink/composables/Subgraph + components/canvas/ · 错误 fix 改 bottom/ + linkStore · 可并行

历史

时间 事件 hash
2026-05-31 11:15 dispatched · 用户验收 ADR-08 议题④ 失败 "子图永远无法展开 + 没生成端口 · 无法连线" → 拍板派 5 hotfix · 本 fork P2 ⭐ 最复杂 · 1.5d ClaudeA
2026-05-31 13:59 zombie · ClaudeA 完成 · 4 候选根因实测:① 推导=正确(外→内=inputPorts,内→外=outputPorts)· ② 端口渲染=缺失(已修)· ③ 双击=已正确(dblclick→open-subgraph→SubgraphCanvas)· ④ 单击=缺失(已修+高亮选中)· 修改 4 文件:LinkEditor.vue(255 行)+ useSubgraph.ts(53 行)+ linkStore.ts(17 行)+ useSubgraph-ports-derive.test.ts(新增 205 行)· 共 511 行 19 删除 · ADR-08 §议题④ 用户验收闭环 · v1.5 铁律 truth-check(用户给 hash 完全对齐) 40781e8