feat: 升级至 v0.3.12 — 文件与代码类工具全面优化(16 项)
【阶段一 · 紧急修复 F1】 - F1-1 file_editor regex 计数 bug:强制 g 标志 + 同一 RegExp 实例做 match+replace - F1-2 code_search 统一 safeResolvePath(含路径遍历 + MEMORY.md 拦截) - F1-3 git_commit amend 模式:message 可选 + --no-edit 防止编辑器 hang - F1-4 write_file append 模式原子化:显式 open(O_APPEND)+write+fsync+close 【阶段二 · 增强现有工具 F2】 - F2-1 read_file 智能编码检测:BOM(UTF-8/UTF-16 LE/BE) + GBK 降级 - F2-2 read_file 支持 tail 模式(读取末尾 N 行,适用日志) - F2-3 search_files 二进制过滤 + 智能编码检测 - F2-4 search_files 多 glob 匹配(逗号分隔,如 *.ts,*.js) - F2-5 file_editor 新增 find_replace 操作(字面量替换,规避正则歧义) - F2-6 file_editor regex 支持跨行匹配(multiline 参数) - F2-7 file_editor 新增 backup 参数(编辑前 .bak 备份) 【阶段三 · 新增工具 F3】 - F3-1 file_move:双路径校验 + overwrite + 自动建父目录 + rename 原子 - F3-2 file_info:大小/时间/类型/编码/二进制/权限位 【阶段四 · 优化 F4】 - F4-1 diff_viewer Uint32Array→Uint16Array(省一半内存)+ safeResolvePath + 智能编码 - F4-2 错误处理统一:diff-viewer/file-editor/code-search catch 块改用 extractErrorMessage 【阶段五 · 验证 F5】 - tsc --noEmit 类型检查通过 - 人工审查通过:路径校验/错误处理/原子性/资源释放/权限策略/导出注册 工具数量:13 → 15(新增 file_move / file_info)
This commit is contained in:
@@ -12,12 +12,13 @@
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { resolve } from 'path';
|
||||
import log from 'electron-log';
|
||||
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
|
||||
import type { MetonaToolDef } from '../../../harness/types';
|
||||
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';
|
||||
import { isPathWithinWorkspace } from './file-guard';
|
||||
// F1-2: 统一用 safeResolvePath(含 isPathWithinWorkspace + MEMORY.md 拦截)
|
||||
// F4-2: 引入 extractErrorMessage 统一错误处理
|
||||
import { safeResolvePath, extractErrorMessage } from './file-guard';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -60,31 +61,37 @@ export class CodeSearchTool implements IMetonaTool {
|
||||
};
|
||||
|
||||
async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
|
||||
const pattern = args.pattern as string;
|
||||
if (typeof pattern !== 'string' || !pattern) {
|
||||
return { results: [], count: 0, error: 'Pattern is required and must be a string' };
|
||||
}
|
||||
if (pattern.length > 500) {
|
||||
return { results: [], count: 0, error: 'Pattern too long (max 500 chars)' };
|
||||
}
|
||||
// F4-2: 外层 try-catch 防止 safeResolvePath 抛出异常向上传播
|
||||
// (read_file/write_file/list_directory 均有外层 try-catch,code_search 此前缺失)
|
||||
try {
|
||||
const pattern = args.pattern as string;
|
||||
if (typeof pattern !== 'string' || !pattern) {
|
||||
return { results: [], count: 0, error: 'Pattern is required and must be a string' };
|
||||
}
|
||||
if (pattern.length > 500) {
|
||||
return { results: [], count: 0, error: 'Pattern too long (max 500 chars)' };
|
||||
}
|
||||
|
||||
const searchPath = args.path
|
||||
? this.resolveSearchPath(args.path as string, context.workspacePath)
|
||||
: context.workspacePath;
|
||||
const fileGlob = args.file_glob as string | undefined;
|
||||
const caseSensitive = (args.case_sensitive as boolean) ?? false;
|
||||
const contextBefore = Math.min(5, Math.max(0, (args.context_before as number) ?? 0));
|
||||
const contextAfter = Math.min(5, Math.max(0, (args.context_after as number) ?? 0));
|
||||
const maxResults = Math.min(200, Math.max(1, (args.max_results as number) ?? 50));
|
||||
const searchPath = args.path
|
||||
? safeResolvePath(args.path as string, context.workspacePath)
|
||||
: context.workspacePath;
|
||||
const fileGlob = args.file_glob as string | undefined;
|
||||
const caseSensitive = (args.case_sensitive as boolean) ?? false;
|
||||
const contextBefore = Math.min(5, Math.max(0, (args.context_before as number) ?? 0));
|
||||
const contextAfter = Math.min(5, Math.max(0, (args.context_after as number) ?? 0));
|
||||
const maxResults = Math.min(200, Math.max(1, (args.max_results as number) ?? 50));
|
||||
|
||||
const opts = { fileGlob, caseSensitive, contextBefore, contextAfter, maxResults };
|
||||
const opts = { fileGlob, caseSensitive, contextBefore, contextAfter, maxResults };
|
||||
|
||||
// 优先使用 ripgrep,回退到 JS 实现
|
||||
const hasRg = await this.checkRipgrep();
|
||||
if (hasRg) {
|
||||
return this.searchWithRipgrep(pattern, searchPath, opts, context);
|
||||
// 优先使用 ripgrep,回退到 JS 实现
|
||||
const hasRg = await this.checkRipgrep();
|
||||
if (hasRg) {
|
||||
return this.searchWithRipgrep(pattern, searchPath, opts, context);
|
||||
}
|
||||
return this.searchWithJs(pattern, searchPath, opts, context);
|
||||
} catch (error) {
|
||||
return { results: [], count: 0, error: extractErrorMessage(error), success: false };
|
||||
}
|
||||
return this.searchWithJs(pattern, searchPath, opts, context);
|
||||
}
|
||||
|
||||
/** 使用 ripgrep 子进程搜索 */
|
||||
@@ -217,12 +224,4 @@ export class CodeSearchTool implements IMetonaTool {
|
||||
limit: opts.maxResults,
|
||||
}, context);
|
||||
}
|
||||
|
||||
private resolveSearchPath(filePath: string, workspacePath: string): string {
|
||||
const resolved = resolve(workspacePath, filePath);
|
||||
if (!isPathWithinWorkspace(filePath, workspacePath)) {
|
||||
throw new Error(`Path traversal detected: ${filePath}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user