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>
This commit is contained in:
董宏凯andCursor committed 2026-08-02 13:07:21 +08:00
1 parent 21c4c898e7
commit f82363d81f
55 files changed
+6304 -167

No files matched your search

@@ -0,0 +1,58 @@
/**
* MAF 多智能体会商 API 客户端地基。
*
* 复用全局 `apiFetch`(自动注入 Authorization + credentials:include),这里只做:
* - 拼 URL(`${getBackendBaseURL()}/api/maf-roundtable/...`)
* - 统一 `res.ok` 检查 + detail 提取(仿 task-buttons 的 readJson)
* - JSON 解析
*
* 不在此做 snake_case ⇄ camelCase 转换——后端即 snake_case,类型层直传(见 types.ts)。
*/
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
/** MAF roundtable 后端根路径(无尾斜杠)。 */
export const BASE = (): string => `${getBackendBaseURL()}/api/maf-roundtable`;
/**
* 检查 Response 并解析 JSON;失败时抛出带后端 `detail` 的 Error。
*
* - 404 = 不存在或非本人(后端统一返回 404 防止泄漏他人资源存在性)
* - 403 = 非所有者写(admin 也不能写别人的)
* - 503 = 功能关闭 / store 未就绪
*
* 这里不区分状态码语义,统一把 detail 透传给上层 toast/错误展示。
*/
export async function readJson<T>(res: Response, ctx: string): Promise<T> {
if (!res.ok) {
const data = await res.json().catch(() => null);
const detail =
data && typeof data === "object" && "detail" in data
? String((data as { detail: unknown }).detail)
: `${ctx} 失败(HTTP ${res.status})`;
throw new Error(detail);
}
return (await res.json()) as T;
}
/** GET JSON 的快捷封装。 */
export async function getJson<T>(url: string, ctx: string): Promise<T> {
const res = await apiFetch(url, { method: "GET" });
return readJson<T>(res, ctx);
}
/** 发送 JSON body 的快捷封装。 */
export async function sendJson<T>(
url: string,
method: "POST" | "PUT" | "DELETE" | "PATCH",
body: unknown,
ctx: string,
): Promise<T> {
const res = await apiFetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
return readJson<T>(res, ctx);
}