opencode-gbk-tools 0.1.27 → 0.1.28

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  为 OpenCode 提供一套自动识别编码的文本工具,以及面向 `GBK` / `GB18030` 的专用工具。
4
4
 
5
- 解决 OpenCode 内置工具难以稳定处理非 UTF-8 文本文件的问题,并通过 plugin 让所有 agents 默认获得这些工具与规则;同时提供一个只允许调用 `text_*` / `gbk_*` 自定义工具的 `gbk-engine` agent。
5
+ 解决 OpenCode 内置工具难以稳定处理非 UTF-8 文本文件的问题,并通过 plugin 让所有 agents 默认获得这些工具与规则;同时提供一个只允许调用 `gbk_*` 自定义工具的 `gbk-engine` agent。
6
6
 
7
7
  ---
8
8
 
@@ -17,8 +17,8 @@
17
17
  | `gbk_write` | 写入或追加内容到 GBK 文件(`append=true` 支持追加) |
18
18
  | `gbk_edit` | 精确替换 GBK 文件中的指定文本块 |
19
19
  | `gbk_search` | 在 GBK 文件中搜索关键词,返回行号和上下文 |
20
- | 本地/全局 plugin 规则 | 给所有 agents 注入“优先使用 `text_*`”的系统提示,并统一开放 `gbk_*` 工具 |
21
- | `gbk-engine` agent | 禁用 OpenCode 内置读写编辑工具,只保留 `text_*` / `gbk_*` 自定义工具 |
20
+ | 本地/全局 plugin 规则 | 给所有 agents 注入“默认优先使用 `gbk_*`,`text_*` 作为兼容工具”的系统提示,并统一开放 `text_*` / `gbk_*` 工具 |
21
+ | `gbk-engine` agent | 禁用 OpenCode 内置读写编辑工具,只保留 `gbk_*` 自定义工具 |
22
22
 
23
23
  ---
24
24
 
@@ -34,7 +34,7 @@ npx opencode-gbk-tools install
34
34
 
35
35
  - `text_read` / `text_write` / `text_edit`
36
36
  - `gbk_read` / `gbk_write` / `gbk_edit` / `gbk_search`
37
- - 对文本文件优先使用 `text_*` 的系统提示
37
+ - 默认优先使用 `gbk_*`、`text_*` 作为兼容工具的系统提示
38
38
  - `.opencode/agents/gbk-engine.md` 或 `~/.config/opencode/agents/gbk-engine.md`
39
39
 
40
40
  如果你希望强制禁用内置 `read` / `write` / `edit` / `apply_patch`,可以直接切换到专属 `gbk-engine` agent。
@@ -77,8 +77,8 @@ npx opencode-gbk-tools uninstall
77
77
  优先使用建议:
78
78
 
79
79
  ```text
80
- 通用文本文件:text_read / text_write / text_edit
81
- 明确 GBK 文件:gbk_read / gbk_write / gbk_edit / gbk_search
80
+ 默认优先:gbk_read / gbk_write / gbk_edit / gbk_search
81
+ 兼容性场景:text_read / text_write / text_edit
82
82
  ```
83
83
 
84
84
  编辑动作建议:
@@ -208,6 +208,7 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
208
208
 
209
209
  | 版本 | 说明 |
210
210
  |------|------|
211
+ | 0.1.28 | 对齐 plugin/agent 文档说明到当前产品行为:默认优先推荐 `gbk_*`、`text_*` 作为兼容工具,`gbk-engine` 仅保留 `gbk_*`;修复 `src/lib/gbk-file.ts` 中多处用户可见乱码错误文案;修复 `text_*` 大文件流式替换跨块命中遗漏问题并补充回归测试 |
211
212
  | 0.1.27 | 重新发布当前稳定内容,包含:新建 `.txt` 文件在 `encoding=auto` 下默认使用 `gbk`;保留已有文件的原编码检测与保持逻辑;修复 plugin 追加 system 规则时可能触发部分 Jinja/chat template 的“System message must be at the beginning”异常;同步更新工具提示、README 与测试覆盖 |
212
213
  | 0.1.26 | 仅提升发布版本号,用于重新发布当时的稳定内容 |
213
214
  | 0.1.25 | 恢复并正式发布专属 `gbk-engine` agent;通过 agent 白名单禁用内置 `read` / `write` / `edit` / `apply_patch`,只保留 `text_*` / `gbk_*` 自定义工具,并把 agent 纳入构建、安装与 release manifest |
@@ -8,9 +8,6 @@ tools:
8
8
  write: false
9
9
  edit: false
10
10
  apply_patch: false
11
- text_read: true
12
- text_write: true
13
- text_edit: true
14
11
  gbk_read: true
15
12
  gbk_write: true
16
13
  gbk_edit: true
@@ -21,12 +18,12 @@ permission:
21
18
  ---
22
19
  你是 `gbk-engine`。
23
20
 
24
- 你只能使用 `opencode-gbk-tools` 提供的 `text_*` 与 `gbk_*` 工具,不允许退回到 OpenCode 内置的 `read`、`write`、`edit`、`apply_patch` 或其他内置工具。
21
+ 你只能使用 `opencode-gbk-tools` 提供的 `gbk_*` 工具,不允许退回到 OpenCode 内置的 `read`、`write`、`edit`、`apply_patch` 或其他内置工具。
25
22
 
26
23
  工作规则:
27
24
 
28
- - 通用文本文件优先使用 `text_read`、`text_write`、`text_edit`
29
- - 只有在用户明确指定 `GBK` / `GB18030` 文件,或自动编码检测失败时,才优先使用 `gbk_*` 工具
25
+ - 所有文本文件默认优先使用 `gbk_read`、`gbk_write`、`gbk_edit`、`gbk_search`
26
+ - 不再使用 `text_read`、`text_write`、`text_edit`
30
27
  - 编辑前先读取目标文件;对大文件先搜索,再局部读取
31
28
  - 插入内容优先使用 `mode="insertAfter"` 或 `mode="insertBefore"` 搭配 `anchor` / `content`
32
29
  - 只有在精确替换现有内容时,才使用 `oldString` / `newString`
@@ -16406,45 +16406,103 @@ function assertInsertArguments(input) {
16406
16406
  throw createGbkError("GBK_INVALID_ARGUMENT", "content \u4E0D\u80FD\u4E3A\u7A7A");
16407
16407
  }
16408
16408
  if (input.replaceAll !== void 0) {
16409
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\uFFFD? replaceAll");
16409
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 replaceAll");
16410
16410
  }
16411
16411
  if (input.startLine !== void 0 || input.endLine !== void 0 || input.startAnchor !== void 0 || input.endAnchor !== void 0) {
16412
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\uFFFD? startLine/endLine/startAnchor/endAnchor");
16412
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 startLine/endLine/startAnchor/endAnchor");
16413
16413
  }
16414
16414
  }
16415
- function findOccurrenceIndex(text, token, occurrence) {
16416
- assertStringArgument(text, "text");
16417
- assertStringArgument(token, "anchor");
16418
- assertPositiveInteger(occurrence, "occurrence");
16415
+ function collectOccurrencePositions(text, token) {
16419
16416
  if (token.length === 0) {
16420
- throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
16417
+ return [];
16421
16418
  }
16422
- let count = 0;
16419
+ const positions = [];
16423
16420
  let searchFrom = 0;
16424
16421
  while (true) {
16425
16422
  const index = text.indexOf(token, searchFrom);
16426
16423
  if (index === -1) {
16427
- break;
16428
- }
16429
- count += 1;
16430
- if (count === occurrence) {
16431
- return { index, total: countOccurrences(text, token) };
16424
+ return positions;
16432
16425
  }
16426
+ positions.push(index);
16433
16427
  searchFrom = index + token.length;
16434
16428
  }
16435
- if (count === 0) {
16436
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\uFFFD?: ${token}`);
16429
+ }
16430
+ function buildFlexibleSearchVariants(token, newlineStyle) {
16431
+ const variants = /* @__PURE__ */ new Set();
16432
+ const pushVariant = (value) => {
16433
+ if (typeof value !== "string" || value.length === 0) {
16434
+ return;
16435
+ }
16436
+ variants.add(value);
16437
+ };
16438
+ pushVariant(token);
16439
+ pushVariant(alignTextToNewlineStyle(token, newlineStyle));
16440
+ const prefixedBlock = parseLineNumberPrefixedBlock(token);
16441
+ if (prefixedBlock) {
16442
+ pushVariant(prefixedBlock.strippedText);
16443
+ pushVariant(alignTextToNewlineStyle(prefixedBlock.strippedText, newlineStyle));
16444
+ }
16445
+ const trimmedLines = trimTrailingEmptyLines(splitNormalizedLines(token));
16446
+ if (trimmedLines.length > 0) {
16447
+ const trimmedToken = trimmedLines.join("\n");
16448
+ pushVariant(trimmedToken);
16449
+ pushVariant(alignTextToNewlineStyle(trimmedToken, newlineStyle));
16450
+ const trimmedPrefixedBlock = parseLineNumberPrefixedBlock(trimmedToken);
16451
+ if (trimmedPrefixedBlock) {
16452
+ pushVariant(trimmedPrefixedBlock.strippedText);
16453
+ pushVariant(alignTextToNewlineStyle(trimmedPrefixedBlock.strippedText, newlineStyle));
16454
+ }
16455
+ }
16456
+ return [...variants];
16457
+ }
16458
+ function findNextFlexibleMatch(text, token, searchFrom, newlineStyle) {
16459
+ let bestMatch = null;
16460
+ for (const candidate of buildFlexibleSearchVariants(token, newlineStyle)) {
16461
+ const index = text.indexOf(candidate, searchFrom);
16462
+ if (index === -1) {
16463
+ continue;
16464
+ }
16465
+ if (bestMatch === null || index < bestMatch.index || index === bestMatch.index && candidate.length > bestMatch.matchedToken.length) {
16466
+ bestMatch = { index, matchedToken: candidate };
16467
+ }
16437
16468
  }
16438
- throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\uFFFD? ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\uFFFD? ${occurrence} \u5904`);
16469
+ return bestMatch;
16470
+ }
16471
+ function findFlexibleOccurrenceIndex(text, token, occurrence, newlineStyle) {
16472
+ assertStringArgument(text, "text");
16473
+ assertStringArgument(token, "anchor");
16474
+ assertPositiveInteger(occurrence, "occurrence");
16475
+ if (token.length === 0) {
16476
+ throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
16477
+ }
16478
+ let maxMatches = 0;
16479
+ for (const candidate of buildFlexibleSearchVariants(token, newlineStyle)) {
16480
+ const positions = collectOccurrencePositions(text, candidate);
16481
+ if (positions.length === 0) {
16482
+ continue;
16483
+ }
16484
+ maxMatches = Math.max(maxMatches, positions.length);
16485
+ if (positions.length >= occurrence) {
16486
+ return {
16487
+ index: positions[occurrence - 1],
16488
+ total: positions.length,
16489
+ matchedToken: candidate
16490
+ };
16491
+ }
16492
+ }
16493
+ if (maxMatches === 0) {
16494
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
16495
+ }
16496
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${maxMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
16439
16497
  }
16440
16498
  function insertByAnchor(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
16441
16499
  const alignedContent = newlineStyle === "crlf" ? content.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? content.replace(/\r\n/g, "\n") : content;
16442
- const located = findOccurrenceIndex(text, anchor, occurrence);
16443
- const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
16500
+ const located = findFlexibleOccurrenceIndex(text, anchor, occurrence, newlineStyle);
16501
+ const insertionPoint = mode === "insertAfter" ? located.index + located.matchedToken.length : located.index;
16444
16502
  const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
16445
16503
  if (alreadyExists) {
16446
16504
  if (ifExists === "error") {
16447
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
16505
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
16448
16506
  }
16449
16507
  if (ifExists === "skip") {
16450
16508
  return {
@@ -16485,7 +16543,7 @@ function normalizeOptionalPositiveInteger(value, name) {
16485
16543
  }
16486
16544
  function assertNotBinary(buffer) {
16487
16545
  if (buffer.includes(0)) {
16488
- throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\uFFFD? GBK \u6587\u672C\u5904\u7406");
16546
+ throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6309 GBK \u6587\u672C\u5904\u7406");
16489
16547
  }
16490
16548
  }
16491
16549
  function detectNewlineStyle(text) {
@@ -16563,19 +16621,15 @@ function applyAnchors(text, startAnchor, endAnchor) {
16563
16621
  }
16564
16622
  let rangeStart = 0;
16565
16623
  let rangeEnd = text.length;
16624
+ const newlineStyle = detectNewlineStyle(text);
16566
16625
  if (startAnchor) {
16567
- const found = text.indexOf(startAnchor);
16568
- if (found === -1) {
16569
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8D77\u59CB\u951A\uFFFD?: ${startAnchor}`);
16570
- }
16571
- rangeStart = found + startAnchor.length;
16626
+ const found = findFlexibleOccurrenceIndex(text, startAnchor, 1, newlineStyle);
16627
+ rangeStart = found.index + found.matchedToken.length;
16572
16628
  }
16573
16629
  if (endAnchor) {
16574
- const found = text.indexOf(endAnchor, rangeStart);
16575
- if (found === -1) {
16576
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u7ED3\u675F\u951A\uFFFD?: ${endAnchor}`);
16577
- }
16578
- rangeEnd = found;
16630
+ const searchText = text.slice(rangeStart);
16631
+ const found = findFlexibleOccurrenceIndex(searchText, endAnchor, 1, newlineStyle);
16632
+ rangeEnd = rangeStart + found.index;
16579
16633
  }
16580
16634
  if (rangeEnd < rangeStart) {
16581
16635
  throw createGbkError("GBK_INVALID_ARGUMENT", "\u951A\u70B9\u8303\u56F4\u65E0\u6548");
@@ -16626,9 +16680,9 @@ function getNearestContext(content, oldString) {
16626
16680
  }
16627
16681
  function buildNoMatchMessage(content, oldString) {
16628
16682
  return [
16629
- "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\uFFFD?",
16630
- "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\uFFFD?/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\uFFFD?",
16631
- "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFFFD?",
16683
+ "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\u3002",
16684
+ "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\u884C/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\u3002",
16685
+ "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFF1A",
16632
16686
  getNearestContext(content, oldString)
16633
16687
  ].join("\n");
16634
16688
  }
@@ -16640,8 +16694,10 @@ function stripLineNumberPrefixes(lines) {
16640
16694
  return lines.map((l) => l.replace(/^\d+: /, ""));
16641
16695
  }
16642
16696
  function parseLineNumberPrefixedBlock(text) {
16643
- const lines = splitNormalizedLines(text);
16644
- if (!hasLineNumberPrefixes(lines)) {
16697
+ const normalizedText = normalizeNewlines(text);
16698
+ const hasTrailingNewline = normalizedText.endsWith("\n");
16699
+ const lines = trimTrailingEmptyLines(splitNormalizedLines(text));
16700
+ if (lines.length === 0 || !hasLineNumberPrefixes(lines)) {
16645
16701
  return null;
16646
16702
  }
16647
16703
  const lineNumbers = [];
@@ -16656,7 +16712,7 @@ function parseLineNumberPrefixedBlock(text) {
16656
16712
  }
16657
16713
  return {
16658
16714
  lineNumbers,
16659
- strippedText: strippedLines.join("\n"),
16715
+ strippedText: `${strippedLines.join("\n")}${hasTrailingNewline ? "\n" : ""}`,
16660
16716
  isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
16661
16717
  startLine: lineNumbers[0],
16662
16718
  endLine: lineNumbers[lineNumbers.length - 1]
@@ -16751,10 +16807,10 @@ async function resolveReadableGbkFile(input) {
16751
16807
  try {
16752
16808
  stat = await fs2.stat(candidatePath);
16753
16809
  } catch (error45) {
16754
- throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\uFFFD?: ${candidatePath}`, error45);
16810
+ throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
16755
16811
  }
16756
16812
  if (stat.isDirectory()) {
16757
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
16813
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
16758
16814
  }
16759
16815
  return {
16760
16816
  filePath: candidatePath,
@@ -16951,7 +17007,7 @@ function replaceScopedTextContent(scopeText, oldString, newString, replaceAll, n
16951
17007
  } else if (occurrencesBefore === 0) {
16952
17008
  throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
16953
17009
  } else if (occurrencesBefore > 1) {
16954
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${oldString}`);
17010
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${oldString}`);
16955
17011
  }
16956
17012
  const alignedNewString = alignTextToNewlineStyle(newString, newlineStyle);
16957
17013
  return {
@@ -17009,14 +17065,17 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17009
17065
  }
17010
17066
  }
17011
17067
  async function replaceLargeGbkFileByAnchor(input) {
17068
+ const lineIndex = await getGbkLineIndex(input);
17069
+ const newlineStyle = lineIndex.newlineStyle;
17012
17070
  const tempPath = path2.join(
17013
17071
  path2.dirname(input.filePath),
17014
17072
  `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17015
17073
  );
17016
17074
  const handle = await fs2.open(tempPath, "w");
17017
- const alignedContent = input.content.replace(/\r\n/g, "\n");
17018
- const anchorLength = input.anchor.length;
17019
- const carryLength = Math.max(anchorLength + alignedContent.length, 1);
17075
+ const alignedContent = alignTextToNewlineStyle(input.content, newlineStyle);
17076
+ const anchorVariants = buildFlexibleSearchVariants(input.anchor, newlineStyle);
17077
+ const maxAnchorLength = anchorVariants.reduce((maxLength, candidate) => Math.max(maxLength, candidate.length), input.anchor.length);
17078
+ const carryLength = Math.max(maxAnchorLength + alignedContent.length, 1);
17020
17079
  let decoded = "";
17021
17080
  let scanFrom = 0;
17022
17081
  let totalMatches = 0;
@@ -17040,23 +17099,23 @@ async function replaceLargeGbkFileByAnchor(input) {
17040
17099
  await visitDecodedTextChunks(input, async (text) => {
17041
17100
  decoded += text;
17042
17101
  while (!inserted) {
17043
- const foundAt = decoded.indexOf(input.anchor, scanFrom);
17044
- if (foundAt === -1) {
17102
+ const located = findNextFlexibleMatch(decoded, input.anchor, scanFrom, newlineStyle);
17103
+ if (located === null) {
17045
17104
  break;
17046
17105
  }
17047
17106
  totalMatches += 1;
17048
- const afterAnchor = foundAt + anchorLength;
17107
+ const afterAnchor = located.index + located.matchedToken.length;
17049
17108
  if (totalMatches !== input.occurrence) {
17050
17109
  scanFrom = afterAnchor;
17051
17110
  continue;
17052
17111
  }
17053
- const before = decoded.slice(0, foundAt);
17112
+ const before = decoded.slice(0, located.index);
17054
17113
  const after = decoded.slice(afterAnchor);
17055
- const anchorAndAfter = decoded.slice(foundAt);
17114
+ const anchorAndAfter = decoded.slice(located.index);
17056
17115
  const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17057
17116
  if (alreadyExists) {
17058
17117
  if (input.ifExists === "error") {
17059
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
17118
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17060
17119
  }
17061
17120
  if (input.ifExists === "skip") {
17062
17121
  skipped = true;
@@ -17068,7 +17127,7 @@ async function replaceLargeGbkFileByAnchor(input) {
17068
17127
  }
17069
17128
  if (input.mode === "insertAfter") {
17070
17129
  bytesWritten += await writeEncodedText(handle, input.encoding, before);
17071
- bytesWritten += await writeEncodedText(handle, input.encoding, input.anchor);
17130
+ bytesWritten += await writeEncodedText(handle, input.encoding, located.matchedToken);
17072
17131
  bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
17073
17132
  bytesWritten += await writeEncodedText(handle, input.encoding, after);
17074
17133
  } else {
@@ -17087,23 +17146,23 @@ async function replaceLargeGbkFileByAnchor(input) {
17087
17146
  });
17088
17147
  if (!inserted) {
17089
17148
  while (true) {
17090
- const foundAt = decoded.indexOf(input.anchor, scanFrom);
17091
- if (foundAt === -1) {
17149
+ const located = findNextFlexibleMatch(decoded, input.anchor, scanFrom, newlineStyle);
17150
+ if (located === null) {
17092
17151
  break;
17093
17152
  }
17094
17153
  totalMatches += 1;
17095
- const afterAnchor = foundAt + anchorLength;
17154
+ const afterAnchor = located.index + located.matchedToken.length;
17096
17155
  if (totalMatches !== input.occurrence) {
17097
17156
  scanFrom = afterAnchor;
17098
17157
  continue;
17099
17158
  }
17100
- const before = decoded.slice(0, foundAt);
17159
+ const before = decoded.slice(0, located.index);
17101
17160
  const after = decoded.slice(afterAnchor);
17102
- const anchorAndAfter = decoded.slice(foundAt);
17161
+ const anchorAndAfter = decoded.slice(located.index);
17103
17162
  const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17104
17163
  if (alreadyExists) {
17105
17164
  if (input.ifExists === "error") {
17106
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
17165
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17107
17166
  }
17108
17167
  if (input.ifExists === "skip") {
17109
17168
  skipped = true;
@@ -17111,16 +17170,16 @@ async function replaceLargeGbkFileByAnchor(input) {
17111
17170
  break;
17112
17171
  }
17113
17172
  }
17114
- decoded = input.mode === "insertAfter" ? `${before}${input.anchor}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
17173
+ decoded = input.mode === "insertAfter" ? `${before}${located.matchedToken}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
17115
17174
  inserted = true;
17116
17175
  break;
17117
17176
  }
17118
17177
  }
17119
17178
  if (!inserted && totalMatches === 0) {
17120
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\uFFFD?: ${input.anchor}`);
17179
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${input.anchor}`);
17121
17180
  }
17122
17181
  if (!inserted && totalMatches > 0) {
17123
- throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\uFFFD? ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\uFFFD? ${input.occurrence} \u5904`);
17182
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\u5230 ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${input.occurrence} \u5904`);
17124
17183
  }
17125
17184
  await finalizeInserted();
17126
17185
  await handle.close();
@@ -17147,35 +17206,57 @@ async function replaceLargeGbkFileByAnchor(input) {
17147
17206
  }
17148
17207
  }
17149
17208
  async function replaceLargeGbkFileText(input) {
17209
+ const lineIndex = await getGbkLineIndex(input);
17210
+ const newlineStyle = lineIndex.newlineStyle;
17150
17211
  const tempPath = path2.join(
17151
17212
  path2.dirname(input.filePath),
17152
17213
  `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17153
17214
  );
17154
17215
  const handle = await fs2.open(tempPath, "w");
17155
17216
  const carryLength = Math.max(input.oldString.length - 1, 0);
17217
+ const alignedNewString = alignTextToNewlineStyle(input.newString, newlineStyle);
17156
17218
  let carry = "";
17157
17219
  let occurrencesBefore = 0;
17158
17220
  let bytesWritten = 0;
17221
+ let replacedFirstMatch = false;
17159
17222
  const flushText = async (text, flush = false) => {
17160
17223
  const combined = carry + text;
17161
- const splitAt = flush ? combined.length : Math.max(0, combined.length - carryLength);
17162
- const processable = combined.slice(0, splitAt);
17163
- carry = combined.slice(splitAt);
17164
- if (processable.length === 0) {
17224
+ if (combined.length === 0) {
17165
17225
  return;
17166
17226
  }
17167
- const matchCount = countOccurrences(processable, input.oldString);
17168
- const seenBefore = occurrencesBefore;
17169
- occurrencesBefore += matchCount;
17170
- let output = processable;
17171
- if (input.replaceAll) {
17172
- if (matchCount > 0) {
17173
- output = processable.split(input.oldString).join(input.newString);
17227
+ const safeEnd = flush ? combined.length : Math.max(0, combined.length - carryLength);
17228
+ const outputParts = [];
17229
+ let cursor = 0;
17230
+ let flushUpto = safeEnd;
17231
+ while (true) {
17232
+ const index = combined.indexOf(input.oldString, cursor);
17233
+ if (index === -1) {
17234
+ break;
17235
+ }
17236
+ const matchEnd = index + input.oldString.length;
17237
+ if (!flush && matchEnd > safeEnd) {
17238
+ flushUpto = index;
17239
+ break;
17174
17240
  }
17175
- } else if (seenBefore === 0 && matchCount > 0) {
17176
- output = processable.replace(input.oldString, input.newString);
17241
+ occurrencesBefore += 1;
17242
+ outputParts.push(combined.slice(cursor, index));
17243
+ if (input.replaceAll) {
17244
+ outputParts.push(alignedNewString);
17245
+ } else if (!replacedFirstMatch) {
17246
+ outputParts.push(alignedNewString);
17247
+ replacedFirstMatch = true;
17248
+ } else {
17249
+ outputParts.push(input.oldString);
17250
+ }
17251
+ cursor = matchEnd;
17252
+ }
17253
+ outputParts.push(combined.slice(cursor, flushUpto));
17254
+ carry = combined.slice(flushUpto);
17255
+ const processable = outputParts.join("");
17256
+ if (processable.length === 0) {
17257
+ return;
17177
17258
  }
17178
- bytesWritten += await writeEncodedText(handle, input.encoding, output);
17259
+ bytesWritten += await writeEncodedText(handle, input.encoding, processable);
17179
17260
  };
17180
17261
  try {
17181
17262
  await visitDecodedTextChunks(input, async (text) => {
@@ -17183,10 +17264,10 @@ async function replaceLargeGbkFileText(input) {
17183
17264
  });
17184
17265
  await flushText("", true);
17185
17266
  if (occurrencesBefore === 0) {
17186
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\uFFFD?: ${input.oldString}`);
17267
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
17187
17268
  }
17188
17269
  if (!input.replaceAll && occurrencesBefore > 1) {
17189
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${input.oldString}`);
17270
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
17190
17271
  }
17191
17272
  await handle.close();
17192
17273
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
@@ -17335,7 +17416,7 @@ async function replaceGbkFileText(input) {
17335
17416
  } else if (occurrencesBefore === 0) {
17336
17417
  throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
17337
17418
  } else if (occurrencesBefore > 1) {
17338
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${effectiveOldString}`);
17419
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
17339
17420
  }
17340
17421
  const fileNewlineStyle = detectNewlineStyle(current.content);
17341
17422
  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;
@@ -16329,7 +16329,7 @@ function normalizeOptionalPositiveInteger(value, name) {
16329
16329
  }
16330
16330
  function assertNotBinary(buffer) {
16331
16331
  if (buffer.includes(0)) {
16332
- throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\uFFFD? GBK \u6587\u672C\u5904\u7406");
16332
+ throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6309 GBK \u6587\u672C\u5904\u7406");
16333
16333
  }
16334
16334
  }
16335
16335
  function splitLinesWithNumbers(text, offset = 1, limit = 2e3) {
@@ -16420,10 +16420,10 @@ async function resolveReadableGbkFile(input) {
16420
16420
  try {
16421
16421
  stat = await fs2.stat(candidatePath);
16422
16422
  } catch (error45) {
16423
- throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\uFFFD?: ${candidatePath}`, error45);
16423
+ throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
16424
16424
  }
16425
16425
  if (stat.isDirectory()) {
16426
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
16426
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
16427
16427
  }
16428
16428
  return {
16429
16429
  filePath: candidatePath,
@@ -16313,7 +16313,7 @@ function assertEncodingSupported(encoding) {
16313
16313
  }
16314
16314
  function assertNotBinary(buffer) {
16315
16315
  if (buffer.includes(0)) {
16316
- throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\uFFFD? GBK \u6587\u672C\u5904\u7406");
16316
+ throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6309 GBK \u6587\u672C\u5904\u7406");
16317
16317
  }
16318
16318
  }
16319
16319
  async function resolveReadableGbkFile(input) {
@@ -16324,10 +16324,10 @@ async function resolveReadableGbkFile(input) {
16324
16324
  try {
16325
16325
  stat = await fs2.stat(candidatePath);
16326
16326
  } catch (error45) {
16327
- throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\uFFFD?: ${candidatePath}`, error45);
16327
+ throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
16328
16328
  }
16329
16329
  if (stat.isDirectory()) {
16330
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
16330
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
16331
16331
  }
16332
16332
  return {
16333
16333
  filePath: candidatePath,
@@ -16370,10 +16370,10 @@ async function writeGbkFile(input) {
16370
16370
  try {
16371
16371
  const stat = await fs2.stat(candidatePath);
16372
16372
  if (stat.isDirectory()) {
16373
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
16373
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
16374
16374
  }
16375
16375
  if (!overwrite) {
16376
- throw createGbkError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\uFFFD?: ${candidatePath}`);
16376
+ throw createGbkError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\u5728: ${candidatePath}`);
16377
16377
  }
16378
16378
  } catch (error45) {
16379
16379
  if (error45 instanceof Error && "code" in error45) {