/** * APS Electron 主进程(对标 Codex 桌面壳) * - 安装到电脑后用户数据固定:%USERPROFILE%\.aps(同 ~/.codex) * - 含 skills / sessions / data / cache / tmp / logs / packs * - 升级审计与 nonce 续接(round-44 FF ~ round-46 KK):sidecar 一次性审计 token + * nonce 门禁;sidecar 重启后用新 nonce 对 pending 升级重跑健康检查(见 upgrade-nonce.cjs) */ const { app, BrowserWindow, Menu, shell, dialog, ipcMain, session } = require('electron'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const os = require('os'); const { SidecarManager } = require('./sidecar.cjs'); const { Updater, probeHealth } = require('./updater.cjs'); const { createUpgradeNonceCoordinator, UPGRADE_NONCE_EVENT_VERIFIED, UPGRADE_NONCE_EVENT_FAILED } = require('./upgrade-nonce.cjs'); /** 对标 Codex:始终落在用户主目录,安装包不得写到 Program Files */ const USER_APS_HOME = path.join(os.homedir(), '.aps'); const isDev = !app.isPackaged; // 打包安装态强制用户目录;开发态也默认 ~/.aps(可用环境变量覆盖) const APS_HOME = (!isDev || !process.env.APS_HOME) ? USER_APS_HOME : process.env.APS_HOME; process.env.APS_HOME = APS_HOME; process.env.APS_MODE = 'desktop'; const UI_URL = process.env.APS_UI_URL || 'http://localhost:5173'; const isMac = process.platform === 'darwin'; const PACKAGED_CSP = [ "default-src 'self'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "font-src 'self' data:", "connect-src 'self' ws: wss:", "object-src 'none'", "base-uri 'none'", "frame-ancestors 'none'", "form-action 'self'", ].join('; '); let mainWindow = null; let sidecar = null; app.setName('工业智核 APS'); function ensureApsHome() { const layout = ['skills', 'sessions', 'cache', 'tmp', 'logs', 'data', 'packs']; fs.mkdirSync(APS_HOME, { recursive: true }); for (const name of layout) { fs.mkdirSync(path.join(APS_HOME, name), { recursive: true }); } const cfg = path.join(APS_HOME, 'config.json'); if (!fs.existsSync(cfg)) { fs.writeFileSync(cfg, JSON.stringify({ version: 1, mode: 'desktop', layout, note: '对标 Codex ~/.codex', }, null, 2), 'utf8'); } const deviceFile = path.join(APS_HOME, 'device-id'); if (!fs.existsSync(deviceFile)) { fs.writeFileSync(deviceFile, crypto.randomUUID(), { encoding: 'utf8', mode: 0o600 }); } } function getDeviceId() { ensureApsHome(); return fs.readFileSync(path.join(APS_HOME, 'device-id'), 'utf8').trim(); } /** 懒加载升级模块:校验/备份/安装/健康检查/失败自动回滚(audit 本地日志 + 可选上报) */ function createUpdater(options = {}) { return new Updater({ apsHome: APS_HOME, logger: console, ...options, }); } /** 升级 nonce 续接协调器(round-46 KK,矩阵 103 最后本地项): * sidecar 重启后把新 nonce 交给待验证升级,重跑 probeHealth(升级后健康检查 + 版本比对), * 成功上报 audit(带新 nonce)、失败本地 JSONL 留痕。纯逻辑见 upgrade-nonce.cjs, * 此处仅做 Electron 接线:apply-update 前后维护 pending + restarted 事件触发。 */ const upgradeNonceCoordinator = createUpgradeNonceCoordinator({ statePath: path.join(APS_HOME, 'upgrade-state.json'), // round-48:pending 跨进程持久化,应用重启可续接 verifyHealth: ({ origin, nonce, expectedVersion }) => probeHealth({ base: origin, expectedVersion, sidecarNonce: nonce }), onComplete: async ({ pending, health, nonce }) => { // 续接成功:用新 nonce 建 updater 上报 audit(本地 JSONL 主通道 + 可选 /api/gov/audit/events) const updater = createUpdater({ ...(pending.audit || {}), sidecarNonce: nonce, }); await updater.writeAudit({ event: UPGRADE_NONCE_EVENT_VERIFIED, payload: { fromVersion: pending.currentVersion || null, toVersion: pending.targetVersion || null, expectedVersion: pending.expectedVersion || null, version: health?.version ?? null, nonce, verifiedVia: 'sidecar-restart', }, }); }, onFailure: async ({ pending, error }) => { try { const updater = createUpdater({ ...(pending.audit || {}), sidecarNonce: pending.nonce || null, }); await updater.writeAudit({ event: UPGRADE_NONCE_EVENT_FAILED, result: 'FAILED', payload: { fromVersion: pending.currentVersion || null, toVersion: pending.targetVersion || null, expectedVersion: pending.expectedVersion || null, nonce: pending.nonce || null, error: { code: error.code, message: error.message }, }, }); } catch (writeError) { console.error('[upgrade-nonce] 失败留痕写入异常:', writeError); } console.error('[upgrade-nonce] 升级后健康检查失败(新 nonce 续接未通过):', error.message); }, logger: console, }); /** 升级审计通道(round-44 FF):优先使用渲染进程显式传入的 audit 凭据(IPC 显式覆盖); * 否则回落到 sidecar 一次性审计 token(仅限本机、进程内,不外泄,不写盘/不进子进程 env)。 * 返回 Updater 构造参数片段或 null(本地 JSONL 为默认通道)。 */ function resolveAuditOptions(requested = {}) { if (requested.apiUrl && requested.token) { return { auditApiUrl: requested.apiUrl, auditToken: requested.token }; } const origin = sidecar?.origin || process.env.APS_API_URL || null; const token = sidecar?.auditToken || null; if (!origin || !token) return null; return { auditApiUrl: origin, auditToken: token, ...(sidecar?.nonce ? { sidecarNonce: sidecar.nonce } : {}), }; } async function startBundledApi() { if (isDev) return process.env.APS_API_URL || 'http://127.0.0.1:8000'; sidecar = new SidecarManager({ resourcesPath: process.resourcesPath, apsHome: APS_HOME, appVersion: app.getVersion(), }); installSidecarRequestGuard(); sidecar.on('restarting', () => suspendPackagedWindows()); sidecar.on('restarted', ({ origin }) => { process.env.APS_API_URL = origin; reloadPackagedWindows(origin); // round-46 KK:若有待验证升级(pendingUpgrade status='verifying'),用新 sidecar nonce // 重跑升级后健康检查(版本比对):成功 → 上报 audit(带新 nonce);失败 → 本地 JSONL 留痕。 void upgradeNonceCoordinator.onSidecarRestarted({ origin, nonce: sidecar.nonce, }).catch((error) => { console.error('[upgrade-nonce] restarted 续接异常:', error); }); }); sidecar.on('fatal', (error) => { dialog.showErrorBox( 'APS local service stopped', `${error.message}\n\nLog: ${path.join(APS_HOME, 'logs', 'sidecar.log')}`, ); app.quit(); }); const apiUrl = await sidecar.start(); process.env.APS_API_URL = apiUrl; return apiUrl; } function installSidecarRequestGuard() { const desktopSession = session.defaultSession; desktopSession.webRequest.onBeforeSendHeaders((details, callback) => { const origin = sidecar?.origin; if (origin && details.url.startsWith(`${origin}/`) && sidecar.nonce) { details.requestHeaders['x-aps-sidecar-nonce'] = sidecar.nonce; } callback({ requestHeaders: details.requestHeaders }); }); desktopSession.webRequest.onHeadersReceived((details, callback) => { const origin = sidecar?.origin; if (!origin || !details.url.startsWith(`${origin}/`)) { callback({ responseHeaders: details.responseHeaders }); return; } callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [PACKAGED_CSP], }, }); }); desktopSession.setPermissionRequestHandler((_webContents, _permission, callback) => { callback(false); }); desktopSession.setPermissionCheckHandler(() => false); } function suspendPackagedWindows() { for (const win of BrowserWindow.getAllWindows()) { win.hide(); win.webContents.stop(); void win.loadURL('about:blank').catch(() => {}); } } function reloadPackagedWindows(origin) { for (const win of BrowserWindow.getAllWindows()) { win.webContents.once('did-finish-load', () => { if (!win.isDestroyed()) win.show(); }); void win.loadURL(origin).catch((error) => { dialog.showErrorBox('APS desktop UI failed to reload', String(error)); app.quit(); }); } } function isTrustedRendererUrl(url) { if (url === 'about:blank') return true; const trustedOrigin = isDev ? UI_URL : sidecar?.origin; if (!trustedOrigin) return false; try { return new URL(url).origin === new URL(trustedOrigin).origin; } catch { return false; } } function isTrustedIpcEvent(event) { const url = event.senderFrame?.url || event.sender?.getURL?.() || ''; return url !== 'about:blank' && isTrustedRendererUrl(url); } function focusedWebContents() { const win = BrowserWindow.getFocusedWindow() || mainWindow; return win?.webContents ?? null; } /** 把菜单动作发给渲染进程(Web UI) */ function sendMenuAction(action) { const wc = focusedWebContents(); if (wc) wc.send('menu:action', action); } function openApsHome() { ensureApsHome(); shell.openPath(APS_HOME); } function showAbout() { dialog.showMessageBox(BrowserWindow.getFocusedWindow() || mainWindow, { type: 'info', title: '关于工业智核 APS', message: '工业智核 APS', detail: [ '计划排产智能体 · 桌面端', `用户目录:${APS_HOME}`, '对标 Codex:skills / sessions / data 本地落盘', ].join('\n'), buttons: ['确定'], }); } /** Codex 风格中文菜单栏 */ function buildAppMenu() { const template = [ ...(isMac ? [{ label: app.name, submenu: [ { label: '关于工业智核 APS', click: showAbout }, { type: 'separator' }, { label: '服务', role: 'services' }, { type: 'separator' }, { label: '隐藏', role: 'hide' }, { label: '隐藏其他', role: 'hideOthers' }, { label: '显示全部', role: 'unhide' }, { type: 'separator' }, { label: '退出', role: 'quit' }, ], }] : []), { label: '文件', submenu: [ { label: '新建窗口', accelerator: 'CmdOrCtrl+Shift+N', click: () => createWindow(), }, { label: '新建话题', accelerator: 'CmdOrCtrl+N', click: () => sendMenuAction('new-chat'), }, { type: 'separator' }, { label: '打开用户目录…', accelerator: 'CmdOrCtrl+O', click: openApsHome, }, { label: '打开技能目录…', click: () => { ensureApsHome(); shell.openPath(path.join(APS_HOME, 'skills')); }, }, { type: 'separator' }, { label: '关闭窗口', accelerator: 'CmdOrCtrl+W', role: 'close', }, ...(!isMac ? [ { type: 'separator' }, { label: '退出', accelerator: 'Alt+F4', role: 'quit' }, ] : []), ], }, { label: '编辑', submenu: [ { label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: '重做', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, { type: 'separator' }, { label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' }, { label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' }, { label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' }, { label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }, ], }, { label: '视图', submenu: [ { label: '重新加载', accelerator: 'CmdOrCtrl+R', role: 'reload' }, { label: '强制重新加载', accelerator: 'CmdOrCtrl+Shift+R', role: 'forceReload' }, { label: '开发者工具', accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I', role: 'toggleDevTools' }, { type: 'separator' }, { label: '实际大小', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }, { label: '放大', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' }, { label: '缩小', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }, { type: 'separator' }, { label: '全屏', accelerator: isMac ? 'Ctrl+Command+F' : 'F11', role: 'togglefullscreen' }, { type: 'separator' }, { label: '命令面板', accelerator: 'CmdOrCtrl+K', click: () => sendMenuAction('command-palette'), }, { label: '打开设置', accelerator: 'CmdOrCtrl+,', click: () => sendMenuAction('open-settings'), }, ], }, { label: '窗口', submenu: [ { label: '最小化', role: 'minimize' }, { label: '缩放', role: 'zoom' }, ...(isMac ? [ { type: 'separator' }, { label: '前置全部窗口', role: 'front' }, ] : [ { type: 'separator' }, { label: '关闭', role: 'close' }, ]), ], }, { label: '帮助', submenu: [ { label: '打开用户目录 ~/.aps', click: openApsHome, }, { label: '系统路径说明', click: () => sendMenuAction('open-paths'), }, { type: 'separator' }, { label: '关于工业智核 APS', click: showAbout, }, ], }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } function createWindow() { const win = new BrowserWindow({ width: 1440, height: 900, minWidth: 1100, minHeight: 700, title: 'APS 智能排产工作台', show: false, // 自绘顶栏 + 窗口按钮(最小化 / 最大化·还原 / 关闭) ...(process.platform === 'win32' ? { frame: false, titleBarStyle: 'hidden', autoHideMenuBar: true, } : {}), webPreferences: { preload: path.resolve(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, additionalArguments: [ `--aps-home=${encodeURIComponent(APS_HOME)}`, `--aps-skills-dir=${encodeURIComponent(path.join(APS_HOME, 'skills'))}`, `--aps-sessions-dir=${encodeURIComponent(path.join(APS_HOME, 'sessions'))}`, `--aps-api-url=${encodeURIComponent(process.env.APS_API_URL || '')}`, ], }, }); if (!mainWindow) mainWindow = win; win.on('closed', () => { if (mainWindow === win) mainWindow = null; }); win.once('ready-to-show', () => win.show()); const pushState = () => { if (win.isDestroyed()) return; win.webContents.send('aps:window-state', { maximized: win.isMaximized(), fullScreen: win.isFullScreen(), }); }; win.on('maximize', pushState); win.on('unmaximize', pushState); win.on('enter-full-screen', pushState); win.on('leave-full-screen', pushState); win.webContents.on('did-finish-load', pushState); if (isDev) { win.loadURL(UI_URL); // 仅首窗默认开 DevTools,避免多窗刷屏 if (BrowserWindow.getAllWindows().length === 1) { win.webContents.openDevTools({ mode: 'detach' }); } } else { win.loadURL(process.env.APS_API_URL); } win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('https://')) void shell.openExternal(url); return { action: 'deny' }; }); const guardNavigation = (event, url) => { if (isTrustedRendererUrl(url)) return; event.preventDefault(); if (url.startsWith('https://')) void shell.openExternal(url); }; win.webContents.on('will-navigate', guardNavigation); win.webContents.on('will-redirect', guardNavigation); return win; } const hasSingleInstanceLock = app.requestSingleInstanceLock(); if (!hasSingleInstanceLock) app.quit(); app.on('second-instance', () => { const win = mainWindow || BrowserWindow.getAllWindows()[0]; if (!win) return; if (win.isMinimized()) win.restore(); win.show(); win.focus(); }); app.whenReady().then(async () => { if (!hasSingleInstanceLock) return; ensureApsHome(); // Windows 英文系统下 Electron 默认菜单是英文 File/Edit; // 中文菜单改由 Web 顶栏(AppMenuBar)呈现,避免双菜单且保证文案为中文。 Menu.setApplicationMenu(null); try { await startBundledApi(); createWindow(); } catch (error) { const message = error instanceof Error ? error.message : String(error); dialog.showErrorBox( 'APS local service failed to start', `${message}\n\nLog: ${path.join(APS_HOME, 'logs', 'sidecar.log')}`, ); app.quit(); return; } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); app.on('before-quit', () => sidecar?.stop()); ipcMain.handle('aps:get-home', (event) => { if (!isTrustedIpcEvent(event)) throw new Error('Untrusted IPC sender'); return { mode: 'desktop', home: APS_HOME, skills: path.join(APS_HOME, 'skills'), sessions: path.join(APS_HOME, 'sessions'), }; }); ipcMain.handle('aps:get-device-id', (event) => { if (!isTrustedIpcEvent(event)) throw new Error('Untrusted IPC sender'); return getDeviceId(); }); const UPGRADE_IPC_GUARD = 'Untrusted IPC sender'; /** 检查离线升级:manifestPath + packagePath(U 盘/介质)→ {updateAvailable, currentVersion, targetVersion, verified} */ ipcMain.handle('aps:check-for-update', async (event, options) => { if (!isTrustedIpcEvent(event)) throw new Error(UPGRADE_IPC_GUARD); const currentVersion = app.getVersion(); const updater = createUpdater(); return updater.checkForUpdate({ currentVersion, ...(options || {}) }); }); /** 应用离线升级:校验 → 备份 → 安装 → 健康检查 → 失败自动回滚。 * round-46 KK:apply-update 前登记 pendingUpgrade(installing),sidecar 重启后用新 nonce * 续接最终健康检查(restarted 事件驱动,见 upgrade-nonce.cjs);同时把当前 sidecar nonce * 显式传给 applyUpdate 的安装后健康检查,避免初检带着过期 nonce。 */ ipcMain.handle('aps:apply-update', async (event, options) => { if (!isTrustedIpcEvent(event)) throw new Error(UPGRADE_IPC_GUARD); const audit = resolveAuditOptions((options || {}).audit) || {}; const updater = createUpdater(audit); const currentVersion = app.getVersion(); // round-46 KK:sidecar 存在(打包态)才登记 pending——nonce 续接依赖 sidecar 重启事件; // dev 态(无 sidecar)由 applyUpdate 自带的健康检查负责即可。 const pending = sidecar ? upgradeNonceCoordinator.setPending({ id: crypto.randomUUID(), currentVersion, targetVersion: (options || {}).expectedVersion || null, expectedVersion: (options || {}).expectedVersion || null, healthBase: (options || {}).healthBase || sidecar?.origin || process.env.APS_API_URL || null, audit, startedAt: new Date().toISOString(), }) : null; try { const report = await updater.applyUpdate({ currentVersion, installDir: path.dirname(process.execPath), ...(options || {}), ...(sidecar?.nonce ? { sidecarNonce: sidecar.nonce } : {}), }); // applyUpdate 自带健康检查为初步校验;保留 pending 等 sidecar 重启(新 nonce)做最终校验。 // 无 sidecar(dev)时 deferFinalVerify=false 直接完成。 if (pending) { void upgradeNonceCoordinator.markInstalled({ id: pending.id, report, deferFinalVerify: Boolean(sidecar) }) .catch((error) => console.error('[upgrade-nonce] markInstalled 异常:', error)); } return report; } catch (error) { if (pending) upgradeNonceCoordinator.markFailed({ id: pending.id, error }); throw error; } }); /** 手动回滚:恢复备份 + 审计(也用于升级失败后的恢复操作) */ ipcMain.handle('aps:rollback', async (event, options) => { if (!isTrustedIpcEvent(event)) throw new Error(UPGRADE_IPC_GUARD); const updater = createUpdater({ ...(resolveAuditOptions((options || {}).audit) || {}), }); return updater.rollback({ installDir: path.dirname(process.execPath), ...(options || {}), }); }); ipcMain.handle('aps:pick-directory', async (event) => { if (!isTrustedIpcEvent(event)) throw new Error('Untrusted IPC sender'); const win = BrowserWindow.fromWebContents(event.sender); const result = await dialog.showOpenDialog(win ?? undefined, { title: '选择工程目录', properties: ['openDirectory', 'createDirectory'], }); if (result.canceled || !result.filePaths?.[0]) return null; return result.filePaths[0]; }); ipcMain.on('aps:toggle-devtools', (event) => { if (!isTrustedIpcEvent(event)) return; const wc = event.sender; if (wc.isDevToolsOpened()) wc.closeDevTools(); else wc.openDevTools({ mode: 'detach' }); }); function winFrom(event) { if (!isTrustedIpcEvent(event)) return null; return BrowserWindow.fromWebContents(event.sender); } ipcMain.on('aps:toggle-fullscreen', (event) => { const win = winFrom(event); if (!win) return; win.setFullScreen(!win.isFullScreen()); }); ipcMain.on('aps:leave-fullscreen', (event) => { const win = winFrom(event); if (!win) return; // Esc 只退真正全屏,避免误伤「仅最大化」 if (win.isFullScreen()) win.setFullScreen(false); }); ipcMain.on('aps:window-minimize', (event) => { winFrom(event)?.minimize(); }); ipcMain.on('aps:window-maximize', (event) => { const win = winFrom(event); if (!win) return; // 「还原」同时覆盖:全屏退出 + 最大化还原 if (win.isFullScreen()) { win.setFullScreen(false); return; } if (win.isMaximized()) win.unmaximize(); else win.maximize(); }); ipcMain.on('aps:window-close', (event) => { winFrom(event)?.close(); }); ipcMain.on('aps:window-state-request', (event) => { const win = winFrom(event); if (!win) return; event.sender.send('aps:window-state', { maximized: win.isMaximized(), fullScreen: win.isFullScreen(), }); });