feat: update webui extensions, models, and agent config
Some checks failed
CI / build-check-test (push) Has been cancelled
npm audit / audit (push) Has been cancelled

This commit is contained in:
2026-06-10 21:45:53 +08:00
parent cf5edd6394
commit d51a055d78
64 changed files with 3238 additions and 1665 deletions

View File

@@ -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", {