/** * /db 命令 —— 数据库工作空间入口。 * * 轻量路由器:委托给各子命令处理器模块。 */ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, } from "@earendil-works/pi-coding-agent"; import { DynamicBorder } from "@earendil-works/pi-coding-agent"; import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui"; import type { DatabaseWorkspaceService } from "../state/workspace"; import { STATUS_KEY, handleSwitch } from "./switch"; import { handleAdd } from "./add"; import { handleTables } from "./tables"; import { handleSchema } from "./schema"; import { handleQuery } from "./query"; import { handleHistory } from "./history"; import { handleFavorite } from "./favorites"; import { handleRelations } from "./relations"; import { RelatedBrowserCacheStore, openRelatedBrowser } from "./related-browser"; import { sendDbStatus } from "./llm-context"; import { writeToggle } from "../state/extension-toggle"; // ====== 自动补全项类型(结构上匹配 pi-tui AutocompleteItem)====== interface AutocompleteItem { value: string; label: string; description?: string; } // ====== 命令注册 ====== const SUBCOMMANDS = [ "switch", "add", "tables", "schema", "query", "history", "favorite", "relations", "related", "on", "off", ] as const; export function registerDbCommand( pi: ExtensionAPI, getWorkspace: () => DatabaseWorkspaceService, enabled: boolean, toggleBaseDir: string, ): void { // 禁用态:只注册精简版 /db——唯一入口是 on(重新启用)。 // 命令不进模型上下文,零成本;不初始化 workspace(on 分支不需要)。 if (!enabled) { pi.registerCommand("db", { description: "Database extension is disabled. Run /db on to enable.", getArgumentCompletions: async (prefix) => { const sub = prefix.trim().split(/\s+/)[0] ?? ""; return ["on"].filter((s) => s.startsWith(sub)).map((s) => ({ value: `${s} `, label: s })); }, handler: async (args, ctx) => { if (!ctx.hasUI) return; const sub = args.trim().split(/\s+/).find(Boolean); if (sub === "on") { await handleToggle(ctx, true, toggleBaseDir); } else { ctx.ui.notify("数据库扩展已禁用,运行 /db on 启用。", "warning"); } }, }); return; } // 最近关联查询缓存——注册时创建一次,注入 query 路径写入、 // related 路径读取(替代 module 级单例,见 AGENTS.md 注入接缝)。 const relatedCache = new RelatedBrowserCacheStore(); pi.registerCommand("db", { description: "Database workspace: /db (panel) | switch | add | tables | schema | query [table] | history [kw] | favorite | relations | related | on | off", getArgumentCompletions: async (prefix) => { return getCompletions(prefix, getWorkspace()); }, handler: async (args, ctx) => { // 无 UI 时(print/json 模式)对话框是空操作——提前退出。 if (!ctx.hasUI) return; const ws = getWorkspace(); const [sub, ...rest] = args.trim().split(/\s+/).filter(Boolean); switch (sub) { case undefined: { // 在展示交互式仪表盘之前发送静默 LLM 上下文 sendDbStatus(pi, ws); const action = await showDashboard(ctx, ws); if (!action) return; await dispatchAction(action, ctx, ws, pi, rest, relatedCache); break; } case "switch": await handleSwitch(ctx, ws, pi); break; case "add": await handleAdd(ctx, ws); break; case "tables": await handleTables(ctx, ws, pi); break; case "schema": await handleSchema(ctx, ws, pi, rest[0]); break; case "query": await handleQuery(ctx, ws, pi, rest.join(" ") || undefined, relatedCache); break; case "history": await handleHistory(ctx, ws, pi, rest[0]); break; case "favorite": await handleFavorite(ctx, ws, pi, rest); break; case "relations": await handleRelations(ctx, ws, pi, rest); break; case "related": await handleRelatedBrowser(ctx, relatedCache); break; case "on": await handleToggle(ctx, true, toggleBaseDir); break; case "off": await handleToggle(ctx, false, toggleBaseDir); break; default: ctx.ui.notify(`未知命令: ${sub}。可用:${SUBCOMMANDS.join(", ")}`, "warning"); } }, }); } // ====== 关联表浏览器 ================================================ /** 打开最近一次关联查询的浏览器;无缓存时提示引导。 */ async function handleRelatedBrowser( ctx: ExtensionCommandContext, relatedCache: RelatedBrowserCacheStore, ): Promise { const cache = relatedCache.get(); if (!cache) { ctx.ui.notify( "没有可浏览的关联表——先执行关联表查询(/db query 选表后选择「📎 是,一起查询关联表」)", "info", ); return; } await openRelatedBrowser(ctx, cache); } // ====== 参数补全 ====== export async function getCompletions( prefix: string, ws: DatabaseWorkspaceService, ): Promise { const parts = prefix.trim().split(/\s+/); const hasTrailingSpace = prefix.endsWith(" "); const sub = parts[0]; const partial = hasTrailingSpace ? "" : (parts[1] ?? ""); const subSubs: Record = { favorite: ["add"], relations: ["add", "remove", "discover"], }; // 当第一个词与拥有子子命令的子命令完全匹配时,立即显示第二层。 // 同时覆盖 "relations"(输入中)和 "relations "(Tab 补全后带尾随 // 空格,可能被 pi 剥掉)两种情况。 if (parts.length === 1 && SUBCOMMANDS.includes(sub as any) && sub in subSubs) { return subSubs[sub] .filter((s) => s.startsWith(partial)) .map((s) => ({ value: `${sub} ${s} `, label: s })); } // 表名参数(schema、query)——在第一级部分匹配之前触发, // 避免精确子命令匹配自引用。 const takesTable = sub === "schema" || sub === "query"; if (takesTable && ws.isReady) { try { const tables = await ws.getTables(); const tablePartial = partial; const valuePrefix = `${sub} `; return tables .filter((t) => t.toLowerCase().startsWith(tablePartial.toLowerCase())) .map((t) => ({ value: `${valuePrefix}${t}`, label: t })); } catch { return null; } } // 第一个参数:子命令名(部分匹配)。 if (parts.length === 1 && !hasTrailingSpace) { return SUBCOMMANDS.filter((s) => s.startsWith(sub)).map((s) => ({ value: s + " ", label: s, })); } // 输入中的子子命令过滤(如 "relations a" → "add")。 if (subSubs[sub] && parts.length === 2 && !hasTrailingSpace) { const filtered = subSubs[sub].filter((s) => s.startsWith(partial)); if (filtered.length > 0) { return filtered.map((s) => ({ value: `${sub} ${s} `, label: s })); } } return null; } // ====== 仪表盘 ================================================ interface DashboardAction { value: string; label: string; /** 该操作是否需要已连接的数据库 */ needsConnection?: boolean; } const DASHBOARD_ACTIONS: DashboardAction[] = [ { value: "switch", label: "🔄 切换环境/数据库", needsConnection: false }, { value: "add", label: "➕ 添加新连接", needsConnection: false }, { value: "tables", label: "📋 浏览数据表", needsConnection: true }, { value: "schema", label: "🔍 查看表结构", needsConnection: true }, { value: "query", label: "💬 SQL 查询", needsConnection: true }, { value: "history", label: "📜 查询历史", needsConnection: true }, { value: "favorite", label: "⭐ 收藏查询", needsConnection: true }, { value: "relations", label: "🔗 表关联关系", needsConnection: true }, { value: "related", label: "📎 关联表浏览器", needsConnection: false }, ]; /** 构建交互式仪表盘组件。 */ async function showDashboard( ctx: ExtensionCommandContext, ws: DatabaseWorkspaceService, ): Promise { // 构建状态行 let statusLines: string[] = []; if (ws.current) { const conn = ws.getCurrentConnection(); statusLines = [ `📡 环境:${ws.current.environment}`, `⚡ 连接:${ws.current.connectionId}(${conn?.host ?? "?"})`, `🗃️ 数据库:${ws.current.database}`, ]; } else if (ws.isConfigured) { const envs = ws.getEnvironments(); statusLines = [`⚡ 可用环境:${envs.join(", ")}`]; } else { statusLines = ["⚠️ 尚未配置数据库连接", `配置文件:${ws.configPath}`]; } // 构建操作项——未连接时禁用需要连接的操作 const items: SelectItem[] = DASHBOARD_ACTIONS.map((a) => ({ value: a.value, label: a.label, description: a.needsConnection && !ws.isReady ? "(需要先连接数据库)" : undefined, })); // 警告 const warnings = ws.getConfigWarnings(); const showWarnings = warnings.length > 0; return ctx.ui.custom((tui, theme, _kb, done) => { const container = new Container(); // 上边框 container.addChild(new DynamicBorder((s) => theme.fg("accent", s))); // 表头 container.addChild(new Text(theme.fg("accent", theme.bold("🗄 数据库工作区")), 1, 0)); // 状态 for (const line of statusLines) { container.addChild(new Text(theme.fg("dim", ` ${line}`), 1, 0)); } // 警告 if (showWarnings) { for (const w of warnings) { container.addChild(new Text(theme.fg("warning", ` ⚠ ${w}`), 1, 0)); } } // 分隔线 container.addChild(new Text(theme.fg("dim", " ─── 操作 ─────────────────"), 1, 0)); // 操作列表 const selectList = new SelectList(items, Math.min(items.length, 12), { selectedPrefix: (t) => theme.fg("accent", t), selectedText: (t) => theme.fg("accent", t), description: (t) => theme.fg("muted", t), scrollInfo: (t) => theme.fg("dim", t), noMatch: (t) => theme.fg("warning", t), }); selectList.onSelect = (item) => { const action = DASHBOARD_ACTIONS.find((a) => a.value === item.value); if (action?.needsConnection && !ws.isReady) { return; } done(item.value); }; selectList.onCancel = () => done(void 0); container.addChild(selectList); // 下边框 container.addChild(new DynamicBorder((s) => theme.fg("accent", s))); return { render: (w) => container.render(w), invalidate: () => container.invalidate(), handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); }, }; }); } /** 将仪表盘操作路由到正确的处理器。 */ async function dispatchAction( action: string, ctx: ExtensionCommandContext, ws: DatabaseWorkspaceService, pi: ExtensionAPI, rest: string[], relatedCache: RelatedBrowserCacheStore, ): Promise { switch (action) { case "switch": await handleSwitch(ctx, ws, pi); break; case "add": await handleAdd(ctx, ws); break; case "tables": await handleTables(ctx, ws, pi); break; case "schema": await handleSchema(ctx, ws, pi, rest[0]); break; case "query": await handleQuery(ctx, ws, pi, rest.join(" ") || undefined, relatedCache); break; case "history": await handleHistory(ctx, ws, pi, rest[0]); break; case "favorite": await handleFavorite(ctx, ws, pi, rest); break; case "relations": await handleRelations(ctx, ws, pi, rest); break; case "related": await handleRelatedBrowser(ctx, relatedCache); break; } } // ====== 开关 ================================================ /** 切换扩展启用状态:写标志后 reload,工厂重跑自然生效(旧 ctx 此后失效)。 */ async function handleToggle( ctx: ExtensionCommandContext, enabled: boolean, baseDir: string, ): Promise { writeToggle(baseDir, enabled); ctx.ui.notify(`数据库扩展已${enabled ? "启用" : "禁用"},正在重载…`, "info"); await ctx.reload(); } // ====== 状态栏辅助 ====== /** 会话开始时恢复状态栏。 */ export function restoreStatusBar(ws: DatabaseWorkspaceService, ctx: ExtensionContext): void { if (ws.isReady) { ctx.ui.setStatus(STATUS_KEY, ws.statusLabel); ctx.ui.setWidget(STATUS_KEY, [ `🗄 ${ws.current!.environment}/${ws.current!.database} @${ws.current!.connectionId}`, ]); } }