feat(app): 阶段2 接真实数据——REST + WS 直连本地 daemon

前端从 mock 切换到真实数据源(按 ⑧ 前端 API 契约,复用现有 :4517 daemon API):

- src/api.js:REST 封装 + WS 单向订阅(断线指数退避重连)
- src/adapt.js:纯映射层,把 API 的 camelCase 行映射成各 surface 期望形状
  (扁平任务→children 树、priority 整数→P0/1/2、complexity→cplx、终态→归档卡)
- src/app.jsx:改为 useEffect 拉取 + WS 增量刷新;decide/sync 走真实 POST;
  审批闸收紧到 plan/spec/exec_review 三态(needs_attention 仍在任务树可见)
- vite.config.js:加 /api + /ws 反代到 :4517;去掉 jsxInject(surface 走全局
  React,避免生产构建 rollup 从 design/ 解析 react 失败)
- main.jsx:移除 data.js mock 导入

验证:dev + 生产构建双通过;真实数据渲染(4 项目/审批闸带 diff/真实配额),
WS 已连接,0 console 错误。token/成本类(agentSummary)属 ⑧ ★ 新 /api/usage
(预算特性),暂留空待补。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 14:29:19 +08:00
parent 1fcd811c12
commit d6ae4dbe13
5 changed files with 387 additions and 47 deletions
+59
View File
@@ -0,0 +1,59 @@
// 真实数据层:直连本地 maestro daemon 的 REST + WS(按 ⑧ 前端 API 契约)。
// 路径用同源相对地址,dev 下经 vite.config.js 的 proxy 反代到 :4517。
async function http(method, path, body) {
const opt = { method, headers: {} };
if (body !== undefined) {
opt.headers['Content-Type'] = 'application/json';
opt.body = JSON.stringify(body);
}
const res = await fetch(path, opt);
if (!res.ok) {
let msg = res.statusText;
try { msg = (await res.json()).error || msg; } catch { /* 非 JSON 错误体 */ }
throw new Error(`${method} ${path}${res.status} ${msg}`);
}
if (res.status === 204) return null;
const ct = res.headers.get('content-type') || '';
return ct.includes('application/json') ? res.json() : res.text();
}
export const api = {
listProjects: () => http('GET', '/api/projects'),
projectTasks: (pid) => http('GET', `/api/projects/${pid}/tasks`),
projectEvents: (pid) => http('GET', `/api/projects/${pid}/events`),
agents: () => http('GET', '/api/agents'),
approvals: () => http('GET', '/api/approvals'),
usage: () => http('GET', '/api/usage'),
syncProject: (pid) => http('POST', `/api/projects/${pid}/sync`),
decide: (taskId, action, reason, merge) =>
http('POST', `/api/tasks/${taskId}/decide`, { action, reason, merge }),
createTask: (pid, body) => http('POST', `/api/projects/${pid}/tasks`, body),
patchProject: (pid, body) => http('PATCH', `/api/projects/${pid}`, body),
};
// WS 单向订阅:连上后服务端推送 events 表记录。断线自动重连(指数退避,封顶 10s)。
export function subscribeEvents(onEvent, onStatus) {
let ws = null;
let backoff = 500;
let closedByUser = false;
function connect() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${proto}://${location.host}/ws`);
ws.onopen = () => { backoff = 500; onStatus?.('open'); };
ws.onmessage = (msg) => {
try { onEvent(JSON.parse(msg.data)); } catch { /* 忽略非 JSON 帧 */ }
};
ws.onclose = () => {
onStatus?.('closed');
if (closedByUser) return;
setTimeout(connect, backoff);
backoff = Math.min(backoff * 2, 10000);
};
ws.onerror = () => ws.close();
}
connect();
return () => { closedByUser = true; ws?.close(); };
}