// main.js —— Electron 主进程(瘦客户端壳)。 // 启动流程:读后端各服务地址配置 → 起本地静态 + /api 反代服务器(仅监听 127.0.0.1)→ 建窗口加载。 // 前端 ui/ 源码零改动:相对路径 /api 与 SSE 流靠「同源」天然可用,后端 CORS 也无需改。 // 反代功能上只走 bff(BFF 是前端唯一入口);qa/rag/vstore/paper/bioflow 地址在设置里可改、持久化, // 实际的各服务再路由由 BFF 自己的 config/.env 完成(见 README)。 const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron'); const path = require('path'); const fs = require('fs'); const { createServer } = require('./server'); const { loadServices, saveServices } = require('./config'); let mainWindow = null; let settingsWindow = null; let server = null; let currentOrigin = null; /** 前端构建产物目录:开发态在 desktop/renderer-dist,打包态在 resources/renderer-dist(extraResources)。 */ function staticDir() { return app.isPackaged ? path.join(process.resourcesPath, 'renderer-dist') : path.join(__dirname, 'renderer-dist'); } function startServer(bffUrl) { return new Promise((resolve, reject) => { const srv = createServer({ staticDir: staticDir(), backendUrl: bffUrl }); srv.once('error', reject); srv.listen(0, '127.0.0.1', () => { server = srv; resolve(`http://127.0.0.1:${srv.address().port}`); }); }); } function stopServer() { if (server) { server.close(); server = null; } } async function restartServer(bffUrl) { stopServer(); currentOrigin = await startServer(bffUrl); return currentOrigin; } function createMainWindow() { mainWindow = new BrowserWindow({ width: 1280, height: 860, minWidth: 960, minHeight: 640, title: '海古纪 PlantAI', backgroundColor: '#0f1115', show: false, webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); mainWindow.loadURL(currentOrigin); mainWindow.once('ready-to-show', () => mainWindow.show()); mainWindow.on('closed', () => { mainWindow = null; }); } function openSettingsWindow() { if (settingsWindow) { settingsWindow.focus(); return; } settingsWindow = new BrowserWindow({ width: 500, height: 560, resizable: false, minimizable: false, maximizable: false, title: '后端服务设置', parent: mainWindow || undefined, modal: !!mainWindow, autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); settingsWindow.loadFile(path.join(__dirname, 'settings.html')); settingsWindow.on('closed', () => { settingsWindow = null; }); } function buildMenu() { const template = [ { label: '设置', submenu: [ { label: '后端服务地址…', click: openSettingsWindow }, { type: 'separator' }, { role: 'reload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'quit', label: '退出' }, ], }, ]; if (process.platform === 'darwin') { template.unshift({ label: app.name, submenu: [{ role: 'about' }, { type: 'separator' }, { role: 'quit' }], }); } Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } app.whenReady().then(async () => { buildMenu(); const dir = staticDir(); if (!fs.existsSync(path.join(dir, 'index.html'))) { dialog.showErrorBox( '缺少前端构建产物', `未找到 ${dir}\n\n请先构建前端:\n cd ui && npm run build\n cd desktop && npm run build:ui` ); app.quit(); return; } ipcMain.handle('desktop:get-config', () => loadServices(app.getPath('userData'))); ipcMain.handle('desktop:set-config', async (_event, services) => { // 用户显式保存后,文件配置优先于环境变量(否则 env 会一直盖住用户设置) delete process.env.PLANTAI_BFF_URL; const saved = saveServices(app.getPath('userData'), services); await restartServer(saved.bff); if (mainWindow) await mainWindow.loadURL(currentOrigin); return saved; }); const services = loadServices(app.getPath('userData')); currentOrigin = await startServer(services.bff); createMainWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createMainWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); app.on('will-quit', stopServer);