deerflow-code/frontend-web/docs/roundtable-dag-orchestration.md
2026-09-07 18:24:55 +08:00

60 KiB
Raw Blame History

圆桌 DAG 编排 + 并行执行显示 —— 分步实现需求文档

目标读者:实现本功能的前端 / 后端工程师。 本文是可逐步落地的需求 + 设计文档,每个 Phase 都能独立交付、独立验收。


0. 背景与目标

0.1 现状

Step 2 圆桌目前有两种编排模式(orchestrationMode,见 hooks/useStep2Orchestration.ts):

  • free(自由模式):总控(leader)自由决定派活给谁,runOrchestration 循环驱动。
  • chain(线性链条):用户把席位排成一条线性链 A → B → C,runChainOrchestration 按顺序逐个派活,最后总控收口。链路用 DispatchChainModal.tsx 的横向流程图展示(总控 → 席位1 → 席位2 → … → 共识 → 报告)。

链条数据现在只是一个有序数组 selectedAgents(线性),节点状态类型见 lib/job-types.ts(ChainNodeState = pending | active | done)。

0.2 目标(本次新增)

  1. DAG 编排:用户可视化指定智能体之间的直接依赖关系——有的串行(A → B → C),有的并行(A B C 同时跑)。编排结果是一张流程图(DAG)。
  2. 并行执行:DAG 里同一层(互不依赖)的智能体并行跑,墙钟时间从「串行求和」降到「并行取最慢」。
  3. 并行禁止写文件:并行的智能体禁用文件写入 / 沙箱命令,避免它们同时打开沙箱互相打架。产物以消息正文交付;落文件留给串行 / 收口阶段。
  4. 并行高性能显示:并行时收起左侧、顶部头像条切换、主区同时显示 2 个智能体的消息流,其余只接流不渲染,避免页面卡死。

0.3 设计总原则

  • 复用 现有 special run / leader run / rollback 抢占 / 轻量交付状态接口(GET /threads/{id}/last-ai-message),不另起炉灶。
  • 线性链是 DAG 的特例:chain 模式可平滑迁移成"每层 1 个节点"的 DAG,老数据不破坏。
  • 分层(Stage)优先于自由连线:编辑交互首选「分层」模型(每个 stage 放若干并行席位,stage 间串行),实现成本低、覆盖 95% 的"串 / 并"诉求;自由连线画布作为后续可选增强(见 §2.3)。

0.4 已确认的设计决策(✅ 本期按这些实现)

以下三项已与需求方确认锁定,实现时直接按此,不再二选一。

  1. ✅ 编排模型 = 分层 Stage:本期只做分层(每个 stage 放若干并行席位、stage 间串行)。自由连线画布本期不做,仅在数据模型层预留(§1.2)以免将来返工。
  2. ✅ 并行跳过 leader、前端直派:DAG 由用户显式指定,执行时前端直接对 stage 内席位发起 special run,不每个 stage 再跑一轮总控派活;只在最后 finalSynthesis 跑一轮 leader 综合(即 §3.2 的方案 B)。
  3. ✅ 主区固定 2 列,窄屏降 1 列:并行时主区最多并排显示 2 个被选中智能体的消息流;窄屏(断点见 §5.3)降为 1 列 + 头像条横向滚动。不做 3+ 列。

1. 数据模型

1.1 DAG 的「分层」表示(推荐,主模型)

把 DAG 表示成有序的 stage 列表,每个 stage 内的席位并行、stage 之间串行:

// lib/dag-types.ts (新建)

/** 一个并行批次:stage 内所有席位并行执行。 */
export interface OrchestrationStage {
  /** stage 稳定 id(拖拽重排 / diff 用)。 */
  id: string;
  /** 本 stage 并行执行的席位(agent_id 列表)。1 个 = 退化为串行节点。 */
  agentIds: string[];
}

/** 用户编排出的执行计划。stages 顺序即串行顺序。 */
export interface OrchestrationPlan {
  /** 编排模式标识,持久化进 step2。 */
  mode: "dag";
  /** 串行执行的批次;每批内并行。 */
  stages: OrchestrationStage[];
  /** 收口:是否在所有 stage 完成后由总控综合(默认 true)。 */
  finalSynthesis: boolean;
}
  • 串行 A → B → C = stages: [{agentIds:[A]}, {agentIds:[B]}, {agentIds:[C]}]。
  • 并行 A B C = stages: [{agentIds:[A,B,C]}]。
  • 混合 A →(B C)→ D = stages: [{[A]}, {[B,C]}, {[D]}]。

为什么用分层而不是「邻接表 / 边」:执行调度本就是「拓扑分批」,分层就是分批的结果,前端不必再做拓扑排序,也天然避免环;编辑器交互更直观(拖卡片进 stage)。真正的自由连线 DAG 见 §2.3(可选)。

1.2 通用 DAG 表示(§2.3 自由连线时才需要)

若将来做自由连线画布,再引入边表示,并在执行前做一次拓扑排序 → 分层,落回 §1.1 的 stages 结构给调度器用:

export interface DagEdge { from: string; to: string }   // from 完成后才能跑 to
export interface DagGraph { nodes: string[]; edges: DagEdge[] }
// 执行前:toposortToStages(graph) -> OrchestrationStage[]

1.3 持久化

  • 复用 roundtable_drafts.step2 JSON(见后端 app.gateway.routers roundtable-drafts + hooks/useStep2Orchestration.ts 里对 step2 的读写)。
  • 在 step2 增加字段:orchestrationPlan: OrchestrationPlan | null。
  • orchestrationMode 扩展为 "free" | "chain" | "dag";chain 旧数据读取时即时转换为单节点-stage 的 dag(兼容层,不改老 draft)。

1.4 校验(纯函数,可单测)

lib/dag-types.ts 内提供:

validatePlan(plan: OrchestrationPlan, selectedAgentIds: string[]): {
  ok: boolean;
  errors: string[];   // 面向用户的中文提示
}

校验项:

  • 每个 agentId 必须在本次选定席位 selectedAgents 内(防止编排了不存在的席位 → 复用后端 _partition_dispatch 同思路的"必须在 roster 内")。
  • 不允许空 stage;不允许同一席位出现在多个 stage(一个席位只跑一次)。
  • 至少 1 个 stage。
  • (可选)单 stage 并行度上限提示(如 > 6 并行给 warning:远程模型并发 + 沙箱压力)。

2. 编排编辑器(流程图构建)交互

2.1 入口与整体布局

  • 在 Step 2 顶部模式切换里新增 「依赖编排」(dag)模式(与现有 自由 / 链条 并列)。
  • 选择 dag 后弹出 / 进入编排编辑器(复用 Dialog 大弹窗,参考 DispatchChainModal 的尺寸 w-[94vw] max-w-[1240px])。

2.2 推荐交互:分层(Stage)编辑器(Phase 2 实现)

┌ 依赖编排 ───────────────────────────────────────── [校验✓] [保存] ┐
│  可选席位(点/拖加入某个 stage)                                   │
│  [情报收集] [环境评估] [方案设计] [风险审查] [执行规划] [前端实现]  │
│ ─────────────────────────────────────────────────────────────── │
│  Stage 1(并行)              Stage 2(并行)       Stage 3        │
│  ┌──────────┐  ─────▶  ┌──────────┐ ┌────────┐ ─▶ ┌──────────┐    │
│  │ 情报收集 │          │ 方案设计 │ │风险审查│    │ 执行规划 │    │
│  └──────────┘          └──────────┘ └────────┘    └──────────┘    │
│   [+ 拖席位到此]        [+]                       [+]            │
│  [+ 新建 Stage(串在最后)]                                       │
│ ─────────────────────────────────────────────────────────────── │
│  说明:同一 Stage 内的席位会**并行**执行;Stage 之间**串行**       │
│  (后一个 Stage 能看到前面所有 Stage 的交付摘要)。                │
└───────────────────────────────────────────────────────────────────┘

交互要点:

  • 加入 stage:从顶部可选席位拖拽到某个 stage,或点席位 →「加入 Stage N」。
  • 移动 / 重排:席位可在 stage 间拖动;stage 可整体左右拖动重排(改串行顺序)。
  • 删除:席位 / 空 stage 可删。
  • 并行度提示:stage 内 ≥2 席位即标「并行」徽标;过多给 warning。
  • 实时校验:validatePlan 实时跑,错误在顶部红条提示,「保存」在校验通过前禁用。
  • 预览流程图:编辑区下方实时渲染只读流程图缩略(复用 §7 的 DAG 流程图组件)。

拖拽可用现成轻量方案(HTML5 DnD 或 @dnd-kit,看项目是否已有依赖;没有就先用「点选 + 加入/移出按钮」的无拖拽版,Phase 2 先上无拖拽、Phase 2.5 再加拖拽)。

2.3 可选增强:自由连线画布(后续,非本期必须)

  • 节点自由摆放 + 拉线建边 → DagGraph(§1.2)。
  • 实现成本高(画布 / 连线 / 命中检测 / 自动布局),且分层模型已覆盖绝大多数诉求。本期不做,仅在数据模型上预留 §1.2,避免将来返工。

2.4 验收(Phase 2)

  • 能拖 / 点把席位分配到多个 stage,保存进 step2.orchestrationPlan。
  • 刷新 / 重进 draft 能恢复编排。
  • 非法编排(空 stage / 席位重复 / 席位不在 roster)有明确中文提示且无法保存。

3. 执行调度(前端编排循环 + 后端复用)

3.1 新增 runDagOrchestration(useStep2Orchestration.ts)

与现有 runOrchestration / runChainOrchestration 并列,逻辑:

for (stageIndex, stage of plan.stages):
  if isStale() break               // 复用现有 epoch 抢占(人工干预/暂停)
  // 1) 总控按本 stage 的席位并行派活(一轮 leader,dispatched = stage.agentIds)
  //    —— 或跳过 leader,前端直接对 stage 内每个席位发起 special run(见 3.2)
  // 2) 并行跑 stage 内所有席位
  await Promise.all(stage.agentIds.map(seat =>
        streamMultiAgent({ agentType:"special", agentName:seat,
                           newMessage: buildStageTask(seat, plan, stageIndex),
                           parallelNoFile: stage.agentIds.length > 1,  // §4
                           ... })))
  // 3) 等本 stage 全部 ✅(Promise.all 天然 barrier)再进入下一 stage
// 4) finalSynthesis:最后一轮 leader(synthesisMode=true)综合所有 stage 交付

要点:

  • 批内并行:Promise.all(不是现在的 for await 串行)。
  • 批间串行:Promise.all 即 barrier,自动等齐。
  • 依赖可见性:进入 stage N 的席位时,把前面所有 stage 已交付席位的摘要拼进它的 newMessage(复用 §交付状态:网关 _collect_delivery_status / GET last-ai-message?max_chars=N,前端也可在 stage 边界拉一次摘要拼进 task)。这样 (B C) 能看到 A 的产出。
  • 抢占 / 干预 / 暂停:每个 stage 边界检查 isStale()(epoch),与现有自由 / 链条模式一致。
  • rollback:special run 已统一 multitask_strategy=rollback,并行多席位各自不同 thread,互不冲突。

3.2 是否每 stage 都走 leader? — ✅ 已定:前端直派(方案 B)

本期采用方案 B:DAG 由用户显式指定,不需要 leader 再"决定派给谁"。前端直接对 stage 内每个席位发起 special run,省掉每 stage 的 leader LLM 往返(更快、更可控);只在所有 stage 完成后、finalSynthesis 为真时跑一轮 leader(synthesisMode=true)综合所有交付。

(备选方案 A:每 stage 跑一轮 leader 派活——更贴现有架构但每 stage 多一次 leader LLM、更慢。本期不采用,留档备查。)

3.3 后端改动

  • 基本不需要新接口:special run(POST /api/multi-agent/run/stream agent_type=special)、leader run、轻量交付读(GET /threads/{id}/last-ai-message)、轻量追加(POST /threads/{id}/messages/append)都已就绪。
  • 仅需 §4 的「并行禁写文件」run policy 开关。

3.4 验收(Phase 3)

  • (B C) 两席位同时出现在执行(后端日志两条 special run 时间重叠;前端两个流并行增长)。
  • stage 间严格串行(stage 2 不早于 stage 1 全部完成)。
  • stage N 席位的输入里能看到 stage <N 的交付摘要。

4. 并行时禁止写文件(防沙箱打架)

4.1 后端 run policy

app/gateway/roundtable_run_policy.py 现有角色:leader / seat / report。新增一个角色或开关:

  • 方案:给 seat 增加一个「并行变体」seat_parallel,其 excluded_tools 在 seat 基础上再禁 write_file / bash / str_replace / present_files(与 report 角色禁写的思路一致)。
  • _special_run(multi_agent.py)按请求里的一个新 flag(如 no_file: true)选择 seat_parallel policy。

4.2 前端传参

  • streamMultiAgent 请求体新增 parallel_no_file?: boolean(或复用 no_file)。
  • runDagOrchestration 在 stage.agentIds.length > 1(真并行)时置 true;单节点 stage(实际串行)不限制,可正常写文件。

4.3 产物归属

  • 并行席位的产物 = 消息正文(被 _collect_delivery_status / 综合块读取)。
  • 需要落文件 / 画图的(如 Step3 报告、roundtable-report)放在串行的收口阶段单独跑,不在并行批次里。

4.4 验收(Phase 4)

  • 并行 stage 的席位 run 日志显示 Excluded ... ['write_file','bash','str_replace','present_files', ...]。
  • 并行期间不再出现多个席位抢同一沙箱 / 写文件冲突。

5. 并行执行的前端显示

5.1 布局总览(并行进行时)

┌ 多智能体圆桌会商中心 ─────────────────────────────────────────────┐
│ [头像条:进行中的并行智能体]            [模型选择 ▾] [暂停会商]     │
│  (情报)●  (方案)●  (风险)●  (执行)  …                              │
│   ↑选中    ↑选中    loading  hidden                                │
│ ───────────────────────────────────────────────────────────────── │
│  ┌─ 方案设计(选中)──────────┐  ┌─ 风险审查(选中)──────────┐   │
│  │ 一、整体架构…(流式渲染)  │  │ 1. 合规风险 IDFA/OAID…     │   │
│  │ …                          │  │ …                          │   │
│  └────────────────────────────┘  └────────────────────────────┘   │
└───────────────────────────────────────────────────────────────────┘
(左侧"参与角色"栏在并行时整体收起,腾出横向空间放两列消息)

5.2 关键交互(与需求逐条对齐)

  1. 收起左侧:并行执行时把左侧「参与角色 / 人工参与」栏整体折叠(一个 isParallelRunning 状态控制;非并行恢复)。
  2. 头像条:位置在消息列表上方、模型选择器左侧。展示当前并行批次的所有进行中智能体头像:
    • 进行中 → 头像加 loading 动画(复用 DispatchChainModal 的 animate-ping 光环 + Loader2Icon animate-spin 角标)。
    • 已完成 → 打勾(复用现有 CheckIcon 角标)。
    • 选中 → 高亮描边(当前正在主区显示的那 2 个)。
  3. 主区同时显示 2 个:被选中的 2 个智能体的消息列表并排两列渲染。
  4. 多于 2 个先隐藏:未选中的智能体不渲染 DOM(见 §6 性能)。
  5. 点头像切换:点击未选中的头像 → 把它设为选中之一(替换当前 2 个里的一个,或按"最近点击保留 2 个"的 LRU 策略);被换下的智能体停止渲染但继续接流。

5.3 选中策略 — ✅ 固定 2 列,窄屏降 1 列

  • 维护 selectedParallelSeats: string[],最多 2(固定 2 列)。不做 3+ 列。
  • 响应式:宽屏 2 列并排;窄屏(建议断点 < 1024px / lg)降为1 列,头像条横向滚动切换;此时 selectedParallelSeats 仍可存 2 个,但主区只渲染当前 1 个(点头像切换哪个在前)。
  • 默认选中本批次最先开始的(宽屏 2 个 / 窄屏 1 个)。
  • 点头像:若已选中则取消;未选中则加入(超过上限时按 LRU 踢掉最早选中的)。被换下的席位停止渲染但继续接流(§6)。

5.4 验收(Phase 5)

  • 并行时左侧收起、出现头像条、主区两列。
  • 进行中头像有 loading;完成打勾;选中有高亮。
  • 点头像能切换主区显示的智能体,且切换是即时的(数据已在内存,见 §6)。

6. 流式高性能策略(避免页面卡死)

核心矛盾:N 个并行席位同时高频 onTextDelta,若都 setState + Markdown 渲染会卡死。

6.1 全接收、惰性渲染

  • 所有并行 run 的流都正常订阅(不丢数据):每个席位的增量文本累积进 store / ref(Map<agentId, { text, status, charCount }>)。
  • 只有被选中(主区显示)的席位才把累积文本挂到 DOM 渲染(Markdown)。
  • 未选中席位:只更新内存 + 头像条上的轻量进度(字数 / loading),不挂载消息 DOM。
  • 切换选中时:把该席位已累积的全文一次性渲染 + 继续接增量。

6.2 渲染节流

  • 选中席位的流式渲染节流/合帧:用 requestAnimationFrame 或 ~50–100ms 批量 flush 累积的 token,而不是每个 token 一次 setState。
  • Markdown 渲染对"流式中"可降级(纯文本 / 轻量渲染),完成后再做完整 Markdown(现有 MessageBubble 的 streaming 标志可复用:streaming 时简渲、done 后全渲)。

6.3 头像条进度

  • 头像条只显示廉价信息:loading 动画 + 可选字数(charCount,从内存 Map 取,节流更新,例如每 500ms 刷一次)。不触发消息体渲染。

6.4 数据流(建议结构)

streamMultiAgent(seat).onTextDelta(text) →
    parallelStore.append(seatId, text)          // 纯内存累积,O(1)
    if seatId ∈ selectedParallelSeats:          // 仅选中的进 React state(节流)
        scheduleFlush(seatId)                    // rAF / 节流 setState
    else:
        bumpCharCount(seatId)                    // 仅更新头像条进度(节流)

6.5 验收(Phase 6)

  • 6+ 席位并行时页面不卡(输入 / 滚动 / 切换流畅)。
  • 切到一个之前隐藏的席位,能立刻看到它到目前为止的全部输出(证明流没丢、只是没渲染)。

7. 流程图执行态可视化(DAG 版)

  • 把现有 DispatchChainModal.tsx(横向线性流程图)扩展 / 复刻成 DAG 流程图:
    • 按 stage 分列,stage 内席位纵向并列,stage 间用连接线。
    • 节点状态复用 ChainNodeState(pending / active / done)+ 现有动画(animate-ping / Loader2Icon / CheckIcon)。
    • 末尾接 共识 → 结果绘制 里程碑(沿用现有)。
  • 用途:编辑器里的只读预览(§2.2 底部)+ 执行时的总览(可选小窗 / 顶部缩略)。
总控 ─▶ ┌ Stage1 ┐ ─▶ ┌ Stage2 ┐ ─▶ ┌ Stage3 ┐ ─▶ 🤝共识 ─▶ 📊报告
        │ 情报●  │     │ 方案✓  │     │ 执行○  │
        └────────┘     │ 风险●  │     └────────┘
                       └────────┘

7.1 验收(Phase 7)

  • 编辑器底部 / 执行时能看到 DAG 流程图,节点状态随执行实时更新(active/done)。

7.2 实施结论(2026-06-07,✅ 已完成)

  • 新组件 components/DagFlowChart.tsx:纯展示组件,复刻 DispatchChainModal 的节点 / 连线 / 里程碑视觉(animate-ping 光环 + Loader2Icon 转圈 + CheckIcon 打勾 + rt-chain-connector 连线动画),但按 stage 分列:总控 ─▶ ┌Stage1┐ ─▶ ┌Stage2┐ ─▶ 🤝共识 ─▶ 📊结果绘制,stage 内席位纵向并列。compact 紧凑模式给缩略图用。所有状态由 props 传入(stages / resolveSeat / seatStates / coordinatorState / consensusState / reportState);不传 seatStates → 全 pending(只读预览)。aggregateStageState 把 stage 内席位聚合成列状态(任一 active→active、全 done→done)驱动 stage 间连线。
  • 编辑器只读预览(§2.2 底部):BusinessChainEditorPage 底部加「编排预览」section,把 stageGroups(去空 stage) 映射成 DagFlowChart 的 stages + resolvePreviewSeat(席位名/配色),compact、全 pending、随编辑实时变。
  • 执行态总览(Step 2 顶部缩略):useStep2Orchestration 新增 dagNodeStates: Record<agentId, ChainNodeState>,runDagOrchestration 跨 stage 维护(进入 stage→该批 active、席位终态→done、resetStep2State 清空);经 RoundtablePlanningPage 把 orchestrationMode/orchestrationPlan/dagNodeStates 传到 Step2Panel,dag 模式下会话区顶部 compact 渲染 DagFlowChart。总控/共识里程碑状态由 Step2Panel 从节点状态 + isOrchestrating/hasConsensus 派生(中转轮时无席位 active + isOrchestrating → 总控 active)。
  • 改动文件:components/DagFlowChart.tsx(新)、pages/BusinessChainEditorPage.tsx、hooks/useStep2Orchestration.ts、components/Step2Panel.tsx、pages/RoundtablePlanningPage.tsx。typecheck 零新增(基线 65)。
  • 下次实地验收:①编辑器底部随分层编辑实时画出流程图;②Step2 dag 模式顶部出现缩略流程图,跑起来后当前 stage 列点亮(active 转圈)、完成打勾、中转时总控亮、最后共识亮。

8. 分步实现计划(每步可独立交付 + 验收)

Phase 内容 主要文件 依赖 可独立验收
1 DAG 数据模型 + 校验 + 持久化字段 lib/dag-types.ts(新)、draft step2 读写 — 纯函数单测:validatePlan / chain→dag 兼容转换
2 分层编排编辑器(先无拖拽:点选加入/移出) components/DagPlanEditor.tsx(新)、Step2 模式切换 P1 能编排并保存/恢复,非法编排有提示
2.5 编辑器拖拽 同上 + @dnd-kit(若引入) P2 拖拽分配/重排
3 runDagOrchestration:批内并行 + 批间串行 + 依赖摘要注入 hooks/useStep2Orchestration.ts、api/multi-agent.ts P1 后端日志并行 special run 时间重叠;stage 严格串行
4 并行禁写文件 run policy roundtable_run_policy.py、multi_agent.py、api/multi-agent.ts P3 并行 run 的 excluded_tools 含 write_file/bash/...
5 并行显示骨架:收起左侧 + 头像条 + 两列 + 选中切换 components/Step2Panel.tsx、components/ParallelSeatStrip.tsx(新) P3 见 §5.4
6 流式高性能:全接收 / 惰性渲染 / 节流 hooks/useStep2Orchestration.ts(parallelStore)、Step2Panel P5 见 §6.5(6+ 并行不卡)
7 DAG 流程图执行态可视化 components/DagFlowChart.tsx(复刻自 DispatchChainModal) P2、P3 见 §7.1
8 边界 / 异常 各处 全部 见 §9

落地顺序建议:P1 → P3 → P4 → P5 → P6(先把"能并行跑 + 能看 + 不卡"打通),P2 编辑器可与 P3 并行开发(先用一份 hardcode/简单 UI 的 plan 驱动 P3 联调),P7 / P2.5 / 2.3 作为增强后置。


9. 边界与异常(Phase 8)

  • 某并行节点失败 / 超时:单个席位失败不拖垮整 stage;标该节点 error,stage 其余 ✅ 仍推进;收口时把"X 失败"如实告知总控(复用交付状态 ⬜ + 失败标记)。
  • 依赖未满足:调度器保证 stage 串行,理论不会发生;防御性校验 stage N 启动前 stage <N 必须全部终态(done/failed)。
  • 人工干预 / 暂停 / 恢复:在 stage 边界检查 isStale()(epoch);干预指令插入后,从"下一个未开始的 stage"继续(已完成 stage 不重跑)。
  • 并行禁写文件 vs 席位本想写文件:在席位 SOUL / 派活提示里说明"本轮为并行研讨,产出请直接写在回复正文,不要写文件",降低模型困惑。
  • 窄屏:主区两列降为一列 + 头像条横向滚动。
  • 单 stage 全并行(一次性 N 个):给并行度 warning(远程模型并发限流 / 成本)。

10. 与现有代码的衔接点速查

  • 编排循环:hooks/useStep2Orchestration.ts(runOrchestration / runChainOrchestration 旁新增 runDagOrchestration;复用 isStale() epoch、abortAllInFlight、appendStreamingDialogue / appendToDialogue)。
  • 流式 API:api/multi-agent.ts(streamMultiAgent,新增 parallel_no_file 入参;onTextDelta(text, messageId) 已是按席位回调,天然支持并行分流)。
  • 显示:components/Step2Panel.tsx(消息列表 step2RoundtableDialogues.map;新增头像条 ParallelSeatStrip + 两列容器 + 折叠左侧)。
  • 流程图:components/DispatchChainModal.tsx / lib/job-types.ts / lib/business-chain.ts(复刻成 DAG 版)。
  • 常量 / 元数据:lib/roundtable-constants.ts(BUILTIN_AGENT_META 头像/中文名、buildAgentNameToRole)。
  • 后端 run policy:app/gateway/roundtable_run_policy.py(新增 seat_parallel)、app/gateway/routers/multi_agent.py(_special_run 读 no_file flag)。
  • 交付摘要:GET /api/threads/{id}/last-ai-message?max_chars=N(依赖注入 stage N 输入用)。

11. 一句话总结

把「线性链条」升级为「分层 DAG」:编辑器用分层 stage(拖卡片)表达串/并;调度用 Promise.all 批内并行、批间 barrier 串行,依赖摘要在 stage 边界注入;并行禁写文件避免沙箱打架;显示收起左侧、头像条切换、主区两列、未选中只接流不渲染(rAF 节流)保证不卡。全部建立在现有 special/leader run + 轻量交付接口之上,后端几乎零新接口。


12. 已知问题与排查记录(2026-06-06)

12.1 ❗并行席位「步骤条」不显示 —— 根因:seat_parallel 工具面禁得过多

现象:并行两列里的席位气泡看不到步骤条(MessageStepsCard),而串行席位有。

排查结论(已定位,待修):

  1. 步骤条只渲染「工具调用」步骤。MessageStepsCard 的 isToolStep 只认 tool_calling:<toolName> 的步骤进折叠卡;纯阶段步骤(connecting: / streaming: 这类 reasoning)只渲染成底部一行小灯泡,不出大卡(见 MessageStepsCard.tsx L80-100)。
  2. 并行席位几乎调不动任何会产生步骤的工具。后端 app/gateway/roundtable_run_policy.py:
    • _SEAT_EXCLUDED = [web_search, ask_clarification, present_files, view_image, agent_orchestration] —— 注意 web_search 对所有席位(串行也)就禁了(注释「圆桌研讨里联网搜索一律禁用」)。
    • _SEAT_PARALLEL_EXCLUDED = _SEAT_EXCLUDED + [write_file, bash, str_replace]。
    • 于是并行席位可用工具几乎只剩 read_file / ls / task / write_todos / memory / hindsight_*,研究/分析类席位基本一个都不会调 → 没有任何 tool_calling 步骤 → 步骤条(大卡)不出现。
  3. 对比串行为何「有步骤条」:串行席位(seat policy)能 write_file,会产生 tool_calling:write_file 步骤 → 大卡出现。并行被禁了 write_file + 又没有 search,所以一个工具步都没有。

用户诉求:并行时只禁真正会冲突的写操作,搜索信息 / 调用技能等必要操作不要禁。

关键事实(影响方案选择):圆桌每个席位是独立 thread = 独立 sandbox(后端 per-thread 隔离 .deer-flow/users/{uid}/threads/{tid}/)。所以并行席位写文件在后端并不会互相冲突;当初 §4「并行禁写文件」的「打架」其实主要是前端单沙箱预览(activeArtifactThreadId 只能同时显示一个)+ 产物归集到正文的简化考量,并非后端真冲突。→ 这意味着「并行禁写」的必要性比原设想弱,可放宽。

✅ 决策:选 A(2026-06-06 已实施):把 seat_parallel 放宽到只禁 write_file / bash / str_replace(防前端单沙箱预览串台 + 产物走正文)+ 仍禁 ask_clarification / present_files / view_image / agent_orchestration(打断/越权类),但放开 web_search 与技能(搜索不碰 sandbox;技能靠 read_file)。改在 app/gateway/roundtable_run_policy.py 的 _SEAT_PARALLEL_EXCLUDED(不再继承 seat 的 web_search 禁用,显式列举),测试 tests/test_roundtable_run_policy.py 同步更新(test_seat_parallel_allows_web_search_and_skills)。 → 并行席位现在能联网搜索/用技能 → 产生 tool_calling 步骤 → 步骤条显示。

备选 B(连写文件也放开,沙箱按 thread 隔离)/ C(重新评估「圆桌串行也一律禁 web_search」)本期不做,留档。串行席位目前仍禁搜索(seat policy 不变)。

12.2 其它已修(本轮反馈)

  • 最左侧全局工作区侧栏并行时收起:改用与「打开沙箱」同一路径 useSidebarSafe()?.setOpen(false)(之前只收了圆桌「参与角色」栏 step2SidebarExpanded)。并行结束不自动展开(与沙箱收起不回弹一致)。
  • 并行两列改回完整 Markdown(撤销 §6.2 流式纯文本降级 plainWhileStreaming),与串行同款 Step2Message。
  • 头像条不再显示字数(去掉 charCount 徽标 + 字段)。
  • 致命 bug 已修:用户干预 / 并行跑完后不再重跑整张 DAG。dagPlanExecutedRef 闸门 + DAG 各阶段跑完后释放锁 await runOrchestration(handoff) 交回总控继续派活/收口;后续干预/恢复一律走 leader 自由派活。
  • 并行两列宽表溢出已修(2026-06-07):模型输出的 Markdown 表格(如「法律免责条款」表)没有宽度约束 / 横向滚动容器,在并行两列的窄列里会超出左列、盖到相邻列。修法:MessageBubble.sharedMarkdownComponents.table(+ markdown-components.tsx)把 <table> 套进 <div class="max-w-full overflow-x-auto">——表格保持自然宽度、超出时列内横向滚动而非撑破列宽。min-w-0 链(grid minmax(0,1fr) → 列 min-w-0 → 气泡 flex-1 min-w-0)本就齐全,只差 table 这一层。
  • 关于「并行席位无步骤条」:步骤条(MessageStepsCard 大卡)只在席位产生 tool_calling 步骤时出现(§12.1)。纯分析型席位若本轮直接写分析正文、没调用任何工具(web_search 等),就只显示一行「正在输出」思考指示、不出大卡——这是设计使然,非解析失败。要确认是否真调了工具:浏览器 devtools 设 window.__MULTI_AGENT_DEBUG__=true 看 frame# event=messages 里有无 tool_calls,或查后端 special run 日志。

12.3 ✅需求变更:Stage 间「经总控中转 + 广播」(2026-06-06 提出,2026-06-07 已实施)

当前实现(方案 B,§3.2):DAG 各 stage 由前端直接派活,stage 间不跑 leader,靠前端把 buildDependencySummary 拼进下一 stage 的 task(priorSummary)。各 stage 跑完后才 await runOrchestration 交回总控收口。

新诉求:业务链条调用时,每一轮 stage 之间都要经总控中转 + 广播,而不是前端直接串下去。具体:

  1. 第 1 轮并行调用 stage1 的 A、B、C;
  2. A/B/C 全部完成后,把这一批的输出广播出来(广播给其它席位 + 总控 thread);
  3. 总控(leader)接收这批交付 → 再往下继续派活(派 stage2 的 D、E);
  4. 派活时把(承上的)消息广播给下一轮的 D、E;
  5. 以此类推,直到所有 stage 跑完再收口。

即把 §3.2 的「方案 B 前端直派」改成 「方案 A' = 计划约束下的总控中转」:stage 边界插入一轮 leader,由它综合上一批交付并驱动下一批(下一批席位仍由编排计划钉死为 D、E,不是 leader 自由乱派——leader 负责「承上启下 + 广播上下文」,计划负责「派给谁」)。

实施要点(下次会话):

  • 每个 stage 完成 → 广播该批输出:后端 _special_run 已对每个 seat 做 detached 后广播(「子智能体X完成…交付内容为:…」→ 其它席位 + 总控)。但它是 detached/best-effort + 截断;要让总控可靠地在下一轮前看到,需在 stage barrier 处等广播落地(或前端在 stage 边界用 GET /threads/{id}/last-ai-message 主动收齐各席位交付再喂给 leader)。
  • 插入 leader 中转轮:runDagOrchestration 在进入下一 stage 前,跑一轮 leader(synthesisMode=true 让后端注入上一批完整交付),让总控产出一段「承上启下」说明;把这段说明(而非机械 priorSummary)作为下一 stage 席位 task 的上下文 / 或广播给下一批席位 thread(复用 POST /threads/{id}/messages/append)。
  • 下一批仍由计划钉死:leader 中转轮不自由决定派给谁(计划已定 D、E);可参考 buildChainLeaderTask 的「约束 leader 只派给指定席位」思路,或干脆 leader 只做综合、前端照计划派 D、E 并把 leader 综合 + 广播注入其 task。
  • 与「并行禁写」「§12.1 放开搜索」并存:中转/广播不影响工具面。
  • 与致命 bug 修复并存:最后一个 stage 跑完仍走「交回总控(runOrchestration)收口/继续派活」;用户干预仍走 leader、不重跑 DAG。

影响文件(预估):hooks/useStep2Orchestration.ts(runDagOrchestration 的 stage 循环里加 leader 中转 + 广播收齐)、可能复用后端 _leader_run(synthesisMode 注入全文)/ /messages/append(广播)/ /last-ai-message(收齐交付)。后端基本无需新接口。

✅ 实施结论(2026-06-07):后端零改动,全部在前端 runDagOrchestration 内完成,复用既有「传摘要按需传全文」优化链路:

  1. 中转轮位置:每个非最后的 stage barrier 到达后(Promise.all 收齐本批交付),插入一轮 leader「中转轮」,展示为「总控协调(承上启下)」气泡。最后一个 stage 后不插中转,综合/收口仍交给末尾的 runOrchestration 接管(与致命 bug 修复一致)。
  2. 可靠收齐交付(传摘要):中转轮跑 streamMultiAgent({agentType:"leader", synthesisMode:false})。后端 _leader_run 本就在每轮 leader 前用 _collect_delivery_status(走轻量 GET /last-ai-message?max_chars=600 接口)客观核对各席位真实交付并把摘要注入总控输入(_build_seat_status_block)——比 detached 广播可靠(直接实读各席位 thread,不依赖 best-effort 广播是否落地)。故中转轮传 synthesisMode:false:只综合本批、不收口,只注入摘要;完整交付正文留到末尾收口的 synthesis 轮(_build_full_deliverables_block,全 ✅ 才注入)按需读取——即「传摘要按需传全文」。
  3. 承上启下驱动下一批:中转轮产出的综合说明存进 leaderBridge,作为下一 stage 席位 task 的上下文(buildStageTask({priorSummary: leaderBridge, priorIsBridge:true}),措辞为「承上启下综合说明」而非机械「交付摘要」),替代原来的 buildDependencySummary(deliveries)。中转失败/无产出时回退机械依赖摘要,不阻断。
  4. 下一批由计划钉死:中转轮提示(buildDagTransitTask)明确告知总控「下一阶段席位已由编排计划钉死为 X、Y,你不要自行 agent_orchestration 派活、不要收口」,前端仍照计划直派下一批;即便弱模型仍派了活,前端忽略其 dispatched、只取 content 当 bridge。
  5. 广播:本批输出广播给其它席位 + 总控 = 后端 _special_run 既有 detached 后广播(截断摘要);承上消息给下一批 = 注入其 task new_message(席位 run 输入必见,比 /messages/append 更可靠,省一次写盘)。
  6. 澄清/抢占/失败:中转轮触发澄清 → 暂停 DAG(回复后走 leader 自由派活、不重跑 DAG,沿用 dagPlanExecutedRef 闸门);被 isStale()/abort 抢占 → 静默收尾;异常 → 该气泡标错误、清空 bridge 继续。

纯函数:lib/dag-types.ts 新增 buildDagTransitTask(中转轮提示)+ buildStageTask 加 priorIsBridge 形参;lib/dag-types.test.ts 加 4 个单测(共 27 个全过)。

⚠️ §12.3 的「单独中转轮 + 前端直派」已被 §12.4 取代(2026-06-07 同日):见下。buildDagTransitTask / buildStageTask / buildDependencySummary 仍保留在 lib(带单测),但 runDagOrchestration 不再调用它们。

12.4 ✅ 需求再变更:DAG 改成「总控驱动每 stage 派活」(2026-06-07,§3.2 方案 A)

新诉求(用户原话):把并行(DAG)逻辑改得和串行一样——串行时总控给多个智能体派活、它们完成后汇报给总控、总控再继续往下派活;并行也改成「由总控判断要执行哪些步骤,这些步骤完成后再汇报给总控,总控继续往下派活」。

已确认的两个关键决策:

  1. 每批「派给谁」:✅ 计划钉死成员,但走总控派活(不是总控自由乱派)。即 stage 成员仍由编排计划定死,但改成总控每轮真正 agent_orchestration 派活给这些席位(约束只派给它们、且每个都派到)。= §3.2「方案 A」+ 批内并行。
  2. 适用范围:✅ 只改 dag 模式(recommend 仍串行逐个、chain 不动)。

实现(后端零改动,全在 runDagOrchestration):每个 stage 两步——

  • (a) 总控派活轮:跑一轮 leader,提示 buildDagStageLeaderTask(首 stage 带任务背景;后续 stage 靠后端注入的此前交付摘要承上;列出本 stage 钉死席位的 agent_name,要求逐个派到、不派清单外、不收口)。synthesisMode:false → 后端 _build_seat_status_block 走轻量 last-ai-message 注入各席位摘要(传摘要按需传全文)。
  • (b) 批内并行:本 stage 席位并行跑总控派给各自的 task——pickDagSeatTask(leaderRes.dispatched, content, agentId, name) 命中其 agent_name;总控漏派/写偏则回退总控正文/模板,不借用别席位 task(多席位并行避免张冠李戴)。完成后交付经后端 _special_run detached 后广播回总控;下一 stage 的派活轮即可看到。
  • barrier 后进入下一 stage,总控再派下一批。全部 stage 跑完 → 交回 runOrchestration 收口(synthesisMode 按需注入全文)。

相比 §12.3 的变化:§12.3 是「前端直派 + stage 间单独插一轮只综合的中转轮」;§12.4 把每个 stage 本身就改成「总控派活轮 + 批内并行」——总控既承上(注入的摘要)又派活,不再需要单独的中转轮。与 chain 模式同构,区别是一次钉死一批、批内并行。

纯函数:lib/dag-types.ts 新增 buildDagStageLeaderTask(每 stage 派活提示)+ pickDagSeatTask(取某席位 task,多席位不张冠李戴);lib/dag-types.test.ts 加 5 个单测(共 32 个全过)。runDagOrchestration 不再用 buildStageTask/buildDependencySummary/buildDagTransitTask(保留在 lib)。

Phase 7 流程图兼容:总控派活轮无席位 active + isOrchestrating → 流程图「总控」节点显示 active(承上派活中),随后该 stage 列点亮,无缝衔接。

下次实地验收:①每个 stage 前出现「总控协调(派活中/承上派活中)」气泡,内容是总控对本 stage 席位的派活说明;②后端日志该轮 leader 的 _partition_dispatch 派给的正是本 stage 钉死席位;③这些席位并行跑(special 日志时间重叠);④完成后下一 stage 总控派活轮能看到上一批交付摘要([leader-roster] 日志)。

12.5 ✅ 流程图弹窗化 + DAG「快速/沙箱」执行子模式(2026-06-07)

三个改动一起做:

(1) 流程图改成弹窗:Step2 顶部不再内联 DagFlowChart(挤占两列消息高度,截图 样式优化-1.png)。改成一条细栏「分层编排执行中 · 共 N 个阶段 + [查看编排流程图]」按钮,点开 Dialog 看全尺寸流程图(非 compact)。编辑器(BusinessChainEditorPage)底部预览保持内联不变。改 Step2Panel.tsx(加 dagFlowOpen state + Dialog)。

(2) 推荐弹窗新增 DAG 执行子模式(快速/沙箱):BusinessChainPicker 在选中链条有分层并行(走 dag)时,「进入研讨」旁出现「执行方式:快速·双列禁写 / 沙箱·单列可写」二选一(只 dag 时出现,§已确认)。

  • 快速(fast,默认):同 stage 席位并行、主区双列、parallelNoFile=true(seat_parallel 禁写文件)。= 之前的行为。
  • 沙箱(sandbox):同 stage 席位仍并行,但主区单列(selectedParallelSeats 最多 1)、parallelNoFile=false(seat policy 允许写文件);点头像切换查看,沙箱跟随当前席位。
  • 数据流:SeatSelectionResult.dagExecMode → RoundtablePlanningPage state(持久化进 DraftStep2(Run)Snapshot.dagExecMode,mirror orchestrationPlan)→ useStep2Orchestration option → runDagOrchestration。

(3) 沙箱跟随当前显示席位(防 bug 重点,用户特别强调):useStep2Orchestration

  • sandboxFollowThreadIdRef = 当前显示席位的 thread;openSeatVirtualArtifact 的 auto 守卫:sandbox 模式下,后台并行席位写文件时若不是当前显示席位,不自动打开(否则沙箱内容和主区列表错位)。
  • 切换显示席位的 effect(keyed on selectedParallelSeats + dagExecMode):该席位有产物(stub 里有 write_file/str_replace)→ openSeatLatestArtifact 强制打开它最新的文件;无产物→ 关闭沙箱面板(setActiveArtifactThreadId(null) + deselectArtifact + setArtifactsOpen(false))。
  • 当前显示席位 live 写文件 → handleArtifactToolPhase auto-open,守卫放行(follow===该席位)。后台席位写 → 守卫拦掉、只缓存 stub;切到它时 effect 再打开。
  • toggleParallelSeat:sandbox 单列 = 点头像切换(始终保留 1 个,不取消到空);fast 双列 = 原 LRU 2 个。

改动文件:lib/business-chain.ts(SeatSelectionResult.dagExecMode)、components/BusinessChainPicker.tsx(子模式 UI)、api/drafts.ts(快照 dagExecMode)、pages/RoundtablePlanningPage.tsx(state/snapshot/hydrate/confirm/reset 全链)、hooks/useStep2Orchestration.ts(option + parallelNoFile/单列 + 沙箱跟随 + toggle cap)、components/Step2Panel.tsx(流程图弹窗 + 单列 grid)。typecheck 零新增(基线 65),dag-types 32 单测全过。

下次实地验收(沙箱跟随是重点):①推荐弹窗选有并行的链条 → 出现「快速/沙箱」二选一;②快速=双列禁写(无沙箱);③沙箱=单列、席位能写文件、点头像切换列表时沙箱内容同步切换:有产物显示、无产物关闭;④后台席位写文件不会把沙箱抢到非当前列表;⑤流程图点按钮弹窗、全尺寸、节点随执行点亮。

12.6 ✅ 修:并行席位「跟着派活」(2026-06-07)

现象(截图 bug-1.png):总控派活后,并行席位的气泡里显示的是总控的整段派活叙述(「我来调度各智能体…第1阶段任务清单:席位1…席位2…」),还去调 skill_list 找 agent_orchestration——席位在模仿派活。

根因:总控这轮没真正调用 agent_orchestration 工具(弱模型 deepseek 用文字罗列了派活计划)→ 后端解析的 dispatched=[] → 前端 pickDagSeatTask 回退把总控整段叙述喂给每个席位 → 席位照抄叙述、模仿派活。(席位本就被 run policy 禁了 agent_orchestration,派不了活,只是空转一轮叙述 + 调 skill_list。)

修 v1(护栏,效果不足):pickDagSeatTask 加角色边界护栏 + 回退措辞改「挑出属于你的那份」;buildDagStageLeaderTask 加「必须真正调用工具」。→ 没拦住:总控对 deepseek 仍只写文字不调工具(dispatched 还是空),席位拿到的任务正文本身就是那段「我来调度…席位1/2/3」,护栏被淹没;3/4 席位仍模仿派活,有的甚至绕道 bash+python requests.get('/api/agents') 调内部接口派活(bug-1~4)。

✅ 修 v2(根治:回退绝不注入派活叙述):pickDagSeatTask 去掉 leaderContent 入参——

  • 命中总控结构化派活(真调了工具)→ 请你作为「X」,完成总控派给你的本阶段任务:<dispatched task> + 护栏;
  • 未命中 / dispatched 空(总控只写文字)→ 绝不把总控派活叙述喂给席位,改给干净本职任务:「请你作为「X」,从你的专业职责出发,独立完成本阶段属于你的分析与交付。任务背景与此前各阶段交付已在你的会话上下文里(后端广播注入),据此承接」+ 护栏。护栏强化到「不要用 bash/python/脚本访问内部接口(/api/agents、agent_orchestration)派活、不要复述『我来调度』总控口吻」。
  • buildDagStageLeaderTask 的「必须真正调用工具」保留(让命中率更高、尽量走 exact-match)。
  • dag-types.test.ts 改 2 单测(共 33 过)。typecheck 65。

⚠️ 残留次要污染源(若仍复发再处理):DAG 每个 stage 的 leader 轮,后端 _leader_run 会把 leader 的输入(含「请你作为总控…派活」)预广播进所有席位线程(总控智能体收到用户消息:…)。席位的当轮任务已干净(修 v2),这条只是历史上下文,理论上被「最新的干净任务 + 护栏」压制。若实测仍模仿派活,下一步:后端给 DAG leader 轮加 suppress_pre_broadcast / 或广播一条净化过的背景(去掉派活措辞)。

12.7 ✅ 修 4 项(2026-06-07):席位仍派活(v3 后端)+ 沙箱关不掉 + 并行不自动下滑 + 流程图只到 Step2

实测 §12.6 仍有席位派活(视觉设计师 skill_manage 找 agent-orchestration、列 dispatch all 4)——坐实了「残留次要污染源」:后端 leader 预广播把派活 prompt 推进席位线程,弱模型从历史里捡到派活意图。一起修了 4 项:

  1. 席位仍派活(根治·后端):_leader_run 加 suppress_pre_broadcast 参数;DAG「每 stage 派活轮」前端传 suppressPreBroadcast: true(api/multi-agent.ts → suppress_pre_broadcast,run/stream 读取)。为真时:不把总控派活 prompt 预广播给席位、也不把「总控给 X 派活」detached 广播给兄弟席位。recommend/chain 不传该 flag,行为不变。配套:pickDagSeatTask 加 seedMessage 入参——预广播抑制后席位线程没有用户原始消息了,把任务背景直接写进席位 task(此前各阶段交付仍由 special 完成后广播注入)。→ 席位线程里任何地方都没有派活语句,根治模仿。
  2. 沙箱关闭按钮关不掉:沙箱跟随 effect 之前每次 render(deps 身份变)都重跑 → 把用户刚关的沙箱强制重开。加 lastReconciledSeatRef 守卫:只在显示席位真正变化时才开/关沙箱;用户手动关闭(席位没变)时 effect 早退,不再回弹。
  3. 并行时自动下滑失效:平铺列表 ↔ 网格(两列/单列)切换时 scrollHeight 突变、scrollTop 被浏览器 clamp → useAutoScroll 误判「用户上滚」而停跟随。Step2Panel 加 effect:isParallelRunning / selectedParallelSeats 变化时 scrollStep2ToBottom() 重新拉到底 + 恢复跟随。
  4. 流程图只展示 Step2:DagFlowChart 加 showReportMilestone(默认 true);Step2 弹窗 + 编辑器预览传 false —— 只到「共识」(Step2 终点),去掉「结果绘制」(那是 Step3)。

改动:后端 multi_agent.py(_leader_run suppress 两处广播 + run/stream 读 flag);前端 api/multi-agent.ts、lib/dag-types.ts(pickDagSeatTask + seedMessage)、hooks/useStep2Orchestration.ts(传 flag + seed + 沙箱跟随守卫)、components/Step2Panel.tsx(自动下滑 effect + showReportMilestone)、components/DagFlowChart.tsx、pages/BusinessChainEditorPage.tsx。dag-types.test.ts 共 34 单测过;前端 tsc 65 基线零新增;后端 py_compile OK。

12.8 ✅ 席位产出 md 文件 + 禁写 .py(2026-06-07)

现象:某次 7 席位全都没产出 md 文件;且一直有的问题——席位有时写 .py 脚本(用户要求:不写 .py,其它格式可以)。

根因(没 md):两层都堵了——(a)快速模式 seat_parallel policy 禁了 write_file(原「产物走正文」设计);(b)DAG 席位提示词写的是「直接输出你的交付正文」,即使沙箱模式(允许写)也不会去写文件。

修:

  1. 后端 policy 放开写文件、统一禁 bash(roundtable_run_policy.py):
    • seat_parallel:去掉 write_file / str_replace 禁用(可写 md),保留禁 bash + 打断/越权类;仍放开 web_search。
    • seat:新增禁 bash(原来允许)——圆桌席位是「文档产出者」,不该跑 shell/python 脚本(弱模型爱写 .py 再跑生成产物,乱且抓不到预览);改为直接 write_file 写成品文档。
    • report:bash 已随 seat 禁,改为 [*_SEAT_EXCLUDED, "str_replace"](集合不变)。
    • 测试 test_roundtable_run_policy.py:test_seats_can_write_files_but_not_run_bash(seat / seat_parallel 都可写文件、都禁 bash),6 用例过。
  2. 前端提示词(pickDagSeatTask)加「产物要求」:把完整交付写成一个 .md 文件(write_file 到 outputs)+ 正文给摘要;禁写 .py 或任何脚本文件,.md/.html/.txt 等其它格式都可以,唯独不要 .py。
  3. 快速模式(双列)不自动开沙箱(openSeatVirtualArtifact 新增守卫):放开写文件后,双列里多席位并发写文件会让单沙箱预览反复串台——fast 模式 DAG 并行批次完全不自动开沙箱(文件仍写盘,Step3/文件卡可查看);沙箱模式(单列)仍按「沙箱跟随当前席位」预览。

关于 .py 的强度:write_file 是全局工具(所有 agent 共用),无法在网关层按扩展名硬拦,故 .py 禁止走提示词(强约束)+ 禁 bash(写了也跑不了,脚本生成产物的动机被掐断)。若实测仍偶发写 .py 且需硬拦,下一步:给 lead_agent 的 write_file 工具加一个「按 run context 禁某些扩展名」的开关,网关 _run_payload 对圆桌席位置 [".py"](只影响圆桌 run,不动其它 agent)。

改动:roundtable_run_policy.py、test_roundtable_run_policy.py、lib/dag-types.ts、lib/dag-types.test.ts(35 过)、hooks/useStep2Orchestration.ts。前端 tsc 65、后端 py_compile OK + 6 policy 单测过。⚠️ 改了后端,需重启 Gateway。

12.9 ✅ 席位「按需取前序全文」只读工具 read_peer_delivery(2026-06-07)

诉求:A,B,C → D,E 时,D,E 默认拿 A,B,C 的摘要,但识别到需要全文时也能取到。之前只有总控(leader)能「摘要默认+按需全文」(跨 thread 读),下游席位不能(席位间 thread 隔离、只有 1500 字截断广播)。

实现(真·按需,全文只在调用工具时才进 LLM 上下文):

  1. 新 harness 只读工具 read_peer_delivery(seat_name)(packages/harness/deerflow/tools/builtins/roundtable_peers_tool.py):从 runtime.context["roundtable_peer_deliveries"](网关注入的 [{name,content}])按席位名取全文;空名→列出可取席位;非圆桌场景→「无」。只读 context dict,不跨 thread、不碰 sandbox、无网络,纯 deerflow.*(不违反 harness→app 防火墙)。注册进 tools/tools.py 的 builtin(始终在,只读无副作用)。
  2. 网关注入 context:_run_payload 加 extra_context;_special_run 收 peer_deliveries → 注入 context.roundtable_peer_deliveries;run/stream 读 peer_deliveries(规整 [{name,content}])。不进 prompt → 不调工具就不耗 token。
  3. run policy:read_peer_delivery 从 leader(总控自己跨 thread 读,不需要)+ report(Step3 不读前序)排除;席位(seat/seat_parallel)保留。
  4. 前端:runDagOrchestration 跨 stage 累积完整交付 priorDeliveries,stage>0 席位的 special run 带 peerDeliveries(api/multi-agent.ts → peer_deliveries);pickDagSeatTask 加 hasPeers 形参,有前序时在 task 里提示「需要某前序席位全文时调用 read_peer_delivery('<名>'),平时用摘要」。

最终语义:D、E 默认看 A、B、C 的摘要(1500 字广播);识别到不够 → 调 read_peer_delivery('情报收集') 取全文。总控仍是摘要默认+综合轮全文。= 用户要的「摘要默认、按需全文」对席位也成立了。

改动:后端 roundtable_peers_tool.py(新)、tools/tools.py、roundtable_run_policy.py、multi_agent.py;前端 api/multi-agent.ts、lib/dag-types.ts、lib/dag-types.test.ts(36 过)、hooks/useStep2Orchestration.ts。前端 tsc 65;后端 py_compile OK + 6 policy 单测过 + 工具导入/行为 smoke 通过。⚠️ 改了后端 + 新增 harness 工具,需重启 Gateway。

12.10 ✅ 修:沙箱模式第一个席位不自动开沙箱(2026-06-07,前端)

现象:沙箱(单列)模式并行时,第一个席位的消息列表不自动打开沙箱(要手动开);切换到其它席位时正常自动打开。

根因:沙箱跟随 effect(§12.5)在选中席位还没写产物时走「关闭沙箱」分支,里面同时调了 deselectArtifact() 和 setArtifactsOpen(false)。后者是 context 的 wrapped setOpen(context.tsx:73):关闭时若 autoOpen 为真 → 把 autoOpen 置 false。于是第一个席位刚选中(还没写文件)→ autoOpen 被关 → 该席位随后 live 写文件时,openSeatVirtualArtifact 的 !flags.autoOpen 守卫拦掉自动打开。切换到已写过文件的席位能开,是因为那条路走 openSeatLatestArtifact(force,auto=false,绕过 autoOpen 守卫)。

修(一行):关闭分支只保留 deselectArtifact()(它走 raw setOpen(false),不动 autoOpen),删掉多余的 setArtifactsOpen(false)。这样面板关掉、但 autoOpen 仍为真 → 第一个席位 live 写文件时正常自动开沙箱。(若用户手动关过沙箱,autoOpen 本就为 false,不会自动重开 —— 尊重用户。)改 hooks/useStep2Orchestration.ts 沙箱跟随 effect。tsc 65。

12.11 ✅ 修:快速模式又生成文件了 + Fragment 警告(2026-06-07);记录无关 404

  1. 快速模式不应生成文件(回退 §12.8 的过度放开):§12.8 为「让席位产 md」把 seat_parallel(快速模式)也放开了 write_file —— 但快速模式(双列)的设计就是产物走正文、不写文件,写文件归沙箱模式(单列 + 沙箱预览)。修:
    • roundtable_run_policy.py:seat_parallel 重新禁 write_file / str_replace(保留禁 bash);seat(沙箱模式)仍可写。
    • useStep2Orchestration.ts:parallelNoFile = dagExecMode !== "sandbox"(快速模式一律禁写,含单节点 stage;沙箱模式放开)。
    • pickDagSeatTask 加 allowFiles 形参:沙箱(true)=写 .md 文件 + 禁 .py;快速(false)=产物走正文、不写文件。hook 按 dagExecMode === "sandbox" 传。
    • 测试:test_sandbox_seat_writes_files_fast_parallel_does_not(6 过);dag-types.test.ts 拆成沙箱/快速两个产物要求用例(37 过)。
    • 语义:快速=双列、产物走正文、不写文件;沙箱=单列、写 .md、沙箱跟随预览。
  2. Invalid prop data-lov-id supplied to React.Fragment:DagFlowChart 的 stage 循环用了具名 <Fragment key>,dev 代码标注插件(lovable-tagger)给它注入 data-lov-id,而 Fragment 只接受 key/children → React 警告。修:换成 <div className="contents" key=...>(display:contents 不生成盒子,Connector/StageColumn 仍是外层 flex 直接子项,布局不变,且能接收 data-*)。移除 Fragment import。
  3. (无关,非本期引入)/api/getTokenByEmail?email=…%40local.deerflow 404 ×60:来自 src/core/studio/api.ts 的 fetchStudioToken(studio/expert-auth 引导),用户邮箱被映射到 legacy @local.deerflow 域、后端 getTokenByEmail 不认 → 404,React Query 跨多个 useStudioToken 调用点重试 → 重复多次。与圆桌 DAG 改动无关(圆桌头像是本地 SVG,本期未新增任何前端 GET)。需要的话单独排查 studio token 链路。

13. 实施进度(截至 2026-06-06,下次会话续)

Phase 内容 状态
1 DAG 数据模型 + 校验 + 持久化字段(lib/dag-types.ts,24 单测) ✅ 完成
2 分层编辑器(做进业务链条页):链条加 stages 字段(前后端 + alembic 20260607_03 + 11 后端单测);BusinessChainEditorPage 竖列分组 Stage(DnD + 按钮);纯逻辑 lib/chain-stages.ts(19 单测) ✅ 完成
3 runDagOrchestration:批内 Promise.all 并行 + 批间 barrier 串行 + 依赖摘要注入(方案 B 前端直派) ✅ 完成
4 并行 run policy seat_parallel(roundtable_run_policy.py + multi_agent.py 读 parallel_no_file,6 后端单测);§12.1 方案 A 已放开 web_search/技能 ✅ 完成
5 并行显示:收起左侧 + 头像条 + 两列 + 选中切换 ✅ 完成(本轮修了侧栏/Markdown/字数)
6 流式高性能:ref 全量累积 + 节流 flush(仅选中渲染) ✅ 完成
7 DAG 流程图执行态可视化(DispatchChainModal 复刻成按 stage 分列;后改成弹窗见 §12.5) ✅ 完成(2026-06-07)

端到端闭环已通:链条页编排分层 → 存库 → 选链条进 Step2 → runDagOrchestration 并行直派 → 各阶段完交回总控派活/收口。

✅ §12.3 已实施(2026-06-07):见 §12.3「实施结论」。runDagOrchestration 在每个非末尾 stage barrier 后插入 leader 中转轮(synthesisMode:false 走轻量 last-ai-message 注入摘要、收口轮才按需全文),承上启下驱动下一批;后端零改动;dag-types.ts 加 buildDagTransitTask + buildStageTask.priorIsBridge,27 单测全过。

下次会话 TODO(优先级从高到低):

  1. 运行时实地验收 §12.3:①每个 stage 之间出现「总控协调(承上启下)」气泡,内容是对上一批的综合;②后端日志中转轮 [leader-roster] ... 注入席位交付状态 显示上一批 ✅、未跑的 ⬜;③下一 stage 席位 task 里带的是总控承上说明(非机械摘要);④中转轮不应真的派活(即使派了前端也忽略)。
  2. 运行时实地验收(旧):①§12.1 后并行席位能搜索、步骤条显示;②并行 special 日志时间重叠 + Excluded 不含 web_search 含 write_file;③用户干预后是「总控派活」而非重跑 DAG;④最左全局侧栏 + 参与角色栏并行收起且不回弹。
  3. ✅ Phase 7 DAG 流程图可视化已完成(2026-06-07):见 §7.2。DagFlowChart 组件 + 编辑器只读预览 + Step2 执行态缩略(dagNodeStates 实时驱动)。
  4. (可选)串行也确定性注入上一轮交付摘要。

关键文件速查:

  • 前端:hooks/useStep2Orchestration.ts(runDagOrchestration / dagPlanExecutedRef / 并行 store+flush)、components/Step2Panel.tsx(两列+头像条)、components/ParallelSeatStrip.tsx、lib/dag-types.ts、lib/chain-stages.ts、pages/BusinessChainEditorPage.tsx、pages/RoundtablePlanningPage.tsx(侧栏收起 effect + handleConfirmAgents dag 分支)、components/BusinessChainPicker.tsx(链条→dag)。
  • 后端:app/gateway/roundtable_run_policy.py(seat_parallel,待放宽)、app/gateway/routers/multi_agent.py(_special_run 读 parallel_no_file)、persistence/roundtable_chains/(stages 列)、migrations/versions/20260607_03_chain_stages.py。
  • 测试:frontend-web 用 esbuild 转译 .test.ts 后 node --test(项目无 vitest);后端 PYTHONPATH=. uv run pytest tests/test_roundtable_chain_stages.py tests/test_roundtable_run_policy.py。