const { app, BrowserWindow ,ipcMain} = require('electron') const path = require('path') const http = require('http') const os = require('os') const fs = require('fs') const config = require('../configs/config.js') const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged // 修复缓存权限问题:设置用户数据目录到有权限的位置 // 必须在 app.whenReady() 之前调用 if (process.platform === 'win32') { try { // 设置缓存目录到用户临时目录,避免权限问题 const userDataPath = path.join(os.tmpdir(), 'electron-react-vite-app') // 确保目录存在 if (!fs.existsSync(userDataPath)) { fs.mkdirSync(userDataPath, { recursive: true }) } // 创建缓存子目录 const cacheDir = path.join(userDataPath, 'cache') const gpuCacheDir = path.join(userDataPath, 'gpu-cache') if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }) } if (!fs.existsSync(gpuCacheDir)) { fs.mkdirSync(gpuCacheDir, { recursive: true }) } // 设置用户数据路径(必须在 app.whenReady() 之前) app.setPath('userData', userDataPath) // 设置缓存目录到有权限的位置 app.commandLine.appendSwitch('disk-cache-dir', cacheDir) app.commandLine.appendSwitch('gpu-disk-cache-dir', gpuCacheDir) console.log(`[OK] Cache directories set to: ${userDataPath}`) } catch (error) { console.warn('[WARN] Failed to set cache directories:', error.message) // 如果设置失败,尝试禁用 GPU 缓存作为备选方案 app.commandLine.appendSwitch('disable-gpu-sandbox') } } /** * 检测 Vite 开发服务器实际使用的端口 * 如果配置的端口被占用,Vite 会自动尝试下一个端口 * 需要确认是 Vite 服务器,而不仅仅是端口响应 */ async function findVitePort(startPort, maxAttempts = 10) { const viteHost = config.vite?.host || 'localhost' for (let offset = 0; offset < maxAttempts; offset++) { const port = startPort + offset const isViteServer = await new Promise((resolve) => { const req = http.get(`http://${viteHost}:${port}`, (res) => { // 检查状态码 if (res.statusCode !== 200) { resolve(false) return } // 读取响应数据确认是否是 Vite 服务器 let data = '' let resolved = false res.on('data', (chunk) => { if (resolved) return data += chunk.toString() // 如果响应包含 Vite 特征,立即确认 if (data.length > 100 && (data.includes('/vite') || data.includes('Vite') || data.includes('vite/client'))) { resolved = true resolve(true) } }) res.on('end', () => { if (resolved) return // 检查响应内容是否包含 Vite 特征 if (data.includes('/vite') || data.includes('Vite') || data.includes('vite/client')) { resolve(true) } else { resolve(false) } }) }) req.on('error', () => { resolve(false) }) req.setTimeout(2000, () => { req.destroy() resolve(false) }) }) if (isViteServer) { return port } } // 如果找不到,返回配置的端口 return startPort } // 保存主窗口引用,用于推送消息 let mainWindowInstance = null async function createWindow() { const mainWindow = new BrowserWindow({ width: config.window.width, height: config.window.height, autoHideMenuBar: config.window.autoHideMenuBar, // 从配置文件读取 webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) // 保存窗口引用 mainWindowInstance = mainWindow if (isDev) { // 从配置文件读取 Vite 开发服务器端口 const configPort = config.vite?.port || 5173 // 检测实际使用的端口(如果配置端口被占用,Vite 会自动尝试下一个) const vitePort = await findVitePort(configPort) const viteHost = config.vite?.host || 'localhost' console.log(`Loading Vite dev server at http://${viteHost}:${vitePort}`) mainWindow.loadURL(`http://${viteHost}:${vitePort}`) // 根据配置文件决定是否打开调试侧边栏 if (config.devTools.enabled) { mainWindow.webContents.openDevTools() } } else { mainWindow.loadFile(path.join(__dirname, '../dist/index.html')) } } const { spawn } = require('child_process') // 存储运行中的进程 const runningProcesses = new Map() // Execute Node.js script ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) => { return new Promise((resolve, reject) => { const scriptPath = path.join(__dirname, '../nodejs', `${scriptName}.js`) const processKey = `${scriptName}-${parameters.join('-')}` // 如果进程已运行,先停止它 if (runningProcesses.has(processKey)) { const oldProcess = runningProcesses.get(processKey) oldProcess.kill() runningProcesses.delete(processKey) } const nodeProcess = spawn('node', [scriptPath, ...parameters]) runningProcesses.set(processKey, nodeProcess) let stdout = '' let stderr = '' let resolved = false nodeProcess.stdout.on('data', (data) => { const dataStr = data.toString() stdout += dataStr // 检查是否是 JSON 格式的成功消息(用于持续运行的脚本) try { const lines = dataStr.trim().split('\n') for (const line of lines) { if (line.trim().startsWith('{')) { const json = JSON.parse(line.trim()) if (json.success && !resolved) { resolved = true // 对于持续运行的脚本,收到成功消息后立即返回,不等待进程结束 resolve({ success: true, stdout: line.trim(), stderr: stderr.trim(), exitCode: null }) } } } } catch (e) { // 不是 JSON,继续收集输出 } }) nodeProcess.stderr.on('data', (data) => { stderr += data.toString() }) nodeProcess.on('close', (code) => { runningProcesses.delete(processKey) if (!resolved) { resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code }) } }) nodeProcess.on('error', (error) => { runningProcesses.delete(processKey) if (!resolved) { reject(error) } }) // 对于持续运行的脚本,设置超时,如果 5 秒内没有收到成功消息,也返回 setTimeout(() => { if (!resolved) { resolved = true resolve({ success: true, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: null, message: 'Script is running in background' }) } }, 5000) }) }) // Execute Python script ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) => { return new Promise((resolve, reject) => { let pythonPath = 'python' if (config.pythonPath?.path) { const configPythonPath = path.join(config.pythonPath.path, 'python.exe') if (fs.existsSync(configPythonPath)) { pythonPath = configPythonPath } } const scriptPath = path.join(__dirname, '../python/scripts', `${scriptName}.py`) if (!fs.existsSync(scriptPath)) { reject({ success: false, error: `Script file not found: ${scriptPath}`, stderr: `Script file not found: ${scriptPath}` }) return } const pythonProcess = spawn(pythonPath, [scriptPath, ...parameters]) let stdout = '' let stderr = '' pythonProcess.stdout.on('data', (data) => { stdout += data.toString() }) pythonProcess.stderr.on('data', (data) => { stderr += data.toString() }) pythonProcess.on('close', (code) => { resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code }) }) pythonProcess.on('error', (error) => { reject({ success: false, error: error.message, stderr: error.message }) }) }) }) // IPC 实时通信处理 // 处理前端请求(异步,返回 Promise) ipcMain.handle('ipc-request', async (event, channel, data) => { // 可以根据不同的 channel 处理不同的请求 // 例如:'run-nodejs', 'get-status' 等 if (channel === 'run-nodejs') { // 通过 IPC 调用 run-nodejs-script const { scriptName, ...parameters } = data return new Promise((resolve, reject) => { const scriptPath = path.join(__dirname, '../nodejs', `${scriptName}.js`) const processKey = `${scriptName}-${parameters.join('-')}` if (runningProcesses.has(processKey)) { const oldProcess = runningProcesses.get(processKey) oldProcess.kill() runningProcesses.delete(processKey) } const nodeProcess = spawn('node', [scriptPath, ...Object.values(parameters)]) runningProcesses.set(processKey, nodeProcess) let stdout = '' let stderr = '' let resolved = false nodeProcess.stdout.on('data', (chunk) => { stdout += chunk.toString() try { const lines = chunk.toString().trim().split('\n') for (const line of lines) { if (line.trim().startsWith('{')) { const json = JSON.parse(line.trim()) if (json.success && !resolved) { resolved = true resolve({ success: true, stdout: line.trim(), stderr: stderr.trim(), exitCode: null }) } } } } catch (e) {} }) nodeProcess.stderr.on('data', (chunk) => { stderr += chunk.toString() }) nodeProcess.on('close', (code) => { runningProcesses.delete(processKey) if (!resolved) { resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code }) } }) nodeProcess.on('error', (error) => { runningProcesses.delete(processKey) if (!resolved) { reject(error) } }) setTimeout(() => { if (!resolved) { resolved = true resolve({ success: true, stdout: stdout.trim(), stderr: stderr.trim(), exitCode: null, message: 'Script is running in background' }) } }, 5000) }) } // 默认响应 return { success: true, channel, data, timestamp: Date.now() } }) // 监听前端发送的消息(不需要响应) ipcMain.on('ipc-message', (event, channel, data) => { // 处理前端发送的消息 console.log(`[IPC] Received message on channel "${channel}":`, data) }) // 推送消息到前端的辅助函数 function pushToFrontend(channel, data) { if (mainWindowInstance && !mainWindowInstance.isDestroyed()) { mainWindowInstance.webContents.send(channel, data) } } // 导出推送函数供其他模块使用 global.pushToFrontend = pushToFrontend app.whenReady().then(() => { createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } })