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

122 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 家族):
```ts
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` 零改动