feat(ai-tools): 新增 AI 运行时探针

This commit is contained in:
Syngnat
2026-06-08 19:50:39 +08:00
parent 5c867fd121
commit dc38602d32
12 changed files with 427 additions and 2 deletions

View File

@@ -878,6 +878,8 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
sqlLogs: useStore.getState().sqlLogs,
savedQueries: useStore.getState().savedQueries,
sqlSnippets: useStore.getState().sqlSnippets,
skills,
dynamicModels,
});
const toolResultMsg: AIChatMessage = buildToolResultMessage({
id: genId(),
@@ -996,7 +998,7 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
console.error('Failed to chain tool call', e);
setSending(false);
}
}, [availableTools, buildSystemContextMessages, mcpTools, sid]);
}, [availableTools, buildSystemContextMessages, dynamicModels, mcpTools, sid, skills]);
const handleSend = useCallback(async () => {
const text = input.trim();

View File

@@ -26,6 +26,8 @@ describe('AIBuiltinToolsCatalog', () => {
expect(markup).toContain('inspect_table_bundle');
expect(markup).toContain('全库快速摸底');
expect(markup).toContain('inspect_database_bundle');
expect(markup).toContain('查看 AI 当前能力');
expect(markup).toContain('inspect_ai_runtime');
expect(markup).toContain('查看当前 AI 上下文');
expect(markup).toContain('inspect_ai_context');
expect(markup).toContain('查看当前连接');

View File

@@ -37,6 +37,11 @@ const BUILTIN_TOOL_FLOWS = [
steps: 'inspect_database_bundle → inspect_table_bundle',
description: '适合先看整库有哪些表、每张表大概有哪些字段,再对目标表继续做深挖快照。',
},
{
title: '查看 AI 当前能力',
steps: 'inspect_ai_runtime → inspect_ai_context / inspect_current_connection',
description: '适合先确认当前模型、安全级别、上下文级别、Skills 和 MCP 工具,再决定让 AI 走哪条探针链路。',
},
{
title: '查看当前 AI 上下文',
steps: 'inspect_ai_context → inspect_table_bundle / get_columns',

View File

@@ -169,6 +169,61 @@ describe('aiLocalToolExecutor', () => {
expect(result.content).toContain('CREATE TABLE orders');
});
it('returns the current ai runtime snapshot so the model can inspect provider, safety, skills, and tools', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_runtime', {}),
connections: [buildConnection()],
mcpTools: [{
alias: 'browser_open',
originalName: 'browser_open',
serverId: 'server-1',
serverName: 'browser',
title: '打开浏览器',
description: '打开页面',
}],
skills: [{
id: 'skill-1',
name: '结构审查',
systemPrompt: '先核对字段',
enabled: true,
scopes: ['database'],
requiredTools: ['get_columns'],
}],
dynamicModels: ['gpt-5.4', 'gpt-4.1'],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getAIRuntimeState: vi.fn().mockResolvedValue({
activeProviderId: 'provider-1',
providers: [{
id: 'provider-1',
type: 'openai',
name: 'OpenAI 主账号',
apiKey: '',
hasSecret: true,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-5.4',
models: ['gpt-5.4', 'gpt-4.1'],
maxTokens: 32000,
temperature: 0.2,
}],
safetyLevel: 'readonly',
contextLevel: 'with_samples',
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"hasActiveProvider":true');
expect(result.content).toContain('"name":"OpenAI 主账号"');
expect(result.content).toContain('"safetyLevel":"readonly"');
expect(result.content).toContain('"contextLevel":"with_samples"');
expect(result.content).toContain('"enabledSkillCount":1');
expect(result.content).toContain('"alias":"browser_open"');
expect(result.content).toContain('"builtinToolCount":');
});
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', {}),

View File

@@ -2,21 +2,27 @@ import { DBGetAllColumns, DBGetDatabases, DBGetTables } from '../../../wailsjs/g
import type { SqlLog } from '../../store';
import type {
AIContextLevel,
AIChatMessage,
AIContextItem,
AIMCPToolDescriptor,
AIProviderConfig,
AISafetyLevel,
AISkillConfig,
AIToolCall,
SavedConnection,
SavedQuery,
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,
@@ -33,6 +39,13 @@ export interface AIToolContextEntry {
tables: string[];
}
interface AILocalRuntimeState {
providers?: AIProviderConfig[];
activeProviderId?: string;
safetyLevel?: AISafetyLevel | string;
contextLevel?: AIContextLevel | string;
}
interface AILocalToolRuntime {
getDatabases: (config: any) => Promise<any>;
getTables: (config: any, dbName: string) => Promise<any>;
@@ -45,6 +58,7 @@ 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 {
@@ -59,6 +73,8 @@ export interface ExecuteLocalAIToolCallOptions {
sqlLogs?: SqlLog[];
savedQueries?: SavedQuery[];
sqlSnippets?: SqlSnippet[];
skills?: AISkillConfig[];
dynamicModels?: string[];
runtime?: Partial<AILocalToolRuntime>;
}
@@ -110,8 +126,28 @@ const buildDefaultRuntime = (): AILocalToolRuntime => ({
}
return service.AICallMCPTool(name, args);
},
getAIRuntimeState: async () => {
const service = (window as any).go?.aiservice?.Service;
if (!service) {
return undefined;
}
const [providers, activeProviderId, safetyLevel, contextLevel] = await Promise.all([
typeof service.AIGetProviders === 'function' ? service.AIGetProviders() : Promise.resolve([]),
typeof service.AIGetActiveProvider === 'function' ? service.AIGetActiveProvider() : Promise.resolve(''),
typeof service.AIGetSafetyLevel === 'function' ? service.AIGetSafetyLevel() : Promise.resolve(''),
typeof service.AIGetContextLevel === 'function' ? service.AIGetContextLevel() : Promise.resolve(''),
]);
return {
providers: Array.isArray(providers) ? providers : [],
activeProviderId: String(activeProviderId || '').trim(),
safetyLevel: String(safetyLevel || '').trim(),
contextLevel: String(contextLevel || '').trim(),
};
},
});
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));
@@ -188,6 +224,8 @@ export async function executeLocalAIToolCall({
sqlLogs = [],
savedQueries = [],
sqlSnippets = [],
skills = [],
dynamicModels = [],
runtime,
}: ExecuteLocalAIToolCallOptions): Promise<ExecuteLocalAIToolCallResult> {
const mergedRuntime = { ...buildDefaultRuntime(), ...(runtime || {}) };
@@ -198,6 +236,27 @@ export async function executeLocalAIToolCall({
try {
const args = JSON.parse(toolCall.function.arguments || '{}');
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({

View File

@@ -0,0 +1,119 @@
import { describe, expect, it } from 'vitest';
import type { AIMCPToolDescriptor, AIProviderConfig, AISkillConfig } from '../../types';
import { buildAIRuntimeSnapshot } from './aiRuntimeInsights';
const providers: AIProviderConfig[] = [{
id: 'provider-1',
type: 'openai',
name: 'OpenAI 主账号',
apiKey: '',
hasSecret: true,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-5.4',
models: ['gpt-5.4', 'gpt-4.1'],
maxTokens: 32000,
temperature: 0.2,
}];
const skills: AISkillConfig[] = [
{
id: 'skill-1',
name: '结构审查',
systemPrompt: '先核对字段。',
enabled: true,
scopes: ['database'],
requiredTools: ['get_columns'],
},
{
id: 'skill-2',
name: '已禁用技能',
systemPrompt: 'ignore',
enabled: false,
scopes: ['global'],
},
];
const mcpTools: AIMCPToolDescriptor[] = [{
alias: 'browser_open',
originalName: 'browser_open',
serverId: 'server-1',
serverName: 'browser',
title: '打开浏览器',
description: '打开目标页面',
}];
describe('buildAIRuntimeSnapshot', () => {
it('returns a sanitized runtime snapshot for the active provider, tools, and skills', () => {
const snapshot = buildAIRuntimeSnapshot({
providers,
activeProviderId: 'provider-1',
safetyLevel: 'readonly',
contextLevel: 'with_samples',
skills,
mcpTools,
dynamicModels: ['gpt-5.4', 'gpt-4.1-mini'],
builtinToolNames: ['inspect_ai_runtime', 'get_columns', 'inspect_current_connection'],
});
expect(snapshot).toMatchObject({
hasActiveProvider: true,
providerCount: 1,
safetyLevel: 'readonly',
safetyLabel: '只读',
contextLevel: 'with_samples',
contextLabel: '结构+样例',
dynamicModelCount: 2,
enabledSkillCount: 1,
builtinToolCount: 3,
mcpToolCount: 1,
totalAvailableToolCount: 4,
capabilities: {
canWriteData: false,
canUseSampleContext: true,
hasExternalMCPTools: true,
hasCustomSkills: true,
},
});
expect(snapshot.activeProvider).toMatchObject({
id: 'provider-1',
name: 'OpenAI 主账号',
model: 'gpt-5.4',
hasSecret: true,
});
expect(JSON.stringify(snapshot)).not.toContain('apiKey');
expect(snapshot.enabledSkills).toEqual([
{
id: 'skill-1',
name: '结构审查',
scopes: ['database'],
requiredTools: ['get_columns'],
},
]);
expect(snapshot.mcpTools).toEqual([
{
alias: 'browser_open',
title: '打开浏览器',
serverName: 'browser',
},
]);
});
it('returns a clear empty state when no provider is active', () => {
const snapshot = buildAIRuntimeSnapshot({
providers: [],
activeProviderId: '',
safetyLevel: 'readonly',
contextLevel: 'schema_only',
skills: [],
mcpTools: [],
builtinToolNames: [],
});
expect(snapshot).toMatchObject({
hasActiveProvider: false,
activeProvider: null,
message: '当前未启用 AI 供应商',
});
});
});

View File

@@ -0,0 +1,140 @@
import type {
AIContextLevel,
AIMCPToolDescriptor,
AIProviderConfig,
AISafetyLevel,
AISkillConfig,
} from '../../types';
const SAFETY_LEVEL_LABELS: Record<string, string> = {
readonly: '只读',
readwrite: '读写',
full: '完全开放',
};
const CONTEXT_LEVEL_LABELS: Record<string, string> = {
schema_only: '仅结构',
with_samples: '结构+样例',
with_results: '结构+结果',
};
const BUILTIN_TOOL_PREVIEW_LIMIT = 30;
const MCP_TOOL_PREVIEW_LIMIT = 40;
const DYNAMIC_MODEL_PREVIEW_LIMIT = 20;
const sliceList = <T,>(items: T[], limit: number) => {
const list = Array.isArray(items) ? items : [];
return {
items: list.slice(0, limit),
truncated: list.length > limit,
total: list.length,
};
};
const normalizeSafetyLevel = (value: AISafetyLevel | string | undefined): string =>
String(value || 'unknown').trim() || 'unknown';
const normalizeContextLevel = (value: AIContextLevel | string | undefined): string =>
String(value || 'unknown').trim() || 'unknown';
export const buildAIRuntimeSnapshot = (params: {
providers?: AIProviderConfig[];
activeProviderId?: string | null;
safetyLevel?: AISafetyLevel | string;
contextLevel?: AIContextLevel | string;
skills?: AISkillConfig[];
mcpTools?: AIMCPToolDescriptor[];
dynamicModels?: string[];
builtinToolNames?: string[];
}) => {
const {
providers = [],
activeProviderId = '',
safetyLevel,
contextLevel,
skills = [],
mcpTools = [],
dynamicModels = [],
builtinToolNames = [],
} = params;
const activeProvider = providers.find((provider) => provider.id === activeProviderId) || null;
const enabledSkills = skills.filter((skill) => skill?.enabled);
const builtinPreview = sliceList(
builtinToolNames
.map((name) => String(name || '').trim())
.filter(Boolean)
.sort((left, right) => left.localeCompare(right)),
BUILTIN_TOOL_PREVIEW_LIMIT,
);
const mcpPreview = sliceList(
mcpTools.map((tool) => ({
alias: tool.alias,
title: tool.title || tool.originalName || tool.alias,
serverName: tool.serverName,
})),
MCP_TOOL_PREVIEW_LIMIT,
);
const dynamicModelPreview = sliceList(
dynamicModels
.map((model) => String(model || '').trim())
.filter(Boolean),
DYNAMIC_MODEL_PREVIEW_LIMIT,
);
const normalizedSafetyLevel = normalizeSafetyLevel(safetyLevel);
const normalizedContextLevel = normalizeContextLevel(contextLevel);
return {
hasActiveProvider: Boolean(activeProvider),
activeProvider: activeProvider ? {
id: activeProvider.id,
name: activeProvider.name,
type: activeProvider.type,
apiFormat: activeProvider.apiFormat || 'openai',
model: activeProvider.model || '',
baseUrl: activeProvider.baseUrl || '',
hasSecret: activeProvider.hasSecret ?? Boolean(activeProvider.secretRef || activeProvider.apiKey),
declaredModelCount: Array.isArray(activeProvider.models) ? activeProvider.models.length : 0,
} : null,
providerCount: providers.length,
providers: providers.map((provider) => ({
id: provider.id,
name: provider.name,
type: provider.type,
active: provider.id === activeProviderId,
model: provider.model || '',
})),
safetyLevel: normalizedSafetyLevel,
safetyLabel: SAFETY_LEVEL_LABELS[normalizedSafetyLevel] || normalizedSafetyLevel,
contextLevel: normalizedContextLevel,
contextLabel: CONTEXT_LEVEL_LABELS[normalizedContextLevel] || normalizedContextLevel,
dynamicModelCount: dynamicModelPreview.total,
dynamicModels: dynamicModelPreview.items,
dynamicModelsTruncated: dynamicModelPreview.truncated,
enabledSkillCount: enabledSkills.length,
enabledSkills: enabledSkills.map((skill) => ({
id: skill.id,
name: skill.name,
scopes: Array.isArray(skill.scopes) ? skill.scopes : [],
requiredTools: Array.isArray(skill.requiredTools) ? skill.requiredTools : [],
})),
builtinToolCount: builtinPreview.total,
builtinTools: builtinPreview.items,
builtinToolsTruncated: builtinPreview.truncated,
mcpToolCount: mcpPreview.total,
mcpTools: mcpPreview.items,
mcpToolsTruncated: mcpPreview.truncated,
totalAvailableToolCount: builtinPreview.total + mcpPreview.total,
capabilities: {
canWriteData: normalizedSafetyLevel !== 'readonly',
canUseSampleContext: normalizedContextLevel === 'with_samples' || normalizedContextLevel === 'with_results',
canUseResultContext: normalizedContextLevel === 'with_results',
hasExternalMCPTools: mcpPreview.total > 0,
hasCustomSkills: enabledSkills.length > 0,
hasDynamicModelsLoaded: dynamicModelPreview.total > 0,
},
message: activeProvider
? `当前 AI 正在使用 ${activeProvider.name || activeProvider.id},共暴露 ${builtinPreview.total + mcpPreview.total} 个工具`
: '当前未启用 AI 供应商',
};
};

View File

@@ -68,13 +68,14 @@ describe('buildAISystemContextMessages', () => {
connections: [connections[0]],
tabs: [],
activeTabId: null,
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'],
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'],
skills,
userPromptSettings,
});
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_ai_context 读取当前挂载的表结构上下文');
expect(joined).toContain('inspect_current_connection');
expect(joined).toContain('inspect_saved_queries');

View File

@@ -95,6 +95,19 @@ const appendSkillPromptGroup = (
});
};
const appendAIRuntimeInspectionGuidance = (
messages: AISystemContextMessage[],
availableToolNames: string[],
) => {
if (!availableToolNames.includes('inspect_ai_runtime')) {
return;
}
messages.push({
role: 'system',
content: '如果用户提到“你现在用的哪个模型”“当前安全级别”“你现在能调用什么工具”“当前启用了哪些 skills / MCP 工具”,优先调用 inspect_ai_runtime 读取当前 AI 运行状态,不要凭记忆或假设回答。',
});
};
const resolveDatabaseDisplayType = (config: ConnectionConfig | undefined): string => {
const dbType = config?.type || 'unknown';
return dbType === 'diros' ? 'Doris' : dbType.charAt(0).toUpperCase() + dbType.slice(1);
@@ -205,6 +218,7 @@ export function buildAISystemContextMessages({
6. expectedSignals 必须是字符串数组,描述执行后需要重点观察的信号。
7. 如果命令权限不允许某类操作,就不要输出该类命令;无法满足时直接说明限制。`,
});
appendAIRuntimeInspectionGuidance(systemMessages, availableToolNames);
appendCustomPromptGroup(systemMessages, ['jvmDiagnostic'], userPromptSettings);
appendSkillPromptGroup(systemMessages, ['jvmDiagnostic'], skills, availableToolNames);
return systemMessages;
@@ -238,6 +252,7 @@ ${resourcePath ? `当前资源路径:${resourcePath}` : '当前未选中具体
5. payload 只能使用 {"format":"json","value":{...}} 或 {"format":"text","value":"..."} 这两种包装形式,不要输出脚本、命令或裸值。
6. 不要输出脚本、命令或“已经执行成功”之类的表述。`,
});
appendAIRuntimeInspectionGuidance(systemMessages, availableToolNames);
appendCustomPromptGroup(systemMessages, ['jvm'], userPromptSettings);
appendSkillPromptGroup(systemMessages, ['jvm'], skills, availableToolNames);
return systemMessages;
@@ -309,6 +324,7 @@ SELECT * FROM users WHERE status = 1;
content: '如果用户提到“当前 AI 上下文”“当前关联了哪些表”“现在带了哪些表结构”,优先调用 inspect_ai_context 读取当前挂载的表结构上下文,不要凭记忆复述。',
});
}
appendAIRuntimeInspectionGuidance(systemMessages, availableToolNames);
if (availableToolNames.includes('inspect_current_connection')) {
systemMessages.push({
role: 'system',

View File

@@ -23,6 +23,7 @@ interface AIToolCallingBlockProps {
}
const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_ai_runtime: '读取当前 AI 运行状态',
get_connections: '获取可用连接信息',
get_databases: '扫描数据库列表',
get_tables: '分析表结构信息',

View File

@@ -3,6 +3,13 @@ import { describe, expect, it } from 'vitest';
import { BUILTIN_AI_TOOL_INFO, buildAvailableAIChatTools } from './aiToolRegistry';
describe('aiToolRegistry', () => {
it('registers the ai-runtime inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_runtime');
expect(info).toBeTruthy();
expect(info?.desc).toContain('AI 自身运行状态');
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();
@@ -36,6 +43,7 @@ describe('aiToolRegistry', () => {
},
}]);
expect(tools.some((item) => item.function.name === 'inspect_ai_runtime')).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);

View File

@@ -309,6 +309,23 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
},
},
},
{
name: "inspect_ai_runtime",
icon: "🎛️",
desc: "查看当前 AI 自身运行状态",
detail:
"返回当前启用的模型供应商、模型名、安全级别、上下文级别、启用的 Skills以及当前已暴露的内置工具和 MCP 工具。适合用户问“你现在能调用什么”“当前用的哪个模型”“为什么不能执行写操作”时,先读真实运行状态再回答。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_runtime",
description:
"读取当前 AI 运行时快照,包括当前供应商、模型、安全级别、上下文级别、启用的 Skills、当前可用的内置工具与 MCP 工具。适用于用户询问当前 AI 能力边界、当前使用哪个模型、为什么不能执行某些操作时,先读取真实运行状态,避免模型猜测。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_context",
icon: "🧷",