0.5 缩减重构
This commit is contained in:
+127
-108
@@ -1,29 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
#!/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"
|
||||
* - CELERY_POOL:只在 CELERY_CMD 未覆盖时生效,设置 Celery worker pool;Windows 默认 "solo",其他平台默认使用 Celery 自身默认值
|
||||
* - 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, execSync } = require("child_process");
|
||||
const path = require("path");
|
||||
const net = require("net");
|
||||
const { URL } = require("url");
|
||||
const fs = require("fs");
|
||||
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const frontendDir = path.join(rootDir, "wolai-frontend");
|
||||
const backendDir = path.join(rootDir, "wolai-backend");
|
||||
|
||||
|
||||
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 celeryPoolFromEnv = (process.env.CELERY_POOL || "").trim();
|
||||
const skipCelery =
|
||||
(process.env.SKIP_CELERY || "").toLowerCase() === "1" ||
|
||||
(process.env.SKIP_CELERY || "").toLowerCase() === "true";
|
||||
@@ -40,13 +42,13 @@ const tasks = [
|
||||
},
|
||||
{
|
||||
name: "backend",
|
||||
command:
|
||||
process.env.BACKEND_CMD ||
|
||||
`${pythonBin} -m uvicorn app.main:app --reload --port 8000`,
|
||||
cwd: backendDir,
|
||||
},
|
||||
];
|
||||
|
||||
command:
|
||||
process.env.BACKEND_CMD ||
|
||||
`${pythonBin} -m uvicorn app.main:app --reload --port 8000`,
|
||||
cwd: backendDir,
|
||||
},
|
||||
];
|
||||
|
||||
const children = [];
|
||||
let shuttingDown = false;
|
||||
|
||||
@@ -179,16 +181,16 @@ 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;
|
||||
}, {});
|
||||
.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;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// 约定:全局仅使用仓库根目录的 .env.all 作为配置来源(生产优先)。
|
||||
@@ -202,38 +204,50 @@ const mergedEnv = {
|
||||
...process.env,
|
||||
...envFromAll,
|
||||
};
|
||||
|
||||
|
||||
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: mergedEnv,
|
||||
});
|
||||
|
||||
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 getDefaultCeleryPool() {
|
||||
if (celeryPoolFromEnv) return celeryPoolFromEnv;
|
||||
if (process.platform === "win32") return "solo";
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildDefaultCeleryCommand() {
|
||||
const pool = getDefaultCeleryPool();
|
||||
const poolArg = pool ? ` --pool=${pool}` : "";
|
||||
return `${celeryBin} -A app.workers.celery_app worker --loglevel=info${poolArg}`;
|
||||
}
|
||||
|
||||
function startTask(task) {
|
||||
logPrefix(task.name, `启动命令:${task.command}`);
|
||||
const child = spawn(task.command, {
|
||||
cwd: task.cwd,
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
env: mergedEnv,
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -267,40 +281,40 @@ function shutdown(code) {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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() {
|
||||
// 说明:Next dev 在异常退出时可能残留 `.next/dev/lock`,会导致后续启动直接失败。
|
||||
// 这里在启动前做一次“安全清理”,避免重启后仍卡住。
|
||||
@@ -342,36 +356,41 @@ async function main() {
|
||||
}
|
||||
|
||||
if (!skipCelery) {
|
||||
const defaultCeleryCommand = buildDefaultCeleryCommand();
|
||||
const celeryTask = {
|
||||
name: "celery",
|
||||
command: celeryCmdFromEnv || `${celeryBin} -A app.workers.celery_app worker --loglevel=info`,
|
||||
command: celeryCmdFromEnv || defaultCeleryCommand,
|
||||
cwd: backendDir,
|
||||
};
|
||||
|
||||
if (celeryCmdFromEnv) {
|
||||
tasks.push(celeryTask);
|
||||
} else if (await checkRedisReachable(redisUrl)) {
|
||||
const defaultPool = getDefaultCeleryPool();
|
||||
if (defaultPool) {
|
||||
logPrefix("celery", `未显式设置 CELERY_CMD,当前平台默认使用 worker pool:${defaultPool}`);
|
||||
}
|
||||
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user