feat: update webui extensions, models, and agent config
This commit is contained in:
@@ -32,13 +32,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PID_FILE = join(__dirname, ".webui.pid");
|
||||
const DIST_DIR = join(__dirname, "frontend", "dist");
|
||||
const FRONTEND_DIR = join(__dirname, "frontend");
|
||||
const WEBUI_STARTUP_TIMEOUT_MS = process.platform === "win32" ? 20_000 : 10_000;
|
||||
|
||||
let serverProcess: ChildProcess | null = null;
|
||||
let serverPort = DEFAULT_WEBUI_PORT;
|
||||
let systemdConfig: SystemdServiceConfig | null = null;
|
||||
|
||||
function findTsx(): string {
|
||||
// 从扩展所在目录向上找 repo 根目录
|
||||
// On Linux/Mac, use the tsx shebang script directly
|
||||
let dir = __dirname;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const candidate = join(dir, "node_modules", ".bin", "tsx");
|
||||
@@ -47,10 +48,23 @@ function findTsx(): string {
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
// fallback
|
||||
return join(__dirname, "..", "..", "..", "node_modules", ".bin", "tsx");
|
||||
}
|
||||
|
||||
function findTsxMjs(): string {
|
||||
// On Windows, spawn node + tsx/dist/cli.mjs directly to avoid the cmd.exe wrapper
|
||||
// that shell:true introduces and which causes process lifecycle / probe timing issues
|
||||
let dir = __dirname;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const candidate = join(dir, "node_modules", "tsx", "dist", "cli.mjs");
|
||||
if (existsSync(candidate)) return candidate;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return join(__dirname, "..", "..", "..", "node_modules", "tsx", "dist", "cli.mjs");
|
||||
}
|
||||
|
||||
function findRepoRoot(): string {
|
||||
let dir = __dirname;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
@@ -106,7 +120,7 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForStartupByProbe(port: number, timeoutMs = 10000): Promise<void> {
|
||||
async function waitForStartupByProbe(port: number, timeoutMs = WEBUI_STARTUP_TIMEOUT_MS): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if ((await probePort(port)) === "running") return;
|
||||
@@ -150,7 +164,7 @@ function waitForStartup(port: number, child: ChildProcess): Promise<void> {
|
||||
});
|
||||
};
|
||||
|
||||
timeout = setTimeout(() => finish(new Error("webui 启动超时")), 10000);
|
||||
timeout = setTimeout(() => finish(new Error("webui 启动超时")), WEBUI_STARTUP_TIMEOUT_MS);
|
||||
setTimeout(check, 300);
|
||||
});
|
||||
}
|
||||
@@ -174,19 +188,26 @@ function probePort(port: number): Promise<"free" | "running" | "occupied"> {
|
||||
}
|
||||
|
||||
function findNpm(): string {
|
||||
const local = join(dirname(process.execPath), "npm");
|
||||
if (existsSync(local)) return local;
|
||||
// On Windows prefer npm.cmd; the bare 'npm' is a bash script that Windows can't execute
|
||||
const candidates = process.platform === "win32" ? ["npm.cmd", "npm"] : ["npm"];
|
||||
|
||||
for (const name of candidates) {
|
||||
const local = join(dirname(process.execPath), name);
|
||||
if (existsSync(local)) return local;
|
||||
}
|
||||
|
||||
let dir = __dirname;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const candidate = join(dir, "node_modules", ".bin", "npm");
|
||||
if (existsSync(candidate)) return candidate;
|
||||
for (const name of candidates) {
|
||||
const candidate = join(dir, "node_modules", ".bin", name);
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
|
||||
return "npm";
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
function runNpm(
|
||||
@@ -197,6 +218,7 @@ function runNpm(
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
if (result.error) {
|
||||
return { ok: false, status: result.status, error: result.error.message };
|
||||
@@ -286,7 +308,6 @@ async function startServer(port: number, ctx: any): Promise<void> {
|
||||
}
|
||||
|
||||
const repoRoot = findRepoRoot();
|
||||
const tsxBin = findTsx();
|
||||
const serverFile = join(__dirname, "backend", "main.ts");
|
||||
|
||||
if (!existsSync(serverFile)) {
|
||||
@@ -294,7 +315,15 @@ async function startServer(port: number, ctx: any): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
serverProcess = spawn(tsxBin, [serverFile, "--port", String(port)], {
|
||||
// On Windows, spawn node + tsx/dist/cli.mjs directly instead of tsx.cmd via shell:true.
|
||||
// The cmd.exe wrapper (shell:true) makes serverProcess point to cmd.exe, not the real
|
||||
// server process, breaking exit-code detection and causing startup probe timeouts.
|
||||
const [spawnCmd, spawnArgs] =
|
||||
process.platform === "win32"
|
||||
? [process.execPath, [findTsxMjs(), serverFile, "--port", String(port)]]
|
||||
: [findTsx(), [serverFile, "--port", String(port)]];
|
||||
|
||||
serverProcess = spawn(spawnCmd, spawnArgs, {
|
||||
cwd: repoRoot,
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
env: {
|
||||
@@ -315,6 +344,10 @@ async function startServer(port: number, ctx: any): Promise<void> {
|
||||
try {
|
||||
await waitForStartup(port, serverProcess);
|
||||
} catch (err: unknown) {
|
||||
// Kill the orphaned backend process so the port is freed for the next attempt
|
||||
if (serverProcess && serverProcess.exitCode === null) {
|
||||
killProcessTree(serverProcess.pid!);
|
||||
}
|
||||
clearPidFile();
|
||||
serverProcess = null;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
@@ -331,7 +364,7 @@ function notifyLanAddresses(ctx: any, port: number): void {
|
||||
const nets = networkInterfaces();
|
||||
for (const name of Object.keys(nets)) {
|
||||
for (const net of nets[name]) {
|
||||
if (net.family === "IPv4" && !net.internal) {
|
||||
if ((net.family === "IPv4" || net.family === 4) && !net.internal) {
|
||||
ctx.ui.notify(` 局域网: http://${net.address}:${port}`, "info");
|
||||
}
|
||||
}
|
||||
@@ -340,6 +373,19 @@ function notifyLanAddresses(ctx: any, port: number): void {
|
||||
|
||||
function findPidsByPort(port: number): number[] {
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
const out = execSync(`netstat -ano`, { encoding: "utf8" });
|
||||
const pids = new Set<number>();
|
||||
for (const line of out.split(/\r?\n/)) {
|
||||
if (!line.includes(`:${port} `)) continue;
|
||||
const match = line.trim().match(/(\d+)\s*$/);
|
||||
if (match) {
|
||||
const pid = parseInt(match[1], 10);
|
||||
if (Number.isFinite(pid) && pid > 0) pids.add(pid);
|
||||
}
|
||||
}
|
||||
return [...pids];
|
||||
}
|
||||
const out = execSync(`ss -tlnp 'sport = :${port}'`, { encoding: "utf8" });
|
||||
const pids = new Set<number>();
|
||||
for (const match of out.matchAll(/pid=(\d+)/g)) {
|
||||
@@ -354,7 +400,11 @@ function findPidsByPort(port: number): number[] {
|
||||
|
||||
function killProcessTree(pid: number): void {
|
||||
try {
|
||||
process.kill(pid, "SIGTERM");
|
||||
if (process.platform === "win32") {
|
||||
spawnSync("taskkill", ["/F", "/T", "/PID", String(pid)], { encoding: "utf8" });
|
||||
} else {
|
||||
process.kill(pid, "SIGTERM");
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -408,11 +458,12 @@ async function stopServer(ctx: any, port = serverPort || DEFAULT_WEBUI_PORT): Pr
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverProcess) {
|
||||
serverProcess.kill("SIGTERM");
|
||||
if (serverProcess?.pid) {
|
||||
killProcessTree(serverProcess.pid);
|
||||
serverProcess = null;
|
||||
serverPort = 0;
|
||||
clearPidFile();
|
||||
await waitForPortFree(port);
|
||||
ctx.ui.notify("网页服务已停止", "info");
|
||||
return;
|
||||
}
|
||||
@@ -420,7 +471,7 @@ async function stopServer(ctx: any, port = serverPort || DEFAULT_WEBUI_PORT): Pr
|
||||
const pid = readPidFile();
|
||||
if (pid && isProcessAlive(pid)) {
|
||||
try {
|
||||
process.kill(pid, "SIGTERM");
|
||||
killProcessTree(pid);
|
||||
await waitForPortFree(port);
|
||||
clearPidFile();
|
||||
ctx.ui.notify(`网页服务已停止(PID ${pid})`, "info");
|
||||
@@ -479,11 +530,6 @@ export default function (pi: ExtensionAPI) {
|
||||
|
||||
if (ensureSystemdService(systemdConfig, serverPort || DEFAULT_WEBUI_PORT)) {
|
||||
console.log(`[webui] 已注册 systemd 保活服务: ${WEBUI_SERVICE_NAME}`);
|
||||
} else {
|
||||
const reason = getSystemdDisabledReason();
|
||||
if (reason) {
|
||||
console.warn(`[webui] systemd 保活不可用,回退进程内管理: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
pi.registerCommand("webui", {
|
||||
|
||||
Reference in New Issue
Block a user