From 2abea4738660301d91b2206474fb099dd6f82beb Mon Sep 17 00:00:00 2001 From: kurihada Date: Wed, 17 Dec 2025 12:00:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E9=9C=80=E8=A6=81=E7=9A=84=E6=96=87=E4=BB=B6=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除以下工具及相关文件: - copy_file: 复制文件 - create_directory: 创建目录 - delete_file: 删除文件 - move_file: 移动文件 - search_files: 搜索文件 清理范围: - 工具实现文件 (5个) - 工具描述文件 (5个) - 单元测试文件 (6个) - Agent presets 中的引用 - Checkpoint 系统中的触发类型 - Hook 系统中的相关处理 --- packages/core/src/agent/executor.ts | 5 +- packages/core/src/agent/presets/build.ts | 5 - .../core/src/agent/presets/code-reviewer.ts | 2 +- packages/core/src/agent/presets/explore.ts | 5 - packages/core/src/agent/presets/guide.ts | 4 - packages/core/src/agent/presets/plan.ts | 1 - .../core/src/checkpoint/checkpoint-store.ts | 11 - .../core/src/checkpoint/commit-message.ts | 3 - packages/core/src/checkpoint/manager.ts | 2 - packages/core/src/checkpoint/safety.ts | 3 - packages/core/src/checkpoint/types.ts | 9 - packages/core/src/core/agent-tool-executor.ts | 9 - .../descriptions/filesystem/copy_file.txt | 1 - .../filesystem/create_directory.txt | 1 - .../descriptions/filesystem/delete_file.txt | 1 - .../descriptions/filesystem/move_file.txt | 1 - .../descriptions/filesystem/search_files.txt | 1 - .../core/src/tools/filesystem/copy_file.ts | 140 ------------ .../src/tools/filesystem/create_directory.ts | 91 -------- .../core/src/tools/filesystem/delete_file.ts | 99 --------- packages/core/src/tools/filesystem/index.ts | 7 - .../core/src/tools/filesystem/move_file.ts | 122 ----------- .../core/src/tools/filesystem/search_files.ts | 107 ---------- packages/core/src/tools/index.ts | 10 - packages/core/src/tools/load_description.ts | 5 - .../core/tests/checkpoint/checkpoint.test.ts | 1 - packages/core/tests/hooks/hooks.test.ts | 19 -- .../filesystem/copy_file-extended.test.ts | 192 ----------------- .../unit/tools/filesystem/copy_file.test.ts | 173 --------------- .../tools/filesystem/create_directory.test.ts | 156 -------------- .../unit/tools/filesystem/delete_file.test.ts | 173 --------------- .../unit/tools/filesystem/move_file.test.ts | 171 --------------- .../tools/filesystem/search_files.test.ts | 199 ------------------ .../tests/unit/tools/load_description.test.ts | 6 +- 34 files changed, 4 insertions(+), 1731 deletions(-) delete mode 100644 packages/core/src/tools/descriptions/filesystem/copy_file.txt delete mode 100644 packages/core/src/tools/descriptions/filesystem/create_directory.txt delete mode 100644 packages/core/src/tools/descriptions/filesystem/delete_file.txt delete mode 100644 packages/core/src/tools/descriptions/filesystem/move_file.txt delete mode 100644 packages/core/src/tools/descriptions/filesystem/search_files.txt delete mode 100644 packages/core/src/tools/filesystem/copy_file.ts delete mode 100644 packages/core/src/tools/filesystem/create_directory.ts delete mode 100644 packages/core/src/tools/filesystem/delete_file.ts delete mode 100644 packages/core/src/tools/filesystem/move_file.ts delete mode 100644 packages/core/src/tools/filesystem/search_files.ts delete mode 100644 packages/core/tests/unit/tools/filesystem/copy_file-extended.test.ts delete mode 100644 packages/core/tests/unit/tools/filesystem/copy_file.test.ts delete mode 100644 packages/core/tests/unit/tools/filesystem/create_directory.test.ts delete mode 100644 packages/core/tests/unit/tools/filesystem/delete_file.test.ts delete mode 100644 packages/core/tests/unit/tools/filesystem/move_file.test.ts delete mode 100644 packages/core/tests/unit/tools/filesystem/search_files.test.ts diff --git a/packages/core/src/agent/executor.ts b/packages/core/src/agent/executor.ts index ac729ad..706eeec 100644 --- a/packages/core/src/agent/executor.ts +++ b/packages/core/src/agent/executor.ts @@ -319,11 +319,10 @@ export class AgentExecutor { } // 文件写入权限检查 - if (['write_file', 'edit_file', 'delete_file'].includes(toolName)) { + if (['write_file', 'edit_file'].includes(toolName)) { const filePermission = permission.file; if (filePermission) { - const operation = toolName === 'write_file' ? 'write' : - toolName === 'edit_file' ? 'edit' : 'delete'; + const operation = toolName === 'write_file' ? 'write' : 'edit'; const action = filePermission[operation]; if (action === 'deny') { return { allowed: false, reason: `${operation} 操作被禁止` }; diff --git a/packages/core/src/agent/presets/build.ts b/packages/core/src/agent/presets/build.ts index 38d755f..7f3d8b0 100644 --- a/packages/core/src/agent/presets/build.ts +++ b/packages/core/src/agent/presets/build.ts @@ -159,14 +159,9 @@ export const buildAgent: Omit = { 'edit_file', 'multi_edit', 'list_directory', - 'create_directory', - 'search_files', 'glob', 'grep', 'get_file_info', - 'move_file', - 'copy_file', - 'delete_file', // ============ Shell ============ 'bash', diff --git a/packages/core/src/agent/presets/code-reviewer.ts b/packages/core/src/agent/presets/code-reviewer.ts index a7783ad..816ab13 100644 --- a/packages/core/src/agent/presets/code-reviewer.ts +++ b/packages/core/src/agent/presets/code-reviewer.ts @@ -48,7 +48,7 @@ export const codeReviewerAgent: Omit = { enabled: [ 'read_file', 'list_directory', - 'search_files', + 'glob', 'grep', 'git_status', 'git_diff', diff --git a/packages/core/src/agent/presets/explore.ts b/packages/core/src/agent/presets/explore.ts index abc7781..531db37 100644 --- a/packages/core/src/agent/presets/explore.ts +++ b/packages/core/src/agent/presets/explore.ts @@ -48,7 +48,6 @@ export const exploreAgent: Omit = { - **glob**: 文件模式匹配 (*.ts, src/**/*.tsx) - **grep**: 代码内容搜索 - **read_file**: 读取文件内容 -- **search_files**: 文件名搜索 - **list_directory**: 目录列表 - **bash**: 只读命令 (ls, tree, find, git log/status/diff) @@ -81,10 +80,6 @@ export const exploreAgent: Omit = { // 禁用所有写入操作 'write_file', 'edit_file', - 'delete_file', - 'move_file', - 'copy_file', - 'create_directory', 'multi_edit', // 禁用 Todo(由父 Agent 管理) 'todo_read', diff --git a/packages/core/src/agent/presets/guide.ts b/packages/core/src/agent/presets/guide.ts index 98a69ad..e47c4cf 100644 --- a/packages/core/src/agent/presets/guide.ts +++ b/packages/core/src/agent/presets/guide.ts @@ -78,10 +78,6 @@ export const guideAgent: Omit = { // 禁用所有写入操作 'write_file', 'edit_file', - 'delete_file', - 'move_file', - 'copy_file', - 'create_directory', 'multi_edit', // 禁用 Todo(由父 Agent 管理) 'todo_read', diff --git a/packages/core/src/agent/presets/plan.ts b/packages/core/src/agent/presets/plan.ts index 6ba1b2b..9481257 100644 --- a/packages/core/src/agent/presets/plan.ts +++ b/packages/core/src/agent/presets/plan.ts @@ -90,7 +90,6 @@ export const planAgent: Omit = { 'read_file', 'write_file', 'list_directory', - 'search_files', 'glob', 'grep', 'get_file_info', diff --git a/packages/core/src/checkpoint/checkpoint-store.ts b/packages/core/src/checkpoint/checkpoint-store.ts index 4c6ac68..ccfedbc 100644 --- a/packages/core/src/checkpoint/checkpoint-store.ts +++ b/packages/core/src/checkpoint/checkpoint-store.ts @@ -213,11 +213,6 @@ export class CheckpointStore { return autoCheckpoint.beforeWrite; case 'edit_file': return autoCheckpoint.beforeEdit; - case 'delete_file': - return autoCheckpoint.beforeDelete; - case 'move_file': - case 'copy_file': - return autoCheckpoint.beforeMove; case 'bash': return autoCheckpoint.beforeBash; default: @@ -234,12 +229,6 @@ export class CheckpointStore { return `Write file: ${params.file_path || params.path}`; case 'edit_file': return `Edit file: ${params.file_path || params.path}`; - case 'delete_file': - return `Delete file: ${params.file_path || params.path}`; - case 'move_file': - return `Move: ${params.source} -> ${params.destination}`; - case 'copy_file': - return `Copy: ${params.source} -> ${params.destination}`; case 'bash': return `Bash: ${String(params.command).slice(0, 50)}`; default: diff --git a/packages/core/src/checkpoint/commit-message.ts b/packages/core/src/checkpoint/commit-message.ts index 752a59d..aaf6acf 100644 --- a/packages/core/src/checkpoint/commit-message.ts +++ b/packages/core/src/checkpoint/commit-message.ts @@ -47,9 +47,6 @@ export class CommitMessageGenerator { manual: 'checkpoint', 'tool:write_file': 'write', 'tool:edit_file': 'edit', - 'tool:delete_file': 'delete', - 'tool:move_file': 'move', - 'tool:copy_file': 'copy', 'tool:bash': 'bash', task_start: 'session-start', task_complete: 'session-end', diff --git a/packages/core/src/checkpoint/manager.ts b/packages/core/src/checkpoint/manager.ts index 822113f..712a51e 100644 --- a/packages/core/src/checkpoint/manager.ts +++ b/packages/core/src/checkpoint/manager.ts @@ -63,8 +63,6 @@ export class CheckpointManager { autoCheckpoint: { beforeWrite: true, beforeEdit: true, - beforeDelete: true, - beforeMove: true, beforeBash: false, }, maxCheckpoints: 100, diff --git a/packages/core/src/checkpoint/safety.ts b/packages/core/src/checkpoint/safety.ts index 59fd133..4e5520e 100644 --- a/packages/core/src/checkpoint/safety.ts +++ b/packages/core/src/checkpoint/safety.ts @@ -175,9 +175,6 @@ export class CheckpointSafetyChecker { const aiTriggers = [ 'tool:write_file', 'tool:edit_file', - 'tool:delete_file', - 'tool:move_file', - 'tool:copy_file', 'tool:bash', 'task_start', 'task_complete', diff --git a/packages/core/src/checkpoint/types.ts b/packages/core/src/checkpoint/types.ts index 9996904..a5670e5 100644 --- a/packages/core/src/checkpoint/types.ts +++ b/packages/core/src/checkpoint/types.ts @@ -11,9 +11,6 @@ export type CheckpointTrigger = | 'manual' // 用户手动 | 'tool:write_file' // 写文件前 | 'tool:edit_file' // 编辑文件前 - | 'tool:delete_file' // 删除文件前 - | 'tool:move_file' // 移动文件前 - | 'tool:copy_file' // 复制文件前 | 'tool:bash' // bash 命令前 | 'task_start' // 任务开始 | 'task_complete' // 任务完成 @@ -64,10 +61,6 @@ export interface CheckpointConfig { beforeWrite: boolean; /** 编辑文件前创建检查点 */ beforeEdit: boolean; - /** 删除文件前创建检查点 */ - beforeDelete: boolean; - /** 移动/复制文件前创建检查点 */ - beforeMove: boolean; /** bash 命令前创建检查点 */ beforeBash: boolean; }; @@ -87,8 +80,6 @@ export const DEFAULT_CHECKPOINT_CONFIG: CheckpointConfig = { autoCheckpoint: { beforeWrite: true, beforeEdit: true, - beforeDelete: true, - beforeMove: true, beforeBash: false, }, maxCheckpoints: 100, diff --git a/packages/core/src/core/agent-tool-executor.ts b/packages/core/src/core/agent-tool-executor.ts index 28fe51a..117e957 100644 --- a/packages/core/src/core/agent-tool-executor.ts +++ b/packages/core/src/core/agent-tool-executor.ts @@ -346,15 +346,6 @@ export class AgentToolExecutor { if (gitManager) { await gitManager.onFileChanged(filePath, 'modify'); } - } else if (toolName === 'delete_file') { - await hookManager.triggerFileDeleted({ - path: filePath, - tool: toolName, - sessionId, - }); - if (gitManager) { - await gitManager.onFileChanged(filePath, 'delete'); - } } } diff --git a/packages/core/src/tools/descriptions/filesystem/copy_file.txt b/packages/core/src/tools/descriptions/filesystem/copy_file.txt deleted file mode 100644 index c6c30ad..0000000 --- a/packages/core/src/tools/descriptions/filesystem/copy_file.txt +++ /dev/null @@ -1 +0,0 @@ -复制文件或目录。支持递归复制整个目录结构。 \ No newline at end of file diff --git a/packages/core/src/tools/descriptions/filesystem/create_directory.txt b/packages/core/src/tools/descriptions/filesystem/create_directory.txt deleted file mode 100644 index 02648d0..0000000 --- a/packages/core/src/tools/descriptions/filesystem/create_directory.txt +++ /dev/null @@ -1 +0,0 @@ -创建新目录。支持递归创建父目录。如果目录已存在则不会报错。 \ No newline at end of file diff --git a/packages/core/src/tools/descriptions/filesystem/delete_file.txt b/packages/core/src/tools/descriptions/filesystem/delete_file.txt deleted file mode 100644 index 648f7f6..0000000 --- a/packages/core/src/tools/descriptions/filesystem/delete_file.txt +++ /dev/null @@ -1 +0,0 @@ -删除文件或目录。删除目录时可以选择是否递归删除。需要谨慎使用。 \ No newline at end of file diff --git a/packages/core/src/tools/descriptions/filesystem/move_file.txt b/packages/core/src/tools/descriptions/filesystem/move_file.txt deleted file mode 100644 index 6eae352..0000000 --- a/packages/core/src/tools/descriptions/filesystem/move_file.txt +++ /dev/null @@ -1 +0,0 @@ -移动或重命名文件/目录。可以将文件移动到新位置或更改文件名。 \ No newline at end of file diff --git a/packages/core/src/tools/descriptions/filesystem/search_files.txt b/packages/core/src/tools/descriptions/filesystem/search_files.txt deleted file mode 100644 index 6b9c0c5..0000000 --- a/packages/core/src/tools/descriptions/filesystem/search_files.txt +++ /dev/null @@ -1 +0,0 @@ -在目录中搜索匹配模式的文件 \ No newline at end of file diff --git a/packages/core/src/tools/filesystem/copy_file.ts b/packages/core/src/tools/filesystem/copy_file.ts deleted file mode 100644 index 430a6f6..0000000 --- a/packages/core/src/tools/filesystem/copy_file.ts +++ /dev/null @@ -1,140 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import type { ToolResult } from '../../types/index.js'; -import type { ToolWithMetadata } from '../types.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: ToolWithMetadata = { - name: 'copy_file', - description: loadDescription('copy_file'), - metadata: { - name: 'copy_file', - category: 'filesystem', - description: '复制文件或目录', - keywords: ['copy', 'file', 'cp', 'duplicate', '复制', '文件', '拷贝'], - deferLoading: true, - }, - 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/packages/core/src/tools/filesystem/create_directory.ts b/packages/core/src/tools/filesystem/create_directory.ts deleted file mode 100644 index 43fd933..0000000 --- a/packages/core/src/tools/filesystem/create_directory.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import type { ToolResult } from '../../types/index.js'; -import type { ToolWithMetadata } from '../types.js'; -import { loadDescription } from '../load_description.js'; -import { getPermissionManager } from '../../permission/index.js'; - -export const createDirectoryTool: ToolWithMetadata = { - name: 'create_directory', - description: loadDescription('create_directory'), - metadata: { - name: 'create_directory', - category: 'filesystem', - description: '创建目录', - keywords: ['create', 'directory', 'mkdir', 'folder', 'new', '创建', '目录', '文件夹', '新建'], - deferLoading: true, - }, - 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/packages/core/src/tools/filesystem/delete_file.ts b/packages/core/src/tools/filesystem/delete_file.ts deleted file mode 100644 index b66283d..0000000 --- a/packages/core/src/tools/filesystem/delete_file.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import type { ToolResult } from '../../types/index.js'; -import type { ToolWithMetadata } from '../types.js'; -import { loadDescription } from '../load_description.js'; -import { getPermissionManager } from '../../permission/index.js'; - -export const deleteFileTool: ToolWithMetadata = { - name: 'delete_file', - description: loadDescription('delete_file'), - metadata: { - name: 'delete_file', - category: 'filesystem', - description: '删除文件或目录', - keywords: ['delete', 'remove', 'file', 'rm', '删除', '移除', '文件'], - deferLoading: true, - }, - 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/packages/core/src/tools/filesystem/index.ts b/packages/core/src/tools/filesystem/index.ts index 75a81a6..2b84a34 100644 --- a/packages/core/src/tools/filesystem/index.ts +++ b/packages/core/src/tools/filesystem/index.ts @@ -6,17 +6,10 @@ export { multiEditTool } from './multi_edit.js'; // 目录操作 export { listDirTool } from './list_directory.js'; -export { createDirectoryTool } from './create_directory.js'; // 搜索 -export { searchFilesTool } from './search_files.js'; export { globTool } from './glob.js'; export { grepTool } from './grep.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/packages/core/src/tools/filesystem/move_file.ts b/packages/core/src/tools/filesystem/move_file.ts deleted file mode 100644 index eb6ea33..0000000 --- a/packages/core/src/tools/filesystem/move_file.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import type { ToolResult } from '../../types/index.js'; -import type { ToolWithMetadata } from '../types.js'; -import { loadDescription } from '../load_description.js'; -import { getPermissionManager } from '../../permission/index.js'; - -export const moveFileTool: ToolWithMetadata = { - name: 'move_file', - description: loadDescription('move_file'), - metadata: { - name: 'move_file', - category: 'filesystem', - description: '移动或重命名文件/目录', - keywords: ['move', 'rename', 'file', 'mv', '移动', '重命名', '文件'], - deferLoading: true, - }, - 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/packages/core/src/tools/filesystem/search_files.ts b/packages/core/src/tools/filesystem/search_files.ts deleted file mode 100644 index 5e21215..0000000 --- a/packages/core/src/tools/filesystem/search_files.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import type { ToolResult } from '../../types/index.js'; -import type { ToolWithMetadata } from '../types.js'; -import { loadDescription } from '../load_description.js'; -import { getPermissionManager } from '../../permission/index.js'; - -export const searchFilesTool: ToolWithMetadata = { - name: 'search_files', - description: loadDescription('search_files'), - metadata: { - name: 'search_files', - category: 'filesystem', - description: '按文件名搜索文件', - keywords: ['search', 'file', 'find', 'glob', 'pattern', '搜索', '文件', '查找', '匹配'], - deferLoading: true, - }, - parameters: { - directory: { - type: 'string', - description: '搜索的起始目录', - required: true, - }, - pattern: { - type: 'string', - description: '文件名匹配模式(支持 glob 模式,如 *.ts)', - required: true, - }, - }, - execute: async (params: Record): Promise => { - const directory = params.directory as string; - const pattern = params.pattern as string; - const 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 { - if (depth > 10) return; - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.name.startsWith('.') || entry.name === 'node_modules') { - continue; - } - - if (entry.isDirectory()) { - await searchRecursive(fullPath, depth + 1); - } else if (regex.test(entry.name)) { - matches.push(fullPath); - } - } - } catch { - // 忽略权限错误 - } - } - - try { - await searchRecursive(absolutePath); - return { - success: true, - output: - matches.length > 0 - ? matches.join('\n') - : '没有找到匹配的文件', - }; - } catch (error) { - return { - success: false, - output: '', - error: error instanceof Error ? error.message : String(error), - }; - } - }, -}; diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 1407943..782e9c5 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -21,14 +21,9 @@ import { editFileTool, multiEditTool, listDirTool, - createDirectoryTool, - searchFilesTool, globTool, grepTool, getFileInfoTool, - moveFileTool, - copyFileTool, - deleteFileTool, } from './filesystem/index.js'; // Web 工具 @@ -87,14 +82,9 @@ const allToolsWithMetadata: ToolWithMetadata[] = [ editFileTool, multiEditTool, listDirTool, - createDirectoryTool, - searchFilesTool, globTool, grepTool, getFileInfoTool, - moveFileTool, - copyFileTool, - deleteFileTool, // Web 工具 (deferLoading: false) webSearchTool, diff --git a/packages/core/src/tools/load_description.ts b/packages/core/src/tools/load_description.ts index 1250946..5a93d3a 100644 --- a/packages/core/src/tools/load_description.ts +++ b/packages/core/src/tools/load_description.ts @@ -15,14 +15,9 @@ const TOOL_CATEGORY_MAP: Record = { edit_file: 'filesystem', multi_edit: 'filesystem', list_directory: 'filesystem', - create_directory: 'filesystem', - search_files: 'filesystem', glob: 'filesystem', grep: 'filesystem', get_file_info: 'filesystem', - move_file: 'filesystem', - copy_file: 'filesystem', - delete_file: 'filesystem', // web web_search: 'web', web_extract: 'web', diff --git a/packages/core/tests/checkpoint/checkpoint.test.ts b/packages/core/tests/checkpoint/checkpoint.test.ts index 6ccbfc7..2776f53 100644 --- a/packages/core/tests/checkpoint/checkpoint.test.ts +++ b/packages/core/tests/checkpoint/checkpoint.test.ts @@ -218,7 +218,6 @@ describe('CheckpointManager', () => { it('should determine if checkpoint should be created for tool', () => { expect(manager.shouldCreateCheckpoint('write_file')).toBe(true); expect(manager.shouldCreateCheckpoint('edit_file')).toBe(true); - expect(manager.shouldCreateCheckpoint('delete_file')).toBe(true); expect(manager.shouldCreateCheckpoint('bash')).toBe(false); // 默认禁用 expect(manager.shouldCreateCheckpoint('read_file')).toBe(false); }); diff --git a/packages/core/tests/hooks/hooks.test.ts b/packages/core/tests/hooks/hooks.test.ts index fd9e47d..69d80e2 100644 --- a/packages/core/tests/hooks/hooks.test.ts +++ b/packages/core/tests/hooks/hooks.test.ts @@ -328,25 +328,6 @@ describe('HookManager', () => { expect(triggered).toBe(true); }); - it('should trigger file.deleted hook', async () => { - let triggered = false; - const hooks: Hooks = { - 'file.deleted': async (input, output) => { - triggered = true; - expect(input.path).toBe('/test/old-file.ts'); - }, - }; - - manager.registerHooks(hooks); - - await manager.triggerFileDeleted({ - path: '/test/old-file.ts', - tool: 'delete_file', - sessionId: 'test-session', - }); - - expect(triggered).toBe(true); - }); }); describe('Config Hooks', () => { diff --git a/packages/core/tests/unit/tools/filesystem/copy_file-extended.test.ts b/packages/core/tests/unit/tools/filesystem/copy_file-extended.test.ts deleted file mode 100644 index 35f395c..0000000 --- a/packages/core/tests/unit/tools/filesystem/copy_file-extended.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock fs/promises -vi.mock('fs/promises', () => ({ - stat: vi.fn(), - copyFile: vi.fn().mockResolvedValue(undefined), - mkdir: vi.fn().mockResolvedValue(undefined), - readdir: vi.fn().mockResolvedValue([]), -})); - -// 可变状态 -let mockCheckResults: Array<{ - allowed: boolean; - action?: string; - reason?: string; - needsConfirmation?: boolean; -}> = []; -let checkCallIndex = 0; - -// Mock permission manager -vi.mock('../../../../src/permission/index.js', () => ({ - getPermissionManager: () => ({ - checkFilePermission: vi.fn(async () => { - const result = mockCheckResults[checkCallIndex] || { allowed: true }; - checkCallIndex++; - return result; - }), - }), -})); - -// Mock loadDescription -vi.mock('../../../../src/tools/load_description.js', () => ({ - loadDescription: vi.fn(() => '复制文件'), -})); - -import { copyFileTool } from '../../../../src/tools/filesystem/copy_file.js'; -import * as fs from 'fs/promises'; - -describe('copyFileTool - 扩展测试', () => { - beforeEach(() => { - vi.clearAllMocks(); - mockCheckResults = [{ allowed: true }, { allowed: true }]; - checkCallIndex = 0; - // 重置 mock 默认值 - vi.mocked(fs.stat).mockReset(); - vi.mocked(fs.copyFile).mockReset().mockResolvedValue(undefined); - vi.mocked(fs.mkdir).mockReset().mockResolvedValue(undefined); - vi.mocked(fs.readdir).mockReset().mockResolvedValue([]); - }); - - describe('递归复制目录', () => { - it('递归复制包含文件的目录', async () => { - vi.mocked(fs.stat) - .mockResolvedValueOnce({ isDirectory: () => true } as any) - .mockRejectedValueOnce(new Error('ENOENT')) - .mockResolvedValueOnce({ isDirectory: () => false } as any); - - vi.mocked(fs.readdir).mockResolvedValueOnce(['file1.txt'] as any); - - const result = await copyFileTool.execute({ - source: 'src_dir', - destination: 'dest_dir', - }); - - expect(result.success).toBe(true); - expect(fs.mkdir).toHaveBeenCalled(); - expect(fs.copyFile).toHaveBeenCalled(); - }); - - it('递归复制包含子目录的目录', async () => { - // 调用顺序: - // 1. execute: stat(source) - 检查源是否存在 - // 2. execute: stat(dest) - 检查目标(ENOENT) - // 3. copyRecursive: stat(source) - 判断是否是目录 - // 4. copyRecursive: stat(source/subdir) - 递归判断子目录 - vi.mocked(fs.stat) - .mockResolvedValueOnce({ isDirectory: () => true } as any) // source 存在且是目录 - .mockRejectedValueOnce(new Error('ENOENT')) // dest 不存在 - .mockResolvedValueOnce({ isDirectory: () => true } as any) // copyRecursive(source) - .mockResolvedValueOnce({ isDirectory: () => true } as any); // copyRecursive(source/subdir) - - vi.mocked(fs.readdir) - .mockResolvedValueOnce(['subdir'] as any) // 第一层目录 - .mockResolvedValueOnce([] as any); // 子目录为空 - - const result = await copyFileTool.execute({ - source: 'src_dir', - destination: 'dest_dir', - }); - - expect(result.success).toBe(true); - expect(fs.mkdir).toHaveBeenCalled(); - }); - }); - - describe('目标位置权限', () => { - it('目标位置需要确认时返回错误', async () => { - mockCheckResults = [ - { allowed: true }, - { allowed: false, action: 'ask', needsConfirmation: true, reason: '首次复制到此位置' }, - ]; - - vi.mocked(fs.stat).mockResolvedValueOnce({ isDirectory: () => false } as any); - - const result = await copyFileTool.execute({ - source: 'src.txt', - destination: '/new/location/dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要用户确认'); - expect(result.error).toContain('首次复制到此位置'); - }); - }); - - describe('绝对路径处理', () => { - it('源和目标都是绝对路径', async () => { - vi.mocked(fs.stat) - .mockResolvedValueOnce({ isDirectory: () => false } as any) - .mockRejectedValueOnce(new Error('ENOENT')); - - const result = await copyFileTool.execute({ - source: '/absolute/source/file.txt', - destination: '/absolute/destination/file.txt', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('/absolute/source/file.txt'); - expect(result.output).toContain('/absolute/destination/file.txt'); - }); - }); - - describe('权限检查细节', () => { - it('源文件权限被拒绝(无原因)', async () => { - mockCheckResults = [ - { allowed: false, action: 'deny' }, - ]; - - const result = await copyFileTool.execute({ - source: 'src.txt', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('不允许读取此文件'); - }); - - it('源文件需要确认(无原因)', async () => { - mockCheckResults = [ - { allowed: false, action: 'ask', needsConfirmation: true }, - ]; - - const result = await copyFileTool.execute({ - source: 'src.txt', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要权限确认'); - }); - - it('目标权限被拒绝(无原因)', async () => { - mockCheckResults = [ - { allowed: true }, - { allowed: false, action: 'deny' }, - ]; - - const result = await copyFileTool.execute({ - source: 'src.txt', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('不允许复制到此位置'); - }); - - it('目标需要确认(无原因)', async () => { - mockCheckResults = [ - { allowed: true }, - { allowed: false, action: 'ask', needsConfirmation: true }, - ]; - - const result = await copyFileTool.execute({ - source: 'src.txt', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要权限确认'); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/filesystem/copy_file.test.ts b/packages/core/tests/unit/tools/filesystem/copy_file.test.ts deleted file mode 100644 index d2107f2..0000000 --- a/packages/core/tests/unit/tools/filesystem/copy_file.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock fs/promises -vi.mock('fs/promises', () => ({ - stat: vi.fn(), - copyFile: vi.fn().mockResolvedValue(undefined), - mkdir: vi.fn().mockResolvedValue(undefined), - readdir: vi.fn().mockResolvedValue([]), -})); - -// Mock permission manager -vi.mock('../../../../src/permission/index.js', () => ({ - getPermissionManager: vi.fn(() => ({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: true, - action: 'allow', - }), - })), -})); - -// Mock loadDescription -vi.mock('../../../../src/tools/load_description.js', () => ({ - loadDescription: vi.fn(() => '复制文件'), -})); - -import { copyFileTool } from '../../../../src/tools/filesystem/copy_file.js'; -import * as fs from 'fs/promises'; -import { getPermissionManager } from '../../../../src/permission/index.js'; - -describe('copyFileTool - 文件复制工具', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(fs.stat).mockResolvedValue({ - isDirectory: () => false, - isFile: () => true, - } as any); - }); - - describe('工具定义', () => { - it('有正确的名称', () => { - expect(copyFileTool.name).toBe('copy_file'); - }); - - it('有正确的元数据', () => { - expect(copyFileTool.metadata.category).toBe('filesystem'); - expect(copyFileTool.metadata.keywords).toContain('copy'); - expect(copyFileTool.metadata.keywords).toContain('cp'); - }); - - it('定义了必需参数', () => { - expect(copyFileTool.parameters.source.required).toBe(true); - expect(copyFileTool.parameters.destination.required).toBe(true); - }); - }); - - describe('execute - 执行', () => { - it('成功复制文件', async () => { - // 第一次调用检查源文件,第二次调用检查目标是否是目录 - vi.mocked(fs.stat) - .mockResolvedValueOnce({ isDirectory: () => false } as any) - .mockRejectedValueOnce(new Error('ENOENT')); // 目标不存在 - - const result = await copyFileTool.execute({ - source: 'src.txt', - destination: 'dest.txt', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('已复制'); - expect(fs.copyFile).toHaveBeenCalled(); - }); - - it('复制到已存在的目录', async () => { - vi.mocked(fs.stat) - .mockResolvedValueOnce({ isDirectory: () => false } as any) // 源文件 - .mockResolvedValueOnce({ isDirectory: () => true } as any); // 目标是目录 - - const result = await copyFileTool.execute({ - source: 'file.txt', - destination: '/target/dir', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('file.txt'); - }); - - it('递归复制目录', async () => { - vi.mocked(fs.stat) - .mockResolvedValueOnce({ isDirectory: () => true } as any) // 源是目录 - .mockRejectedValueOnce(new Error('ENOENT')); // 目标不存在 - - vi.mocked(fs.readdir).mockResolvedValueOnce([]); - - const result = await copyFileTool.execute({ - source: 'src_dir', - destination: 'dest_dir', - }); - - expect(result.success).toBe(true); - expect(fs.mkdir).toHaveBeenCalled(); - }); - - it('源文件读取权限被拒绝', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'deny', - reason: '不允许读取', - }), - } as any); - - const result = await copyFileTool.execute({ - source: '/etc/passwd', - destination: 'copy.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('权限被拒绝'); - }); - - it('目标位置写入权限被拒绝', async () => { - const mockCheck = vi.fn() - .mockResolvedValueOnce({ allowed: true }) // 读取权限 - .mockResolvedValueOnce({ allowed: false, action: 'deny', reason: '不允许写入' }); // 复制权限 - - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: mockCheck, - } as any); - - const result = await copyFileTool.execute({ - source: 'src.txt', - destination: '/protected/dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('权限被拒绝'); - }); - - it('源文件需要确认', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'ask', - needsConfirmation: true, - }), - } as any); - - const result = await copyFileTool.execute({ - source: '/sensitive/file', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要用户确认'); - }); - - it('源文件不存在返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }), - } as any); - - vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT: no such file')); - - const result = await copyFileTool.execute({ - source: 'nonexistent.txt', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('ENOENT'); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/filesystem/create_directory.test.ts b/packages/core/tests/unit/tools/filesystem/create_directory.test.ts deleted file mode 100644 index 9e45cba..0000000 --- a/packages/core/tests/unit/tools/filesystem/create_directory.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock fs/promises -vi.mock('fs/promises', () => ({ - stat: vi.fn(), - mkdir: vi.fn().mockResolvedValue(undefined), -})); - -// Mock permission manager -vi.mock('../../../../src/permission/index.js', () => ({ - getPermissionManager: vi.fn(() => ({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: true, - action: 'allow', - }), - })), -})); - -// Mock loadDescription -vi.mock('../../../../src/tools/load_description.js', () => ({ - loadDescription: vi.fn(() => '创建目录'), -})); - -import { createDirectoryTool } from '../../../../src/tools/filesystem/create_directory.js'; -import * as fs from 'fs/promises'; -import { getPermissionManager } from '../../../../src/permission/index.js'; - -describe('createDirectoryTool - 创建目录工具', () => { - beforeEach(() => { - vi.clearAllMocks(); - // 默认目录不存在 - vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT')); - }); - - describe('工具定义', () => { - it('有正确的名称', () => { - expect(createDirectoryTool.name).toBe('create_directory'); - }); - - it('有正确的元数据', () => { - expect(createDirectoryTool.metadata.category).toBe('filesystem'); - expect(createDirectoryTool.metadata.keywords).toContain('create'); - expect(createDirectoryTool.metadata.keywords).toContain('directory'); - expect(createDirectoryTool.metadata.keywords).toContain('mkdir'); - }); - - it('定义了必需的 path 参数', () => { - expect(createDirectoryTool.parameters.path.required).toBe(true); - }); - }); - - describe('execute - 执行', () => { - it('成功创建目录', async () => { - const result = await createDirectoryTool.execute({ path: 'new_dir' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('已创建目录'); - expect(fs.mkdir).toHaveBeenCalledWith( - expect.any(String), - { recursive: true } - ); - }); - - it('目录已存在返回成功', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - isDirectory: () => true, - } as any); - - const result = await createDirectoryTool.execute({ path: 'existing_dir' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('目录已存在'); - expect(fs.mkdir).not.toHaveBeenCalled(); - }); - - it('路径是文件返回错误', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - isDirectory: () => false, - } as any); - - const result = await createDirectoryTool.execute({ path: 'file.txt' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('路径已存在且不是目录'); - }); - - it('权限被拒绝时返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'deny', - reason: '不允许创建目录', - }), - } as any); - - const result = await createDirectoryTool.execute({ path: '/protected/dir' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('权限被拒绝'); - }); - - it('需要确认时返回提示', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'ask', - needsConfirmation: true, - }), - } as any); - - const result = await createDirectoryTool.execute({ path: 'new_dir' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要用户确认'); - }); - - it('创建嵌套目录', async () => { - // 确保权限检查通过 - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }), - } as any); - - const result = await createDirectoryTool.execute({ path: 'a/b/c/d' }); - - expect(result.success).toBe(true); - expect(fs.mkdir).toHaveBeenCalledWith( - expect.any(String), - { recursive: true } - ); - }); - - it('传递正确参数给权限检查', async () => { - const mockCheck = vi.fn().mockResolvedValue({ allowed: true }); - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: mockCheck, - } as any); - - await createDirectoryTool.execute({ path: 'test_dir' }); - - expect(mockCheck).toHaveBeenCalledWith( - expect.objectContaining({ - operation: 'mkdir', - }) - ); - }); - - it('处理创建错误', async () => { - vi.mocked(fs.mkdir).mockRejectedValue(new Error('Permission denied')); - - const result = await createDirectoryTool.execute({ path: 'new_dir' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('Permission denied'); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/filesystem/delete_file.test.ts b/packages/core/tests/unit/tools/filesystem/delete_file.test.ts deleted file mode 100644 index 5121426..0000000 --- a/packages/core/tests/unit/tools/filesystem/delete_file.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock fs/promises -vi.mock('fs/promises', () => ({ - stat: vi.fn(), - unlink: vi.fn().mockResolvedValue(undefined), - rmdir: vi.fn().mockResolvedValue(undefined), - rm: vi.fn().mockResolvedValue(undefined), - readdir: vi.fn().mockResolvedValue([]), -})); - -// Mock permission manager -vi.mock('../../../../src/permission/index.js', () => ({ - getPermissionManager: vi.fn(() => ({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: true, - action: 'allow', - }), - })), -})); - -// Mock loadDescription -vi.mock('../../../../src/tools/load_description.js', () => ({ - loadDescription: vi.fn(() => '删除文件'), -})); - -import { deleteFileTool } from '../../../../src/tools/filesystem/delete_file.js'; -import * as fs from 'fs/promises'; -import { getPermissionManager } from '../../../../src/permission/index.js'; - -describe('deleteFileTool - 文件删除工具', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('工具定义', () => { - it('有正确的名称', () => { - expect(deleteFileTool.name).toBe('delete_file'); - }); - - it('有正确的元数据', () => { - expect(deleteFileTool.metadata.category).toBe('filesystem'); - expect(deleteFileTool.metadata.keywords).toContain('delete'); - expect(deleteFileTool.metadata.keywords).toContain('remove'); - expect(deleteFileTool.metadata.keywords).toContain('rm'); - }); - - it('定义了必需的 path 参数', () => { - expect(deleteFileTool.parameters.path.required).toBe(true); - }); - - it('定义了可选的 recursive 参数', () => { - expect(deleteFileTool.parameters.recursive.required).toBe(false); - }); - }); - - describe('execute - 执行', () => { - it('成功删除文件', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - isDirectory: () => false, - } as any); - - const result = await deleteFileTool.execute({ path: 'file.txt' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('已删除文件'); - expect(fs.unlink).toHaveBeenCalled(); - }); - - it('删除空目录', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - isDirectory: () => true, - } as any); - vi.mocked(fs.readdir).mockResolvedValue([]); - - const result = await deleteFileTool.execute({ path: 'empty_dir' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('已删除目录'); - expect(fs.rmdir).toHaveBeenCalled(); - }); - - it('非空目录无 recursive 返回错误', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - isDirectory: () => true, - } as any); - vi.mocked(fs.readdir).mockResolvedValue(['file1.txt', 'file2.txt'] as any); - - const result = await deleteFileTool.execute({ path: 'nonempty_dir' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('目录不为空'); - expect(result.error).toContain('recursive: true'); - }); - - it('递归删除非空目录', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - isDirectory: () => true, - } as any); - - const result = await deleteFileTool.execute({ - path: 'nonempty_dir', - recursive: true, - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('已删除目录'); - expect(fs.rm).toHaveBeenCalledWith( - expect.any(String), - { recursive: true } - ); - }); - - it('权限被拒绝时返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'deny', - reason: '不允许删除', - }), - } as any); - - const result = await deleteFileTool.execute({ path: '/protected/file' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('权限被拒绝'); - }); - - it('需要确认时返回提示', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'ask', - needsConfirmation: true, - }), - } as any); - - const result = await deleteFileTool.execute({ path: 'important.txt' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要用户确认'); - }); - - it('文件不存在返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }), - } as any); - - vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT')); - - const result = await deleteFileTool.execute({ path: 'nonexistent.txt' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('ENOENT'); - }); - - it('传递正确参数给权限检查', async () => { - const mockCheck = vi.fn().mockResolvedValue({ allowed: true }); - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: mockCheck, - } as any); - vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => false } as any); - - await deleteFileTool.execute({ path: 'test.txt' }); - - expect(mockCheck).toHaveBeenCalledWith( - expect.objectContaining({ - operation: 'delete', - }) - ); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/filesystem/move_file.test.ts b/packages/core/tests/unit/tools/filesystem/move_file.test.ts deleted file mode 100644 index 8561442..0000000 --- a/packages/core/tests/unit/tools/filesystem/move_file.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock fs/promises -vi.mock('fs/promises', () => ({ - access: vi.fn().mockResolvedValue(undefined), - stat: vi.fn(), - rename: vi.fn().mockResolvedValue(undefined), - mkdir: vi.fn().mockResolvedValue(undefined), -})); - -// Mock permission manager -vi.mock('../../../../src/permission/index.js', () => ({ - getPermissionManager: vi.fn(() => ({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: true, - action: 'allow', - }), - })), -})); - -// Mock loadDescription -vi.mock('../../../../src/tools/load_description.js', () => ({ - loadDescription: vi.fn(() => '移动文件'), -})); - -import { moveFileTool } from '../../../../src/tools/filesystem/move_file.js'; -import * as fs from 'fs/promises'; -import { getPermissionManager } from '../../../../src/permission/index.js'; - -describe('moveFileTool - 文件移动工具', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('工具定义', () => { - it('有正确的名称', () => { - expect(moveFileTool.name).toBe('move_file'); - }); - - it('有正确的元数据', () => { - expect(moveFileTool.metadata.category).toBe('filesystem'); - expect(moveFileTool.metadata.keywords).toContain('move'); - expect(moveFileTool.metadata.keywords).toContain('rename'); - expect(moveFileTool.metadata.keywords).toContain('mv'); - }); - - it('定义了必需参数', () => { - expect(moveFileTool.parameters.source.required).toBe(true); - expect(moveFileTool.parameters.destination.required).toBe(true); - }); - }); - - describe('execute - 执行', () => { - it('成功移动文件', async () => { - vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT')); // 目标不存在 - - const result = await moveFileTool.execute({ - source: 'old.txt', - destination: 'new.txt', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('已移动'); - expect(fs.rename).toHaveBeenCalled(); - }); - - it('移动到已存在的目录', async () => { - vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as any); - - const result = await moveFileTool.execute({ - source: 'file.txt', - destination: '/target/dir', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('file.txt'); - }); - - it('源文件移动权限被拒绝', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'deny', - reason: '不允许移动', - }), - } as any); - - const result = await moveFileTool.execute({ - source: '/protected/file', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('权限被拒绝'); - }); - - it('目标位置写入权限被拒绝', async () => { - const mockCheck = vi.fn() - .mockResolvedValueOnce({ allowed: true }) // 移动权限 - .mockResolvedValueOnce({ allowed: false, action: 'deny', reason: '不允许写入' }); // 写入权限 - - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: mockCheck, - } as any); - - const result = await moveFileTool.execute({ - source: 'src.txt', - destination: '/protected/dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('权限被拒绝'); - }); - - it('需要确认时返回提示', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'ask', - needsConfirmation: true, - }), - } as any); - - const result = await moveFileTool.execute({ - source: 'file.txt', - destination: 'new.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要用户确认'); - }); - - it('源文件不存在返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }), - } as any); - - vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT')); - - const result = await moveFileTool.execute({ - source: 'nonexistent.txt', - destination: 'dest.txt', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('ENOENT'); - }); - - it('创建目标目录', async () => { - // 确保权限检查通过 - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }), - } as any); - // 源文件存在 - vi.mocked(fs.access).mockResolvedValue(undefined); - // 目标不存在 - vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT')); - - const result = await moveFileTool.execute({ - source: 'file.txt', - destination: '/new/path/file.txt', - }); - - expect(result.success).toBe(true); - expect(fs.mkdir).toHaveBeenCalledWith( - expect.any(String), - { recursive: true } - ); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/filesystem/search_files.test.ts b/packages/core/tests/unit/tools/filesystem/search_files.test.ts deleted file mode 100644 index 7180f64..0000000 --- a/packages/core/tests/unit/tools/filesystem/search_files.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock fs/promises -vi.mock('fs/promises', () => ({ - readdir: vi.fn(), -})); - -// Mock permission manager -vi.mock('../../../../src/permission/index.js', () => ({ - getPermissionManager: vi.fn(() => ({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: true, - action: 'allow', - }), - })), -})); - -// Mock loadDescription -vi.mock('../../../../src/tools/load_description.js', () => ({ - loadDescription: vi.fn(() => '按文件名搜索文件'), -})); - -import { searchFilesTool } from '../../../../src/tools/filesystem/search_files.js'; -import * as fs from 'fs/promises'; -import { getPermissionManager } from '../../../../src/permission/index.js'; - -describe('searchFilesTool - 文件搜索工具', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('工具定义', () => { - it('有正确的名称', () => { - expect(searchFilesTool.name).toBe('search_files'); - }); - - it('有正确的元数据', () => { - expect(searchFilesTool.metadata.category).toBe('filesystem'); - expect(searchFilesTool.metadata.keywords).toContain('search'); - expect(searchFilesTool.metadata.keywords).toContain('find'); - expect(searchFilesTool.metadata.keywords).toContain('glob'); - }); - - it('定义了必需参数', () => { - expect(searchFilesTool.parameters.directory.required).toBe(true); - expect(searchFilesTool.parameters.pattern.required).toBe(true); - }); - }); - - describe('execute - 执行', () => { - it('成功搜索并返回匹配文件', async () => { - vi.mocked(fs.readdir).mockResolvedValue([ - { name: 'test.ts', isDirectory: () => false, isFile: () => true }, - { name: 'test.js', isDirectory: () => false, isFile: () => true }, - ] as any); - - const result = await searchFilesTool.execute({ - directory: '.', - pattern: '*.ts', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('test.ts'); - expect(result.output).not.toContain('test.js'); - }); - - it('没有匹配时返回提示', async () => { - vi.mocked(fs.readdir).mockResolvedValue([ - { name: 'test.js', isDirectory: () => false, isFile: () => true }, - ] as any); - - const result = await searchFilesTool.execute({ - directory: '.', - pattern: '*.tsx', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('没有找到匹配的文件'); - }); - - it('递归搜索子目录', async () => { - vi.mocked(fs.readdir) - .mockResolvedValueOnce([ - { name: 'src', isDirectory: () => true, isFile: () => false }, - { name: 'index.ts', isDirectory: () => false, isFile: () => true }, - ] as any) - .mockResolvedValueOnce([ - { name: 'app.ts', isDirectory: () => false, isFile: () => true }, - ] as any); - - const result = await searchFilesTool.execute({ - directory: '.', - pattern: '*.ts', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('index.ts'); - expect(result.output).toContain('app.ts'); - }); - - it('跳过隐藏文件和 node_modules', async () => { - // 第一次调用返回根目录内容,第二次调用返回 src 目录内容 - vi.mocked(fs.readdir) - .mockResolvedValueOnce([ - { name: '.git', isDirectory: () => true, isFile: () => false }, - { name: 'node_modules', isDirectory: () => true, isFile: () => false }, - { name: 'src', isDirectory: () => true, isFile: () => false }, - ] as any) - .mockResolvedValueOnce([ - { name: 'index.ts', isDirectory: () => false, isFile: () => true }, - ] as any); - - await searchFilesTool.execute({ - directory: '.', - pattern: '*', - }); - - // 不应该进入隐藏目录或 node_modules,只进入 src - expect(fs.readdir).toHaveBeenCalledTimes(2); // 根目录 + src - }); - - it('权限被拒绝时返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'deny', - reason: '不允许搜索', - }), - } as any); - - const result = await searchFilesTool.execute({ - directory: '/protected', - pattern: '*', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('权限被拒绝'); - }); - - it('需要确认时返回提示', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'ask', - needsConfirmation: true, - }), - } as any); - - const result = await searchFilesTool.execute({ - directory: '.', - pattern: '*', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要用户确认'); - }); - - it('支持 glob 模式匹配', async () => { - // 恢复权限检查 - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }), - } as any); - vi.mocked(fs.readdir).mockResolvedValue([ - { name: 'component.tsx', isDirectory: () => false, isFile: () => true }, - { name: 'helper.ts', isDirectory: () => false, isFile: () => true }, - { name: 'style.css', isDirectory: () => false, isFile: () => true }, - ] as any); - - // *.tsx 模式会匹配 component.tsx - const result = await searchFilesTool.execute({ - directory: '.', - pattern: '*.tsx', - }); - - expect(result.success).toBe(true); - expect(result.output).toContain('component.tsx'); - expect(result.output).not.toContain('style.css'); - }); - - it('传递正确参数给权限检查', async () => { - const mockCheck = vi.fn().mockResolvedValue({ allowed: true }); - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: mockCheck, - } as any); - vi.mocked(fs.readdir).mockResolvedValue([]); - - await searchFilesTool.execute({ - directory: 'src', - pattern: '*.ts', - }); - - expect(mockCheck).toHaveBeenCalledWith( - expect.objectContaining({ - operation: 'search', - }) - ); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/load_description.test.ts b/packages/core/tests/unit/tools/load_description.test.ts index 1d2403d..05ed5aa 100644 --- a/packages/core/tests/unit/tools/load_description.test.ts +++ b/packages/core/tests/unit/tools/load_description.test.ts @@ -165,13 +165,9 @@ describe('loadDescription', () => { { tool: 'write_file', category: 'filesystem' }, { tool: 'edit_file', category: 'filesystem' }, { tool: 'list_directory', category: 'filesystem' }, - { tool: 'create_directory', category: 'filesystem' }, - { tool: 'search_files', category: 'filesystem' }, + { tool: 'glob', category: 'filesystem' }, { tool: 'grep', category: 'filesystem' }, { tool: 'get_file_info', category: 'filesystem' }, - { tool: 'move_file', category: 'filesystem' }, - { tool: 'copy_file', category: 'filesystem' }, - { tool: 'delete_file', category: 'filesystem' }, // web { tool: 'web_search', category: 'web' }, { tool: 'web_extract', category: 'web' },