mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
✨ feat(ai-tools): 新增 MCP 配置探针并拆分本地执行器
This commit is contained in:
@@ -28,6 +28,8 @@ describe('AIBuiltinToolsCatalog', () => {
|
||||
expect(markup).toContain('inspect_database_bundle');
|
||||
expect(markup).toContain('查看 AI 当前能力');
|
||||
expect(markup).toContain('inspect_ai_runtime');
|
||||
expect(markup).toContain('排查 MCP 接入状态');
|
||||
expect(markup).toContain('inspect_mcp_setup');
|
||||
expect(markup).toContain('查看当前 AI 上下文');
|
||||
expect(markup).toContain('inspect_ai_context');
|
||||
expect(markup).toContain('查看当前连接');
|
||||
|
||||
@@ -42,6 +42,11 @@ const BUILTIN_TOOL_FLOWS = [
|
||||
steps: 'inspect_ai_runtime → inspect_ai_context / inspect_current_connection',
|
||||
description: '适合先确认当前模型、安全级别、上下文级别、Skills 和 MCP 工具,再决定让 AI 走哪条探针链路。',
|
||||
},
|
||||
{
|
||||
title: '排查 MCP 接入状态',
|
||||
steps: 'inspect_mcp_setup → inspect_ai_runtime',
|
||||
description: '适合先确认当前配置了哪些 MCP 服务、哪些已启用、外部客户端有没有写入当前 GoNavi 路径,再结合运行时工具列表判断为什么某个工具没暴露出来。',
|
||||
},
|
||||
{
|
||||
title: '查看当前 AI 上下文',
|
||||
steps: 'inspect_ai_context → inspect_table_bundle / get_columns',
|
||||
|
||||
@@ -224,6 +224,62 @@ describe('aiLocalToolExecutor', () => {
|
||||
expect(result.content).toContain('"builtinToolCount":');
|
||||
});
|
||||
|
||||
it('returns the current mcp setup snapshot so the model can inspect configured servers and client install state', async () => {
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_mcp_setup', {}),
|
||||
connections: [buildConnection()],
|
||||
mcpTools: [{
|
||||
alias: 'browser_open',
|
||||
originalName: 'browser_open',
|
||||
serverId: 'server-1',
|
||||
serverName: 'Browser',
|
||||
title: '打开页面',
|
||||
description: '打开页面',
|
||||
}],
|
||||
toolContextMap: new Map(),
|
||||
runtime: {
|
||||
getDatabases: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
getMCPServers: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'server-1',
|
||||
name: 'Browser',
|
||||
transport: 'stdio',
|
||||
command: 'uvx',
|
||||
args: ['mcp-server-browser'],
|
||||
env: {
|
||||
OPENAI_API_KEY: '***',
|
||||
},
|
||||
enabled: true,
|
||||
timeoutSeconds: 20,
|
||||
},
|
||||
]),
|
||||
getMCPClientInstallStatuses: vi.fn().mockResolvedValue([
|
||||
{
|
||||
client: 'codex',
|
||||
displayName: 'Codex',
|
||||
installed: true,
|
||||
matchesCurrent: false,
|
||||
clientDetected: true,
|
||||
clientCommand: 'codex',
|
||||
clientPath: 'C:/Tools/codex.exe',
|
||||
configPath: 'C:/Users/demo/.codex/config.toml',
|
||||
command: 'gonavi-mcp-server',
|
||||
args: ['stdio'],
|
||||
message: '检测到旧的 GoNavi 路径',
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('"serverCount":1');
|
||||
expect(result.content).toContain('"name":"Browser"');
|
||||
expect(result.content).toContain('"launchCommandPreview":"uvx mcp-server-browser"');
|
||||
expect(result.content).toContain('"displayName":"Codex"');
|
||||
expect(result.content).toContain('"launchCommandPreview":"gonavi-mcp-server stdio"');
|
||||
});
|
||||
|
||||
it('returns the current connection snapshot so the model can inspect host, db, and ssh state', async () => {
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_current_connection', {}),
|
||||
|
||||
@@ -2,12 +2,9 @@ import { DBGetAllColumns, DBGetDatabases, DBGetTables } from '../../../wailsjs/g
|
||||
|
||||
import type { SqlLog } from '../../store';
|
||||
import type {
|
||||
AIContextLevel,
|
||||
AIChatMessage,
|
||||
AIContextItem,
|
||||
AIMCPToolDescriptor,
|
||||
AIProviderConfig,
|
||||
AISafetyLevel,
|
||||
AISkillConfig,
|
||||
AIToolCall,
|
||||
SavedConnection,
|
||||
@@ -15,23 +12,14 @@ import type {
|
||||
SqlSnippet,
|
||||
TabData,
|
||||
} from '../../types';
|
||||
import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { buildAIReadonlyPreviewSQL } from '../../utils/aiSqlLimit';
|
||||
import { buildPaginatedSelectSQL, quoteQualifiedIdent } from '../../utils/sql';
|
||||
import { resolveAITableSchemaToolResult } from '../../utils/aiTableSchemaTool';
|
||||
import { buildAIContextSnapshot } from './aiContextInsights';
|
||||
import { buildCurrentConnectionSnapshot } from './aiConnectionInsights';
|
||||
import { buildAIRuntimeSnapshot } from './aiRuntimeInsights';
|
||||
import {
|
||||
buildSavedQueriesSnapshot,
|
||||
buildSqlSnippetsSnapshot,
|
||||
} from './aiSavedSqlInsights';
|
||||
import {
|
||||
buildActiveTabSnapshot,
|
||||
buildRecentSqlLogsSnapshot,
|
||||
buildWorkspaceTabsSnapshot,
|
||||
} from './aiWorkspaceInsights';
|
||||
executeSnapshotInspectionToolCall,
|
||||
type AISnapshotInspectionRuntime,
|
||||
} from './aiSnapshotInspectionToolExecutor';
|
||||
|
||||
export interface AIToolContextEntry {
|
||||
connectionId: string;
|
||||
@@ -39,14 +27,7 @@ export interface AIToolContextEntry {
|
||||
tables: string[];
|
||||
}
|
||||
|
||||
interface AILocalRuntimeState {
|
||||
providers?: AIProviderConfig[];
|
||||
activeProviderId?: string;
|
||||
safetyLevel?: AISafetyLevel | string;
|
||||
contextLevel?: AIContextLevel | string;
|
||||
}
|
||||
|
||||
interface AILocalToolRuntime {
|
||||
export interface AILocalToolRuntime extends AISnapshotInspectionRuntime {
|
||||
getDatabases: (config: any) => Promise<any>;
|
||||
getTables: (config: any, dbName: string) => Promise<any>;
|
||||
getAllColumns: (config: any, dbName: string) => Promise<any>;
|
||||
@@ -58,7 +39,6 @@ interface AILocalToolRuntime {
|
||||
query: (config: any, dbName: string, sql: string) => Promise<any>;
|
||||
checkSQL?: (sql: string) => Promise<{ allowed?: boolean; operationType?: string } | undefined>;
|
||||
callMCPTool?: (name: string, args: string) => Promise<{ content?: string; isError?: boolean } | undefined>;
|
||||
getAIRuntimeState?: () => Promise<AILocalRuntimeState | undefined>;
|
||||
}
|
||||
|
||||
export interface ExecuteLocalAIToolCallOptions {
|
||||
@@ -144,10 +124,22 @@ const buildDefaultRuntime = (): AILocalToolRuntime => ({
|
||||
contextLevel: String(contextLevel || '').trim(),
|
||||
};
|
||||
},
|
||||
getMCPServers: async () => {
|
||||
const service = (window as any).go?.aiservice?.Service;
|
||||
if (typeof service?.AIGetMCPServers !== 'function') {
|
||||
return undefined;
|
||||
}
|
||||
return service.AIGetMCPServers();
|
||||
},
|
||||
getMCPClientInstallStatuses: async () => {
|
||||
const service = (window as any).go?.aiservice?.Service;
|
||||
if (typeof service?.AIGetMCPClientInstallStatuses !== 'function') {
|
||||
return undefined;
|
||||
}
|
||||
return service.AIGetMCPClientInstallStatuses();
|
||||
},
|
||||
});
|
||||
|
||||
const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name);
|
||||
|
||||
const normalizeTableList = (rows: any[]): string[] =>
|
||||
rows.map((row) => row.Table || row.table || (Object.values(row)[0] as string));
|
||||
|
||||
@@ -235,86 +227,32 @@ export async function executeLocalAIToolCall({
|
||||
|
||||
try {
|
||||
const args = JSON.parse(toolCall.function.arguments || '{}');
|
||||
const snapshotInspectionResult = await executeSnapshotInspectionToolCall({
|
||||
toolName: toolCall.function.name,
|
||||
args,
|
||||
activeContext,
|
||||
aiContexts,
|
||||
connections,
|
||||
tabs,
|
||||
activeTabId,
|
||||
mcpTools,
|
||||
sqlLogs,
|
||||
savedQueries,
|
||||
sqlSnippets,
|
||||
skills,
|
||||
dynamicModels,
|
||||
runtime: mergedRuntime,
|
||||
});
|
||||
if (snapshotInspectionResult) {
|
||||
content = snapshotInspectionResult.content;
|
||||
success = snapshotInspectionResult.success;
|
||||
return {
|
||||
content,
|
||||
success,
|
||||
toolName: buildToolName(toolCall, descriptor),
|
||||
};
|
||||
}
|
||||
switch (toolCall.function.name) {
|
||||
case 'inspect_ai_runtime': {
|
||||
try {
|
||||
const runtimeState = typeof mergedRuntime.getAIRuntimeState === 'function'
|
||||
? await mergedRuntime.getAIRuntimeState()
|
||||
: undefined;
|
||||
content = JSON.stringify(buildAIRuntimeSnapshot({
|
||||
providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [],
|
||||
activeProviderId: runtimeState?.activeProviderId || '',
|
||||
safetyLevel: runtimeState?.safetyLevel,
|
||||
contextLevel: runtimeState?.contextLevel,
|
||||
skills,
|
||||
mcpTools,
|
||||
dynamicModels,
|
||||
builtinToolNames: BUILTIN_AI_TOOL_NAMES,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取当前 AI 运行状态失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_current_connection': {
|
||||
try {
|
||||
content = JSON.stringify(buildCurrentConnectionSnapshot({
|
||||
activeContext,
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取当前连接失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_active_tab': {
|
||||
try {
|
||||
content = JSON.stringify(buildActiveTabSnapshot({
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent: args.includeContent !== false,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取当前活动页签失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_workspace_tabs': {
|
||||
try {
|
||||
content = JSON.stringify(buildWorkspaceTabsSnapshot({
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent: args.includeContent === true,
|
||||
limit: args.limit,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取当前工作区页签失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_ai_context': {
|
||||
try {
|
||||
content = JSON.stringify(buildAIContextSnapshot({
|
||||
activeContext,
|
||||
aiContexts,
|
||||
connections,
|
||||
includeDDL: args.includeDDL === true,
|
||||
ddlLimit: args.ddlLimit,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取当前 AI 上下文失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'get_connections': {
|
||||
const availableConnections = connections.map((connection) => ({
|
||||
id: connection.id,
|
||||
@@ -708,50 +646,6 @@ export async function executeLocalAIToolCall({
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_recent_sql_logs': {
|
||||
try {
|
||||
content = JSON.stringify(buildRecentSqlLogsSnapshot({
|
||||
sqlLogs,
|
||||
limit: args.limit,
|
||||
status: args.status,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `获取最近 SQL 日志失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_saved_queries': {
|
||||
try {
|
||||
content = JSON.stringify(buildSavedQueriesSnapshot({
|
||||
savedQueries,
|
||||
connections,
|
||||
keyword: args.keyword,
|
||||
connectionId: args.connectionId,
|
||||
dbName: args.dbName,
|
||||
limit: args.limit,
|
||||
includeSql: args.includeSql !== false,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取已保存查询失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_sql_snippets': {
|
||||
try {
|
||||
content = JSON.stringify(buildSqlSnippetsSnapshot({
|
||||
sqlSnippets,
|
||||
keyword: args.keyword,
|
||||
limit: args.limit,
|
||||
includeBody: args.includeBody !== false,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取 SQL 片段失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'preview_table_rows': {
|
||||
const connection = findConnection(connections, args.connectionId);
|
||||
if (!connection) {
|
||||
|
||||
59
frontend/src/components/ai/aiMCPInsights.test.ts
Normal file
59
frontend/src/components/ai/aiMCPInsights.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildMCPSetupSnapshot } from './aiMCPInsights';
|
||||
|
||||
describe('aiMCPInsights', () => {
|
||||
it('builds a combined snapshot for local mcp servers, tools, and external client install state', () => {
|
||||
const snapshot = buildMCPSetupSnapshot({
|
||||
mcpServers: [
|
||||
{
|
||||
id: 'server-1',
|
||||
name: 'Browser',
|
||||
transport: 'stdio',
|
||||
command: 'uvx',
|
||||
args: ['mcp-server-browser'],
|
||||
env: {
|
||||
OPENAI_API_KEY: '***',
|
||||
BASE_URL: 'http://127.0.0.1',
|
||||
},
|
||||
enabled: true,
|
||||
timeoutSeconds: 20,
|
||||
},
|
||||
],
|
||||
mcpClientStatuses: [
|
||||
{
|
||||
client: 'claude-code',
|
||||
displayName: 'Claude Code',
|
||||
installed: true,
|
||||
matchesCurrent: true,
|
||||
clientDetected: true,
|
||||
clientCommand: 'claude',
|
||||
clientPath: 'C:/Tools/claude.exe',
|
||||
configPath: 'C:/Users/demo/.claude/mcp.json',
|
||||
command: 'gonavi-mcp-server',
|
||||
args: ['stdio'],
|
||||
message: '已写入当前 GoNavi 路径',
|
||||
},
|
||||
],
|
||||
mcpTools: [
|
||||
{
|
||||
alias: 'browser_open',
|
||||
originalName: 'browser_open',
|
||||
serverId: 'server-1',
|
||||
serverName: 'Browser',
|
||||
title: '打开页面',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(snapshot.serverCount).toBe(1);
|
||||
expect(snapshot.enabledServerCount).toBe(1);
|
||||
expect(snapshot.discoveredMCPToolCount).toBe(1);
|
||||
expect(snapshot.servers[0].launchCommandPreview).toBe('uvx mcp-server-browser');
|
||||
expect(snapshot.servers[0].envVarCount).toBe(2);
|
||||
expect(snapshot.servers[0].discoveredToolCount).toBe(1);
|
||||
expect(snapshot.clients[0].displayName).toBe('Claude Code');
|
||||
expect(snapshot.clients[0].launchCommandPreview).toBe('gonavi-mcp-server stdio');
|
||||
expect(snapshot.currentClientCount).toBe(1);
|
||||
});
|
||||
});
|
||||
93
frontend/src/components/ai/aiMCPInsights.ts
Normal file
93
frontend/src/components/ai/aiMCPInsights.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { AIMCPClientInstallStatus, AIMCPServerConfig, AIMCPToolDescriptor } from '../../types';
|
||||
|
||||
const SERVER_TOOL_PREVIEW_LIMIT = 20;
|
||||
|
||||
const quoteCommandPart = (value: string): string => {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
return /[\s"]/u.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
|
||||
};
|
||||
|
||||
const formatLaunchPreview = (command?: string, args?: string[]): string =>
|
||||
[String(command || '').trim(), ...(Array.isArray(args) ? args : [])]
|
||||
.map((item) => quoteCommandPart(String(item || '').trim()))
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const sortByName = <T extends { name?: string }>(items: T[]): T[] =>
|
||||
items.slice().sort((left, right) => String(left.name || '').localeCompare(String(right.name || '')));
|
||||
|
||||
export const buildMCPSetupSnapshot = (params: {
|
||||
mcpServers?: AIMCPServerConfig[];
|
||||
mcpClientStatuses?: AIMCPClientInstallStatus[];
|
||||
mcpTools?: AIMCPToolDescriptor[];
|
||||
}) => {
|
||||
const {
|
||||
mcpServers = [],
|
||||
mcpClientStatuses = [],
|
||||
mcpTools = [],
|
||||
} = params;
|
||||
|
||||
const normalizedServers = sortByName(
|
||||
(Array.isArray(mcpServers) ? mcpServers : []).map((server) => {
|
||||
const serverTools = mcpTools
|
||||
.filter((tool) => tool.serverId === server.id)
|
||||
.map((tool) => ({
|
||||
alias: tool.alias,
|
||||
title: tool.title || tool.originalName || tool.alias,
|
||||
}));
|
||||
return {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
transport: server.transport,
|
||||
enabled: server.enabled !== false,
|
||||
timeoutSeconds: server.timeoutSeconds,
|
||||
command: server.command,
|
||||
args: Array.isArray(server.args) ? server.args : [],
|
||||
launchCommandPreview: formatLaunchPreview(server.command, server.args),
|
||||
envKeys: Object.keys(server.env || {}).sort(),
|
||||
envVarCount: Object.keys(server.env || {}).length,
|
||||
discoveredToolCount: serverTools.length,
|
||||
discoveredTools: serverTools.slice(0, SERVER_TOOL_PREVIEW_LIMIT),
|
||||
discoveredToolsTruncated: serverTools.length > SERVER_TOOL_PREVIEW_LIMIT,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const normalizedClientStatuses = (Array.isArray(mcpClientStatuses) ? mcpClientStatuses : [])
|
||||
.map((status) => ({
|
||||
client: status.client,
|
||||
displayName: status.displayName,
|
||||
installed: status.installed,
|
||||
matchesCurrent: status.matchesCurrent,
|
||||
clientDetected: status.clientDetected === true,
|
||||
clientCommand: status.clientCommand || '',
|
||||
clientPath: status.clientPath || '',
|
||||
configPath: status.configPath || '',
|
||||
launchCommandPreview: formatLaunchPreview(status.command, status.args),
|
||||
message: status.message || '',
|
||||
}))
|
||||
.sort((left, right) => left.displayName.localeCompare(right.displayName));
|
||||
|
||||
const enabledServerCount = normalizedServers.filter((server) => server.enabled).length;
|
||||
const installedClientCount = normalizedClientStatuses.filter((item) => item.installed).length;
|
||||
const currentClientCount = normalizedClientStatuses.filter((item) => item.matchesCurrent).length;
|
||||
|
||||
return {
|
||||
serverCount: normalizedServers.length,
|
||||
enabledServerCount,
|
||||
disabledServerCount: normalizedServers.length - enabledServerCount,
|
||||
discoveredMCPToolCount: Array.isArray(mcpTools) ? mcpTools.length : 0,
|
||||
servers: normalizedServers,
|
||||
clientInstallCount: normalizedClientStatuses.length,
|
||||
installedClientCount,
|
||||
currentClientCount,
|
||||
detectedClientCount: normalizedClientStatuses.filter((item) => item.clientDetected).length,
|
||||
clients: normalizedClientStatuses,
|
||||
message: normalizedServers.length > 0
|
||||
? `当前共配置 ${normalizedServers.length} 个 MCP 服务,其中 ${enabledServerCount} 个已启用`
|
||||
: '当前还没有配置任何 MCP 服务',
|
||||
};
|
||||
};
|
||||
215
frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts
Normal file
215
frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import type {
|
||||
AIContextItem,
|
||||
AIMCPClientInstallStatus,
|
||||
AIMCPServerConfig,
|
||||
AIMCPToolDescriptor,
|
||||
AIProviderConfig,
|
||||
AISafetyLevel,
|
||||
AISkillConfig,
|
||||
SavedConnection,
|
||||
SavedQuery,
|
||||
SqlSnippet,
|
||||
TabData,
|
||||
} from '../../types';
|
||||
import type { SqlLog } from '../../store';
|
||||
import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry';
|
||||
import { buildAIContextSnapshot } from './aiContextInsights';
|
||||
import { buildCurrentConnectionSnapshot } from './aiConnectionInsights';
|
||||
import { buildMCPSetupSnapshot } from './aiMCPInsights';
|
||||
import { buildAIRuntimeSnapshot } from './aiRuntimeInsights';
|
||||
import {
|
||||
buildSavedQueriesSnapshot,
|
||||
buildSqlSnippetsSnapshot,
|
||||
} from './aiSavedSqlInsights';
|
||||
import {
|
||||
buildActiveTabSnapshot,
|
||||
buildRecentSqlLogsSnapshot,
|
||||
buildWorkspaceTabsSnapshot,
|
||||
} from './aiWorkspaceInsights';
|
||||
|
||||
export interface AISnapshotInspectionRuntimeState {
|
||||
providers?: AIProviderConfig[];
|
||||
activeProviderId?: string;
|
||||
safetyLevel?: AISafetyLevel | string;
|
||||
contextLevel?: string;
|
||||
}
|
||||
|
||||
export interface AISnapshotInspectionRuntime {
|
||||
getAIRuntimeState?: () => Promise<AISnapshotInspectionRuntimeState | undefined>;
|
||||
getMCPServers?: () => Promise<AIMCPServerConfig[] | undefined>;
|
||||
getMCPClientInstallStatuses?: () => Promise<AIMCPClientInstallStatus[] | undefined>;
|
||||
}
|
||||
|
||||
interface ExecuteSnapshotInspectionToolCallOptions {
|
||||
toolName: string;
|
||||
args: Record<string, any>;
|
||||
activeContext?: { connectionId: string; dbName: string } | null;
|
||||
aiContexts?: Record<string, AIContextItem[]>;
|
||||
connections: SavedConnection[];
|
||||
tabs?: TabData[];
|
||||
activeTabId?: string | null;
|
||||
mcpTools: AIMCPToolDescriptor[];
|
||||
sqlLogs?: SqlLog[];
|
||||
savedQueries?: SavedQuery[];
|
||||
sqlSnippets?: SqlSnippet[];
|
||||
skills?: AISkillConfig[];
|
||||
dynamicModels?: string[];
|
||||
runtime?: AISnapshotInspectionRuntime;
|
||||
}
|
||||
|
||||
interface SnapshotInspectionResult {
|
||||
content: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name);
|
||||
|
||||
export async function executeSnapshotInspectionToolCall(
|
||||
options: ExecuteSnapshotInspectionToolCallOptions,
|
||||
): Promise<SnapshotInspectionResult | null> {
|
||||
const {
|
||||
toolName,
|
||||
args,
|
||||
activeContext = null,
|
||||
aiContexts = {},
|
||||
connections,
|
||||
tabs = [],
|
||||
activeTabId = null,
|
||||
mcpTools,
|
||||
sqlLogs = [],
|
||||
savedQueries = [],
|
||||
sqlSnippets = [],
|
||||
skills = [],
|
||||
dynamicModels = [],
|
||||
runtime,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
switch (toolName) {
|
||||
case 'inspect_ai_runtime': {
|
||||
const runtimeState = typeof runtime?.getAIRuntimeState === 'function'
|
||||
? await runtime.getAIRuntimeState()
|
||||
: undefined;
|
||||
return {
|
||||
content: JSON.stringify(buildAIRuntimeSnapshot({
|
||||
providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [],
|
||||
activeProviderId: runtimeState?.activeProviderId || '',
|
||||
safetyLevel: runtimeState?.safetyLevel,
|
||||
contextLevel: runtimeState?.contextLevel,
|
||||
skills,
|
||||
mcpTools,
|
||||
dynamicModels,
|
||||
builtinToolNames: BUILTIN_AI_TOOL_NAMES,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
case 'inspect_mcp_setup': {
|
||||
const [mcpServers, mcpClientInstallStatuses] = await Promise.all([
|
||||
typeof runtime?.getMCPServers === 'function' ? runtime.getMCPServers() : Promise.resolve(undefined),
|
||||
typeof runtime?.getMCPClientInstallStatuses === 'function' ? runtime.getMCPClientInstallStatuses() : Promise.resolve(undefined),
|
||||
]);
|
||||
return {
|
||||
content: JSON.stringify(buildMCPSetupSnapshot({
|
||||
mcpServers: Array.isArray(mcpServers) ? mcpServers : [],
|
||||
mcpClientStatuses: Array.isArray(mcpClientInstallStatuses) ? mcpClientInstallStatuses : [],
|
||||
mcpTools,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
case 'inspect_current_connection':
|
||||
return {
|
||||
content: JSON.stringify(buildCurrentConnectionSnapshot({
|
||||
activeContext,
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_active_tab':
|
||||
return {
|
||||
content: JSON.stringify(buildActiveTabSnapshot({
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent: args.includeContent !== false,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_workspace_tabs':
|
||||
return {
|
||||
content: JSON.stringify(buildWorkspaceTabsSnapshot({
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent: args.includeContent === true,
|
||||
limit: args.limit,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_ai_context':
|
||||
return {
|
||||
content: JSON.stringify(buildAIContextSnapshot({
|
||||
activeContext,
|
||||
aiContexts,
|
||||
connections,
|
||||
includeDDL: args.includeDDL === true,
|
||||
ddlLimit: args.ddlLimit,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_recent_sql_logs':
|
||||
return {
|
||||
content: JSON.stringify(buildRecentSqlLogsSnapshot({
|
||||
sqlLogs,
|
||||
limit: args.limit,
|
||||
status: args.status,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_saved_queries':
|
||||
return {
|
||||
content: JSON.stringify(buildSavedQueriesSnapshot({
|
||||
savedQueries,
|
||||
connections,
|
||||
keyword: args.keyword,
|
||||
connectionId: args.connectionId,
|
||||
dbName: args.dbName,
|
||||
limit: args.limit,
|
||||
includeSql: args.includeSql !== false,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_sql_snippets':
|
||||
return {
|
||||
content: JSON.stringify(buildSqlSnippetsSnapshot({
|
||||
sqlSnippets,
|
||||
keyword: args.keyword,
|
||||
limit: args.limit,
|
||||
includeBody: args.includeBody !== false,
|
||||
})),
|
||||
success: true,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
const label = {
|
||||
inspect_ai_runtime: '读取当前 AI 运行状态失败',
|
||||
inspect_mcp_setup: '读取 MCP 配置状态失败',
|
||||
inspect_current_connection: '读取当前连接失败',
|
||||
inspect_active_tab: '读取当前活动页签失败',
|
||||
inspect_workspace_tabs: '读取当前工作区页签失败',
|
||||
inspect_ai_context: '读取当前 AI 上下文失败',
|
||||
inspect_recent_sql_logs: '获取最近 SQL 日志失败',
|
||||
inspect_saved_queries: '读取已保存查询失败',
|
||||
inspect_sql_snippets: '读取 SQL 片段失败',
|
||||
}[toolName] || '读取本地探针快照失败';
|
||||
return {
|
||||
content: `${label}: ${error?.message || error}`,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => {
|
||||
connections: [connections[0]],
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'],
|
||||
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_mcp_setup', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'],
|
||||
skills,
|
||||
userPromptSettings,
|
||||
});
|
||||
@@ -76,6 +76,7 @@ describe('buildAISystemContextMessages', () => {
|
||||
const joined = messages.map((message) => message.content).join('\n');
|
||||
expect(joined).toContain('inspect_workspace_tabs 盘点当前工作区');
|
||||
expect(joined).toContain('inspect_ai_runtime 读取当前 AI 运行状态');
|
||||
expect(joined).toContain('inspect_mcp_setup 读取真实 MCP 配置');
|
||||
expect(joined).toContain('inspect_ai_context 读取当前挂载的表结构上下文');
|
||||
expect(joined).toContain('inspect_current_connection');
|
||||
expect(joined).toContain('inspect_saved_queries');
|
||||
|
||||
@@ -108,6 +108,19 @@ const appendAIRuntimeInspectionGuidance = (
|
||||
});
|
||||
};
|
||||
|
||||
const appendMCPSetupInspectionGuidance = (
|
||||
messages: AISystemContextMessage[],
|
||||
availableToolNames: string[],
|
||||
) => {
|
||||
if (!availableToolNames.includes('inspect_mcp_setup')) {
|
||||
return;
|
||||
}
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: '如果用户提到“我现在配了哪些 MCP”“Claude/Codex 有没有接入 GoNavi MCP”“为什么外部客户端用不了”“当前 MCP 服务启用了哪些”,优先调用 inspect_mcp_setup 读取真实 MCP 配置和外部客户端接入状态,不要凭记忆猜测。',
|
||||
});
|
||||
};
|
||||
|
||||
const resolveDatabaseDisplayType = (config: ConnectionConfig | undefined): string => {
|
||||
const dbType = config?.type || 'unknown';
|
||||
return dbType === 'diros' ? 'Doris' : dbType.charAt(0).toUpperCase() + dbType.slice(1);
|
||||
@@ -325,6 +338,7 @@ SELECT * FROM users WHERE status = 1;
|
||||
});
|
||||
}
|
||||
appendAIRuntimeInspectionGuidance(systemMessages, availableToolNames);
|
||||
appendMCPSetupInspectionGuidance(systemMessages, availableToolNames);
|
||||
if (availableToolNames.includes('inspect_current_connection')) {
|
||||
systemMessages.push({
|
||||
role: 'system',
|
||||
|
||||
@@ -24,6 +24,7 @@ interface AIToolCallingBlockProps {
|
||||
|
||||
const TOOL_ACTION_LABELS: Record<string, string> = {
|
||||
inspect_ai_runtime: '读取当前 AI 运行状态',
|
||||
inspect_mcp_setup: '读取当前 MCP 配置状态',
|
||||
get_connections: '获取可用连接信息',
|
||||
get_databases: '扫描数据库列表',
|
||||
get_tables: '分析表结构信息',
|
||||
|
||||
@@ -10,6 +10,13 @@ describe('aiToolRegistry', () => {
|
||||
expect(info?.tool.function.description).toContain('当前供应商');
|
||||
});
|
||||
|
||||
it('registers the mcp-setup inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_setup');
|
||||
expect(info).toBeTruthy();
|
||||
expect(info?.desc).toContain('MCP 配置');
|
||||
expect(info?.tool.function.description).toContain('外部客户端');
|
||||
});
|
||||
|
||||
it('registers the current-connection inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_current_connection');
|
||||
expect(info).toBeTruthy();
|
||||
@@ -44,6 +51,7 @@ describe('aiToolRegistry', () => {
|
||||
}]);
|
||||
|
||||
expect(tools.some((item) => item.function.name === 'inspect_ai_runtime')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_mcp_setup')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_current_connection')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_saved_queries')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_sql_snippets')).toBe(true);
|
||||
|
||||
@@ -326,6 +326,23 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_mcp_setup",
|
||||
icon: "🪛",
|
||||
desc: "查看当前 MCP 配置与外部接入状态",
|
||||
detail:
|
||||
"返回当前本地配置了哪些 MCP 服务、哪些已启用、每个服务声明了什么启动命令,以及 Claude Code / Codex 这类外部客户端的写入状态与命令检测结果。适合用户问“我现在配了哪些 MCP”“为什么外部客户端还用不了”“MCP 到底写没写进去”时先读真实状态。",
|
||||
params: "无参数",
|
||||
tool: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "inspect_mcp_setup",
|
||||
description:
|
||||
"读取当前本地 MCP 配置快照,包括 MCP 服务列表、启用状态、启动命令、环境变量 key、已发现工具,以及外部客户端的 GoNavi MCP 写入状态与本机 CLI 检测结果。适用于用户提到 MCP 服务配置、Claude/Codex 是否已接入、为什么外部客户端用不了、当前到底启用了哪些 MCP 时,先读取真实配置再回答。",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_ai_context",
|
||||
icon: "🧷",
|
||||
|
||||
Reference in New Issue
Block a user