Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions __tests__/tool-result-summary.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
22 changes: 15 additions & 7 deletions tools/db-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -117,6 +119,7 @@ export function registerDbTools(
details: { matches, added },
};
},
renderResult: dbToolResultRenderer(LOADER_TOOL_NAME),
});

pi.registerTool({
Expand Down Expand Up @@ -175,6 +178,7 @@ export function registerDbTools(
},
};
},
renderResult: dbToolResultRenderer("db_query"),
});

pi.registerTool({
Expand Down Expand Up @@ -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({
Expand All @@ -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: [
{
Expand All @@ -278,6 +285,7 @@ export function registerDbTools(
details,
};
},
renderResult: dbToolResultRenderer("db_tables"),
});

pi.registerTool({
Expand Down
60 changes: 60 additions & 0 deletions tools/tool-result-render.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>): 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<unknown>,
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);
};
}
94 changes: 94 additions & 0 deletions tools/tool-result-summary.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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<DbQueryDetails>;
return `db_query:${d.rowCount} 行 · ${d.elapsed ?? ""}(${d.connection ?? "?"}/${d.database ?? "?"})`;
}
case "db_tables": {
const d = details as unknown as Partial<DbTablesDetails>;
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<DbDiscoverDetails>;
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<DbToolsDetails>;
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;
}
}
Loading