'use strict'; const fs = require('fs'); const path = require('path'); const { structuredPatch } = require('diff'); const { getContainerContext, resolveContainerPath, execContainerAction } = require('./container_bridge'); function resolvePath(workspace, p) { if (path.isAbsolute(p)) return p; return path.join(workspace, p); } function diffSummary(oldText, newText) { const patch = structuredPatch('old', 'new', oldText, newText, '', '', { context: 1 }); let added = 0; let removed = 0; const hunks = patch.hunks.map((hunk) => { let oldLine = hunk.oldStart; let newLine = hunk.newStart; const lines = hunk.lines.map((line) => { const prefix = line[0]; const content = line.slice(1); let lineNum = null; if (prefix === ' ') { lineNum = oldLine; oldLine += 1; newLine += 1; } else if (prefix === '-') { lineNum = oldLine; removed += 1; oldLine += 1; } else if (prefix === '+') { lineNum = newLine; added += 1; newLine += 1; } return { prefix, lineNum, content }; }); return lines; }); return { added, removed, hunks }; } function countLines(text) { if (!text) return 0; return String(text).split(/\r?\n/).length; } function findMatchLineNumbers(content, target) { if (!target) return []; const lines = []; let start = 0; while (true) { const idx = content.indexOf(target, start); if (idx === -1) break; const lineNo = content.slice(0, idx).split(/\r?\n/).length; lines.push(lineNo); start = idx + Math.max(1, target.length); } return lines; } function editFileTool(workspace, args) { const target = resolvePath(workspace, args.file_path); const oldString = args.old_string ?? ''; const newString = args.new_string ?? ''; if (!Object.prototype.hasOwnProperty.call(args || {}, 'replace_all')) { return { success: false, error: '缺少必要参数: replace_all(必须显式传入 true 或 false)' }; } const replaceAll = args.replace_all; if (typeof replaceAll !== 'boolean') { return { success: false, error: 'replace_all 必须是 true 或 false' }; } const shortOldStringNotice = countLines(oldString) < 3; const ctx = getContainerContext(); if (ctx) { const resolved = resolveContainerPath(workspace, args.file_path || '.', ctx); if (resolved.error) return { success: false, error: resolved.error }; const rel = resolved.relativePath; if (!oldString) return { success: false, error: 'old_string 不能为空,请从 read_file 内容中精确复制' }; const readResp = execContainerAction(ctx, 'read_file', { path: rel }); if (!readResp.success) return { success: false, error: readResp.error || '读取失败' }; const original = readResp.content || ''; if (!original.includes(oldString)) { return { success: false, error: 'old_string 未匹配到内容' }; } const matchedLines = findMatchLineNumbers(original, oldString); const foundCount = matchedLines.length; const replacements = replaceAll ? (original.split(oldString).length - 1) : 1; const updated = replaceAll ? original.split(oldString).join(newString) : original.replace(oldString, newString); const writeResp = execContainerAction(ctx, 'write_file', { path: rel, content: updated, mode: 'w', }); if (!writeResp.success) return { success: false, error: writeResp.error || '写入失败' }; const diff = diffSummary(original, updated); const messageParts = []; if (shortOldStringNotice) { messageParts.push('提示:old_string 少于3行,已继续执行;需要批量替换的场景可以单行或不足一行'); } if (foundCount > 1) { messageParts.push(`发现${foundCount}处,于${matchedLines.join(',')}行共替换${replacements}处`); } return { success: true, path: resolved.containerPath, replacements, found_matches: foundCount, matched_lines: matchedLines, message: messageParts.length > 0 ? messageParts.join(';') : undefined, diff, }; } try { let creating = false; if (!fs.existsSync(target)) { creating = true; fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, '', 'utf8'); } const stat = fs.statSync(target); if (!stat.isFile()) { return { success: false, error: '目标不是文件' }; } const original = fs.readFileSync(target, 'utf8'); if (oldString === '') { return { success: false, error: 'old_string 不能为空,请从 read_file 内容中精确复制' }; } if (!creating && !original.includes(oldString)) { return { success: false, error: 'old_string 未匹配到内容' }; } const matchedLines = creating ? [] : findMatchLineNumbers(original, oldString); const foundCount = matchedLines.length; const updated = creating ? newString : (replaceAll ? original.split(oldString).join(newString) : original.replace(oldString, newString)); let replacements = creating ? 0 : (replaceAll ? (original.split(oldString).length - 1) : 1); if (creating && newString) replacements = 1; fs.writeFileSync(target, updated, 'utf8'); const diff = diffSummary(original, updated); const messageParts = []; if (shortOldStringNotice) { messageParts.push('提示:old_string 少于3行,已继续执行;需要批量替换的场景可以单行或不足一行'); } if (foundCount > 1) { messageParts.push(`发现${foundCount}处,于${matchedLines.join(',')}行共替换${replacements}处`); } return { success: true, path: target, replacements, found_matches: foundCount, matched_lines: matchedLines, message: messageParts.length > 0 ? messageParts.join(';') : undefined, diff, }; } catch (err) { return { success: false, error: err.message || String(err) }; } } module.exports = { editFileTool, resolvePath };