import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { spawn } from "node:child_process"; import { closeSync, openSync } from "node:fs"; import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { homedir } from "node:os"; import { randomBytes } from "node:crypto"; const DATA_DIR = process.env.PI_FEISHU_AGENT_HOME || join(homedir(), ".pi/agent/feishu-agent"); const CONFIG_PATH = process.env.PI_FEISHU_AGENT_CONFIG || join(DATA_DIR, "config.json"); const LEGACY_CONFIG_PATH = join(homedir(), ".pi/agent/extensions/feishu-bridge/config.json"); const PID_PATH = join(DATA_DIR, "service.pid"); const LOG_PATH = "/tmp/pi-feishu-ws.log"; const SCRIPT_PATH = fileURLToPath(new URL("./ws-standalone.mjs", import.meta.url)); type Config = { app_id: string; app_secret: string; encrypt_key?: string; verification_token?: string; reply_max_chars?: number; allow_all?: boolean; allowed_chat_ids?: string[]; allowed_open_ids?: string[]; pairing_code?: string; }; async function loadConfig(): Promise { for (const path of [CONFIG_PATH, LEGACY_CONFIG_PATH]) { try { return JSON.parse(await readFile(path, "utf8")); } catch {} } return { app_id: "", app_secret: "", encrypt_key: "", verification_token: "", reply_max_chars: 12000 }; } async function saveConfig(config: Config) { await mkdir(dirname(CONFIG_PATH), { recursive: true }); await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }); await chmod(CONFIG_PATH, 0o600); } async function readPid(): Promise { try { const pid = Number((await readFile(PID_PATH, "utf8")).trim()); if (!pid) return null; process.kill(pid, 0); return pid; } catch { return null; } } async function startDetached(cwd: string): Promise { const existing = await readPid(); if (existing) return existing; await mkdir(DATA_DIR, { recursive: true }); const logFd = openSync(LOG_PATH, "a"); const child = spawn(process.execPath, [SCRIPT_PATH], { cwd, detached: true, stdio: ["ignore", logFd, logFd], env: { ...process.env, FEISHU_PI_CWD: cwd }, }); child.unref(); closeSync(logFd); if (!child.pid) throw new Error("Failed to start pi-feishu-agent"); await writeFile(PID_PATH, String(child.pid), { mode: 0o600 }); await chmod(PID_PATH, 0o600); return child.pid; } async function stopDetached(): Promise { const pid = await readPid(); if (!pid) return false; process.kill(pid, "SIGTERM"); await rm(PID_PATH, { force: true }); return true; } async function configure(ctx: ExtensionCommandContext) { const current = await loadConfig(); const appId = await ctx.ui.input("Feishu App ID", current.app_id || "cli_..."); if (appId === undefined) return; const appSecret = await ctx.ui.input( current.app_secret ? "Feishu App Secret(留空保留现有值)" : "Feishu App Secret", current.app_secret ? "留空保留" : "", ); if (appSecret === undefined) return; const verificationToken = await ctx.ui.input("Verification Token(可选,留空保留)", ""); if (verificationToken === undefined) return; const encryptKey = await ctx.ui.input("Encrypt Key(可选,留空保留)", ""); if (encryptKey === undefined) return; await saveConfig({ ...current, app_id: appId.trim() || current.app_id, app_secret: appSecret.trim() && appSecret !== "留空保留" ? appSecret.trim() : current.app_secret, verification_token: verificationToken.trim() || current.verification_token || "", encrypt_key: encryptKey.trim() || current.encrypt_key || "", reply_max_chars: current.reply_max_chars || 12000, allow_all: current.allow_all === true, allowed_chat_ids: current.allowed_chat_ids || [], allowed_open_ids: current.allowed_open_ids || [], pairing_code: current.pairing_code || randomBytes(8).toString("hex"), }); const saved = await loadConfig(); ctx.ui.notify(`配置已保存:${CONFIG_PATH}\n配对码:${saved.pairing_code}\n在目标飞书会话发送:/pair ${saved.pairing_code}`, "info"); } export default function (pi: ExtensionAPI) { const start = async (_args: string, ctx: ExtensionCommandContext) => { const config = await loadConfig(); if (!config.app_id || !config.app_secret) { ctx.ui.notify("缺少飞书配置,请先运行 /feishu-agent-config", "error"); return; } if (process.platform === "darwin") { const result = await pi.exec("launchctl", ["print", `gui/${process.getuid()}/com.pi-feishu-agent`]); if (result.code === 0) { ctx.ui.notify("Pi Feishu Agent 已由 launchd 管理,无需重复启动。", "info"); return; } } const pid = await startDetached(ctx.cwd); ctx.ui.notify(`Pi Feishu Agent 已启动(PID ${pid})\n日志:${LOG_PATH}`, "info"); }; const stop = async (_args: string, ctx: ExtensionCommandContext) => { if (process.platform === "darwin") { const result = await pi.exec("launchctl", ["print", `gui/${process.getuid()}/com.pi-feishu-agent`]); if (result.code === 0) { ctx.ui.notify("服务由 launchd 管理,请运行:pi-feishu-agent uninstall-service", "info"); return; } } const stopped = await stopDetached(); ctx.ui.notify(stopped ? "已停止 Pi Feishu Agent" : "服务未运行", "info"); }; const status = async (_args: string, ctx: ExtensionCommandContext) => { const pid = await readPid(); const config = await loadConfig(); let launchd = false; if (process.platform === "darwin") { const result = await pi.exec("launchctl", ["print", `gui/${process.getuid()}/com.pi-feishu-agent`]); launchd = result.code === 0; } ctx.ui.notify([ `status: ${launchd ? "running (launchd)" : pid ? `running (PID ${pid})` : "stopped"}`, `config: ${config.app_id && config.app_secret ? "ready" : "missing"}`, `cwd: ${ctx.cwd}`, `log: ${LOG_PATH}`, ].join("\n"), "info"); }; pi.registerCommand("feishu-agent-config", { description: "Configure Pi Feishu Agent", handler: configure }); pi.registerCommand("feishu-agent-start", { description: "Start Pi Feishu Agent", handler: start }); pi.registerCommand("feishu-agent-stop", { description: "Stop Pi Feishu Agent", handler: stop }); pi.registerCommand("feishu-agent-status", { description: "Show Pi Feishu Agent status", handler: status }); // Backward-compatible aliases for the local prototype. pi.registerCommand("feishu-config", { description: "Configure Pi Feishu Agent", handler: configure }); pi.registerCommand("feishu-start", { description: "Start Pi Feishu Agent", handler: start }); pi.registerCommand("feishu-stop", { description: "Stop Pi Feishu Agent", handler: stop }); pi.registerCommand("feishu-status", { description: "Show Pi Feishu Agent status", handler: status }); pi.registerCommand("feishu-agent-doctor", { description: "Diagnose Pi Feishu Agent", handler: async (_args, ctx) => { try { await access(SCRIPT_PATH); const result = await pi.exec(process.execPath, [SCRIPT_PATH, "doctor"], { timeout: 90_000 }); ctx.ui.notify((result.stdout || result.stderr || "doctor completed").trim(), result.code === 0 ? "info" : "error"); } catch (err: any) { ctx.ui.notify(`doctor failed: ${err.message}`, "error"); } }, }); }