/** * APS Electron 主进程(对标 Codex 桌面壳) * - 安装到电脑后用户数据固定:%USERPROFILE%\.aps(同 ~/.codex) * - 含 skills / sessions / data / cache / tmp / logs / packs */ const { app, BrowserWindow, Menu, shell, dialog, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const os = require('os'); const { spawn } = require('child_process'); /** 对标 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'; let mainWindow = null; let apiChild = 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(); } function startBundledApi() { if (isDev) return; const repoRoot = path.resolve(__dirname, '..', '..'); const py = process.env.PYTHON || (process.platform === 'win32' ? 'python' : 'python3'); apiChild = spawn(py, ['-m', 'uvicorn', 'server.main:app', '--host', '127.0.0.1', '--port', '8000'], { cwd: repoRoot, env: { ...process.env, APS_MODE: 'desktop', APS_HOME, }, stdio: 'ignore', }); } 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: false, }, }); 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 { const distIndex = path.join(__dirname, 'web', 'dist', 'index.html'); if (fs.existsSync(distIndex)) { win.loadFile(distIndex); } else { win.loadURL('http://127.0.0.1:8000/'); } } win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); return win; } app.whenReady().then(() => { ensureApsHome(); // Windows 英文系统下 Electron 默认菜单是英文 File/Edit; // 中文菜单改由 Web 顶栏(AppMenuBar)呈现,避免双菜单且保证文案为中文。 Menu.setApplicationMenu(null); startBundledApi(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (apiChild) { try { apiChild.kill(); } catch { /* ignore */ } } if (process.platform !== 'darwin') app.quit(); }); ipcMain.handle('aps:get-home', () => ({ mode: 'desktop', home: APS_HOME, skills: path.join(APS_HOME, 'skills'), sessions: path.join(APS_HOME, 'sessions'), })); ipcMain.handle('aps:get-device-id', () => getDeviceId()); ipcMain.handle('aps:pick-directory', async (event) => { 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) => { const wc = event.sender; if (wc.isDevToolsOpened()) wc.closeDevTools(); else wc.openDevTools({ mode: 'detach' }); }); function winFrom(event) { 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(), }); });