2026-04-13 19:21:42 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-23 10:55:04 +08:00
|
|
|
|
* 同时热启动前端、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"
|
2026-04-13 19:21:42 +08:00
|
|
|
|
* - CELERY_POOL:只在 CELERY_CMD 未覆盖时生效,设置 Celery worker pool;Windows 默认 "solo",其他平台默认使用 Celery 自身默认值
|
2025-11-23 10:55:04 +08:00
|
|
|
|
* - 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。
|
|
|
|
|
|
*/
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
|
const { spawn, execSync } = require("child_process");
|
2025-11-23 10:55:04 +08:00
|
|
|
|
const path = require("path");
|
|
|
|
|
|
const net = require("net");
|
|
|
|
|
|
const { URL } = require("url");
|
2025-12-26 07:52:40 +08:00
|
|
|
|
const fs = require("fs");
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
|
|
const rootDir = path.resolve(__dirname, "..");
|
|
|
|
|
|
const frontendDir = path.join(rootDir, "wolai-frontend");
|
|
|
|
|
|
const backendDir = path.join(rootDir, "wolai-backend");
|
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
|
const pythonBin = process.env.PYTHON_BIN || "python";
|
|
|
|
|
|
const celeryBin = process.env.CELERY_BIN || "celery";
|
2026-04-13 19:21:42 +08:00
|
|
|
|
const celeryPoolFromEnv = (process.env.CELERY_POOL || "").trim();
|
2025-11-23 10:55:04 +08:00
|
|
|
|
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";
|
2026-01-17 10:12:53 +08:00
|
|
|
|
const frontendPortFromEnv = Number(process.env.FRONTEND_PORT || 3000);
|
2026-02-01 08:47:40 +08:00
|
|
|
|
const backendPortFromEnv = Number(process.env.BACKEND_PORT || 8000);
|
2025-11-23 10:55:04 +08:00
|
|
|
|
|
|
|
|
|
|
const tasks = [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "frontend",
|
|
|
|
|
|
command: process.env.FRONTEND_CMD || "pnpm dev",
|
|
|
|
|
|
cwd: frontendDir,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "backend",
|
2026-04-13 19:21:42 +08:00
|
|
|
|
command:
|
|
|
|
|
|
process.env.BACKEND_CMD ||
|
|
|
|
|
|
`${pythonBin} -m uvicorn app.main:app --reload --port 8000`,
|
|
|
|
|
|
cwd: backendDir,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
|
const children = [];
|
|
|
|
|
|
let shuttingDown = false;
|
|
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
|
async function isPortFree(host, port, timeoutMs = 400) {
|
|
|
|
|
|
return await new Promise((resolve) => {
|
|
|
|
|
|
const socket = net.createConnection({ host, port });
|
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
|
socket.destroy();
|
|
|
|
|
|
resolve(true);
|
|
|
|
|
|
}, timeoutMs);
|
|
|
|
|
|
|
|
|
|
|
|
socket.once("connect", () => {
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
|
socket.end();
|
|
|
|
|
|
resolve(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
socket.once("error", () => {
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
|
resolve(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function findFreePort(startPort) {
|
|
|
|
|
|
let port = Number.isFinite(startPort) ? Math.floor(startPort) : 3000;
|
|
|
|
|
|
port = Math.max(1, Math.min(65535, port));
|
|
|
|
|
|
|
|
|
|
|
|
// 最多尝试 20 个端口,避免无限循环。
|
|
|
|
|
|
for (let i = 0; i < 20; i += 1) {
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
|
const free = await isPortFree("127.0.0.1", port);
|
|
|
|
|
|
if (free) return port;
|
|
|
|
|
|
port += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return startPort;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getListeningPidsByPort(port) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 说明:netstat 输出示例:
|
|
|
|
|
|
// TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 12345
|
|
|
|
|
|
const out = execSync("netstat -ano -p tcp", { encoding: "utf8" });
|
|
|
|
|
|
const pids = new Set();
|
|
|
|
|
|
for (const line of out.split(/\r?\n/)) {
|
|
|
|
|
|
if (!line.includes(`:${port}`)) continue;
|
|
|
|
|
|
if (!/LISTENING/i.test(line)) continue;
|
|
|
|
|
|
const parts = line.trim().split(/\s+/);
|
|
|
|
|
|
const pid = Number(parts[parts.length - 1]);
|
|
|
|
|
|
if (Number.isFinite(pid) && pid > 0) {
|
|
|
|
|
|
pids.add(pid);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return [...pids];
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 08:47:40 +08:00
|
|
|
|
function getProcessNameByPid(pid) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 输出为 CSV,示例:
|
|
|
|
|
|
// "Image Name","PID","Session Name","Session#","Mem Usage"
|
|
|
|
|
|
// "python.exe","12345","Console","1","12,345 K"
|
|
|
|
|
|
const out = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, {
|
|
|
|
|
|
encoding: "utf8",
|
|
|
|
|
|
}).trim();
|
|
|
|
|
|
if (!out || /No tasks are running/i.test(out)) return "";
|
|
|
|
|
|
const firstLine = out.split(/\r?\n/)[0].trim();
|
|
|
|
|
|
if (!firstLine) return "";
|
|
|
|
|
|
const cells = firstLine
|
|
|
|
|
|
.split('","')
|
|
|
|
|
|
.map((s) => s.replace(/^"/, "").replace(/"$/, ""));
|
|
|
|
|
|
return (cells[0] || "").trim();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 10:12:53 +08:00
|
|
|
|
async function ensurePortFree(port, nameForLog) {
|
|
|
|
|
|
const free = await isPortFree("127.0.0.1", port);
|
|
|
|
|
|
if (free) return true;
|
|
|
|
|
|
|
|
|
|
|
|
const pids = getListeningPidsByPort(port);
|
|
|
|
|
|
if (pids.length === 0) {
|
|
|
|
|
|
logPrefix(nameForLog, `检测到端口 ${port} 被占用,但无法定位 PID。`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 08:47:40 +08:00
|
|
|
|
let killPids = pids;
|
|
|
|
|
|
// 安全策略:后端默认只结束 python 进程(避免误杀其他服务)。
|
|
|
|
|
|
if (nameForLog === "backend") {
|
|
|
|
|
|
killPids = pids.filter((pid) => {
|
|
|
|
|
|
const procName = getProcessNameByPid(pid).toLowerCase();
|
|
|
|
|
|
return procName.includes("python");
|
|
|
|
|
|
});
|
|
|
|
|
|
if (killPids.length === 0) {
|
|
|
|
|
|
logPrefix(
|
|
|
|
|
|
nameForLog,
|
|
|
|
|
|
`检测到端口 ${port} 被占用,但没有可安全结束的 python 进程(PIDs=${pids.join(", ")})。请手动释放端口后重试。`,
|
|
|
|
|
|
);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logPrefix(nameForLog, `检测到端口 ${port} 被占用,准备重启(结束旧进程):${killPids.join(", ")}`);
|
|
|
|
|
|
for (const pid of killPids) {
|
2026-01-17 10:12:53 +08:00
|
|
|
|
try {
|
|
|
|
|
|
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 等待端口释放
|
|
|
|
|
|
for (let i = 0; i < 20; i += 1) {
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
|
const ok = await isPortFree("127.0.0.1", port, 250);
|
|
|
|
|
|
if (ok) return true;
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logPrefix(nameForLog, `端口 ${port} 仍未释放,可能有其他程序占用。`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 07:52:40 +08:00
|
|
|
|
function loadEnvFile(filePath) {
|
|
|
|
|
|
if (!fs.existsSync(filePath)) return {};
|
|
|
|
|
|
const content = fs.readFileSync(filePath, "utf8");
|
|
|
|
|
|
return content
|
2026-04-13 19:21:42 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}, {});
|
2025-12-26 07:52:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 08:47:40 +08:00
|
|
|
|
// 约定:全局仅使用仓库根目录的 .env.all 作为配置来源(生产优先)。
|
|
|
|
|
|
const envAllPath = path.join(rootDir, ".env.all");
|
|
|
|
|
|
if (!fs.existsSync(envAllPath)) {
|
|
|
|
|
|
console.error("[system] 缺少 .env.all:请在仓库根目录创建全局唯一 env 文件后重试。");
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const envFromAll = loadEnvFile(envAllPath);
|
2025-12-26 07:52:40 +08:00
|
|
|
|
const mergedEnv = {
|
|
|
|
|
|
...process.env,
|
2026-02-01 08:47:40 +08:00
|
|
|
|
...envFromAll,
|
2025-12-26 07:52:40 +08:00
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
|
function logPrefix(name, message) {
|
|
|
|
|
|
console.log(`[${name}] ${message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 19:21:42 +08:00
|
|
|
|
function getDefaultCeleryPool() {
|
|
|
|
|
|
if (celeryPoolFromEnv) return celeryPoolFromEnv;
|
|
|
|
|
|
if (process.platform === "win32") return "solo";
|
|
|
|
|
|
return "";
|
2025-11-23 10:55:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 19:21:42 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
|
function shutdown(code) {
|
|
|
|
|
|
if (shuttingDown) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
shuttingDown = true;
|
|
|
|
|
|
logPrefix("system", "收到终止信号,正在关闭所有子进程…");
|
|
|
|
|
|
|
|
|
|
|
|
for (const child of children) {
|
2026-02-01 08:47:40 +08:00
|
|
|
|
if (!child.pid) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Windows 下,shell 子进程常常无法可靠传播 SIGINT/SIGTERM 到孙进程(例如 uvicorn --reload)。
|
|
|
|
|
|
// 这里优先用 taskkill /T /F 确保整个进程树被结束,避免残留占用端口导致下次启动失败。
|
|
|
|
|
|
if (process.platform === "win32") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
execSync(`taskkill /PID ${child.pid} /T /F`, { stdio: "ignore" });
|
|
|
|
|
|
continue;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// fallback to signals
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
|
if (!child.killed) {
|
|
|
|
|
|
child.kill("SIGINT");
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (!child.killed) {
|
|
|
|
|
|
child.kill("SIGTERM");
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => process.exit(code), 200);
|
|
|
|
|
|
}
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
|
async function main() {
|
2026-01-17 10:12:53 +08:00
|
|
|
|
// 说明:Next dev 在异常退出时可能残留 `.next/dev/lock`,会导致后续启动直接失败。
|
|
|
|
|
|
// 这里在启动前做一次“安全清理”,避免重启后仍卡住。
|
|
|
|
|
|
const nextDevLockPath = path.join(frontendDir, ".next", "dev", "lock");
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.existsSync(nextDevLockPath)) {
|
|
|
|
|
|
fs.rmSync(nextDevLockPath, { force: true });
|
|
|
|
|
|
logPrefix("frontend", `检测到残留的 Next dev lock,已移除:${nextDevLockPath}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
logPrefix("frontend", `尝试移除 Next dev lock 失败:${error.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 说明:你外网绑定了 3000 端口,这里默认强制使用 3000。
|
|
|
|
|
|
// 如果检测到 3000 被占用,则自动结束旧进程后重启,以保证始终跑在 3000。
|
|
|
|
|
|
const desiredFrontendPort = frontendPortFromEnv;
|
|
|
|
|
|
const frontendPortOk = await ensurePortFree(desiredFrontendPort, "frontend");
|
|
|
|
|
|
if (!frontendPortOk) {
|
|
|
|
|
|
console.error(`前端端口 ${desiredFrontendPort} 无法释放,已中止启动。`);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const frontendPort = desiredFrontendPort;
|
|
|
|
|
|
const frontendUrl = `http://localhost:${frontendPort}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 说明:在 Windows 的 cmd.exe 下,`pnpm dev -- -p 3000` 会把 `--` 原样传给 next,导致 next 把 `-p` 误当成目录。
|
|
|
|
|
|
// 用 `pnpm dev -p 3000` 在 PowerShell/cmd.exe 下都能正确传参。
|
|
|
|
|
|
tasks[0].command = process.env.FRONTEND_CMD || `pnpm dev -p ${frontendPort}`;
|
|
|
|
|
|
logPrefix("frontend", `前端目录:${frontendDir}`);
|
|
|
|
|
|
logPrefix("frontend", `前端地址:${frontendUrl}`);
|
|
|
|
|
|
|
2026-02-01 08:47:40 +08:00
|
|
|
|
const desiredBackendPort = backendPortFromEnv;
|
|
|
|
|
|
if (!process.env.BACKEND_CMD) {
|
|
|
|
|
|
const backendPortOk = await ensurePortFree(desiredBackendPort, "backend");
|
|
|
|
|
|
if (!backendPortOk) {
|
|
|
|
|
|
console.error(`后端端口 ${desiredBackendPort} 无法释放,已中止启动。`);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
tasks[1].command = `${pythonBin} -m uvicorn app.main:app --reload --port ${desiredBackendPort}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
|
if (!skipCelery) {
|
2026-04-13 19:21:42 +08:00
|
|
|
|
const defaultCeleryCommand = buildDefaultCeleryCommand();
|
2025-11-23 10:55:04 +08:00
|
|
|
|
const celeryTask = {
|
|
|
|
|
|
name: "celery",
|
2026-04-13 19:21:42 +08:00
|
|
|
|
command: celeryCmdFromEnv || defaultCeleryCommand,
|
2025-11-23 10:55:04 +08:00
|
|
|
|
cwd: backendDir,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (celeryCmdFromEnv) {
|
|
|
|
|
|
tasks.push(celeryTask);
|
|
|
|
|
|
} else if (await checkRedisReachable(redisUrl)) {
|
2026-04-13 19:21:42 +08:00
|
|
|
|
const defaultPool = getDefaultCeleryPool();
|
|
|
|
|
|
if (defaultPool) {
|
|
|
|
|
|
logPrefix("celery", `未显式设置 CELERY_CMD,当前平台默认使用 worker pool:${defaultPool}`);
|
|
|
|
|
|
}
|
2025-11-23 10:55:04 +08:00
|
|
|
|
tasks.push(celeryTask);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Redis 未就绪时直接跳过 Celery,避免热调试流程整体退出。
|
2026-04-13 19:21:42 +08:00
|
|
|
|
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);
|
|
|
|
|
|
});
|