55 KiB
多智能体圆桌研讨 · 前端开发文档
本文档面向维护 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+ReadableStreamSSE 解析(见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同样的策略 —— 全局选中模型由selectedModelstate 维护,首次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_callingonClarification(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→ phaseconnectedevent: messages+extractAiTextDelta→onTextDeltaevent: messages+type:"tool"且name==="ask_clarification"→extractClarificationToolMessage解出完整澄清(options 已是{id,label}),按tool_call_id去重后onClarification实时回调并收进result.clarifications- tool call 帧(
tool_calls/tool_call_chunks非空)→ phasetool_calling(含ask_clarification工具名) - 终态帧识别:
isFinalIntentFrame(必须有status+content);status==="clarification"且未收到任何流式 tool 澄清帧时,才用终态帧字段兜底合成单条澄清
Abort 行为:
sendIntentMessage内建AbortController,保存到intentAborterRefstopIntentStream()用于「暂停」按钮、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-intentroundtable-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/streamMultiAgentleader /streamMultiAgentspecial)都传selectedModel || undefined;special 还会先看agentConfigs[seat]?.modelper-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(对接后端时一并解决):
- 自动保存覆盖手动重命名 —— 现在所有
updateDraft一律不带 title;标题只由 create(派生)与显式「重命名」改写。 - 快速连续前进(1→2→3)产生重复草稿 —— 所有保存经一条串行队列(
saveQueueRef)+ 同步镜像currentDraftIdRef,create 完成即写 id,后续保存读到 id 走 update 而非再 create。 - 加载读到内存旧列表快照 —— 改为
getDraft(id)实时拉。 - 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)。
- 逐题选择隔离:选择 Map 改为
- 展示条件:
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。
- 新增
draftIdprop(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
- mimeType 是
- 命中 →
- 各席位最终交付(中段折叠区)数据源:父组件传入的
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=trueURL,触发浏览器附件下载 - 关闭: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 新增默认研讨席位
- 后端
.deer-flow/agents/roundtable-xxx/ BUILTIN_AGENT_META+DEFAULT_SELECTED_AGENTS+RecommendAgentsDialog的FALLBACK_AGENT_IDS- 无需改 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 头像 + getAvatarlib/roundtable-constants.ts—— 常量、类型、纯工具函数(buildRoleTemplates 等)lib/step-display.tsx—— ChainOfThought 折叠卡的图标 + 文案 + 子节点渲染
主页面只保留:顶栏(StepNavigation / HistoryDropdown)、左右两侧栏(LeftSidebar / RightSidebar)、步骤路由、推荐弹窗,以及编排 hook 与跨步 handler(handleConfirmAgents / handleStartNewTask)。
继续重构请遵守:
- 不要在 Panel 组件里写 fetch / setState 业务流程 —— Panel 只渲染,事件透传到 hook。
- hook 之间不要互相 import —— 跨 hook 的数据通过主页面的 ref / callback 桥接(参考
buildInitialIntent与loadedFromDraftRef的实现)。 - 草稿 snapshot 形状(Step 1 的
Step1Snapshot/ Step 2 的Step2Snapshot)与api/drafts.ts的DraftStep{1,2}Snapshot是同一份语义,改字段时两边都要改;后端以 JSON 整存,不强约束嵌套字段。 - lint/typecheck:重构后
pnpm typecheck与pnpm build必须保持 0 个新增错误(预存量错误来自其它无关模块如strategy-components/next/linkshim,与本特性无关)。
12.7 文档协同
@/roundtable-planning/ 内一切公共 API 都应当:
- 在
api/*.ts文件级注释里说明 endpoint - 在本页第 4 节 / 第 10 节同步对照
- 后端字段变化(如
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 |