优化工具

This commit is contained in:
2025-12-10 17:11:46 +08:00
parent 4b451b2d58
commit af1185c4d7
18 changed files with 316 additions and 214 deletions
+8 -1
View File
@@ -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<string, AITool> {
const vercelTools: Record<string, AITool> = {};
+5 -19
View File
@@ -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);
+2 -1
View File
@@ -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',
+1
View File
@@ -0,0 +1 @@
执行 bash 命令。可以用于运行系统命令、安装包、git 操作等。
+1
View File
@@ -0,0 +1 @@
通过字符串替换编辑文件的部分内容。比 write_file 更高效,适合修改文件的一小部分。
@@ -0,0 +1 @@
列出指定目录下的文件和文件夹
+1
View File
@@ -0,0 +1 @@
读取指定文件的内容
+1
View File
@@ -0,0 +1 @@
在目录中搜索匹配模式的文件
+1
View File
@@ -0,0 +1 @@
创建新文件或完全覆盖现有文件。如果只需修改文件的一部分,请使用 edit_file。
+69
View File
@@ -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<string, unknown>): Promise<ToolResult> => {
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),
};
}
},
};
-189
View File
@@ -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<string, unknown>): Promise<ToolResult> => {
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<string, unknown>): Promise<ToolResult> => {
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<string, unknown>): Promise<ToolResult> => {
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<string, unknown>): Promise<ToolResult> => {
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<void> {
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),
};
}
},
};
+14 -3
View File
@@ -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';
];
+43
View File
@@ -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<string, unknown>): Promise<ToolResult> => {
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),
};
}
},
};
+15
View File
@@ -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}`);
}
}
+36
View File
@@ -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<string, unknown>): Promise<ToolResult> => {
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),
};
}
},
};
+74
View File
@@ -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<string, unknown>): Promise<ToolResult> => {
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<void> {
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),
};
}
},
};
+43
View File
@@ -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<string, unknown>): Promise<ToolResult> => {
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),
};
}
},
};