From d6cf7d846f4a158c68b7f6d62026a32fc8889298 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 10 Jun 2026 14:19:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=205=E4=B8=AA=E6=96=87=E4=BB=B6=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=B7=A5=E5=85=B7=E5=A2=9E=E5=BC=BA=20=E2=80=94=20lis?= =?UTF-8?q?t=5Fdirectory(2000)/edit=5Ffile(regex)/delete=5Ffile(=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=A4=A7=E5=B0=8F)/read=5Fmultiple(50=E6=96=87?= =?UTF-8?q?=E4=BB=B610KB)/tree(=E6=B7=B1=E5=BA=A65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/tool-handlers-fs.ts | 84 +++++++++++++++++++------- src/renderer/services/tool-registry.ts | 17 +++--- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/src/main/tool-handlers-fs.ts b/src/main/tool-handlers-fs.ts index b779a31..e1206dd 100644 --- a/src/main/tool-handlers-fs.ts +++ b/src/main/tool-handlers-fs.ts @@ -237,7 +237,7 @@ export async function handleListDir(params: { path: string; recursive?: boolean; const allowed = checkPathAllowed(dirPath, 'read'); if (!allowed.ok) return { success: false, error: allowed.reason }; - const maxEntries = 500; + const maxEntries = 2000; const startOffset = params.offset || 0; const entries: Array<{ name: string; type: string; size: number | null; modified: string }> = []; let truncated = false; @@ -408,7 +408,28 @@ export async function handleDeleteFile(params: { path: string; recursive?: boole if (!allowed.ok) return { success: false, error: allowed.reason }; const stat = await fs.stat(filePath); - if (stat.isDirectory()) { + const isDir = stat.isDirectory(); + let deletedSize = stat.size; + let deletedCount = 1; + + if (isDir) { + // 统计目录内容 + let fileCount = 0; + async function countDir(dir: string): Promise { + const items = await fs.readdir(dir, { withFileTypes: true }); + for (const item of items) { + const fp = path.join(dir, item.name); + try { + const s = await fs.stat(fp); + deletedSize += s.size; + if (item.isFile()) fileCount++; + else if (item.isDirectory()) await countDir(fp); + } catch { /* skip */ } + } + } + await countDir(filePath); + deletedCount = fileCount; + if (params.recursive) { await fs.rm(filePath, { recursive: true, force: true }); } else { @@ -418,7 +439,12 @@ export async function handleDeleteFile(params: { path: string; recursive?: boole await fs.unlink(filePath); } - return { success: true, path: filePath, deleted: true }; + return { + success: true, path: filePath, deleted: true, + type: isDir ? 'directory' : 'file', + ...(isDir && { filesDeleted: deletedCount }), + deletedSize, + }; } catch (err) { return { success: false, error: (err as Error).message }; } @@ -445,7 +471,7 @@ export async function handleAppendFile(params: { path: string; content: string; } } -export async function handleEditFile(params: { path: string; old_text: string; new_text: string; all?: boolean }): Promise { +export async function handleEditFile(params: { path: string; old_text: string; new_text: string; all?: boolean; use_regex?: boolean }): Promise { try { const filePath = resolvePath(params.path); const allowed = checkPathAllowed(filePath, 'write'); @@ -453,26 +479,42 @@ export async function handleEditFile(params: { path: string; old_text: string; n const content = await fs.readFile(filePath, 'utf-8'); - if (!content.includes(params.old_text)) { - return { success: false, error: '未找到要替换的文本' }; - } - - let newContent: string; let replaceCount: number; + let newContent: string; - if (params.all) { - const parts = content.split(params.old_text); - replaceCount = parts.length - 1; - newContent = parts.join(params.new_text); + if (params.use_regex) { + try { + const regex = new RegExp(params.old_text, params.all ? 'g' : ''); + if (!regex.test(content)) { + return { success: false, error: '未找到匹配正则的文本' }; + } + // 重新创建 regex 因为 test() 会消耗 lastIndex(带g标志时) + const re = new RegExp(params.old_text, params.all ? 'g' : ''); + const matches = content.match(re); + replaceCount = matches ? matches.length : 1; + const finalRe = new RegExp(params.old_text, params.all ? 'g' : ''); + newContent = content.replace(finalRe, params.new_text); + } catch (regexErr) { + return { success: false, error: `无效的正则表达式: ${(regexErr as Error).message}` }; + } } else { - replaceCount = 1; - newContent = content.replace(params.old_text, params.new_text); + if (!content.includes(params.old_text)) { + return { success: false, error: '未找到要替换的文本' }; + } + if (params.all) { + const parts = content.split(params.old_text); + replaceCount = parts.length - 1; + newContent = parts.join(params.new_text); + } else { + replaceCount = 1; + newContent = content.replace(params.old_text, params.new_text); + } } - sendLog('info', `✂️ edit_file`, `${filePath} (${replaceCount} 处替换)`); + sendLog('info', `✂️ edit_file`, `${filePath} (${replaceCount} 处替换${params.use_regex ? ', 正则模式' : ''})`); await fs.writeFile(filePath, newContent, 'utf-8'); sendLog('success', `✂️ edit_file 完成`, `${newContent.length}B`); - return { success: true, path: filePath, replaceCount, newSize: newContent.length }; + return { success: true, path: filePath, replaceCount, newSize: newContent.length, regex: params.use_regex || false }; } catch (err) { sendLog('error', `✂️ edit_file 失败`, (err as Error).message); return { success: false, error: (err as Error).message }; @@ -508,7 +550,7 @@ export async function handleTree(params: { path: string; max_depth?: number; inc const allowed = checkPathAllowed(rootPath, 'read'); if (!allowed.ok) return { success: false, error: allowed.reason }; - const maxDepth = params.max_depth || 3; + const maxDepth = params.max_depth || 5; const includeHidden = params.include_hidden || false; const lines: string[] = []; let fileCount = 0; @@ -724,10 +766,10 @@ export async function handleReplaceInFiles(params: { path: string; glob: string; export async function handleReadMultipleFiles(params: { paths: string[]; max_chars_per_file?: number }): Promise { try { - const maxChars = params.max_chars_per_file || 2000; + const maxChars = params.max_chars_per_file || 10000; - // 并行读取所有文件(最多20个) - const filesToRead = params.paths.slice(0, 20).map(async (p) => { + // 并行读取所有文件(最多50个) + const filesToRead = params.paths.slice(0, 50).map(async (p) => { const filePath = resolvePath(p); const allowed = checkPathAllowed(filePath, 'read'); if (!allowed.ok) { diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index 2b0ef60..9b8f761 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -50,7 +50,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: 'function', function: { name: 'list_directory', - description: 'List directory contents. Returns file names, types, sizes, and modification times. Supports offset-based pagination for directories with many entries.', + description: 'List directory contents up to 2000 entries. Returns names, types, sizes, modification times. Supports offset pagination, recursion, and extension filtering.', parameters: { type: 'object', required: ['path'], @@ -102,13 +102,13 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: 'function', function: { name: 'delete_file', - description: 'Delete a file or directory. Requires user confirmation.', + description: 'Delete a file or directory. Returns deleted item count and total size. Use recursive=true for non-empty directories.', parameters: { type: 'object', required: ['path'], properties: { path: { type: 'string', description: 'Path to delete.' }, - recursive: { type: 'boolean', description: 'Recursive delete for directories. DANGEROUS.' } + recursive: { type: 'boolean', description: 'Recursive delete for directories. Returns filesDeleted count and total deletedSize.' } } } } @@ -197,15 +197,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: 'function', function: { name: 'edit_file', - description: 'Find and replace text in a file. More efficient than rewriting the whole file for small changes. Requires user confirmation.', + description: 'Find and replace text in a file. Supports literal string matching and regex (use_regex=true). Use all=true to replace all occurrences. More efficient than write_file for small edits.', parameters: { type: 'object', required: ['path', 'old_text', 'new_text'], properties: { path: { type: 'string', description: 'File path to edit.' }, - old_text: { type: 'string', description: 'Exact text to find.' }, + old_text: { type: 'string', description: 'Text to find. Can be a regex pattern when use_regex=true.' }, new_text: { type: 'string', description: 'Replacement text.' }, - all: { type: 'boolean', description: 'Replace all occurrences. Default: false (replace first only).' } + all: { type: 'boolean', description: 'Replace all occurrences. Default: false.' }, + use_regex: { type: 'boolean', description: 'Treat old_text as a regular expression. Default: false.' } } } } @@ -228,7 +229,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: 'function', function: { name: 'tree', - description: 'Display directory structure as a tree. Shows nested files and folders.', + description: 'Display directory structure as a tree. Shows nested files and folders. Default depth: 5 levels.', parameters: { type: 'object', required: ['path'], @@ -292,7 +293,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ type: 'function', function: { name: 'read_multiple_files', - description: 'Read multiple files at once. Returns contents of all requested files.', + description: 'Read up to 50 files at once in parallel. Returns content of all requested files (10KB each by default).', parameters: { type: 'object', required: ['paths'],