diff --git a/src/core/agent.ts b/src/core/agent.ts index a9cd0bf..18f6392 100644 --- a/src/core/agent.ts +++ b/src/core/agent.ts @@ -1,8 +1,16 @@ import { createAnthropic } from '@ai-sdk/anthropic'; import { createDeepSeek } from '@ai-sdk/deepseek'; -import { generateText, streamText, stepCountIs, type ModelMessage, type Tool as AITool, type LanguageModel } from 'ai'; +import { + generateText, + streamText, + stepCountIs, + type ModelMessage, + type Tool as AITool, + type LanguageModel, +} from 'ai'; import type { Tool, ToolResult, Message, AgentConfig, ProviderType } from '../types/index.js'; import { buildZodSchema } from '../types/index.js'; +import { ToolRegistry } from '../tools/registry.js'; // Provider 工厂函数类型 type ProviderFactory = (apiKey: string) => (model: string) => LanguageModel; @@ -22,9 +30,17 @@ const providers: Record = { export class Agent { private getModel: (model: string) => LanguageModel; private config: AgentConfig; - private tools: Map = new Map(); private conversationHistory: ModelMessage[] = []; + // 工具注册表 + private registry: ToolRegistry | null = null; + + // 已发现的工具(通过 tool_search 发现的) + private discoveredTools: Set = new Set(); + + // 兼容旧模式:直接注册的工具 + private legacyTools: Map = new Map(); + constructor(config: AgentConfig) { this.config = config; @@ -35,30 +51,67 @@ export class Agent { this.getModel = providerFactory(config.apiKey); } - // 注册单个工具 - registerTool(customTool: Tool): void { - this.tools.set(customTool.name, customTool); + /** + * 设置工具注册表(新模式:支持动态工具发现) + */ + setRegistry(registry: ToolRegistry): void { + this.registry = registry; } - // 批量注册工具 + /** + * 注册单个工具(兼容旧代码) + */ + registerTool(customTool: Tool): void { + this.legacyTools.set(customTool.name, customTool); + } + + /** + * 批量注册工具(兼容旧代码) + */ registerTools(tools: Tool[]): void { for (const tool of tools) { - this.tools.set(tool.name, tool); + this.legacyTools.set(tool.name, tool); } } - // 将自定义工具转换为 Vercel AI SDK 的工具格式 + /** + * 获取当前可用的工具 + * - 如果使用 registry 模式:返回核心工具 + 已发现的工具 + * - 如果使用旧模式:返回所有注册的工具 + */ + private getAvailableTools(): Tool[] { + if (this.registry) { + // 新模式:核心工具 + 已发现的工具 + const coreTools = this.registry.getCoreTools(); + const discoveredTools = this.registry.getTools([...this.discoveredTools]); + return [...coreTools, ...discoveredTools]; + } else { + // 旧模式:返回所有注册的工具 + return [...this.legacyTools.values()]; + } + } + + /** + * 将工具转换为 Vercel AI SDK 的工具格式 + */ private getVercelTools(): Record { const vercelTools: Record = {}; + const availableTools = this.getAvailableTools(); - for (const [name, customTool] of this.tools) { - const schema = buildZodSchema(customTool.parameters); + for (const tool of availableTools) { + const schema = buildZodSchema(tool.parameters); - vercelTools[name] = { - description: customTool.description, + vercelTools[tool.name] = { + description: tool.description, inputSchema: schema, execute: async (params) => { - const result = await customTool.execute(params as Record); + const result = await tool.execute(params as Record); + + // 如果是 tool_search 调用,解析结果并注入发现的工具 + if (tool.name === 'tool_search' && result.success) { + this.handleToolSearchResult(result.output); + } + return result; }, } as AITool; @@ -67,11 +120,25 @@ export class Agent { return vercelTools; } - // 发送消息并处理响应(流式) - async chat( - userMessage: string, - onStream?: (text: string) => void - ): Promise { + /** + * 处理 tool_search 的结果,将发现的工具添加到可用列表 + */ + private handleToolSearchResult(output: string): void { + // 解析输出,提取工具名称 + // 格式: "- tool_name: description [category]" + const matches = output.matchAll(/^- (\w+):/gm); + for (const match of matches) { + const toolName = match[1]; + if (this.registry?.has(toolName)) { + this.discoveredTools.add(toolName); + } + } + } + + /** + * 发送消息并处理响应(流式) + */ + async chat(userMessage: string, onStream?: (text: string) => void): Promise { // 添加用户消息到历史 this.conversationHistory.push({ role: 'user', @@ -97,7 +164,12 @@ export class Agent { const output = (chunk as { output?: ToolResult }).output; if (output && typeof output === 'object') { if (output.success) { - onStream(`[结果: ${output.output}]\n`); + // 截断过长的输出 + const displayOutput = + output.output.length > 500 + ? output.output.substring(0, 500) + '...(截断)' + : output.output; + onStream(`[结果: ${displayOutput}]\n`); } else { onStream(`[错误: ${output.error}]\n`); } @@ -137,20 +209,47 @@ export class Agent { return fullResponse; } - // 清空对话历史 + /** + * 清空对话历史和发现的工具 + */ clearHistory(): void { this.conversationHistory = []; + this.discoveredTools.clear(); } - // 获取对话历史 + /** + * 获取对话历史 + */ getHistory(): Message[] { return this.conversationHistory - .filter((msg): msg is ModelMessage & { role: 'user' | 'assistant' } => - msg.role === 'user' || msg.role === 'assistant' + .filter( + (msg): msg is ModelMessage & { role: 'user' | 'assistant' } => + msg.role === 'user' || msg.role === 'assistant' ) .map((msg) => ({ role: msg.role, content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), })); } + + /** + * 获取当前可用工具的数量 + */ + getToolCount(): { core: number; discovered: number; total: number } { + if (this.registry) { + const coreCount = this.registry.getCoreTools().length; + const discoveredCount = this.discoveredTools.size; + return { + core: coreCount, + discovered: discoveredCount, + total: coreCount + discoveredCount, + }; + } else { + return { + core: this.legacyTools.size, + discovered: 0, + total: this.legacyTools.size, + }; + } + } } diff --git a/src/index.ts b/src/index.ts index b63ac04..f1b2f3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { Command } from 'commander'; import { Agent } from './core/agent.js'; import { TerminalUI } from './ui/terminal.js'; import { loadConfig, initConfig } from './utils/config.js'; -import { allTools } from './tools/index.js'; +import { toolRegistry } from './tools/index.js'; import { getPermissionManager, promptPermission } from './permission/index.js'; const program = new Command(); @@ -37,8 +37,8 @@ program const config = loadConfig(); const agent = new Agent(config); - // 注册所有工具 - agent.registerTools(allTools); + // 设置工具注册表(支持动态工具发现) + agent.setRegistry(toolRegistry); try { await agent.chat(question, (text) => { @@ -60,8 +60,8 @@ program.action(async () => { const config = loadConfig(); const agent = new Agent(config); - // 注册所有工具 - agent.registerTools(allTools); + // 设置工具注册表(支持动态工具发现) + agent.setRegistry(toolRegistry); // 启动终端 UI const ui = new TerminalUI(agent); diff --git a/src/tools/filesystem/copy_file.ts b/src/tools/filesystem/copy_file.ts index db98072..430a6f6 100644 --- a/src/tools/filesystem/copy_file.ts +++ b/src/tools/filesystem/copy_file.ts @@ -1,6 +1,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; @@ -18,9 +19,16 @@ async function copyRecursive(source: string, dest: string): Promise { } } -export const copyFileTool: Tool = { +export const copyFileTool: ToolWithMetadata = { name: 'copy_file', description: loadDescription('copy_file'), + metadata: { + name: 'copy_file', + category: 'filesystem', + description: '复制文件或目录', + keywords: ['copy', 'file', 'cp', 'duplicate', '复制', '文件', '拷贝'], + deferLoading: true, + }, parameters: { source: { type: 'string', diff --git a/src/tools/filesystem/create_directory.ts b/src/tools/filesystem/create_directory.ts index 8cae9ce..43fd933 100644 --- a/src/tools/filesystem/create_directory.ts +++ b/src/tools/filesystem/create_directory.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const createDirectoryTool: Tool = { +export const createDirectoryTool: ToolWithMetadata = { name: 'create_directory', description: loadDescription('create_directory'), + metadata: { + name: 'create_directory', + category: 'filesystem', + description: '创建目录', + keywords: ['create', 'directory', 'mkdir', 'folder', 'new', '创建', '目录', '文件夹', '新建'], + deferLoading: true, + }, parameters: { path: { type: 'string', diff --git a/src/tools/filesystem/delete_file.ts b/src/tools/filesystem/delete_file.ts index d1dbc51..b66283d 100644 --- a/src/tools/filesystem/delete_file.ts +++ b/src/tools/filesystem/delete_file.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const deleteFileTool: Tool = { +export const deleteFileTool: ToolWithMetadata = { name: 'delete_file', description: loadDescription('delete_file'), + metadata: { + name: 'delete_file', + category: 'filesystem', + description: '删除文件或目录', + keywords: ['delete', 'remove', 'file', 'rm', '删除', '移除', '文件'], + deferLoading: true, + }, parameters: { path: { type: 'string', diff --git a/src/tools/filesystem/edit_file.ts b/src/tools/filesystem/edit_file.ts index ade3ca6..9a2d07a 100644 --- a/src/tools/filesystem/edit_file.ts +++ b/src/tools/filesystem/edit_file.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const editFileTool: Tool = { +export const editFileTool: ToolWithMetadata = { name: 'edit_file', description: loadDescription('edit_file'), + metadata: { + name: 'edit_file', + category: 'filesystem', + description: '编辑文件内容(查找替换)', + keywords: ['edit', 'file', 'replace', 'modify', 'change', 'update', '编辑', '文件', '替换', '修改', '更新'], + deferLoading: true, + }, parameters: { path: { type: 'string', diff --git a/src/tools/filesystem/get_file_info.ts b/src/tools/filesystem/get_file_info.ts index 9da62cb..6b97c7e 100644 --- a/src/tools/filesystem/get_file_info.ts +++ b/src/tools/filesystem/get_file_info.ts @@ -1,6 +1,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; @@ -45,9 +46,16 @@ function formatPermissions(mode: number): string { return `${fileType?.[1] || 'unknown'} (${perms})`; } -export const getFileInfoTool: Tool = { +export const getFileInfoTool: ToolWithMetadata = { name: 'get_file_info', description: loadDescription('get_file_info'), + metadata: { + name: 'get_file_info', + category: 'filesystem', + description: '获取文件元信息', + keywords: ['file', 'info', 'stat', 'size', 'permission', 'metadata', '文件', '信息', '大小', '权限', '属性'], + deferLoading: true, + }, parameters: { path: { type: 'string', diff --git a/src/tools/filesystem/grep_content.ts b/src/tools/filesystem/grep_content.ts index 73a338d..168522e 100644 --- a/src/tools/filesystem/grep_content.ts +++ b/src/tools/filesystem/grep_content.ts @@ -1,6 +1,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; @@ -10,9 +11,16 @@ interface GrepMatch { content: string; } -export const grepContentTool: Tool = { +export const grepContentTool: ToolWithMetadata = { name: 'grep_content', description: loadDescription('grep_content'), + metadata: { + name: 'grep_content', + category: 'filesystem', + description: '在文件内容中搜索文本', + keywords: ['grep', 'search', 'content', 'text', 'find', 'regex', '搜索', '内容', '文本', '查找', '正则'], + deferLoading: true, + }, parameters: { directory: { type: 'string', diff --git a/src/tools/filesystem/list_directory.ts b/src/tools/filesystem/list_directory.ts index f0d647b..13b147f 100644 --- a/src/tools/filesystem/list_directory.ts +++ b/src/tools/filesystem/list_directory.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const listDirTool: Tool = { +export const listDirTool: ToolWithMetadata = { name: 'list_directory', description: loadDescription('list_directory'), + metadata: { + name: 'list_directory', + category: 'filesystem', + description: '列出目录内容', + keywords: ['list', 'directory', 'ls', 'dir', 'folder', '列出', '目录', '文件夹', '查看'], + deferLoading: true, + }, parameters: { path: { type: 'string', diff --git a/src/tools/filesystem/move_file.ts b/src/tools/filesystem/move_file.ts index 66a2c58..eb6ea33 100644 --- a/src/tools/filesystem/move_file.ts +++ b/src/tools/filesystem/move_file.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const moveFileTool: Tool = { +export const moveFileTool: ToolWithMetadata = { name: 'move_file', description: loadDescription('move_file'), + metadata: { + name: 'move_file', + category: 'filesystem', + description: '移动或重命名文件/目录', + keywords: ['move', 'rename', 'file', 'mv', '移动', '重命名', '文件'], + deferLoading: true, + }, parameters: { source: { type: 'string', diff --git a/src/tools/filesystem/read_file.ts b/src/tools/filesystem/read_file.ts index babf29c..106eae4 100644 --- a/src/tools/filesystem/read_file.ts +++ b/src/tools/filesystem/read_file.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const readFileTool: Tool = { +export const readFileTool: ToolWithMetadata = { name: 'read_file', description: loadDescription('read_file'), + metadata: { + name: 'read_file', + category: 'filesystem', + description: '读取文件内容', + keywords: ['read', 'file', 'content', 'cat', 'view', 'open', '读取', '文件', '内容', '查看', '打开'], + deferLoading: true, + }, parameters: { path: { type: 'string', diff --git a/src/tools/filesystem/search_files.ts b/src/tools/filesystem/search_files.ts index 2ba3be8..5e21215 100644 --- a/src/tools/filesystem/search_files.ts +++ b/src/tools/filesystem/search_files.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const searchFilesTool: Tool = { +export const searchFilesTool: ToolWithMetadata = { name: 'search_files', description: loadDescription('search_files'), + metadata: { + name: 'search_files', + category: 'filesystem', + description: '按文件名搜索文件', + keywords: ['search', 'file', 'find', 'glob', 'pattern', '搜索', '文件', '查找', '匹配'], + deferLoading: true, + }, parameters: { directory: { type: 'string', diff --git a/src/tools/filesystem/write_file.ts b/src/tools/filesystem/write_file.ts index 3b41266..78e3177 100644 --- a/src/tools/filesystem/write_file.ts +++ b/src/tools/filesystem/write_file.ts @@ -1,12 +1,20 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; -export const writeFileTool: Tool = { +export const writeFileTool: ToolWithMetadata = { name: 'write_file', description: loadDescription('write_file'), + metadata: { + name: 'write_file', + category: 'filesystem', + description: '写入文件内容', + keywords: ['write', 'file', 'save', 'create', '写入', '文件', '保存', '创建', '新建'], + deferLoading: true, + }, parameters: { path: { type: 'string', diff --git a/src/tools/index.ts b/src/tools/index.ts index d503391..52db516 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,8 +1,12 @@ -import type { Tool } from '../types/index.js'; +import type { ToolWithMetadata } from './types.js'; +import { toolRegistry } from './registry.js'; // Shell 工具 import { bashTool } from './shell/index.js'; +// 核心工具 +import { toolSearchTool } from './tool-search.js'; + // 文件系统工具 import { readFileTool, @@ -18,30 +22,33 @@ import { deleteFileTool, } from './filesystem/index.js'; -// 所有可用工具的注册中心 -// 添加新工具只需在此数组中添加一行 -export const allTools: Tool[] = [ - // Shell +// 所有工具列表(用于注册) +const allToolsWithMetadata: ToolWithMetadata[] = [ + // 核心工具 (deferLoading: false) + toolSearchTool, bashTool, - // 文件读写 + // 文件系统工具 (deferLoading: true) readFileTool, writeFileTool, editFileTool, - - // 目录操作 listDirTool, createDirectoryTool, - - // 搜索 searchFilesTool, grepContentTool, - - // 文件信息 getFileInfoTool, - - // 文件管理 moveFileTool, copyFileTool, deleteFileTool, ]; + +// 注册所有工具到 registry +toolRegistry.registerAll(allToolsWithMetadata); + +// 导出 +export { toolRegistry } from './registry.js'; +export { toolSearchTool } from './tool-search.js'; +export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js'; + +// 兼容旧代码:导出所有工具数组(基础 Tool 类型) +export const allTools = toolRegistry.getAllTools(); diff --git a/src/tools/registry.ts b/src/tools/registry.ts new file mode 100644 index 0000000..ff8ff7b --- /dev/null +++ b/src/tools/registry.ts @@ -0,0 +1,120 @@ +import type { Tool } from '../types/index.js'; +import type { ToolMetadata, ToolWithMetadata, ToolSearchResult } from './types.js'; +import { searchTools } from './search.js'; + +/** + * 工具注册中心 + * 管理所有工具的注册、查询和搜索 + */ +class ToolRegistry { + private tools: Map = new Map(); + + /** + * 注册单个工具 + */ + register(tool: ToolWithMetadata): void { + this.tools.set(tool.name, tool); + } + + /** + * 批量注册工具 + */ + registerAll(tools: ToolWithMetadata[]): void { + for (const tool of tools) { + this.register(tool); + } + } + + /** + * 获取核心工具 (deferLoading: false) + * 这些工具在会话开始时就可用 + */ + getCoreTools(): Tool[] { + const coreTools: Tool[] = []; + for (const tool of this.tools.values()) { + if (!tool.metadata.deferLoading) { + coreTools.push(this.toBasicTool(tool)); + } + } + return coreTools; + } + + /** + * 获取指定工具 + */ + getTool(name: string): Tool | undefined { + const tool = this.tools.get(name); + return tool ? this.toBasicTool(tool) : undefined; + } + + /** + * 获取多个工具 + */ + getTools(names: string[]): Tool[] { + const result: Tool[] = []; + for (const name of names) { + const tool = this.getTool(name); + if (tool) { + result.push(tool); + } + } + return result; + } + + /** + * 搜索工具 + * @param query 搜索查询 + * @param limit 返回结果数量限制 + * @returns 匹配的工具元数据列表 + */ + search(query: string, limit: number = 5): ToolSearchResult[] { + const allMetadata = this.getAllMetadata(); + return searchTools(query, allMetadata, limit); + } + + /** + * 获取所有工具的元数据 + */ + getAllMetadata(): ToolMetadata[] { + return [...this.tools.values()].map((tool) => tool.metadata); + } + + /** + * 获取所有工具 (用于兼容旧代码) + */ + getAllTools(): Tool[] { + return [...this.tools.values()].map((tool) => this.toBasicTool(tool)); + } + + /** + * 检查工具是否存在 + */ + has(name: string): boolean { + return this.tools.has(name); + } + + /** + * 获取工具数量 + */ + get size(): number { + return this.tools.size; + } + + /** + * 将 ToolWithMetadata 转换为基础 Tool 类型 + */ + private toBasicTool(tool: ToolWithMetadata): Tool { + return { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + execute: tool.execute, + }; + } +} + +// 导出单例 +export const toolRegistry = new ToolRegistry(); + +// 也导出类,方便测试 +export { ToolRegistry }; diff --git a/src/tools/search.ts b/src/tools/search.ts new file mode 100644 index 0000000..ad3f26b --- /dev/null +++ b/src/tools/search.ts @@ -0,0 +1,83 @@ +import type { ToolMetadata, ToolSearchResult } from './types.js'; + +/** + * 分词函数,支持中英文 + */ +function tokenize(text: string): string[] { + return text + .toLowerCase() + .split(/[\s,,、_\-]+/) + .filter((t) => t.length > 0); +} + +/** + * 计算工具与查询的匹配分数 + */ +function calculateScore(queryTerms: string[], tool: ToolMetadata): number { + let score = 0; + + const nameLower = tool.name.toLowerCase(); + const descLower = tool.description.toLowerCase(); + const keywordsLower = tool.keywords.map((k) => k.toLowerCase()); + + for (const term of queryTerms) { + // 名称精确匹配 (最高分) + if (nameLower === term) { + score += 10; + } + // 名称包含匹配 + else if (nameLower.includes(term)) { + score += 5; + } + + // 关键词精确匹配 + if (keywordsLower.includes(term)) { + score += 8; + } + // 关键词包含匹配 + else if (keywordsLower.some((k) => k.includes(term) || term.includes(k))) { + score += 3; + } + + // 描述包含匹配 + if (descLower.includes(term)) { + score += 2; + } + } + + return score; +} + +/** + * 搜索工具 + * @param query 搜索查询 + * @param allTools 所有工具的元数据 + * @param limit 返回结果数量限制 + * @returns 匹配的工具列表(按分数排序) + */ +export function searchTools( + query: string, + allTools: ToolMetadata[], + limit: number = 5 +): ToolSearchResult[] { + const queryTerms = tokenize(query); + + if (queryTerms.length === 0) { + return []; + } + + const results = allTools + // 只搜索延迟加载的工具 + .filter((tool) => tool.deferLoading) + .map((tool) => ({ + name: tool.name, + description: tool.description, + category: tool.category, + score: calculateScore(queryTerms, tool), + })) + .filter((r) => r.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit); + + return results; +} diff --git a/src/tools/shell/bash.ts b/src/tools/shell/bash.ts index 4db67cf..7908f37 100644 --- a/src/tools/shell/bash.ts +++ b/src/tools/shell/bash.ts @@ -1,14 +1,22 @@ import { exec } from 'child_process'; import { promisify } from 'util'; -import type { Tool, ToolResult } from '../../types/index.js'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; import { loadDescription } from '../load_description.js'; import { getPermissionManager } from '../../permission/index.js'; const execAsync = promisify(exec); -export const bashTool: Tool = { +export const bashTool: ToolWithMetadata = { name: 'bash', description: loadDescription('bash'), + metadata: { + name: 'bash', + category: 'shell', + description: '执行 shell 命令', + keywords: ['bash', 'shell', 'command', 'execute', 'run', 'terminal', '命令', '执行', '终端', 'sh', 'cmd'], + deferLoading: false, // 核心工具,始终加载 + }, parameters: { command: { type: 'string', diff --git a/src/tools/tool-search.ts b/src/tools/tool-search.ts new file mode 100644 index 0000000..202f75c --- /dev/null +++ b/src/tools/tool-search.ts @@ -0,0 +1,68 @@ +import type { ToolResult } from '../types/index.js'; +import type { ToolWithMetadata } from './types.js'; +import { toolRegistry } from './registry.js'; + +/** + * tool_search 工具 + * 用于搜索可用的工具,实现动态工具发现 + */ +export const toolSearchTool: ToolWithMetadata = { + name: 'tool_search', + description: `搜索可用的工具。当你需要执行某项任务但当前没有合适的工具时,使用此工具搜索。 + +可搜索的能力类别: +- 文件操作: 读取、写入、编辑、复制、移动、删除文件 +- 目录操作: 列出目录、创建目录、搜索文件 +- 内容搜索: 在文件中搜索文本、grep +- Shell: 执行命令行命令 +- Git: 版本控制操作 (即将支持) +- 网络: HTTP请求、网页抓取 (即将支持) + +搜索后返回的工具将可以直接使用。`, + + parameters: { + query: { + type: 'string', + description: '描述你需要的功能,如 "读取文件内容"、"搜索代码"、"移动文件"、"执行命令"', + required: true, + }, + }, + + metadata: { + name: 'tool_search', + category: 'core', + description: '搜索可用工具', + keywords: ['search', 'find', 'tool', 'discover', '搜索', '查找', '工具', '发现'], + deferLoading: false, // 核心工具,始终加载 + }, + + execute: async (params: Record): Promise => { + const query = params.query as string; + + if (!query || query.trim().length === 0) { + return { + success: false, + output: '', + error: '请提供搜索关键词', + }; + } + + const results = toolRegistry.search(query, 5); + + if (results.length === 0) { + return { + success: true, + output: `没有找到与 "${query}" 匹配的工具。请尝试其他关键词,或使用更通用的描述。`, + }; + } + + const toolList = results + .map((t) => `- ${t.name}: ${t.description} [${t.category}]`) + .join('\n'); + + return { + success: true, + output: `找到 ${results.length} 个相关工具:\n\n${toolList}\n\n这些工具现在可以使用了。请选择合适的工具来完成任务。`, + }; + }, +}; diff --git a/src/tools/types.ts b/src/tools/types.ts new file mode 100644 index 0000000..251efe7 --- /dev/null +++ b/src/tools/types.ts @@ -0,0 +1,30 @@ +import type { ToolParameter, ToolResult } from '../types/index.js'; + +// 工具类别 +export type ToolCategory = 'core' | 'filesystem' | 'shell' | 'git' | 'web' | 'database'; + +// 工具元数据 +export interface ToolMetadata { + name: string; + category: ToolCategory; + description: string; // 简短描述,用于搜索结果展示 + keywords: string[]; // 搜索关键词 + deferLoading: boolean; // true = 延迟加载,false = 始终加载 +} + +// 扩展后的工具定义(包含元数据) +export interface ToolWithMetadata { + name: string; + description: string; + parameters: Record; + execute: (params: Record) => Promise; + metadata: ToolMetadata; +} + +// 搜索结果 +export interface ToolSearchResult { + name: string; + description: string; + category: ToolCategory; + score: number; +}