8765e61498
任务树:四维多选下拉筛选(状态/复杂度/优先级/执行者)、已选置顶、新建任务按钮移右上角(0任务也能建)。 四块主区(Agent/审批闸/任务树/归档)统一标题组件(图标+名称+数量徽标)+ 折叠按钮(localStorage 记忆)。 侧栏:全局 Agent 弹层改常驻卡片(项目默认3+折叠+按活跃排序)、顶部项目拖拽排序、全局设置移入用户菜单弹框(保存/取消,落库)、记忆当前项目。 adaptProject 用后端 summary 的 running/attention 判状态点颜色(青/琥珀/灰)+ 紫色关注数徽标;agent 标题不再越界;i18n 5 语言补全。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
3.4 KiB
JavaScript
81 lines
3.4 KiB
JavaScript
// 真实数据层:直连本地 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),
|
||
reorderProjects: (order) => http('POST', '/api/projects/reorder', { order }),
|
||
createProject: (body) => http('POST', '/api/projects', body),
|
||
getSettings: () => http('GET', '/api/settings'),
|
||
putSettings: (data) => http('PUT', '/api/settings', data),
|
||
takeover: (taskId) => http('POST', `/api/tasks/${taskId}/takeover`),
|
||
patchTask: (taskId, body) => http('PATCH', `/api/tasks/${taskId}`, body),
|
||
cancelTask: (taskId) => http('POST', `/api/tasks/${taskId}/cancel`),
|
||
requeueTask: (taskId) => http('POST', `/api/tasks/${taskId}/requeue`),
|
||
deleteTask: (taskId) => http('DELETE', `/api/tasks/${taskId}`),
|
||
// 附件上传走 multipart(不能用 JSON helper,content-type 由浏览器带 boundary)
|
||
uploadAttachments: async (taskId, files) => {
|
||
const fd = new FormData();
|
||
for (const f of files) fd.append('files', f, f.name);
|
||
const res = await fetch(`/api/tasks/${taskId}/attachments`, { method: 'POST', body: fd });
|
||
if (!res.ok) {
|
||
let msg = res.statusText;
|
||
try { msg = (await res.json()).error || msg; } catch { /* 非 JSON */ }
|
||
throw new Error(`上传附件失败 → ${res.status} ${msg}`);
|
||
}
|
||
return res.json();
|
||
},
|
||
};
|
||
|
||
// 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(); };
|
||
}
|