feat: 新增 6 个工具并重组目录结构
新增工具: - grep_content: 在文件内容中搜索文本/正则表达式 - get_file_info: 获取文件元信息(大小、权限、时间等) - move_file: 移动或重命名文件/目录 - copy_file: 复制文件或目录(支持递归) - delete_file: 删除文件或目录 - create_directory: 创建目录 目录重组: - src/tools/shell/ - Shell 相关工具(bash) - src/tools/filesystem/ - 文件系统工具(12个) - 每个子目录有独立的 index.ts 导出 权限系统扩展: - 新增操作类型: grep, info, move, copy, mkdir - 读取类操作默认允许,写入类操作需要确认
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
async function copyRecursive(source: string, dest: string): Promise<void> {
|
||||
const stats = await fs.stat(source);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
await fs.mkdir(dest, { recursive: true });
|
||||
const entries = await fs.readdir(source);
|
||||
for (const entry of entries) {
|
||||
await copyRecursive(path.join(source, entry), path.join(dest, entry));
|
||||
}
|
||||
} else {
|
||||
await fs.copyFile(source, dest);
|
||||
}
|
||||
}
|
||||
|
||||
export const copyFileTool: Tool = {
|
||||
name: 'copy_file',
|
||||
description: loadDescription('copy_file'),
|
||||
parameters: {
|
||||
source: {
|
||||
type: 'string',
|
||||
description: '源文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
description: '目标路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const source = params.source as string;
|
||||
const destination = params.destination as string;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absoluteSource = path.isAbsolute(source)
|
||||
? source
|
||||
: path.join(cwd, source);
|
||||
|
||||
const absoluteDest = path.isAbsolute(destination)
|
||||
? destination
|
||||
: path.join(cwd, destination);
|
||||
|
||||
// 权限检查 - 源文件需要 read 权限
|
||||
const permissionManager = getPermissionManager();
|
||||
const sourcePermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'read',
|
||||
path: absoluteSource,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!sourcePermResult.allowed) {
|
||||
if (sourcePermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 读取 ${absoluteSource}\n原因: ${sourcePermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${sourcePermResult.reason || '不允许读取此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查 - 目标位置需要 copy 权限
|
||||
const destPermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'copy',
|
||||
path: absoluteDest,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!destPermResult.allowed) {
|
||||
if (destPermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 复制到 ${absoluteDest}\n原因: ${destPermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${destPermResult.reason || '不允许复制到此位置'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查源文件是否存在
|
||||
const sourceStats = await fs.stat(absoluteSource);
|
||||
|
||||
// 检查目标是否是目录
|
||||
let finalDest = absoluteDest;
|
||||
try {
|
||||
const destStats = await fs.stat(absoluteDest);
|
||||
if (destStats.isDirectory()) {
|
||||
// 如果目标是目录,将源文件复制到该目录下
|
||||
finalDest = path.join(absoluteDest, path.basename(absoluteSource));
|
||||
}
|
||||
} catch {
|
||||
// 目标不存在,直接使用目标路径
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
await fs.mkdir(path.dirname(finalDest), { recursive: true });
|
||||
|
||||
// 执行复制
|
||||
if (sourceStats.isDirectory()) {
|
||||
await copyRecursive(absoluteSource, finalDest);
|
||||
} else {
|
||||
await fs.copyFile(absoluteSource, finalDest);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `已复制: ${absoluteSource} -> ${finalDest}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
export const createDirectoryTool: Tool = {
|
||||
name: 'create_directory',
|
||||
description: loadDescription('create_directory'),
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要创建的目录路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const dirPath = params.path as string;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absolutePath = path.isAbsolute(dirPath)
|
||||
? dirPath
|
||||
: path.join(cwd, dirPath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'mkdir',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 创建目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许创建此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查目录是否已存在
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
return {
|
||||
success: true,
|
||||
output: `目录已存在: ${absolutePath}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `路径已存在且不是目录: ${absolutePath}`,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// 目录不存在,继续创建
|
||||
}
|
||||
|
||||
// 创建目录(递归创建父目录)
|
||||
await fs.mkdir(absolutePath, { recursive: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `已创建目录: ${absolutePath}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
export const deleteFileTool: Tool = {
|
||||
name: 'delete_file',
|
||||
description: loadDescription('delete_file'),
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '要删除的文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
recursive: {
|
||||
type: 'boolean',
|
||||
description: '是否递归删除目录(默认 false)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const recursive = (params.recursive as boolean) || false;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'delete',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 删除 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许删除此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
if (!recursive) {
|
||||
// 检查目录是否为空
|
||||
const entries = await fs.readdir(absolutePath);
|
||||
if (entries.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `目录不为空。如需删除非空目录,请设置 recursive: true`,
|
||||
};
|
||||
}
|
||||
await fs.rmdir(absolutePath);
|
||||
} else {
|
||||
await fs.rm(absolutePath, { recursive: true });
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: `已删除目录: ${absolutePath}`,
|
||||
};
|
||||
} else {
|
||||
await fs.unlink(absolutePath);
|
||||
return {
|
||||
success: true,
|
||||
output: `已删除文件: ${absolutePath}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.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 cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'edit',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 编辑 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许编辑此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let unitIndex = 0;
|
||||
let size = bytes;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatPermissions(mode: number): string {
|
||||
const types: Record<number, string> = {
|
||||
0o140000: 'socket',
|
||||
0o120000: 'symbolic link',
|
||||
0o100000: 'regular file',
|
||||
0o060000: 'block device',
|
||||
0o040000: 'directory',
|
||||
0o020000: 'character device',
|
||||
0o010000: 'FIFO',
|
||||
};
|
||||
|
||||
const fileType = Object.entries(types).find(([mask]) => (mode & 0o170000) === Number(mask));
|
||||
|
||||
const perms = [
|
||||
(mode & 0o400) ? 'r' : '-',
|
||||
(mode & 0o200) ? 'w' : '-',
|
||||
(mode & 0o100) ? 'x' : '-',
|
||||
(mode & 0o040) ? 'r' : '-',
|
||||
(mode & 0o020) ? 'w' : '-',
|
||||
(mode & 0o010) ? 'x' : '-',
|
||||
(mode & 0o004) ? 'r' : '-',
|
||||
(mode & 0o002) ? 'w' : '-',
|
||||
(mode & 0o001) ? 'x' : '-',
|
||||
].join('');
|
||||
|
||||
return `${fileType?.[1] || 'unknown'} (${perms})`;
|
||||
}
|
||||
|
||||
export const getFileInfoTool: Tool = {
|
||||
name: 'get_file_info',
|
||||
description: loadDescription('get_file_info'),
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const filePath = params.path as string;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'info',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 获取文件信息 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许获取此文件信息'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
const info = [
|
||||
`路径: ${absolutePath}`,
|
||||
`类型: ${stats.isDirectory() ? '目录' : stats.isFile() ? '文件' : stats.isSymbolicLink() ? '符号链接' : '其他'}`,
|
||||
`大小: ${formatSize(stats.size)}`,
|
||||
`权限: ${formatPermissions(stats.mode)}`,
|
||||
`创建时间: ${stats.birthtime.toLocaleString()}`,
|
||||
`修改时间: ${stats.mtime.toLocaleString()}`,
|
||||
`访问时间: ${stats.atime.toLocaleString()}`,
|
||||
`inode: ${stats.ino}`,
|
||||
`硬链接数: ${stats.nlink}`,
|
||||
];
|
||||
|
||||
// 如果是符号链接,显示目标
|
||||
if (stats.isSymbolicLink()) {
|
||||
try {
|
||||
const target = await fs.readlink(absolutePath);
|
||||
info.push(`链接目标: ${target}`);
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是目录,统计子项数量
|
||||
if (stats.isDirectory()) {
|
||||
try {
|
||||
const entries = await fs.readdir(absolutePath);
|
||||
info.push(`子项数量: ${entries.length}`);
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: info.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
interface GrepMatch {
|
||||
file: string;
|
||||
line: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const grepContentTool: Tool = {
|
||||
name: 'grep_content',
|
||||
description: loadDescription('grep_content'),
|
||||
parameters: {
|
||||
directory: {
|
||||
type: 'string',
|
||||
description: '搜索的起始目录',
|
||||
required: true,
|
||||
},
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: '搜索的文本或正则表达式模式',
|
||||
required: true,
|
||||
},
|
||||
file_pattern: {
|
||||
type: 'string',
|
||||
description: '文件名匹配模式(可选,如 *.ts)',
|
||||
required: false,
|
||||
},
|
||||
max_results: {
|
||||
type: 'number',
|
||||
description: '最大结果数量(可选,默认 100)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const directory = params.directory as string;
|
||||
const pattern = params.pattern as string;
|
||||
const filePattern = params.file_pattern as string | undefined;
|
||||
const maxResults = (params.max_results as number) || 100;
|
||||
const cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(directory)
|
||||
? directory
|
||||
: path.join(cwd, directory);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'grep',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 搜索目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许搜索此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const matches: GrepMatch[] = [];
|
||||
const searchRegex = new RegExp(pattern, 'gi');
|
||||
const fileRegex = filePattern
|
||||
? new RegExp(filePattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i')
|
||||
: null;
|
||||
|
||||
async function searchFile(filePath: string): Promise<void> {
|
||||
if (matches.length >= maxResults) return;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (matches.length >= maxResults) break;
|
||||
|
||||
if (searchRegex.test(lines[i])) {
|
||||
matches.push({
|
||||
file: filePath,
|
||||
line: i + 1,
|
||||
content: lines[i].trim().substring(0, 200), // 截断过长的行
|
||||
});
|
||||
}
|
||||
// 重置正则表达式的 lastIndex
|
||||
searchRegex.lastIndex = 0;
|
||||
}
|
||||
} catch {
|
||||
// 忽略无法读取的文件(如二进制文件)
|
||||
}
|
||||
}
|
||||
|
||||
async function searchDirectory(dir: string, depth = 0): Promise<void> {
|
||||
if (depth > 10 || matches.length >= maxResults) return;
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (matches.length >= maxResults) break;
|
||||
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
// 跳过隐藏文件和 node_modules
|
||||
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await searchDirectory(fullPath, depth + 1);
|
||||
} else if (entry.isFile()) {
|
||||
// 检查文件名是否匹配
|
||||
if (fileRegex && !fileRegex.test(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
await searchFile(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略权限错误
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await searchDirectory(absolutePath);
|
||||
|
||||
if (matches.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: '没有找到匹配的内容',
|
||||
};
|
||||
}
|
||||
|
||||
const output = matches
|
||||
.map((m) => `${m.file}:${m.line}: ${m.content}`)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `找到 ${matches.length} 处匹配${matches.length >= maxResults ? '(已达上限)' : ''}:\n\n${output}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// 文件读写
|
||||
export { readFileTool } from './read_file.js';
|
||||
export { writeFileTool } from './write_file.js';
|
||||
export { editFileTool } from './edit_file.js';
|
||||
|
||||
// 目录操作
|
||||
export { listDirTool } from './list_directory.js';
|
||||
export { createDirectoryTool } from './create_directory.js';
|
||||
|
||||
// 搜索
|
||||
export { searchFilesTool } from './search_files.js';
|
||||
export { grepContentTool } from './grep_content.js';
|
||||
|
||||
// 文件信息
|
||||
export { getFileInfoTool } from './get_file_info.js';
|
||||
|
||||
// 文件管理
|
||||
export { moveFileTool } from './move_file.js';
|
||||
export { copyFileTool } from './copy_file.js';
|
||||
export { deleteFileTool } from './delete_file.js';
|
||||
@@ -0,0 +1,68 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.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 cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(dirPath)
|
||||
? dirPath
|
||||
: path.join(cwd, dirPath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'list',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 列出目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许列出此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
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,114 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
export const moveFileTool: Tool = {
|
||||
name: 'move_file',
|
||||
description: loadDescription('move_file'),
|
||||
parameters: {
|
||||
source: {
|
||||
type: 'string',
|
||||
description: '源文件或目录的路径',
|
||||
required: true,
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
description: '目标路径',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const source = params.source as string;
|
||||
const destination = params.destination as string;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const absoluteSource = path.isAbsolute(source)
|
||||
? source
|
||||
: path.join(cwd, source);
|
||||
|
||||
const absoluteDest = path.isAbsolute(destination)
|
||||
? destination
|
||||
: path.join(cwd, destination);
|
||||
|
||||
// 权限检查 - 源文件需要 move 权限
|
||||
const permissionManager = getPermissionManager();
|
||||
const sourcePermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'move',
|
||||
path: absoluteSource,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!sourcePermResult.allowed) {
|
||||
if (sourcePermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 移动 ${absoluteSource}\n原因: ${sourcePermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${sourcePermResult.reason || '不允许移动此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查 - 目标位置需要 write 权限
|
||||
const destPermResult = await permissionManager.checkFilePermission({
|
||||
operation: 'write',
|
||||
path: absoluteDest,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!destPermResult.allowed) {
|
||||
if (destPermResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 写入到 ${absoluteDest}\n原因: ${destPermResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${destPermResult.reason || '不允许写入到此位置'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查源文件是否存在
|
||||
await fs.access(absoluteSource);
|
||||
|
||||
// 检查目标是否是目录
|
||||
let finalDest = absoluteDest;
|
||||
try {
|
||||
const destStats = await fs.stat(absoluteDest);
|
||||
if (destStats.isDirectory()) {
|
||||
// 如果目标是目录,将源文件移动到该目录下
|
||||
finalDest = path.join(absoluteDest, path.basename(absoluteSource));
|
||||
}
|
||||
} catch {
|
||||
// 目标不存在,直接使用目标路径
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
await fs.mkdir(path.dirname(finalDest), { recursive: true });
|
||||
|
||||
// 执行移动
|
||||
await fs.rename(absoluteSource, finalDest);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `已移动: ${absoluteSource} -> ${finalDest}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.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 cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'read',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 读取 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许读取此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
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,99 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.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 cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(directory)
|
||||
? directory
|
||||
: path.join(cwd, directory);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'search',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 搜索目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许搜索此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
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,68 @@
|
||||
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';
|
||||
import { getPermissionManager } from '../../permission/index.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 cwd = process.cwd();
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(cwd, filePath);
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'write',
|
||||
path: absolutePath,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 写入 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许写入此文件'}`,
|
||||
};
|
||||
}
|
||||
|
||||
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