diff --git a/packages/core/src/agent/presets/build.ts b/packages/core/src/agent/presets/build.ts index 7f3d8b0..0ea82ab 100644 --- a/packages/core/src/agent/presets/build.ts +++ b/packages/core/src/agent/presets/build.ts @@ -158,10 +158,8 @@ export const buildAgent: Omit = { 'write_file', 'edit_file', 'multi_edit', - 'list_directory', 'glob', 'grep', - 'get_file_info', // ============ Shell ============ 'bash', diff --git a/packages/core/src/agent/presets/code-reviewer.ts b/packages/core/src/agent/presets/code-reviewer.ts index 816ab13..eaecca9 100644 --- a/packages/core/src/agent/presets/code-reviewer.ts +++ b/packages/core/src/agent/presets/code-reviewer.ts @@ -47,7 +47,6 @@ export const codeReviewerAgent: Omit = { tools: { enabled: [ 'read_file', - 'list_directory', 'glob', 'grep', 'git_status', diff --git a/packages/core/src/agent/presets/explore.ts b/packages/core/src/agent/presets/explore.ts index 531db37..9439fdb 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**: 读取文件内容 -- **list_directory**: 目录列表 - **bash**: 只读命令 (ls, tree, find, git log/status/diff) ## 工作原则 diff --git a/packages/core/src/agent/presets/plan.ts b/packages/core/src/agent/presets/plan.ts index 9481257..7c969ec 100644 --- a/packages/core/src/agent/presets/plan.ts +++ b/packages/core/src/agent/presets/plan.ts @@ -89,10 +89,8 @@ export const planAgent: Omit = { // 文件操作,限制只能写入 plan 目录 'read_file', 'write_file', - 'list_directory', 'glob', 'grep', - 'get_file_info', // Git 只读 'git_status', 'git_diff', diff --git a/packages/core/src/tools/descriptions/filesystem/get_file_info.txt b/packages/core/src/tools/descriptions/filesystem/get_file_info.txt deleted file mode 100644 index 653661f..0000000 --- a/packages/core/src/tools/descriptions/filesystem/get_file_info.txt +++ /dev/null @@ -1 +0,0 @@ -获取文件或目录的详细信息,包括大小、权限、创建时间、修改时间等元数据。 \ No newline at end of file diff --git a/packages/core/src/tools/descriptions/filesystem/list_directory.txt b/packages/core/src/tools/descriptions/filesystem/list_directory.txt deleted file mode 100644 index 573b9e6..0000000 --- a/packages/core/src/tools/descriptions/filesystem/list_directory.txt +++ /dev/null @@ -1 +0,0 @@ -列出指定目录下的文件和文件夹 \ No newline at end of file diff --git a/packages/core/src/tools/filesystem/get_file_info.ts b/packages/core/src/tools/filesystem/get_file_info.ts deleted file mode 100644 index 6b97c7e..0000000 --- a/packages/core/src/tools/filesystem/get_file_info.ts +++ /dev/null @@ -1,142 +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'; - -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: ToolWithMetadata = { - name: 'get_file_info', - description: loadDescription('get_file_info'), - metadata: { - name: 'get_file_info', - category: 'filesystem', - description: '获取文件元信息', - keywords: ['file', 'info', 'stat', 'size', 'permission', 'metadata', '文件', '信息', '大小', '权限', '属性'], - deferLoading: true, - }, - 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/packages/core/src/tools/filesystem/index.ts b/packages/core/src/tools/filesystem/index.ts index 2b84a34..dcb67c8 100644 --- a/packages/core/src/tools/filesystem/index.ts +++ b/packages/core/src/tools/filesystem/index.ts @@ -4,12 +4,7 @@ export { writeFileTool } from './write_file.js'; export { editFileTool } from './edit_file.js'; export { multiEditTool } from './multi_edit.js'; -// 目录操作 -export { listDirTool } from './list_directory.js'; - // 搜索 export { globTool } from './glob.js'; export { grepTool } from './grep.js'; -// 文件信息 -export { getFileInfoTool } from './get_file_info.js'; diff --git a/packages/core/src/tools/filesystem/list_directory.ts b/packages/core/src/tools/filesystem/list_directory.ts deleted file mode 100644 index 13b147f..0000000 --- a/packages/core/src/tools/filesystem/list_directory.ts +++ /dev/null @@ -1,76 +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 listDirTool: ToolWithMetadata = { - name: 'list_directory', - description: loadDescription('list_directory'), - metadata: { - name: 'list_directory', - category: 'filesystem', - description: '列出目录内容', - keywords: ['list', 'directory', 'ls', 'dir', 'folder', '列出', '目录', '文件夹', '查看'], - 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: 'list', - path: absolutePath, - workdir: cwd, - }); - - if (!permResult.allowed) { - if (permResult.needsConfirmation) { - return { - success: false, - output: '', - error: `需要用户确认: 列出目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`, - }; - } - return { - success: false, - output: '', - error: `权限被拒绝: ${permResult.reason || '不允许列出此目录'}`, - }; - } - - try { - const entries = await fs.readdir(absolutePath, { withFileTypes: true }); - const result = entries - .map((entry) => { - const prefix = entry.isDirectory() ? '📁' : '📄'; - return `${prefix} ${entry.name}`; - }) - .join('\n'); - - return { - success: true, - output: result || '(空目录)', - }; - } catch (error) { - return { - success: false, - output: '', - error: error instanceof Error ? error.message : String(error), - }; - } - }, -}; diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 782e9c5..1a0c34c 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -20,10 +20,8 @@ import { writeFileTool, editFileTool, multiEditTool, - listDirTool, globTool, grepTool, - getFileInfoTool, } from './filesystem/index.js'; // Web 工具 @@ -81,10 +79,8 @@ const allToolsWithMetadata: ToolWithMetadata[] = [ writeFileTool, editFileTool, multiEditTool, - listDirTool, globTool, grepTool, - getFileInfoTool, // Web 工具 (deferLoading: false) webSearchTool, diff --git a/packages/core/src/tools/load_description.ts b/packages/core/src/tools/load_description.ts index 5a93d3a..12b30ac 100644 --- a/packages/core/src/tools/load_description.ts +++ b/packages/core/src/tools/load_description.ts @@ -14,10 +14,8 @@ const TOOL_CATEGORY_MAP: Record = { write_file: 'filesystem', edit_file: 'filesystem', multi_edit: 'filesystem', - list_directory: 'filesystem', glob: 'filesystem', grep: 'filesystem', - get_file_info: 'filesystem', // web web_search: 'web', web_extract: 'web', diff --git a/packages/core/tests/unit/tools/filesystem/get_file_info.test.ts b/packages/core/tests/unit/tools/filesystem/get_file_info.test.ts deleted file mode 100644 index b1a6cc4..0000000 --- a/packages/core/tests/unit/tools/filesystem/get_file_info.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock fs/promises -vi.mock('fs/promises', () => ({ - stat: vi.fn(), - readlink: vi.fn(), - 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 { getFileInfoTool } from '../../../../src/tools/filesystem/get_file_info.js'; -import * as fs from 'fs/promises'; -import { getPermissionManager } from '../../../../src/permission/index.js'; - -describe('getFileInfoTool - 获取文件信息工具', () => { - const mockStats = { - isDirectory: () => false, - isFile: () => true, - isSymbolicLink: () => false, - size: 1024, - mode: 0o100644, - birthtime: new Date('2024-01-01'), - mtime: new Date('2024-01-15'), - atime: new Date('2024-01-20'), - ino: 12345, - nlink: 1, - }; - - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(fs.stat).mockResolvedValue(mockStats as any); - }); - - describe('工具定义', () => { - it('有正确的名称', () => { - expect(getFileInfoTool.name).toBe('get_file_info'); - }); - - it('有正确的元数据', () => { - expect(getFileInfoTool.metadata.category).toBe('filesystem'); - expect(getFileInfoTool.metadata.keywords).toContain('file'); - expect(getFileInfoTool.metadata.keywords).toContain('info'); - expect(getFileInfoTool.metadata.keywords).toContain('stat'); - }); - - it('定义了必需的 path 参数', () => { - expect(getFileInfoTool.parameters.path.required).toBe(true); - }); - }); - - describe('execute - 执行', () => { - it('成功获取文件信息', async () => { - const result = await getFileInfoTool.execute({ path: 'test.txt' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('路径:'); - expect(result.output).toContain('类型: 文件'); - expect(result.output).toContain('大小:'); - expect(result.output).toContain('权限:'); - expect(result.output).toContain('创建时间:'); - expect(result.output).toContain('修改时间:'); - expect(result.output).toContain('inode:'); - }); - - it('正确显示目录信息', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - ...mockStats, - isDirectory: () => true, - isFile: () => false, - } as any); - vi.mocked(fs.readdir).mockResolvedValue(['file1', 'file2', 'dir1'] as any); - - const result = await getFileInfoTool.execute({ path: 'test_dir' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('类型: 目录'); - expect(result.output).toContain('子项数量: 3'); - }); - - it('正确显示符号链接信息', async () => { - vi.mocked(fs.stat).mockResolvedValue({ - ...mockStats, - isSymbolicLink: () => true, - isFile: () => false, - } as any); - vi.mocked(fs.readlink).mockResolvedValue('/real/path'); - - const result = await getFileInfoTool.execute({ path: 'link' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('类型: 符号链接'); - expect(result.output).toContain('链接目标: /real/path'); - }); - - it('正确格式化文件大小', async () => { - // 测试不同大小 - const sizes = [ - { size: 500, expected: 'B' }, - { size: 1024, expected: 'KB' }, - { size: 1024 * 1024, expected: 'MB' }, - { size: 1024 * 1024 * 1024, expected: 'GB' }, - ]; - - for (const { size, expected } of sizes) { - vi.mocked(fs.stat).mockResolvedValue({ - ...mockStats, - size, - } as any); - - const result = await getFileInfoTool.execute({ path: 'test.txt' }); - - expect(result.output).toContain(expected); - } - }); - - it('权限被拒绝时返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'deny', - reason: '不允许获取信息', - }), - } as any); - - const result = await getFileInfoTool.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 getFileInfoTool.execute({ path: 'file.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 getFileInfoTool.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); - - await getFileInfoTool.execute({ path: 'test.txt' }); - - expect(mockCheck).toHaveBeenCalledWith( - expect.objectContaining({ - operation: 'info', - }) - ); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/filesystem/list_directory.test.ts b/packages/core/tests/unit/tools/filesystem/list_directory.test.ts deleted file mode 100644 index b051cd1..0000000 --- a/packages/core/tests/unit/tools/filesystem/list_directory.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { listDirTool } from '../../../../src/tools/filesystem/list_directory.js'; - -// 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 * as fs from 'fs/promises'; -import { getPermissionManager } from '../../../../src/permission/index.js'; - -describe('listDirTool - 列出目录工具', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('工具定义', () => { - it('有正确的名称', () => { - expect(listDirTool.name).toBe('list_directory'); - }); - - it('有正确的元数据', () => { - expect(listDirTool.metadata.category).toBe('filesystem'); - expect(listDirTool.metadata.keywords).toContain('list'); - expect(listDirTool.metadata.keywords).toContain('directory'); - expect(listDirTool.metadata.keywords).toContain('ls'); - }); - - it('定义了必需的 path 参数', () => { - expect(listDirTool.parameters.path).toBeDefined(); - expect(listDirTool.parameters.path.required).toBe(true); - }); - }); - - describe('execute - 执行', () => { - it('成功列出目录内容', async () => { - vi.mocked(fs.readdir).mockResolvedValue([ - { name: 'file1.txt', isDirectory: () => false }, - { name: 'folder', isDirectory: () => true }, - { name: 'file2.js', isDirectory: () => false }, - ] as any); - - const result = await listDirTool.execute({ path: './' }); - - expect(result.success).toBe(true); - expect(result.output).toContain('file1.txt'); - expect(result.output).toContain('folder'); - expect(result.output).toContain('file2.js'); - }); - - it('使用正确的图标区分文件和目录', async () => { - vi.mocked(fs.readdir).mockResolvedValue([ - { name: 'file.txt', isDirectory: () => false }, - { name: 'folder', isDirectory: () => true }, - ] as any); - - const result = await listDirTool.execute({ path: './' }); - - expect(result.output).toMatch(/📄.*file\.txt/); - expect(result.output).toMatch(/📁.*folder/); - }); - - it('空目录显示提示', async () => { - vi.mocked(fs.readdir).mockResolvedValue([]); - - const result = await listDirTool.execute({ path: './' }); - - expect(result.success).toBe(true); - expect(result.output).toBe('(空目录)'); - }); - - it('权限被拒绝时返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: false, - action: 'deny', - reason: '不允许列出此目录', - }), - } as any); - - const result = await listDirTool.execute({ path: '/etc' }); - - 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 listDirTool.execute({ path: '/home/user' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('需要用户确认'); - }); - - it('目录不存在时返回错误', async () => { - vi.mocked(getPermissionManager).mockReturnValue({ - checkFilePermission: vi.fn().mockResolvedValue({ - allowed: true, - action: 'allow', - }), - } as any); - vi.mocked(fs.readdir).mockRejectedValue(new Error('ENOENT: no such directory')); - - const result = await listDirTool.execute({ path: './nonexistent' }); - - expect(result.success).toBe(false); - expect(result.error).toContain('ENOENT'); - }); - - it('使用 withFileTypes 选项调用 readdir', async () => { - vi.mocked(fs.readdir).mockResolvedValue([]); - - await listDirTool.execute({ path: './' }); - - expect(fs.readdir).toHaveBeenCalledWith( - expect.any(String), - { withFileTypes: true } - ); - }); - }); -}); diff --git a/packages/core/tests/unit/tools/load_description.test.ts b/packages/core/tests/unit/tools/load_description.test.ts index 05ed5aa..c484d39 100644 --- a/packages/core/tests/unit/tools/load_description.test.ts +++ b/packages/core/tests/unit/tools/load_description.test.ts @@ -164,10 +164,8 @@ describe('loadDescription', () => { { tool: 'read_file', category: 'filesystem' }, { tool: 'write_file', category: 'filesystem' }, { tool: 'edit_file', category: 'filesystem' }, - { tool: 'list_directory', category: 'filesystem' }, { tool: 'glob', category: 'filesystem' }, { tool: 'grep', category: 'filesystem' }, - { tool: 'get_file_info', category: 'filesystem' }, // web { tool: 'web_search', category: 'web' }, { tool: 'web_extract', category: 'web' },