feat: add pi lab ai management integration

This commit is contained in:
Agent Board
2026-07-04 21:47:42 +08:00
parent 36d027a4a1
commit c8472bb898
62 changed files with 15077 additions and 443 deletions
+112
View File
@@ -415,6 +415,35 @@ function getProcessNameByPid(pid) {
}
}
function getProcessCommandByPid(pid) {
if (process.platform === "win32") {
try {
const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value`, {
encoding: "utf8",
});
return out.replace(/^CommandLine=/m, "").trim();
} catch {
return "";
}
}
try {
return execSync(`ps -p ${pid} -o args=`, { encoding: "utf8" }).trim();
} catch {
return "";
}
}
function getProcessGroupByPid(pid) {
if (process.platform === "win32") return pid;
try {
const pgid = Number(execSync(`ps -p ${pid} -o pgid=`, { encoding: "utf8" }).trim());
return Number.isFinite(pgid) && pgid > 0 ? pgid : pid;
} catch {
return pid;
}
}
function terminatePid(pid) {
if (process.platform === "win32") {
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
@@ -441,6 +470,84 @@ function forceKillPid(pid) {
}
}
function terminateProcessGroup(pgid) {
if (process.platform === "win32") {
terminatePid(pgid);
return;
}
try {
process.kill(-pgid, "SIGTERM");
} catch {
terminatePid(pgid);
}
}
function forceKillProcessGroup(pgid) {
if (process.platform === "win32") {
forceKillPid(pgid);
return;
}
try {
process.kill(-pgid, "SIGKILL");
} catch {
forceKillPid(pgid);
}
}
function collectStaleMnoteWebCargoPids() {
if (process.platform === "win32") return [];
const currentPgid = getProcessGroupByPid(process.pid);
try {
const out = execSync("pgrep -f 'cargo (watch|run).*mnote-web|cargo-watch watch.*mnote-web'", {
encoding: "utf8",
});
return out
.split(/\r?\n/)
.map((line) => Number(line.trim()))
.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid)
.filter((pid) => getProcessGroupByPid(pid) !== currentPgid)
.filter((pid) => {
const command = getProcessCommandByPid(pid);
return !command.includes("pgrep -f");
});
} catch {
return [];
}
}
async function stopStaleMnoteWebCargoProcesses() {
const pids = collectStaleMnoteWebCargoPids();
if (pids.length === 0) return true;
const safePids = pids.filter((pid) => {
const command = getProcessCommandByPid(pid);
return (
command.includes("mnote-web") &&
command.includes("cargo") &&
(command.includes("cargo watch") ||
command.includes("cargo-watch watch") ||
command.includes("cargo run -p mnote-web"))
);
});
if (safePids.length === 0) return true;
const pgids = [...new Set(safePids.map(getProcessGroupByPid).filter((pgid) => pgid > 0))];
logPrefix("mnote-web", `检测到陈旧 cargo-watch/cargo run,先清理进程组:${pgids.join(", ")}`);
pgids.forEach(terminateProcessGroup);
for (let i = 0; i < 20; i += 1) {
// eslint-disable-next-line no-await-in-loop
await new Promise((r) => setTimeout(r, 150));
const remaining = collectStaleMnoteWebCargoPids().filter((pid) =>
safePids.includes(pid) || pgids.includes(getProcessGroupByPid(pid)),
);
if (remaining.length === 0) return true;
}
pgids.forEach(forceKillProcessGroup);
return true;
}
async function ensurePortFree(port, nameForLog) {
const free = await isPortFree("127.0.0.1", port);
if (free) return true;
@@ -665,6 +772,7 @@ async function main() {
// 如果检测到 3000 被占用,则自动结束旧进程后重启,以保证始终跑在 3000。
const desiredFrontendPort = runtimePlan.publicPort;
const frontendOwnerName = "mnote-web";
await stopStaleMnoteWebCargoProcesses();
const frontendPortOk = await ensurePortFree(desiredFrontendPort, frontendOwnerName);
if (!frontendPortOk) {
console.error(`前端端口 ${desiredFrontendPort} 无法释放,已中止启动。`);
@@ -762,8 +870,11 @@ module.exports = {
buildDefaultOpencodeCommand,
buildDefaultOpenHubCommand,
checkOpenHubHealth,
collectStaleMnoteWebCargoPids,
ensurePortFree,
getListeningPidsByPort,
getProcessCommandByPid,
getProcessGroupByPid,
getProcessNameByPid,
isHttpHealthy,
isPortFree,
@@ -772,5 +883,6 @@ module.exports = {
resolveBackendExecutable,
shouldStartBackend,
shouldStartOpenHub,
stopStaleMnoteWebCargoProcesses,
terminatePid,
};