# 多智能体规划 · 后台挂起执行 + 进度可视化 —— 分步实现方案 > 目标:在「多智能体规划」第二步增加**后台挂起**能力——把当前任务挂到后台,自动一路跑到 > 最后一步(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 | 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/_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 export async function getJob(id: string): Promise export function streamJob(id: string, on: (e: JobProgressEvent) => void): () => void // 返回 unsubscribe export async function resumeJob(id: string, answer: string): Promise export async function cancelJob(id: string): Promise ``` - `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 {/* 大弹窗 */} … 任务标题 + 阶段标签(leader_thinking/dispatching/report_drawing…) … {/* 横向链路:总控 ──▶ 席位1 ──▶ 席位2 ──▶ … ──▶ [共识] ──▶ [报告绘制] */}
{nodes.map((n,i)=>( {/* 活动连线:流动虚线动画 */} {/* 活动节点:彩色+脉冲光环 */} ))}
轮次 {cycle}/8 · {statusLabel} {/* Phase 8 */}
``` **节点颜色**:`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`(已安装) ```