0.4.0 convex及界面修改

This commit is contained in:
liaibo
2026-02-01 08:47:40 +08:00
parent d1f055f51a
commit af92c4b149
636 changed files with 7522 additions and 1815 deletions
+70 -8
View File
@@ -30,6 +30,7 @@ const skipCelery =
const celeryCmdFromEnv = process.env.CELERY_CMD;
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379/0";
const frontendPortFromEnv = Number(process.env.FRONTEND_PORT || 3000);
const backendPortFromEnv = Number(process.env.BACKEND_PORT || 8000);
const tasks = [
{
@@ -106,6 +107,26 @@ function getListeningPidsByPort(port) {
}
}
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 "";
}
}
async function ensurePortFree(port, nameForLog) {
const free = await isPortFree("127.0.0.1", port);
if (free) return true;
@@ -116,8 +137,24 @@ async function ensurePortFree(port, nameForLog) {
return false;
}
logPrefix(nameForLog, `检测到端口 ${port} 被占用,准备重启(结束旧进程):${pids.join(", ")}`);
for (const pid of pids) {
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) {
try {
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
} catch {
@@ -154,14 +191,16 @@ function loadEnvFile(filePath) {
}, {});
}
const envFromRoot = loadEnvFile(path.join(rootDir, ".env.local"));
const envFromFrontend = loadEnvFile(path.join(frontendDir, ".env.local"));
const envFromBackend = loadEnvFile(path.join(backendDir, ".env.local"));
// 约定:全局仅使用仓库根目录的 .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);
const mergedEnv = {
...process.env,
...envFromRoot,
...envFromFrontend,
...envFromBackend,
...envFromAll,
};
function logPrefix(name, message) {
@@ -203,6 +242,19 @@ function shutdown(code) {
logPrefix("system", "收到终止信号,正在关闭所有子进程…");
for (const child of children) {
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
}
}
if (!child.killed) {
child.kill("SIGINT");
setTimeout(() => {
@@ -279,6 +331,16 @@ async function main() {
logPrefix("frontend", `前端目录:${frontendDir}`);
logPrefix("frontend", `前端地址:${frontendUrl}`);
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}`;
}
if (!skipCelery) {
const celeryTask = {
name: "celery",