feat(ai): 优化内置工具目录检索与参数提示

- 为内置工具目录增加关键词搜索和结果计数

- 参数提示补充类型、默认值、枚举和示例信息

- 补充目录渲染和参数摘要提取测试
This commit is contained in:
Syngnat
2026-06-11 22:29:37 +08:00
parent 6f4e80c749
commit cba8ff394c
4 changed files with 304 additions and 34 deletions

View File

@@ -5,16 +5,18 @@ import { describe, expect, it } from 'vitest';
import AIBuiltinToolsCatalog from './AIBuiltinToolsCatalog';
import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
const renderCatalog = () => (
<AIBuiltinToolsCatalog
darkMode={false}
overlayTheme={buildOverlayWorkbenchTheme(false)}
cardBg="#fff"
cardBorder="rgba(0,0,0,0.08)"
/>
);
describe('AIBuiltinToolsCatalog', () => {
it('renders the workspace flows, snapshot tools, and local saved-sql discovery tools', () => {
const markup = renderToStaticMarkup(
<AIBuiltinToolsCatalog
darkMode={false}
overlayTheme={buildOverlayWorkbenchTheme(false)}
cardBg="#fff"
cardBorder="rgba(0,0,0,0.08)"
/>,
);
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('正文预览最多返回多少字符');
});

View File

@@ -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<AIBuiltinToolsCatalogProps> = ({
overlayTheme,
cardBg,
cardBorder,
}) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ fontSize: 13, color: overlayTheme.mutedText, marginBottom: 4 }}>
AI
</div>
<div style={{ display: 'grid', gap: 8 }}>
{BUILTIN_TOOL_FLOWS.map((flow) => (
<div
key={flow.title}
}) => {
const [searchText, setSearchText] = useState('');
const visibleFlows = filterBuiltinToolFlows(BUILTIN_TOOL_FLOWS, searchText);
const visibleTools = filterBuiltinTools(BUILTIN_AI_TOOL_INFO, searchText);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ fontSize: 13, color: overlayTheme.mutedText, marginBottom: 4 }}>
AI
</div>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 10px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.78)',
}}
>
<SearchOutlined style={{ color: overlayTheme.mutedText }} />
<input
aria-label="搜索内置工具"
value={searchText}
onChange={(event) => setSearchText(event.target.value)}
placeholder="搜索工具、流程或参数,例如 mcp / lineLimit / allowMutating / 事务"
style={{
fontSize: 12,
color: overlayTheme.mutedText,
padding: '10px 12px',
borderRadius: 10,
flex: 1,
border: 'none',
outline: 'none',
background: 'transparent',
color: overlayTheme.titleText,
fontSize: 13,
}}
/>
{searchText && (
<button
type="button"
onClick={() => setSearchText('')}
style={{
border: 'none',
background: 'transparent',
color: overlayTheme.mutedText,
cursor: 'pointer',
fontSize: 12,
}}
>
</button>
)}
</label>
<div style={{ fontSize: 12, color: overlayTheme.mutedText }}>
{visibleFlows.length}/{BUILTIN_TOOL_FLOWS.length} {visibleTools.length}/{BUILTIN_AI_TOOL_INFO.length}
</div>
{visibleFlows.length > 0 && (
<div style={{ display: 'grid', gap: 8 }}>
{visibleFlows.map((flow) => (
<div
key={flow.title}
style={{
fontSize: 12,
color: overlayTheme.mutedText,
padding: '10px 12px',
borderRadius: 10,
background: cardBg,
border: `1px solid ${cardBorder}`,
}}
>
<div style={{ fontWeight: 700, color: overlayTheme.titleText }}>{flow.title}</div>
<div style={{ marginTop: 4, fontFamily: 'var(--gn-font-mono)' }}>{flow.steps}</div>
<div style={{ marginTop: 4, opacity: 0.8, lineHeight: 1.6 }}>{flow.description}</div>
</div>
))}
</div>
)}
{visibleTools.length === 0 && (
<div
style={{
padding: '18px 16px',
borderRadius: 14,
border: `1px dashed ${cardBorder}`,
background: cardBg,
border: `1px solid ${cardBorder}`,
color: overlayTheme.mutedText,
fontSize: 13,
lineHeight: 1.7,
}}
>
<div style={{ fontWeight: 700, color: overlayTheme.titleText }}>{flow.title}</div>
<div style={{ marginTop: 4, fontFamily: 'var(--gn-font-mono)' }}>{flow.steps}</div>
<div style={{ marginTop: 4, opacity: 0.8, lineHeight: 1.6 }}>{flow.description}</div>
mcpschema
</div>
))}
</div>
{BUILTIN_AI_TOOL_INFO.map((tool) => {
)}
{visibleTools.map((tool) => {
const parameterDetails = describeBuiltinToolParameters(tool);
return (
<div
@@ -104,6 +173,9 @@ export const AIBuiltinToolsCatalog: React.FC<AIBuiltinToolsCatalogProps> = ({
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<code style={{ fontFamily: 'var(--gn-font-mono)', fontSize: 12 }}>{item.name}</code>
<span style={{ fontSize: 11, color: overlayTheme.mutedText }}>
{item.typeLabel}
</span>
<span
style={{
padding: '1px 8px',
@@ -123,10 +195,20 @@ export const AIBuiltinToolsCatalog: React.FC<AIBuiltinToolsCatalogProps> = ({
{item.enumValues.join(' / ')}
</span>
)}
{item.defaultValue && (
<span style={{ fontSize: 11, color: overlayTheme.mutedText }}>
{item.defaultValue}
</span>
)}
</div>
{item.description && (
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>{item.description}</div>
)}
{item.exampleValue && (
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
<code style={{ fontFamily: 'var(--gn-font-mono)' }}>{item.exampleValue}</code>
</div>
)}
</div>
))}
</div>
@@ -134,8 +216,9 @@ export const AIBuiltinToolsCatalog: React.FC<AIBuiltinToolsCatalogProps> = ({
)}
</div>
);
})}
</div>
);
})}
</div>
);
};
export default AIBuiltinToolsCatalog;

View File

@@ -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 接入状态');
});
});

View File

@@ -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, any>): 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<string, any>, 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<string, any> : {};
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(' '),
]),
]);
});
};