From e435b2f8f88517a1a31780593f6b4c41562d08a5 Mon Sep 17 00:00:00 2001 From: kurihada Date: Wed, 10 Dec 2025 18:25:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=206=20=E4=B8=AA?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=B9=B6=E9=87=8D=E7=BB=84=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增工具: - 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 - 读取类操作默认允许,写入类操作需要确认 --- src/permission/checkers/file.ts | 5 + src/permission/types.ts | 18 ++- src/tools/descriptions/copy_file.txt | 1 + src/tools/descriptions/create_directory.txt | 1 + src/tools/descriptions/delete_file.txt | 1 + src/tools/descriptions/get_file_info.txt | 1 + src/tools/descriptions/grep_content.txt | 1 + src/tools/descriptions/move_file.txt | 1 + src/tools/filesystem/copy_file.ts | 132 +++++++++++++++ src/tools/filesystem/create_directory.ts | 83 ++++++++++ src/tools/filesystem/delete_file.ts | 91 +++++++++++ src/tools/{ => filesystem}/edit_file.ts | 6 +- src/tools/filesystem/get_file_info.ts | 134 ++++++++++++++++ src/tools/filesystem/grep_content.ts | 159 +++++++++++++++++++ src/tools/filesystem/index.ts | 20 +++ src/tools/{ => filesystem}/list_directory.ts | 6 +- src/tools/filesystem/move_file.ts | 114 +++++++++++++ src/tools/{ => filesystem}/read_file.ts | 6 +- src/tools/{ => filesystem}/search_files.ts | 6 +- src/tools/{ => filesystem}/write_file.ts | 6 +- src/tools/index.ts | 49 ++++-- src/tools/{ => shell}/bash.ts | 6 +- src/tools/shell/index.ts | 1 + 23 files changed, 819 insertions(+), 29 deletions(-) create mode 100644 src/tools/descriptions/copy_file.txt create mode 100644 src/tools/descriptions/create_directory.txt create mode 100644 src/tools/descriptions/delete_file.txt create mode 100644 src/tools/descriptions/get_file_info.txt create mode 100644 src/tools/descriptions/grep_content.txt create mode 100644 src/tools/descriptions/move_file.txt create mode 100644 src/tools/filesystem/copy_file.ts create mode 100644 src/tools/filesystem/create_directory.ts create mode 100644 src/tools/filesystem/delete_file.ts rename src/tools/{ => filesystem}/edit_file.ts (93%) create mode 100644 src/tools/filesystem/get_file_info.ts create mode 100644 src/tools/filesystem/grep_content.ts create mode 100644 src/tools/filesystem/index.ts rename src/tools/{ => filesystem}/list_directory.ts (90%) create mode 100644 src/tools/filesystem/move_file.ts rename src/tools/{ => filesystem}/read_file.ts (89%) rename src/tools/{ => filesystem}/search_files.ts (93%) rename src/tools/{ => filesystem}/write_file.ts (90%) rename src/tools/{ => shell}/bash.ts (91%) create mode 100644 src/tools/shell/index.ts diff --git a/src/permission/checkers/file.ts b/src/permission/checkers/file.ts index c8fae6d..15a8ab4 100644 --- a/src/permission/checkers/file.ts +++ b/src/permission/checkers/file.ts @@ -22,7 +22,12 @@ const DEFAULT_CONFIG: FilePermissionConfig = { edit: 'ask', // 编辑需要确认 list: 'allow', // 列目录默认允许 search: 'allow', // 搜索默认允许 + grep: 'allow', // 内容搜索默认允许 + info: 'allow', // 获取文件信息默认允许 + move: 'ask', // 移动需要确认 + copy: 'ask', // 复制需要确认 delete: 'ask', // 删除需要确认 + mkdir: 'ask', // 创建目录需要确认 }, sensitivePaths: [ // 系统关键路径 - 拒绝 diff --git a/src/permission/types.ts b/src/permission/types.ts index 486bf7a..f0e35e9 100644 --- a/src/permission/types.ts +++ b/src/permission/types.ts @@ -26,7 +26,18 @@ export interface PermissionContext { } // 文件操作类型 -export type FileOperation = 'read' | 'write' | 'edit' | 'list' | 'search' | 'delete'; +export type FileOperation = + | 'read' // 读取文件 + | 'write' // 写入文件 + | 'edit' // 编辑文件 + | 'list' // 列出目录 + | 'search' // 搜索文件 + | 'grep' // 搜索内容 + | 'info' // 获取文件信息 + | 'move' // 移动/重命名 + | 'copy' // 复制 + | 'delete' // 删除 + | 'mkdir'; // 创建目录 // 文件权限请求上下文 export interface FilePermissionContext { @@ -46,7 +57,12 @@ export interface FilePermissionConfig { edit: PermissionAction; list: PermissionAction; search: PermissionAction; + grep: PermissionAction; + info: PermissionAction; + move: PermissionAction; + copy: PermissionAction; delete: PermissionAction; + mkdir: PermissionAction; }; // 敏感路径规则(优先于操作默认策略) sensitivePaths: { diff --git a/src/tools/descriptions/copy_file.txt b/src/tools/descriptions/copy_file.txt new file mode 100644 index 0000000..c6c30ad --- /dev/null +++ b/src/tools/descriptions/copy_file.txt @@ -0,0 +1 @@ +复制文件或目录。支持递归复制整个目录结构。 \ No newline at end of file diff --git a/src/tools/descriptions/create_directory.txt b/src/tools/descriptions/create_directory.txt new file mode 100644 index 0000000..02648d0 --- /dev/null +++ b/src/tools/descriptions/create_directory.txt @@ -0,0 +1 @@ +创建新目录。支持递归创建父目录。如果目录已存在则不会报错。 \ No newline at end of file diff --git a/src/tools/descriptions/delete_file.txt b/src/tools/descriptions/delete_file.txt new file mode 100644 index 0000000..648f7f6 --- /dev/null +++ b/src/tools/descriptions/delete_file.txt @@ -0,0 +1 @@ +删除文件或目录。删除目录时可以选择是否递归删除。需要谨慎使用。 \ No newline at end of file diff --git a/src/tools/descriptions/get_file_info.txt b/src/tools/descriptions/get_file_info.txt new file mode 100644 index 0000000..653661f --- /dev/null +++ b/src/tools/descriptions/get_file_info.txt @@ -0,0 +1 @@ +获取文件或目录的详细信息,包括大小、权限、创建时间、修改时间等元数据。 \ No newline at end of file diff --git a/src/tools/descriptions/grep_content.txt b/src/tools/descriptions/grep_content.txt new file mode 100644 index 0000000..69b455c --- /dev/null +++ b/src/tools/descriptions/grep_content.txt @@ -0,0 +1 @@ +在指定目录中搜索文件内容。支持正则表达式,可以指定文件类型过滤。用于查找代码中的特定文本、函数调用、变量引用等。 \ No newline at end of file diff --git a/src/tools/descriptions/move_file.txt b/src/tools/descriptions/move_file.txt new file mode 100644 index 0000000..6eae352 --- /dev/null +++ b/src/tools/descriptions/move_file.txt @@ -0,0 +1 @@ +移动或重命名文件/目录。可以将文件移动到新位置或更改文件名。 \ No newline at end of file diff --git a/src/tools/filesystem/copy_file.ts b/src/tools/filesystem/copy_file.ts new file mode 100644 index 0000000..db98072 --- /dev/null +++ b/src/tools/filesystem/copy_file.ts @@ -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 { + 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): Promise => { + 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), + }; + } + }, +}; diff --git a/src/tools/filesystem/create_directory.ts b/src/tools/filesystem/create_directory.ts new file mode 100644 index 0000000..8cae9ce --- /dev/null +++ b/src/tools/filesystem/create_directory.ts @@ -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): Promise => { + 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), + }; + } + }, +}; diff --git a/src/tools/filesystem/delete_file.ts b/src/tools/filesystem/delete_file.ts new file mode 100644 index 0000000..d1dbc51 --- /dev/null +++ b/src/tools/filesystem/delete_file.ts @@ -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): Promise => { + 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), + }; + } + }, +}; diff --git a/src/tools/edit_file.ts b/src/tools/filesystem/edit_file.ts similarity index 93% rename from src/tools/edit_file.ts rename to src/tools/filesystem/edit_file.ts index 534a00d..ade3ca6 100644 --- a/src/tools/edit_file.ts +++ b/src/tools/filesystem/edit_file.ts @@ -1,8 +1,8 @@ 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'; +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', diff --git a/src/tools/filesystem/get_file_info.ts b/src/tools/filesystem/get_file_info.ts new file mode 100644 index 0000000..9da62cb --- /dev/null +++ b/src/tools/filesystem/get_file_info.ts @@ -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 = { + 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): Promise => { + 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), + }; + } + }, +}; diff --git a/src/tools/filesystem/grep_content.ts b/src/tools/filesystem/grep_content.ts new file mode 100644 index 0000000..73a338d --- /dev/null +++ b/src/tools/filesystem/grep_content.ts @@ -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): Promise => { + 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 { + 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 { + 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), + }; + } + }, +}; diff --git a/src/tools/filesystem/index.ts b/src/tools/filesystem/index.ts new file mode 100644 index 0000000..147fe5b --- /dev/null +++ b/src/tools/filesystem/index.ts @@ -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'; diff --git a/src/tools/list_directory.ts b/src/tools/filesystem/list_directory.ts similarity index 90% rename from src/tools/list_directory.ts rename to src/tools/filesystem/list_directory.ts index dc879a0..f0d647b 100644 --- a/src/tools/list_directory.ts +++ b/src/tools/filesystem/list_directory.ts @@ -1,8 +1,8 @@ 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'; +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', diff --git a/src/tools/filesystem/move_file.ts b/src/tools/filesystem/move_file.ts new file mode 100644 index 0000000..66a2c58 --- /dev/null +++ b/src/tools/filesystem/move_file.ts @@ -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): Promise => { + 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), + }; + } + }, +}; diff --git a/src/tools/read_file.ts b/src/tools/filesystem/read_file.ts similarity index 89% rename from src/tools/read_file.ts rename to src/tools/filesystem/read_file.ts index a7a6473..babf29c 100644 --- a/src/tools/read_file.ts +++ b/src/tools/filesystem/read_file.ts @@ -1,8 +1,8 @@ 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'; +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', diff --git a/src/tools/search_files.ts b/src/tools/filesystem/search_files.ts similarity index 93% rename from src/tools/search_files.ts rename to src/tools/filesystem/search_files.ts index 31b745b..2ba3be8 100644 --- a/src/tools/search_files.ts +++ b/src/tools/filesystem/search_files.ts @@ -1,8 +1,8 @@ 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'; +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', diff --git a/src/tools/write_file.ts b/src/tools/filesystem/write_file.ts similarity index 90% rename from src/tools/write_file.ts rename to src/tools/filesystem/write_file.ts index 9d3b9bf..3b41266 100644 --- a/src/tools/write_file.ts +++ b/src/tools/filesystem/write_file.ts @@ -1,8 +1,8 @@ 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'; +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', diff --git a/src/tools/index.ts b/src/tools/index.ts index 0f0476c..d503391 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,18 +1,47 @@ 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, +// Shell 工具 +import { bashTool } from './shell/index.js'; + +// 文件系统工具 +import { readFileTool, writeFileTool, editFileTool, listDirTool, + createDirectoryTool, searchFilesTool, + grepContentTool, + getFileInfoTool, + moveFileTool, + copyFileTool, + deleteFileTool, +} from './filesystem/index.js'; + +// 所有可用工具的注册中心 +// 添加新工具只需在此数组中添加一行 +export const allTools: Tool[] = [ + // Shell + bashTool, + + // 文件读写 + readFileTool, + writeFileTool, + editFileTool, + + // 目录操作 + listDirTool, + createDirectoryTool, + + // 搜索 + searchFilesTool, + grepContentTool, + + // 文件信息 + getFileInfoTool, + + // 文件管理 + moveFileTool, + copyFileTool, + deleteFileTool, ]; diff --git a/src/tools/bash.ts b/src/tools/shell/bash.ts similarity index 91% rename from src/tools/bash.ts rename to src/tools/shell/bash.ts index f912ffa..4db67cf 100644 --- a/src/tools/bash.ts +++ b/src/tools/shell/bash.ts @@ -1,8 +1,8 @@ import { exec } from 'child_process'; import { promisify } from 'util'; -import type { Tool, ToolResult } from '../types/index.js'; -import { loadDescription } from './load_description.js'; -import { getPermissionManager } from '../permission/index.js'; +import type { Tool, ToolResult } from '../../types/index.js'; +import { loadDescription } from '../load_description.js'; +import { getPermissionManager } from '../../permission/index.js'; const execAsync = promisify(exec); diff --git a/src/tools/shell/index.ts b/src/tools/shell/index.ts new file mode 100644 index 0000000..0a4aad8 --- /dev/null +++ b/src/tools/shell/index.ts @@ -0,0 +1 @@ +export { bashTool } from './bash.js';