#!/usr/bin/env node /** * 一键启动:wolai-backend + 桌面端(Electron + 内置 Next standalone) * * 目的: * - 本地联调无需反复安装 NSIS 安装包 * - Electron 走内置 Next(更接近安装包运行方式) * * 可选环境变量: * - SKIP_DESKTOP_NEXT_BUILD=1:跳过 build:desktop:next(仅启动) * - PYTHON_BIN:默认 python * - BACKEND_CMD:覆盖后端启动命令 * - ELECTRON_CMD:覆盖 Electron 启动命令 * - MNOTE_DATA_DIR:覆盖桌面端 data 目录(默认 /data-desktop-test) */ const { spawnSync, spawn } = require("child_process"); const fs = require("fs"); const net = require("net"); const path = require("path"); const rootDir = path.resolve(__dirname, ".."); const backendDir = path.join(rootDir, "wolai-backend"); const pythonBin = process.env.PYTHON_BIN || "python"; const dataDir = process.env.MNOTE_DATA_DIR || path.join(rootDir, "data-desktop-test"); const backendCmd = process.env.BACKEND_CMD || `${pythonBin} -m uvicorn app.main:app --reload --host 127.0.0.1 --port 8000`; const electronCmd = process.env.ELECTRON_CMD || "electron desktop-electron/main.js"; function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) return {}; const content = fs.readFileSync(filePath, "utf8"); return content .split(/\r?\n/) .filter((line) => line.trim() && !line.trim().startsWith("#")) .reduce((acc, line) => { const idx = line.indexOf("="); if (idx === -1) return acc; const key = line.slice(0, idx).trim(); const value = line.slice(idx + 1).trim(); acc[key] = value; return acc; }, {}); } function runOrExit(command, cwd, env) { const r = spawnSync(command, { cwd, env, stdio: "inherit", shell: true, }); if (r.status !== 0) process.exit(r.status ?? 1); } async function waitForPort(host, port, timeoutMs = 20_000) { const start = Date.now(); // eslint-disable-next-line no-constant-condition while (true) { const ok = await new Promise((resolve) => { const socket = net.createConnection({ host, port }); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 500); socket.once("connect", () => { clearTimeout(timer); socket.end(); resolve(true); }); socket.once("error", () => { clearTimeout(timer); resolve(false); }); }); if (ok) return; if (Date.now() - start > timeoutMs) { throw new Error(`等待端口超时:${host}:${port}`); } // eslint-disable-next-line no-await-in-loop await new Promise((r) => setTimeout(r, 120)); } } async function main() { const envMerged = { ...process.env, ...loadEnvFile(path.join(rootDir, ".env.local")), ...loadEnvFile(path.join(rootDir, ".env")), ...loadEnvFile(path.join(backendDir, ".env")), PYTHONUTF8: "1", MNOTE_DATA_DIR: dataDir, }; const skipBuild = (process.env.SKIP_DESKTOP_NEXT_BUILD || "").toLowerCase() === "1" || (process.env.SKIP_DESKTOP_NEXT_BUILD || "").toLowerCase() === "true"; if (!skipBuild) { console.log("[desktop-local] 构建桌面端内置 Next(standalone)…"); runOrExit("pnpm run build:desktop:next", rootDir, envMerged); } else { console.log("[desktop-local] 已跳过 build:desktop:next(SKIP_DESKTOP_NEXT_BUILD=1)"); } console.log(`[desktop-local] data 目录:${dataDir}`); console.log(`[desktop-local] 后端命令:${backendCmd}`); console.log(`[desktop-local] Electron 命令:${electronCmd}`); const children = []; let shuttingDown = false; const shutdown = () => { if (shuttingDown) return; shuttingDown = true; console.log("[desktop-local] 正在关闭子进程…"); for (const child of children) { try { if (!child.killed) child.kill("SIGINT"); } catch {} } setTimeout(() => process.exit(0), 200); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); const backend = spawn(backendCmd, { cwd: backendDir, env: envMerged, stdio: "inherit", shell: true, }); children.push(backend); backend.on("exit", () => shutdown()); await waitForPort("127.0.0.1", 8000).catch(() => { console.log("[desktop-local] 提示:未检测到后端 8000 端口就绪(可能仍在启动或启动失败)。"); }); const electron = spawn(electronCmd, { cwd: rootDir, env: envMerged, stdio: "inherit", shell: true, }); children.push(electron); electron.on("exit", () => shutdown()); } main().catch((err) => { console.error(err instanceof Error ? err.stack : String(err)); process.exit(1); });