From 0bec27ce59575ba87050e6d171904803643bd53a Mon Sep 17 00:00:00 2001 From: zavier <765324639@qq.com> Date: Mon, 3 Aug 2026 23:08:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20=E5=B7=A5=E5=85=B7=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E6=8A=98=E5=8F=A0=E4=B8=BA=E4=B8=80=E8=A1=8C=E6=91=98?= =?UTF-8?q?=E8=A6=81=EF=BC=8Cctrl+o=20=E5=B1=95=E5=BC=80=E5=85=A8=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit db_query/db_tables/db_discover/db_tools 此前未定义 renderResult, pi 默认把 content 全文展示在页面上,表数量多或 schema 大时刷屏。 新增 tools/tool-result-summary.ts(纯函数,按 details 形状生成中文 摘要,未知形状返回 undefined)+ tools/tool-result-render.ts(薄层 渲染器工厂:isPartial 显示处理中、错误红色展示、展开态全文、折叠态 摘要 + keyHint 提示)。db_tables 的 details 补充 columnCount/ indexCount,db_discover 补充 databaseCount 供摘要使用。渲染器抛错 时 pi 自动回退默认渲染,无兜底负担。 --- __tests__/tool-result-summary.test.ts | 135 ++++++++++++++++++++++++++ tools/db-tools.ts | 22 +++-- tools/tool-result-render.ts | 60 ++++++++++++ tools/tool-result-summary.ts | 94 ++++++++++++++++++ 4 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 __tests__/tool-result-summary.test.ts create mode 100644 tools/tool-result-render.ts create mode 100644 tools/tool-result-summary.ts diff --git a/__tests__/tool-result-summary.test.ts b/__tests__/tool-result-summary.test.ts new file mode 100644 index 0000000..f7f7411 --- /dev/null +++ b/__tests__/tool-result-summary.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect } from "vitest"; +import { summarizeDbToolResult } from "../tools/tool-result-summary"; + +describe("summarizeDbToolResult", () => { + describe("db_query", () => { + it("生成行数摘要", () => { + expect( + summarizeDbToolResult("db_query", { + connection: "local", + database: "test_db", + rowCount: 42, + elapsed: "8ms", + }), + ).toBe("db_query:42 行 · 8ms(local/test_db)"); + }); + + it("rowCount 缺失时返回 undefined", () => { + expect( + summarizeDbToolResult("db_query", { + connection: "local", + database: "test_db", + elapsed: "8ms", + }), + ).toBeUndefined(); + }); + }); + + describe("db_tables", () => { + it("列表模式:表数量", () => { + expect( + summarizeDbToolResult("db_tables", { + connection: "local", + database: "test_db", + tables: ["users", "orders", "items"], + }), + ).toBe("db_tables:test_db 表列表(3 个)"); + }); + + it("schema 模式:列数与索引数", () => { + expect( + summarizeDbToolResult("db_tables", { + connection: "local", + database: "test_db", + table: "users", + columnCount: 5, + indexCount: 2, + }), + ).toBe("db_tables:test_db.users 结构(5 列 / 2 索引)"); + }); + + it("schema 模式缺少计数时返回 undefined", () => { + expect( + summarizeDbToolResult("db_tables", { + connection: "local", + database: "test_db", + table: "users", + }), + ).toBeUndefined(); + }); + + it("列表模式缺少 tables 时返回 undefined", () => { + expect( + summarizeDbToolResult("db_tables", { + connection: "local", + database: "test_db", + table: undefined, + }), + ).toBeUndefined(); + }); + }); + + describe("db_discover", () => { + it("只列连接", () => { + expect( + summarizeDbToolResult("db_discover", { + connections: ["local", "staging"], + connection: undefined, + }), + ).toBe("db_discover:2 个连接"); + }); + + it("连接 + 目标库数量", () => { + expect( + summarizeDbToolResult("db_discover", { + connections: ["local", "staging"], + connection: "local", + databaseCount: 7, + }), + ).toBe("db_discover:2 个连接 · 7 个数据库"); + }); + + it("connections 缺失时返回 undefined", () => { + expect(summarizeDbToolResult("db_discover", { connection: "local" })).toBeUndefined(); + }); + }); + + describe("db_tools loader", () => { + it("有新增工具时显示已启用列表", () => { + expect( + summarizeDbToolResult("db_tools", { + matches: ["db_discover", "db_list_relations"], + added: ["db_discover", "db_list_relations"], + }), + ).toBe("db_tools:已启用 db_discover、db_list_relations"); + }); + + it("无新增时显示已激活列表", () => { + expect( + summarizeDbToolResult("db_tools", { + matches: ["db_discover"], + added: [], + }), + ).toBe("db_tools:已激活 db_discover"); + }); + + it("无匹配时给出提示", () => { + expect(summarizeDbToolResult("db_tools", { matches: [], added: [] })).toBe( + "db_tools:无匹配工具", + ); + }); + + it("缺少数组字段时返回 undefined", () => { + expect(summarizeDbToolResult("db_tools", { added: [] })).toBeUndefined(); + }); + }); + + it("未知工具名返回 undefined", () => { + expect(summarizeDbToolResult("db_mutate", { sql: "UPDATE t SET x=1" })).toBeUndefined(); + }); + + it("非对象 details 返回 undefined", () => { + expect(summarizeDbToolResult("db_query", undefined)).toBeUndefined(); + expect(summarizeDbToolResult("db_query", "text")).toBeUndefined(); + }); +}); diff --git a/tools/db-tools.ts b/tools/db-tools.ts index db1589c..e48cd64 100644 --- a/tools/db-tools.ts +++ b/tools/db-tools.ts @@ -27,6 +27,8 @@ import { formatSchemaMarkdown } from "../formatting/schema-table"; import { MutationValidationError } from "../connection/sql-policy"; import { showMutationConfirm } from "../commands/mutate-confirm"; import { LOADER_TOOL_NAME, LAZY_TOOL_INFO, matchDbTools } from "./db-tool-catalog"; +import { dbToolResultRenderer } from "./tool-result-render"; +import type { DbTablesDetails } from "./tool-result-summary"; export { applyInitialToolSet } from "./db-tool-catalog"; function truncate(text: string, hint = "查询范围过大,请缩小查询或添加 LIMIT。"): string { @@ -117,6 +119,7 @@ export function registerDbTools( details: { matches, added }, }; }, + renderResult: dbToolResultRenderer(LOADER_TOOL_NAME), }); pi.registerTool({ @@ -175,6 +178,7 @@ export function registerDbTools( }, }; }, + renderResult: dbToolResultRenderer("db_query"), }); pi.registerTool({ @@ -207,16 +211,19 @@ export function registerDbTools( ]; const targetId = params.connection ?? ws.current?.connectionId; + let databaseCount: number | undefined; if (targetId) { const dbs = await ws.getDatabases(targetId); + databaseCount = dbs.length; lines.push("", `${targetId} 上的数据库(${dbs.length} 个):`, ...dbs); } return { content: [{ type: "text", text: lines.join("\n") }], - details: { connections: conns.map((c) => c.id), connection: targetId }, + details: { connections: conns.map((c) => c.id), connection: targetId, databaseCount }, }; }, + renderResult: dbToolResultRenderer("db_discover"), }); pi.registerTool({ @@ -243,15 +250,15 @@ export function registerDbTools( database: params.database, }); // 统一 details 形状——两种模式仅设置不同字段。 - const details: { - connection: string; - database: string; - table?: string; - tables?: string[]; - } = { connection: target.connectionId, database: target.database }; + const details: DbTablesDetails = { + connection: target.connectionId, + database: target.database, + }; if (params.table) { const { columns, indexes } = await ws.getTableSchema(params.table, target); details.table = params.table; + details.columnCount = columns.length; + details.indexCount = indexes.length; return { content: [ { @@ -278,6 +285,7 @@ export function registerDbTools( details, }; }, + renderResult: dbToolResultRenderer("db_tables"), }); pi.registerTool({ diff --git a/tools/tool-result-render.ts b/tools/tool-result-render.ts new file mode 100644 index 0000000..0804803 --- /dev/null +++ b/tools/tool-result-render.ts @@ -0,0 +1,60 @@ +/** + * 数据库工具结果的自定义 TUI 渲染。 + * + * 默认(折叠)只显示一行摘要(summarizeDbToolResult),用户按 + * ctrl+o(app.tools.expand)展开后展示 content 全文。渲染器是薄层—— + * 文案生成在 tool-result-summary.ts(纯函数,可测试)。渲染器抛错时 + * pi 的 tool-execution 会自动回退到默认渲染,因此无需兜底逻辑。 + */ + +import { Text, type Component } from "@earendil-works/pi-tui"; +import { + keyHint, + type AgentToolResult, + type Theme, + type ToolRenderResultOptions, +} from "@earendil-works/pi-coding-agent"; +import { summarizeDbToolResult, type SummarizableDbTool } from "./tool-result-summary"; + +/** 渲染上下文的最小结构类型——只取渲染器用到的字段(ToolRenderContext 未从包顶层导出)。 */ +interface RenderContext { + isError: boolean; +} + +/** 取结果的第一段文本内容(这些工具只返回文本)。 */ +function firstText(result: AgentToolResult): string { + const block = result.content.find((c) => c.type === "text"); + return block?.type === "text" ? block.text : ""; +} + +/** + * renderResult 工厂——每个常驻工具注册时传入自己的名字, + * 渲染器按该工具的 details 形状生成折叠态摘要。 + */ +export function dbToolResultRenderer(toolName: SummarizableDbTool) { + return ( + result: AgentToolResult, + options: ToolRenderResultOptions, + theme: Theme, + context: RenderContext, + ): Component => { + if (options.isPartial) { + return new Text(theme.fg("warning", `${toolName} 处理中…`), 0, 0); + } + if (context.isError) { + return new Text(theme.fg("error", firstText(result) || `${toolName} 执行失败`), 0, 0); + } + if (options.expanded) { + return new Text(theme.fg("toolOutput", firstText(result)), 0, 0); + } + const summary = summarizeDbToolResult(toolName, result.details); + if (summary) { + return new Text( + theme.fg("muted", summary) + " " + keyHint("app.tools.expand", "展开"), + 0, + 0, + ); + } + return new Text(theme.fg("toolOutput", firstText(result)), 0, 0); + }; +} diff --git a/tools/tool-result-summary.ts b/tools/tool-result-summary.ts new file mode 100644 index 0000000..616f62d --- /dev/null +++ b/tools/tool-result-summary.ts @@ -0,0 +1,94 @@ +/** + * 数据库工具结果的折叠态摘要——纯函数,无 pi 导入、无 I/O。 + * + * db-tools.ts 中常驻工具的 renderResult 用它生成默认(折叠)展示的 + * 一行摘要;用户按 ctrl+o(app.tools.expand)展开后展示 content 全文。 + * 摘要按各工具的 details 形状生成,未知工具或未知形状返回 undefined, + * 由调用方回退到完整内容。 + */ + +/** db_query 的 details 形状。 */ +export interface DbQueryDetails { + connection: string; + database: string; + rowCount: number; + elapsed: string; +} + +/** db_tables 的 details 形状(列表 / schema 两种模式仅设置不同字段)。 */ +export interface DbTablesDetails { + connection: string; + database: string; + /** schema 模式:目标表名。 */ + table?: string; + /** 列表模式:全部表名。 */ + tables?: string[]; + /** schema 模式:列数。 */ + columnCount?: number; + /** schema 模式:索引数。 */ + indexCount?: number; +} + +/** db_discover 的 details 形状。 */ +export interface DbDiscoverDetails { + connections: string[]; + connection: string; + /** 目标连接上的数据库数(未指定 connection 时缺省)。 */ + databaseCount?: number; +} + +/** db_tools loader 的 details 形状。 */ +export interface DbToolsDetails { + matches: string[]; + added: string[]; +} + +/** 带折叠摘要的常驻工具名。 */ +export type SummarizableDbTool = "db_query" | "db_tables" | "db_discover" | "db_tools"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +/** + * 生成工具结果的折叠态摘要(中文,用户可见)。 + * + * 返回 undefined 表示该工具或 details 形状没有已知摘要—— + * 调用方应回退到完整内容展示。 + */ +export function summarizeDbToolResult(toolName: string, details: unknown): string | undefined { + if (!isRecord(details)) return undefined; + switch (toolName) { + case "db_query": { + if (typeof details.rowCount !== "number") return undefined; + const d = details as unknown as Partial; + return `db_query:${d.rowCount} 行 · ${d.elapsed ?? ""}(${d.connection ?? "?"}/${d.database ?? "?"})`; + } + case "db_tables": { + const d = details as unknown as Partial; + if (d.table !== undefined) { + if (typeof d.columnCount !== "number" || typeof d.indexCount !== "number") { + return undefined; + } + return `db_tables:${d.database ?? "?"}.${d.table} 结构(${d.columnCount} 列 / ${d.indexCount} 索引)`; + } + if (!Array.isArray(d.tables)) return undefined; + return `db_tables:${d.database ?? "?"} 表列表(${d.tables.length} 个)`; + } + case "db_discover": { + const d = details as unknown as Partial; + if (!Array.isArray(d.connections)) return undefined; + const base = `db_discover:${d.connections.length} 个连接`; + return typeof d.databaseCount === "number" ? `${base} · ${d.databaseCount} 个数据库` : base; + } + case "db_tools": { + const d = details as unknown as Partial; + if (!Array.isArray(d.added) || !Array.isArray(d.matches)) return undefined; + if (d.added.length > 0) return `db_tools:已启用 ${d.added.join("、")}`; + if (d.matches.length > 0) return `db_tools:已激活 ${d.matches.join("、")}`; + return "db_tools:无匹配工具"; + } + default: + return undefined; + } +}