// @ts-check const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const path = require('path'); const { spawn } = require('child_process'); /** @type {import('child_process').ChildProcess | null} */ let pythonProcess = null; const BACKEND_PORT = 9231; const BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`; const isDev = !app.isPackaged; function startPythonBackend() { const serverDir = isDev ? path.join(__dirname, '..', '..', 'server') : path.join(process.resourcesPath, 'server'); // In dev, use Python directly; in production, use PyInstaller binary if (isDev) { const venvPython = path.join(serverDir, 'venv', 'bin', 'python3'); const mainPy = path.join(serverDir, 'main.py'); pythonProcess = spawn(venvPython, [mainPy], { cwd: serverDir, env: { ...process.env, ENGIMIND_PORT: String(BACKEND_PORT) }, stdio: ['pipe', 'pipe', 'pipe'], }); } else { const binaryName = process.platform === 'win32' ? 'engimind-server.exe' : 'engimind-server'; const binary = path.join(serverDir, binaryName); pythonProcess = spawn(binary, [], { env: { ...process.env, ENGIMIND_PORT: String(BACKEND_PORT) }, stdio: ['pipe', 'pipe', 'pipe'], }); } pythonProcess.stdout?.on('data', (data) => { console.log(`[server] ${data.toString().trim()}`); }); pythonProcess.stderr?.on('data', (data) => { console.error(`[server] ${data.toString().trim()}`); }); pythonProcess.on('exit', (code) => { console.log(`[server] exited with code ${code}`); pythonProcess = null; }); } function stopPythonBackend() { if (pythonProcess) { pythonProcess.kill('SIGTERM'); setTimeout(() => { if (pythonProcess && !pythonProcess.killed) { pythonProcess.kill('SIGKILL'); } }, 3000); } } async function waitForBackend(maxWait = 15000) { const start = Date.now(); while (Date.now() - start < maxWait) { try { const resp = await fetch(`${BACKEND_URL}/health`); if (resp.ok) return true; } catch {} await new Promise(r => setTimeout(r, 300)); } return false; } function createWindow() { const win = new BrowserWindow({ width: 1600, height: 960, title: 'EngiMind — 工程 AI 协作空间', titleBarStyle: 'hiddenInset', backgroundColor: '#ffffff', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, }, }); if (isDev) { win.loadURL('http://localhost:5173'); win.webContents.openDevTools({ mode: 'detach' }); } else { win.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); } return win; } // ── IPC Handlers ── // File dialog ipcMain.handle('dialog:openFiles', async (_, filters) => { const result = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'], filters: filters || [ { name: '所有支持的文件', extensions: ['pdf', 'xlsx', 'xls', 'docx', 'dwg', 'dxf', 'shp', 'geojson', 'kml', 'gpkg'] }, { name: 'PDF', extensions: ['pdf'] }, { name: 'Excel', extensions: ['xlsx', 'xls'] }, { name: 'Word', extensions: ['docx'] }, { name: 'CAD', extensions: ['dwg', 'dxf'] }, { name: 'GIS', extensions: ['shp', 'geojson', 'kml', 'gpkg'] }, ], }); return result.canceled ? [] : result.filePaths; }); ipcMain.handle('dialog:openFile', async (_, filters) => { const result = await dialog.showOpenDialog({ properties: ['openFile'], filters: filters || [ { name: 'Documents', extensions: ['pdf', 'xlsx', 'xls', 'docx'] }, ], }); return result.canceled ? null : result.filePaths[0]; }); // Backend URL for renderer ipcMain.handle('getBackendURL', () => BACKEND_URL); // ── App lifecycle ── app.whenReady().then(async () => { // In dev mode, skip auto-starting Python if it's already running externally if (isDev) { // Check if backend is already running let alreadyRunning = false; try { const resp = await fetch(`${BACKEND_URL}/health`); if (resp.ok) alreadyRunning = true; } catch {} if (!alreadyRunning) { startPythonBackend(); const ready = await waitForBackend(); if (!ready) { console.error('Python backend failed to start within timeout'); } } else { console.log('[electron] Python backend already running at', BACKEND_URL); } } else { startPythonBackend(); const ready = await waitForBackend(); if (!ready) { console.error('Python backend failed to start within timeout'); } } createWindow(); }); app.on('window-all-closed', () => { stopPythonBackend(); if (process.platform !== 'darwin') app.quit(); }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); app.on('before-quit', () => { stopPythonBackend(); });