opencode-gbk-tools 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/dist/cli/index.js +11 -2
- package/dist/opencode-tools/gbk_edit.js +419 -13
- package/dist/opencode-tools/gbk_read.js +123 -114
- package/dist/opencode-tools/gbk_search.js +52 -56
- package/dist/opencode-tools/gbk_write.js +19 -8
- package/dist/opencode-tools/text_edit.js +1 -0
- package/dist/opencode-tools/text_read.js +1 -0
- package/dist/opencode-tools/text_write.js +1 -0
- package/dist/plugin/index.js +540 -174
- package/dist/plugins/opencode-gbk-tools.js +18818 -2
- package/dist/release-manifest.json +2 -50
- package/package.json +1 -1
- package/dist/agents/gbk-engine.md +0 -39
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
为 OpenCode 提供一套自动识别编码的文本工具,以及面向 `GBK` / `GB18030` 的专用工具。
|
|
4
4
|
|
|
5
|
-
解决 OpenCode 内置工具难以稳定处理非 UTF-8
|
|
5
|
+
解决 OpenCode 内置工具难以稳定处理非 UTF-8 文本文件的问题,并通过 plugin 让所有 agents 默认获得这些工具与规则,无需单独切换专属 GBK agent。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| `gbk_write` | 写入或追加内容到 GBK 文件(`append=true` 支持追加) |
|
|
18
18
|
| `gbk_edit` | 精确替换 GBK 文件中的指定文本块 |
|
|
19
19
|
| `gbk_search` | 在 GBK 文件中搜索关键词,返回行号和上下文 |
|
|
20
|
-
| 本地/全局 plugin 规则 | 给所有 agents 注入“优先使用 `text_
|
|
20
|
+
| 本地/全局 plugin 规则 | 给所有 agents 注入“优先使用 `text_*`”的系统提示,并统一开放 `gbk_*` 工具 |
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -27,7 +27,15 @@
|
|
|
27
27
|
npx opencode-gbk-tools install
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
安装完成后会注册 `opencode-gbk-tools` plugin,由该 plugin 统一向全部 agents 暴露 `text_*` / `gbk_*` 工具与规则。
|
|
31
|
+
|
|
32
|
+
重启 OpenCode 后,**全部 agents** 都会默认获得:
|
|
33
|
+
|
|
34
|
+
- `text_read` / `text_write` / `text_edit`
|
|
35
|
+
- `gbk_read` / `gbk_write` / `gbk_edit` / `gbk_search`
|
|
36
|
+
- 对文本文件优先使用 `text_*` 的系统提示
|
|
37
|
+
|
|
38
|
+
不再需要单独安装或切换专属 `gbk-engine` agent。
|
|
31
39
|
|
|
32
40
|
---
|
|
33
41
|
|
|
@@ -41,6 +49,8 @@ npx opencode-gbk-tools install
|
|
|
41
49
|
npx opencode-gbk-tools uninstall
|
|
42
50
|
```
|
|
43
51
|
|
|
52
|
+
卸载后会移除已安装的工具、plugin 与 manifest,所有 agents 恢复为未安装前的默认行为。
|
|
53
|
+
|
|
44
54
|
---
|
|
45
55
|
|
|
46
56
|
## 工具使用说明
|
|
@@ -181,7 +191,7 @@ A:重启 OpenCode。工具在 OpenCode 启动时加载,安装后需要重启
|
|
|
181
191
|
A:请先安装 Node.js(https://nodejs.org/),版本需要 18 或以上。
|
|
182
192
|
|
|
183
193
|
**Q:Windows 路径在哪?**
|
|
184
|
-
A
|
|
194
|
+
A:全局安装的 plugin 文件位于 `C:\Users\你的用户名\.config\opencode\plugins\opencode-gbk-tools.js`
|
|
185
195
|
|
|
186
196
|
**Q:gbk_edit 提示"未找到需要替换的文本"?**
|
|
187
197
|
A:检查 `oldString` 是否包含了行号前缀(如 `"3787: "`),去掉行号前缀后重试。若要在文件末尾追加内容,请使用 `gbk_write [append=true]`。
|
|
@@ -195,6 +205,8 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
|
|
|
195
205
|
|
|
196
206
|
| 版本 | 说明 |
|
|
197
207
|
|------|------|
|
|
208
|
+
| 0.1.16 | 去掉专属 `gbk-engine` agent 安装链路,改为通过 plugin + tools 让全部 agents 统一支持 `text_*` / `gbk_*`;同步更新安装说明 |
|
|
209
|
+
| 0.1.15 | 为 GBK 大文件统一引入行字节索引与流式读/搜/改路径,提升局部编辑、搜索与大块修改时的性能、稳定性与准确性 |
|
|
198
210
|
| 0.1.14 | 重新发布当前稳定产物,核对 `npm pack --dry-run` 输出包含完整 `dist/`,为下一次公开发布提供一致的打包基线 |
|
|
199
211
|
| 0.1.13 | `text_write` 在新文件 + `encoding=auto` 下改为显式报错,不再静默默认 `utf8`;同时优化 `text_read` 流式读取与 `text_edit` 预览生成,减少重复读取 |
|
|
200
212
|
| 0.1.12 | 仅提升发布版本号,用于重新发布当前 `0.1.11` 的稳定内容 |
|
package/dist/cli/index.js
CHANGED
|
@@ -60,10 +60,19 @@ function resolveTargetBase(target, cwd) {
|
|
|
60
60
|
// src/cli/install.ts
|
|
61
61
|
async function installCommand(args) {
|
|
62
62
|
const targetBase = resolveTargetBase(args.target, args.cwd);
|
|
63
|
-
const allowedArtifacts = new Set(args.artifacts ?? ["
|
|
63
|
+
const allowedArtifacts = new Set(args.artifacts ?? ["plugin"]);
|
|
64
64
|
const releaseManifest = JSON.parse(await fs3.readFile(path3.join(args.packageRoot, "dist", "release-manifest.json"), "utf8"));
|
|
65
65
|
const selectedArtifacts = releaseManifest.artifacts.filter((artifact) => allowedArtifacts.has(artifact.kind));
|
|
66
66
|
const existingManifest = await loadInstalledManifest(targetBase);
|
|
67
|
+
const selectedRelativePaths = new Set(selectedArtifacts.map((artifact) => artifact.relativePath));
|
|
68
|
+
if (existingManifest) {
|
|
69
|
+
for (const file of existingManifest.files) {
|
|
70
|
+
if (selectedRelativePaths.has(file.relativePath)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
await fs3.rm(path3.join(targetBase, file.relativePath), { force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
67
76
|
for (const artifact of selectedArtifacts) {
|
|
68
77
|
const targetPath = path3.join(targetBase, artifact.relativePath);
|
|
69
78
|
if (await pathExists(targetPath) && !existingManifest && !args.force) {
|
|
@@ -181,7 +190,7 @@ async function setupCommand(args) {
|
|
|
181
190
|
await fs6.mkdir(configBase, { recursive: true });
|
|
182
191
|
const configPath = await resolveConfigFile(configBase);
|
|
183
192
|
await ensurePluginConfigured(configPath, pluginName);
|
|
184
|
-
const installResult = await installCommand({ ...args, force: true, artifacts: ["
|
|
193
|
+
const installResult = await installCommand({ ...args, force: true, artifacts: ["plugin"] });
|
|
185
194
|
return {
|
|
186
195
|
configPath,
|
|
187
196
|
targetBase: installResult.targetBase
|
|
@@ -16305,6 +16305,14 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
|
|
|
16305
16305
|
|
|
16306
16306
|
// src/lib/gbk-file.ts
|
|
16307
16307
|
var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
|
|
16308
|
+
var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
|
|
16309
|
+
var gbkLineIndexCache = /* @__PURE__ */ new Map();
|
|
16310
|
+
function toSafeNumber(value) {
|
|
16311
|
+
return typeof value === "bigint" ? Number(value) : value;
|
|
16312
|
+
}
|
|
16313
|
+
function invalidateGbkLineIndex(filePath) {
|
|
16314
|
+
gbkLineIndexCache.delete(filePath);
|
|
16315
|
+
}
|
|
16308
16316
|
function assertStringArgument(value, name) {
|
|
16309
16317
|
if (typeof value !== "string") {
|
|
16310
16318
|
throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
@@ -16417,6 +16425,18 @@ function detectNewlineStyle(text) {
|
|
|
16417
16425
|
}
|
|
16418
16426
|
return "none";
|
|
16419
16427
|
}
|
|
16428
|
+
function finalizeNewlineStyle(crlfCount, lfCount) {
|
|
16429
|
+
if (crlfCount > 0 && lfCount === 0) {
|
|
16430
|
+
return "crlf";
|
|
16431
|
+
}
|
|
16432
|
+
if (lfCount > 0 && crlfCount === 0) {
|
|
16433
|
+
return "lf";
|
|
16434
|
+
}
|
|
16435
|
+
if (crlfCount > 0 && lfCount > 0) {
|
|
16436
|
+
return "mixed";
|
|
16437
|
+
}
|
|
16438
|
+
return "none";
|
|
16439
|
+
}
|
|
16420
16440
|
function applyLineRange(text, startLine, endLine) {
|
|
16421
16441
|
if (startLine === void 0 && endLine === void 0) {
|
|
16422
16442
|
return {
|
|
@@ -16542,6 +16562,29 @@ function hasLineNumberPrefixes(lines) {
|
|
|
16542
16562
|
function stripLineNumberPrefixes(lines) {
|
|
16543
16563
|
return lines.map((l) => l.replace(/^\d+: /, ""));
|
|
16544
16564
|
}
|
|
16565
|
+
function parseLineNumberPrefixedBlock(text) {
|
|
16566
|
+
const lines = splitNormalizedLines(text);
|
|
16567
|
+
if (!hasLineNumberPrefixes(lines)) {
|
|
16568
|
+
return null;
|
|
16569
|
+
}
|
|
16570
|
+
const lineNumbers = [];
|
|
16571
|
+
const strippedLines = [];
|
|
16572
|
+
for (const line of lines) {
|
|
16573
|
+
const match = /^(\d+): ?(.*)$/.exec(line);
|
|
16574
|
+
if (!match) {
|
|
16575
|
+
return null;
|
|
16576
|
+
}
|
|
16577
|
+
lineNumbers.push(Number(match[1]));
|
|
16578
|
+
strippedLines.push(match[2]);
|
|
16579
|
+
}
|
|
16580
|
+
return {
|
|
16581
|
+
lineNumbers,
|
|
16582
|
+
strippedText: strippedLines.join("\n"),
|
|
16583
|
+
isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
|
|
16584
|
+
startLine: lineNumbers[0],
|
|
16585
|
+
endLine: lineNumbers[lineNumbers.length - 1]
|
|
16586
|
+
};
|
|
16587
|
+
}
|
|
16545
16588
|
function matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content) {
|
|
16546
16589
|
for (let start = 0; start < contentLines.length; start += 1) {
|
|
16547
16590
|
let contentIndex = start;
|
|
@@ -16658,7 +16701,7 @@ async function readWholeGbkTextFile(input) {
|
|
|
16658
16701
|
}
|
|
16659
16702
|
async function visitDecodedTextChunks(input, visitor) {
|
|
16660
16703
|
const decoder = import_iconv_lite.default.getDecoder(input.encoding);
|
|
16661
|
-
const stream = createReadStream(input.filePath);
|
|
16704
|
+
const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
|
|
16662
16705
|
try {
|
|
16663
16706
|
for await (const chunk of stream) {
|
|
16664
16707
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
@@ -16681,6 +16724,90 @@ async function visitDecodedTextChunks(input, visitor) {
|
|
|
16681
16724
|
stream.destroy();
|
|
16682
16725
|
}
|
|
16683
16726
|
}
|
|
16727
|
+
async function getGbkLineIndex(input) {
|
|
16728
|
+
const cached2 = gbkLineIndexCache.get(input.filePath);
|
|
16729
|
+
if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
|
|
16730
|
+
return cached2;
|
|
16731
|
+
}
|
|
16732
|
+
const lineStartOffsets = [0];
|
|
16733
|
+
let byteOffset = 0;
|
|
16734
|
+
let previousByteWasCR = false;
|
|
16735
|
+
let crlfCount = 0;
|
|
16736
|
+
let lfCount = 0;
|
|
16737
|
+
const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
|
|
16738
|
+
try {
|
|
16739
|
+
for await (const chunk of stream) {
|
|
16740
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16741
|
+
assertNotBinary(buffer);
|
|
16742
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
16743
|
+
const byte = buffer[index];
|
|
16744
|
+
if (byte === 10) {
|
|
16745
|
+
if (previousByteWasCR) {
|
|
16746
|
+
crlfCount += 1;
|
|
16747
|
+
} else {
|
|
16748
|
+
lfCount += 1;
|
|
16749
|
+
}
|
|
16750
|
+
lineStartOffsets.push(byteOffset + index + 1);
|
|
16751
|
+
previousByteWasCR = false;
|
|
16752
|
+
continue;
|
|
16753
|
+
}
|
|
16754
|
+
previousByteWasCR = byte === 13;
|
|
16755
|
+
}
|
|
16756
|
+
byteOffset += buffer.length;
|
|
16757
|
+
}
|
|
16758
|
+
} catch (error45) {
|
|
16759
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16760
|
+
throw error45;
|
|
16761
|
+
}
|
|
16762
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
|
|
16763
|
+
} finally {
|
|
16764
|
+
stream.destroy();
|
|
16765
|
+
}
|
|
16766
|
+
const result = {
|
|
16767
|
+
filePath: input.filePath,
|
|
16768
|
+
fileSize: toSafeNumber(input.stat.size),
|
|
16769
|
+
mtimeMs: toSafeNumber(input.stat.mtimeMs),
|
|
16770
|
+
lineStartOffsets,
|
|
16771
|
+
totalLines: lineStartOffsets.length,
|
|
16772
|
+
newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
|
|
16773
|
+
};
|
|
16774
|
+
gbkLineIndexCache.set(input.filePath, result);
|
|
16775
|
+
return result;
|
|
16776
|
+
}
|
|
16777
|
+
async function readDecodedGbkByteRange(input, start, endExclusive) {
|
|
16778
|
+
if (endExclusive <= start) {
|
|
16779
|
+
return "";
|
|
16780
|
+
}
|
|
16781
|
+
const decoder = import_iconv_lite.default.getDecoder(input.encoding);
|
|
16782
|
+
const stream = createReadStream(input.filePath, {
|
|
16783
|
+
start,
|
|
16784
|
+
end: endExclusive - 1,
|
|
16785
|
+
highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
|
|
16786
|
+
});
|
|
16787
|
+
const parts = [];
|
|
16788
|
+
try {
|
|
16789
|
+
for await (const chunk of stream) {
|
|
16790
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16791
|
+
assertNotBinary(buffer);
|
|
16792
|
+
const text = decoder.write(buffer);
|
|
16793
|
+
if (text.length > 0) {
|
|
16794
|
+
parts.push(text);
|
|
16795
|
+
}
|
|
16796
|
+
}
|
|
16797
|
+
const trailingText = decoder.end() ?? "";
|
|
16798
|
+
if (trailingText.length > 0) {
|
|
16799
|
+
parts.push(trailingText);
|
|
16800
|
+
}
|
|
16801
|
+
return parts.join("");
|
|
16802
|
+
} catch (error45) {
|
|
16803
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16804
|
+
throw error45;
|
|
16805
|
+
}
|
|
16806
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
|
|
16807
|
+
} finally {
|
|
16808
|
+
stream.destroy();
|
|
16809
|
+
}
|
|
16810
|
+
}
|
|
16684
16811
|
async function writeAll(handle, buffer) {
|
|
16685
16812
|
let offset = 0;
|
|
16686
16813
|
while (offset < buffer.length) {
|
|
@@ -16696,6 +16823,249 @@ async function writeEncodedText(handle, encoding, text) {
|
|
|
16696
16823
|
await writeAll(handle, buffer);
|
|
16697
16824
|
return buffer.byteLength;
|
|
16698
16825
|
}
|
|
16826
|
+
async function copyFileByteRangeToHandle(sourcePath, handle, start, endExclusive) {
|
|
16827
|
+
if (endExclusive <= start) {
|
|
16828
|
+
return 0;
|
|
16829
|
+
}
|
|
16830
|
+
const stream = createReadStream(sourcePath, {
|
|
16831
|
+
start,
|
|
16832
|
+
end: endExclusive - 1,
|
|
16833
|
+
highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
|
|
16834
|
+
});
|
|
16835
|
+
let bytesWritten = 0;
|
|
16836
|
+
try {
|
|
16837
|
+
for await (const chunk of stream) {
|
|
16838
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16839
|
+
await writeAll(handle, buffer);
|
|
16840
|
+
bytesWritten += buffer.byteLength;
|
|
16841
|
+
}
|
|
16842
|
+
return bytesWritten;
|
|
16843
|
+
} catch (error45) {
|
|
16844
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16845
|
+
throw error45;
|
|
16846
|
+
}
|
|
16847
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${sourcePath}`, error45);
|
|
16848
|
+
} finally {
|
|
16849
|
+
stream.destroy();
|
|
16850
|
+
}
|
|
16851
|
+
}
|
|
16852
|
+
function alignTextToNewlineStyle(text, newlineStyle) {
|
|
16853
|
+
return newlineStyle === "crlf" ? text.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? text.replace(/\r\n/g, "\n") : text;
|
|
16854
|
+
}
|
|
16855
|
+
function replaceScopedTextContent(scopeText, oldString, newString, replaceAll, newlineStyle) {
|
|
16856
|
+
const occurrencesBefore = countOccurrences(scopeText, oldString);
|
|
16857
|
+
if (!replaceAll && occurrencesBefore === 0) {
|
|
16858
|
+
const loose = tryLooseBlockReplace(scopeText, oldString, newString);
|
|
16859
|
+
if (loose !== null) {
|
|
16860
|
+
return {
|
|
16861
|
+
replacedText: loose.content,
|
|
16862
|
+
replacements: 1,
|
|
16863
|
+
occurrencesBefore: loose.occurrencesBefore
|
|
16864
|
+
};
|
|
16865
|
+
}
|
|
16866
|
+
}
|
|
16867
|
+
if (replaceAll) {
|
|
16868
|
+
if (occurrencesBefore === 0) {
|
|
16869
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
|
|
16870
|
+
}
|
|
16871
|
+
} else if (occurrencesBefore === 0) {
|
|
16872
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
|
|
16873
|
+
} else if (occurrencesBefore > 1) {
|
|
16874
|
+
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${oldString}`);
|
|
16875
|
+
}
|
|
16876
|
+
const alignedNewString = alignTextToNewlineStyle(newString, newlineStyle);
|
|
16877
|
+
return {
|
|
16878
|
+
replacedText: replaceAll ? scopeText.split(oldString).join(alignedNewString) : scopeText.replace(oldString, alignedNewString),
|
|
16879
|
+
replacements: replaceAll ? occurrencesBefore : 1,
|
|
16880
|
+
occurrencesBefore
|
|
16881
|
+
};
|
|
16882
|
+
}
|
|
16883
|
+
async function replaceLargeGbkFileTextInLineRange(input) {
|
|
16884
|
+
const lineIndex = await getGbkLineIndex(input);
|
|
16885
|
+
const requestedStartLine = input.startLine ?? 1;
|
|
16886
|
+
const requestedEndLine = input.endLine ?? lineIndex.totalLines;
|
|
16887
|
+
if (requestedEndLine < requestedStartLine) {
|
|
16888
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "endLine \u4E0D\u80FD\u5C0F\u4E8E startLine");
|
|
16889
|
+
}
|
|
16890
|
+
const actualStartLine = Math.min(requestedStartLine, lineIndex.totalLines);
|
|
16891
|
+
const actualEndLine = Math.min(requestedEndLine, lineIndex.totalLines);
|
|
16892
|
+
const rangeStart = lineIndex.lineStartOffsets[Math.max(0, actualStartLine - 1)] ?? toSafeNumber(input.stat.size);
|
|
16893
|
+
const rangeEnd = actualEndLine < lineIndex.totalLines ? lineIndex.lineStartOffsets[actualEndLine] : toSafeNumber(input.stat.size);
|
|
16894
|
+
const scopeText = await readDecodedGbkByteRange(input, rangeStart, rangeEnd);
|
|
16895
|
+
const replaced = replaceScopedTextContent(
|
|
16896
|
+
scopeText,
|
|
16897
|
+
input.oldString,
|
|
16898
|
+
input.newString,
|
|
16899
|
+
input.replaceAll,
|
|
16900
|
+
lineIndex.newlineStyle
|
|
16901
|
+
);
|
|
16902
|
+
const tempPath = path2.join(
|
|
16903
|
+
path2.dirname(input.filePath),
|
|
16904
|
+
`${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
|
|
16905
|
+
);
|
|
16906
|
+
const handle = await fs2.open(tempPath, "w");
|
|
16907
|
+
let bytesWritten = 0;
|
|
16908
|
+
try {
|
|
16909
|
+
bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, 0, rangeStart);
|
|
16910
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, replaced.replacedText);
|
|
16911
|
+
bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, rangeEnd, toSafeNumber(input.stat.size));
|
|
16912
|
+
await handle.close();
|
|
16913
|
+
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
16914
|
+
await fs2.chmod(tempPath, mode);
|
|
16915
|
+
await fs2.rename(tempPath, input.filePath);
|
|
16916
|
+
return {
|
|
16917
|
+
mode: "replace",
|
|
16918
|
+
filePath: input.filePath,
|
|
16919
|
+
encoding: input.encoding,
|
|
16920
|
+
replacements: replaced.replacements,
|
|
16921
|
+
occurrencesBefore: replaced.occurrencesBefore,
|
|
16922
|
+
bytesRead: input.stat.size,
|
|
16923
|
+
bytesWritten
|
|
16924
|
+
};
|
|
16925
|
+
} catch (error45) {
|
|
16926
|
+
await handle.close().catch(() => void 0);
|
|
16927
|
+
await fs2.rm(tempPath, { force: true }).catch(() => void 0);
|
|
16928
|
+
throw error45;
|
|
16929
|
+
}
|
|
16930
|
+
}
|
|
16931
|
+
async function replaceLargeGbkFileByAnchor(input) {
|
|
16932
|
+
const tempPath = path2.join(
|
|
16933
|
+
path2.dirname(input.filePath),
|
|
16934
|
+
`${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
|
|
16935
|
+
);
|
|
16936
|
+
const handle = await fs2.open(tempPath, "w");
|
|
16937
|
+
const alignedContent = input.content.replace(/\r\n/g, "\n");
|
|
16938
|
+
const anchorLength = input.anchor.length;
|
|
16939
|
+
const carryLength = Math.max(anchorLength + alignedContent.length, 1);
|
|
16940
|
+
let decoded = "";
|
|
16941
|
+
let scanFrom = 0;
|
|
16942
|
+
let totalMatches = 0;
|
|
16943
|
+
let inserted = false;
|
|
16944
|
+
let skipped = false;
|
|
16945
|
+
let bytesWritten = 0;
|
|
16946
|
+
const flushPrefix = async (preserveTailLength) => {
|
|
16947
|
+
const flushLength = Math.max(0, decoded.length - preserveTailLength);
|
|
16948
|
+
if (flushLength === 0) {
|
|
16949
|
+
return;
|
|
16950
|
+
}
|
|
16951
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded.slice(0, flushLength));
|
|
16952
|
+
decoded = decoded.slice(flushLength);
|
|
16953
|
+
scanFrom = Math.max(0, scanFrom - flushLength);
|
|
16954
|
+
};
|
|
16955
|
+
const finalizeInserted = async () => {
|
|
16956
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
|
|
16957
|
+
decoded = "";
|
|
16958
|
+
};
|
|
16959
|
+
try {
|
|
16960
|
+
await visitDecodedTextChunks(input, async (text) => {
|
|
16961
|
+
decoded += text;
|
|
16962
|
+
while (!inserted) {
|
|
16963
|
+
const foundAt = decoded.indexOf(input.anchor, scanFrom);
|
|
16964
|
+
if (foundAt === -1) {
|
|
16965
|
+
break;
|
|
16966
|
+
}
|
|
16967
|
+
totalMatches += 1;
|
|
16968
|
+
const afterAnchor = foundAt + anchorLength;
|
|
16969
|
+
if (totalMatches !== input.occurrence) {
|
|
16970
|
+
scanFrom = afterAnchor;
|
|
16971
|
+
continue;
|
|
16972
|
+
}
|
|
16973
|
+
const before = decoded.slice(0, foundAt);
|
|
16974
|
+
const after = decoded.slice(afterAnchor);
|
|
16975
|
+
const anchorAndAfter = decoded.slice(foundAt);
|
|
16976
|
+
const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
|
|
16977
|
+
if (alreadyExists) {
|
|
16978
|
+
if (input.ifExists === "error") {
|
|
16979
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
|
|
16980
|
+
}
|
|
16981
|
+
if (input.ifExists === "skip") {
|
|
16982
|
+
skipped = true;
|
|
16983
|
+
inserted = true;
|
|
16984
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
|
|
16985
|
+
decoded = "";
|
|
16986
|
+
return;
|
|
16987
|
+
}
|
|
16988
|
+
}
|
|
16989
|
+
if (input.mode === "insertAfter") {
|
|
16990
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, before);
|
|
16991
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, input.anchor);
|
|
16992
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
|
|
16993
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, after);
|
|
16994
|
+
} else {
|
|
16995
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, before);
|
|
16996
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
|
|
16997
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, anchorAndAfter);
|
|
16998
|
+
}
|
|
16999
|
+
decoded = "";
|
|
17000
|
+
scanFrom = 0;
|
|
17001
|
+
inserted = true;
|
|
17002
|
+
return;
|
|
17003
|
+
}
|
|
17004
|
+
if (!inserted) {
|
|
17005
|
+
await flushPrefix(carryLength);
|
|
17006
|
+
}
|
|
17007
|
+
});
|
|
17008
|
+
if (!inserted) {
|
|
17009
|
+
while (true) {
|
|
17010
|
+
const foundAt = decoded.indexOf(input.anchor, scanFrom);
|
|
17011
|
+
if (foundAt === -1) {
|
|
17012
|
+
break;
|
|
17013
|
+
}
|
|
17014
|
+
totalMatches += 1;
|
|
17015
|
+
const afterAnchor = foundAt + anchorLength;
|
|
17016
|
+
if (totalMatches !== input.occurrence) {
|
|
17017
|
+
scanFrom = afterAnchor;
|
|
17018
|
+
continue;
|
|
17019
|
+
}
|
|
17020
|
+
const before = decoded.slice(0, foundAt);
|
|
17021
|
+
const after = decoded.slice(afterAnchor);
|
|
17022
|
+
const anchorAndAfter = decoded.slice(foundAt);
|
|
17023
|
+
const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
|
|
17024
|
+
if (alreadyExists) {
|
|
17025
|
+
if (input.ifExists === "error") {
|
|
17026
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
|
|
17027
|
+
}
|
|
17028
|
+
if (input.ifExists === "skip") {
|
|
17029
|
+
skipped = true;
|
|
17030
|
+
inserted = true;
|
|
17031
|
+
break;
|
|
17032
|
+
}
|
|
17033
|
+
}
|
|
17034
|
+
decoded = input.mode === "insertAfter" ? `${before}${input.anchor}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
|
|
17035
|
+
inserted = true;
|
|
17036
|
+
break;
|
|
17037
|
+
}
|
|
17038
|
+
}
|
|
17039
|
+
if (!inserted && totalMatches === 0) {
|
|
17040
|
+
throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${input.anchor}`);
|
|
17041
|
+
}
|
|
17042
|
+
if (!inserted && totalMatches > 0) {
|
|
17043
|
+
throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\u5230 ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${input.occurrence} \u5904`);
|
|
17044
|
+
}
|
|
17045
|
+
await finalizeInserted();
|
|
17046
|
+
await handle.close();
|
|
17047
|
+
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
17048
|
+
await fs2.chmod(tempPath, mode);
|
|
17049
|
+
await fs2.rename(tempPath, input.filePath);
|
|
17050
|
+
invalidateGbkLineIndex(input.filePath);
|
|
17051
|
+
return {
|
|
17052
|
+
mode: input.mode,
|
|
17053
|
+
filePath: input.filePath,
|
|
17054
|
+
encoding: input.encoding,
|
|
17055
|
+
anchor: input.anchor,
|
|
17056
|
+
occurrence: input.occurrence,
|
|
17057
|
+
anchorMatches: totalMatches,
|
|
17058
|
+
inserted: !skipped,
|
|
17059
|
+
skipped,
|
|
17060
|
+
bytesRead: input.stat.size,
|
|
17061
|
+
bytesWritten
|
|
17062
|
+
};
|
|
17063
|
+
} catch (error45) {
|
|
17064
|
+
await handle.close().catch(() => void 0);
|
|
17065
|
+
await fs2.rm(tempPath, { force: true }).catch(() => void 0);
|
|
17066
|
+
throw error45;
|
|
17067
|
+
}
|
|
17068
|
+
}
|
|
16699
17069
|
async function replaceLargeGbkFileText(input) {
|
|
16700
17070
|
const tempPath = path2.join(
|
|
16701
17071
|
path2.dirname(input.filePath),
|
|
@@ -16742,6 +17112,7 @@ async function replaceLargeGbkFileText(input) {
|
|
|
16742
17112
|
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
16743
17113
|
await fs2.chmod(tempPath, mode);
|
|
16744
17114
|
await fs2.rename(tempPath, input.filePath);
|
|
17115
|
+
invalidateGbkLineIndex(input.filePath);
|
|
16745
17116
|
return {
|
|
16746
17117
|
filePath: input.filePath,
|
|
16747
17118
|
encoding: input.encoding,
|
|
@@ -16766,8 +17137,18 @@ async function replaceGbkFileText(input) {
|
|
|
16766
17137
|
if (mode === "insertAfter" || mode === "insertBefore") {
|
|
16767
17138
|
assertInsertArguments(input);
|
|
16768
17139
|
const resolved2 = await resolveReadableGbkFile(normalizedInput);
|
|
16769
|
-
const current2 = await readWholeGbkTextFile(resolved2);
|
|
16770
17140
|
const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
|
|
17141
|
+
if (resolved2.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
|
|
17142
|
+
return await replaceLargeGbkFileByAnchor({
|
|
17143
|
+
...resolved2,
|
|
17144
|
+
mode,
|
|
17145
|
+
anchor: input.anchor,
|
|
17146
|
+
content: input.content,
|
|
17147
|
+
occurrence,
|
|
17148
|
+
ifExists: input.ifExists ?? "skip"
|
|
17149
|
+
});
|
|
17150
|
+
}
|
|
17151
|
+
const current2 = await readWholeGbkTextFile(resolved2);
|
|
16771
17152
|
const insertResult = insertByAnchor(
|
|
16772
17153
|
current2.content,
|
|
16773
17154
|
mode,
|
|
@@ -16812,24 +17193,47 @@ async function replaceGbkFileText(input) {
|
|
|
16812
17193
|
}
|
|
16813
17194
|
const replaceAll = normalizedInput.replaceAll ?? false;
|
|
16814
17195
|
const resolved = await resolveReadableGbkFile(normalizedInput);
|
|
16815
|
-
const
|
|
17196
|
+
const prefixedBlock = parseLineNumberPrefixedBlock(input.oldString);
|
|
17197
|
+
const derivedScopedRange = prefixedBlock?.isContiguous ? {
|
|
17198
|
+
startLine: normalizedInput.startLine ?? prefixedBlock.startLine,
|
|
17199
|
+
endLine: normalizedInput.endLine ?? prefixedBlock.endLine,
|
|
17200
|
+
oldString: prefixedBlock.strippedText
|
|
17201
|
+
} : null;
|
|
17202
|
+
const effectiveOldString = derivedScopedRange?.oldString ?? input.oldString;
|
|
17203
|
+
const hasScopedRange = normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || normalizedInput.startAnchor !== void 0 || normalizedInput.endAnchor !== void 0 || derivedScopedRange !== null;
|
|
16816
17204
|
if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && !hasScopedRange) {
|
|
16817
17205
|
return await replaceLargeGbkFileText({
|
|
16818
17206
|
...resolved,
|
|
16819
|
-
oldString:
|
|
17207
|
+
oldString: effectiveOldString,
|
|
16820
17208
|
newString: input.newString,
|
|
16821
17209
|
replaceAll
|
|
16822
17210
|
});
|
|
16823
17211
|
}
|
|
17212
|
+
if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && normalizedInput.startAnchor === void 0 && normalizedInput.endAnchor === void 0 && (normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || derivedScopedRange !== null)) {
|
|
17213
|
+
return await replaceLargeGbkFileTextInLineRange({
|
|
17214
|
+
...resolved,
|
|
17215
|
+
oldString: effectiveOldString,
|
|
17216
|
+
newString: input.newString,
|
|
17217
|
+
replaceAll,
|
|
17218
|
+
startLine: derivedScopedRange?.startLine ?? normalizedInput.startLine,
|
|
17219
|
+
endLine: derivedScopedRange?.endLine ?? normalizedInput.endLine
|
|
17220
|
+
});
|
|
17221
|
+
}
|
|
16824
17222
|
const current = await readWholeGbkTextFile(resolved);
|
|
16825
|
-
const
|
|
16826
|
-
|
|
17223
|
+
const scopedInput = derivedScopedRange === null ? normalizedInput : {
|
|
17224
|
+
...normalizedInput,
|
|
17225
|
+
startLine: derivedScopedRange.startLine,
|
|
17226
|
+
endLine: derivedScopedRange.endLine
|
|
17227
|
+
};
|
|
17228
|
+
const scope = resolveEditScope(current.content, scopedInput);
|
|
17229
|
+
const occurrencesBefore = countOccurrences(scope.selectedText, effectiveOldString);
|
|
16827
17230
|
if (!replaceAll && occurrencesBefore === 0) {
|
|
16828
|
-
const loose = tryLooseBlockReplace(scope.selectedText,
|
|
17231
|
+
const loose = tryLooseBlockReplace(scope.selectedText, effectiveOldString, input.newString);
|
|
16829
17232
|
if (loose !== null) {
|
|
16830
17233
|
const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
|
|
16831
17234
|
const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
|
|
16832
17235
|
await fs2.writeFile(current.filePath, buffer2);
|
|
17236
|
+
invalidateGbkLineIndex(current.filePath);
|
|
16833
17237
|
return {
|
|
16834
17238
|
mode: "replace",
|
|
16835
17239
|
filePath: current.filePath,
|
|
@@ -16843,19 +17247,20 @@ async function replaceGbkFileText(input) {
|
|
|
16843
17247
|
}
|
|
16844
17248
|
if (replaceAll) {
|
|
16845
17249
|
if (occurrencesBefore === 0) {
|
|
16846
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
17250
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
|
|
16847
17251
|
}
|
|
16848
17252
|
} else if (occurrencesBefore === 0) {
|
|
16849
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
17253
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
|
|
16850
17254
|
} else if (occurrencesBefore > 1) {
|
|
16851
|
-
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${
|
|
17255
|
+
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
|
|
16852
17256
|
}
|
|
16853
17257
|
const fileNewlineStyle = detectNewlineStyle(current.content);
|
|
16854
17258
|
const alignedNewString = fileNewlineStyle === "crlf" ? input.newString.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : fileNewlineStyle === "lf" ? input.newString.replace(/\r\n/g, "\n") : input.newString;
|
|
16855
|
-
const replaced = replaceAll ? scope.selectedText.split(
|
|
17259
|
+
const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
|
|
16856
17260
|
const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
|
|
16857
17261
|
const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
|
|
16858
17262
|
await fs2.writeFile(current.filePath, buffer);
|
|
17263
|
+
invalidateGbkLineIndex(current.filePath);
|
|
16859
17264
|
return {
|
|
16860
17265
|
mode: "replace",
|
|
16861
17266
|
filePath: current.filePath,
|
|
@@ -16871,7 +17276,8 @@ async function replaceGbkFileText(input) {
|
|
|
16871
17276
|
var gbk_edit_default = tool({
|
|
16872
17277
|
description: `Edit GBK/GB18030 encoded text files with exact string replacement.
|
|
16873
17278
|
|
|
16874
|
-
|
|
17279
|
+
For whole-file replace, large files use a streaming path.
|
|
17280
|
+
For line-scoped replace, large files use a cached line-byte index and only decode the selected line window.
|
|
16875
17281
|
Safe to use on files with more than 2000 lines.
|
|
16876
17282
|
|
|
16877
17283
|
CRITICAL \u2014 do NOT include line number prefixes in oldString or newString:
|
|
@@ -16915,7 +17321,7 @@ Insert mode:
|
|
|
16915
17321
|
},
|
|
16916
17322
|
async execute(args, context) {
|
|
16917
17323
|
const result = await replaceGbkFileText({ ...args, context });
|
|
16918
|
-
const isReplace =
|
|
17324
|
+
const isReplace = "replacements" in result;
|
|
16919
17325
|
const title = isReplace ? `GBK \u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}` : result.inserted ? `GBK \u63D2\u5165 ${result.encoding.toUpperCase()} #${result.occurrence}` : `GBK \u8DF3\u8FC7 ${result.encoding.toUpperCase()} #${result.occurrence}`;
|
|
16920
17326
|
context.metadata({
|
|
16921
17327
|
title,
|