agent-Specialization/easyagent/src/tools/edit_file.js

167 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 };