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:
2026-07-21 16:08:39 +08:00
parent 8973ea6d47
commit 058ee2de36
12 changed files with 586 additions and 100 deletions
+30 -31
View File
@@ -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-catchcode_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;
}
}