feat: 5个文件系统工具增强 — list_directory(2000)/edit_file(regex)/delete_file(返回大小)/read_multiple(50文件10KB)/tree(深度5)

This commit is contained in:
thzxx
2026-06-10 14:19:03 +08:00
parent 04f2446337
commit d6cf7d846f
2 changed files with 72 additions and 29 deletions
+63 -21
View File
@@ -237,7 +237,7 @@ export async function handleListDir(params: { path: string; recursive?: boolean;
const allowed = checkPathAllowed(dirPath, 'read'); const allowed = checkPathAllowed(dirPath, 'read');
if (!allowed.ok) return { success: false, error: allowed.reason }; if (!allowed.ok) return { success: false, error: allowed.reason };
const maxEntries = 500; const maxEntries = 2000;
const startOffset = params.offset || 0; const startOffset = params.offset || 0;
const entries: Array<{ name: string; type: string; size: number | null; modified: string }> = []; const entries: Array<{ name: string; type: string; size: number | null; modified: string }> = [];
let truncated = false; 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 }; if (!allowed.ok) return { success: false, error: allowed.reason };
const stat = await fs.stat(filePath); 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<void> {
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) { if (params.recursive) {
await fs.rm(filePath, { recursive: true, force: true }); await fs.rm(filePath, { recursive: true, force: true });
} else { } else {
@@ -418,7 +439,12 @@ export async function handleDeleteFile(params: { path: string; recursive?: boole
await fs.unlink(filePath); 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) { } catch (err) {
return { success: false, error: (err as Error).message }; 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<ToolResult> { export async function handleEditFile(params: { path: string; old_text: string; new_text: string; all?: boolean; use_regex?: boolean }): Promise<ToolResult> {
try { try {
const filePath = resolvePath(params.path); const filePath = resolvePath(params.path);
const allowed = checkPathAllowed(filePath, 'write'); 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'); 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 replaceCount: number;
let newContent: string;
if (params.all) { if (params.use_regex) {
const parts = content.split(params.old_text); try {
replaceCount = parts.length - 1; const regex = new RegExp(params.old_text, params.all ? 'g' : '');
newContent = parts.join(params.new_text); 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 { } else {
replaceCount = 1; if (!content.includes(params.old_text)) {
newContent = content.replace(params.old_text, params.new_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'); await fs.writeFile(filePath, newContent, 'utf-8');
sendLog('success', `✂️ edit_file 完成`, `${newContent.length}B`); 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) { } catch (err) {
sendLog('error', `✂️ edit_file 失败`, (err as Error).message); sendLog('error', `✂️ edit_file 失败`, (err as Error).message);
return { success: false, error: (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'); const allowed = checkPathAllowed(rootPath, 'read');
if (!allowed.ok) return { success: false, error: allowed.reason }; 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 includeHidden = params.include_hidden || false;
const lines: string[] = []; const lines: string[] = [];
let fileCount = 0; 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<ToolResult> { export async function handleReadMultipleFiles(params: { paths: string[]; max_chars_per_file?: number }): Promise<ToolResult> {
try { try {
const maxChars = params.max_chars_per_file || 2000; const maxChars = params.max_chars_per_file || 10000;
// 并行读取所有文件(最多20个) // 并行读取所有文件(最多50个)
const filesToRead = params.paths.slice(0, 20).map(async (p) => { const filesToRead = params.paths.slice(0, 50).map(async (p) => {
const filePath = resolvePath(p); const filePath = resolvePath(p);
const allowed = checkPathAllowed(filePath, 'read'); const allowed = checkPathAllowed(filePath, 'read');
if (!allowed.ok) { if (!allowed.ok) {
+9 -8
View File
@@ -50,7 +50,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
type: 'function', type: 'function',
function: { function: {
name: 'list_directory', 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: { parameters: {
type: 'object', type: 'object',
required: ['path'], required: ['path'],
@@ -102,13 +102,13 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
type: 'function', type: 'function',
function: { function: {
name: 'delete_file', 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: { parameters: {
type: 'object', type: 'object',
required: ['path'], required: ['path'],
properties: { properties: {
path: { type: 'string', description: 'Path to delete.' }, 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', type: 'function',
function: { function: {
name: 'edit_file', 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: { parameters: {
type: 'object', type: 'object',
required: ['path', 'old_text', 'new_text'], required: ['path', 'old_text', 'new_text'],
properties: { properties: {
path: { type: 'string', description: 'File path to edit.' }, 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.' }, 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', type: 'function',
function: { function: {
name: 'tree', 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: { parameters: {
type: 'object', type: 'object',
required: ['path'], required: ['path'],
@@ -292,7 +293,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
type: 'function', type: 'function',
function: { function: {
name: 'read_multiple_files', 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: { parameters: {
type: 'object', type: 'object',
required: ['paths'], required: ['paths'],