main.mjs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import { app, BrowserWindow, ipcMain, dialog } from "electron";
  2. import { fileURLToPath } from "url";
  3. import path from "path";
  4. import { spawn } from "child_process";
  5. import fs from "fs";
  6. import Papa from "./libs/papaparse/papaparse.js";
  7. process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = path.dirname(__filename);
  10. const isDev = process.env.NODE_ENV === "development";
  11. let mainWindow = null;
  12. function parsePythonOutput(output) {
  13. const text = output.trim();
  14. if (!text) return null;
  15. try {
  16. return JSON.parse(text);
  17. } catch {
  18. const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
  19. for (let i = lines.length - 1; i >= 0; i -= 1) {
  20. try {
  21. return JSON.parse(lines[i]);
  22. } catch {
  23. // try previous line
  24. }
  25. }
  26. return text;
  27. }
  28. }
  29. function normalizeCsvPath(filePath) {
  30. if (typeof filePath !== "string" || !filePath.trim()) {
  31. throw new Error(`CSV 路径无效: ${filePath}`);
  32. }
  33. let resolvedPath = path.normalize(filePath.trim());
  34. if (fs.existsSync(resolvedPath)) {
  35. try {
  36. resolvedPath = fs.realpathSync.native(resolvedPath);
  37. } catch {
  38. // keep normalized path
  39. }
  40. }
  41. return resolvedPath;
  42. }
  43. function getServesBaseDir() {
  44. return isDev
  45. ? path.join(__dirname, "../serves")
  46. : path.join(process.resourcesPath, "app.asar.unpacked", "serves");
  47. }
  48. function hasEntryModule(dir) {
  49. if (!fs.existsSync(dir)) return false;
  50. return fs
  51. .readdirSync(dir)
  52. .some(
  53. (file) =>
  54. file.startsWith("entry.") &&
  55. (file.endsWith(".pyd") || file.endsWith(".so"))
  56. );
  57. }
  58. function getPythonRunnerBackend(jiguangDir, isWin) {
  59. const runnerPy = path.join(jiguangDir, "runner.py");
  60. if (fs.existsSync(runnerPy) && hasEntryModule(jiguangDir)) {
  61. return {
  62. command: isWin ? "python" : "python3.11",
  63. prefixArgs: [runnerPy],
  64. cwd: jiguangDir,
  65. type: "python-runner",
  66. };
  67. }
  68. return null;
  69. }
  70. function getNuitkaRunnerBackend(jiguangDir, isWin) {
  71. const runnerExeName = isWin ? "api_runner.exe" : "api_runner";
  72. const runnerCandidates = [
  73. { exe: path.join(jiguangDir, "runner.dist", runnerExeName), cwd: path.join(jiguangDir, "runner.dist") },
  74. { exe: path.join(jiguangDir, runnerExeName), cwd: jiguangDir },
  75. ];
  76. for (const candidate of runnerCandidates) {
  77. if (fs.existsSync(candidate.exe)) {
  78. return {
  79. command: candidate.exe,
  80. prefixArgs: [],
  81. cwd: candidate.cwd,
  82. type: "nuitka-runner",
  83. };
  84. }
  85. }
  86. return null;
  87. }
  88. function getLegacyBackend(distDir, isWin) {
  89. const legacyExePath = path.join(distDir, isWin ? "api_test.exe" : "api_test");
  90. if (fs.existsSync(legacyExePath)) {
  91. return {
  92. command: legacyExePath,
  93. prefixArgs: [],
  94. cwd: distDir,
  95. type: "legacy-exe",
  96. };
  97. }
  98. return null;
  99. }
  100. /**
  101. * 解析 Python 后端启动方式。
  102. *
  103. * 开发环境:python runner.py → api_runner.exe → api_test.exe
  104. * 生产环境:api_runner.exe → api_test.exe(禁止 python runner.py)
  105. */
  106. function resolvePythonBackend() {
  107. const servesBase = getServesBaseDir();
  108. const jiguangDir = path.join(servesBase, "JiGuang-program");
  109. const distDir = path.join(servesBase, "dist");
  110. const isWin = process.platform === "win32";
  111. if (isDev) {
  112. return (
  113. getPythonRunnerBackend(jiguangDir, isWin) ||
  114. getNuitkaRunnerBackend(jiguangDir, isWin) ||
  115. getLegacyBackend(distDir, isWin)
  116. );
  117. }
  118. return getNuitkaRunnerBackend(jiguangDir, isWin) || getLegacyBackend(distDir, isWin);
  119. }
  120. function runPythonBackend(apiName, params = {}) {
  121. const backend = resolvePythonBackend();
  122. if (!backend) {
  123. const servesBase = getServesBaseDir();
  124. return Promise.reject(
  125. `未找到 Python 后端。请确认以下路径之一存在:\n` +
  126. ` - ${path.join(servesBase, "JiGuang-program", "runner.dist", "api_runner.exe")}\n` +
  127. ` - ${path.join(servesBase, "JiGuang-program", "api_runner.exe")}\n` +
  128. ` - ${path.join(servesBase, "dist", "api_test.exe")}`
  129. );
  130. }
  131. const payload = Buffer.from(JSON.stringify(params)).toString("base64");
  132. const args = [...backend.prefixArgs, apiName, payload];
  133. console.log(
  134. `🐍 启动 Python 后端 [${backend.type}]:`,
  135. backend.command,
  136. args.join(" ")
  137. );
  138. return new Promise((resolve, reject) => {
  139. const child = spawn(backend.command, args, {
  140. cwd: backend.cwd,
  141. encoding: "utf8",
  142. windowsHide: true,
  143. env: {
  144. ...process.env,
  145. PYTHONIOENCODING: "utf-8",
  146. PYTHONUTF8: "1",
  147. },
  148. });
  149. let stdoutData = "";
  150. let stderrData = "";
  151. child.stdout.on("data", (data) => {
  152. const text = data.toString();
  153. stdoutData += text;
  154. console.log("🐍 Python stdout:", text);
  155. });
  156. child.stderr.on("data", (data) => {
  157. const text = data.toString();
  158. stderrData += text;
  159. console.error("🐍 Python stderr:", text);
  160. });
  161. child.on("error", (err) => {
  162. reject(`启动 Python 后端失败: ${err.message}`);
  163. });
  164. child.on("close", (code) => {
  165. const output = stdoutData.trim();
  166. if (code !== 0) {
  167. reject(
  168. `Python 后端退出码 ${code}\n` +
  169. (stderrData || output || "无错误输出")
  170. );
  171. return;
  172. }
  173. const parsed = parsePythonOutput(output);
  174. if (typeof parsed === "string") {
  175. console.warn("⚠️ Python 输出不是 JSON:", output);
  176. }
  177. resolve(parsed);
  178. });
  179. });
  180. }
  181. function createWindow() {
  182. mainWindow = new BrowserWindow({
  183. width: 1400,
  184. height: 900,
  185. minWidth: 800,
  186. minHeight: 600,
  187. webPreferences: {
  188. preload: path.join(__dirname, "./preload.mjs"),
  189. contextIsolation: true,
  190. nodeIntegration: false,
  191. allowRunningInsecureContent: false,
  192. },
  193. icon: path.join(__dirname, "../src/assets/images/login/bg.png"), // 开发环境的图标
  194. });
  195. //环境判断引入不同的页面
  196. if (process.env.NODE_ENV === "development") {
  197. mainWindow.loadURL("http://localhost:5173");
  198. mainWindow.webContents.openDevTools();
  199. } else {
  200. //生产环境将引入打包好的文件
  201. mainWindow.loadFile(path.join(__dirname, "../dist/index.html"));
  202. }
  203. ipcMain.handle("run-python-exe", async (_event, apiName, params = {}) => {
  204. return runPythonBackend(apiName, params);
  205. });
  206. ipcMain.handle("get-install-path", async () => {
  207. return process.execPath; // 获取可执行文件路径
  208. });
  209. // 监听前端请求,读取 CSV 文件
  210. ipcMain.handle("read-csv", async (event, filePath) => {
  211. try {
  212. const resolvedPath = normalizeCsvPath(filePath);
  213. if (!fs.existsSync(resolvedPath)) {
  214. console.warn("CSV 文件不存在,返回空列表:", resolvedPath);
  215. return [];
  216. }
  217. const csvData = fs.readFileSync(resolvedPath, "utf-8");
  218. const parsedData = Papa.parse(csvData, {
  219. header: true,
  220. skipEmptyLines: true, // ← 这个也可以帮助跳过完全空的行
  221. transformHeader: (header) => header.trim(), // ← 这个也可以帮助跳过完全空的行
  222. });
  223. // 过滤掉所有字段都为空的行
  224. const cleanData = parsedData.data.filter((row) =>
  225. Object.values(row).some((val) => val && val.trim() !== "")
  226. );
  227. return cleanData; // 返回解析后的数据
  228. } catch (error) {
  229. console.error("读取 CSV 失败:", error);
  230. return { error: error.message };
  231. }
  232. });
  233. // 监听 get-file-path 事件 监听渲染进程请求文件路径
  234. ipcMain.handle("get-file-path", async () => {
  235. try {
  236. const { filePaths } = await dialog.showOpenDialog({
  237. properties: ["openFile", "multiSelections"], // ✅ 允许选择多个文件
  238. });
  239. if (filePaths.length > 0) {
  240. console.log("用户选择的文件路径:", filePaths[0]);
  241. return filePaths[0]; // 返回文件路径
  242. } else {
  243. console.log("用户取消了选择");
  244. return null; // 用户没有选择文件
  245. }
  246. } catch (error) {
  247. console.error("获取文件路径失败:", error);
  248. return null;
  249. }
  250. });
  251. mainWindow.on("close", (event) => {
  252. event.preventDefault(); // ✅ 阻止默认关闭行为
  253. const choice = dialog.showMessageBoxSync(mainWindow, {
  254. type: "question",
  255. buttons: ["取消", "退出"],
  256. defaultId: 0, // 默认选中 "取消"
  257. title: "确认退出",
  258. message: "退出程序正在分析中的数据会丢失,确定要退出程序吗?",
  259. });
  260. if (choice === 1) {
  261. // ✅ 直接移除监听器,允许窗口关闭
  262. mainWindow.removeAllListeners("close");
  263. mainWindow.close();
  264. }
  265. });
  266. mainWindow.on("closed", () => {
  267. mainWindow = null; // ✅ 窗口关闭后清空引用
  268. });
  269. }
  270. app.whenReady().then(createWindow);
  271. app.on("window-all-closed", () => {
  272. if (process.platform !== "darwin") app.quit();
  273. });
  274. app.on("activate", () => {
  275. if (BrowserWindow.getAllWindows().length === 0) createWindow();
  276. });
  277. export { createWindow };