deerflow-code/frontend-web/docs/multi-agent-frontend-dev.md
2026-09-07 18:24:55 +08:00

55 KiB
Raw Blame History

多智能体圆桌研讨 · 前端开发文档

本文档面向维护 frontend-web 圆桌规划功能的开发者。后端实现见 multi-agent-backend-dev.md;HTTP 字段速查见 multi-agent-api.md;推荐弹窗专项进度见 recommend-agents-progress.md(若不存在则为待补文件)。

⚠️ 历史版本曾引用 multi-agent-step1-api.md、multi-agent-step3-api.md,当前仓库未提供这两份文件;Step 1(intent)/ Step 3(artifacts)的接口签名直接参考本页第 4 节即可。


0. 速读:你需要先知道的事

  • 三步 UI 通过 currentStep ∈ {1,2,3} 切换,不使用 React Router 子路由。主页面 RoundtablePlanningPage.tsx(≈ 1 150 行)只负责组合 + 编排,业务状态全部下沉到三个 hook(见下面)。
  • 业务状态分三块:useStep1Intent(任务理解 + 澄清)、useStep2Orchestration(编排循环 + 对话流 + Persona)、useDraftPersistence(草稿)。改 Step X 的逻辑请直接进对应 hook,不要回到主页面里写。
  • 消息气泡有共享件:Step 1 / Step 2 的对话气泡共用 MessageBubble.tsx 里的 MessageBubble + MessageLoadingPlaceholder + StreamingCursor + MarkdownContent。改气泡通用样式(头像、header、loading、流式 cursor、Markdown 渲染配置)请改这里,不要在两个 Panel 各改一遍。
  • 步骤折叠卡与主聊天对齐:思考过程 / 工具调用展示走 MessageStepsCard,always-show-last-tool + 折叠 "查看其他 N 个步骤" 的视觉与主聊天 chats/new 的 MessageGroup 一致。工具图标 / 文案映射在 lib/step-display.tsx,新增工具时与 src/core/i18n/locales/zh-CN.ts 的 toolCalls 字段对齐。
  • <think> 标签自动提取为「思考过程」:lib/reasoning.ts::splitInlineReasoning(text) 把 <think>...</think>(含未闭合的流式中段)抽离成 reasoning,正文走 markdown;由 MessageBubble.tsx::ReasoningBlock 渲染一个折叠卡。Step 1 / Step 2 气泡和推荐弹窗的 rationale 都走这一套,与主聊天 Reasoning 元素的视觉对齐。
  • 不使用 LangGraph SDK:所有 API 都是自研的 apiFetch + ReadableStream SSE 解析(见 api/intent.ts、api/recommend.ts、api/multi-agent.ts)。
  • 草稿/历史持久化在后端(2026-06):草稿不再存浏览器 localStorage,改走后端 MySQL(/api/roundtable-drafts,按登录用户隔离、跨设备保留)。api/drafts.ts 封装 CRUD,useDraftPersistence 异步对接、对外接口不变。推荐弹窗带「会话级推荐历史」:每个草稿一串历史,重开弹窗先显示上次推荐结果,可「重新分析」重跑(详见 §5.7 / §6.2)。后端实现见 backend doc 的「圆桌草稿持久化」一节。
  • 不写死模型名:与 /page/canvas/ai-writing 同样的策略 —— 全局选中模型由 selectedModel state 维护,首次 useModels() 完成后初始化为 availableModels[0].name,顶栏「模型」下拉可切换。所有 API 调用统一传 model: selectedModel || undefined;后端缺省由 lead_agent._resolve_model_name() 回退到 config.yaml 的 models[0]。内网部署只需在 config.yaml 配自家模型即可,前后端均无需改动。
  • 席位区间硬约束:推荐弹窗要求 2 ≤ 选中数 ≤ 8(MIN_SELECTED / MAX_SELECTED)。
  • 协调智能体单例(2026-05):COORDINATOR_AGENT_ID = "roundtable-coordinator" 是固定常量,不再每会话生成新 id。后端 seeder 自动落地,「本次席位列表」改由后端通过 thread 历史 human 消息注入,前端无感。newCoordinatorName() 仅作为兼容性壳保留。
  • 总控编排循环上限:MAX_CYCLES = 8,超出后强制停止并 toast。
  • 席位派活策略:顺序(不是并行)。旧实现并行触发多路 SSE 会让浏览器卡顿,现已改为顺序。
  • 调试开关:window.__MULTI_AGENT_INIT_TIMING__ 与 window.__MULTI_AGENT_DEBUG__(不是 __MULTI_AGENT_STREAM_DEBUG__,文档历史版本有误)。

1. 功能定位

页面路由:/page/workspace/roundtable/planning(侧栏「圆桌规划」)

入口组件:src/roundtable-planning/pages/RoundtablePlanningPage.tsx

核心能力:

步骤 用户目标 真实后端对接
1 与「任务理解智能体」多轮澄清,得到结构化意图 intent.ts
1→2 从候选池选 2–8 个研讨席位 recommend.ts + RecommendAgentsDialog
2 总控派活、子智能体顺序交付、可澄清/挂起 multi-agent.ts + 编排循环
3 查看总控结论、席位交付、产出文件下载 HighFidelityReport + listSessionArtifacts

步骤门禁:

  • Step 2:intentReady 非空才可点击(顶栏按钮禁用 + toast 提示)。
  • Step 3:hasConsensus === true 才可点击。Leader 派活返回空数组才会自然收敛触发 hasConsensus,手动跳转的「达成共识生成全案成果」按钮也会校验。

2. 目录结构

frontend-web/src/roundtable-planning/
├── index.ts                            # 导出 RoundtablePlanningPage、HighFidelityReport
├── pages/
│   └── RoundtablePlanningPage.tsx      # 主页面:组合 hook + 顶栏/侧栏 + 三步路由(≈1 150 行)
├── api/
│   ├── intent.ts                       # Step 1:initIntent、streamIntent
│   ├── recommend.ts                    # 推荐:streamRecommend、filterRecommendCandidates、parseRecommendStreamContent;**+ 推荐历史 listRecommendHistory / saveRecommendHistory**
│   ├── drafts.ts                       # **草稿后端 CRUD**:listDrafts / getDraft / createDraft / updateDraft / deleteDraft / deriveDraftName(替代旧 localStorage utils/drafts.ts)
│   └── multi-agent.ts                  # Step 2/3:initMultiAgent、streamMultiAgent、listSessionArtifacts、buildArtifactDownloadUrl
├── components/
│   ├── Avatars.tsx                     # 所有 SVG 头像 + getAvatar 选择器
│   ├── MessageBubble.tsx               # Step 1/2 共享:消息壳 + loading 占位 + 流式 cursor + MD 配置
│   ├── MessageStepsCard.tsx            # Step 1/2 共享:工具调用 + 思考过程的折叠卡(对齐主聊天 MessageGroup)
│   ├── Step1Panel.tsx                  # Step 1 对话面板(不含状态,只读 props)
│   ├── Step2Panel.tsx                  # Step 2 主面板(圆桌对话流 + 澄清条 + 干预输入)
│   ├── Step3Panel.tsx                  # Step 3 薄壳,内部仍是 HighFidelityReport
│   ├── PersonaDrawer.tsx               # Step 2 Persona 抽屉(System Prompt / Temp / Model)
│   ├── IntentClarificationCard.tsx     # Step 1 选项卡片(单选/多选)+ formatIntentClarificationAnswer
│   ├── RecommendAgentsDialog.tsx       # Step 1→2 推荐弹窗(含流式 rationale + 候选筛选)
│   ├── HighFidelityReport.tsx          # Step 3 报告 + 文件清单
│   ├── ArtifactPreviewModal.tsx        # Markdown/纯文本/JSON/CSV/YAML 等文本类文件 in-app 预览
│   └── ScrollToBottomButton.tsx        # 对话区「回到底部」浮动按钮
├── hooks/
│   ├── useAutoScroll.ts                # 滚动跟随 + 用户上滑暂停跟随
│   ├── useStep1Intent.ts               # Step 1 状态机:init、send、stream、abort、INTENT_READY 同步
│   ├── useStep2Orchestration.ts        # Step 2 状态机:initMultiAgent + 编排循环 + 对话流管理 + Persona
│   └── useDraftPersistence.ts          # 草稿(后端版):异步走 api/drafts;自动保存(前进切换)/手动保存/加载/重命名/删除/ensureDraft
├── lib/
│   ├── clarification.ts                # resolveAllowMultiple(单选/多选解析),与主聊天 ClarificationCard 对齐
│   ├── reasoning.ts                    # splitInlineReasoning:从模型流式文本里提取 `<think>...</think>` 思考过程
│   ├── roundtable-constants.ts         # 类型(RoleEntry / SelectedAgent) + 常量 + 纯工具函数(buildRoleTemplates / splitIntentReadyBlock 等)
│   └── step-display.tsx                # Step 2 ChainOfThought 折叠卡图标 + 文案 + 子节点渲染
├── utils/                              # (旧 drafts.ts 已删除 —— 草稿改走后端 api/drafts.ts)
└── styles/
    └── roundtable-planning.css         # 页面主题变量(浅红品牌色 + 深色覆盖)

模块依赖方向(自上而下):

pages/RoundtablePlanningPage
   ├─ uses → hooks/useStep1Intent
   ├─ uses → hooks/useStep2Orchestration
   ├─ uses → hooks/useDraftPersistence  (依赖 step1 / step2 的 getSnapshot + hydrate)
   ├─ uses → components/Step{1,2,3}Panel  (纯 props 驱动)
   ├─ uses → components/RecommendAgentsDialog
   └─ uses → components/Avatars

hooks/useStep1Intent     → api/intent
hooks/useStep2Orchestration → api/multi-agent + components/PersonaDrawer 的类型
hooks/useDraftPersistence → utils/drafts + step1/step2 的 Snapshot 类型

主页面通过 ref 桥接两个 hook 之间的依赖(buildInitialIntent 在 Step 2 init 时从 Step 1 状态读取),避免 hook 互相引用造成的循环导入。

路由注册(src/pages/WorkspaceRoutes.tsx):

<Route path="roundtable/planning" element={<WorkspaceLayout><RoundtablePlanningPage /></WorkspaceLayout>} />

侧栏入口:src/components/page-sidebar.tsx → /page/roundtable/planning。

环境变量(由 scripts/start-frontend.sh 注入):

  • VITE_BACKEND_BASE_URL → getBackendBaseURL(),所有 API 前缀
  • VITE_LANGGRAPH_BASE_URL → 本功能不直接用 LangGraph SDK,全部走 Gateway 封装

3. 三步数据流(前端视角)

[进入 Step 1]
  useEffect → runStep1Init()
    initIntent(model) → { agent_id, thread_id }
    setIntentThreadId、setTimelineEntries
    interactiveMessages seed 欢迎语(本地 seed,非 API)

[用户发送 / 选项卡片提交]
  sendIntentMessage(text)
    追加 user 气泡 + agent 占位(streaming + phase)
    streamIntent({ onTextDelta, onPhaseChange, signal })
    终态:
      done + summary       → setIntentReady,解锁 Step 2 / 推荐弹窗
      clarification        → IntentClarificationCard + pendingIntentClarificationMsgId
      asking               → 继续对话
      error                → toast + 红色 (错误) 文案

[点击「进入多智能体研讨」且 intentReady]
  setIsRecommendDialogOpen(true)
  RecommendAgentsDialog
    listAgents() → filterRecommendCandidates()
    streamRecommend({ onRationaleUpdate, onPicksUpdate, onPhaseChange })
    用户勾选 2–8 个 → onConfirm(agentIds)
  handleConfirmAgents
    setSelectedAgents(agentIds.map(...))
    step2InitRef.current = false      // 复位 Step 2 init 闸门
    setCurrentStep(2)

[进入 Step 2]
  useEffect(step2InitRef 闸门)→ initMultiAgent(SSE)
    onSeatReady:右侧席位面板逐个「在线」
    onCoordinatorReady / init_done → threadIds + coordinatorName
  runOrchestration(buildInitialIntent(), threadIds, coordinatorName)
    while !cancelled && cycles<8:
      leader stream → 若 clarification:暂停循环,展示 Step 2 底部澄清条
      否则 dispatched[] → **顺序** streamMultiAgent(special) → 再 leader 综合…
      dispatched.length === 0 → hasConsensus = true,可进 Step 3

[进入 Step 3]
  HighFidelityReport
      consensusText ← lastLeaderContent
      seatDeliveries ← step2RoundtableDialogues.filter(d => d.confidence === "子智能体交付")
      Object.entries(threadIds).filter(name !== coordinatorName).map → listSessionArtifacts(threadId)
      文本类文件 → ArtifactPreviewModal;其它 → window.open(downloadUrl)

4. API 层

4.1 api/intent.ts — Step 1

函数 说明
initIntent(model?) POST /api/intent/init → { status, agent_id, thread_id }
streamIntent(req) POST /api/intent/stream,读 SSE 至终态帧

StreamIntentRequest 重要字段:

  • threadId:必填,跨多轮对话保持
  • signal:必填(页面通过 intentAborterRef 控制暂停 / 切步取消)
  • onTextDelta(text, messageId):每个 AI 文本增量
  • onPhaseChange(phase, toolName?):connecting → connected → streaming|tool_calling
  • onClarification(clarification, index)(2026-06):每解析到一条 ask_clarification 工具结果帧就回调一次,让前端实时逐个渲染澄清卡。任务理解智能体一轮可能问多个问题,这是「实时接收显示协助过程」的关键入口。

StreamIntentResult:

字段 类型 说明
status "asking" | "clarification" | "done" | "error" 终态分支
content string 完整 AI 文本(含 [INTENT_READY] 块)
summary IntentSummary | null 仅 done 有;前端 fallback 也会从 content 里用 splitIntentReadyBlock 解
clarification ClarificationPayload | null 第一个澄清(向后兼容字段)
clarifications ClarificationPayload[] 本轮全部澄清问题(2026-06)。来源是流式 type:"tool" 帧而非终态帧——终态帧只带第 1 个,多问题必须靠它。每条带 id(= tool_call_id)供前端按题维护选择。

SSE 解析要点:

  • event: metadata → phase connected
  • event: messages + extractAiTextDelta → onTextDelta
  • event: messages + type:"tool" 且 name==="ask_clarification" → extractClarificationToolMessage 解出完整澄清(options 已是 {id,label}),按 tool_call_id 去重后 onClarification 实时回调并收进 result.clarifications
  • tool call 帧(tool_calls / tool_call_chunks 非空)→ phase tool_calling(含 ask_clarification 工具名)
  • 终态帧识别:isFinalIntentFrame(必须有 status + content);status==="clarification" 且未收到任何流式 tool 澄清帧时,才用终态帧字段兜底合成单条澄清

Abort 行为:

  • sendIntentMessage 内建 AbortController,保存到 intentAborterRef
  • stopIntentStream() 用于「暂停」按钮、currentStep !== 1 自动调用、handleStartNewTask、handleLoadDraft
  • 触发 AbortError 时不当作错误:在气泡尾部追加「(已暂停)」并 toast「⏸️ 已暂停任务理解回复」

4.2 api/recommend.ts — 推荐弹窗

函数 说明
filterRecommendCandidates(agents) 从 GET /api/agents 结果剔除 intent / recommender / roundtable-coordinator-*
streamRecommend(req) POST /api/recommend/stream,无 init(一次性、无状态)
parseRecommendStreamContent(raw) 将累计文本拆成 { rationale, picks[] },供边流边渲染

EXCLUDED_AGENT_IDS 内置常量(仅 recommend.ts 内可见):

  • roundtable-intent
  • roundtable-recommender
  • 任何以 roundtable-coordinator- 开头的 agent_id(动态创建的协调器壳)

流式 UI 拆分(与主聊天的 [RECOMMEND_READY] 分界类似):

  • 标记常量:RECOMMEND_READY_MARKER = "[RECOMMEND_READY]"
  • parseRecommendStreamContent(raw):用 PICK_OBJECT_PATTERN 正则增量匹配出已完成的 {agent_id, reason} 对象
  • onRationaleUpdate:仅 marker 之前的自然语言(弹窗顶部展示)
  • onPicksUpdate:从部分 JSON 增量解析 picks[](卡片边流边出,过滤已见 id)
  • 过滤 TitleMiddleware 产生的标题增量(langgraph_node 含 TitleMiddleware 或 tags 含 middleware:title)

终态语义:

  • status === "done" 且 picks.length >= 2 → 弹窗用 picks 作为默认勾选
  • 否则 → phase: "fallback",使用 FALLBACK_AGENT_IDS(6 个内置 roundtable-* 席位)与候选池交集

4.3 api/multi-agent.ts — Step 2 & Step 3 文件

函数 说明
initMultiAgent(payload) SSE:seat_ready(N 次) → coordinator_ready → init_done
streamMultiAgent(req) leader / special 一轮
listSessionArtifacts(threadId) GET /api/threads/{id}/artifacts
buildArtifactDownloadUrl(threadId, virtualPath, { download? }) 预览或附件下载 URL

initMultiAgent 事件(data: JSON 的 event 字段):

{ event: "seat_ready", agent_name, thread_id, display_name, description }
{ event: "coordinator_ready", main_agent_name, thread_id }
{ event: "init_done", main_agent_name, thread_ids }   // thread_ids: Record<agent_name, thread_id>
{ event: "error", detail }

streamMultiAgent 终态(isFinalStatusFrame):

status 含义 前端处理
[["agent","task"], ...] 派活列表(数组形态) 顺序触发 special;最终用 nextLeaderTask 综合
[] 共识达成(空数组) hasConsensus = true,跳出循环
"clarification" 总控追问 setPendingClarification,暂停循环
"子智能体 X 完成..." special 完成 更新对应对话气泡 confidence
"error" 失败 toast + 红色气泡

为什么 special 是顺序而不是并行:旧版并行触发多路 SSE,每路都在 token 级别对 step2RoundtableDialogues 整表 setState,N 个并行 stream 让浏览器卡顿明显。改为顺序后同一时刻只有一个 SSE 在跑,UX 平滑很多(详见 RoundtablePlanningPage.tsx 编排循环里的注释)。

buildArtifactDownloadUrl 路径编码:将 virtualPath 按 / 切段后逐段 encodeURIComponent 再拼回,避免文件名里的中文 / 空格触发后端 404;download=true 加在 query string 上而非 path。

调试开关:

window.__MULTI_AGENT_INIT_TIMING__ = true;  // 默认开启;init 各阶段耗时(fetch.start / first-byte / seat_ready#n / coordinator_ready / init_done / total)
window.__MULTI_AGENT_DEBUG__       = true;  // 默认开启;stream 帧计数 + lastEvent + bad JSON 提示,前缀 [stream:<agentType>:<agentName>]

⚠️ 文档历史版本曾写作 __MULTI_AGENT_STREAM_DEBUG__,实际代码并不识别这个名字。


5. 主页面状态(RoundtablePlanningPage)

注意:自 2026-05 重构后,绝大多数业务状态从主页面下沉到 3 个 hook。下面列出的 state 仍按"位置"分组以便对照原代码,实际所有者请看右列。

维度 所有者 暴露给主页面的方式
currentStep、showToast、selectedModel、roles、isRecommendDialogOpen 主页面 useState
Step 1 全部 state + handler useStep1Intent hook 返回值
Step 2 全部 state + handler + 编排循环 useStep2Orchestration hook 返回值
草稿(drafts / currentDraftId / 重命名 / 历史下拉) useDraftPersistence hook 返回值
loadedFromDraftRef 共享 latch useDraftPersistence 拥有,Step 2 init effect 通过 options 读 hook 间共享 ref

5.1 步骤与门禁

  • currentStep: 1 | 2 | 3
  • Step 2 锁定:intentReady == null 时顶栏 Step 2 按钮禁用(visual + aria-disabled),同时点击「进入多智能体研讨」会 toast 「⚠️ 请先与任务理解智能体完成澄清…」
  • Step 3:hasConsensus === false 时顶栏 Step 3 与底部「达成共识生成全案成果」按钮均禁用

5.2 Step 1 关键 state

状态 用途
intentThreadId /api/intent/init 返回的线程 id;未就绪前发送按钮置灰
intentReady IntentSummary,驱动 Step 2 初始 prompt + 右侧摘要面板 + 顶栏 step2 解锁
interactiveMessages 问答气泡列表(user / agent,含 streaming/phase/clarification 元数据)
pendingIntentClarificationMsgId 当前可操作的 agent 消息 id(保证只有最新一条 clarification 展示可点卡片)
intentClarificationSelections Map<msgId, {selectedIds, custom}>,多选 / 多消息时各自维护
isIntentSending 发送中(屏蔽双发 / 显示「暂停」按钮)
intentAborterRef 当前 stream 的 AbortController
intentInitError init 失败信息;驱动右侧角色面板「异常」状态与红色提示
step1InitRef 闸门:true 即认为已 init 过
step1InitGenRef 单调递增的代数标记,详见下文
timelineEntries 右侧「澄清记录」时间线(init / user 反馈 / 任务意图已明确)

step1InitGenRef 的存在原因:

React 18 StrictMode 开发环境下,effect 会 mount → cleanup → mount 两次。早期实现用闭包 cancelled 标志做防抖,结果是:第二次 mount 因 step1InitRef.current === true 早返回,但第一次 mount 的 async 仍未完成;当它最终 resolve 时,cancelled 已被 cleanup 置 true,因此 setIntentThreadId 被跳过,右栏卡在「正在上线」。改为单调代数后:每次 runStep1Init 自增 step1InitGenRef;async 完成时检查 myGen === step1InitGenRef.current,等价于「我是当前最新一次 init」,否则丢弃结果。这也覆盖了「新建任务 / 加载草稿期间旧 init 晚归」的场景。

splitIntentReadyBlock(text)(页面内工具函数,使用 INTENT_READY_RE 正则):

  • 从气泡正文解析 [INTENT_READY]\n```json\n{...}\n``` 块
  • 返回 { before, summary, after },前后文与结构化卡片分别渲染
  • 单独的 useEffect 监听 interactiveMessages:若已存在含 [INTENT_READY] 的 agent 消息但 intentReady 仍为 null(极少数终态帧丢 summary 的场景),主动 setIntentReady,与后端 "READY 优先" 对齐

5.3 推荐与 Step 2 席位

状态 用途
selectedAgents { agent_id, name }[],默认 DEFAULT_SELECTED_AGENTS(6 内置)
roleTemplates / roles 右侧席位面板(buildRoleTemplates,commander 占 id=1,agents 从 id=2 开始)
agentNameToRole 流式输出时根据 agent_name(小写)反查 speakerId、avatarType、displayName
roundtableAgentIds selectedAgents.map(a => a.agent_id),传给 /api/multi-agent/init 的 agent_names
threadIds / coordinatorName /api/multi-agent/init 结果;离开 Step 2 不清空,允许回来继续看
step2RoundtableDialogues Step 2 对话流(含 streaming / confidence / time / avatarType)
lastLeaderContent / hasConsensus Step 3 总控结论 + 共识标记
pendingClarification Step 2 总控澄清(复用 ClarificationPayload)
clarificationDraft 底部自定义回答输入框(受 allowCustom 控制)
isRoundTableRunning 用户挂起 / 恢复协商的 UI 开关
isOrchestrating 编排循环正在 run(屏蔽并发触发)
orchestrationCancelRef 与 abortAllInFlight 配合中断当前循环
activeAbortersRef 当前 Step 2 所有 in-flight stream 的 AbortController 集合
activeSpeakerId / activeSpeakerSet 圆桌可视化高亮:单 leader 时用单值,多席位并行时用 Set
selectedAgentId 当前 Persona 抽屉所在的席位 id(点击圆桌头像打开)
agentConfigs Map<seatId, {model, weight, prompt, temp}> Persona 覆盖;下一轮该席位的派活会用此 model
isUpperPanelCollapsed 是否折叠圆桌可视化 + 调控台,给对话区让出垂直空间

协调智能体命名(2026-05 改造):现在是固定单例 id roundtable-coordinator(见 lib/roundtable-constants.ts::COORDINATOR_AGENT_ID)。newCoordinatorName() 函数为兼容性保留,内部直接返回这个常量字符串。后端 ensure_roundtable_functional_agents() 缺失自动落地,不再每次新建。filterRecommendCandidates 在 EXCLUDED_AGENT_IDS 集合里同时排除:

  • roundtable-coordinator(新版单例 id,永远过滤);
  • roundtable-coordinator-* 前缀(历史残留,管理员清理之前先在 UI 隐藏)。

历史草稿兼容性:用户加载 2026-05 前保存的草稿时,存储的 coordinatorName 仍是 roundtable-coordinator-<ts36> 形式。只要那个旧 agent 目录还在磁盘上(即运维没手动删过),加载后继续在 Step 2 干活就能正常工作;如果旧 agent 被清理掉了,需要让用户「重新起草」从头跑一遍 Step 1 → Step 2。

5.4 编排循环 runOrchestration(seedMessage, ids, coord)

伪代码:

isOrchestrating = true
orchestrationCancelRef = false
nextLeaderTask = seedMessage
cycles = 0
MAX_CYCLES = 8

while !cancelled && cycles < MAX_CYCLES:
  cycles += 1
  highlight(speakerId=2)   // 总控
  leaderBubble = appendStreamingDialogue("总控协调", confidence: "调度中")
  res = streamMultiAgent(leader, ids, coord, nextLeaderTask, onTextDelta, onPhaseChange)

  if res.clarification:
    finalize(leaderBubble, content, "等待用户澄清")
    setPendingClarification(res.clarification)
    break        // 等用户回复后由 submitClarification 重启循环

  finalize(leaderBubble, content, dispatched.length === 0 ? "全案盖印" : "派活说明")
  if res.content: setLastLeaderContent(res.content)

  if res.dispatched.length === 0:
    setHasConsensus(true); setConsensusPercentage(100); break

  for [subName, task] of res.dispatched:        // **顺序**,不是并行
    meta = agentNameToRole[subName.toLowerCase()]
    markSpeakerStreaming(meta.speakerId)
    perSeatModel = agentConfigs[meta.speakerId]?.model || DEFAULT_MODEL
    subBubble = appendStreamingDialogue(meta.displayName, confidence: "调度中")
    specRes = streamMultiAgent(special, ..., perSeatModel, signal=subAborter)
    finalize(subBubble, specRes.content, "子智能体交付")
    markSpeakerIdle(meta.speakerId)
    if cancelled: break

  setConsensusPercentage(prev => min(95, prev + 8))
  nextLeaderTask = "各子智能体已就上一轮派活完成交付,请你结合最新结论..."

if cycles >= MAX_CYCLES:
  toast("⏱️ 已达到最大研讨轮次,自动停止以避免循环")

clearAllStreamingSpeakers()
isOrchestrating = false

要点:

  • buildInitialIntent():
    • 有 intentReady → 拼装 objective / 约束条件 / 关键假设 + 「不要再反问用户」终结语
    • 无 intentReady(用户跳过澄清 / 顶栏直接切到 Step 2)→ 拼接 Step 1 内 interactiveMessages 里 sender==='user' 的内容,逐行 用户#N: ...,绝不注入 mock 的「预算 500 万 / 72 小时」数字
    • 用户输入完全为空 → 让总控先调用 ask_clarification 而不是凭空派活
  • 用户「干预研审」:handleInjectToRoundtable 追加一个「💡 (人工指令介入) ...」气泡,然后构造 runOrchestration("用户人工干预新指令:...", ...)
  • 挂起 / 恢复:
    • 挂起 = setIsRoundTableRunning(false) → stopOrchestration() → orchestrationCancelRef = true + abortAllInFlight()
    • 恢复 = setIsRoundTableRunning(true),effect 触发 runOrchestration("用户已恢复研讨,请继续上一轮的协调工作。", ...)
  • 澄清恢复:submitClarification 把用户回答拼到 prompt 里再 runOrchestration,不会重建 thread

5.5 Persona 抽屉(Step 2 圆桌可视化点击)

  • 点击圆桌头像 → setSelectedAgentId(seatId) → 在上半部分下方展开抽屉
  • 抽屉控件:
    • System Prompt:仅前端记录(后端覆写未开放)
    • Temp:仅前端记录
    • 推理模型:选项来自 useModels();显示值优先级 agentConfigs[seat]?.model > selectedModel
  • 「参数覆盖并重启动算研讨」按钮 → handleSaveAgentAndRecalculate:
    • 把新的 (seatId, model) 写入 agentConfigs
    • 追加一条「⚡ [模型偏好已保存] ...」对话气泡
    • 不立即重启编排;下一轮该席位被派活时 runOrchestration 内会读 agentConfigs[meta.speakerId]?.model || selectedModel || undefined 作为 special 调用的 model 参数

5.6 全局模型选择器(右侧 sidebar 顶部)

  • 位置:右侧 sidebar currentStep !== 3 时显示的第一个面板 —— 在「当前意图摘要」正上方。Step 3 时整个 sidebar 切换成「最终共识结论 / 风险与预算摘要 / 导出与交付」,所以模型选择器仅在 Step 1 / Step 2 可见(此时也是唯一会发起新模型调用的阶段)
  • state:const [selectedModel, setSelectedModel] = useState<string>("")
  • 初始化:useEffect(() => ...) 在 availableModels 首次返回后把 selectedModel 设为 availableModels[0].name;后端配置变化导致当前选中 model 不在列表里时也回退到首项
  • 传参规则:所有 5 个 API 调用点(initIntent / streamIntent / streamRecommend / initMultiAgent / streamMultiAgent leader / streamMultiAgent special)都传 selectedModel || undefined;special 还会先看 agentConfigs[seat]?.model per-seat 覆盖
  • 禁用态:availableModels.length === 0 或 modelsLoading → disabled,placeholder 显示「加载中…」或「未配置模型」
  • 可改换为 Radix Select:当前用原生 <select> 与 Persona 抽屉保持一致;如需统一样式可改为 @/components/ui/select,行为不变

5.7 草稿持久化(api/drafts.ts + 后端 MySQL,2026-06 改造)

重大变更:草稿从浏览器 localStorage 迁到后端 MySQL,按登录用户隔离、跨设备/会话保留。旧 utils/drafts.ts 已删除;useDraftPersistence 仍保持对外接口不变(列表项含 name,由后端 title 映射而来),主页面无需改动。后端表/路由见 backend doc「圆桌草稿持久化」。

  • API 客户端 api/drafts.ts:listDrafts()(轻量元数据)/ getDraft(id)(全量 step1+step2)/ createDraft / updateDraft(partial)/ deleteDraft / deriveDraftName。snake_case↔camelCase 映射在这一层收口(后端 furthest_step/created_at/updated_at ↔ 前端 furthestStep/...)。
  • 快照形状:DraftStep1Snapshot.{intentThreadId, intentReady, interactiveMessages, timelineEntries}、DraftStep2Snapshot.{selectedAgents, threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit, seatStubMessages, orchestrationMode?, chain?};以 JSON 整存进后端 step1/step2(PortableLongText/LONGTEXT,避免大对话超 64KB)。
    • selectedAgents(2026-06 修复):本次研讨实际选中的席位。不存的话加载草稿后 Step 2/3 左侧「参与角色」会回退到 DEFAULT_SELECTED_AGENTS(默认 6 个)而非当时真正选的那几个 —— roleTemplates/agentNameToRole 都由 selectedAgents 派生。useStep2Orchestration.hydrateFromDraft 在该字段非空时 setSelectedAgents,老草稿(无此字段)保持当前值不强行覆盖。
    • orchestrationMode? / chain?(2026-06 业务链条):Step 2 编排模式(recommend=总控自由派活 / chain=按业务链条顺序派活)+ 来源链条 {id,title}。是主页面 state,由页面在 getStep2Snapshot 写入、在包裹版 hydrateStep2 里读出后 setOrchestrationMode/setChainMeta(step2 hook 的 hydrateFromDraft 不直接消费)。老草稿无字段 → 默认 recommend。业务链条完整设计见 multi-agent-business-chain-dev.md。
  • deriveDraftName({intentReady, interactiveMessages}):优先 intentReady.objective,否则第一个 user 消息,否则 未命名草稿 <时间>,截断 40 字。仅在 create 时写一次(见下方 bug 修复)。
  • 列表加载:挂载时 useEffect 调 listDrafts();增删改后 refreshList() 重拉。
  • 自动保存策略:仅在「前进」步骤切换时保存(1→2、2→3),后退不触发。
  • 手动保存 / ensureDraft:handleManualSave 走同一条保存队列;ensureDraft(furthestStep?) 幂等保证后端有一条对应草稿并返回 id(推荐弹窗打开前调用,让推荐历史有 draft_id 归属)。
  • 加载草稿 handleLoadDraft:
    • 走 getDraft(id) 实时拉服务端最新全量(不再读内存里可能过期的列表快照);404 → toast「草稿已不存在」并刷新列表。
    • 先置 loadedFromDraftRef.current = true,再 setCurrentStep(draft.furthestStep)(顺序不能反)。
    • setStep2InitGate(draft.furthestStep >= 2):曾走到 Step 2 才跳过自动 init。
    • 角色面板基线:被恢复时全部「在线」,否则 init 流程逐个「在线」。

本次随手修复的 4 个旧 localStorage 版 bug(对接后端时一并解决):

  1. 自动保存覆盖手动重命名 —— 现在所有 updateDraft 一律不带 title;标题只由 create(派生)与显式「重命名」改写。
  2. 快速连续前进(1→2→3)产生重复草稿 —— 所有保存经一条串行队列(saveQueueRef)+ 同步镜像 currentDraftIdRef,create 完成即写 id,后续保存读到 id 走 update 而非再 create。
  3. 加载读到内存旧列表快照 —— 改为 getDraft(id) 实时拉。
  4. localStorage 配额静默写失败 / 数据丢失 —— 迁后端后不复存在。

6. 组件说明

6.1 IntentClarificationCard

  • Props:payload(ClarificationPayload)、selection、onSubmit(overrideSelection?)、disabled、submitting、fallbackContent、deferSubmit?(2026-06)
  • 单选(默认 allowMultiple === false):resolveAllowMultiple 返回 false → 点击选项即 onSubmit({selectedIds:[id], custom: ""}),UI 不显示「发送回答」按钮(除非 allowCustom)
  • 多选:勾选多个 + 输入补充,再点「发送回答」;答案格式见 formatIntentClarificationAnswer:
    • 仅选项 → 我选择:A、B
    • 仅自定义 → 直接发自定义文本
    • 同时存在 → 我选择:A、B;补充:…
  • 单问题 vs 多问题的判断(2026-06):Step1Message 按 clarifications.length 分流,默认单选(后端 allow_multiple 缺省 false,resolveAllowMultiple 兜底):
    • 单问题(最常见):deferSubmit={false},沿用原交互 —— 单选点击选项即发送;多选 / 允许自定义则走卡片自带的「发送回答」按钮。不显示合并按钮。
    • 多问题:deferSubmit={true} —— 连单选点击也只记录选择(不立即发送),卡片自身隐藏按钮;父组件在卡片下方渲染一个「发送全部回答」按钮,统一提交。
    • 该判断纯在前端(不动工具/后端):依据 clarifications.length + allow_multiple,后端 ask_clarification 本就默认单选。
  • 合并提交逻辑:submitIntentClarifications(msgId, clarifications, overrideByKey?) 逐题取选择、格式化、拼成一条消息(多题带 1. <问题>\n答:<答案> 编号;单题直接发答案;至少答一题才发)。overrideByKey 供「单题单选点击即发送」用——此时选择还没写进 state map(setState 异步),把即时选择按 clarificationKey 传进去直接用。
    • 逐题选择隔离:选择 Map 改为 Map<string, IntentClarificationSelection>,key 由 clarificationKey(msgId, clar.id, index) 生成(clar.id = tool_call_id,缺省退化为 idx-N)。
  • 展示条件:isAgent && clarifications.length>0 && !intentReady && !parts.summary;流式期也展示(卡片 disabled,只读,体现「协助过程」实时出现),仅当终态且 pendingIntentClarificationMsgId === msg.id && !streaming 时才可交互。出现 [INTENT_READY] / summary 后隐藏。
  • 步骤链强化:onClarification 同时往 msg.steps 追加一条 追问 N:<问题>,让 MessageStepsCard 把每个 ask_clarification 调用显示成独立步骤。

6.2 RecommendAgentsDialog

席位来源选择器(2026-06 业务链条):弹窗顶部加了模式分段切换 智能推荐 | 业务链条。智能推荐 = 本节描述的现状(零行为变更,整体收进 {mode==="recommend"} 分支);业务链条 = BusinessChainPicker(选一条已配置链条,卡片标题 + 彩色席位 tag + → 箭头)。onConfirm 统一回传 SeatSelectionResult{ agents, mode, chain? };mode==="chain" 时 Step 2 由总控按链条顺序派活(useStep2Orchestration.runChainOrchestration)。配置页与编排详见 multi-agent-business-chain-dev.md。

  • 新增 draftId prop(2026-06):本次推荐归属的草稿/会话 id。由主页面在打开弹窗前 await drafts.ensureDraft(1) 拿到并传入(见 §6.6)。null 时退化为「直接推荐、不存历史」的旧行为。
  • 打开时管线(start(forceFresh=false)):listAgents() → filterRecommendCandidates() → 若 draftId 有历史 → listRecommendHistory() 取最近一次直接展示(不重跑模型);否则 streamRecommend() 跑新推荐。
  • 阶段 UI(InternalState.phase):
    • idle → loading_agents → loading_history → history(展示上次推荐,可直接进入研讨或「重新分析」)
    • 或 loading_agents → recommending(顶部「模型生成内容」流式;推荐卡 streaming=true)→ ready / fallback / error
  • 推荐历史(会话级):
    • 打开时若该 draftId 有历史,进入 history 态:蓝色提示条标注上次推荐时间,picks 默认勾选、rationale 回显,不自动重跑模型。
    • 「重新分析」按钮(history 态显示该文案,其它态为「重新推荐」)→ start(true) 强制跑新推荐,跳过历史。
    • 每次新推荐完成(ready/fallback)后 saveRecommendHistory(draftId, {objective,status,model,rationale,picks,candidates}) best-effort 追加一条(失败仅告警不阻断)。
    • history/fallback/ready 三态都允许 onConfirm(用户可直接采纳历史 picks)。
  • 布局分两段:
    • 顶部「模型生成内容」:RecommenderOutput —— 状态行(spinner + 调度推荐 / 已连接 / 正在生成)+ MessageStepsCard 步骤卡 + ReasoningBlock(从 <think> 拆出来)+ markdown rationale + 末尾 StreamingCursor。视觉与 Step 1/Step 2 气泡的内容区一致,参考主聊天 chats/new。
    • 底部「推荐参会智能体」:候选卡片(AgentCard),分「推荐参会 / 其他候选」两组。
  • 选择约束:2 ≤ selected ≤ 8;底部实时显示「至少再选 N 个」「已选 X / 8」
  • 失败兜底:候选为空 → error;status==="asking" 或 picks 不足 2 → fallback,自动用 FALLBACK_AGENT_IDS 与候选池交集勾选
  • 反竞态:内部 generationRef,每次 start() 自增,所有 setState(含历史读取/流式回调)都校验 myGen === generationRef.current,避免「重新分析」时旧 stream / 旧历史写脏 state
  • onConfirm(agentIds) → 父组件 handleConfirmAgents:复位 step2InitRef 并 setCurrentStep(2),触发 Step 2 init

6.3 HighFidelityReport(Step 3)

  • 输入:threadIds、coordinatorName、consensusText、hasConsensus、seatDeliveries、seatNameMap、onReset
  • seatThreadEntries:过滤掉 coordinatorName,给每个 sub-thread 拉一次 listSessionArtifacts(Promise.all 并行)
  • reloadKey:「刷新文件清单」按钮自增,触发 effect 重新拉
  • 文件预览策略(shouldPreviewInModal):
    • 命中 → ArtifactPreviewModal(Markdown 用 react-markdown 渲染,其它当 plain text)
    • 不命中(pdf / png / html / 二进制)→ window.open(buildArtifactDownloadUrl),交给浏览器原生 viewer
    • 命中规则:
      • mimeType 是 text/markdown / text/plain / text/csv / application/json / text/*
      • 或文件名后缀属于 md / markdown / txt / json / csv / log / yml / yaml / xml
  • 各席位最终交付(中段折叠区)数据源:父组件传入的 seatDeliveries,按发送者去重(后到的覆盖前者)
  • 「重新起草」→ 父组件 handleStartNewTask(清空并 runStep1Init)
  • 「打印 / 另存 PDF」→ window.print(),浏览器原生流程
  • Step 3 不显示左侧栏(2026-06):LeftSidebar 在 currentStep === 3 时 return null(原「参与与结论贡献」列已移除),右侧栏在 Step !== 1 时本就 return null,因此中间 middle-viewport(flex-1)会自适应撑满整行,报告区获得最大宽度。

6.4 ArtifactPreviewModal

  • 通过 buildArtifactDownloadUrl(threadId, path)(不带 download=true)GET 文件正文
  • Markdown 文件(mime===text/markdown 或后缀 .md/.markdown)→ react-markdown + remark-gfm
  • 其它文本类 → <pre> 直出
  • 头部「下载」按钮单独走 download=true URL,触发浏览器附件下载
  • 关闭:Esc / 点击外层遮罩(精确匹配 e.target === e.currentTarget,避免内部冒泡误关)

6.5 共享 UI 模式与辅助

  • 三段式流式气泡:appendStreamingDialogue → appendToDialogue(仅追加增量)→ finalizeDialogue(设最终 content + confidence + streaming=false)。三函数都通过 id 匹配,避免数组下标错位
  • Phase 到中文文案:phaseLabel(phase, agentType, toolName):
    • connecting → 「调度中」
    • connected → 「已连接,等待响应」
    • tool_calling 按 toolName:agent_orchestration → 正在派活、ask_clarification → 正在提问、present_files → 正在交付文件,其它 → 「调用工具:」
    • tool_result → 「处理工具结果」
    • streaming → leader 时「正在派活说明」、special 时「正在输出」
    • Step 1 的 intentPhaseLabel:ask_clarification → 正在生成追问、streaming → 正在思考问题
  • Markdown 渲染:Step 1 / 2 / 3 都用 react-markdown + remark-gfm,自定义紧凑间距的 components(p 用 my-1 whitespace-pre-wrap 等)
  • 自动滚动:useAutoScroll(ref, [list]) 跟随;用户上滑后停止跟随;ScrollToBottomButton 一键回底
  • activeSpeakerSet:圆桌可视化里多席位同时高亮需要 Set,单 leader 时仍可用 activeSpeakerId;两者均触发 ring 动画
  • 状态条颜色:研讨中 红 / 在线 绿 / 已完成 绿勾 / 正在上线 黄旋 / 其它灰

7. 澄清交互(与主聊天对齐)

能力 实现位置
结构化 payload 类型 multi-agent.ts → ClarificationPayload(intent.ts 复用)
单选/多选 lib/clarification.ts → resolveAllowMultiple(缺省单选)
答案拼接 formatIntentClarificationAnswer(IntentClarificationCard)
Step 1 卡片 IntentClarificationCard + submitIntentClarification
Step 2 卡片 页面底部 pendingClarification 区块 + submitClarification → 再 runOrchestration
主聊天参考 src/components/workspace/messages/message-list.tsx → ClarificationCard

后端字段 allow_multiple 缺省为 false(单选、点击即发送)。

关键差异:

  • Step 1 用 IntentClarificationCard 组件(嵌在气泡里),Step 2 直接在页面底部渲染 amber 色带(没有抽象成独立组件,紧贴对话区)。
  • Step 2 澄清回复后不重建 thread;新 prompt 自带「用户已回复你刚才的澄清问题。问题:... 用户回答:...」上下文。

8. 与主聊天的差异

维度 主聊天 /page/workspace/chats/new 圆桌规划
SDK @langchain/langgraph-sdk 直连 自研 SSE 解析(apiFetch + ReadableStream)
澄清展示 message-list ToolMessage additional_kwargs 终态帧 status: "clarification"
Agent 用户可选 assistant 内置 intent + 动态 coordinator + 可选席位
线程 单 thread Step 1 一线程;Step 2 每席位 + 协调各一线程
终态语义 LangGraph state 自定义 {status, content, ...} 终态帧
暂停 LangGraph interrupt AbortController.abort()

复用思路:澄清卡片交互、答案文案格式;不要假设能直接复用 LangGraph React hooks。


9. 关键交互清单

交互 行为
Step 1 发送 Enter 发送(Shift+Enter 换行);发送中显示「暂停」
Step 1 暂停 stopIntentStream(),气泡尾部追加 (已暂停) 并 toast
Step 1 进入 Step 2 需 intentReady;打开推荐弹窗而非直接切步
Step 1 跳过此项 仅本地追加「系统」气泡,不发请求;适合演示
Step 2 init 首次进入自动 SSE init;离开 Step 2 不销毁 threadIds(可返回继续看,不会二次 init)
Step 2 挂起/恢复 isRoundTableRunning + stopOrchestration / 恢复时再 leader 一轮
Step 2 干预 底部输入框 → runOrchestration 新 seed,wrap 进「用户人工干预新指令:...」 prompt
Step 2 Persona 点圆桌头像打开抽屉;保存后下一轮该席位使用所选模型
新建任务 handleStartNewTask:abort 全部、清 state、runStep1Init,与 Step 3「重新起草」共用
历史草稿 顶栏下拉:加载 / 重命名 / 删除(删除带 window.confirm)
Step 3 预览 文本类 → 模态预览;其它 → 新窗口下载

10. 类型与常量速查

// Step 1 输出 → Step 2 输入
interface IntentSummary {
  objective: string;
  constraints: string[];
  assumptions?: string[];
}

// Step 2 线程映射(init_done.thread_ids)
type ThreadIdMap = Record<string, string>;

// 派活
type LeaderDispatch = [string, string]; // [agent_name, task]

// 当前选中的研讨席位
type SelectedAgent = { agent_id: string; name: string };

// Step 1 单条澄清问题
interface ClarificationPayload {
  question: string;
  clarificationType: string;
  context: string;
  options: unknown[];
  allowCustom: boolean;
  allowMultiple?: boolean;
}

// Step 2 stream 终态
interface StreamMultiAgentResult {
  dispatched: LeaderDispatch[];
  message: string | null;
  content: string;
  agentName: string | null;
  clarification: ClarificationPayload | null;
}

核心常量:

常量 值 位置
COMMANDER_SEAT_ID 1 RoundtablePlanningPage
MAX_CYCLES 8 RoundtablePlanningPage(runOrchestration 内)
MIN_SELECTED / MAX_SELECTED 2 / 8 RecommendAgentsDialog
FALLBACK_AGENT_IDS 6 内置 roundtable-* RecommendAgentsDialog
BUILTIN_AGENT_META 6 内置头像/中文名 RoundtablePlanningPage
FALLBACK_AVATAR_TYPES [blue, purple, emerald, amber, pink] RoundtablePlanningPage
STORAGE_KEY 已废弃 —— 草稿改存后端 /api/roundtable-drafts,不再用 localStorage (旧 utils/drafts.ts,已删除)
INTENT_READY_RE /\[INTENT_READY\]\s*```json\s*(\{[\s\S]*?\})\s*```/ RoundtablePlanningPage
RECOMMEND_READY_MARKER "[RECOMMEND_READY]" recommend.ts
EXCLUDED_AGENT_IDS / 前缀 roundtable-intent、roundtable-recommender、roundtable-coordinator-* recommend.ts

11. 故障排查

现象 可能原因 排查
Step 1 一直「等待澄清」但气泡里出现绿色 [INTENT_READY] 摘要 终态帧未带 summary 看 splitIntentReadyBlock + 同步 useEffect,必要时手动刷新
Step 1 角色面板卡在「正在上线」 StrictMode 双 mount 导致 init 异步被丢弃 已用 step1InitGenRef 修复;检查是否被改坏
选项全是多选 allow_multiple 未传或为 true Network 终态帧;resolveAllowMultiple
Step 2 init 卡住 init SSE error / 代理 Network init;控制台 [init-timing]
Step 2 stream 看不到任何文字 TitleMiddleware 过滤错杀 / phase 卡在 connecting 控制台搜 [stream: 前缀(需 __MULTI_AGENT_DEBUG__)
总控不派活 dispatched 一直 [] 且非 clarification 看 leader 终态帧;模型 SOUL 配置
推荐弹窗为空 filterRecommendCandidates 把所有候选都过滤了 GET /api/agents 列表;检查 EXCLUDED_AGENT_IDS
推荐弹窗一直「等待推荐结果」 recommender 模型死循环 控制台看 [stream: 日志,或点「重新推荐」
Step 3 各席位文件为空 子 agent 未写 outputs 或 thread id 错 对应 thread 调 listSessionArtifacts
Step 3 预览空白 文件不是文本类,但被错误命中 shouldPreviewInModal 检查 mime + 后缀;不在白名单的应该跳 window.open
Step 3 文件名乱码 / 404 buildArtifactDownloadUrl 编码错 看 path 是否被分段 encode;不能整体 encode(会把 / 也编了)
草稿加载后重复 init loadedFromDraftRef 未置位 handleLoadDraft 流程,注意 ref 必须先于 setCurrentStep
顶栏 Step 2 / Step 3 一直灰 intentReady / hasConsensus 没设上 看终态帧;leader.dispatched 是否真为 []
Persona 抽屉改模型不生效 下一轮才生效 / 抽屉 model 未传给 streamMultiAgent 看 agentConfigs[meta.speakerId]?.model 的传递路径
并行 SSE 卡顿 误改为 parallel 派活 保持顺序 dispatch,详见 §5.4 注释

本地调试开关:

window.__MULTI_AGENT_INIT_TIMING__ = true;  // init 各阶段耗时(默认开)
window.__MULTI_AGENT_DEBUG__       = true;  // stream 帧计数(默认开)

12. 扩展指南

12.1 新增默认研讨席位

  1. 后端 .deer-flow/agents/roundtable-xxx/
  2. BUILTIN_AGENT_META + DEFAULT_SELECTED_AGENTS + RecommendAgentsDialog 的 FALLBACK_AGENT_IDS
  3. 无需改 API 层(agent_names 由推荐结果传入)

12.2 调整 Step 1 澄清轮次上限

逻辑在 agent SOUL,不在前端;前端只需处理任意轮 asking / clarification / done。

12.3 调整 Step 2 编排上限

RoundtablePlanningPage.tsx 内 MAX_CYCLES = 8,超出会 toast「⏱️ 已达到最大研讨轮次,自动停止以避免循环」。若放宽:注意 step2RoundtableDialogues 数组会线性增长,超长讨论需要考虑虚拟滚动。

12.4 把 special 改回并行

不推荐;若一定要做:

  • 把 for ... of leaderRes.dispatched 改成 await Promise.all(leaderRes.dispatched.map(...))
  • 评估浏览器性能:每 token 一次 setStep2RoundtableDialogues(prev => prev.map(...)),N=6 时实测明显掉帧
  • 用更高效的派发结构(如按 id 分桶 Map<id, Bubble> + ref 缓存)改善之后再启用

12.5 接入真实 Persona 覆盖

当前 prompt / temp 只在前端记录;要落地需:

  • 后端开放 per-agent override 接口或在 streamMultiAgent 新增 prompt_override / temperature 字段
  • 前端:agentConfigs[meta.speakerId] 一并传入

12.6 拆分页面(已完成 · 2026-05)

主页面早期是 4 000+ 行单文件,2026-05 重构按"hooks 抽逻辑、components 抽 UI"原则拆分:

  • hooks/useStep1Intent.ts —— Step 1 全部状态机、init effect、INTENT_READY 同步
  • hooks/useStep2Orchestration.ts —— Step 2 状态、编排循环、对话流、Persona、澄清
  • hooks/useDraftPersistence.ts —— 草稿自动保存 / 手动保存 / 加载 / 重命名 / 删除
  • components/Step{1,2,3}Panel.tsx —— 纯 UI,只接收 props,不持有业务状态
  • components/PersonaDrawer.tsx —— Persona 抽屉
  • components/Avatars.tsx —— 所有 SVG 头像 + getAvatar
  • lib/roundtable-constants.ts —— 常量、类型、纯工具函数(buildRoleTemplates 等)
  • lib/step-display.tsx —— ChainOfThought 折叠卡的图标 + 文案 + 子节点渲染

主页面只保留:顶栏(StepNavigation / HistoryDropdown)、左右两侧栏(LeftSidebar / RightSidebar)、步骤路由、推荐弹窗,以及编排 hook 与跨步 handler(handleConfirmAgents / handleStartNewTask)。

继续重构请遵守:

  1. 不要在 Panel 组件里写 fetch / setState 业务流程 —— Panel 只渲染,事件透传到 hook。
  2. hook 之间不要互相 import —— 跨 hook 的数据通过主页面的 ref / callback 桥接(参考 buildInitialIntent 与 loadedFromDraftRef 的实现)。
  3. 草稿 snapshot 形状(Step 1 的 Step1Snapshot / Step 2 的 Step2Snapshot)与 api/drafts.ts 的 DraftStep{1,2}Snapshot 是同一份语义,改字段时两边都要改;后端以 JSON 整存,不强约束嵌套字段。
  4. lint/typecheck:重构后 pnpm typecheck 与 pnpm build 必须保持 0 个新增错误(预存量错误来自其它无关模块如 strategy-components / next/link shim,与本特性无关)。

12.7 文档协同

@/roundtable-planning/ 内一切公共 API 都应当:

  1. 在 api/*.ts 文件级注释里说明 endpoint
  2. 在本页第 4 节 / 第 10 节同步对照
  3. 后端字段变化(如 init_done 字段名)需要同步改 initMultiAgent 解析逻辑 + multi-agent-api.md

13. 路径速查

资源 路径
主页面(组合 + 顶栏 + 侧栏) frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx
Step 1 hook frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts
Step 2 hook(含编排循环) frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts
草稿 hook frontend-web/src/roundtable-planning/hooks/useDraftPersistence.ts
Step 1 面板 frontend-web/src/roundtable-planning/components/Step1Panel.tsx
Step 2 面板 frontend-web/src/roundtable-planning/components/Step2Panel.tsx
Step 3 面板 frontend-web/src/roundtable-planning/components/Step3Panel.tsx
消息气泡共享件 frontend-web/src/roundtable-planning/components/MessageBubble.tsx
步骤折叠卡(工具/思考) frontend-web/src/roundtable-planning/components/MessageStepsCard.tsx
<think> 提取工具 frontend-web/src/roundtable-planning/lib/reasoning.ts
Persona 抽屉 frontend-web/src/roundtable-planning/components/PersonaDrawer.tsx
头像集合 frontend-web/src/roundtable-planning/components/Avatars.tsx
常量/工具函数 frontend-web/src/roundtable-planning/lib/roundtable-constants.ts
ChainOfThought 渲染 frontend-web/src/roundtable-planning/lib/step-display.tsx
Step 1 API frontend-web/src/roundtable-planning/api/intent.ts
推荐 API(含推荐历史) frontend-web/src/roundtable-planning/api/recommend.ts
草稿 API(后端 CRUD) frontend-web/src/roundtable-planning/api/drafts.ts
Step 2/3 API frontend-web/src/roundtable-planning/api/multi-agent.ts
澄清卡片 frontend-web/src/roundtable-planning/components/IntentClarificationCard.tsx
推荐弹窗 frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx
Step 3 报告 frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx
文件预览 frontend-web/src/roundtable-planning/components/ArtifactPreviewModal.tsx
回底按钮 frontend-web/src/roundtable-planning/components/ScrollToBottomButton.tsx
滚动跟随 frontend-web/src/roundtable-planning/hooks/useAutoScroll.ts
澄清单选逻辑 frontend-web/src/roundtable-planning/lib/clarification.ts
后端草稿/历史路由 offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py
路由 frontend-web/src/pages/WorkspaceRoutes.tsx
样式 frontend-web/src/roundtable-planning/styles/roundtable-planning.css
主聊天澄清参考 frontend-web/src/components/workspace/messages/message-list.tsx
后端开发文档 frontend-web/docs/multi-agent-backend-dev.md
API 速查 frontend-web/docs/multi-agent-api.md