/** * models-config.ts — extension entry for the visual config panel. * * /providers → start local HTTP server (if not running) + open browser. * session_shutdown → stop server. * * On-demand: server only runs while pi is running AND user opened the panel. */ import { spawn } from "node:child_process"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { startServer, PANEL_PORT, getOrCreatePanelToken, isPortUp, type ServerHandle, } from "./server.js"; let handle: ServerHandle | null = null; /** 关掉其他进程开的旧 server (POST /api/shutdown), 等端口释放。 */ async function shutdownRemoteServer(port: number, token: string): Promise { try { await fetch(`http://127.0.0.1:${port}/api/shutdown`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); } catch { // server 可能已关闭, 忽略 } // 等端口释放 (最多 2s) for (let i = 0; i < 20; i++) { if (!(await isPortUp(port))) return; await new Promise((r) => setTimeout(r, 100)); } } function openBrowser(url: string) { // best-effort, never throws, detached so it survives pi exit try { const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open"; const args = process.platform === "win32" ? ["/c", "start", "", url] : [url]; const child = spawn(cmd, args, { detached: true, stdio: "ignore" }); child.unref(); } catch { // ignore — user can copy the URL manually } } export default function (pi: ExtensionAPI) { pi.registerCommand("providers", { description: "可视化配置 models.json (本地网页)", handler: async (_args, ctx) => { if (ctx.hasUI === false) { ctx.ui.notify("/providers 需要交互模式", "error"); return; } if (handle) { ctx.ui.notify(`配置面板已在运行: ${handle.url}`, "info"); openBrowser(handle.url); return; } // 其他 pi CLI 可能已开了面板 server。关掉旧的, 用本进程代码起新的, // 确保面板始终跑在当前 CLI 进程加载的最新扩展代码上。 if (await isPortUp(PANEL_PORT)) { await shutdownRemoteServer(PANEL_PORT, getOrCreatePanelToken()); } try { handle = await startServer(() => { handle = null; }, () => pi.events.emit("roundrobin:config-changed", undefined)); ctx.ui.notify(`配置面板已启动 → ${handle.url}`, "info"); openBrowser(handle.url); } catch (e) { ctx.ui.notify(`启动失败: ${e instanceof Error ? e.message : e}`, "error"); handle = null; } }, }); pi.on("session_shutdown", async () => { if (handle) { try { await handle.close(); } catch { // ignore } handle = null; } }); }