mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
✨ feat(ai-tools): 新增外部 SQL 文件探针
This commit is contained in:
@@ -46,6 +46,7 @@ describe('AIChatPanel message render isolation', () => {
|
||||
expect(systemContextSource).toContain('inspect_workspace_tabs 盘点当前工作区');
|
||||
expect(systemContextSource).toContain('inspect_current_connection');
|
||||
expect(systemContextSource).toContain('inspect_external_sql_directories');
|
||||
expect(systemContextSource).toContain('inspect_external_sql_file');
|
||||
expect(source).toContain('tabs: useStore.getState().tabs');
|
||||
expect(source).toContain('activeTabId: useStore.getState().activeTabId');
|
||||
expect(source).toContain('externalSQLDirectories: useStore.getState().externalSQLDirectories');
|
||||
|
||||
@@ -48,6 +48,8 @@ describe('AIBuiltinToolsCatalog', () => {
|
||||
expect(markup).toContain('inspect_saved_connections');
|
||||
expect(markup).toContain('盘点外部 SQL 目录');
|
||||
expect(markup).toContain('inspect_external_sql_directories');
|
||||
expect(markup).toContain('读取外部 SQL 文件');
|
||||
expect(markup).toContain('inspect_external_sql_file');
|
||||
expect(markup).toContain('读取当前页签');
|
||||
expect(markup).toContain('inspect_active_tab');
|
||||
expect(markup).toContain('盘点当前工作区');
|
||||
|
||||
@@ -92,6 +92,11 @@ const BUILTIN_TOOL_FLOWS = [
|
||||
steps: 'inspect_external_sql_directories → inspect_workspace_tabs / inspect_active_tab',
|
||||
description: '适合先确认本地配置了哪些外部 SQL 目录、目录绑定到哪个连接/库,以及当前打开的 SQL 文件来自哪里,再继续分析脚本内容。',
|
||||
},
|
||||
{
|
||||
title: '读取外部 SQL 文件',
|
||||
steps: 'inspect_external_sql_directories → inspect_external_sql_file → inspect_active_tab',
|
||||
description: '适合先定位具体脚本路径,再直接读取目录中的 SQL 文件内容;如果这个文件已经在编辑器里打开,再继续结合当前页签草稿一起分析。',
|
||||
},
|
||||
{
|
||||
title: '读取当前页签',
|
||||
steps: 'inspect_active_tab → get_columns / get_indexes / execute_sql',
|
||||
|
||||
69
frontend/src/components/ai/aiExternalSqlFileInsights.test.ts
Normal file
69
frontend/src/components/ai/aiExternalSqlFileInsights.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { ExternalSQLDirectory, SavedConnection, TabData } from '../../types';
|
||||
import { buildExternalSQLFileSnapshot } from './aiExternalSqlFileInsights';
|
||||
|
||||
const connections: SavedConnection[] = [
|
||||
{
|
||||
id: 'conn-1',
|
||||
name: '本地开发库',
|
||||
config: {
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('aiExternalSqlFileInsights', () => {
|
||||
it('builds a file snapshot with directory metadata, open tab context, and truncated content preview', () => {
|
||||
const externalSQLDirectories: ExternalSQLDirectory[] = [
|
||||
{
|
||||
id: 'dir-1',
|
||||
name: '报表脚本',
|
||||
path: 'D:/sql/reports',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
createdAt: 1,
|
||||
},
|
||||
];
|
||||
const tabs: TabData[] = [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'daily.sql',
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
filePath: 'D:/sql/reports/daily.sql',
|
||||
query: 'select 1',
|
||||
},
|
||||
];
|
||||
|
||||
const snapshot = buildExternalSQLFileSnapshot({
|
||||
filePath: 'D:/sql/reports/daily.sql',
|
||||
previewCharLimit: 12,
|
||||
readResult: {
|
||||
content: 'SELECT * FROM orders WHERE status = \'paid\';',
|
||||
filePath: 'D:/sql/reports/daily.sql',
|
||||
name: 'daily.sql',
|
||||
},
|
||||
externalSQLDirectories,
|
||||
connections,
|
||||
tabs,
|
||||
});
|
||||
|
||||
expect(snapshot.hasMatchedDirectory).toBe(true);
|
||||
expect(snapshot.directory).toMatchObject({
|
||||
name: '报表脚本',
|
||||
connectionName: '本地开发库',
|
||||
connectionType: 'mysql',
|
||||
dbName: 'crm',
|
||||
});
|
||||
expect(snapshot.hasOpenTab).toBe(true);
|
||||
expect(snapshot.openTabCount).toBe(1);
|
||||
expect(snapshot.fileName).toBe('daily.sql');
|
||||
expect(snapshot.contentPreview).toBe('SELECT * FRO');
|
||||
expect(snapshot.truncated).toBe(true);
|
||||
});
|
||||
});
|
||||
116
frontend/src/components/ai/aiExternalSqlFileInsights.ts
Normal file
116
frontend/src/components/ai/aiExternalSqlFileInsights.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { ExternalSQLDirectory, SavedConnection, TabData } from '../../types';
|
||||
import {
|
||||
findBestMatchingExternalSQLDirectory,
|
||||
normalizeExternalSQLPath,
|
||||
} from './aiExternalSqlPathUtils';
|
||||
|
||||
const normalizePreviewCharLimit = (input: unknown): number => {
|
||||
const value = Math.floor(Number(input) || 12000);
|
||||
if (value < 1) return 1;
|
||||
if (value > 40000) return 40000;
|
||||
return value;
|
||||
};
|
||||
|
||||
const normalizeFileReadPayload = (
|
||||
readResult: unknown,
|
||||
): {
|
||||
content: string;
|
||||
filePath: string;
|
||||
name: string;
|
||||
isLargeFile: boolean;
|
||||
fileSize: number;
|
||||
fileSizeMB: string;
|
||||
} => {
|
||||
if (typeof readResult === 'string') {
|
||||
return {
|
||||
content: readResult,
|
||||
filePath: '',
|
||||
name: '',
|
||||
isLargeFile: false,
|
||||
fileSize: 0,
|
||||
fileSizeMB: '',
|
||||
};
|
||||
}
|
||||
if (!readResult || typeof readResult !== 'object') {
|
||||
return {
|
||||
content: '',
|
||||
filePath: '',
|
||||
name: '',
|
||||
isLargeFile: false,
|
||||
fileSize: 0,
|
||||
fileSizeMB: '',
|
||||
};
|
||||
}
|
||||
const payload = readResult as Record<string, unknown>;
|
||||
return {
|
||||
content: typeof payload.content === 'string' ? payload.content : '',
|
||||
filePath: String(payload.filePath || '').trim(),
|
||||
name: String(payload.name || '').trim(),
|
||||
isLargeFile: payload.isLargeFile === true,
|
||||
fileSize: Number(payload.fileSize || 0),
|
||||
fileSizeMB: String(payload.fileSizeMB || '').trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildExternalSQLFileSnapshot = (params: {
|
||||
filePath: unknown;
|
||||
previewCharLimit?: unknown;
|
||||
readResult?: unknown;
|
||||
externalSQLDirectories?: ExternalSQLDirectory[];
|
||||
connections: SavedConnection[];
|
||||
tabs?: TabData[];
|
||||
}) => {
|
||||
const {
|
||||
filePath,
|
||||
previewCharLimit,
|
||||
readResult,
|
||||
externalSQLDirectories = [],
|
||||
connections,
|
||||
tabs = [],
|
||||
} = params;
|
||||
|
||||
const requestedFilePath = normalizeExternalSQLPath(filePath);
|
||||
const payload = normalizeFileReadPayload(readResult);
|
||||
const resolvedFilePath = normalizeExternalSQLPath(payload.filePath || requestedFilePath);
|
||||
const matchedDirectory = findBestMatchingExternalSQLDirectory(resolvedFilePath, externalSQLDirectories);
|
||||
const matchedConnection = connections.find((item) => item.id === matchedDirectory?.connectionId);
|
||||
const matchingTabs = tabs.filter(
|
||||
(tab) => normalizeExternalSQLPath(tab.filePath || '').toLowerCase() === resolvedFilePath.toLowerCase(),
|
||||
);
|
||||
const previewLimit = normalizePreviewCharLimit(previewCharLimit);
|
||||
const previewContent = payload.content.slice(0, previewLimit);
|
||||
const inferredName = payload.name || resolvedFilePath.split('/').filter(Boolean).pop() || '';
|
||||
|
||||
return {
|
||||
requestedFilePath,
|
||||
resolvedFilePath,
|
||||
fileName: inferredName,
|
||||
previewCharLimit: previewLimit,
|
||||
hasMatchedDirectory: Boolean(matchedDirectory),
|
||||
directory: matchedDirectory ? {
|
||||
id: matchedDirectory.id,
|
||||
name: matchedDirectory.name,
|
||||
path: matchedDirectory.path,
|
||||
connectionId: matchedDirectory.connectionId || '',
|
||||
connectionName: matchedConnection?.name || '',
|
||||
connectionType: matchedConnection?.config?.type || '',
|
||||
dbName: matchedDirectory.dbName || '',
|
||||
} : null,
|
||||
hasOpenTab: matchingTabs.length > 0,
|
||||
openTabCount: matchingTabs.length,
|
||||
openTabs: matchingTabs.slice(0, 5).map((tab) => ({
|
||||
tabId: tab.id,
|
||||
title: tab.title,
|
||||
dbName: tab.dbName || '',
|
||||
connectionId: tab.connectionId || '',
|
||||
isActiveFileTab: true,
|
||||
})),
|
||||
isLargeFile: payload.isLargeFile,
|
||||
fileSize: payload.fileSize,
|
||||
fileSizeMB: payload.fileSizeMB,
|
||||
hasContentPreview: previewContent.length > 0,
|
||||
truncated: payload.content.length > previewContent.length,
|
||||
contentPreview: previewContent,
|
||||
contentLength: payload.content.length,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ExternalSQLDirectory, SavedConnection, TabData } from '../../types';
|
||||
import {
|
||||
isExternalSQLPathInsideDirectory,
|
||||
} from './aiExternalSqlPathUtils';
|
||||
|
||||
const normalizeLimit = (input: unknown, fallback: number, max: number): number => {
|
||||
const value = Math.floor(Number(input) || fallback);
|
||||
@@ -9,9 +12,6 @@ const normalizeLimit = (input: unknown, fallback: number, max: number): number =
|
||||
|
||||
const normalizeKeyword = (input: unknown): string => String(input || '').trim().toLowerCase();
|
||||
|
||||
const normalizePath = (input: unknown): string =>
|
||||
String(input || '').trim().replace(/\\/g, '/').replace(/\/+$/u, '');
|
||||
|
||||
const matchesKeyword = (keyword: string, fields: Array<string | undefined>): boolean => {
|
||||
if (!keyword) {
|
||||
return true;
|
||||
@@ -19,18 +19,6 @@ const matchesKeyword = (keyword: string, fields: Array<string | undefined>): boo
|
||||
return fields.some((field) => String(field || '').toLowerCase().includes(keyword));
|
||||
};
|
||||
|
||||
const belongsToDirectory = (filePath: string, directoryPath: string): boolean => {
|
||||
if (!filePath || !directoryPath) {
|
||||
return false;
|
||||
}
|
||||
const normalizedFilePath = normalizePath(filePath).toLowerCase();
|
||||
const normalizedDirectoryPath = normalizePath(directoryPath).toLowerCase();
|
||||
if (!normalizedFilePath || !normalizedDirectoryPath) {
|
||||
return false;
|
||||
}
|
||||
return normalizedFilePath === normalizedDirectoryPath || normalizedFilePath.startsWith(`${normalizedDirectoryPath}/`);
|
||||
};
|
||||
|
||||
export const buildExternalSQLDirectoriesSnapshot = (params: {
|
||||
externalSQLDirectories?: ExternalSQLDirectory[];
|
||||
connections: SavedConnection[];
|
||||
@@ -79,7 +67,7 @@ export const buildExternalSQLDirectoriesSnapshot = (params: {
|
||||
|
||||
const visibleDirectories = filteredDirectories.slice(0, safeLimit).map((directory) => {
|
||||
const connection = connections.find((item) => item.id === directory.connectionId);
|
||||
const matchingTabs = externalSqlTabs.filter((tab) => belongsToDirectory(String(tab.filePath || ''), directory.path));
|
||||
const matchingTabs = externalSqlTabs.filter((tab) => isExternalSQLPathInsideDirectory(String(tab.filePath || ''), directory.path));
|
||||
|
||||
return {
|
||||
id: directory.id,
|
||||
|
||||
29
frontend/src/components/ai/aiExternalSqlPathUtils.ts
Normal file
29
frontend/src/components/ai/aiExternalSqlPathUtils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ExternalSQLDirectory } from '../../types';
|
||||
|
||||
export const normalizeExternalSQLPath = (input: unknown): string =>
|
||||
String(input || '').trim().replace(/\\/g, '/').replace(/\/+$/u, '');
|
||||
|
||||
export const isExternalSQLPathInsideDirectory = (filePath: string, directoryPath: string): boolean => {
|
||||
if (!filePath || !directoryPath) {
|
||||
return false;
|
||||
}
|
||||
const normalizedFilePath = normalizeExternalSQLPath(filePath).toLowerCase();
|
||||
const normalizedDirectoryPath = normalizeExternalSQLPath(directoryPath).toLowerCase();
|
||||
if (!normalizedFilePath || !normalizedDirectoryPath) {
|
||||
return false;
|
||||
}
|
||||
return normalizedFilePath === normalizedDirectoryPath || normalizedFilePath.startsWith(`${normalizedDirectoryPath}/`);
|
||||
};
|
||||
|
||||
export const findBestMatchingExternalSQLDirectory = (
|
||||
filePath: string,
|
||||
directories: ExternalSQLDirectory[],
|
||||
): ExternalSQLDirectory | undefined => {
|
||||
const normalizedFilePath = normalizeExternalSQLPath(filePath).toLowerCase();
|
||||
if (!normalizedFilePath) {
|
||||
return undefined;
|
||||
}
|
||||
return [...directories]
|
||||
.filter((directory) => isExternalSQLPathInsideDirectory(normalizedFilePath, directory.path))
|
||||
.sort((left, right) => normalizeExternalSQLPath(right.path).length - normalizeExternalSQLPath(left.path).length)[0];
|
||||
};
|
||||
@@ -656,6 +656,90 @@ describe('aiLocalToolExecutor', () => {
|
||||
expect(result.content).not.toContain('运维脚本');
|
||||
});
|
||||
|
||||
it('reads a configured external sql file so the model can inspect script content directly', async () => {
|
||||
const readSQLFile = vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
content: 'SELECT * FROM orders WHERE status = \'paid\';',
|
||||
filePath: 'D:/sql/reports/daily.sql',
|
||||
name: 'daily.sql',
|
||||
},
|
||||
});
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_external_sql_file', {
|
||||
filePath: 'D:/sql/reports/daily.sql',
|
||||
previewCharLimit: 18,
|
||||
}),
|
||||
connections: [buildConnection()],
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
title: 'daily.sql',
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
filePath: 'D:/sql/reports/daily.sql',
|
||||
query: 'select 1',
|
||||
},
|
||||
],
|
||||
mcpTools: [],
|
||||
toolContextMap: new Map(),
|
||||
externalSQLDirectories: [
|
||||
{
|
||||
id: 'dir-1',
|
||||
name: '报表脚本',
|
||||
path: 'D:/sql/reports',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
createdAt: 1,
|
||||
},
|
||||
],
|
||||
runtime: {
|
||||
getDatabases: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
readSQLFile,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(readSQLFile).toHaveBeenCalledWith('D:/sql/reports/daily.sql');
|
||||
expect(result.content).toContain('"fileName":"daily.sql"');
|
||||
expect(result.content).toContain('"connectionName":"主库"');
|
||||
expect(result.content).toContain('"hasOpenTab":true');
|
||||
expect(result.content).toContain('SELECT * FROM orde');
|
||||
});
|
||||
|
||||
it('blocks external sql file reads outside configured directories', async () => {
|
||||
const readSQLFile = vi.fn();
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_external_sql_file', {
|
||||
filePath: 'D:/private/secret.sql',
|
||||
}),
|
||||
connections: [buildConnection()],
|
||||
mcpTools: [],
|
||||
toolContextMap: new Map(),
|
||||
externalSQLDirectories: [
|
||||
{
|
||||
id: 'dir-1',
|
||||
name: '报表脚本',
|
||||
path: 'D:/sql/reports',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
createdAt: 1,
|
||||
},
|
||||
],
|
||||
runtime: {
|
||||
getDatabases: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
readSQLFile,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.content).toContain('目标文件不在已配置的外部 SQL 目录中');
|
||||
expect(readSQLFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('blocks execute_sql when the AI safety check rejects the statement', async () => {
|
||||
const query = vi.fn();
|
||||
const result = await executeLocalAIToolCall({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DBGetAllColumns, DBGetDatabases, DBGetTables } from '../../../wailsjs/go/app/App';
|
||||
import { DBGetAllColumns, DBGetDatabases, DBGetTables, ReadSQLFile } from '../../../wailsjs/go/app/App';
|
||||
|
||||
import type { AISnapshotInspectionRuntime } from './aiSnapshotInspectionToolExecutor';
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface AILocalToolRuntime extends AISnapshotInspectionRuntime {
|
||||
getDatabases: (config: any) => Promise<any>;
|
||||
getTables: (config: any, dbName: string) => Promise<any>;
|
||||
getAllColumns: (config: any, dbName: string) => Promise<any>;
|
||||
readSQLFile: (filePath: string) => Promise<any>;
|
||||
getColumns: (config: any, dbName: string, tableName: string) => Promise<any>;
|
||||
getIndexes: (config: any, dbName: string, tableName: string) => Promise<any>;
|
||||
getForeignKeys: (config: any, dbName: string, tableName: string) => Promise<any>;
|
||||
@@ -28,6 +29,7 @@ export const buildDefaultLocalToolRuntime = (): AILocalToolRuntime => ({
|
||||
getDatabases: DBGetDatabases,
|
||||
getTables: DBGetTables,
|
||||
getAllColumns: DBGetAllColumns,
|
||||
readSQLFile: ReadSQLFile,
|
||||
getColumns: async (config, dbName, tableName) => {
|
||||
const mod = await import('../../../wailsjs/go/app/App');
|
||||
return mod.DBGetColumns(config, dbName, tableName);
|
||||
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
buildSqlSnippetsSnapshot,
|
||||
} from './aiSavedSqlInsights';
|
||||
import { buildSavedConnectionsSnapshot } from './aiSavedConnectionInsights';
|
||||
import { buildExternalSQLFileSnapshot } from './aiExternalSqlFileInsights';
|
||||
import { buildExternalSQLDirectoriesSnapshot } from './aiExternalSqlInsights';
|
||||
import { findBestMatchingExternalSQLDirectory } from './aiExternalSqlPathUtils';
|
||||
import {
|
||||
buildActiveTabSnapshot,
|
||||
buildRecentSqlLogsSnapshot,
|
||||
@@ -49,6 +51,7 @@ export interface AISnapshotInspectionRuntime {
|
||||
getAIRuntimeState?: () => Promise<AISnapshotInspectionRuntimeState | undefined>;
|
||||
getMCPServers?: () => Promise<AIMCPServerConfig[] | undefined>;
|
||||
getMCPClientInstallStatuses?: () => Promise<AIMCPClientInstallStatus[] | undefined>;
|
||||
readSQLFile?: (filePath: string) => Promise<any>;
|
||||
}
|
||||
|
||||
interface ExecuteSnapshotInspectionToolCallOptions {
|
||||
@@ -237,6 +240,41 @@ export async function executeSnapshotInspectionToolCall(
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_external_sql_file': {
|
||||
const requestedFilePath = String(args.filePath || '').trim();
|
||||
if (!requestedFilePath) {
|
||||
return {
|
||||
content: '读取外部 SQL 文件失败: filePath 不能为空',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
if (!findBestMatchingExternalSQLDirectory(requestedFilePath, externalSQLDirectories)) {
|
||||
return {
|
||||
content: '读取外部 SQL 文件失败: 目标文件不在已配置的外部 SQL 目录中',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
const readResult = typeof runtime?.readSQLFile === 'function'
|
||||
? await runtime.readSQLFile(requestedFilePath)
|
||||
: { success: false, message: '当前环境暂不支持读取本地 SQL 文件' };
|
||||
if (!readResult?.success) {
|
||||
return {
|
||||
content: `读取外部 SQL 文件失败: ${readResult?.message || '未知错误'}`,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: JSON.stringify(buildExternalSQLFileSnapshot({
|
||||
filePath: requestedFilePath,
|
||||
previewCharLimit: args.previewCharLimit,
|
||||
readResult: readResult?.data,
|
||||
externalSQLDirectories,
|
||||
connections,
|
||||
tabs,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
case 'inspect_active_tab':
|
||||
return {
|
||||
content: JSON.stringify(buildActiveTabSnapshot({
|
||||
@@ -328,6 +366,7 @@ export async function executeSnapshotInspectionToolCall(
|
||||
inspect_connection_capabilities: '读取当前连接能力矩阵失败',
|
||||
inspect_saved_connections: '读取本地连接清单失败',
|
||||
inspect_external_sql_directories: '读取外部 SQL 目录失败',
|
||||
inspect_external_sql_file: '读取外部 SQL 文件失败',
|
||||
inspect_ai_sessions: '读取本地 AI 会话清单失败',
|
||||
inspect_active_tab: '读取当前活动页签失败',
|
||||
inspect_workspace_tabs: '读取当前工作区页签失败',
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => {
|
||||
connections: [connections[0]],
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'get_columns'],
|
||||
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'get_columns'],
|
||||
skills,
|
||||
userPromptSettings,
|
||||
});
|
||||
@@ -86,6 +86,7 @@ describe('buildAISystemContextMessages', () => {
|
||||
expect(joined).toContain('inspect_connection_capabilities');
|
||||
expect(joined).toContain('inspect_saved_connections');
|
||||
expect(joined).toContain('inspect_external_sql_directories');
|
||||
expect(joined).toContain('inspect_external_sql_file');
|
||||
expect(joined).toContain('inspect_saved_queries');
|
||||
expect(joined).toContain('inspect_ai_sessions');
|
||||
expect(joined).toContain('inspect_sql_snippets');
|
||||
|
||||
@@ -429,6 +429,12 @@ SELECT * FROM users WHERE status = 1;
|
||||
content: '如果用户提到“外部 SQL 目录”“目录里的脚本”“某个 SQL 文件放在哪个目录”“当前打开的 SQL 文件来自哪里”,优先调用 inspect_external_sql_directories 读取真实外部 SQL 目录资产,再决定继续读取活动页签还是定位具体脚本。',
|
||||
});
|
||||
}
|
||||
if (availableToolNames.includes('inspect_external_sql_file')) {
|
||||
systemMessages.push({
|
||||
role: 'system',
|
||||
content: '如果用户已经给出了某个外部 SQL 文件路径,或明确提到“帮我看看这个目录里的 report.sql / job.sql 在写什么”,优先调用 inspect_external_sql_file 读取真实文件内容;如果这个文件已经在编辑器中打开,再结合 inspect_active_tab 看当前草稿。',
|
||||
});
|
||||
}
|
||||
if (availableToolNames.includes('inspect_saved_queries')) {
|
||||
systemMessages.push({
|
||||
role: 'system',
|
||||
|
||||
@@ -44,6 +44,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
|
||||
inspect_connection_capabilities: '读取当前连接能力矩阵',
|
||||
inspect_saved_connections: '盘点本地已保存连接',
|
||||
inspect_external_sql_directories: '盘点外部 SQL 目录',
|
||||
inspect_external_sql_file: '读取外部 SQL 文件',
|
||||
inspect_ai_sessions: '盘点本地 AI 历史会话',
|
||||
inspect_active_tab: '读取当前活动页签',
|
||||
inspect_workspace_tabs: '盘点当前工作区页签',
|
||||
|
||||
@@ -520,6 +520,30 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_external_sql_file",
|
||||
icon: "📄",
|
||||
desc: "读取外部 SQL 文件内容",
|
||||
detail:
|
||||
"传入具体 filePath,读取已配置外部 SQL 目录中的 SQL 文件内容,并返回所属目录、绑定连接/数据库、是否已有打开页签,以及截断后的正文预览。适合用户提到“看一下这个目录里的某个脚本”“帮我解释 report.sql 在写什么”时,先读取真实文件内容再分析。",
|
||||
params: "filePath, previewCharLimit?",
|
||||
tool: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "inspect_external_sql_file",
|
||||
description:
|
||||
"读取指定外部 SQL 文件的内容预览,仅用于已配置外部 SQL 目录中的 SQL 文件。返回文件路径、所属目录、绑定连接/数据库、是否已在工作区打开,以及截断后的正文内容。适用于用户提到某个目录中的具体 SQL 脚本、想让 AI 直接解释脚本逻辑、或想确认某个外部 SQL 文件内容时,先读真实文件再回答。",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
filePath: { type: "string", description: "必填,要读取的 SQL 文件绝对路径,通常先通过 inspect_external_sql_directories 找到" },
|
||||
previewCharLimit: { type: "number", description: "可选,正文预览最多返回多少字符,默认 12000,最大 40000" },
|
||||
},
|
||||
required: ["filePath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_active_tab",
|
||||
icon: "📍",
|
||||
|
||||
@@ -73,6 +73,13 @@ describe('aiToolRegistry', () => {
|
||||
expect(info?.tool.function.description).toContain('当前打开的外部 SQL 文件页签');
|
||||
});
|
||||
|
||||
it('registers the external-sql-file inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_external_sql_file');
|
||||
expect(info).toBeTruthy();
|
||||
expect(info?.desc).toContain('外部 SQL 文件内容');
|
||||
expect(info?.tool.function.description).toContain('目录中的具体 SQL 脚本');
|
||||
});
|
||||
|
||||
it('registers the saved-query and sql-snippet inspectors as builtin tools', () => {
|
||||
const savedQueryTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_saved_queries');
|
||||
const aiSessionsTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_sessions');
|
||||
@@ -112,6 +119,7 @@ describe('aiToolRegistry', () => {
|
||||
expect(tools.some((item) => item.function.name === 'inspect_connection_capabilities')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_saved_connections')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_external_sql_directories')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_external_sql_file')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_saved_queries')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_ai_sessions')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_sql_snippets')).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user