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
51 changes: 18 additions & 33 deletions commands/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
import { handleHistory } from "./history";
import { handleFavorite } from "./favorites";
import { handleRelations } from "./relations";
import { getLastRelatedCache } from "./query";
import { openRelatedBrowser } from "./related-browser";
import { RelatedBrowserCacheStore, openRelatedBrowser } from "./related-browser";
import { sendDbStatus } from "./llm-context";
import { writeToggle } from "../state/extension-toggle";

// ====== 自动补全项类型(结构上匹配 pi-tui AutocompleteItem)======
Expand Down Expand Up @@ -76,6 +76,10 @@
return;
}

// 最近关联查询缓存——注册时创建一次,注入 query 路径写入、
// related 路径读取(替代 module 级单例,见 AGENTS.md 注入接缝)。
const relatedCache = new RelatedBrowserCacheStore();

pi.registerCommand("db", {
description:
"Database workspace: /db (panel) | switch | add | tables | schema <table> | query [table] | history [kw] | favorite | relations | related | on | off",
Expand All @@ -89,15 +93,15 @@
if (!ctx.hasUI) return;

const ws = getWorkspace();
const [sub, ...rest] = args.trim().split(/\s+/).filter(Boolean);

Check warning on line 96 in commands/db.ts

View workflow job for this annotation

GitHub Actions / checks

unicorn(prefer-array-find)

Prefer `find` over filtering and accessing the first result.

switch (sub) {
case undefined: {
// 在展示交互式仪表盘之前发送静默 LLM 上下文
sendLLMContext(ws, pi);
sendDbStatus(pi, ws);
const action = await showDashboard(ctx, ws);
if (!action) return;
await dispatchAction(action, ctx, ws, pi, rest);
await dispatchAction(action, ctx, ws, pi, rest, relatedCache);
break;
}
case "switch":
Expand All @@ -113,7 +117,7 @@
await handleSchema(ctx, ws, pi, rest[0]);
break;
case "query":
await handleQuery(ctx, ws, pi, rest.join(" ") || undefined);
await handleQuery(ctx, ws, pi, rest.join(" ") || undefined, relatedCache);
break;
case "history":
await handleHistory(ctx, ws, pi, rest[0]);
Expand All @@ -125,7 +129,7 @@
await handleRelations(ctx, ws, pi, rest);
break;
case "related":
await handleRelatedBrowser(ctx);
await handleRelatedBrowser(ctx, relatedCache);
break;
case "on":
await handleToggle(ctx, true, toggleBaseDir);
Expand All @@ -143,8 +147,11 @@
// ====== 关联表浏览器 ================================================

/** 打开最近一次关联查询的浏览器;无缓存时提示引导。 */
async function handleRelatedBrowser(ctx: ExtensionCommandContext): Promise<void> {
const cache = getLastRelatedCache();
async function handleRelatedBrowser(
ctx: ExtensionCommandContext,
relatedCache: RelatedBrowserCacheStore,
): Promise<void> {
const cache = relatedCache.get();
if (!cache) {
ctx.ui.notify(
"没有可浏览的关联表——先执行关联表查询(/db query 选表后选择「📎 是,一起查询关联表」)",
Expand Down Expand Up @@ -239,29 +246,6 @@
{ value: "related", label: "📎 关联表浏览器", needsConnection: false },
];

/** 发送静默上下文消息,让 LLM 知道数据库状态。 */
function sendLLMContext(ws: DatabaseWorkspaceService, pi: ExtensionAPI): void {
if (ws.isReady) {
pi.sendMessage(
{
customType: "db-active-db",
content: `Current database: ${ws.current!.database} (connection: ${ws.current!.connectionId}, environment: ${ws.current!.environment}). Config file: ${ws.configPath}.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: false },
);
} else if (ws.isConfigured) {
pi.sendMessage(
{
customType: "db-hint",
content: `Database connections are configured but no database is selected. Tell the user to run /db switch to connect. Config file: ${ws.configPath}.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: false },
);
}
}

/** 构建交互式仪表盘组件。 */
async function showDashboard(
ctx: ExtensionCommandContext,
Expand Down Expand Up @@ -357,6 +341,7 @@
ws: DatabaseWorkspaceService,
pi: ExtensionAPI,
rest: string[],
relatedCache: RelatedBrowserCacheStore,
): Promise<void> {
switch (action) {
case "switch":
Expand All @@ -372,7 +357,7 @@
await handleSchema(ctx, ws, pi, rest[0]);
break;
case "query":
await handleQuery(ctx, ws, pi, rest.join(" ") || undefined);
await handleQuery(ctx, ws, pi, rest.join(" ") || undefined, relatedCache);
break;
case "history":
await handleHistory(ctx, ws, pi, rest[0]);
Expand All @@ -384,7 +369,7 @@
await handleRelations(ctx, ws, pi, rest);
break;
case "related":
await handleRelatedBrowser(ctx);
await handleRelatedBrowser(ctx, relatedCache);
break;
}
}
Expand Down
66 changes: 66 additions & 0 deletions commands/llm-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* 发给 LLM 的静默上下文消息 —— 唯一构建点。
*
* AGENTS.md 要求这些字符串保持英文且稳定;本模块是它们的单一归属。
* index.ts(session_start)、db.ts(仪表盘)、switch.ts(切换后)共用。
*/

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import type { DatabaseWorkspaceService } from "../state/workspace";

/** 已选择数据库:告知当前目标、配置文件与可用工具。 */
export function sendActiveDb(pi: ExtensionAPI, ws: DatabaseWorkspaceService): void {
const db = ws.current!;
pi.sendMessage(
{
customType: "db-active-db",
content:
`Current database: ${db.database} (connection: ${db.connectionId}, ` +
`environment: ${db.environment}). Config file: ${ws.configPath}. ` +
`Use db_query and db_tables to query this database.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: false },
);
}

/** 已配置但未选择数据库:引导用户运行 /db switch。 */
export function sendConfiguredHint(pi: ExtensionAPI, ws: DatabaseWorkspaceService): void {
pi.sendMessage(
{
customType: "db-hint",
content:
`Database connections are configured but no database is selected. ` +
`Tell the user to run /db switch to connect. Config file: ${ws.configPath}.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: false },
);
}

/**
* 按工作空间状态发送对应消息(active 或 hint);两者皆无时不发。
* 会话启动与仪表盘入口共用。
*/
export function sendDbStatus(pi: ExtensionAPI, ws: DatabaseWorkspaceService): void {
if (ws.isReady) {
sendActiveDb(pi, ws);
} else if (ws.isConfigured) {
sendConfiguredHint(pi, ws);
}
}

/** 未配置任何连接:triggerTurn 让 AI 主动提议建立第一个连接。 */
export function sendUnconfiguredHint(pi: ExtensionAPI, configPath: string): void {
pi.sendMessage(
{
customType: "db-hint",
content:
`No database connections are configured yet. Help the user create their first ` +
`connection in ${configPath}. Ask for host, port, username, password, and default ` +
`database name. After the config file is written, tell the user to run /db switch to connect.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: true },
);
}
22 changes: 8 additions & 14 deletions commands/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { READONLY_SQL_RE } from "../connection/sql-policy";
import { formatTableCompact } from "../formatting/result-table";
import { pickTableFuzzy, withLoader } from "./utils";
import type { QueryResultEntryData, RelatedTuiData } from "./renderers";
import type { RelatedBrowserCache } from "./related-browser";
import type { RelatedBrowserCacheStore } from "./related-browser";

interface ExecutedResult {
columns: string[];
Expand Down Expand Up @@ -77,15 +77,6 @@ export function toRelatedTuiData(related: RelatedResult[]): RelatedTuiData[] {
}));
}

// ── 最近关联查询缓存(/db related 浏览器入口用)──

let lastRelatedCache: RelatedBrowserCache | null = null;

/** 最近一次关联查询的结果缓存;无关联查询时返回 null。 */
export function getLastRelatedCache(): RelatedBrowserCache | null {
return lastRelatedCache;
}

// ── 展示(双受众:TUI 条目 + LLM 上下文)──────────

async function displayQueryResult(
Expand All @@ -94,6 +85,7 @@ async function displayQueryResult(
pi: ExtensionAPI,
result: ExecutedResult,
related: RelatedResult[] = [],
relatedCache?: RelatedBrowserCacheStore,
): Promise<void> {
const database = ws.current!.database;
try {
Expand All @@ -106,7 +98,7 @@ async function displayQueryResult(

const relatedTui = toRelatedTuiData(related);
if (relatedTui.length > 0) {
lastRelatedCache = { database, sql: result.sql, related: relatedTui };
relatedCache?.set({ database, sql: result.sql, related: relatedTui });
}

pi.appendEntry("db-query-result", {
Expand Down Expand Up @@ -173,6 +165,7 @@ async function queryByTable(
ws: DatabaseWorkspaceService,
pi: ExtensionAPI,
preSelectedTable?: string,
relatedCache?: RelatedBrowserCacheStore,
): Promise<void> {
const table = preSelectedTable ?? (await pickTableFuzzy(ctx, ws, "选择数据表"));
if (!table) return;
Expand Down Expand Up @@ -206,7 +199,7 @@ async function queryByTable(
ctx.ui.notify(`查询出错:${err.message}`, "error");
return;
}
await displayQueryResult(ctx, ws, pi, result, result.related);
await displayQueryResult(ctx, ws, pi, result, result.related, relatedCache);
} else {
await executeAndDisplay(ctx, ws, pi, sql);
}
Expand All @@ -233,6 +226,7 @@ export async function handleQuery(
ws: DatabaseWorkspaceService,
pi: ExtensionAPI,
tableArg?: string,
relatedCache?: RelatedBrowserCacheStore,
): Promise<void> {
if (!ws.isReady) {
ctx.ui.notify("未选择数据库,请先执行 /db switch", "warning");
Expand All @@ -247,7 +241,7 @@ export async function handleQuery(
tables = [];
}
if (tables.includes(tableArg)) {
return await queryByTable(ctx, ws, pi, tableArg);
return await queryByTable(ctx, ws, pi, tableArg, relatedCache);
}
if (READONLY_SQL_RE.test(tableArg)) {
return await executeAndDisplay(ctx, ws, pi, tableArg);
Expand All @@ -264,5 +258,5 @@ export async function handleQuery(
if (choice === "__sql__") {
return await queryRaw(ctx, ws, pi);
}
return await queryByTable(ctx, ws, pi, choice);
return await queryByTable(ctx, ws, pi, choice, relatedCache);
}
16 changes: 16 additions & 0 deletions commands/related-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ export interface RelatedBrowserCache {
related: RelatedTuiData[];
}

/**
* 最近关联查询缓存的持有者——经调用链注入(db.ts 注册时创建、
* 传给 query 路径写入、related 路径读取),替代 module 级单例。
*/
export class RelatedBrowserCacheStore {
private current: RelatedBrowserCache | null = null;

set(cache: RelatedBrowserCache): void {
this.current = cache;
}

get(): RelatedBrowserCache | null {
return this.current;
}
}

// ── 表切换 reducer(纯函数,可单测)────────────────

export interface BrowserNav {
Expand Down
19 changes: 3 additions & 16 deletions commands/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import type { DatabaseWorkspaceService } from "../state/workspace";
import { withLoader } from "./utils";
import { sendActiveDb, sendUnconfiguredHint } from "./llm-context";

export const STATUS_KEY = "db-workspace";

Expand Down Expand Up @@ -81,14 +82,7 @@ export async function showWorkspacePanel(
// display: false 让它不进聊天;triggerTurn 让 AI
// 主动提议帮助建立第一个连接。
if (!ws.isConfigured && pi) {
pi.sendMessage(
{
customType: "db-hint",
content: `No database connections are configured yet. Help the user create their first connection in ${ws.configPath}. Ask for host, port, username, password, and default database name. After the config file is written, tell the user to run /db switch to connect.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: true },
);
sendUnconfiguredHint(pi, ws.configPath);
}
}

Expand Down Expand Up @@ -180,14 +174,7 @@ export async function handleSwitch(

// 告知 LLM 当前激活的数据库,避免它猜测。
// display: false 避免冗余消息污染聊天。
pi.sendMessage(
{
customType: "db-active-db",
content: `Current database: ${database} (connection: ${connectionId}, environment: ${env}). Use db_query and db_tables to query this database.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: false },
);
sendActiveDb(pi, ws);

ctx.ui.notify(`已连接:${env}/${database} @ ${connectionId}`, "info");
}
2 changes: 1 addition & 1 deletion docs/extension-toggle.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 扩展开关设计:Extension Toggle

> 状态:设计稿(待评审
> 状态:已实施(`/db on|off` 位于 `commands/db.ts`
> 目标版本:0.9.0

## 1. 背景与目标
Expand Down
2 changes: 1 addition & 1 deletion docs/table-tool-consolidation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 表工具合并方案:db_list_tables + db_table_schema → db_tables

> 状态:待实施(目标版本 v0.8.0,与 tool-loading 合并发布
> 状态:已实施(`db_tables` 位于 `tools/db-tools.ts`
> 前置:v0.8.0 Dynamic Tool Loading 已完成(`docs/tool-loading-redesign.md`)

## 1. 背景与目标
Expand Down
23 changes: 2 additions & 21 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { readToggle } from "./state/extension-toggle";
import { registerDbCommand, restoreStatusBar } from "./commands/db";
import { registerRenderers } from "./commands/renderers";
import { registerDbTools, applyInitialToolSet } from "./tools/db-tools";
import { sendDbStatus } from "./commands/llm-context";

const baseDir = dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -57,27 +58,7 @@ export default function (pi: ExtensionAPI) {
restoreStatusBar(ws, ctx);
// 恢复会话时告知 LLM 当前激活的数据库,
// 避免它通过一次失败的调用去发现。
if (ws.isReady) {
const db = ws.current!;
pi.sendMessage(
{
customType: "db-active-db",
content: `Current database: ${db.database} (connection: ${db.connectionId}, environment: ${db.environment}). Config file: ${ws.configPath}.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: false },
);
} else if (ws.isConfigured) {
// 已配置但未切换——让 AI 知道,以便协助用户选择数据库。
pi.sendMessage(
{
customType: "db-hint",
content: `Database connections are configured but no database is selected. Tell the user to run /db switch to connect. Config file: ${ws.configPath}.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: false },
);
}
sendDbStatus(pi, ws);
});

// 关闭时清理
Expand Down
Loading
Loading