195 lines
5.1 KiB
JavaScript
195 lines
5.1 KiB
JavaScript
// 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();
|
|
}
|
|
});
|
|
}
|