- 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>
11 KiB
11 KiB
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:
confirmed_facts(主):每条已确认事实 →task节点(label=text 截断、detail=全文、group=confirmed_by);evidence_refs连到evidence(→role)或tool_results(tool:{name}:{turn}→deliverable)节点;证据根用start。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 处)
- 新建
pages/MafRoutes.tsx(仿RoundtableRoutes.tsx):index→MafSessionsPage、runs/:runId→MafRunDetailPage、*→重定向。 - 改
pages/PageRoutes.tsx:加import MafRoutes+<Route path="maf/*" element={<MafRoutes/>}/>(插notebook后)。 - 改
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客户端控制页面空态)。
实施顺序
api/types.ts+api/client.ts(地基)api/{sessions,runs,events,approvals}.ts(端点 hook)hooks/{use-maf-run-status,use-maf-events}.ts(轮询核心)lib/{event-timeline,fact-flow-graph}.ts(纯函数)components/*(6 个组件)pages/*(2 个页面)+index.tspages/MafRoutes.tsx+ 接入PageRoutes.tsx+sidebar-menu.tspnpm 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零改动