167 lines
5.9 KiB
JavaScript
167 lines
5.9 KiB
JavaScript
'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 };
|