// main.js — Electron 主进程 // 功能:启动 FastAPI 后端 → 等待就绪 → 加载 Web UI → 管理生命周期 const { app, BrowserWindow, dialog } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const http = require('http'); const fs = require('fs'); // ---------- 路径配置 ---------- const AGENT_DIR = path.join(__dirname, '..'); // Agent/ 目录 const VENV_PYTHON = path.join(AGENT_DIR, 'myenv', 'Scripts', 'python.exe'); const HOST = '127.0.0.1'; const PORT = 8765; const BASE_URL = `http://${HOST}:${PORT}`; // ---------- 全局状态 ---------- let mainWindow = null; let pythonProcess = null; // ---------- FastAPI 生命周期 ---------- function startFastAPI() { return new Promise((resolve, reject) => { // 如果找不到虚拟环境 Python,尝试使用系统 Python let pythonExe = VENV_PYTHON; if (!fs.existsSync(pythonExe)) { pythonExe = 'python'; // 回退到 PATH 中的 Python console.log('[Electron] 未找到 venv Python,使用系统 Python'); } console.log(`[Electron] Python: ${pythonExe}`); console.log(`[Electron] Agent 目录: ${AGENT_DIR}`); pythonProcess = spawn(pythonExe, [ '-m', 'uvicorn', 'web.server:app', '--host', HOST, '--port', String(PORT), '--log-level', 'warning', ], { cwd: AGENT_DIR, env: { ...process.env, PYTHONPATH: 'src', PYTHONUNBUFFERED: '1', }, shell: true, // Windows 需要 shell 以正确解析路径 stdio: ['ignore', 'pipe', 'pipe'], }); pythonProcess.stdout.on('data', (data) => { console.log(`[FastAPI] ${data.toString().trim()}`); }); pythonProcess.stderr.on('data', (data) => { console.log(`[FastAPI] ${data.toString().trim()}`); }); pythonProcess.on('error', (err) => { console.error('[Electron] 启动 FastAPI 失败:', err.message); reject(err); }); pythonProcess.on('exit', (code) => { console.log(`[Electron] FastAPI 进程退出,退出码: ${code}`); pythonProcess = null; // 非正常退出时提醒用户 if (code !== 0 && code !== null && mainWindow) { dialog.showErrorBox( '后端服务异常', `FastAPI 服务意外退出(退出码: ${code})。\n请检查终端输出了解详情。` ); } }); // 轮询等待服务就绪 waitForReady(resolve, reject, 30); // 最多等 30 秒 }); } function waitForReady(resolve, reject, retriesLeft) { if (retriesLeft <= 0) { reject(new Error('FastAPI 启动超时,请检查依赖是否安装完整')); return; } const req = http.get(`${BASE_URL}/api/health`, (res) => { if (res.statusCode === 200) { console.log('[Electron] FastAPI 服务就绪'); resolve(); } else { retry(); } }); req.on('error', () => retry()); req.setTimeout(2000, () => { req.destroy(); retry(); }); function retry() { setTimeout(() => waitForReady(resolve, reject, retriesLeft - 1), 1000); } } function stopFastAPI() { if (pythonProcess) { console.log('[Electron] 正在关闭 FastAPI...'); // Windows 上 kill 整个进程树 if (process.platform === 'win32') { spawn('taskkill', ['/pid', String(pythonProcess.pid), '/f', '/t']); } else { pythonProcess.kill('SIGTERM'); } pythonProcess = null; } } // ---------- 窗口管理 ---------- function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, title: 'minor Agent', webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, show: false, // 等 ready-to-show 再显示,避免白屏 }); mainWindow.loadURL(BASE_URL); mainWindow.once('ready-to-show', () => { mainWindow.show(); }); mainWindow.on('closed', () => { mainWindow = null; }); // 在默认浏览器打开外部链接 mainWindow.webContents.setWindowOpenHandler(({ url }) => { require('electron').shell.openExternal(url); return { action: 'deny' }; }); } // ---------- 应用生命周期 ---------- app.whenReady().then(async () => { try { await startFastAPI(); createWindow(); } catch (err) { dialog.showErrorBox( '启动失败', `无法启动 minor Agent:\n${err.message}\n\n` + '请确保已安装依赖,或通过命令行运行:\n' + `cd "${AGENT_DIR}"\n` + 'set PYTHONPATH=src\n' + 'python -m uvicorn web.server:app --host 127.0.0.1 --port 8765' ); app.quit(); } }); app.on('window-all-closed', () => { stopFastAPI(); app.quit(); }); app.on('before-quit', () => { stopFastAPI(); }); // macOS: 点击 Dock 图标重新创建窗口 app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); // 防止多实例(可选) const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { app.quit(); } else { app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); }