#!/usr/bin/env node /** * 同时热启动前端、FastAPI 与 Celery。 * 可使用以下环境变量调整行为: * - FRONTEND_CMD:覆盖前端启动命令,默认为 "pnpm dev" * - BACKEND_CMD:覆盖 FastAPI 启动命令,默认为 "python -m uvicorn app.main:app --reload --port 8000" * - CELERY_CMD:覆盖 Celery 启动命令,默认为 "celery -A app.workers.celery_app worker --loglevel=info" * - PYTHON_BIN:只在 BACKEND_CMD 未覆盖时,设置 Python 可执行文件,默认 "python" * - CELERY_BIN:只在 CELERY_CMD 未覆盖时,设置 Celery 可执行文件,默认 "celery" * - REDIS_URL:仅用于探测 Redis 是否就绪,默认 "redis://localhost:6379/0" * - SKIP_CELERY:设为 "1" or "true" 可跳过 Celery。 */ const { spawn } = require("child_process"); const path = require("path"); const net = require("net"); const { URL } = require("url"); const rootDir = path.resolve(__dirname, ".."); const frontendDir = path.join(rootDir, "wolai-frontend"); const backendDir = path.join(rootDir, "wolai-backend"); const pythonBin = process.env.PYTHON_BIN || "python"; const celeryBin = process.env.CELERY_BIN || "celery"; const skipCelery = (process.env.SKIP_CELERY || "").toLowerCase() === "1" || (process.env.SKIP_CELERY || "").toLowerCase() === "true"; const celeryCmdFromEnv = process.env.CELERY_CMD; const redisUrl = process.env.REDIS_URL || "redis://localhost:6379/0"; const tasks = [ { name: "frontend", command: process.env.FRONTEND_CMD || "pnpm dev", cwd: frontendDir, }, { name: "backend", command: process.env.BACKEND_CMD || `${pythonBin} -m uvicorn app.main:app --reload --port 8000`, cwd: backendDir, }, ]; const children = []; let shuttingDown = false; function logPrefix(name, message) { console.log(`[${name}] ${message}`); } function startTask(task) { logPrefix(task.name, `启动命令:${task.command}`); const child = spawn(task.command, { cwd: task.cwd, stdio: "inherit", shell: true, env: { ...process.env }, }); child.on("exit", (code, signal) => { if (shuttingDown) { return; } const status = signal !== null ? `因信号 ${signal} 退出` : `退出码 ${code ?? "null"}`; logPrefix(task.name, `进程结束(${status}),准备清理其它任务。`); shutdown(code ?? 0); }); child.on("error", (err) => { logPrefix(task.name, `启动失败:${err.message}`); shutdown(1); }); children.push(child); } function shutdown(code) { if (shuttingDown) { return; } shuttingDown = true; logPrefix("system", "收到终止信号,正在关闭所有子进程…"); for (const child of children) { if (!child.killed) { child.kill("SIGINT"); setTimeout(() => { if (!child.killed) { child.kill("SIGTERM"); } }, 5000); } } setTimeout(() => process.exit(code), 200); } process.on("SIGINT", () => shutdown(0)); process.on("SIGTERM", () => shutdown(0)); async function checkRedisReachable(urlString, timeoutMs = 2000) { try { const url = new URL(urlString); const host = url.hostname || "localhost"; const port = Number(url.port) || 6379; return await new Promise((resolve) => { const socket = net.createConnection({ host, port }); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeoutMs); socket.once("connect", () => { clearTimeout(timer); socket.end(); resolve(true); }); socket.once("error", () => { clearTimeout(timer); resolve(false); }); }); } catch (error) { logPrefix("celery", `REDIS_URL (${urlString}) 解析失败:${error.message},跳过连通性检查。`); return true; } } async function main() { if (!skipCelery) { const celeryTask = { name: "celery", command: celeryCmdFromEnv || `${celeryBin} -A app.workers.celery_app worker --loglevel=info`, cwd: backendDir, }; if (celeryCmdFromEnv) { tasks.push(celeryTask); } else if (await checkRedisReachable(redisUrl)) { tasks.push(celeryTask); } else { // Redis 未就绪时直接跳过 Celery,避免热调试流程整体退出。 logPrefix( "celery", `检测到 ${redisUrl} 无法连接,自动跳过 Celery。请先启动 Redis 或设置 SKIP_CELERY=1 显式跳过。`, ); } } if (tasks.length === 0) { console.error("未配置任何可运行的任务,检查环境变量设置。"); process.exit(1); } for (const task of tasks) { startTask(task); } } main().catch((error) => { logPrefix("system", `启动失败:${error.message}`); process.exit(1); });