优化工具
This commit is contained in:
+8
-1
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
执行 bash 命令。可以用于运行系统命令、安装包、git 操作等。
|
||||
@@ -0,0 +1 @@
|
||||
通过字符串替换编辑文件的部分内容。比 write_file 更高效,适合修改文件的一小部分。
|
||||
@@ -0,0 +1 @@
|
||||
列出指定目录下的文件和文件夹
|
||||
@@ -0,0 +1 @@
|
||||
读取指定文件的内容
|
||||
@@ -0,0 +1 @@
|
||||
在目录中搜索匹配模式的文件
|
||||
@@ -0,0 +1 @@
|
||||
创建新文件或完全覆盖现有文件。如果只需修改文件的一部分,请使用 edit_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),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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
@@ -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';
|
||||
];
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user