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
+205
View File
@@ -0,0 +1,205 @@
// 适配层:把真实 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,
};
}
// ── 任务树(扁平 → 嵌套 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(预算特性),暂留 0。
export function deriveAgentSummary(projects, agentsResp) {
return {
tokensWeek: 0, runsWeek: 0, costWeek: 0, activeNow: agentsResp?.totalActive ?? 0,
byProject: projects.map((p) => ({
id: p.id, name: p.name, hue: p.hue,
active: p.agents || 0, runs: 0, tokens: 0,
})),
};
}
// 当前项目正在跑的 agent runAgentSection 用)
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, 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)} 天前`;
}
+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(); };
}
+115 -44
View File
@@ -1,25 +1,39 @@
// 从 design/ui_kits/console/index.html 的 App() 移植。surface 组件来自全局
// window.MaestroKit*(由各 .jsx 注册);mock 数据/文案来自 window.MAESTRO_MOCK/I18N
// 下一步:把 Mmock)替换成真实 REST + WS 数据源(按 ⑧ 前端 API 契约)。
// 真实数据版 App:从本地 daemon 的 REST 拉取、订阅 WS 增量刷新,
// 通过 src/adapt.js 映射成各 surface 期望的形状(替代 window.MAESTRO_MOCK
import { createRoot } from 'react-dom/client';
import { api, subscribeEvents } from './api.js';
import {
adaptProject, buildTaskTree, adaptApproval, adaptEvent, adaptQuota,
deriveGlobal, deriveAgentSummary, adaptActiveAgents,
} from './adapt.js';
const { Toast } = window.MaestroDesignSystem_a6a290;
const USER = { name: 'local', handle: '@local', plan: 'Claude Code', initial: 'L', hue: 200 };
export function App() {
const M = window.MAESTRO_MOCK;
const I18N = window.MAESTRO_I18N;
const [currentId, setCurrentId] = React.useState('p1');
const [approvals, setApprovals] = React.useState(M.approvals);
const [events, setEvents] = React.useState(M.events);
const [showConfig, setShowConfig] = React.useState(false);
const [archiveItem, setArchiveItem] = React.useState(null);
const [toasts, setToasts] = React.useState([]);
const [lang, setLang] = React.useState(() => {
const saved = localStorage.getItem('maestro-kit-lang');
return I18N[saved] ? saved : 'zh';
});
const [theme, setTheme] = React.useState(() => localStorage.getItem('maestro-kit-theme') || 'dark');
const t = I18N[lang];
// ── 真实数据 state ──────────────────────────────────
const [projectsRaw, setProjectsRaw] = React.useState([]);
const [tasksRaw, setTasksRaw] = React.useState([]);
const [approvalsRaw, setApprovalsRaw] = React.useState([]);
const [eventsRaw, setEventsRaw] = React.useState([]);
const [agentsResp, setAgentsResp] = React.useState({ totalActive: 0, agents: [] });
const [usage, setUsage] = React.useState(null);
const [currentId, setCurrentId] = React.useState(null);
const [wsState, setWsState] = React.useState('closed');
// ── UI state(沿用原型)─────────────────────────────
const [showConfig, setShowConfig] = React.useState(false);
const [archiveItem, setArchiveItem] = React.useState(null);
const [toasts, setToasts] = React.useState([]);
const [theme, setTheme] = React.useState(() => localStorage.getItem('maestro-kit-theme') || 'dark');
React.useEffect(() => {
document.documentElement.dataset.theme = theme;
localStorage.setItem('maestro-kit-theme', theme);
@@ -27,14 +41,8 @@ export function App() {
React.useEffect(() => { localStorage.setItem('maestro-kit-lang', lang); }, [lang]);
const [collapsed, setCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-sidebar') === 'collapsed');
const [evCollapsed, setEvCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-events') === 'collapsed');
const toggleEvents = () => setEvCollapsed((c) => {
localStorage.setItem('maestro-kit-events', c ? 'open' : 'collapsed');
return !c;
});
const toggleCollapse = () => setCollapsed((c) => {
localStorage.setItem('maestro-kit-sidebar', c ? 'open' : 'collapsed');
return !c;
});
const toggleEvents = () => setEvCollapsed((c) => { localStorage.setItem('maestro-kit-events', c ? 'open' : 'collapsed'); return !c; });
const toggleCollapse = () => setCollapsed((c) => { localStorage.setItem('maestro-kit-sidebar', c ? 'open' : 'collapsed'); return !c; });
const SIDE_MIN = 200, SIDE_MAX = 420, EV_MIN = 240, EV_MAX = 520, CENTER_MIN = 480;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const sideMax = () => Math.min(SIDE_MAX, window.innerWidth - (evCollapsed ? 52 : evW) - CENTER_MIN);
@@ -73,49 +81,112 @@ export function App() {
<span style={{ width: 1, background: dragging === which ? 'var(--green)' : 'var(--line-soft)', boxShadow: dragging === which ? '0 0 8px var(--green)' : 'none', transition: 'background .12s' }}></span>
</div>
);
const project = M.projects.find((p) => p.id === currentId);
const isMain = currentId === 'p1';
const tasks = isMain ? M.tasks : [];
const agents = isMain ? M.agents : [];
const gates = isMain ? approvals : [];
// ── 数据加载 ────────────────────────────────────────
const toast = (kind, text) => {
const id = Date.now();
const id = Date.now() + Math.random();
setToasts((list) => [...list, { id, kind, text }]);
setTimeout(() => setToasts((list) => list.filter((x) => x.id !== id)), 2600);
};
const now = () => new Date().toTimeString().slice(0, 8);
const decide = (a, action, reason) => {
setApprovals((list) => list.filter((x) => x.id !== a.id));
if (action === 'accept') {
setEvents((ev) => [{ type: 'approval.granted', time: now(), detail: a.title + ' · ' + a.gate + '_review' + t.gatePassed }, ...ev]);
toast('ok', t.toastAccepted + a.title);
} else {
setEvents((ev) => [{ type: 'approval.rejected', time: now(), detail: a.title + ' ↳ ' + reason }, ...ev]);
toast('warn', t.toastRejected);
const loadGlobal = React.useCallback(async () => {
try {
const [pj, ag, us, ap] = await Promise.all([api.listProjects(), api.agents(), api.usage(), api.approvals()]);
setProjectsRaw(pj); setAgentsResp(ag); setUsage(us); setApprovalsRaw(ap);
setCurrentId((cur) => cur || (pj[0] && pj[0].id) || null);
} catch (e) { toast('warn', '加载失败:' + e.message); }
}, []);
const loadProject = React.useCallback(async (pid) => {
if (!pid) return;
try {
const [tasks, events] = await Promise.all([api.projectTasks(pid), api.projectEvents(pid)]);
setTasksRaw(tasks);
setEventsRaw(events.slice(0, 60));
} catch (e) { toast('warn', '加载任务失败:' + e.message); }
}, []);
React.useEffect(() => { loadGlobal(); }, [loadGlobal]);
React.useEffect(() => { loadProject(currentId); }, [currentId, loadProject]);
// WS:增量刷新。事件即时入流;任务/审批/agent 做轻量去抖刷新。
const refreshTimer = React.useRef(null);
React.useEffect(() => {
const unsub = subscribeEvents((evt) => {
if (!evt || !evt.type) return;
if (!evt.projectId || evt.projectId === currentId) {
setEventsRaw((list) => [evt, ...list].slice(0, 60));
}
clearTimeout(refreshTimer.current);
refreshTimer.current = setTimeout(() => {
loadProject(currentId);
api.approvals().then(setApprovalsRaw).catch(() => {});
api.agents().then(setAgentsResp).catch(() => {});
api.listProjects().then(setProjectsRaw).catch(() => {});
}, 350);
}, setWsState);
return () => { unsub(); clearTimeout(refreshTimer.current); };
}, [currentId, loadProject]);
// ── 派生:映射成 surface 形状 ───────────────────────
const projects = projectsRaw.map(adaptProject);
const project = projects.find((p) => p.id === currentId) || projects[0];
const projNameOf = (pid) => (projectsRaw.find((p) => p.id === pid) || {}).name;
const { active: tasks, archived } = buildTaskTree(tasksRaw);
const REVIEW = new Set(['plan_review', 'spec_review', 'exec_review']);
const gates = approvalsRaw
.filter((a) => a.projectId === currentId && REVIEW.has(a.status))
.map((a) => adaptApproval(a, projNameOf(a.projectId)));
const agents = adaptActiveAgents(agentsResp, currentId);
const events = eventsRaw.map(adaptEvent);
const quota = adaptQuota(usage);
const global = deriveGlobal(projects, agentsResp);
const agentSummary = deriveAgentSummary(projects, agentsResp);
const NONTERMINAL = (s) => !['done', 'cancelled', 'decomposed'].includes(s);
const counts = {
gate: gates.length,
ready: tasksRaw.filter((x) => x.projectId === currentId && (x.status === 'ready' || x.status === 'queued')).length,
run: tasksRaw.filter((x) => x.projectId === currentId && x.status === 'executing').length,
blocked: tasksRaw.filter((x) => x.projectId === currentId && x.status === 'blocked').length,
total: tasksRaw.filter((x) => x.projectId === currentId && NONTERMINAL(x.status)).length,
};
// ── 动作(真实写入)────────────────────────────────
const decide = async (a, action, reason) => {
setApprovalsRaw((list) => list.filter((x) => x.id !== a.id)); // 乐观移除
try {
await api.decide(a.id, action, reason);
toast(action === 'accept' ? 'ok' : 'warn', (action === 'accept' ? t.toastAccepted : t.toastRejected) + (action === 'accept' ? a.title : ''));
} catch (e) {
toast('warn', '裁决失败:' + e.message);
api.approvals().then(setApprovalsRaw).catch(() => {}); // 回滚
}
};
const onSync = async () => {
try { await api.syncProject(currentId); toast('ok', t.toastSynced); loadProject(currentId); loadGlobal(); }
catch (e) { toast('warn', '同步失败:' + e.message); }
};
return (
<div style={{ position: 'relative', display: 'grid', gridTemplateColumns: (collapsed ? '52px' : sideW + 'px') + ' minmax(0,1fr) ' + (evCollapsed ? '52px' : evW + 'px'), height: '100vh' }}>
{!collapsed ? <Resizer which="side" /> : null}
{!evCollapsed ? <Resizer which="ev" /> : null}
<window.MaestroKitSidebar projects={M.projects} currentId={currentId} t={t}
global={M.global} user={M.user} summary={M.agentSummary} onGlobalConfig={() => toast('warn', t.toastModal)}
<window.MaestroKitSidebar projects={projects} currentId={currentId} t={t}
global={global} user={USER} summary={agentSummary} onGlobalConfig={() => toast('warn', t.toastModal)}
collapsed={collapsed} onToggleCollapse={toggleCollapse}
onSelect={setCurrentId} onNewProject={() => toast('warn', t.toastModal)} />
<main style={{ overflowY: 'auto', padding: '0 22px 60px' }}>
<window.MaestroKitTopbar project={project} t={t} theme={theme}
{project ? <window.MaestroKitTopbar project={project} t={t} theme={theme}
onToggleTheme={() => setTheme(theme === 'light' ? 'dark' : 'light')}
lang={lang} onSelectLang={setLang}
counts={{ gate: gates.length, ready: isMain ? 1 : 0, run: agents.length, blocked: isMain ? 1 : 0, total: isMain ? 5 : 0 }}
onSync={() => toast('ok', t.toastSynced)}
onToggleConfig={() => setShowConfig(!showConfig)} />
{showConfig ? <window.MaestroKitConfigPanel project={project} t={t}
counts={counts}
onSync={onSync}
onToggleConfig={() => setShowConfig(!showConfig)} /> : null}
{showConfig && project ? <window.MaestroKitConfigPanel project={project} t={t}
onSave={() => { setShowConfig(false); toast('ok', t.toastSaved); }}
onSync={() => toast('ok', t.toastSynced)}
onSync={onSync}
onClose={() => setShowConfig(false)} /> : null}
<window.MaestroKitAgentSection agents={agents} quota={isMain ? M.quota : null} t={t} />
<window.MaestroKitAgentSection agents={agents} quota={quota} t={t} />
<window.MaestroKitGateSection approvals={gates} onDecide={decide} t={t} />
{tasks.length ? (
<window.MaestroKitTaskSection tasks={tasks} onToast={toast} t={t} />
@@ -125,7 +196,7 @@ export function App() {
{t.emptyTasks}
</div>
)}
{isMain ? <window.MaestroKitArchiveSection items={M.archived} t={t} onOpen={setArchiveItem} /> : null}
<window.MaestroKitArchiveSection items={archived} t={t} onOpen={setArchiveItem} />
</main>
<window.MaestroKitEventPanel events={events} collapsed={evCollapsed} onToggleCollapse={toggleEvents} t={t} />
{archiveItem ? <window.MaestroKitArchiveModal item={archiveItem} t={t} onClose={() => setArchiveItem(null)} /> : null}
+1 -2
View File
@@ -4,8 +4,7 @@ import '@ds/styles.css';
import './ambience.css';
import '@ds/_ds_bundle.js';
// ui_kit 数据与文案(暂用设计系统自带 mock;下一步换成真实 REST+WS
import '@ds/ui_kits/console/data.js';
// 文案 i18n(真实数据由 src/api.js + src/adapt.js 提供,不再用 data.js mock
import '@ds/ui_kits/console/i18n.js';
// 六个整屏 surface(保持 design/ 源文件不动,作为单一真相源)
+7 -1
View File
@@ -10,14 +10,20 @@ export default defineConfig({
resolve: {
alias: { '@ds': path.resolve(__dirname, '../design') },
},
// surface/.jsx 与 app.jsx 统一走全局 Reactglobals.js 注入 window.React),
// 不向各模块注入 import——否则生产构建时 rollup 会试图从 design/ 解析 react 而失败。
esbuild: {
jsx: 'transform',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
jsxInject: `import React from 'react'`,
},
server: {
port: 4519,
fs: { allow: [path.resolve(__dirname, '..')] }, // 允许访问仓库内的 design/
// 把 REST + WS 反代到本地 daemon(:4517),客户端用同源相对路径
proxy: {
'/api': { target: 'http://127.0.0.1:4517', changeOrigin: true },
'/ws': { target: 'http://127.0.0.1:4517', ws: true, changeOrigin: true },
},
},
});