chore: init monorepo snapshot

This commit is contained in:
liaibo
2025-11-23 10:55:04 +08:00
commit c70ff52869
941 changed files with 246586 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
#!/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);
});