opencode-gbk-tools 0.1.5 → 0.1.7

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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: 处理 GBK/GB18030 编码文件的专用代理,只使用 gbk_read、gbk_write、gbk_edit
2
+ description: 处理 GBK/GB18030 编码文件的专用代理,只使用 gbk_read、gbk_write、gbk_edit、gbk_search
3
3
  mode: primary
4
4
  permission:
5
5
  read: deny
@@ -18,6 +18,7 @@ permission:
18
18
  - 读取文件时优先使用 `gbk_read`
19
19
  - 创建或覆盖文件时优先使用 `gbk_write`
20
20
  - 修改已有文件时优先使用 `gbk_edit`
21
+ - 在大文件中搜索内容时使用 `gbk_search`
21
22
  - 大文件读取优先使用 `gbk_read` 的分页或 `tail` 能力,避免一次读取过多内容
22
23
  - 修改时优先缩小编辑范围:能用 `startLine/endLine` 或 `startAnchor/endAnchor` 就不要全文替换
23
24
  - 文件发现可使用 `glob`,但文件内容读取必须使用 `gbk_read`
@@ -25,3 +26,14 @@ permission:
25
26
  - `edit: deny` 同时覆盖内置 `write`、`patch`、`multiedit`
26
27
  - 如果用户请求涉及 UTF-8 文件或二进制文件,先明确说明不适用
27
28
  - 若目标文件编码不确定,先提醒用户确认是 `gbk` 还是 `gb18030`
29
+
30
+ ## 大文件编辑工作流(必须遵守)
31
+
32
+ 当 `gbk_read` 返回 `truncated: true` 时,说明文件超出了读取窗口。
33
+ 此时若需要编辑文件,**必须按以下步骤操作,禁止直接猜测 oldString**:
34
+
35
+ 1. `gbk_search(filePath, pattern)` — 找到目标内容的精确行号和上下文
36
+ 2. `gbk_read(filePath, offset=<lineNumber>, limit=<N>)` — 读取目标行的精确内容
37
+ 3. `gbk_edit(filePath, oldString=<精确内容>, newString=<新内容>)` — 用精确内容替换
38
+
39
+ **禁止**在未确认精确内容的情况下构造 `oldString`,否则 `gbk_edit` 必然报 `GBK_NO_MATCH`。
@@ -16457,14 +16457,14 @@ function buildNoMatchMessage(content, oldString) {
16457
16457
  getNearestContext(content, oldString)
16458
16458
  ].join("\n");
16459
16459
  }
16460
- function tryLooseBlockReplace(content, oldString, newString) {
16461
- const normalizedContent = normalizeNewlines(content);
16462
- const contentLines = splitNormalizedLines(content);
16463
- const oldLines = trimTrailingEmptyLines(splitNormalizedLines(oldString));
16464
- const newLines = splitNormalizedLines(newString);
16465
- if (oldLines.length === 0) {
16466
- return null;
16467
- }
16460
+ function hasLineNumberPrefixes(lines) {
16461
+ const nonEmpty = lines.filter((l) => l.trim().length > 0);
16462
+ return nonEmpty.length > 0 && nonEmpty.every((l) => /^\d+: /.test(l));
16463
+ }
16464
+ function stripLineNumberPrefixes(lines) {
16465
+ return lines.map((l) => l.replace(/^\d+: /, ""));
16466
+ }
16467
+ function matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content) {
16468
16468
  for (let start = 0; start < contentLines.length; start += 1) {
16469
16469
  let contentIndex = start;
16470
16470
  let oldIndex = 0;
@@ -16494,12 +16494,32 @@ function tryLooseBlockReplace(content, oldString, newString) {
16494
16494
  ].join("\n");
16495
16495
  return {
16496
16496
  occurrencesBefore: 1,
16497
- content: detectNewlineStyle(content) === "crlf" ? replacedNormalized.replace(/\n/g, "\r\n") : replacedNormalized
16497
+ content: newlineStyle === "crlf" ? replacedNormalized.replace(/\n/g, "\r\n") : replacedNormalized
16498
16498
  };
16499
16499
  }
16500
16500
  }
16501
16501
  return null;
16502
16502
  }
16503
+ function tryLooseBlockReplace(content, oldString, newString) {
16504
+ const contentLines = splitNormalizedLines(content);
16505
+ const oldLines = trimTrailingEmptyLines(splitNormalizedLines(oldString));
16506
+ const newLines = splitNormalizedLines(newString);
16507
+ const newlineStyle = detectNewlineStyle(content);
16508
+ if (oldLines.length === 0) {
16509
+ return null;
16510
+ }
16511
+ const result = matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content);
16512
+ if (result !== null) {
16513
+ return result;
16514
+ }
16515
+ if (hasLineNumberPrefixes(oldLines)) {
16516
+ const strippedOldLines = trimTrailingEmptyLines(stripLineNumberPrefixes(oldLines));
16517
+ if (strippedOldLines.length > 0) {
16518
+ return matchLooseBlock(contentLines, strippedOldLines, newLines, newlineStyle, content);
16519
+ }
16520
+ }
16521
+ return null;
16522
+ }
16503
16523
  function countOccurrences(text, target) {
16504
16524
  if (target.length === 0) {
16505
16525
  throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
@@ -16725,12 +16745,22 @@ var gbk_edit_default = tool({
16725
16745
  Reads the FULL file content regardless of file size \u2014 not limited by gbk_read's line window.
16726
16746
  Safe to use on files with more than 2000 lines.
16727
16747
 
16748
+ CRITICAL \u2014 do NOT include line number prefixes in oldString or newString:
16749
+ gbk_read output looks like "3787: SENDMSG 0 content". The "3787: " is a navigation prefix, NOT file content.
16750
+ oldString must be the raw file content: "SENDMSG 0 content" (no line number prefix).
16751
+ Including line numbers in oldString will cause GBK_NO_MATCH even if the content exists.
16752
+
16753
+ Recommended workflow for large files (when gbk_read returned truncated=true):
16754
+ 1. gbk_search(pattern) \u2192 find exact lineNumber
16755
+ 2. gbk_read(offset=<lineNumber>, limit=20) \u2192 get the exact block (strip "N: " prefixes)
16756
+ 3. gbk_edit(oldString=<content without prefixes>, newString=<new content>)
16757
+
16728
16758
  For large files, use 'startLine'/'endLine' or 'startAnchor'/'endAnchor' to narrow the search scope
16729
16759
  and avoid false matches. Scoped edits also improve performance on very large files.`,
16730
16760
  args: {
16731
16761
  filePath: tool.schema.string().describe("Target file path"),
16732
- oldString: tool.schema.string().describe("Exact text to replace (must match file content, not gbk_read output with line numbers)"),
16733
- newString: tool.schema.string().describe("Replacement text"),
16762
+ oldString: tool.schema.string().describe("Exact text to replace \u2014 raw file content only, no 'N: ' line number prefixes from gbk_read output"),
16763
+ newString: tool.schema.string().describe("Replacement text \u2014 raw content only, no line number prefixes"),
16734
16764
  replaceAll: tool.schema.boolean().optional().describe("Replace all occurrences (default: false, requires unique match)"),
16735
16765
  startLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based start line (inclusive)"),
16736
16766
  endLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based end line (inclusive)"),
@@ -16609,14 +16609,23 @@ var gbk_read_default = tool({
16609
16609
  Returns up to 'limit' lines (default 2000) starting from 'offset'.
16610
16610
  When the file has more lines than the window, 'truncated' is true and 'totalLines' shows the full count.
16611
16611
 
16612
- IMPORTANT: If 'truncated' is true, the returned content is incomplete.
16613
- DO NOT use the returned content as 'oldString' for gbk_edit on a truncated file.
16614
- To edit content beyond the visible window, use gbk_edit with 'startLine'/'endLine' to target the exact range,
16615
- or read the specific range first with 'offset' set to the desired line number.`,
16612
+ IMPORTANT \u2014 line number format: each output line is prefixed with "N: " (e.g. "3787: content").
16613
+ These prefixes are for navigation only. Strip them before using any line as 'oldString' in gbk_edit.
16614
+
16615
+ IMPORTANT \u2014 do NOT use large limits to read the whole file:
16616
+ - Setting limit=40000 or similar to "read everything" is WRONG. It produces an enormous response
16617
+ that is unreliable to process and may be truncated by the protocol.
16618
+ - To find content in a large file, use gbk_search instead.
16619
+ - To read a specific section, set offset=<lineNumber> and limit=<small number like 10-30>.
16620
+
16621
+ Workflow when truncated=true:
16622
+ 1. gbk_search(pattern) \u2192 get exact lineNumber
16623
+ 2. gbk_read(offset=<lineNumber>, limit=20) \u2192 get the exact block
16624
+ 3. gbk_edit(oldString=<exact content without line prefixes>) \u2192 edit reliably`,
16616
16625
  args: {
16617
16626
  filePath: tool.schema.string().describe("Target file path"),
16618
16627
  offset: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("1-based start line (default: 1)"),
16619
- limit: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Number of lines to read (default: 2000). Use -1 to apply the default."),
16628
+ limit: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Number of lines to read (default: 2000, max recommended: 200). Use -1 to apply the default."),
16620
16629
  tail: tool.schema.boolean().optional().describe("Read last N lines instead of offset-based window"),
16621
16630
  encoding: tool.schema.enum(["gbk", "gb18030"]).optional().describe("Text encoding (default: gbk)"),
16622
16631
  allowExternal: tool.schema.boolean().optional().describe("Allow paths outside workspace root")