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

32 KiB
Raw Blame History

多智能体规划 · 后台挂起执行 + 进度可视化 —— 分步实现方案

目标:在「多智能体规划」第二步增加后台挂起能力——把当前任务挂到后台,自动一路跑到 最后一步(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
历史下拉 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(流程图核心数据)

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 推送的归一化事件)

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(持久化 + 列表展示)

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:
    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 + 连线流动」更直接。

结构(手写方案草图)

<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)或渐变扫光:

/* 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(已安装)