515 lines
32 KiB
Markdown
515 lines
32 KiB
Markdown
# 多智能体规划 · 后台挂起执行 + 进度可视化 —— 分步实现方案
|
||
|
||
> 目标:在「多智能体规划」第二步增加**后台挂起**能力——把当前任务挂到后台,自动一路跑到
|
||
> 最后一步(Step3 结果绘制完成)为止;历史记录下拉框里进行中的任务显示加载动画;点击进行中
|
||
> 的任务弹出一个**大弹窗**,里面是**横向的智能体派活链路流程图**(节点 = 各席位智能体,图标 +
|
||
> 名称,不同颜色,正在发言的节点和它的连接线带加载动画);弹窗里有「查看」按钮,点击进入正在
|
||
> 进行中的任务实时观察。
|
||
|
||
---
|
||
|
||
## 0. 现状与架构事实(动手前必读)
|
||
|
||
研究自现有代码,决定了整套方案的形态。**关键结论:当前编排循环跑在前端,离开页面即中断。**
|
||
|
||
### 0.1 前端现状
|
||
|
||
| 事实 | 位置 |
|
||
|------|------|
|
||
| Step2 编排循环(leader → 顺序派 special → leader … 直到 `dispatched===[]` 共识 / `MAX_CYCLES=8`)**完全由前端 hook 驱动** | `src/roundtable-planning/hooks/useStep2Orchestration.ts:992-1204`(recommend)、`1228-1439`(chain) |
|
||
| 循环由 `currentStep===2` 的 `useEffect` 启停;离开 Step2 的 cleanup 会 `orchestrationCancelRef.current=true` + `abortAllInFlight()` 中断 | `useStep2Orchestration.ts:1468-1541`(尤其 cleanup `1534-1537`、`1471-1477`) |
|
||
| 共识判定:`leaderRes.dispatched.length===0` → `setHasConsensus(true)` | `useStep2Orchestration.ts:1091-1099` |
|
||
| 进入 Step3:手动点按钮,前置 `hasConsensus===true` | `Step2Panel.tsx:370-381`、`RoundtablePlanningPage.tsx:586-595` |
|
||
| Step3「结果绘制」:前端流式调 `roundtable-report` 智能体,写出 `outputs/方案总览.html`,`phase→"done"` 即完成 | `src/roundtable-planning/hooks/useStep3Report.ts:36,51,165,560-596,636-652` |
|
||
| 草稿持久化:`/api/roundtable-drafts`,含 step1/step2/step3 三段快照 + `furthestStep(1|2|3)`,**无显式 status 字段** | `src/roundtable-planning/api/drafts.ts:25-110,112,184-225` |
|
||
| 历史下拉 UI(触发按钮 + 列表 + 重命名/删除) | `RoundtablePlanningPage.tsx:771-892` |
|
||
| 自动保存触发点(Step1 回复完 / Step2 每轮气泡收尾 / Step3 报告保存 / 步骤前进) | `useDraftPersistence.ts:248-316` |
|
||
| 加载草稿恢复:`handleLoadDraft` → `getDraft` → `cancelAllStreams` → hydrate → `setCurrentStep(furthestStep)` | `useDraftPersistence.ts:308-368` |
|
||
|
||
### 0.2 后端现状
|
||
|
||
| 事实 | 位置 |
|
||
|------|------|
|
||
| `/api/multi-agent/init`:为各席位并行建 thread + 单例总控 `roundtable-coordinator`,SSE 返回 `seat_ready`/`coordinator_ready`/`init_done(thread_ids)` | `app/gateway/routers/multi_agent.py:647-823` |
|
||
| `/api/multi-agent/run/stream`:驱动**单轮** leader 或 special 的 LLM 执行,末尾 JSON 帧 `status`(派活列表 / `[]` 共识 / `clarification` / `error`) | `multi_agent.py:1076-1142` |
|
||
| `/api/multi-agent/report/init`:为 Step3 报告建 thread | `multi_agent.py`(report/init) |
|
||
| **后端不存在编排循环**——leader 派活由前端解释后再逐个发 `run/stream`;后端只做单轮执行 + 把派活/完成「广播」追加进各 thread 历史消息 | `multi_agent.py:24-29,487-502,1108-1142` |
|
||
| 派活格式:`agent_orchestration` 工具调用 `{agent_name, task}`(旧式 bash 解析兼容) | `multi_agent.py:525-552` |
|
||
|
||
### 0.3 可复用模板:AI 写作的「后台多阶段流程」
|
||
|
||
AI 写作已经把「后台跑多阶段流程 + 前端 SSE 订阅 + 可 resume + 定时清理」跑通了,**圆桌后台作业直接照搬这套**:
|
||
|
||
| 模板要素 | 位置 |
|
||
|------|------|
|
||
| LangGraph 图驱动多阶段(researching/writing_outline/writing_draft/reviewing/…),条件路由推进,`interrupt()` 暂停点等用户输入 | `packages/harness/deerflow/agents/ai_writing/graph.py`、`nodes/*`(`pause_material_confirm` 等 `45-290`) |
|
||
| Session 持久化表 `ai_writing_sessions`:`id/user_id/title/status/draft_*/transcript(LONGTEXT)/...`,`status∈{in_progress,done,error}` | `packages/harness/deerflow/persistence/ai_writing_sessions/model.py:14-39` |
|
||
| `transcript` 用 `PortableLongText`(MySQL LONGTEXT,避开 64KB),时间用 `BeijingDateTime` | 同上 `27-38` |
|
||
| 进度走 LangGraph Server 标准流 `stream_mode=["messages-tuple","values"]`,**graph 在 langgraph runtime 里跑,任意 worker 可 join stream**(这是后台健壮性的关键) | `ai_writing.py:1-17`、`langgraph.json:16-18` |
|
||
| Checkpointer 共享 `AsyncSqliteSaver`,落 `.deer-flow/data/checkpoints.db`(需挂数据卷) | `runtime/checkpointer/async_provider.py` |
|
||
| Cleanup cron(leader 文件锁、活跃保护、先删 CP 再删 DB、admin 手动触发) | `app/gateway/ai_writing_cleanup.py:26,34-59,111-135,193-215,307-367` |
|
||
| **多 worker 坑**:模块级内存 dict(`_event_queues`)每 worker 一份会丢会话;SSE 进程内直调会整段 buffer。解法:状态进 DB+checkpointer、SSE 走真 TCP loopback `127.0.0.1`(`trust_env=False` 禁代理) | `multi_agent.py:271-293`、`AIWritingPage.tsx` 文件头注释 §3/§5 |
|
||
|
||
### 0.4 决策(已与产品确认)
|
||
|
||
1. **后端真后台执行**:把编排循环搬到后端 run,刷新 / 关页 / 换设备都不中断。
|
||
2. **流程图节点 = 智能体派活链路**:横向排列的席位节点,正在发言的节点 + 连接线加载动画。
|
||
|
||
> ⚠️ **本方案最大的工作量在 Phase 2**:把目前跑在前端 TS 里的编排循环(`runOrchestration`/`runChainOrchestration`)
|
||
> 移植成一个后端 **LangGraph「圆桌总控编排图」**。这是与 AI 写作一致、可在 LangGraph runtime 里
|
||
> 后台跑且天然 checkpoint 的健壮路径。下文每个阶段都给出验收标准,可独立提测。
|
||
|
||
---
|
||
|
||
## 1. 总体架构
|
||
|
||
```
|
||
┌─────────────────────────────── 前端 (frontend-web) ───────────────────────────────┐
|
||
│ Step2Panel ──「后台挂起」按钮──▶ POST /api/roundtable-jobs (start) │
|
||
│ HistoryDropdown ── 进行中任务显示 spinner ──▶ 点击 ──▶ DispatchChainModal(大弹窗) │
|
||
│ DispatchChainModal ── 订阅 GET /api/roundtable-jobs/{id}/stream(SSE) │
|
||
│ 横向派活链路流程图(节点=席位, 活动节点+连线动画) ──「查看」──▶ 进入任务实时观察 │
|
||
└───────────────────────────────────────────────────────────────────────────────────┘
|
||
│ SSE 进度 / 快照
|
||
┌─────────────────────────────── 后端 (backend) ─────────────────────────────────────┐
|
||
│ /api/roundtable-jobs 路由层(start/get/stream/resume/cancel) ── 仿 ai_writing.py │
|
||
│ │ 启动后台 run │
|
||
│ LangGraph「圆桌总控编排图」 roundtable_orchestrator ── 仿 ai_writing graph │
|
||
│ init → leader_call → parse_dispatch → dispatch_seats(顺序) → loop ↺ │
|
||
│ → consensus → report_draw(写 HTML) → done (clarification 点 interrupt()) │
|
||
│ │ 每次状态转移持久化进度 │
|
||
│ roundtable_jobs 表(status/phase/dispatch_chain/active_speaker/...) + 共享 checkpointer│
|
||
│ cleanup cron (仿 ai_writing_cleanup.py) │
|
||
└───────────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**核心思路**:后台作业 = 一个 LangGraph 图的一次 run。图节点内部复用现有的单轮执行能力
|
||
(`multi_agent.py` 里 leader/special 的 run 逻辑),把「解释派活 → 顺序派活 → 回到 leader」的
|
||
循环从前端搬进图节点。前端从「驱动者」退化为「观察者」:通过 SSE 订阅进度渲染。
|
||
|
||
---
|
||
|
||
## 2. 数据契约(前后端共享,先定义后实现)
|
||
|
||
### 2.1 作业状态机 `JobStatus`
|
||
|
||
```
|
||
queued # 已创建,等待 worker 拉起
|
||
running # 编排进行中(Step2 派活循环 / Step3 绘制)
|
||
awaiting_input # 命中 clarification,等用户回答(interrupt 挂起)
|
||
done # 已跑到 Step3 绘制完成
|
||
error # 出错终止
|
||
cancelled # 用户取消
|
||
```
|
||
|
||
### 2.2 作业阶段 `JobPhase`(细化给流程图/进度条用)
|
||
|
||
```
|
||
initializing # 建 thread / 总控就绪
|
||
leader_thinking # 总控在决策派活
|
||
dispatching # 正在顺序派活给席位(配合 active_speaker)
|
||
consensus # 共识达成(dispatched=[])
|
||
report_drawing # Step3 报告绘制中
|
||
report_done # 绘制完成
|
||
```
|
||
|
||
### 2.3 派活链路节点 `DispatchChainNode`(流程图核心数据)
|
||
|
||
```ts
|
||
interface DispatchChainNode {
|
||
agentId: string; // 席位 agent_id(小写)
|
||
name: string; // 显示名
|
||
avatarType: string; // 复用 getAvatarTypeFor(agentId, index) → 决定颜色
|
||
order: number; // 横向排列次序(chain 模式=链条顺序;recommend 模式=席位/发言顺序)
|
||
state: 'pending' | 'active' | 'done'; // 节点状态(驱动颜色/动画)
|
||
lastSpokeCycle?: number; // 最近发言轮次(可选,做 tooltip)
|
||
}
|
||
```
|
||
|
||
### 2.4 进度事件 `JobProgressEvent`(SSE 推送的归一化事件)
|
||
|
||
```ts
|
||
type JobProgressEvent =
|
||
| { type: 'status'; status: JobStatus }
|
||
| { type: 'phase'; phase: JobPhase; cycle: number }
|
||
| { type: 'chain'; nodes: DispatchChainNode[] } // 派活链路全量/增量
|
||
| { type: 'active'; agentId: string | null } // 当前发言席位(null=总控/无)
|
||
| { type: 'consensus'; percentage: number }
|
||
| { type: 'dialogue'; bubble: Step2DialogueLike } // 新增/收尾的对话气泡(供「查看」实时渲染)
|
||
| { type: 'clarification'; question: string } // 命中澄清
|
||
| { type: 'done'; step3: DraftStep3Snapshot } // 终态:最终报告快照
|
||
| { type: 'error'; detail: string };
|
||
```
|
||
|
||
> SSE 通道可直接复用 LangGraph Server 的 `values` 流(图状态快照),由前端 hook 把 `values`
|
||
> 映射成上述归一化事件;也可在路由层做映射后再下发。**推荐前端映射**(少一层后端耦合)。
|
||
|
||
### 2.5 作业记录 `RoundtableJob`(持久化 + 列表展示)
|
||
|
||
```ts
|
||
interface RoundtableJob {
|
||
id: string;
|
||
draftId: string; // 关联的草稿
|
||
userId: string;
|
||
status: JobStatus;
|
||
phase: JobPhase;
|
||
cycle: number; // 当前轮次(0..MAX_CYCLES)
|
||
dispatchChain: DispatchChainNode[];
|
||
activeAgentId: string | null;
|
||
consensusPercentage: number;
|
||
pendingClarification: string | null;
|
||
threadIds: Record<string,string> | null; // 复用 ThreadIdMap
|
||
coordinatorName: string | null;
|
||
orchestrationMode: 'recommend' | 'chain';
|
||
chain?: { id: string; title: string } | null;
|
||
error: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 1 —— 后端:圆桌作业持久化模型
|
||
|
||
**目标**:建一张 `roundtable_jobs` 表(仿 `ai_writing_sessions`),存作业状态/阶段/派活链路,
|
||
并在 `roundtable_drafts` 列表里能 join 出当前作业状态。
|
||
|
||
**涉及文件(新增/改)**
|
||
- 新增 `packages/harness/deerflow/persistence/roundtable_jobs/model.py`(ORM 模型,仿 `ai_writing_sessions/model.py:14-39`)
|
||
- 新增 `.../roundtable_jobs/sql.py`(Repository:`create / get / update_progress / list_by_user / list_older_than / delete`,仿 `ai_writing_sessions/sql.py`)
|
||
- 新增 alembic 迁移 `persistence/migrations/versions/<date>_create_roundtable_jobs.py`
|
||
- 改 `roundtable_drafts` 的列表查询:返回里带上 `status`/`jobId`(LEFT JOIN roundtable_jobs,或在 draft 行上冗余一个 `status` 字段,二选一,见下)
|
||
|
||
**步骤**
|
||
1. 字段设计(对齐 §2.5):`id(PK)`、`draft_id`、`user_id`、`status`、`phase`、`cycle(Int)`、
|
||
`dispatch_chain(PortableLongText/JSON)`、`active_agent_id`、`consensus_percentage(Int)`、
|
||
`pending_clarification(Text)`、`thread_ids(JSON)`、`coordinator_name`、`orchestration_mode`、
|
||
`chain(JSON)`、`error(Text)`、`created_at/updated_at(BeijingDateTime)`。
|
||
2. `dispatch_chain` / `thread_ids` / `chain` 用 `PortableLongText` 存 JSON 字符串,repo 层序列化。
|
||
3. 列表 join:给 `DraftMeta` 增加 `status?: JobStatus` 与 `jobId?: string`。
|
||
**推荐方案**:草稿与作业 1:1(一个草稿最多一个活跃作业),在 draft 列表查询 LEFT JOIN
|
||
`roundtable_jobs` 取最新作业的 `status`。
|
||
4. 索引:`(user_id, updated_at)`、`(draft_id)`、`(status)`(cleanup/列表用)。
|
||
|
||
**验收**
|
||
- 迁移可 `alembic upgrade head` 跑通,MySQL 上 `dispatch_chain` 为 `longtext`。
|
||
- 单测:create→update_progress→get 往返;`list_by_user` 按 `updated_at` 倒序;`list_older_than` 分批。
|
||
|
||
---
|
||
|
||
## Phase 2 —— 后端:圆桌总控编排 LangGraph 图(核心 / 最大工作量)
|
||
|
||
**目标**:把前端的编排循环移植成一个后端 LangGraph 图,可在 LangGraph runtime 里后台跑、天然
|
||
checkpoint,一路跑到 Step3 绘制完成。
|
||
|
||
**涉及文件(新增)**
|
||
- `packages/harness/deerflow/agents/roundtable_orchestrator/graph.py`(图装配,仿 `ai_writing/graph.py`)
|
||
- `.../roundtable_orchestrator/state.py`(图状态 schema:intent、selectedAgents、threadIds、coordinatorName、
|
||
mode/chain、dialogues、dispatchChain、activeAgentId、cycle、consensus、phase、step3 等)
|
||
- `.../roundtable_orchestrator/nodes/*.py`(各节点)
|
||
- 在 `langgraph.json` 注册新 assistant `roundtable_orchestrator`(仿 ai_writing assistant 注册,参考 `langgraph.json:16-18`)
|
||
|
||
**图节点设计(把前端循环逐段翻译过来)**
|
||
|
||
| 节点 | 职责 | 对应前端逻辑 |
|
||
|------|------|------|
|
||
| `init` | 复用 `_create_thread_direct` 批量建席位 thread + 总控就绪(或接收前端已建好的 `threadIds` 直接用) | `useStep2Orchestration.ts:1485-1531` / `multi_agent.py:647-823` |
|
||
| `leader_call` | 跑一轮总控(agent_type=leader),拿 `status`(派活列表 / `[]` / `clarification`) | 单轮逻辑 `multi_agent.py:1076-1142` |
|
||
| `route_after_leader`(条件边) | `clarification`→`interrupt`;有派活→`dispatch_seats`;`[]`→`consensus` | `useStep2Orchestration.ts:1091-1181` |
|
||
| `dispatch_seats` | **顺序**派活:对 dispatched 里每个席位依次跑 special run;每开始一个就更新 `activeAgentId` + 进度持久化 | `useStep2Orchestration.ts:1101-1181`(务必串行,勿并行,详见前端 §5.4 注释) |
|
||
| `clarification_pause` | `interrupt()` 挂起,`status→awaiting_input`,写 `pending_clarification` | 仿 ai_writing `pause_*` `45-290` |
|
||
| `consensus` | `setHasConsensus`、`consensus=100`,落库后进入 `report_draw` | `1091-1099` |
|
||
| `report_draw` | 跑 `roundtable-report`(先 report/init 建 thread),流式写 `outputs/方案总览.html`,产出 `DraftStep3Snapshot` | `useStep3Report.ts:91-149,636-652,560-596` |
|
||
| `done` | `status→done`、写 `step3` 进作业 + 回写草稿 step3 快照 | `RoundtablePlanningPage.tsx:613-617` |
|
||
|
||
**关键实现要点**
|
||
1. **复用单轮执行**:节点内部不要重写 LLM 调用,抽出 `multi_agent.py` 里 leader/special/report 的
|
||
单轮 run 为可被图节点直接 `await` 的内部函数(去掉 HTTP 层,进程内调 LangGraph)。
|
||
2. **派活广播**:保留现有把「总控给 X 派了活 / X 完成」追加进各 thread 历史的广播
|
||
(`multi_agent.py:487-502`),让席位读到全局上下文。
|
||
3. **MAX_CYCLES 兜底**:图里维护 `cycle`,≥8 强制收敛(对齐 `roundtable-constants.ts:13`)。
|
||
4. **chain 模式**:`orchestration_mode==='chain'` 时按 `chain.seats` 固定顺序派活,跳过总控自由决策的
|
||
部分(对齐 `runChainOrchestration` `1228-1439`)。
|
||
5. **进度落库**:每个节点结束 `update_progress(jobId, {phase, cycle, dispatchChain, activeAgentId,
|
||
consensus, dialogues_delta})`,前端 SSE 才有实时数据。
|
||
6. **dialogues 累积**:把每轮气泡写进图状态 `dialogues`,既供「查看」实时渲染,也供作业结束回写草稿 step2。
|
||
|
||
**验收**
|
||
- 用脚本(仿 `scripts/probe_ai_writing_assistant.py`)直接对图发起一次 run,不开前端,能从 intent +
|
||
selectedAgents 一路跑到写出 `方案总览.html`,作业 `status` 终态 `done`。
|
||
- 命中 clarification 时图 `interrupt`,`status=awaiting_input`,`pending_clarification` 有值。
|
||
- chain 模式按链条顺序派活,recommend 模式由总控自由派活,两条路径都能收敛。
|
||
|
||
---
|
||
|
||
## Phase 3 —— 后端:作业控制 + 进度 SSE 接口
|
||
|
||
**目标**:暴露 start/get/stream/resume/cancel,后台 run 走 LangGraph Server(任意 worker 可 join)。
|
||
|
||
**涉及文件(新增/改)**
|
||
- 新增 `app/gateway/routers/roundtable_jobs.py`(仿 `ai_writing.py` 的 CRUD + 控制壳)
|
||
- 在 gateway `app.py` 注册路由
|
||
- 新增 `app/gateway/roundtable_jobs_cleanup.py`(仿 `ai_writing_cleanup.py`)+ 在启动处挂 cron
|
||
|
||
**接口契约**
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| `POST` | `/api/roundtable-jobs` | 入参:`{draftId, intent, selectedAgents, threadIds?, coordinatorName?, model, orchestrationMode, chain?}`。建作业记录(`queued`) + 启动图 run(LangGraph Server thread),返回 `RoundtableJob` |
|
||
| `GET` | `/api/roundtable-jobs/{id}` | 作业快照(轮询兜底 / 弹窗首帧) |
|
||
| `GET` | `/api/roundtable-jobs/{id}/stream` | SSE 进度流:透传图 `values`/`messages-tuple`,或映射成 §2.4 事件下发。**走真 TCP loopback**(`multi_agent.py:271-293`),禁代理 |
|
||
| `POST` | `/api/roundtable-jobs/{id}/resume` | 入参 `{answer}`,回填 clarification → 图 resume,`status→running` |
|
||
| `POST` | `/api/roundtable-jobs/{id}/cancel` | 取消图 run,`status→cancelled` |
|
||
| `GET` | `/api/roundtable-jobs?draftId=` | 按草稿查活跃作业(页面恢复用) |
|
||
| `POST` | `/api/roundtable-jobs/admin/cleanup` | 管理员手动清理(仿 ai_writing admin cleanup `307-367`) |
|
||
|
||
**多 worker 健壮性(务必遵守,照搬 ai_writing 教训)**
|
||
- 作业状态只存 **DB + checkpointer**,**不要**用模块级内存 dict 存 per-session 状态。
|
||
- SSE 必须经真 TCP `127.0.0.1` loopback,不能 `httpx.ASGITransport(app=...)` 进程内直调(会整段 buffer)。
|
||
- 图 run 跑在 LangGraph runtime(独立于处理 HTTP 的 worker),刷新/换 worker 都能重新 join stream。
|
||
- 数据卷:`.deer-flow/data/checkpoints.db` 必须挂出,否则容器重启丢状态。
|
||
|
||
**验收**
|
||
- `POST /start` 后立刻 `GET /{id}` 能拿到 `queued/running`;关掉 SSE 再重连能继续收进度。
|
||
- 杀掉发起 start 的 worker(或多 worker 轮询),`resume`/`stream` 仍命中同一作业。
|
||
- cleanup 干跑:活跃作业永不删,先删 CP 再删 DB。
|
||
|
||
---
|
||
|
||
## Phase 4 —— 前端:作业 API 客户端 + 数据模型扩展
|
||
|
||
**目标**:前端有一层干净的作业 API 与类型,历史列表能拿到 status。
|
||
|
||
**涉及文件(新增/改)**
|
||
- 新增 `src/roundtable-planning/api/roundtable-jobs.ts`:
|
||
```ts
|
||
export async function startJob(payload: StartJobPayload): Promise<RoundtableJob>
|
||
export async function getJob(id: string): Promise<RoundtableJob>
|
||
export function streamJob(id: string, on: (e: JobProgressEvent) => void): () => void // 返回 unsubscribe
|
||
export async function resumeJob(id: string, answer: string): Promise<void>
|
||
export async function cancelJob(id: string): Promise<void>
|
||
```
|
||
- `streamJob` 复用项目现有 SSE/`streamMultiAgent` 的封装方式(`api/multi-agent.ts:403+`),把
|
||
LangGraph `values` 映射成 §2.4 的归一化事件。
|
||
- 改 `src/roundtable-planning/api/drafts.ts`:`DraftMeta` 增 `status?: JobStatus; jobId?: string`
|
||
(`drafts.ts:76-83` + 字段映射 `122-178` 加 snake_case 转换)。
|
||
- 新增 `src/roundtable-planning/lib/job-types.ts`:把 §2.1–2.5 的类型集中导出。
|
||
|
||
**验收**:`pnpm typecheck` 通过;`getJob`/`streamJob` 能拉到 Phase 3 的数据。
|
||
|
||
---
|
||
|
||
## Phase 5 —— 前端:Step2「后台挂起」按钮
|
||
|
||
**目标**:Step2 顶栏/底栏加按钮,点击把当前任务交给后端后台跑,前端停止本地循环、转为观察者。
|
||
|
||
**涉及文件(改)**
|
||
- `src/roundtable-planning/components/Step2Panel.tsx`(按钮放在现有「进入第三步」附近 `370-381`)
|
||
- `src/roundtable-planning/pages/RoundtablePlanningPage.tsx`(接线 + 观察者模式)
|
||
- `src/roundtable-planning/hooks/useStep2Orchestration.ts`(暴露「停止本地循环并交后台」的方法)
|
||
|
||
**步骤**
|
||
1. Step2Panel 加「⏼ 后台挂起」按钮,`onBackgroundSuspend` 回调。
|
||
2. 页面 `handleBackgroundSuspend`:
|
||
- `ensureDraft(2)` 拿到 `draftId`(`useDraftPersistence.ts` ensureDraft)。
|
||
- 收集当前状态:`intentReady`、`selectedAgents`、`threadIds`、`coordinatorName`、`model`、
|
||
`orchestrationMode`、`chain`(全部来自 `step2`/`step1` 现有状态)。
|
||
- `startJob(...)` → 拿到 `jobId`。
|
||
- **停止前端本地编排**:调用 `step2.stopOrchestration()` + 置一个 `backgroundedJobId` 标记,
|
||
让 `currentStep===2` 的 useEffect **不再自启本地循环**(在 `useStep2Orchestration.ts:1468-1541`
|
||
的启动条件里加 `&& !backgrounded`)。
|
||
- toast「已转入后台,可离开页面,任务会自动跑到结果绘制完成」。
|
||
- 刷新历史列表(`drafts` 重新拉,进行中项即出现 spinner)。
|
||
3. **观察者模式**:若当前停留在 Step2 且该 draft 有 running 作业,订阅 `streamJob`,把进度事件渲染成
|
||
对话气泡/活动席位(复用现有 Step2 渲染),而不是本地驱动。
|
||
|
||
**注意:避免「双跑」**——后台已接管后,前端绝不能再并行驱动同一批 thread。用 `backgroundedJobId`
|
||
互斥;加载草稿时若发现有 running 作业,直接进观察者模式。
|
||
|
||
**验收**:点「后台挂起」→ 可立即切到别的任务/路由;回来仍在跑;不会出现前后端同时派活。
|
||
|
||
---
|
||
|
||
## Phase 6 —— 前端:历史下拉框「进行中」加载动画
|
||
|
||
**目标**:历史下拉里 `status==='running'`(或 `awaiting_input`)的草稿条目显示加载动画;点击进行中
|
||
条目不直接 load,而是打开流程图弹窗。
|
||
|
||
**涉及文件(改)**
|
||
- `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` 的 `HistoryDropdown`(`771-892`)
|
||
|
||
**步骤**
|
||
1. 列表项左侧名称前/后,按 `d.status` 渲染状态标记:
|
||
- `running` → 旋转的 `Loader2`(`animate-spin`)+「进行中」。
|
||
- `awaiting_input` → 呼吸点 +「待澄清」(琥珀色)。
|
||
- `done`/无 → 维持现状(显示 `Step {furthestStep}`)。
|
||
- `error` → 红点 +「出错」。
|
||
2. 点击行为分流:
|
||
- `running`/`awaiting_input` → `openDispatchChainModal(d.jobId)`(Phase 7),**不**直接 `onLoad`。
|
||
- 其它 → 维持 `onLoad(id)`。
|
||
3. 列表需要 `status`,确保 Phase 4 的 `DraftMeta.status` 已就位;下拉打开时 `refresh()` 拉最新。
|
||
|
||
**验收**:后台挂起后,历史下拉对应条目转圈;点击弹出流程图弹窗。
|
||
|
||
---
|
||
|
||
## Phase 7 —— 前端:派活链路流程图弹窗(横向 / 大弹窗 / 动画)
|
||
|
||
**目标**:大弹窗,横向排列的智能体派活链路。节点 = 席位(图标 + 名称),不同颜色;正在发言的
|
||
节点和它的连接线带加载动画;含「查看」按钮。
|
||
|
||
**涉及文件(新增)**
|
||
- `src/roundtable-planning/components/DispatchChainModal.tsx`
|
||
|
||
**实现选型**:链路是「总控 → 席位序列」的线性结构 + 需要对**单个节点和单条连线**做精确动画,
|
||
**推荐手写 flex 横向布局 + CSS 动画连接线**(轻、可控)。项目已装 `@xyflow/react@12`(带 `animated`
|
||
边)作为备选,但手写对本场景的「节点 loading + 连线流动」更直接。
|
||
|
||
**结构(手写方案草图)**
|
||
```tsx
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="flex max-h-[88vh] flex-col gap-0 sm:max-w-5xl"> {/* 大弹窗 */}
|
||
<DialogHeader>… 任务标题 + 阶段标签(leader_thinking/dispatching/report_drawing…) …</DialogHeader>
|
||
|
||
{/* 横向链路:总控 ──▶ 席位1 ──▶ 席位2 ──▶ … ──▶ [共识] ──▶ [报告绘制] */}
|
||
<div className="custom-scrollbar overflow-x-auto py-8">
|
||
<div className="flex items-center gap-0 min-w-max px-6">
|
||
<CoordinatorNode active={phase==='leader_thinking'} />
|
||
{nodes.map((n,i)=>(
|
||
<Fragment key={n.agentId}>
|
||
<Connector active={n.state==='active'} /> {/* 活动连线:流动虚线动画 */}
|
||
<ChainNode node={n} /> {/* 活动节点:彩色+脉冲光环 */}
|
||
</Fragment>
|
||
))}
|
||
<Connector active={phase==='consensus'} />
|
||
<MilestoneNode label="共识" done={consensus===100} />
|
||
<Connector active={phase==='report_drawing'} />
|
||
<MilestoneNode label="报告绘制" active={phase==='report_drawing'} done={phase==='report_done'} />
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter className="border-t pt-3 sm:justify-between">
|
||
<span className="text-xs text-muted-foreground">轮次 {cycle}/8 · {statusLabel}</span>
|
||
<Button onClick={onView}>查看</Button> {/* Phase 8 */}
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
```
|
||
|
||
**节点颜色**:`avatarType = getAvatarTypeFor(agentId, order)` → `chainTagClasses(avatarType)`
|
||
(复用 `lib/business-chain.ts:56-72` + `lib/roundtable-constants.ts`),与业务链条卡片同一套色。
|
||
|
||
**节点状态样式**
|
||
- `pending`:灰色描边、低透明度。
|
||
- `active`:席位色填充 + 外圈 `animate-ping`/脉冲光环 + 图标旋转或呼吸。
|
||
- `done`:席位色实心 + 右上角 ✓。
|
||
|
||
**连接线加载动画(active 连线)**:CSS 流动虚线(marching-ants)或渐变扫光:
|
||
```css
|
||
/* roundtable-planning.css 追加 */
|
||
@keyframes chain-flow { to { background-position: 20px 0; } }
|
||
.chain-connector--active {
|
||
background-image: linear-gradient(90deg, var(--brand-500) 50%, transparent 50%);
|
||
background-size: 20px 100%;
|
||
animation: chain-flow 0.6s linear infinite;
|
||
}
|
||
```
|
||
|
||
**数据来源**:弹窗 mount 时 `getJob(jobId)` 取首帧,随后 `streamJob(jobId, onEvent)` 实时更新
|
||
`nodes`/`activeAgentId`/`phase`/`consensus`/`cycle`;`done` 事件后停止动画、全节点置 `done`。
|
||
弹窗关闭时 `unsubscribe`。
|
||
|
||
**验收**:弹窗够大、横向滚动;活动席位节点和它左侧连线在动;阶段推进时高亮顺移;`done` 后静止。
|
||
|
||
---
|
||
|
||
## Phase 8 —— 前端:「查看」进入进行中任务并实时观察
|
||
|
||
**目标**:弹窗「查看」→ 进入该任务,停在 Step2 实时看后台进度(气泡/活动席位随 SSE 刷新),
|
||
跑完自动呈现 Step3 结果。
|
||
|
||
**涉及文件(改)**
|
||
- `RoundtablePlanningPage.tsx`(接 `onView`)
|
||
- `useDraftPersistence.ts`(`handleLoadDraft` 增「带作业」分支)
|
||
- `useStep2Orchestration.ts`(观察者渲染)
|
||
|
||
**步骤**
|
||
1. `onView`:关弹窗 → `handleLoadDraft(draftId)` 恢复该草稿到 Step2(`useDraftPersistence.ts:308-368`),
|
||
但**跳过本地编排自启**(识别到有 running 作业 → 进观察者模式,对齐 Phase 5 的互斥标记)。
|
||
2. 订阅 `streamJob(jobId)`,把 `dialogue` 事件 append 进 `step2RoundtableDialogues`、`active` 事件
|
||
驱动 `activeSpeakerId` 高亮、`consensus` 更新百分比。
|
||
3. 命中 `awaiting_input`:在 Step2 弹出澄清输入(复用现有 `pendingClarification` UI),用户回答 →
|
||
`resumeJob(jobId, answer)`(而非本地 `submitClarification`)。
|
||
4. `done` 事件:用返回的 `DraftStep3Snapshot` 走 `hydrateStep3` + 允许切到 Step3 查看报告
|
||
(`useStep3Report.hydrateSaved` `708-767`);作业列表该项变「已完成」。
|
||
|
||
**验收**:从历史「查看」进入能实时看到后台在跑;澄清能在前端回答并续跑;跑完能看到最终报告。
|
||
|
||
---
|
||
|
||
## Phase 9 —— 边界、健壮性与收尾
|
||
|
||
1. **双跑互斥**:任一 draft 同一时刻只允许一个执行者(前端本地 or 后台作业)。加载草稿/进入 Step2
|
||
前先 `GET /api/roundtable-jobs?draftId=`,有 running 就进观察者模式。
|
||
2. **后台命中澄清**:`awaiting_input` 时历史下拉显示「待澄清」徽标;流程图弹窗显示「等待澄清」;
|
||
只有「查看」进去才能回答(或在弹窗里直接给一个回答框,调用 `resumeJob`)。
|
||
3. **取消**:流程图弹窗/Step2 提供「停止后台」→ `cancelJob`,`status→cancelled`,前端可恢复本地编辑。
|
||
4. **错误恢复**:`error` 事件展示 detail;允许「重试」= 重新 `startJob`(沿用已建 threadIds)。
|
||
5. **MAX_CYCLES / 预算**:后台图内兜底(对齐前端 `MAX_CYCLES=8`),到顶强制收敛并照常进 Step3。
|
||
6. **多 worker**:严格遵守 Phase 3 的「状态进 DB+checkpointer、SSE 走 loopback」;部署若多 worker
|
||
需 sticky 或全靠 DB 态(推荐后者)。
|
||
7. **清理**:`roundtable_jobs` cleanup cron(活跃保护、先删 CP 再删 DB),与 draft 生命周期对齐。
|
||
8. **历史列表性能**:draft 列表 join 作业 status 时注意 N+1;用一次 LEFT JOIN 取最新作业。
|
||
|
||
---
|
||
|
||
## 端到端验证清单(提测前逐条过)
|
||
|
||
- [ ] Step2 点「后台挂起」→ 立刻可离开页面 / 切到别的任务,后台继续跑。
|
||
- [ ] 历史下拉里该任务显示加载动画(转圈)。
|
||
- [ ] 点击进行中任务 → 大弹窗、横向派活链路、活动节点+连线在动、节点不同颜色。
|
||
- [ ] 阶段推进(leader→派活→共识→报告绘制)时弹窗高亮顺移。
|
||
- [ ] 弹窗「查看」→ 进入任务实时观察气泡/活动席位。
|
||
- [ ] 后台命中澄清 → 列表「待澄清」+ 弹窗提示 + 可回答续跑(`resumeJob`)。
|
||
- [ ] 一路自动跑到 Step3「绘制完成」,最终报告可在 Step3 查看,作业 `done`。
|
||
- [ ] 浏览器刷新 / 关页重开后,任务仍在后台跑、状态可恢复(真后台验证)。
|
||
- [ ] 取消后台 → `cancelled`,无残留双跑。
|
||
- [ ] 多 worker(或杀 worker)下 stream/resume 不丢会话。
|
||
- [ ] cleanup 干跑不误删活跃作业。
|
||
|
||
---
|
||
|
||
## 工作量与排期建议
|
||
|
||
| 阶段 | 内容 | 粗估 | 依赖 |
|
||
|------|------|------|------|
|
||
| P0 | 数据契约定型(本文 §2) | 0.5d | — |
|
||
| P1 | 后端作业表 + repo + 迁移 | 1d | P0 |
|
||
| **P2** | **后端编排图(移植循环)** | **3–5d(核心难点)** | P0,P1 |
|
||
| P3 | 后端控制 + SSE + cleanup | 1.5d | P1,P2 |
|
||
| P4 | 前端作业 API + 类型 + draft status | 0.5d | P3 |
|
||
| P5 | Step2 后台挂起按钮 + 互斥 | 1d | P4 |
|
||
| P6 | 历史下拉 loading 动画 | 0.5d | P4 |
|
||
| P7 | 派活链路流程图弹窗 | 1.5d | P4 |
|
||
| P8 | 「查看」实时观察 + 澄清续跑 | 1.5d | P5,P7 |
|
||
| P9 | 边界/健壮性/清理 | 1d | 全部 |
|
||
|
||
> 建议先打通 **P1→P2→P3** 的后端最小闭环(脚本验证图能后台跑到出报告),再做前端 P4–P8。
|
||
> P2 是整条链路成败的关键,务必先用脱离前端的探针脚本(仿 `scripts/probe_ai_writing_assistant.py`)跑通。
|
||
|
||
---
|
||
|
||
## 附:关键参考代码索引
|
||
|
||
- 前端编排循环(待移植到后端):`useStep2Orchestration.ts:992-1204,1228-1439,1468-1541`
|
||
- 共识判定 / 进 Step3:`useStep2Orchestration.ts:1091-1099`、`Step2Panel.tsx:370-381`、`RoundtablePlanningPage.tsx:586-595`
|
||
- Step3 绘制完成标志:`useStep3Report.ts:51,165,560-596,636-652`
|
||
- 草稿模型 / API / 自动保存 / 加载恢复:`api/drafts.ts:25-110,184-225`、`useDraftPersistence.ts:248-316,308-368`
|
||
- 历史下拉 UI:`RoundtablePlanningPage.tsx:771-892`
|
||
- 后端单轮执行 / 派活 / 广播:`multi_agent.py:647-823,1076-1142,525-552,487-502`、loopback `271-293`
|
||
- AI 写作模板:`agents/ai_writing/graph.py`、`ai_writing_sessions/model.py:14-39`、`ai_writing.py`、`ai_writing_cleanup.py:307-367`、`AIWritingPage.tsx` 文件头注释
|
||
- 颜色体系复用:`lib/business-chain.ts:56-72`、`lib/roundtable-constants.ts`(`getAvatarTypeFor`)
|
||
- 流程图备选库:`@xyflow/react@12`(已安装)
|
||
```
|