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:
@@ -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(); };
|
||||
}
|
||||
Reference in New Issue
Block a user