- 修复 desktop 脚本在 Linux 下优先使用后端虚拟环境可执行文件 - 移除 Convex Auth 的本地密钥兜底,回到真实 deployment env - 首页未登录时先跳转 /auth,避免根路由直接触发 workspace 初始化失败
190 lines
5.5 KiB
JavaScript
190 lines
5.5 KiB
JavaScript
#!/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 目录(默认 <repo>/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");
|
||
|
||
function resolveBackendPython() {
|
||
const fromEnv = (process.env.PYTHON_BIN || "").trim();
|
||
if (fromEnv) return fromEnv;
|
||
|
||
const isWin = process.platform === "win32";
|
||
const candidates = isWin
|
||
? [
|
||
path.join(backendDir, ".venv", "Scripts", "python.exe"),
|
||
path.join(backendDir, ".venv312", "Scripts", "python.exe"),
|
||
path.join(backendDir, "venv", "Scripts", "python.exe"),
|
||
]
|
||
: [
|
||
path.join(backendDir, ".venv-linux", "bin", "python"),
|
||
path.join(backendDir, ".venv", "bin", "python"),
|
||
path.join(backendDir, "venv", "bin", "python"),
|
||
];
|
||
|
||
for (const candidate of candidates) {
|
||
if (fs.existsSync(candidate)) {
|
||
return candidate;
|
||
}
|
||
}
|
||
|
||
return "python";
|
||
}
|
||
|
||
const pythonBin = resolveBackendPython();
|
||
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 envAllPath = path.join(rootDir, ".env.all");
|
||
if (!fs.existsSync(envAllPath)) {
|
||
console.error("[desktop-local] 缺少 .env.all:请在仓库根目录创建全局唯一 env 文件后重试。");
|
||
process.exit(1);
|
||
}
|
||
|
||
const envMerged = {
|
||
...process.env,
|
||
// 约定:全局仅使用仓库根目录的 .env.all 作为配置来源(生产优先)。
|
||
...loadEnvFile(envAllPath),
|
||
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);
|
||
});
|