// 适配层:把真实 API 响应(src/model 的 camelCase 行)映射成各 surface 期望的形状 // (即 design/ui_kits/console/data.js 的 mock 字段名)。纯函数,便于测试与替换。 const ARCHIVE_STATUSES = new Set(['done', 'cancelled']); const GATE_STATUSES = new Set(['plan_review', 'spec_review', 'exec_review']); // 项目名 → 稳定色相(0..360),与 mock 的 hue 字段对齐 export function hueFor(name) { let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360; return h; } const prio = (n) => 'P' + (n ?? 2); const shortPath = (p) => (p || '').replace(/^\/Users\/[^/]+/, '~'); // ── 项目 ─────────────────────────────────────────────── export function adaptProject(p) { const s = p.summary || {}; let state = 'idle'; if (p.status === 'paused') state = 'paused'; else if (s.executing > 0) state = 'running'; else if (s.blocked > 0) state = 'blocked'; else if (s.alive > 0) state = 'running'; return { id: p.id, name: p.name, path: shortPath(p.repoPath), branch: p.defaultBranch, autonomy: p.autonomy, concurrency: p.concurrency, hue: hueFor(p.name), state, pending: s.pending || 0, agents: s.executing || 0, // 透传给 ConfigPanel 用的原始字段 model: p.model, verifyCmd: p.verifyCmd, maxRetries: p.maxRetries, repoPath: p.repoPath, logo: p.logo, budgetUsd: p.budgetUsd ?? null, budgetPeriod: p.budgetPeriod ?? 'month', }; } // ── 任务树(扁平 → 嵌套 children)────────────────────── function mapTask(tk) { return { id: tk.id, title: tk.title, cplx: tk.complexity, status: tk.status, prio: prio(tk.priority), deps: tk.deps || [], doc: tk.spec || tk.plan || undefined, ops: tk.operations || undefined, children: [], _raw: tk, }; } // 返回 { active: 顶层活跃任务树, archived: 终态任务卡片[] } export function buildTaskTree(flat) { const byId = new Map(); flat.forEach((tk) => byId.set(tk.id, mapTask(tk))); const roots = []; byId.forEach((node) => { const pid = node._raw.parentId; if (pid && byId.has(pid)) byId.get(pid).children.push(node); else roots.push(node); }); // 子节点按优先级(P0 在前)再按创建时间排序 const sortKids = (n) => { n.children.sort((a, b) => a.prio.localeCompare(b.prio) || a._raw.createdAt.localeCompare(b._raw.createdAt)); n.children.forEach(sortKids); }; roots.forEach(sortKids); const active = []; const archived = []; for (const r of roots) { const hasLiveDescendant = (n) => !ARCHIVE_STATUSES.has(n.status) || n.children.some(hasLiveDescendant); if (ARCHIVE_STATUSES.has(r.status) && !r.children.some(hasLiveDescendant)) { archived.push(adaptArchive(r)); } else { active.push(r); } } active.sort((a, b) => a.prio.localeCompare(b.prio) || a._raw.createdAt.localeCompare(b._raw.createdAt)); archived.sort((a, b) => b.timeFull.localeCompare(a.timeFull)); return { active, archived }; } // ── 审批闸卡片 ───────────────────────────────────────── export function adaptApproval(tk, projName) { const gate = tk.status.replace('_review', ''); // plan | spec | exec let doc; if (gate === 'plan') doc = tk.plan; else if (gate === 'spec') doc = tk.spec; else doc = (tk.result && (tk.result.diffSummary || JSON.stringify(tk.result, null, 2))) || tk.operations; return { id: tk.id, gate, taskId: tk.id, title: tk.title, meta: `${projName || ''} · ${(tk.complexity || '').toUpperCase()}`, doc: doc || '(无可预览内容)', }; } // ── 事件流 ───────────────────────────────────────────── const TIME = (iso) => { try { return new Date(iso).toTimeString().slice(0, 8); } catch { return ''; } }; export function adaptEvent(e) { const p = e.payload || {}; let detail = ''; switch (e.type) { case 'status.changed': detail = `${p.from} → ${p.to}` + (p.by ? ` · ${p.by}` : ''); break; case 'run.started': detail = `${p.kind || 'run'} · ${p.runId || ''}`; break; case 'run.finished': detail = `${p.runId || ''} · ${p.status || ''}`; break; case 'approval.granted': case 'approval.rejected': detail = `${p.gate || ''}` + (p.reason ? ` ↳ ${p.reason}` : ''); break; case 'task.created': detail = p.title || e.taskId || ''; break; case 'project.synced': detail = `导入 ${p.created ?? 0} · 完成 ${p.done ?? 0}`; break; default: detail = JSON.stringify(p).slice(0, 80); } return { type: e.type, time: TIME(e.at), detail }; } // ── 用量 / 配额 ──────────────────────────────────────── const resetIn = (iso) => { if (!iso) return '—'; const ms = new Date(iso).getTime() - Date.now(); if (isNaN(ms) || ms <= 0) return '0m'; const h = Math.floor(ms / 3.6e6), m = Math.floor((ms % 3.6e6) / 6e4); return h > 0 ? `${h}h ${m}m` : `${m}m`; }; export function adaptQuota(u) { return { five: { pct: u?.session?.percent ?? 0, reset: resetIn(u?.session?.resetsAt) }, week: { pct: u?.weekly?.percent ?? 0, reset: resetIn(u?.weekly?.resetsAt) }, }; } // ── 全局概览(从 projects + agents 派生)─────────────── export function deriveGlobal(projects, agentsResp) { const running = projects.filter((p) => p.state === 'running').length; const maxAgents = projects.reduce((s, p) => s + (p.concurrency || 0), 0); return { projectCount: projects.length, runningProjects: running, runningAgents: agentsResp?.totalActive ?? 0, maxAgents, autonomy: projects[0]?.autonomy || 'manual', daemon: 'live', port: 4517, uptime: '—', }; } // 跨项目 agent 概览。token/成本来自 /api/usage 的 cost 明细(按当期窗口聚合)。 export function deriveAgentSummary(projects, agentsResp, cost) { const byPid = new Map((cost?.byProject || []).map((c) => [c.projectId, c])); const tokensTotal = (cost?.byProject || []).reduce((sum, c) => sum + (c.tokens || 0), 0); return { tokensWeek: tokensTotal, runsWeek: 0, costWeek: cost?.total || 0, activeNow: agentsResp?.totalActive ?? 0, byProject: projects.map((p) => { const c = byPid.get(p.id); return { id: p.id, name: p.name, hue: p.hue, active: p.agents || 0, runs: 0, tokens: c?.tokens || 0, cost: c?.costUsd || 0, }; }), }; } // 当前项目正在跑的 agent run(AgentSection 用) export function adaptActiveAgents(agentsResp, projectId) { const out = []; for (const a of agentsResp?.agents || []) { if (projectId && a.projectId !== projectId) continue; for (const r of a.active || []) { out.push({ id: r.runId || r.id, taskId: r.taskId, project: a.projectName, title: r.title || r.taskTitle || r.taskId || '(运行中)', kind: (r.kind || 'run').toUpperCase(), time: TIME(r.startedAt || r.at), meta: `${r.worktree || ''} · ${r.model || ''}`, }); } } return out; } // ── 归档任务详情 ─────────────────────────────────────── function adaptArchive(node) { const tk = node._raw; const cplxU = (tk.complexity || '').toUpperCase(); const statusZh = tk.status === 'done' ? '完成' : '取消'; const attrs = [ ['id', tk.id], ['复杂度', cplxU], ['优先级', node.prio], ['状态', statusZh], ['深度', String(tk.depth ?? 1)], ['创建时间', (tk.createdAt || '').replace('T', ' ').slice(0, 19)], ['最后更新', (tk.updatedAt || '').replace('T', ' ').slice(0, 19)], ]; const detail = { attrs }; if (tk.spec) detail.spec = tk.spec; if (tk.operations) detail.ops = tk.operations; if (tk.result) { detail.result = { branch: tk.result.branch, commits: tk.result.commits || [], diff: tk.result.diffSummary ? tk.result.diffSummary.split('\n').slice(-1)[0] : undefined, }; } detail.approvals = (tk.approvals || []).map((a) => ({ gate: a.gate, action: a.action, actor: a.actor, at: (a.at || '').replace('T', ' ').slice(0, 19), reason: a.reason, })); detail.runs = []; detail.timeline = [{ time: (tk.createdAt || '').slice(5, 16).replace('T', ' '), text: '任务创建', who: 'you' }]; return { id: tk.id, title: tk.title, cplx: tk.complexity, status: tk.status, subs: node.children.length, time: relTime(tk.updatedAt), timeFull: (tk.updatedAt || '').replace('T', ' ').slice(0, 19), detail, }; } function relTime(iso) { if (!iso) return ''; const ms = Date.now() - new Date(iso).getTime(); const h = Math.floor(ms / 3.6e6); if (h < 1) return '刚刚'; if (h < 24) return `${h} 小时前`; return `${Math.floor(h / 24)} 天前`; }