mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
✨ feat(ai): 优化内置工具目录检索与参数提示
- 为内置工具目录增加关键词搜索和结果计数 - 参数提示补充类型、默认值、枚举和示例信息 - 补充目录渲染和参数摘要提取测试
This commit is contained in:
@@ -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('正文预览最多返回多少字符');
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
没有匹配的内置工具。可以改搜更宽泛的关键词,例如 mcp、日志、连接、事务、快捷键、schema。
|
||||
</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;
|
||||
|
||||
93
frontend/src/utils/aiBuiltinToolCatalog.test.ts
Normal file
93
frontend/src/utils/aiBuiltinToolCatalog.test.ts
Normal 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 接入状态');
|
||||
});
|
||||
});
|
||||
@@ -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(' '),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user