Files
deerflow-code/.zcode/plans/plan-sess_dfc63a2a-7431-46c2-a4ba-63080d16e529.md
T
董宏凯andCursor f82363d81f feat(maf): Phase 5 - action coordinator, approval flow, frontend MAF roundtable pages
- Backend: action_coordinator, capability_snapshot, event_normalizer, tool_executor updates
- Backend: approved_tool_executor with write guard, action leases, migration script
- Frontend: maf-roundtable module (API client, event timeline, approval cards, fact flow panel)
- Frontend: MafRoutes, MafSessionsPage, MafRunDetailPage
- Tests: action_leases, migration_phase5, phase5_entry_e2e, real_write_e2e/http_e2e
- Docs: update implementation plan

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 13:07:21 +08:00

11 KiB
Raw Blame History

MAF 多智能体会商 Phase-4 前端实施计划

决策回顾(已与用户确认)

  • 入口:独立顶级菜单 /page/maf/*,新增侧边栏一级项「MAF 会商」
  • 实时更新:纯前端轮询,GET /runs/{id}/events?after_seq=N&limit=1000,间隔 1.8s,复用现有后端,零 SSE、零后端改动
  • 流程图:要。基于 confirmed_facts(来自 run.maf_checkpoint.fact_ledger)+ 末轮 final_result.content 解析的 flow-json,复用 @xyflow/react + flow-graph.ts + FlowGraphPanel
  • 审批:卡片式,仿 IntentClarificationCard 的视觉/交互模式(参数摘要 + 批准/拒绝按钮 + 备注 textarea),落到 POST /approvals/{id}/decision

关键数据契约(已核实后端源码)

  • 后端只读:16 个端点全部命中,无改动。GET /health 返回 enabled 决定页面内容(入口仍可见,未启用时显示空态)。
  • 事件流:单调 (run_id, seq),GET /events?after_seq=N 返回 {events:[...], latest_seq};前端推进 after_seq=latest_seq。payload 已在后端写入时脱敏(sanitize_payload),前端直接渲染。
  • 事件分类(event_normalizer.py):run.started/completed/failed/cancelled、agent.turn_started/message.delta/message.completed、tool.requested/started/completed/failed、skill.started/completed/failed/selected、artifact.created、action.waiting_approval/approved/rejected、workflow.checkpointed。
  • 事实账本(fact_ledger.empty_ledger):candidate_facts/confirmed_facts/evidence/tool_results/executed_actions/pending_actions/failed_or_unknown_actions/conflicts。confirmed_facts 条目含 {candidate_id, text, agent_id, confirmed_by, evidence_refs[]}。
  • Run 字段(run_to_dict):id, session_id, status, maf_checkpoint(含 fact_ledger + final_result + seat_outcomes), last_event_seq, started_at, finished_at, error_code, error_message_safe, token_usage, latency_ms。
  • 终态判定:runIsLive(status) = ["pending","running","waiting_approval","cancelling","cancel_failed"].includes(status),非 live 即停轮询。
  • Action/Approval:GET /runs/{id}/actions、GET /runs/{id}/approvals;POST /approvals/{id}/decision {decision:"approved"|"rejected"}。

复用清单(已定位到文件:行)

  • apiFetch(core/api/fetch-client.ts):内部自动注入 Authorization(getAuthorizationHeaderValue())+ credentials:"include",返回 Response 不解析、不拼 URL。调用方拼 ${getBackendBaseURL()}/api/maf-roundtable/...,自行 res.ok 检查 + res.json()。
  • getBackendBaseURL(core/config/index.ts):未设时返回 "",相对路径同源可用。
  • TanStack Query 模式(strategy-components/api/task-buttons.ts):useQuery+refetchOnWindowFocus:false;useMutation+onSuccess invalidate;queryKey 用嵌套数组 ["maf-roundtable", runId, "events"]。
  • 轮询模式(core/scheduled-tasks/hooks.ts:105-115):函数式 refetchInterval: (query) => runIsLive(query.state.data?.status) ? ms : false。
  • FlowGraphPanel(roundtable-planning/components/FlowGraphPanel.tsx):props {data: FlowGraphData, streaming, onClose, ...},自动 useTheme() 取明暗。FlowGraphData = {title, nodes:[{id,label,type,detail?,group?}], edges:[{from,to,label?}]}。extractFlowGraph(text)(flow-graph.ts:263)容错解析 flow-json 代码块(含半截 JSON)。
  • IntentClarificationCard(roundtable-planning/components/IntentClarificationCard.tsx):卡片视觉模板(rounded-xl border-red-100 + 「需要确认」徽标 + 选项按钮 + textarea + 提交)。
  • 时间线 feed(roundtable-planning/components/Step1Panel.tsx + MessageBubble + useAutoScroll + ScrollToBottomButton):数组入、按 seq 序渲染。
  • useDesensitizeSubtree(roundtable-planning/lib/use-desensitize-subtree.ts):仅挂 header/面板框架;时间线 body 不挂(payload 已脱敏 + hook 自身警告勿用于消息列表)。
  • 路由(pages/PageRoutes.tsx:143-169):<Route path="maf/*" element={<MafRoutes/>}/>(仿 RoundtableRoutes.tsx)。
  • 菜单(core/page-layout/sidebar-menu.ts):在 sidebarMenuItems 数组加一级项,icon 用 lucide 组件(非字符串 key)。新节点经 use-sidebar-menu.ts 自动走 static→inject→override→role→desensitize 流水线,无需额外注册。

文件夹结构(镜像 roundtable-planning/,全新 src/maf-roundtable/)

frontend-web/src/maf-roundtable/
├── api/
│   ├── types.ts            # TS 类型(snake_case 直传,后端即 snake_case)
│   ├── client.ts           # BASE()=getBackendBaseURL()+"/api/maf-roundtable";apiFetch 封装 + readJson<T>/parseDetail
│   ├── sessions.ts         # 会话 CRUD hooks(list/get/create/update/delete)
│   ├── runs.ts             # run hooks(get/list/create/cancel/resume/recover)+ useMafHealth
│   ├── events.ts           # listEvents(runId, afterSeq) 原始 fetch(供轮询 hook 用)
│   └── approvals.ts        # 审批 hooks(list + decide mutation)
├── hooks/
│   ├── use-maf-run-status.ts   # GET /runs/{id} useQuery,函数式 refetchInterval 停终态
│   └── use-maf-events.ts       # ★ cursor 轮询 hook(核心)
├── lib/
│   ├── event-timeline.ts       # 纯函数:events[] → TimelineEntry[](delta 聚合、tool 生命周期折叠、approval 锚点)
│   └── fact-flow-graph.ts      # 纯函数:confirmed_facts+evidence → FlowGraphData
├── components/
│   ├── MafRunHeader.tsx        # 状态徽标 + token/延迟 + 取消/恢复/重试
│   ├── MafEventTimeline.tsx    # 时间线 feed(MessageBubble + useAutoScroll)
│   ├── MafSeatStatusPanel.tsx  # 席位状态矩阵 + execution_stage 徽标
│   ├── MafFactFlowPanel.tsx    # 包一层 FlowGraphPanel,数据来自 fact-flow-graph
│   ├── MafApprovalCard.tsx     # 卡片式审批(批准/拒绝 + 备注)
│   └── MafHealthGate.tsx       # health.enabled=false → 空态
├── pages/
│   ├── MafSessionsPage.tsx     # 会话列表 + 新建
│   └── MafRunDetailPage.tsx    # 单 run 工作区(三栏)
└── index.ts

核心:cursor 轮询 hook(hooks/use-maf-events.ts)

不用纯 useQuery+refetchInterval:cursor after_seq 需每次推进,queryFn 不持有「上次 latest_seq」,塞 queryKey 会抖动。用自定义 hook(结合 scheduled-tasks 的 refetchInterval 思路 + roundtable-planning 的 reader-loop 家族):

export function useMafEvents(runId?: string, active=false) {
  const [events, setEvents] = useState<MafEvent[]>([]);
  const afterSeqRef = useRef(0);                  // 避免 setInterval 闭包旧值
  useEffect(() => {
    if (!runId || !active) return;
    let cancelled = false;
    const poll = async () => {
      const res = await listEvents(runId, afterSeqRef.current);
      if (cancelled || !res.events.length) return;
      setEvents(prev => mergeBySeq(prev, res.events));  // seq 去重
      afterSeqRef.current = res.latest_seq;
    };
    poll();                                        // 进入即补拉(after_seq=0 全量历史)
    const id = setInterval(poll, 1800);
    return () => { cancelled = true; clearInterval(id); };
  }, [runId, active]);
  return events;
}
  • 补拉历史:进入页面 after_seq=0 立即拉全部已落库事件 → 天然支持断线补拉/刷新恢复。
  • 终态停止:active 由 useMafRunStatus 的 runIsLive(status) 传入,终态时 effect 清理 interval。
  • 去重:后端 (run_id,seq) + source_event_id 双重去重,前端再按 seq 做 mergeBySeq,幂等。
  • 切换 run:runId 变 → effect 重置(events 清空、afterSeq 归零)。

流程图数据流(lib/fact-flow-graph.ts)

两源按优先级合并 → FlowGraphData:

  1. confirmed_facts(主):每条已确认事实 → task 节点(label=text 截断、detail=全文、group=confirmed_by);evidence_refs 连到 evidence(→ role)或 tool_results(tool:{name}:{turn} → deliverable)节点;证据根用 start。
  2. final_result.content 的 flow-json(次):用现成 extractFlowGraph(text) 解析叠加为「结论流程图」。

输出喂 <FlowGraphPanel data={...} streaming={!terminal}/>(非终态蚂蚁线动)。两源皆空 → 空态「尚无可绘制事实」。

页面布局(MafRunDetailPage,三栏 / 窄屏堆叠)

  • 左:时间线(MafEventTimeline)—— event-timeline.ts 折叠成可读条目(delta 聚合气泡、tool 折叠「调用 X→完成」、action.waiting_approval 插锚点),复用 MessageBubble + useAutoScroll + ScrollToBottomButton。
  • 中上:席位状态(MafSeatStatusPanel)—— maf_checkpoint.seat_outcomes 的 agent × status 矩阵 + execution_stage 徽标。
  • 中下:事实/结论流程图(MafFactFlowPanel)—— 折叠面板,默认展开。
  • 右:审批卡片(MafApprovalCard 列表)—— 仅 pending 时显示;含工具名、参数摘要(脱敏)、批准/拒绝按钮 + 备注 textarea → POST /approvals/{id}/decision,成功后 invalidate ["maf-roundtable","approvals",runId],事件轮询自然看到 action.approved/rejected。

路由 + 菜单接入(3 处)

  1. 新建 pages/MafRoutes.tsx(仿 RoundtableRoutes.tsx):index→MafSessionsPage、runs/:runId→MafRunDetailPage、*→重定向。
  2. 改 pages/PageRoutes.tsx:加 import MafRoutes + <Route path="maf/*" element={<MafRoutes/>}/>(插 notebook 后)。
  3. 改 core/page-layout/sidebar-menu.ts:数组末尾加 { id:"maf-center", label:"MAF 会商", icon:Network, children:[{id:"maf-roundtable", label:"会商列表", icon:MessageSquareText, path:"/page/maf"}] }。不加 adminOnly(全员可见;/health.enabled=false 由 MafHealthGate 客户端控制页面空态)。

实施顺序

  1. api/types.ts + api/client.ts(地基)
  2. api/{sessions,runs,events,approvals}.ts(端点 hook)
  3. hooks/{use-maf-run-status,use-maf-events}.ts(轮询核心)
  4. lib/{event-timeline,fact-flow-graph}.ts(纯函数)
  5. components/*(6 个组件)
  6. pages/*(2 个页面)+ index.ts
  7. pages/MafRoutes.tsx + 接入 PageRoutes.tsx + sidebar-menu.ts
  8. pnpm typecheck + 手动核对路由/菜单显隐

文件清单

  • 新建(14):maf-roundtable/api/{types,client,sessions,runs,events,approvals}.ts、maf-roundtable/hooks/{use-maf-run-status,use-maf-events}.ts、maf-roundtable/lib/{event-timeline,fact-flow-graph}.ts、maf-roundtable/components/{MafRunHeader,MafEventTimeline,MafSeatStatusPanel,MafFactFlowPanel,MafApprovalCard,MafHealthGate}.tsx、maf-roundtable/pages/{MafSessionsPage,MafRunDetailPage}.tsx、maf-roundtable/index.ts、pages/MafRoutes.tsx
  • 改动(2):pages/PageRoutes.tsx、core/page-layout/sidebar-menu.ts
  • 后端:无改动

验证

  • pnpm typecheck(tsc --noEmit)通过
  • 手动:菜单「MAF 会商」→ 列表 → 新建会话 → 启动 run → 时间线滚动 → 席位状态更新 → 流程图随 confirmed_facts 增长 → 审批可批准 → 终态停轮询
  • 退路:/health.enabled=false → 入口可见但页面显示未启用提示;旧 roundtable-planning 零改动