From af1185c4d78873d3482c9ce402b66dffae55ac70 Mon Sep 17 00:00:00 2001 From: kurihada Date: Wed, 10 Dec 2025 17:11:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/core/agent.ts | 9 +- src/index.ts | 24 +-- src/tools/bash.ts | 3 +- src/tools/descriptions/bash.txt | 1 + src/tools/descriptions/edit_file.txt | 1 + src/tools/descriptions/list_directory.txt | 1 + src/tools/descriptions/read_file.txt | 1 + src/tools/descriptions/search_files.txt | 1 + src/tools/descriptions/write_file.txt | 1 + src/tools/edit_file.ts | 69 ++++++++ src/tools/file.ts | 189 ---------------------- src/tools/index.ts | 17 +- src/tools/list_directory.ts | 43 +++++ src/tools/load_description.ts | 15 ++ src/tools/read_file.ts | 36 +++++ src/tools/search_files.ts | 74 +++++++++ src/tools/write_file.ts | 43 +++++ 18 files changed, 316 insertions(+), 214 deletions(-) create mode 100644 src/tools/descriptions/bash.txt create mode 100644 src/tools/descriptions/edit_file.txt create mode 100644 src/tools/descriptions/list_directory.txt create mode 100644 src/tools/descriptions/read_file.txt create mode 100644 src/tools/descriptions/search_files.txt create mode 100644 src/tools/descriptions/write_file.txt create mode 100644 src/tools/edit_file.ts delete mode 100644 src/tools/file.ts create mode 100644 src/tools/list_directory.ts create mode 100644 src/tools/load_description.ts create mode 100644 src/tools/read_file.ts create mode 100644 src/tools/search_files.ts create mode 100644 src/tools/write_file.ts diff --git a/package.json b/package.json index 4b42ae7..4ec8b28 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "type": "module", "scripts": { - "build": "tsc", + "build": "tsc && cp -r src/tools/descriptions/*.txt dist/tools/descriptions/", "start": "node dist/index.js", "dev": "tsx src/index.ts", "lint": "eslint src/**/*.ts" diff --git a/src/core/agent.ts b/src/core/agent.ts index bb7dfa0..a9cd0bf 100644 --- a/src/core/agent.ts +++ b/src/core/agent.ts @@ -35,11 +35,18 @@ export class Agent { this.getModel = providerFactory(config.apiKey); } - // 注册工具 + // 注册单个工具 registerTool(customTool: Tool): void { this.tools.set(customTool.name, customTool); } + // 批量注册工具 + registerTools(tools: Tool[]): void { + for (const tool of tools) { + this.tools.set(tool.name, tool); + } + } + // 将自定义工具转换为 Vercel AI SDK 的工具格式 private getVercelTools(): Record { const vercelTools: Record = {}; diff --git a/src/index.ts b/src/index.ts index 105812d..94aa6c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,13 +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 { - bashTool, - readFileTool, - writeFileTool, - listDirTool, - searchFilesTool, -} from './tools/index.js'; +import { allTools } from './tools/index.js'; const program = new Command(); @@ -35,15 +29,11 @@ program const config = loadConfig(); const agent = new Agent(config); - // 注册工具 - agent.registerTool(bashTool); - agent.registerTool(readFileTool); - agent.registerTool(writeFileTool); - agent.registerTool(listDirTool); - agent.registerTool(searchFilesTool); + // 注册所有工具 + agent.registerTools(allTools); try { - const response = await agent.chat(question, (text) => { + await agent.chat(question, (text) => { process.stdout.write(text); }); console.log(''); @@ -62,11 +52,7 @@ program.action(async () => { const agent = new Agent(config); // 注册所有工具 - agent.registerTool(bashTool); - agent.registerTool(readFileTool); - agent.registerTool(writeFileTool); - agent.registerTool(listDirTool); - agent.registerTool(searchFilesTool); + agent.registerTools(allTools); // 启动终端 UI const ui = new TerminalUI(agent); diff --git a/src/tools/bash.ts b/src/tools/bash.ts index 929b75f..438f9f5 100644 --- a/src/tools/bash.ts +++ b/src/tools/bash.ts @@ -1,12 +1,13 @@ import { exec } from 'child_process'; import { promisify } from 'util'; import type { Tool, ToolResult } from '../types/index.js'; +import { loadDescription } from './load_description.js'; const execAsync = promisify(exec); export const bashTool: Tool = { name: 'bash', - description: '执行 bash 命令。可以用于运行系统命令、安装包、git 操作等。', + description: loadDescription('bash'), parameters: { command: { type: 'string', diff --git a/src/tools/descriptions/bash.txt b/src/tools/descriptions/bash.txt new file mode 100644 index 0000000..485acae --- /dev/null +++ b/src/tools/descriptions/bash.txt @@ -0,0 +1 @@ +执行 bash 命令。可以用于运行系统命令、安装包、git 操作等。 \ No newline at end of file diff --git a/src/tools/descriptions/edit_file.txt b/src/tools/descriptions/edit_file.txt new file mode 100644 index 0000000..352f2a9 --- /dev/null +++ b/src/tools/descriptions/edit_file.txt @@ -0,0 +1 @@ +通过字符串替换编辑文件的部分内容。比 write_file 更高效,适合修改文件的一小部分。 \ No newline at end of file diff --git a/src/tools/descriptions/list_directory.txt b/src/tools/descriptions/list_directory.txt new file mode 100644 index 0000000..573b9e6 --- /dev/null +++ b/src/tools/descriptions/list_directory.txt @@ -0,0 +1 @@ +列出指定目录下的文件和文件夹 \ No newline at end of file diff --git a/src/tools/descriptions/read_file.txt b/src/tools/descriptions/read_file.txt new file mode 100644 index 0000000..cdcebad --- /dev/null +++ b/src/tools/descriptions/read_file.txt @@ -0,0 +1 @@ +读取指定文件的内容 \ No newline at end of file diff --git a/src/tools/descriptions/search_files.txt b/src/tools/descriptions/search_files.txt new file mode 100644 index 0000000..6b9c0c5 --- /dev/null +++ b/src/tools/descriptions/search_files.txt @@ -0,0 +1 @@ +在目录中搜索匹配模式的文件 \ No newline at end of file diff --git a/src/tools/descriptions/write_file.txt b/src/tools/descriptions/write_file.txt new file mode 100644 index 0000000..07a94df --- /dev/null +++ b/src/tools/descriptions/write_file.txt @@ -0,0 +1 @@ +创建新文件或完全覆盖现有文件。如果只需修改文件的一部分,请使用 edit_file。 \ No newline at end of file diff --git a/src/tools/edit_file.ts b/src/tools/edit_file.ts new file mode 100644 index 0000000..b9659ff --- /dev/null +++ b/src/tools/edit_file.ts @@ -0,0 +1,69 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { Tool, ToolResult } from '../types/index.js'; +import { loadDescription } from './load_description.js'; + +export const editFileTool: Tool = { + name: 'edit_file', + description: loadDescription('edit_file'), + parameters: { + path: { + type: 'string', + description: '要编辑的文件路径', + required: true, + }, + old_string: { + type: 'string', + description: '要被替换的原始字符串(必须精确匹配)', + required: true, + }, + new_string: { + type: 'string', + description: '替换后的新字符串', + required: true, + }, + }, + execute: async (params: Record): Promise => { + const filePath = params.path as string; + const oldString = params.old_string as string; + const newString = params.new_string as string; + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + try { + const content = await fs.readFile(absolutePath, 'utf-8'); + + if (!content.includes(oldString)) { + return { + success: false, + output: '', + error: `未找到要替换的字符串。请确保 old_string 与文件中的内容完全匹配(包括空格和换行)。`, + }; + } + + const occurrences = content.split(oldString).length - 1; + if (occurrences > 1) { + return { + success: false, + output: '', + error: `找到 ${occurrences} 处匹配。old_string 必须唯一,请提供更多上下文使其唯一。`, + }; + } + + const newContent = content.replace(oldString, newString); + await fs.writeFile(absolutePath, newContent, 'utf-8'); + + return { + success: true, + output: `文件已编辑: ${absolutePath}`, + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }, +}; diff --git a/src/tools/file.ts b/src/tools/file.ts deleted file mode 100644 index 4f4ec56..0000000 --- a/src/tools/file.ts +++ /dev/null @@ -1,189 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import type { Tool, ToolResult } from '../types/index.js'; - -// 读取文件工具 -export const readFileTool: Tool = { - name: 'read_file', - description: '读取指定文件的内容', - parameters: { - path: { - type: 'string', - description: '要读取的文件路径(相对或绝对路径)', - required: true, - }, - }, - execute: async (params: Record): Promise => { - const filePath = params.path as string; - const absolutePath = path.isAbsolute(filePath) - ? filePath - : path.join(process.cwd(), filePath); - - try { - const content = await fs.readFile(absolutePath, 'utf-8'); - return { - success: true, - output: content, - }; - } catch (error) { - return { - success: false, - output: '', - error: error instanceof Error ? error.message : String(error), - }; - } - }, -}; - -// 写入文件工具 -export const writeFileTool: Tool = { - name: 'write_file', - description: '创建或覆盖文件内容', - parameters: { - path: { - type: 'string', - description: '要写入的文件路径', - required: true, - }, - content: { - type: 'string', - description: '要写入的内容', - required: true, - }, - }, - execute: async (params: Record): Promise => { - const filePath = params.path as string; - const content = params.content as string; - const absolutePath = path.isAbsolute(filePath) - ? filePath - : path.join(process.cwd(), filePath); - - try { - // 确保目录存在 - await fs.mkdir(path.dirname(absolutePath), { recursive: true }); - await fs.writeFile(absolutePath, content, 'utf-8'); - return { - success: true, - output: `文件已写入: ${absolutePath}`, - }; - } catch (error) { - return { - success: false, - output: '', - error: error instanceof Error ? error.message : String(error), - }; - } - }, -}; - -// 列出目录工具 -export const listDirTool: Tool = { - name: 'list_directory', - description: '列出指定目录下的文件和文件夹', - parameters: { - path: { - type: 'string', - description: '要列出的目录路径', - required: true, - }, - }, - execute: async (params: Record): Promise => { - const dirPath = params.path as string; - const absolutePath = path.isAbsolute(dirPath) - ? dirPath - : path.join(process.cwd(), dirPath); - - try { - const entries = await fs.readdir(absolutePath, { withFileTypes: true }); - const result = entries - .map((entry) => { - const prefix = entry.isDirectory() ? '📁' : '📄'; - return `${prefix} ${entry.name}`; - }) - .join('\n'); - - return { - success: true, - output: result || '(空目录)', - }; - } catch (error) { - return { - success: false, - output: '', - error: error instanceof Error ? error.message : String(error), - }; - } - }, -}; - -// 搜索文件工具 -export const searchFilesTool: Tool = { - name: 'search_files', - description: '在目录中搜索匹配模式的文件', - parameters: { - directory: { - type: 'string', - description: '搜索的起始目录', - required: true, - }, - pattern: { - type: 'string', - description: '文件名匹配模式(支持 glob 模式,如 *.ts)', - required: true, - }, - }, - execute: async (params: Record): Promise => { - const directory = params.directory as string; - const pattern = params.pattern as string; - const absolutePath = path.isAbsolute(directory) - ? directory - : path.join(process.cwd(), directory); - - const matches: string[] = []; - const regex = new RegExp( - pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), - 'i' - ); - - async function searchRecursive(dir: string, depth = 0): Promise { - if (depth > 10) return; // 限制递归深度 - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - // 跳过 node_modules 和隐藏目录 - if (entry.name.startsWith('.') || entry.name === 'node_modules') { - continue; - } - - if (entry.isDirectory()) { - await searchRecursive(fullPath, depth + 1); - } else if (regex.test(entry.name)) { - matches.push(fullPath); - } - } - } catch { - // 忽略权限错误 - } - } - - try { - await searchRecursive(absolutePath); - return { - success: true, - output: - matches.length > 0 - ? matches.join('\n') - : '没有找到匹配的文件', - }; - } catch (error) { - return { - success: false, - output: '', - error: error instanceof Error ? error.message : String(error), - }; - } - }, -}; diff --git a/src/tools/index.ts b/src/tools/index.ts index af0f8ff..0f0476c 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,7 +1,18 @@ -export { bashTool } from './bash.js'; -export { +import type { Tool } from '../types/index.js'; +import { bashTool } from './bash.js'; +import { readFileTool } from './read_file.js'; +import { writeFileTool } from './write_file.js'; +import { editFileTool } from './edit_file.js'; +import { listDirTool } from './list_directory.js'; +import { searchFilesTool } from './search_files.js'; + +// 所有可用工具的注册中心 +// 添加新工具只需在此数组中添加一行 +export const allTools: Tool[] = [ + bashTool, readFileTool, writeFileTool, + editFileTool, listDirTool, searchFilesTool, -} from './file.js'; +]; diff --git a/src/tools/list_directory.ts b/src/tools/list_directory.ts new file mode 100644 index 0000000..b6e1060 --- /dev/null +++ b/src/tools/list_directory.ts @@ -0,0 +1,43 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { Tool, ToolResult } from '../types/index.js'; +import { loadDescription } from './load_description.js'; + +export const listDirTool: Tool = { + name: 'list_directory', + description: loadDescription('list_directory'), + parameters: { + path: { + type: 'string', + description: '要列出的目录路径', + required: true, + }, + }, + execute: async (params: Record): Promise => { + const dirPath = params.path as string; + const absolutePath = path.isAbsolute(dirPath) + ? dirPath + : path.join(process.cwd(), dirPath); + + try { + const entries = await fs.readdir(absolutePath, { withFileTypes: true }); + const result = entries + .map((entry) => { + const prefix = entry.isDirectory() ? '📁' : '📄'; + return `${prefix} ${entry.name}`; + }) + .join('\n'); + + return { + success: true, + output: result || '(空目录)', + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }, +}; diff --git a/src/tools/load_description.ts b/src/tools/load_description.ts new file mode 100644 index 0000000..3cd7eed --- /dev/null +++ b/src/tools/load_description.ts @@ -0,0 +1,15 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export function loadDescription(toolName: string): string { + const filePath = path.join(__dirname, 'descriptions', `${toolName}.txt`); + try { + return fs.readFileSync(filePath, 'utf-8').trim(); + } catch { + throw new Error(`无法加载工具描述文件: ${filePath}`); + } +} diff --git a/src/tools/read_file.ts b/src/tools/read_file.ts new file mode 100644 index 0000000..e715261 --- /dev/null +++ b/src/tools/read_file.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { Tool, ToolResult } from '../types/index.js'; +import { loadDescription } from './load_description.js'; + +export const readFileTool: Tool = { + name: 'read_file', + description: loadDescription('read_file'), + parameters: { + path: { + type: 'string', + description: '要读取的文件路径(相对或绝对路径)', + required: true, + }, + }, + execute: async (params: Record): Promise => { + const filePath = params.path as string; + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + try { + const content = await fs.readFile(absolutePath, 'utf-8'); + return { + success: true, + output: content, + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }, +}; diff --git a/src/tools/search_files.ts b/src/tools/search_files.ts new file mode 100644 index 0000000..26bbc21 --- /dev/null +++ b/src/tools/search_files.ts @@ -0,0 +1,74 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { Tool, ToolResult } from '../types/index.js'; +import { loadDescription } from './load_description.js'; + +export const searchFilesTool: Tool = { + name: 'search_files', + description: loadDescription('search_files'), + parameters: { + directory: { + type: 'string', + description: '搜索的起始目录', + required: true, + }, + pattern: { + type: 'string', + description: '文件名匹配模式(支持 glob 模式,如 *.ts)', + required: true, + }, + }, + execute: async (params: Record): Promise => { + const directory = params.directory as string; + const pattern = params.pattern as string; + const absolutePath = path.isAbsolute(directory) + ? directory + : path.join(process.cwd(), directory); + + const matches: string[] = []; + const regex = new RegExp( + pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), + 'i' + ); + + async function searchRecursive(dir: string, depth = 0): Promise { + if (depth > 10) return; + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.name.startsWith('.') || entry.name === 'node_modules') { + continue; + } + + if (entry.isDirectory()) { + await searchRecursive(fullPath, depth + 1); + } else if (regex.test(entry.name)) { + matches.push(fullPath); + } + } + } catch { + // 忽略权限错误 + } + } + + try { + await searchRecursive(absolutePath); + return { + success: true, + output: + matches.length > 0 + ? matches.join('\n') + : '没有找到匹配的文件', + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }, +}; diff --git a/src/tools/write_file.ts b/src/tools/write_file.ts new file mode 100644 index 0000000..e7d269c --- /dev/null +++ b/src/tools/write_file.ts @@ -0,0 +1,43 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { Tool, ToolResult } from '../types/index.js'; +import { loadDescription } from './load_description.js'; + +export const writeFileTool: Tool = { + name: 'write_file', + description: loadDescription('write_file'), + parameters: { + path: { + type: 'string', + description: '要写入的文件路径', + required: true, + }, + content: { + type: 'string', + description: '要写入的内容', + required: true, + }, + }, + execute: async (params: Record): Promise => { + const filePath = params.path as string; + const content = params.content as string; + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + try { + await fs.mkdir(path.dirname(absolutePath), { recursive: true }); + await fs.writeFile(absolutePath, content, 'utf-8'); + return { + success: true, + output: `文件已写入: ${absolutePath}`, + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + }; + } + }, +};