# 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`):`}/>`(仿 `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/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 家族): ```ts export function useMafEvents(runId?: string, active=false) { const [events, setEvents] = useState([]); 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)` 解析叠加为「结论流程图」。 输出喂 ``(非终态蚂蚁线动)。两源皆空 → 空态「尚无可绘制事实」。 ## 页面布局(`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` + `}/>`(插 `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` 零改动