/** * HoloLake Desktop · Electron 主进程 * * 职责: * 1. 创建应用窗口 * 2. 启动知识库后端服务器(Express + Git 引擎) * 3. 管理应用生命周期 * * 架构: * Electron Main Process * ├── 启动 Express 服务器(Git 引擎层,端口 3890) * ├── 创建 BrowserWindow(加载 Vite 前端或打包后的 dist) * └── 数据存储:~/Library/Application Support/HoloLake Era/data/ */ import { app, BrowserWindow, shell, dialog, ipcMain, safeStorage } from 'electron'; import path from 'path'; import { spawn, ChildProcess } from 'child_process'; import fs from 'fs'; // ─── 配置 ─── const isDev = !app.isPackaged; const SERVER_PORT = 3890; const CLIENT_PORT = 5180; // 数据目录:macOS 标准位置 const DATA_DIR = path.join(app.getPath('userData'), 'data'); const KB_REPO_PATH = path.join(DATA_DIR, 'knowledge-base'); const MODEL_CONFIG_PATH = path.join(app.getPath('userData'), 'model-config.json'); interface ModelConfigFile { baseUrl: string; model: string; encryptedKey?: string; } function readModelConfig(): ModelConfigFile { try { return JSON.parse(fs.readFileSync(MODEL_CONFIG_PATH, 'utf8')) as ModelConfigFile; } catch { return { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o' }; } } function applyModelConfig(): ModelConfigFile { const config = readModelConfig(); process.env.HOLOLAKE_LLM_BASE = config.baseUrl; process.env.HOLOLAKE_LLM_MODEL = config.model; if (config.encryptedKey && safeStorage.isEncryptionAvailable()) { process.env.HOLOLAKE_LLM_KEY = safeStorage.decryptString(Buffer.from(config.encryptedKey, 'base64')); } return config; } // ─── 后端服务器 ─── let serverProcess: ChildProcess | null = null; function startServer(): Promise { return new Promise((resolve, reject) => { if (isDev) { // 开发模式:用 spawn 启动外部 tsx const serverScript = path.resolve(__dirname, '../../guanghu-knowledge-base/server/index.ts'); serverProcess = spawn('npx', ['tsx', serverScript], { env: { ...process.env, KB_PORT: String(SERVER_PORT), KB_REPO_PATH, NODE_ENV: 'development', }, cwd: path.resolve(__dirname, '../..'), stdio: ['pipe', 'pipe', 'pipe'], }); serverProcess.stdout?.on('data', (data: Buffer) => { const msg = data.toString().trim(); console.log(`[KB Server] ${msg}`); if (msg.includes('已启动')) resolve(); }); serverProcess.stderr?.on('data', (data: Buffer) => { console.error(`[KB Server Error] ${data.toString().trim()}`); }); serverProcess.on('error', (err: Error) => { console.error('服务器启动失败:', err); reject(err); }); serverProcess.on('exit', (code: number | null) => { console.log(`[KB Server] 退出,code=${code}`); serverProcess = null; }); setTimeout(() => resolve(), 5000); } else { // 生产模式:直接在主进程内加载 server-bundle try { process.env.KB_PORT = String(SERVER_PORT); process.env.KB_REPO_PATH = KB_REPO_PATH; const serverBundle = path.join(__dirname, 'server-bundle.cjs'); require(serverBundle); console.log(`[KB Server] 内嵌启动,端口 ${SERVER_PORT}`); resolve(); } catch (err) { console.error('服务器内嵌启动失败:', err); reject(err); } } }); } function stopServer(): void { if (serverProcess) { serverProcess.kill('SIGTERM'); serverProcess = null; } } // ─── 窗口 ─── let mainWindow: BrowserWindow | null = null; function createWindow(): void { mainWindow = new BrowserWindow({ width: 1280, height: 860, minWidth: 800, minHeight: 600, titleBarStyle: 'hiddenInset', trafficLightPosition: { x: 16, y: 16 }, backgroundColor: '#0d1117', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, }, }); if (isDev) { mainWindow.loadURL(`http://localhost:${CLIENT_PORT}`); mainWindow.webContents.openDevTools({ mode: 'detach' }); } else { mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); } // 外部链接用系统浏览器打开 mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http')) shell.openExternal(url); return { action: 'deny' }; }); mainWindow.on('closed', () => { mainWindow = null; }); } ipcMain.handle('get-data-path', () => DATA_DIR); ipcMain.handle('agent:get-config', () => { const config = readModelConfig(); return { baseUrl: config.baseUrl, model: config.model, configured: Boolean(config.encryptedKey && safeStorage.isEncryptionAvailable()), }; }); ipcMain.handle('agent:save-config', (_event, input: { baseUrl: string; model: string; apiKey?: string }) => { const baseUrl = String(input.baseUrl || '').trim().replace(/\/$/, ''); const model = String(input.model || '').trim(); if (!/^https:\/\//i.test(baseUrl)) throw new Error('模型服务地址必须使用 HTTPS'); if (!model) throw new Error('模型名称不能为空'); if (!safeStorage.isEncryptionAvailable()) throw new Error('macOS 加密存储当前不可用'); const previous = readModelConfig(); const encryptedKey = input.apiKey ? safeStorage.encryptString(String(input.apiKey)).toString('base64') : previous.encryptedKey; if (!encryptedKey) throw new Error('请填写模型密钥'); fs.mkdirSync(path.dirname(MODEL_CONFIG_PATH), { recursive: true }); fs.writeFileSync(MODEL_CONFIG_PATH, JSON.stringify({ baseUrl, model, encryptedKey }), { mode: 0o600 }); applyModelConfig(); return { baseUrl, model, configured: true }; }); // ─── 应用生命周期 ─── app.whenReady().then(async () => { try { applyModelConfig(); await startServer(); createWindow(); } catch (err) { dialog.showErrorBox('启动失败', `知识库引擎启动失败:${err}`); app.quit(); } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { stopServer(); if (process.platform !== 'darwin') app.quit(); }); app.on('before-quit', () => { stopServer(); });