36 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. 速读:你需要先知道的事
- 整页只有一个文件:
RoundtablePlanningPage.tsx(≈ 3 621 行)。三步 UI 通过currentStep ∈ {1,2,3}切换,不使用 React Router 子路由。 - 不使用 LangGraph SDK:所有 API 都是自研的
apiFetch+ReadableStreamSSE 解析(见api/intent.ts、api/recommend.ts、api/multi-agent.ts)。 - 唯一默认模型:
DEFAULT_MODEL = "deepseek-chat"(页面常量,写在RoundtablePlanningPage.tsx)。useModels()只用于 Persona 抽屉的下拉选项。 - 席位区间硬约束:推荐弹窗要求 2 ≤ 选中数 ≤ 8(
MIN_SELECTED/MAX_SELECTED)。 - 总控编排循环上限:
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 # 三步主页面 + 状态机 + 编排循环(≈3600 行)
├── api/
│ ├── intent.ts # Step 1:initIntent、streamIntent
│ ├── recommend.ts # 推荐:streamRecommend、filterRecommendCandidates、parseRecommendStreamContent
│ └── multi-agent.ts # Step 2/3:initMultiAgent、streamMultiAgent、listSessionArtifacts、buildArtifactDownloadUrl
├── components/
│ ├── 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 # 滚动跟随 + 用户上滑暂停跟随
├── lib/
│ └── clarification.ts # resolveAllowMultiple(单选/多选解析),与主聊天 ClarificationCard 对齐
├── utils/
│ └── drafts.ts # localStorage 草稿存档(list / upsert / get / delete / rename / deriveName)
└── styles/
└── roundtable-planning.css # 页面主题变量(浅红品牌色 + 深色覆盖)
路由注册(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
StreamIntentResult:
| 字段 | 类型 | 说明 |
|---|---|---|
status |
"asking" | "clarification" | "done" | "error" |
终态分支 |
content |
string |
完整 AI 文本(含 [INTENT_READY] 块) |
summary |
IntentSummary | null |
仅 done 有;前端 fallback 也会从 content 里用 splitIntentReadyBlock 解 |
clarification |
ClarificationPayload | null |
仅 clarification 有 |
SSE 解析要点:
event: metadata→ phaseconnectedevent: messages+extractAiTextDelta→onTextDelta- tool call 帧(
tool_calls/tool_call_chunks非空)→ phasetool_calling(含ask_clarification工具名) - 终态帧识别:
isFinalIntentFrame(必须有status+content)
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)
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 |
是否折叠圆桌可视化 + 调控台,给对话区让出垂直空间 |
协调智能体命名:roundtable-coordinator-${Date.now().toString(36)},每次 Step 2 会话新建。后端把这类壳 agent 留在 DB 中,因此 filterRecommendCandidates 默认过滤掉。
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(),默认DEFAULT_MODEL
- 「参数覆盖并重启动算研讨」按钮 →
handleSaveAgentAndRecalculate:- 把新的
(seatId, model)写入agentConfigs - 追加一条「⚡ [模型偏好已保存] ...」对话气泡
- 不立即重启编排;下一轮该席位被派活时
runOrchestration内会读agentConfigs[meta.speakerId]?.model作为 special 调用的model参数
- 把新的
5.6 草稿持久化(utils/drafts.ts)
- Key:
roundtable-planning:drafts:v1(localStorage) - 草稿快照含:
step1.{intentThreadId, intentReady, interactiveMessages, timelineEntries}、step2.{threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit}、furthestStep deriveDraftName({intentReady, interactiveMessages}):优先用intentReady.objective,否则取第一个 user 消息,否则用未命名草稿 <时间>,统一截断到 40 字- 自动保存策略:仅在「前进」步骤切换时保存(1→2、2→3),后退不触发,避免覆盖
furthestStep - 手动保存:顶栏 + Step 1 底部「保存草稿」按钮均调用
handleManualSave - 加载草稿:
handleLoadDraft- 中断所有 in-flight(
abortAllInFlight + stopIntentStream) - 先置
loadedFromDraftRef.current = true,再setCurrentStep(draft.furthestStep),避免 step1/step2 init effect 重新调用 init 把恢复的状态打掉 step1InitGenRef自增,淘汰可能晚归的旧 initstep2InitRef.current = draft.furthestStep >= 2:曾经走到 Step 2 才标记已 init- 角色面板基线状态:被恢复时全部置「在线」,否则 init 流程逐个置「在线」
- 中断所有 in-flight(
6. 组件说明
6.1 IntentClarificationCard
- Props:
payload(ClarificationPayload)、selection、onSubmit(overrideSelection?)、disabled、submitting、fallbackContent - 单选(默认
allowMultiple === false):resolveAllowMultiple返回 false → 点击选项即onSubmit({selectedIds:[id], custom: ""}),UI 不显示「发送回答」按钮(除非allowCustom) - 多选:勾选多个 + 输入补充,再点「发送回答」;答案格式见
formatIntentClarificationAnswer:- 仅选项 →
我选择:A、B - 仅自定义 → 直接发自定义文本
- 同时存在 →
我选择:A、B;补充:…
- 仅选项 →
- 展示条件:
isPendingClarification && !intentReady && !parts.summary && pendingIntentClarificationMsgId === msg.id(已出现[INTENT_READY]则隐藏可操作卡片)
6.2 RecommendAgentsDialog
- 打开时管线:
listAgents() → filterRecommendCandidates() → streamRecommend() - 阶段 UI(
InternalState.phase):idle→loading_agents→recommending(顶部 rationale 流;下方推荐卡 streaming=true)→ready/fallback/error
- 选择约束:
2 ≤ selected ≤ 8;底部实时显示「至少再选 N 个」「已选 X / 8」 - 失败兜底:候选为空 → error;status==="asking" 或 picks 不足 2 →
fallback,自动用FALLBACK_AGENT_IDS与候选池交集勾选 - 反竞态:内部
generationRef,每次runRecommendation自增,所有 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(),浏览器原生流程
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;
}
核心常量:
| 常量 | 值 | 位置 |
|---|---|---|
DEFAULT_MODEL |
"deepseek-chat" |
RoundtablePlanningPage |
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 |
"roundtable-planning:drafts:v1" |
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 拆分页面
当前刻意单文件维护三步。若拆分建议:
useRoundtableStep1.ts/useRoundtableStep2.ts— 状态与副作用- 保持
api/*不变 - 保持
roundtable-planning.css主题变量
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 API | frontend-web/src/roundtable-planning/api/intent.ts |
| 推荐 API | frontend-web/src/roundtable-planning/api/recommend.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 |
| 草稿存档 | frontend-web/src/roundtable-planning/utils/drafts.ts |
| 路由 | 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 |