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

401 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 业务链条模式 · 需求与分步实现文档
> 面向「智能体推荐弹窗」的扩展:在现有**智能推荐**之外,新增**业务链条选择**与**业务链条配置**两块能力;选定业务链条后,Step 2 圆桌研讨**按链条内的固定顺序**逐个发言(而非现在的总控派活)。
>
> 配套阅读:
> - 前端总览:`multi-agent-frontend-dev.md`(推荐弹窗 §6.2、Step 2 编排 §5.4、草稿持久化 §5.7)
> - 后端总览:`multi-agent-backend-dev.md`(Step 2 §6、草稿/历史持久化 §7A)
>
> 本文档约定:**「席位 / 智能体」**指一个 agent;**「业务链条 / chain」**指一组有序排列的 agent 配置,是可命名、可复用、可持久化的实体。
---
## 0. 需求原文拆解
用户原始诉求逐条拆成可实现条目:
| # | 原始诉求 | 拆解 |
|---|----------|------|
| 1 | 推荐弹窗要有「模式选择」 | 弹窗顶部加模式切换(智能推荐 / 业务链条),两种模式互斥,确认后都产出**有序**的 `SelectedAgent[]` |
| 2 | 「智能推荐」就是现在这个 | 现有 `RecommendAgentsDialog` 整体收进「智能推荐」模式,行为不变 |
| 3 | 「业务链条选择」是卡片列表 | 卡片 = 标题 + 一排智能体名 tag;tag 不同颜色;tag 之间有**向左箭头**连接,表达链条次序 |
| 4 | 业务链条需要一个**配置页面** | 左侧智能体列表(复用 `GET /api/agents`),右侧有序长条卡片(名称 + 描述);支持右侧内部拖拽排序、左→右拖入指定位置 |
| 5 | 配置页用弹窗还是新页面? | **本文档决策:独立路由页面**(理由见 §2.1) |
| 6 | 选定链条后,Step 2 用链条内智能体研讨 | 复用既有 Step 2 init/stream;**总控(leader)按链条顺序逐个派活**(§6) |
| 7 | 规划成分步可实现的需求文档 | 见 §7 分阶段实现 |
---
## 1. 目标与非目标
### 1.1 目标
- 推荐弹窗成为「**席位来源选择器**」:无论智能推荐还是业务链条,最终都向 `onConfirm` 交付一个**有序的 `{ agent_id, name }[]`** + 一个 `orchestrationMode`。
- 业务链条是**用户可创建、命名、保存、复用、删除**的有序智能体编排模板。
- Step 2 支持两种编排:
- `recommend` 模式 → 现有总控**自由派活**循环(不动)。
- `chain` 模式 → 总控仍是派活方,但**被约束按链条顺序逐个派活**(新增)。
### 1.2 非目标(本期不做,留扩展点)
- 链条内每个席位的 per-seat prompt/temperature 真实覆盖(仍仅前端记录,沿用现状)。
- 链条的分支 / 条件流转(只做线性顺序链)。
- 链条跨用户共享 / 市场(先做「本人私有链条」)。
- 链条版本管理 / 协作编辑。
---
## 2. 关键设计决策
### 2.1 决策 A:配置页面用「独立路由页面」而非弹窗 ✅
**结论:业务链条的「编辑/管理」用独立路由页面 `/page/workspace/roundtable/chains`;业务链条的「选择」仍内嵌在推荐弹窗里(卡片列表)。**
理由:
| 维度 | 弹窗 | 独立页面 ✅ |
|------|------|------------|
| 双栏 + 拖拽 + 长条卡片 | 弹窗宽度(`sm:max-w-3xl`)+ `max-h-[85vh]` 拖拽空间局促,左右栏挤 | 整屏宽度,拖拽手感好 |
| 复用入口 | 只能从推荐弹窗进 | 可从侧栏、推荐弹窗「管理链条」按钮、深链接进入 |
| 可深链接/刷新 | 不可 | `/roundtable/chains` 可直达、可分享 |
| 推荐弹窗内嵌编辑器 | 弹窗套弹窗 / 模式套编辑,状态复杂 | 关注点分离:弹窗只「选」,页面只「编」 |
**分工**:
- **推荐弹窗**:模式选择 + 业务链条**选择**(只读卡片列表,可点「去配置」跳到页面)。
- **配置页面**:业务链条的**增删改**(双栏拖拽编辑器 + 链条列表管理)。
> 备选:若产品坚持不加路由,可改为**全屏 Dialog(`max-w-[90vw] h-[90vh]`)**承载编辑器,从推荐弹窗内打开。结构与页面版一致,仅外壳不同。本文档按「独立页面」实现,§7 标注了切换成本。
### 2.2 决策 B:链条持久化 —— 后端 MySQL(确定) ✅
**结论:业务链条存后端 MySQL,仿 `roundtable_drafts`(§7A)新建 `roundtable_chains` 表 + `/api/roundtable-chains` 路由,按登录用户隔离、跨设备保留。** 不走 localStorage。
理由:链条是可复用、需跨设备/会话保留的模板,与草稿/推荐历史性质一致;草稿当年正是因 localStorage 换设备即丢才迁后端(§7A),链条不重复踩坑。
落地:
- 表/路由设计见 §5.2,是「复制 §7A 草稿持久化、把字段换成 chain」的机械工作。
- 前端 `api/chains.ts` 直接打后端接口(snake_case↔camelCase 在这层收口,仿 `api/drafts.ts`),**不提供 localStorage 实现**。
- 后端表必须先于前端 UI 落地(§7 阶段 0 即包含后端表/路由)。
### 2.3 决策 C:拖拽库选型 —— `@dnd-kit`
仓库**当前无任何拖拽库**(`package.json` 无 `@dnd-kit` / `react-dnd` / `sortablejs`;`@xyflow/react` 是节点图,不适合列表排序)。
| 方案 | 取舍 |
|------|------|
| **`@dnd-kit/core` + `@dnd-kit/sortable`**(推荐) | React 19 友好、可访问性好、跨容器拖入 + 列表排序 + 插入指示器开箱即用;新增 ~30KB gzip 依赖 |
| 原生 HTML5 DnD(零依赖兜底) | 不加依赖,但跨容器插入位置指示、键盘可达性都要手写,易出 bug |
**建议**:新增 `@dnd-kit/core`、`@dnd-kit/sortable`、`@dnd-kit/utilities`。若产品方禁止新增依赖,退回原生 HTML5 DnD(§7 阶段 2 标注差异)。
### 2.4 决策 D:Step 2「总控按链条顺序派活」,后端零改动 ✅
**形态:总控(leader)仍是唯一派活方,但每一轮被前端约束「只能派给链条里的下一个席位」,逐个走完整条链,最后由总控收口。** 区别于 recommend 模式的「总控自由决定派给谁」。
为什么后端无需改动:
- Step 2 后端 `streamMultiAgent({ agentType: "leader" })` 每轮返回 `dispatched = [[agent, task], ...]`;`agentType: "special"` 单独驱动某席位跑一轮。这两个原语已足够。
- 每个 special 跑完,后端 `_broadcast` 会把「子智能体 X 完成…交付内容为:…」append 进**其它所有席位 + 总控 thread** 的历史(后端 §6.4),所以链路上下文天然累积。
- chain 模式只是**改变前端怎么提示 leader、以及怎么校验它的派活**——把每轮 leader 的提示词约束成「请向链条第 i 位【X】派活」,并对返回的 `dispatched` 做「必须命中链条下一席位」的校验/纠偏。**后端路由、SOUL、表结构都不动。**
→ chain 模式 = 在 `useStep2Orchestration` 里新增 `runChainOrchestration`:按链条游标逐位调 leader(约束派给该位)→ 拿到 task → 调 special → 下一位 → 链尾后调一次 leader 收口。详见 §6。
> 与「前端绕过 leader 直接顺序 special」的区别:本方案**保留总控在每一步的协调/串接职责**(leader 结合前序进展为下一席位编写针对性任务、并在派活说明里承上启下),更符合「总控按链条顺序派活」的语义,圆桌可视化里总控气泡也照常出现。
---
## 3. 数据模型与类型
新增前端类型,建议放 `frontend-web/src/roundtable-planning/lib/business-chain.ts`:
```ts
/** 业务链条里的一个有序席位。 */
export interface ChainSeat {
agent_id: string;
/** 落库时的快照名/描述,避免 agent 改名后链条显示错乱;展示优先用实时 agent 数据,回退到快照。 */
name: string;
description?: string;
}
/** 一条可复用的业务链条。 */
export interface BusinessChain {
id: string;
title: string;
description?: string;
/** 有序席位列表;seats[0] 是第一个发言者。 */
seats: ChainSeat[];
createdAt?: string;
updatedAt?: string;
}
/** 推荐弹窗确认后回传给主页面的统一结果。 */
export interface SeatSelectionResult {
/** 有序席位(与现有 handleConfirmAgents 入参兼容)。 */
agents: Array<{ agent_id: string; name: string }>;
/** 决定 Step 2 走哪种编排。 */
mode: "recommend" | "chain";
/** chain 模式下带上来源链条(用于草稿快照 / Step 2 展示链名)。 */
chain?: Pick<BusinessChain, "id" | "title">;
}
```
**约束**:链条席位数沿用圆桌的 `2 ≤ N ≤ 8`(`MIN_SELECTED`/`MAX_SELECTED`),配置页保存时校验。
**tag 配色**:复用 `roundtable-constants.ts::getAvatarTypeFor(agentId, index)`,得到 `blue|purple|emerald|amber|pink|human`,再映射到已有的 avatar 配色 token(与圆桌可视化、Step 2 气泡同一套色板,视觉统一)。
---
## 4. 现有代码改动点速查
| 位置 | 改动 | 说明 |
|------|------|------|
| `components/RecommendAgentsDialog.tsx` | 包一层「模式选择」外壳,现有内容收进「智能推荐」Tab | `onConfirm` 签名从 `agents[]` 升级为 `SeatSelectionResult`(或加可选第二参 `mode`,见 §7 阶段 3 的兼容策略) |
| `pages/RoundtablePlanningPage.tsx` | `handleConfirmAgents` 接收 `mode`,存到新状态 `orchestrationMode` | 透传给 `useStep2Orchestration` |
| `hooks/useStep2Orchestration.ts` | 新增 `runChainOrchestration` + `orchestrationMode` 分支 | init effect 里按 mode 选择 `runOrchestration` 或 `runChainOrchestration` |
| `lib/roundtable-constants.ts` | 复用 `getAvatarTypeFor` / `SelectedAgent` | 不改,仅引用 |
| `hooks/useDraftPersistence.ts` + `api/drafts.ts` | Step2 快照新增 `orchestrationMode` + `chain` 字段 | 草稿能记住「这次是链条模式」 |
| `pages/WorkspaceRoutes.tsx` | 注册 `roundtable/chains` 路由 | 仿 `roundtable/planning` |
| `components/page-sidebar.tsx`(如有侧栏入口) | 加「业务链条」入口(可选) | 也可只从推荐弹窗进 |
| `api/chains.ts`(新增) | 链条 CRUD 客户端 | 直接打后端 `/api/roundtable-chains` |
| 后端 `roundtable_chains` | 表 + 路由(MySQL) | 仿 `roundtable_drafts`,**必做** |
---
## 5. 链条持久化 API 设计
### 5.1 前端客户端 `api/chains.ts`(直接打后端)
```ts
listChains(): Promise<BusinessChain[]> // 当前用户全部链条
getChain(id: string): Promise<BusinessChain>
createChain(input: Omit<BusinessChain,"id"|"createdAt"|"updatedAt">): Promise<BusinessChain>
updateChain(id: string, patch: Partial<BusinessChain>): Promise<BusinessChain>
deleteChain(id: string): Promise<void>
```
- 实现用 `apiFetch` 打 `/api/roundtable-chains`(§5.2),snake_case↔camelCase 在这层收口(仿 `api/drafts.ts`)。
- 不提供 localStorage 实现;后端表是前端 UI 的硬依赖。
### 5.2 后端表 + 路由(仿 §7A `roundtable_drafts`)
- 表 `roundtable_chains`:`id`(PK) / `user_id`(idx) / `title`(512) / `description`(text) / `seats`(`PortableLongText`,存 `ChainSeat[]` JSON) / `created_at` / `updated_at`(`BeijingDateTime`)。
- 路由 `app/gateway/routers/roundtable_chains.py`,前缀 `/api/roundtable-chains`:`GET ``(列表)/ `POST ``(建)/ `GET /{id}` / `PUT /{id}`(partial,`exclude_unset`)/ `DELETE /{id}`。
- 鉴权/属主:`_current_user_id(request)`,跨用户访问一律 404,与 `roundtable_drafts` 完全一致。
- 启动 `Base.metadata.create_all` 自动建表(MySQL 生产 / sqlite 本地走同一套 SQLAlchemy 代码,`PortableLongText` 各自降级),登记进 `persistence/models/__init__.py`,store 接到 `app.state` 并 `include_router`。
> 整体是「复制 §7A 的草稿持久化、把字段换成 chain」的机械工作,风险低。
---
## 6. Step 2 链条模式编排设计(`runChainOrchestration`)
### 6.1 与现有 `runOrchestration` 的差异
| 维度 | `runOrchestration`(recommend) | `runChainOrchestration`(chain,新增) |
|------|--------------------------------|----------------------------------------|
| 谁派活 | 总控 leader | 总控 leader(**不变**) |
| 派给谁由谁定 | leader 自由决定 | 前端按链条游标**约束 leader 只能派给下一席位** |
| 发言顺序 | leader 每轮自由派 | **链条数组的固定顺序** |
| 循环结构 | `while` 直到 leader 返回 `[]` 共识 / MAX_CYCLES | 按链条游标走一趟(每位:leader 派活 → special 交付) |
| 收口 | leader 返回 `[]` 即共识 | 链尾后再调一次 leader 做总结收口,产出 `lastLeaderContent` + `hasConsensus` |
> **核心**:leader 始终在场,只是**每轮的派活目标被前端钉死成链条的下一席位**——前端在 leader 的 `newMessage` 里写明「本轮请通过 `agent_orchestration` 仅向【X】派活」,并对 `leaderRes.dispatched` 做命中校验/纠偏。
### 6.2 伪代码
```
runChainOrchestration(seedMessage, ids, coord, chainSeats):
isOrchestrating = true
cancel = false
N = chainSeats.length
for (let i = 0; i < N; i++): // 链条游标,逐位推进
if cancel: break
seat = chainSeats[i]
seatMeta = agentNameToRole[seat.agent_id]
// ── (a) 总控派活给「链条下一席位」──────────────────────────
highlightSpeaker(null) // leader 阶段不高亮任何 sub
leaderBubble = appendStreamingDialogue("总控协调", confidence:"链条派活中")
leaderTask = buildChainLeaderTask({
seedMessage, // 任务目标/约束(首轮)
position: i, total: N,
nextSeat: seat, // 「本轮请仅向【X(seat.agent_id)】派活」
isFirst: i === 0,
})
leaderRes = await streamMultiAgent(leader, ids, coord, leaderTask, model=globalModel)
// 约束/纠偏:取 dispatched 中命中 seat.agent_id 的那条 task;
// 若 leader 没派或派错(派给非 seat、或派给多个),前端兜底:
// - 命中就用它给出的 task 文本(保留总控的承上启下措辞);
// - 没命中则用 leader 的正文 content 作为兜底 task,或回退到模板 task。
task = pickChainTask(leaderRes, seat)
// leader 若返回 clarification → 暂停链条,走与现状一致的澄清条(§6.3)
finalizeDialogue(leaderBubble, leaderRes.content, "派活说明")
// ── (b) 被派席位交付(special,顺序、不并行)──────────────────
markSpeakerStreaming(seatMeta.speakerId)
seatBubble = appendStreamingDialogue(seatMeta.displayName, confidence:"调度中")
perSeatModel = agentConfigs[seatMeta.speakerId]?.model || globalModel
specRes = await streamMultiAgent(special, ids, seat.agent_id, task, model=perSeatModel)
finalizeDialogue(seatBubble, specRes.content, "子智能体交付")
markSpeakerIdle(seatMeta.speakerId)
// 后端 _broadcast 已把本席位交付注入其余 thread 历史 → 下一席位天然可见
if cancel: return
// ── (c) 总控收口:综合全链交付,产出最终结论 ──────────────────
leaderBubble = appendStreamingDialogue("总控协调", confidence:"汇总链条结论")
leaderRes = await streamMultiAgent(leader, ids, coord,
"各链条席位已按顺序完成发言,请综合所有交付,输出最终方案,不要再派活。")
finalizeDialogue(leaderBubble, leaderRes.content, "全案盖印")
setLastLeaderContent(leaderRes.content)
setHasConsensus(true); setConsensusPercentage(100)
isOrchestrating = false
```
要点:
- **派活方仍是总控**,符合「总控按链条顺序派活」的语义;圆桌可视化里每位席位前都先出现一个总控派活气泡,再出现该席位交付气泡。
- **约束手段是 prompt + 前端校验,不动后端**:`buildChainLeaderTask` 把「本轮只能派给【X】」写进 leader 的 `newMessage`;`pickChainTask` 对 `leaderRes.dispatched` 做命中校验,派错/未派时兜底,保证链条游标严格推进。
- **复用现有三段式气泡 / 高亮 / abort 基建**(`appendStreamingDialogue` / `finalizeDialogue` / `markSpeakerStreaming` / `activeAbortersRef` / `orchestrationCancelRef`),只是驱动顺序不同。
- **special 仍是顺序**(不并行),与现状不变量一致(文件顶部注释 §1)。
- **收口 leader 调用默认开**(产出 Step 3 结论);若链条末位本身是「总结陈述」类,可关掉收口,直接拿末位交付当 `lastLeaderContent`。配置位 `chainNeedsLeaderSummary`(默认 true)。
### 6.3 澄清 / 挂起 / 恢复 / 干预
- **澄清**:链条中途某轮 leader 返回 `clarification` → 与现状一致,暂停链条、底部弹澄清条;用户回答后从**当前游标**继续(需记录 `chainCursor`)。
- **挂起/恢复**:`isRoundTableRunning` toggle 复用现有机制;恢复时从 `chainCursor` 续跑。MVP 若不做断点续跑,可先只支持「整体停止 / 从头重跑」,UI 文案说明(扩展点,§7 阶段 4 标注)。
- **干预**:可在两席位之间插入一条用户指令,由下一轮 leader 派活前消化。MVP 同上可后置。
### 6.4 多轮链条(扩展点,非 MVP)
单趟遍历是 MVP。若要「链条跑完后总控判断是否再来一轮」,可在收口 leader 处复用 `dispatched===[]?` 判定:收口 leader 若仍派活则回到链条头再跑一轮,直到共识或 MAX_CYCLES。MVP 不做,避免复杂度。
### 6.5 init 复用
chain 模式的 Step 2 init **完全复用** `initMultiAgent`:`agentNames = chainSeats.map(s => s.agent_id)`(**按链条顺序**传入,使 `buildRoleTemplates` 的 seat id 顺序 = 链条顺序,且后端注入 coordinator 的「可调度席位清单」也按此序)。init effect 里按 `orchestrationMode` 决定 init 完成后调 `runOrchestration` 还是 `runChainOrchestration`。
---
## 7. 分阶段实现计划
> 每阶段都可独立交付、独立验收。建议顺序:**数据层 → 配置页 → 弹窗模式 → 链条选择 → Step2 编排 → 持久化兼容与收尾**。
### 阶段 0 · 后端链条表 + 脚手架(1.5 天) ✅ 已完成
**目标**:打地基(含后端持久化),不含前端 UI。
- [x] **后端**:仿 §7A `roundtable_drafts` 新建 `roundtable_chains` 表 + `/api/roundtable-chains` 路由(§5.2):
- [x] `persistence/roundtable_chains/`(model + sql repo + `make_roundtable_chain_store`),登记进 `persistence/models/__init__.py`。
- [x] `app/gateway/routers/roundtable_chains.py`(GET 列表 / POST / GET{id} / PUT{id} / DELETE),鉴权属主同 `roundtable_drafts`;席位 2–8 + 去重校验。
- [x] `deps.py` 接 `app.state.roundtable_chain_store`,`app.py` `include_router`。
- [x] 后端测试 `tests/test_roundtable_chains.py`:CRUD + 部分更新 + 跨用户隔离(5 用例全过)。
- [x] **前端**:新增 `lib/business-chain.ts`(`ChainSeat` / `BusinessChain` / `SeatSelectionResult`,§3)+ `api/chains.ts`(直接打后端,§5.1)。
- [x] (决策 C)安装 `@dnd-kit/core` `@dnd-kit/sortable` `@dnd-kit/utilities`。
- **验收**:✅ 后端 5 用例通过;✅ 表注册进 `Base.metadata`、路由 `/api/roundtable-chains` 加载正常;✅ ruff 0 错;✅ 前端 typecheck 新增文件 0 错。
### 阶段 1 · 业务链条配置页面(2–3 天) ✅ 已完成
**目标**:能创建/编辑/保存/删除链条。
- [x] 路由:`WorkspaceRoutes.tsx` 注册 `roundtable/chains`(套 `WorkspaceLayout`;本页**不需要** `ChatRuntime`,无沙箱依赖)。
- [x] 页面 `roundtable-planning/pages/BusinessChainEditorPage.tsx`:
- [x] 顶部:链条列表下拉(`<select>` 列出已存链条 + 新建项)/ 新建按钮 / 标题+描述输入 / 保存 / 删除(`window.confirm`)。
- [x] **左栏**:候选智能体(`listAgents()` + `filterRecommendCandidates`),竖排卡片,可拖、可点「+」快捷加入;已加入的置灰「已加入」。
- [x] **右栏**:有序长条卡片(序号 tag + 名称 + 描述 + 移除),`SortableContext` 内拖拽排序;空态有占位提示。
- [x] 拖拽用 `@dnd-kit`:左栏 `useDraggable`,右栏 `useDroppable` + `SortableContext`/`useSortable`,`onDragEnd` 计算插入 index(pool→右栏按落点插入;seat→seat `arrayMove`);`DragOverlay` 跟手卡片。
- [x] 校验:`2 ≤ seats ≤ 8`(前端禁用保存 + toast,后端 `_validate_seats` 兜底),重复 agent 去重提示。
- [x] 保存走 `api/chains.ts`(create 用 `nanoid()` 客户端 id,update 走同一 id)。
- **验收**:✅ typecheck 0 新增错误(76 个预存量错误均在无关模块)。**人工验收待跑**:新建「情报→方案→风险→总结」链保存 → 刷新后下拉重载 → 右栏拖拽排序 → 左栏拖入指定位 → 2–8 边界禁用保存。
- **切换成本备注**:若改全屏 Dialog 方案,编辑器组件原样复用,仅去掉路由、外面套 `Dialog`。
- **入口说明**:圆桌本身未在侧栏(`sidebar-menu.ts` 入口被注释),本页暂经直链 `/page/workspace/roundtable/chains` 或顶栏「返回圆桌」往返;阶段 3 推荐弹窗「去配置」按钮会导航到此。
- **视觉(后续重画)**:本页**不**套圆桌 `roundtable-planning` scope,直接遵循《浅红色/深蓝色 UI 规范》——用 app 语义 token(光 `--primary` 红 hue27 / 暗 蓝 hue259,已内置自适应)+ 规范小圆角(输入/按钮/tag/卡片内项 2px = `R_CTRL`,分栏/面板 4px = `R_CARD`)+ 克制阴影(`PANEL_SHADOW`)。筛选框复用通用 `TagSearchBar`,配色随 app 中性 token,不再被红 scope 染色。
### 阶段 2 · 推荐弹窗「模式选择」外壳(1 天) ✅ 已完成
**目标**:弹窗顶部模式切换,智能推荐行为不变。
- [x] `RecommendAgentsDialog` 顶部加**分段按钮**模式切换:`智能推荐 | 业务链条`(active 态品牌色 + 卡底;DialogDescription 随模式切换文案)。
- [x] 现有全部内容原样收进「智能推荐」面板(`{mode === "recommend" && (<>…</>)}` 包裹,**零行为变更**)。
- [x] 「业务链条」面板放占位:居中提示 + 「去配置业务链条」按钮(`handleGoConfig` → 关弹窗 + 跳 `roundtable/chains`)。
- [x] `onConfirm` 统一升级为回传 `SeatSelectionResult`(含 `mode`):智能推荐确认回传 `{ agents, mode: "recommend" }`;主页面 `handleConfirmAgents` 同步改签名取 `result.agents`(`result.mode` 预留给阶段 4)。
- **验收**:✅ typecheck 0 新增错误(总数仍 76);智能推荐分支 JSX/逻辑原样保留(流式/勾选/进入研讨未改);模式切换只切显示、`state`/`selectedIds` 不卸载 → 候选池不丢。**人工验收待跑**:切到业务链条→再切回,推荐结果与勾选仍在。
### 阶段 3 · 业务链条选择卡片列表(1.5 天) ✅ 已完成
**目标**:弹窗「业务链条」面板里选链条。新增 `components/BusinessChainPicker.tsx`,替换阶段 2 的占位。
- [x] 进入该 Tab 时 `listChains()` 拉本人链条 + `listAgents()` 取存活 id/名称,渲染**卡片列表**:
- [x] 卡片标题(链条 title)+ 「N 席」徽章 + 选中 ✓;描述行。
- [x] 标题下一排**智能体名 tag**:按链条顺序**从左到右**、不同颜色(`getAvatarTypeFor`→`chainTagClasses`),tag 之间插 `→`(`ArrowRightIcon`)箭头连接(`seats[0]` 最左、先发言)。
- [x] 底部「配置链条」按钮(跳 `roundtable/chains`)。
- [x] 空态:无链条时引导「去配置页面创建第一条业务链条」+ 按钮。
- [x] 选中一条 → 高亮(`ring-primary`);底部「进入研讨」`onConfirm({ agents: 存活席位有序, mode:"chain", chain:{id,title} })`。
- [x] 链条里有 agent 已不存在(被删)→ 该 tag 置灰删除线 + `title` 提示 + 卡片下方 amber 警告;confirm 按**存活席位**过滤;存活 < `CHAIN_MIN_SEATS` 则禁用「进入研讨」并提示。agent 列表加载失败时 `existingIds=null` → 不误判删除。
- **验收**:✅ typecheck 0 新增错误(总数仍 76)。卡片渲染标题 + 彩色 tag + ← 箭头;选链 → `onConfirm(mode:"chain")` → `handleConfirmAgents` 按链条顺序 `setSelectedAgents` → Step 2 左侧「参与角色」顺序 = 链条顺序(**按顺序派活在阶段 4**)。**人工 UI 验收待跑**。
- **箭头方向**(已确认):tag 左→右排列、`→`(`ArrowRightIcon`)连接,`seats[0]` 最左、先发言,箭头顺读即链条研讨顺序。
### 阶段 4 · Step 2 链条顺序编排(2–3 天) ✅ 已完成
**目标**:chain 模式按链顺序逐席发言。**后端零改动。**
- [x] 主页面新增 `orchestrationMode` 状态,`handleConfirmAgents` 从 `SeatSelectionResult.mode` 写入(chain 模式 toast 带链名);`handleStartNewTask` 复位为 `recommend`;透传给 `useStep2Orchestration`。chain 顺序 = `selectedAgents`(确认时已按链条顺序写入),无需单独 `chainSeats`。
- [x] `useStep2Orchestration` 新增 `runChainOrchestration`(§6.2)+ `orchestrationModeRef`/`selectedAgentsRef`;复用现有三段式气泡/高亮/abort 基建。
- [x] 新增 `runActiveOrchestration` 分发器:init / 恢复 / 干预 / 澄清回复 4 处调用点统一改走它,按 `orchestrationModeRef` 选 `runOrchestration`(recommend)或 `runChainOrchestration`(chain)。
- [x] `runChainOrchestration`:按 `selectedAgents` 游标,每位先 leader 约束派活(`buildChainLeaderTask` 钉死目标席位 + `pickChainTask` 命中校验/纠偏,派错/未派都兜底推进)→ 再对**该席位**跑 special;中途 leader 若 `clarification` 则暂停(MVP 回复后整链重跑)。
- [x] `initMultiAgent` 的 `agentNames` 即 `roundtableAgentIds`(由 `selectedAgents` 派生,已是链条顺序,§6.5)。
- [x] 收口 leader(`finishedAllSeats` 时触发),产出 `lastLeaderContent` + `hasConsensus=true` + `consensusPercentage=100` 解锁 Step 3。
- **验收**:✅ typecheck 0 新增错误(总数仍 76);✅ 唯一直接引用 `runOrchestration` 的是 dispatcher。逻辑层:每席「总控派活气泡 → 席位交付气泡」、严格顺序、单 special 在跑、`_broadcast` 注入前序上下文、leader 纠偏推进、跑完解锁 Step 3、abort 可中断。**人工 UI 验收待跑**。
- **MVP 边界**:chain 模式的「挂起→断点续跑」「逐席间干预」未做断点续跑,`clarification`/恢复/干预当前为**整链重跑**(`chainCursor` 列为扩展点)。
### 阶段 5 · 草稿持久化兼容 + 收尾(1 天) ✅ 已完成
**目标**:草稿记住模式,回归不破。
- [x] `Step2Snapshot`(`useStep2Orchestration.ts`)+ `DraftStep2Snapshot`(`api/drafts.ts`)新增 `orchestrationMode?: "recommend"|"chain"` 与 `chain?: {id,title}|null`。
- [x] 恢复路径:`orchestrationMode`/`chainMeta` 是**主页面 state**,`getStep2Snapshot` 写入;**包裹版 `hydrateStep2`** 在加载时 `setOrchestrationMode(snapshot.orchestrationMode ?? "recommend")` + `setChainMeta(snapshot.chain ?? null)` 再调 `step2.hydrateFromDraft`;老草稿无字段 → 默认 `recommend`(向后兼容)。`handleStartNewTask` 复位两者。
- [x] 文档回写:`multi-agent-frontend-dev.md` §5.7(快照形状 + `orchestrationMode/chain` 说明)、§6.2(席位来源选择器导语);`multi-agent-backend-dev.md` 新增 §7B(`roundtable_chains` 表/路由)。
- **验收**:✅ `pnpm typecheck` 0 新增错误(总数仍 76)。逻辑:chain 会话保存草稿 → 重载 `orchestrationMode="chain"` + 链信息恢复;老草稿(无字段)默认 recommend 照常加载。**人工 UI 验收 + `pnpm build` 待跑**。
### 工期粗估
| 阶段 | 估时 |
|------|------|
| 0 后端链条表 + 脚手架 | 1.5d |
| 1 配置页 | 2–3d |
| 2 弹窗模式壳 | 1d |
| 3 链条选择卡片 | 1.5d |
| 4 Step2 链编排 | 2–3d |
| 5 持久化兼容收尾 | 1d |
| **合计** | **约 9–11 人日**(含后端 `roundtable_chains` 表/路由) |
---
## 8. 验收清单(端到端)
1. 推荐弹窗顶部有「智能推荐 / 业务链条」切换;智能推荐与现状逐像素一致。
2. 配置页 `/page/workspace/roundtable/chains` 左栏列出智能体、右栏有序长条卡片;左→右拖入指定位、右栏内拖拽排序均生效;2–8 校验生效;保存/重载/删除正常。
3. 业务链条卡片:标题 + 彩色智能体 tag + ← 箭头连接,渲染正确。
4. 选定链条 → Step 2 席位顺序 = 链条顺序,并**按顺序逐个发言**,后序能看到前序交付。
5. chain 跑完解锁 Step 3;草稿记住 chain 模式并可重载。
6. 删除链条中引用的 agent 后,选择端有降级提示,不崩。
---
## 9. 风险与开放问题
| # | 项 | 说明 / 建议 |
|---|----|-------------|
| 1 | 配置页:弹窗 vs 页面 | 本文档定为**独立页面**(§2.1)。若产品要弹窗,编辑器组件可复用,仅换外壳。**请确认。** |
| 2 | 链条持久化 | **已定:后端 MySQL `roundtable_chains`**(§2.2 / §5.2),仿草稿持久化。 |
| 3 | Step 2 链编排 | **已定:总控按链条顺序派活**(§2.4 / §6),后端零改动。 |
| 4 | 新增 `@dnd-kit` 依赖 | 若禁止新增依赖,退原生 HTML5 DnD(手感与可达性下降)。**请确认。** |
| 5 | ← 箭头方向 / tag 阅读顺序 | 默认 `seats[0]` 在最左、先发言;箭头仅作视觉连接。**请确认是否要「从右往左读」。** |
| 6 | chain 模式是否需要收口 leader 总结 | 默认需要(产出 Step 3 结论)。若末位席位自带总结可关掉。 |
| 7 | leader 派活纠偏策略 | leader 派错/未派时前端兜底推进游标(§6.2 `pickChainTask`);可加 toast 提示「已按链条强制推进」。 |
| 8 | chain 模式断点续跑 / 逐席干预 | MVP 不做,列为扩展点(§6.3 / §7 阶段 4)。 |
| 9 | 链条是否私有 | 本期仅本人私有(仿草稿按 `user_id` 隔离)。共享/市场留扩展。 |
---
## 10. 路径速查(新增/改动)
| 资源 | 路径 | 状态 |
|------|------|------|
| 链条类型 | `frontend-web/src/roundtable-planning/lib/business-chain.ts` | 新增 |
| 链条 API 客户端 | `frontend-web/src/roundtable-planning/api/chains.ts` | 新增 |
| 配置页 | `frontend-web/src/roundtable-planning/pages/BusinessChainEditorPage.tsx` | 新增 |
| 链条选择面板 | `frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx` | 新增 |
| 链条卡片 | `frontend-web/src/roundtable-planning/components/BusinessChainCard.tsx` | 新增 |
| 推荐弹窗(加模式壳) | `frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx` | 改 |
| 主页面(mode 透传) | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | 改 |
| Step2 编排(加 chain) | `frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts` | 改 |
| 草稿快照(加 mode) | `hooks/useDraftPersistence.ts` + `api/drafts.ts` | 改 |
| 路由注册 | `frontend-web/src/pages/WorkspaceRoutes.tsx` | 改 |
| 后端链条持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_chains/` | 新增 |
| 后端链条路由 | `offline-backend-20260512/backend/app/gateway/routers/roundtable_chains.py` | 新增 |
| 后端 store 接线 | `offline-backend-20260512/backend/app/gateway/deps.py`(`app.state.roundtable_chain_store`)+ `app.py` | 改 |
</content>
</invoke>