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');
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<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) {
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<ToolResult> {
export async function handleEditFile(params: { path: string; old_text: string; new_text: string; all?: boolean; use_regex?: boolean }): Promise<ToolResult> {
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<ToolResult> {
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) {