diff --git a/frontend/src/components/AIChatPanel.tsx b/frontend/src/components/AIChatPanel.tsx index 48bdc4a..b5dc992 100644 --- a/frontend/src/components/AIChatPanel.tsx +++ b/frontend/src/components/AIChatPanel.tsx @@ -878,6 +878,8 @@ export const AIChatPanel: React.FC = ({ 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 = ({ 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(); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index 7fb042a..b9aca11 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -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('查看当前连接'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 1ae4429..afcac52 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -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', diff --git a/frontend/src/components/ai/aiLocalToolExecutor.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.test.ts index 2ea80f3..26edb86 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.test.ts @@ -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', {}), diff --git a/frontend/src/components/ai/aiLocalToolExecutor.ts b/frontend/src/components/ai/aiLocalToolExecutor.ts index a7d9fa1..fa72540 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.ts @@ -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; getTables: (config: any, dbName: string) => Promise; @@ -45,6 +58,7 @@ interface AILocalToolRuntime { query: (config: any, dbName: string, sql: string) => Promise; checkSQL?: (sql: string) => Promise<{ allowed?: boolean; operationType?: string } | undefined>; callMCPTool?: (name: string, args: string) => Promise<{ content?: string; isError?: boolean } | undefined>; + getAIRuntimeState?: () => Promise; } export interface ExecuteLocalAIToolCallOptions { @@ -59,6 +73,8 @@ export interface ExecuteLocalAIToolCallOptions { sqlLogs?: SqlLog[]; savedQueries?: SavedQuery[]; sqlSnippets?: SqlSnippet[]; + skills?: AISkillConfig[]; + dynamicModels?: string[]; runtime?: Partial; } @@ -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 { 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({ diff --git a/frontend/src/components/ai/aiRuntimeInsights.test.ts b/frontend/src/components/ai/aiRuntimeInsights.test.ts new file mode 100644 index 0000000..1518f0b --- /dev/null +++ b/frontend/src/components/ai/aiRuntimeInsights.test.ts @@ -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 供应商', + }); + }); +}); diff --git a/frontend/src/components/ai/aiRuntimeInsights.ts b/frontend/src/components/ai/aiRuntimeInsights.ts new file mode 100644 index 0000000..6be7f1a --- /dev/null +++ b/frontend/src/components/ai/aiRuntimeInsights.ts @@ -0,0 +1,140 @@ +import type { + AIContextLevel, + AIMCPToolDescriptor, + AIProviderConfig, + AISafetyLevel, + AISkillConfig, +} from '../../types'; + +const SAFETY_LEVEL_LABELS: Record = { + readonly: '只读', + readwrite: '读写', + full: '完全开放', +}; + +const CONTEXT_LEVEL_LABELS: Record = { + 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 = (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 供应商', + }; +}; diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index 33e7938..d7ebbac 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -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'); diff --git a/frontend/src/components/ai/aiSystemContextMessages.ts b/frontend/src/components/ai/aiSystemContextMessages.ts index cd44e36..6a2e583 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.ts @@ -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', diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index 8f31175..8354fee 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -23,6 +23,7 @@ interface AIToolCallingBlockProps { } const TOOL_ACTION_LABELS: Record = { + inspect_ai_runtime: '读取当前 AI 运行状态', get_connections: '获取可用连接信息', get_databases: '扫描数据库列表', get_tables: '分析表结构信息', diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index e9bcdf6..a3b8078 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -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); diff --git a/frontend/src/utils/aiToolRegistry.ts b/frontend/src/utils/aiToolRegistry.ts index edd8222..7ba8866 100644 --- a/frontend/src/utils/aiToolRegistry.ts +++ b/frontend/src/utils/aiToolRegistry.ts @@ -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: "🧷",