import { app, BrowserWindow, ipcMain, dialog } from "electron"; import { fileURLToPath } from "url"; import path from "path"; import { spawn } from "child_process"; import fs from "fs"; import Papa from "./libs/papaparse/papaparse.js"; process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isDev = process.env.NODE_ENV === "development"; let mainWindow = null; function parsePythonOutput(output) { const text = output.trim(); if (!text) return null; try { return JSON.parse(text); } catch { const lines = text.split("\n").map((line) => line.trim()).filter(Boolean); for (let i = lines.length - 1; i >= 0; i -= 1) { try { return JSON.parse(lines[i]); } catch { // try previous line } } return text; } } function normalizeCsvPath(filePath) { if (typeof filePath !== "string" || !filePath.trim()) { throw new Error(`CSV 路径无效: ${filePath}`); } let resolvedPath = path.normalize(filePath.trim()); if (fs.existsSync(resolvedPath)) { try { resolvedPath = fs.realpathSync.native(resolvedPath); } catch { // keep normalized path } } return resolvedPath; } function getServesBaseDir() { return isDev ? path.join(__dirname, "../serves") : path.join(process.resourcesPath, "app.asar.unpacked", "serves"); } function hasEntryModule(dir) { if (!fs.existsSync(dir)) return false; return fs .readdirSync(dir) .some( (file) => file.startsWith("entry.") && (file.endsWith(".pyd") || file.endsWith(".so")) ); } function getPythonRunnerBackend(jiguangDir, isWin) { const runnerPy = path.join(jiguangDir, "runner.py"); if (fs.existsSync(runnerPy) && hasEntryModule(jiguangDir)) { return { command: isWin ? "python" : "python3.11", prefixArgs: [runnerPy], cwd: jiguangDir, type: "python-runner", }; } return null; } function getNuitkaRunnerBackend(jiguangDir, isWin) { const runnerExeName = isWin ? "api_runner.exe" : "api_runner"; const runnerCandidates = [ { exe: path.join(jiguangDir, "runner.dist", runnerExeName), cwd: path.join(jiguangDir, "runner.dist") }, { exe: path.join(jiguangDir, runnerExeName), cwd: jiguangDir }, ]; for (const candidate of runnerCandidates) { if (fs.existsSync(candidate.exe)) { return { command: candidate.exe, prefixArgs: [], cwd: candidate.cwd, type: "nuitka-runner", }; } } return null; } function getLegacyBackend(distDir, isWin) { const legacyExePath = path.join(distDir, isWin ? "api_test.exe" : "api_test"); if (fs.existsSync(legacyExePath)) { return { command: legacyExePath, prefixArgs: [], cwd: distDir, type: "legacy-exe", }; } return null; } /** * 解析 Python 后端启动方式。 * * 开发环境:python runner.py → api_runner.exe → api_test.exe * 生产环境:api_runner.exe → api_test.exe(禁止 python runner.py) */ function resolvePythonBackend() { const servesBase = getServesBaseDir(); const jiguangDir = path.join(servesBase, "JiGuang-program"); const distDir = path.join(servesBase, "dist"); const isWin = process.platform === "win32"; if (isDev) { return ( getPythonRunnerBackend(jiguangDir, isWin) || getNuitkaRunnerBackend(jiguangDir, isWin) || getLegacyBackend(distDir, isWin) ); } return getNuitkaRunnerBackend(jiguangDir, isWin) || getLegacyBackend(distDir, isWin); } function runPythonBackend(apiName, params = {}) { const backend = resolvePythonBackend(); if (!backend) { const servesBase = getServesBaseDir(); return Promise.reject( `未找到 Python 后端。请确认以下路径之一存在:\n` + ` - ${path.join(servesBase, "JiGuang-program", "runner.dist", "api_runner.exe")}\n` + ` - ${path.join(servesBase, "JiGuang-program", "api_runner.exe")}\n` + ` - ${path.join(servesBase, "dist", "api_test.exe")}` ); } const payload = Buffer.from(JSON.stringify(params)).toString("base64"); const args = [...backend.prefixArgs, apiName, payload]; console.log( `🐍 启动 Python 后端 [${backend.type}]:`, backend.command, args.join(" ") ); return new Promise((resolve, reject) => { const child = spawn(backend.command, args, { cwd: backend.cwd, encoding: "utf8", windowsHide: true, env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", }, }); let stdoutData = ""; let stderrData = ""; child.stdout.on("data", (data) => { const text = data.toString(); stdoutData += text; console.log("🐍 Python stdout:", text); }); child.stderr.on("data", (data) => { const text = data.toString(); stderrData += text; console.error("🐍 Python stderr:", text); }); child.on("error", (err) => { reject(`启动 Python 后端失败: ${err.message}`); }); child.on("close", (code) => { const output = stdoutData.trim(); if (code !== 0) { reject( `Python 后端退出码 ${code}\n` + (stderrData || output || "无错误输出") ); return; } const parsed = parsePythonOutput(output); if (typeof parsed === "string") { console.warn("⚠️ Python 输出不是 JSON:", output); } resolve(parsed); }); }); } function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 800, minHeight: 600, webPreferences: { preload: path.join(__dirname, "./preload.mjs"), contextIsolation: true, nodeIntegration: false, allowRunningInsecureContent: false, }, icon: path.join(__dirname, "../src/assets/images/login/bg.png"), // 开发环境的图标 }); //环境判断引入不同的页面 if (process.env.NODE_ENV === "development") { mainWindow.loadURL("http://localhost:5173"); mainWindow.webContents.openDevTools(); } else { //生产环境将引入打包好的文件 mainWindow.loadFile(path.join(__dirname, "../dist/index.html")); } ipcMain.handle("run-python-exe", async (_event, apiName, params = {}) => { return runPythonBackend(apiName, params); }); ipcMain.handle("get-install-path", async () => { return process.execPath; // 获取可执行文件路径 }); // 监听前端请求,读取 CSV 文件 ipcMain.handle("read-csv", async (event, filePath) => { try { const resolvedPath = normalizeCsvPath(filePath); if (!fs.existsSync(resolvedPath)) { console.warn("CSV 文件不存在,返回空列表:", resolvedPath); return []; } const csvData = fs.readFileSync(resolvedPath, "utf-8"); const parsedData = Papa.parse(csvData, { header: true, skipEmptyLines: true, // ← 这个也可以帮助跳过完全空的行 transformHeader: (header) => header.trim(), // ← 这个也可以帮助跳过完全空的行 }); // 过滤掉所有字段都为空的行 const cleanData = parsedData.data.filter((row) => Object.values(row).some((val) => val && val.trim() !== "") ); return cleanData; // 返回解析后的数据 } catch (error) { console.error("读取 CSV 失败:", error); return { error: error.message }; } }); // 监听 get-file-path 事件 监听渲染进程请求文件路径 ipcMain.handle("get-file-path", async () => { try { const { filePaths } = await dialog.showOpenDialog({ properties: ["openFile", "multiSelections"], // ✅ 允许选择多个文件 }); if (filePaths.length > 0) { console.log("用户选择的文件路径:", filePaths[0]); return filePaths[0]; // 返回文件路径 } else { console.log("用户取消了选择"); return null; // 用户没有选择文件 } } catch (error) { console.error("获取文件路径失败:", error); return null; } }); mainWindow.on("close", (event) => { event.preventDefault(); // ✅ 阻止默认关闭行为 const choice = dialog.showMessageBoxSync(mainWindow, { type: "question", buttons: ["取消", "退出"], defaultId: 0, // 默认选中 "取消" title: "确认退出", message: "退出程序正在分析中的数据会丢失,确定要退出程序吗?", }); if (choice === 1) { // ✅ 直接移除监听器,允许窗口关闭 mainWindow.removeAllListeners("close"); mainWindow.close(); } }); mainWindow.on("closed", () => { mainWindow = null; // ✅ 窗口关闭后清空引用 }); } app.whenReady().then(createWindow); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); export { createWindow };