diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index a40fd06..c608475 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -5,16 +5,18 @@ import { describe, expect, it } from 'vitest'; import AIBuiltinToolsCatalog from './AIBuiltinToolsCatalog'; import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; +const renderCatalog = () => ( + +); + describe('AIBuiltinToolsCatalog', () => { it('renders the workspace flows, snapshot tools, and local saved-sql discovery tools', () => { - const markup = renderToStaticMarkup( - , - ); + const markup = renderToStaticMarkup(renderCatalog()); expect(markup).toContain('字段反查表'); expect(markup).toContain('get_all_columns'); @@ -111,6 +113,11 @@ describe('AIBuiltinToolsCatalog', () => { expect(markup).toContain('理解样例数据'); expect(markup).toContain('preview_table_rows'); expect(markup).toContain('参数提示'); + expect(markup).toContain('搜索工具、流程或参数'); + expect(markup).toContain('当前显示'); + expect(markup).toContain('类型:string'); + expect(markup).toContain('默认:160'); + expect(markup).toContain('示例:'); expect(markup).toContain('filePath'); expect(markup).toContain('正文预览最多返回多少字符'); }); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index dbec53f..4e8456e 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -1,9 +1,11 @@ -import React from 'react'; -import { ToolOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; +import { SearchOutlined, ToolOutlined } from '@ant-design/icons'; import { BUILTIN_TOOL_FLOWS, describeBuiltinToolParameters, + filterBuiltinToolFlows, + filterBuiltinTools, } from '../../utils/aiBuiltinToolCatalog'; import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; @@ -20,31 +22,98 @@ export const AIBuiltinToolsCatalog: React.FC = ({ overlayTheme, cardBg, cardBorder, -}) => ( -
-
- AI 助手在处理数据库相关问题时,可以自动调用以下内置工具获取真实数据,全程无需人工干预。 -
-
- {BUILTIN_TOOL_FLOWS.map((flow) => ( -
{ + const [searchText, setSearchText] = useState(''); + const visibleFlows = filterBuiltinToolFlows(BUILTIN_TOOL_FLOWS, searchText); + const visibleTools = filterBuiltinTools(BUILTIN_AI_TOOL_INFO, searchText); + + return ( +
+
+ AI 助手在处理数据库相关问题时,可以自动调用以下内置工具获取真实数据,全程无需人工干预。 +
+ +
+ 当前显示 {visibleFlows.length}/{BUILTIN_TOOL_FLOWS.length} 条推荐流程,{visibleTools.length}/{BUILTIN_AI_TOOL_INFO.length} 个内置工具。 +
+ {visibleFlows.length > 0 && ( +
+ {visibleFlows.map((flow) => ( +
+
{flow.title}
+
{flow.steps}
+
{flow.description}
+
+ ))} +
+ )} + {visibleTools.length === 0 && ( +
-
{flow.title}
-
{flow.steps}
-
{flow.description}
+ 没有匹配的内置工具。可以改搜更宽泛的关键词,例如 mcp、日志、连接、事务、快捷键、schema。
- ))} -
- {BUILTIN_AI_TOOL_INFO.map((tool) => { + )} + {visibleTools.map((tool) => { const parameterDetails = describeBuiltinToolParameters(tool); return (
= ({ >
{item.name} + + 类型:{item.typeLabel} + = ({ 可选值:{item.enumValues.join(' / ')} )} + {item.defaultValue && ( + + 默认:{item.defaultValue} + + )}
{item.description && (
{item.description}
)} + {item.exampleValue && ( +
+ 示例:{item.exampleValue} +
+ )}
))}
@@ -134,8 +216,9 @@ export const AIBuiltinToolsCatalog: React.FC = ({ )}
); - })} -
-); + })} + + ); +}; export default AIBuiltinToolsCatalog; diff --git a/frontend/src/utils/aiBuiltinToolCatalog.test.ts b/frontend/src/utils/aiBuiltinToolCatalog.test.ts new file mode 100644 index 0000000..13caafb --- /dev/null +++ b/frontend/src/utils/aiBuiltinToolCatalog.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest'; + +import { + BUILTIN_TOOL_FLOWS, + describeBuiltinToolParameters, + filterBuiltinToolFlows, + filterBuiltinTools, +} from './aiBuiltinToolCatalog'; +import type { AIBuiltinToolInfo } from './aiBuiltinToolInfo.types'; +import { BUILTIN_AI_TOOL_INFO } from './aiToolRegistry'; + +describe('describeBuiltinToolParameters', () => { + it('extracts type, required, enum, default, and example hints from builtin tool schemas', () => { + const tool: AIBuiltinToolInfo = { + name: 'inspect_demo', + icon: '🧪', + desc: '测试工具', + detail: '用于测试参数提示提取。', + params: 'lineLimit?, mode?, serverName?', + tool: { + type: 'function', + function: { + name: 'inspect_demo', + description: '测试工具', + parameters: { + type: 'object', + required: ['mode'], + properties: { + lineLimit: { type: 'number', description: '可选,最多读取多少行,默认 160,最大 200' }, + mode: { type: 'string', enum: ['fast', 'safe'], default: 'safe', description: '运行模式' }, + serverName: { type: 'string', description: '可选,例如 GitHub、Browser、DockerFetch' }, + includeDisabled: { type: ['boolean', 'null'], description: '是否包含禁用项,默认 false' }, + }, + }, + }, + }, + }; + + expect(describeBuiltinToolParameters(tool)).toEqual([ + { + name: 'lineLimit', + required: false, + typeLabel: 'number', + description: '可选,最多读取多少行,默认 160,最大 200', + enumValues: [], + defaultValue: '160', + exampleValue: '', + }, + { + name: 'mode', + required: true, + typeLabel: 'string', + description: '运行模式', + enumValues: ['fast', 'safe'], + defaultValue: 'safe', + exampleValue: '', + }, + { + name: 'serverName', + required: false, + typeLabel: 'string', + description: '可选,例如 GitHub、Browser、DockerFetch', + enumValues: [], + defaultValue: '', + exampleValue: 'GitHub、Browser、DockerFetch', + }, + { + name: 'includeDisabled', + required: false, + typeLabel: 'boolean | null', + description: '是否包含禁用项,默认 false', + enumValues: [], + defaultValue: 'false', + exampleValue: '', + }, + ]); + }); + + it('filters flows and tools by parameter names and descriptions', () => { + const allowMutatingTools = filterBuiltinTools(BUILTIN_AI_TOOL_INFO, 'allowMutating') + .map((tool) => tool.name); + expect(allowMutatingTools).toContain('inspect_ai_safety'); + expect(allowMutatingTools).not.toContain('inspect_mcp_runtime_failures'); + + const executeSqlTools = filterBuiltinTools(BUILTIN_AI_TOOL_INFO, '要执行的 SQL 语句') + .map((tool) => tool.name); + expect(executeSqlTools).toContain('execute_sql'); + + const mcpFlows = filterBuiltinToolFlows(BUILTIN_TOOL_FLOWS, '运行期失败日志') + .map((flow) => flow.title); + expect(mcpFlows).toContain('排查 MCP 接入状态'); + }); +}); diff --git a/frontend/src/utils/aiBuiltinToolCatalog.ts b/frontend/src/utils/aiBuiltinToolCatalog.ts index 5dd6c5b..923a61e 100644 --- a/frontend/src/utils/aiBuiltinToolCatalog.ts +++ b/frontend/src/utils/aiBuiltinToolCatalog.ts @@ -9,8 +9,11 @@ export interface AIBuiltinToolFlow { export interface AIBuiltinToolParameterHint { name: string; required: boolean; + typeLabel: string; description: string; enumValues: string[]; + defaultValue: string; + exampleValue: string; } export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [ @@ -231,6 +234,44 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [ }, ]; +const stringifyHintValue = (value: unknown): string => { + if (value === undefined) return ''; + if (value === null) return 'null'; + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}; + +const readTypeLabel = (schema: Record): string => { + if (Array.isArray(schema.type)) { + return schema.type.map((item) => String(item)).filter(Boolean).join(' | ') || 'any'; + } + if (typeof schema.type === 'string' && schema.type.trim()) { + return schema.type.trim(); + } + if (Array.isArray(schema.enum) && schema.enum.length > 0) { + return 'enum'; + } + return 'any'; +}; + +const readDefaultValue = (schema: Record, description: string): string => { + if (Object.prototype.hasOwnProperty.call(schema, 'default')) { + return stringifyHintValue(schema.default); + } + const match = description.match(/默认\s*([^\s,,;;。))]+)/u); + return match?.[1]?.trim() || ''; +}; + +const readExampleValue = (description: string): string => { + const match = description.match(/(?:例如|示例值?[::])\s*([^。;;\n]+)/u); + return match?.[1]?.trim() || ''; +}; + export const describeBuiltinToolParameters = (tool: AIBuiltinToolInfo): AIBuiltinToolParameterHint[] => { const schema = tool.tool.function.parameters; const properties = schema && typeof schema === 'object' && typeof schema.properties === 'object' @@ -242,11 +283,57 @@ export const describeBuiltinToolParameters = (tool: AIBuiltinToolInfo): AIBuilti return Object.entries(properties).map(([name, config]) => { const normalized = config && typeof config === 'object' ? config as Record : {}; + const description = typeof normalized.description === 'string' ? normalized.description : ''; return { name, required: required.has(name), - description: typeof normalized.description === 'string' ? normalized.description : '', + typeLabel: readTypeLabel(normalized), + description, enumValues: Array.isArray(normalized.enum) ? normalized.enum.map((item) => String(item)) : [], + defaultValue: readDefaultValue(normalized, description), + exampleValue: readExampleValue(description), }; }); }; + +export const normalizeBuiltinToolCatalogSearch = (value: string): string => + value.trim().toLowerCase(); + +const matchesCatalogSearch = (keyword: string, values: unknown[]): boolean => + !keyword || values.some((value) => String(value || '').toLowerCase().includes(keyword)); + +export const filterBuiltinToolFlows = ( + flows: AIBuiltinToolFlow[], + searchText: string, +): AIBuiltinToolFlow[] => { + const keyword = normalizeBuiltinToolCatalogSearch(searchText); + return flows.filter((flow) => matchesCatalogSearch(keyword, [ + flow.title, + flow.steps, + flow.description, + ])); +}; + +export const filterBuiltinTools = ( + tools: AIBuiltinToolInfo[], + searchText: string, +): AIBuiltinToolInfo[] => { + const keyword = normalizeBuiltinToolCatalogSearch(searchText); + return tools.filter((tool) => { + const parameterDetails = describeBuiltinToolParameters(tool); + return matchesCatalogSearch(keyword, [ + tool.name, + tool.desc, + tool.detail, + tool.params, + ...parameterDetails.flatMap((item) => [ + item.name, + item.typeLabel, + item.description, + item.defaultValue, + item.exampleValue, + item.enumValues.join(' '), + ]), + ]); + }); +};