# 多智能体圆桌研讨 · 前端开发文档 本文档面向维护 `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` 字段对齐。 - **`` 标签自动提取为「思考过程」**:`lib/reasoning.ts::splitInlineReasoning(text)` 把 `...`(含未闭合的流式中段)抽离成 `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:从模型流式文本里提取 `...` 思考过程 │ ├── 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`): ```tsx } /> ``` 侧栏入口:`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` 字段): ```ts { 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 { 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。 **调试开关**: ```js 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::] ``` > ⚠️ 文档历史版本曾写作 `__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`,多选 / 多消息时各自维护 | | `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` 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-` 形式。只要那个旧 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("")` - **初始化**:`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**:当前用原生 `