From d6ae4dbe1386e559d111fe209619566e37d2b7f8 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 24 Jun 2026 14:29:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E9=98=B6=E6=AE=B52=20=E6=8E=A5?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E6=95=B0=E6=8D=AE=E2=80=94=E2=80=94REST=20+?= =?UTF-8?q?=20WS=20=E7=9B=B4=E8=BF=9E=E6=9C=AC=E5=9C=B0=20daemon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前端从 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 --- app/src/adapt.js | 205 +++++++++++++++++++++++++++++++++++++++++++++ app/src/api.js | 59 +++++++++++++ app/src/app.jsx | 159 +++++++++++++++++++++++++---------- app/src/main.jsx | 3 +- app/vite.config.js | 8 +- 5 files changed, 387 insertions(+), 47 deletions(-) create mode 100644 app/src/adapt.js create mode 100644 app/src/api.js diff --git a/app/src/adapt.js b/app/src/adapt.js new file mode 100644 index 0000000..3e16b6f --- /dev/null +++ b/app/src/adapt.js @@ -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 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, 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)} 天前`; +} diff --git a/app/src/api.js b/app/src/api.js new file mode 100644 index 0000000..8056f70 --- /dev/null +++ b/app/src/api.js @@ -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(); }; +} diff --git a/app/src/app.jsx b/app/src/app.jsx index 3c30684..ebfac06 100644 --- a/app/src/app.jsx +++ b/app/src/app.jsx @@ -1,25 +1,39 @@ -// 从 design/ui_kits/console/index.html 的 App() 移植。surface 组件来自全局 -// window.MaestroKit*(由各 .jsx 注册);mock 数据/文案来自 window.MAESTRO_MOCK/I18N。 -// 下一步:把 M(mock)替换成真实 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() { ); - 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 (
{!collapsed ? : null} {!evCollapsed ? : null} - toast('warn', t.toastModal)} + toast('warn', t.toastModal)} collapsed={collapsed} onToggleCollapse={toggleCollapse} onSelect={setCurrentId} onNewProject={() => toast('warn', t.toastModal)} />
- 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 ? setShowConfig(!showConfig)} /> : null} + {showConfig && project ? { setShowConfig(false); toast('ok', t.toastSaved); }} - onSync={() => toast('ok', t.toastSynced)} + onSync={onSync} onClose={() => setShowConfig(false)} /> : null} - + {tasks.length ? ( @@ -125,7 +196,7 @@ export function App() { {t.emptyTasks}
)} - {isMain ? : null} + {archiveItem ? setArchiveItem(null)} /> : null} diff --git a/app/src/main.jsx b/app/src/main.jsx index cc416d5..04fc782 100644 --- a/app/src/main.jsx +++ b/app/src/main.jsx @@ -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/ 源文件不动,作为单一真相源) diff --git a/app/vite.config.js b/app/vite.config.js index d651e22..5e2a929 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -10,14 +10,20 @@ export default defineConfig({ resolve: { alias: { '@ds': path.resolve(__dirname, '../design') }, }, + // surface/.jsx 与 app.jsx 统一走全局 React(globals.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 }, + }, }, });