import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { spawnSync } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); function ensurePiKbRoot(): string { process.env.PI_KB_ROOT = packageRoot; return packageRoot; } const HINT = ` ## @suwenguang/pi-kb - 包根环境变量:\`PI_KB_ROOT\`(脚本:\`node "$PI_KB_ROOT/scripts/...."\`) - 斜杠命令:\`/kb-propose\`、\`/kb-design\`、\`/kb-plan\`、\`/kb-apply\`、\`/kb-archive\`、\`/kb-feedback\`、\`/kb-session-retro\`、\`/kb-evolve-setup\`、\`/kb-evomap-setup\`、\`/kb-deepseek-search-setup\`、\`/kb-cursor-setup\`、\`/kb-figma-setup\` 等(prompt templates) - 流程反馈:\`/kb-feedback\` → Gitee Issue;**会话级失败经验** → \`/kb-session-retro\`(自动开 Issue);本地改流程:\`/kb-evolve-setup\` 后源码仓 \`/kb-evolve\`;可选网络路径:\`/kb-evomap-setup\`(默认关闭,见 skills/.../kb-evomap.md) - DeepSeek Search:本包已 bundled \`pi-deepseek-search\`(工具 \`web_search\`);需 DeepSeek Key(\`/login\` 或 \`DEEPSEEK_API_KEY\`);配置引导 \`/kb-deepseek-search-setup\`;实践见 skills/.../kb-deepseek-search.md - Figma Remote MCP:本包已 bundled \`pi-figma-remote-auth\`(\`/figma-remote-auth\`)+ token 同步(\`/figma-oauth-sync\`,session_start 自动);引导 \`/kb-figma-setup\`;实践见 skills/.../kb-figma-remote.md - Cursor Agent:可选;业务仓 \`pi install npm:pi-cursor-sdk -l --approve\` + Cursor API Key;引导 \`/kb-cursor-setup\`;实践见 skills/.../kb-cursor-sdk.md(**不** bundled,因平台二进制) - Gitee token:\`export GITEE_ACCESS_TOKEN=...\` 或一次配置 \`node "$PI_KB_ROOT/scripts/kb-config.mjs" set-token\` - 工种 Agent:本包已 bundled \`pi-subagents\`;定义在 \`agents/\`(\`subagent(agent=kb-*)\`) - 业务仓初始化:\`node "$PI_KB_ROOT/scripts/kb-bootstrap.mjs" --target "$(pwd)"\` 或 \`/kb-init\` - 未初始化门禁:除 \`/kb-init\` 与反馈类(\`/kb-feedback\`、\`/kb-session-retro\`)及进化类(\`/kb-evolve*\`、\`/kb-evomap-setup\`)与 \`/kb-deepseek-search-setup\`、\`/kb-cursor-setup\`、\`/kb-figma-setup\` 外,\`/kb-*\` 须先通过 \`kb-bootstrap-check.mjs\` `.trim(); const DEEPSEEK_SETUP_TIP = "DeepSeek Search 未就绪:请 /login 选择 DeepSeek,或设置 DEEPSEEK_API_KEY,然后 /reload。详情:/kb-deepseek-search-setup"; async function hasDeepSeekKey(ctx: ExtensionContext): Promise { if (process.env.DEEPSEEK_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) return true; try { const key = await ctx.modelRegistry?.getApiKeyForProvider?.("deepseek"); return Boolean(key); } catch { return false; } } /** 启发式:本轮对话是否像 KB 流程摩擦/失败(非编码业务 bug) */ const FRICTION_RE = /门禁失败|硬阻断|bootstrap-check|codegraph-check|manifest.*校验失败|阶段.*阻断|流程摩擦|禁止继续|kb-bootstrap|schema 失败|未通过.*门禁|\/kb-[\w-]+.*失败|重复.*\/kb-|口径歧义|步骤繁琐|不得.*archive|不得标\s*tested/i; const RETRO_CMD_RE = /\/kb-session-retro\b/; function messageText(message: unknown): string { if (!message || typeof message !== "object") return ""; const m = message as { content?: unknown; role?: string }; const c = m.content; if (typeof c === "string") return c; if (!Array.isArray(c)) return ""; return c .map((part) => { if (!part || typeof part !== "object") return ""; const p = part as { type?: string; text?: string }; return p.type === "text" && typeof p.text === "string" ? p.text : ""; }) .join("\n"); } /** 旁路写入摩擦候选(非阻塞;失败忽略) */ function recordFrictionCandidate(snippet: string, commandHint = ""): boolean { const script = path.join(packageRoot, "scripts", "kb-feedback-candidates.mjs"); const args = [script, "add", "--snippet", snippet.slice(0, 400)]; if (commandHint) args.push("--command", commandHint); try { const r = spawnSync(process.execPath, args, { cwd: process.cwd(), encoding: "utf8", timeout: 4000, env: process.env, }); if (r.status !== 0) return false; const out = (r.stdout || "").trim(); if (!out) return false; try { const parsed = JSON.parse(out) as { ok?: boolean; deduped?: boolean }; return Boolean(parsed.ok) && !parsed.deduped; } catch { return true; } } catch { return false; } } function extractCommandHint(text: string): string { const m = text.match(/\/kb-[\w-]+/); return m ? m[0] : ""; } export default function (pi: ExtensionAPI) { ensurePiKbRoot(); let kbFrictionLikely = false; let retroDone = false; let candidateNotified = false; /** @type {Set} */ const seenSnippets = new Set(); function resetSessionFlags() { kbFrictionLikely = false; retroDone = false; candidateNotified = false; seenSnippets.clear(); } pi.on("session_start", async (_event, ctx) => { ensurePiKbRoot(); resetSessionFlags(); // 引导配置:无 DeepSeek Key 时提示(pi-deepseek-search 无 Key 则不注册 web_search) if (!(await hasDeepSeekKey(ctx))) { if (ctx.hasUI) ctx.ui.notify(DEEPSEEK_SETUP_TIP, "info"); else console.warn(`[pi-kb] ${DEEPSEEK_SETUP_TIP}`); } }); pi.on("before_agent_start", async (event) => { const root = ensurePiKbRoot(); if (event.systemPrompt.includes("PI_KB_ROOT=")) { return; } return { systemPrompt: `${event.systemPrompt}\n\n${HINT}\n- PI_KB_ROOT=${root}\n`, }; }); pi.on("input", async (event) => { if (RETRO_CMD_RE.test(event.text)) { retroDone = true; } }); pi.on("agent_end", async (event, ctx) => { let newlyRecorded = false; for (const msg of event.messages ?? []) { const text = messageText(msg); if (!text) continue; if (RETRO_CMD_RE.test(text) && /会话回顾|Issue\s*#|无明显 KB 流程摩擦/.test(text)) { retroDone = true; } if (!FRICTION_RE.test(text)) continue; kbFrictionLikely = true; const key = text.slice(0, 160); if (seenSnippets.has(key)) continue; seenSnippets.add(key); if (recordFrictionCandidate(text, extractCommandHint(text))) { newlyRecorded = true; } } if (newlyRecorded && !candidateNotified && !retroDone) { candidateNotified = true; const tip = "已旁路记录 KB 流程摩擦候选(不阻断)。离开会话时将自动 /kb-session-retro 开 Issue。"; if (ctx?.hasUI) ctx.ui.notify(tip, "info"); else console.warn(`[pi-kb] ${tip}`); } }); async function offerSessionRetro( ctx: ExtensionContext, reason: string, ): Promise<{ cancel?: boolean } | void> { if (!kbFrictionLikely || retroDone) return; // 零确认:取消 /new|/resume,立刻注入会话回顾并自动开 Issue try { pi.sendUserMessage( "/kb-session-retro\n(由扩展在离开会话前自动触发,无需确认:请总结本会话 KB 流程失败经验,合并旁路候选并对可改进项自动提交 Gitee Issue。)", ); if (ctx.hasUI) { ctx.ui.notify("已自动注入 /kb-session-retro(检测到流程摩擦)", "info"); } else { console.warn(`[pi-kb] 已自动注入 /kb-session-retro(${reason})`); } } catch { const tip = "自动注入失败,请手动执行 /kb-session-retro"; if (ctx.hasUI) ctx.ui.notify(tip, "warning"); else console.warn(`[pi-kb] ${tip}`); return; } return { cancel: true }; } pi.on("session_before_switch", async (event, ctx) => { if (event.reason !== "new" && event.reason !== "resume") return; return offerSessionRetro(ctx, event.reason); }); pi.on("session_shutdown", async (event, ctx) => { if (event.reason !== "quit") return; if (!kbFrictionLikely || retroDone) return; if (ctx.hasUI) { ctx.ui.notify("本会话疑似有 KB 流程摩擦,下次可开会话执行 /kb-session-retro 自动反馈", "warning"); } }); pi.registerCommand("kb-root", { description: "打印 @suwenguang/pi-kb 包根路径(PI_KB_ROOT)", handler: async (_args, ctx) => { const root = ensurePiKbRoot(); if (ctx.hasUI) ctx.ui.notify(root, "info"); else console.log(root); }, }); }