opencode-gbk-tools 0.1.9 → 0.1.11
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 +68 -5
- package/dist/cli/index.js +3 -3
- package/dist/opencode-tools/gbk_edit.js +165 -10
- package/dist/opencode-tools/gbk_read.js +13 -0
- package/dist/opencode-tools/gbk_search.js +9 -0
- package/dist/opencode-tools/gbk_write.js +12 -0
- package/dist/opencode-tools/text_edit.js +17016 -0
- package/dist/opencode-tools/text_read.js +16652 -0
- package/dist/opencode-tools/text_write.js +16670 -0
- package/dist/plugin/index.js +1094 -37
- package/dist/plugins/opencode-gbk-tools.js +30 -0
- package/dist/release-manifest.json +29 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# opencode-gbk-tools
|
|
2
2
|
|
|
3
|
-
为 OpenCode
|
|
3
|
+
为 OpenCode 提供一套自动识别编码的文本工具,以及面向 `GBK` / `GB18030` 的专用工具。
|
|
4
4
|
|
|
5
|
-
解决 OpenCode
|
|
5
|
+
解决 OpenCode 内置工具难以稳定处理非 UTF-8 文本文件的问题,并让所有 agents 默认优先使用可保留原编码的 `text_*` 工具。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
| 工具 | 用途 |
|
|
12
12
|
|------|------|
|
|
13
|
+
| `text_read` | 自动识别文件编码并按行读取,优先用于通用文本文件 |
|
|
14
|
+
| `text_write` | 自动保持已有文件编码、BOM 和换行风格后写入 |
|
|
15
|
+
| `text_edit` | 自动保持已有文件编码、BOM 和换行风格后编辑 |
|
|
13
16
|
| `gbk_read` | 读取 GBK/GB18030 文件,支持分页、尾部预览 |
|
|
14
17
|
| `gbk_write` | 写入或追加内容到 GBK 文件(`append=true` 支持追加) |
|
|
15
18
|
| `gbk_edit` | 精确替换 GBK 文件中的指定文本块 |
|
|
16
19
|
| `gbk_search` | 在 GBK 文件中搜索关键词,返回行号和上下文 |
|
|
17
|
-
|
|
|
20
|
+
| 本地/全局 plugin 规则 | 给所有 agents 注入“优先使用 `text_*`”的系统提示 |
|
|
18
21
|
|
|
19
22
|
---
|
|
20
23
|
|
|
@@ -42,6 +45,57 @@ npx opencode-gbk-tools uninstall
|
|
|
42
45
|
|
|
43
46
|
## 工具使用说明
|
|
44
47
|
|
|
48
|
+
### text_read / text_write / text_edit
|
|
49
|
+
|
|
50
|
+
- 默认 `encoding=auto`
|
|
51
|
+
- 已有文件会尽量保持:
|
|
52
|
+
- 原编码
|
|
53
|
+
- BOM
|
|
54
|
+
- 换行风格
|
|
55
|
+
- 当前支持:
|
|
56
|
+
- `utf8`
|
|
57
|
+
- `utf8-bom`
|
|
58
|
+
- `utf16le`
|
|
59
|
+
- `utf16be`
|
|
60
|
+
- `gbk`
|
|
61
|
+
- `gb18030`
|
|
62
|
+
|
|
63
|
+
优先使用建议:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
通用文本文件:text_read / text_write / text_edit
|
|
67
|
+
明确 GBK 文件:gbk_read / gbk_write / gbk_edit / gbk_search
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
编辑动作建议:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
插入到标签前后:用 mode=insertAfter / insertBefore + anchor/content
|
|
74
|
+
精确替换现有文本:用 oldString/newString
|
|
75
|
+
文件末尾追加:用 text_write/gbk_write + append=true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### text_edit — 通用文本编辑
|
|
79
|
+
|
|
80
|
+
插入模式示例:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
text_edit(filePath="文件路径", mode="insertAfter", anchor="[@SkipCheck_Source]", content="\n你好")
|
|
84
|
+
text_edit(filePath="文件路径", mode="insertBefore", anchor="[@SkipCheck_Source]", content="你好\n")
|
|
85
|
+
text_edit(filePath="文件路径", mode="insertAfter", anchor="[@标签]", content="\n你好", occurrence=2, ifExists="skip")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
替换模式示例:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
text_edit(filePath="文件路径", oldString="原文内容", newString="新内容")
|
|
92
|
+
text_edit(filePath="文件路径", oldString="原文", newString="新文", startLine=100, endLine=200)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- `mode` 默认是 `replace`
|
|
96
|
+
- 插入模式下不需要 `oldString`
|
|
97
|
+
- `ifExists` 默认 `skip`,可避免重复插入
|
|
98
|
+
|
|
45
99
|
### gbk_read — 读取文件
|
|
46
100
|
|
|
47
101
|
```
|
|
@@ -67,9 +121,12 @@ gbk_search(filePath="文件路径", pattern="[@标签名]", contextLines=5)
|
|
|
67
121
|
```
|
|
68
122
|
gbk_edit(filePath="文件路径", oldString="原文内容", newString="新内容")
|
|
69
123
|
gbk_edit(filePath="文件路径", oldString="原文", newString="新文", startLine=100, endLine=200)
|
|
124
|
+
gbk_edit(filePath="文件路径", mode="insertAfter", anchor="[@SkipCheck_Source]", content="\n你好")
|
|
70
125
|
```
|
|
71
126
|
|
|
72
|
-
|
|
127
|
+
**推荐:如果你的意图是“在标签后插入一行”,优先使用 `mode="insertAfter"` + `anchor/content`,不要再强依赖 `oldString`。**
|
|
128
|
+
|
|
129
|
+
**只有在精确替换已有文本时,`oldString` 才是推荐方案。并且 `oldString` 必须是文件的原始内容,不能包含 `gbk_read` 输出的行号前缀。**
|
|
73
130
|
|
|
74
131
|
错误示范(包含行号前缀,会失败):
|
|
75
132
|
```
|
|
@@ -108,7 +165,8 @@ gbk_write(filePath="文件路径", content="\r\n新增内容", append=true) #
|
|
|
108
165
|
## 已知限制
|
|
109
166
|
|
|
110
167
|
- 只支持文本文件,不支持二进制文件
|
|
111
|
-
-
|
|
168
|
+
- 自动识别目前只覆盖 `utf8`、`utf8-bom`、`utf16le`、`utf16be`、`gbk`、`gb18030`
|
|
169
|
+
- 编码检测在歧义场景下可能返回 `TEXT_UNKNOWN_ENCODING`,此时应显式指定 `encoding`
|
|
112
170
|
- 对无法映射的字符沿用 `iconv-lite` 默认替代行为
|
|
113
171
|
|
|
114
172
|
---
|
|
@@ -127,12 +185,17 @@ A:全局安装目录为 `C:\Users\你的用户名\.config\opencode\tools\`
|
|
|
127
185
|
**Q:gbk_edit 提示"未找到需要替换的文本"?**
|
|
128
186
|
A:检查 `oldString` 是否包含了行号前缀(如 `"3787: "`),去掉行号前缀后重试。若要在文件末尾追加内容,请使用 `gbk_write [append=true]`。
|
|
129
187
|
|
|
188
|
+
**Q:我只是想在某个标签后插入一行,还要不要传 `oldString`?**
|
|
189
|
+
A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,并传 `anchor/content`。
|
|
190
|
+
|
|
130
191
|
---
|
|
131
192
|
|
|
132
193
|
## 版本历史
|
|
133
194
|
|
|
134
195
|
| 版本 | 说明 |
|
|
135
196
|
|------|------|
|
|
197
|
+
| 0.1.11 | `text_edit` / `gbk_edit` 新增 `insertAfter` / `insertBefore`,插入内容不再依赖 `oldString`;README 与全局提示同步改为“插入优先用 anchor/content” |
|
|
198
|
+
| 0.1.10 | 新增 `text_read` / `text_write` / `text_edit`,自动识别并保持原编码、BOM 与换行风格;同时通过 plugin 规则让所有 agents 默认优先使用 `text_*` |
|
|
136
199
|
| 0.1.9 | `gbk_edit` 修复精确匹配路径写入 CRLF 文件时换行风格变 mixed 的 bug |
|
|
137
200
|
| 0.1.8 | `gbk_write` 新增 `append=true` 追加模式 |
|
|
138
201
|
| 0.1.7 | `gbk_edit` 自动剥离行号前缀后重试匹配 |
|
package/dist/cli/index.js
CHANGED
|
@@ -60,7 +60,7 @@ 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 ?? ["tool", "agent"]);
|
|
63
|
+
const allowedArtifacts = new Set(args.artifacts ?? ["tool", "agent", "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);
|
|
@@ -70,7 +70,7 @@ async function installCommand(args) {
|
|
|
70
70
|
throw new Error(`\u68C0\u6D4B\u5230\u672A\u53D7\u7BA1\u6587\u4EF6\u51B2\u7A81: ${targetPath}`);
|
|
71
71
|
}
|
|
72
72
|
await ensureDir(path3.dirname(targetPath));
|
|
73
|
-
const sourceRoot = artifact.kind === "tool" ? path3.join(args.packageRoot, "dist", "opencode-tools") : path3.join(args.packageRoot, "dist", "agents");
|
|
73
|
+
const sourceRoot = artifact.kind === "tool" ? path3.join(args.packageRoot, "dist", "opencode-tools") : artifact.kind === "agent" ? path3.join(args.packageRoot, "dist", "agents") : path3.join(args.packageRoot, "dist", "plugins");
|
|
74
74
|
await fs3.copyFile(path3.join(sourceRoot, path3.basename(artifact.relativePath)), targetPath);
|
|
75
75
|
}
|
|
76
76
|
const installedManifest = {
|
|
@@ -181,7 +181,7 @@ async function setupCommand(args) {
|
|
|
181
181
|
await fs6.mkdir(configBase, { recursive: true });
|
|
182
182
|
const configPath = await resolveConfigFile(configBase);
|
|
183
183
|
await ensurePluginConfigured(configPath, pluginName);
|
|
184
|
-
const installResult = await installCommand({ ...args, force: true, artifacts: ["agent"] });
|
|
184
|
+
const installResult = await installCommand({ ...args, force: true, artifacts: ["agent", "plugin"] });
|
|
185
185
|
return {
|
|
186
186
|
configPath,
|
|
187
187
|
targetBase: installResult.targetBase
|
|
@@ -16305,6 +16305,82 @@ 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
|
+
function assertStringArgument(value, name) {
|
|
16309
|
+
if (typeof value !== "string") {
|
|
16310
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
16311
|
+
}
|
|
16312
|
+
}
|
|
16313
|
+
function assertReplaceArguments(input) {
|
|
16314
|
+
assertStringArgument(input.oldString, "oldString");
|
|
16315
|
+
assertStringArgument(input.newString, "newString");
|
|
16316
|
+
}
|
|
16317
|
+
function assertInsertArguments(input) {
|
|
16318
|
+
assertStringArgument(input.anchor, "anchor");
|
|
16319
|
+
assertStringArgument(input.content, "content");
|
|
16320
|
+
if (input.content.length === 0) {
|
|
16321
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "content \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16322
|
+
}
|
|
16323
|
+
if (input.replaceAll !== void 0) {
|
|
16324
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 replaceAll");
|
|
16325
|
+
}
|
|
16326
|
+
if (input.startLine !== void 0 || input.endLine !== void 0 || input.startAnchor !== void 0 || input.endAnchor !== void 0) {
|
|
16327
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 startLine/endLine/startAnchor/endAnchor");
|
|
16328
|
+
}
|
|
16329
|
+
}
|
|
16330
|
+
function findOccurrenceIndex(text, token, occurrence) {
|
|
16331
|
+
assertStringArgument(text, "text");
|
|
16332
|
+
assertStringArgument(token, "anchor");
|
|
16333
|
+
assertPositiveInteger(occurrence, "occurrence");
|
|
16334
|
+
if (token.length === 0) {
|
|
16335
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16336
|
+
}
|
|
16337
|
+
let count = 0;
|
|
16338
|
+
let searchFrom = 0;
|
|
16339
|
+
while (true) {
|
|
16340
|
+
const index = text.indexOf(token, searchFrom);
|
|
16341
|
+
if (index === -1) {
|
|
16342
|
+
break;
|
|
16343
|
+
}
|
|
16344
|
+
count += 1;
|
|
16345
|
+
if (count === occurrence) {
|
|
16346
|
+
return { index, total: countOccurrences(text, token) };
|
|
16347
|
+
}
|
|
16348
|
+
searchFrom = index + token.length;
|
|
16349
|
+
}
|
|
16350
|
+
if (count === 0) {
|
|
16351
|
+
throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
|
|
16352
|
+
}
|
|
16353
|
+
throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
|
|
16354
|
+
}
|
|
16355
|
+
function insertByAnchor(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
|
|
16356
|
+
const alignedContent = newlineStyle === "crlf" ? content.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? content.replace(/\r\n/g, "\n") : content;
|
|
16357
|
+
const located = findOccurrenceIndex(text, anchor, occurrence);
|
|
16358
|
+
const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
|
|
16359
|
+
const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
|
|
16360
|
+
if (alreadyExists) {
|
|
16361
|
+
if (ifExists === "error") {
|
|
16362
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
|
|
16363
|
+
}
|
|
16364
|
+
if (ifExists === "skip") {
|
|
16365
|
+
return {
|
|
16366
|
+
outputText: text,
|
|
16367
|
+
inserted: false,
|
|
16368
|
+
skipped: true,
|
|
16369
|
+
anchorMatches: located.total,
|
|
16370
|
+
occurrence,
|
|
16371
|
+
anchor
|
|
16372
|
+
};
|
|
16373
|
+
}
|
|
16374
|
+
}
|
|
16375
|
+
return {
|
|
16376
|
+
outputText: `${text.slice(0, insertionPoint)}${alignedContent}${text.slice(insertionPoint)}`,
|
|
16377
|
+
inserted: true,
|
|
16378
|
+
skipped: false,
|
|
16379
|
+
anchorMatches: located.total,
|
|
16380
|
+
occurrence,
|
|
16381
|
+
anchor
|
|
16382
|
+
};
|
|
16383
|
+
}
|
|
16308
16384
|
function assertEncodingSupported(encoding) {
|
|
16309
16385
|
if (encoding !== "gbk" && encoding !== "gb18030") {
|
|
16310
16386
|
throw createGbkError("GBK_INVALID_ENCODING", `\u4E0D\u652F\u6301\u7684\u7F16\u7801: ${encoding}`);
|
|
@@ -16423,9 +16499,11 @@ function resolveEditScope(text, input) {
|
|
|
16423
16499
|
};
|
|
16424
16500
|
}
|
|
16425
16501
|
function normalizeNewlines(text) {
|
|
16502
|
+
assertStringArgument(text, "text");
|
|
16426
16503
|
return text.replace(/\r\n/g, "\n");
|
|
16427
16504
|
}
|
|
16428
16505
|
function trimRightSpaces(text) {
|
|
16506
|
+
assertStringArgument(text, "text");
|
|
16429
16507
|
return text.replace(/[ \t]+$/g, "");
|
|
16430
16508
|
}
|
|
16431
16509
|
function splitNormalizedLines(text) {
|
|
@@ -16521,6 +16599,8 @@ function tryLooseBlockReplace(content, oldString, newString) {
|
|
|
16521
16599
|
return null;
|
|
16522
16600
|
}
|
|
16523
16601
|
function countOccurrences(text, target) {
|
|
16602
|
+
assertStringArgument(text, "text");
|
|
16603
|
+
assertStringArgument(target, "oldString");
|
|
16524
16604
|
if (target.length === 0) {
|
|
16525
16605
|
throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16526
16606
|
}
|
|
@@ -16677,11 +16757,56 @@ async function replaceLargeGbkFileText(input) {
|
|
|
16677
16757
|
}
|
|
16678
16758
|
}
|
|
16679
16759
|
async function replaceGbkFileText(input) {
|
|
16760
|
+
const mode = input.mode ?? "replace";
|
|
16680
16761
|
const normalizedInput = {
|
|
16681
16762
|
...input,
|
|
16682
16763
|
startLine: normalizeOptionalPositiveInteger(input.startLine, "startLine"),
|
|
16683
16764
|
endLine: normalizeOptionalPositiveInteger(input.endLine, "endLine")
|
|
16684
16765
|
};
|
|
16766
|
+
if (mode === "insertAfter" || mode === "insertBefore") {
|
|
16767
|
+
assertInsertArguments(input);
|
|
16768
|
+
const resolved2 = await resolveReadableGbkFile(normalizedInput);
|
|
16769
|
+
const current2 = await readWholeGbkTextFile(resolved2);
|
|
16770
|
+
const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
|
|
16771
|
+
const insertResult = insertByAnchor(
|
|
16772
|
+
current2.content,
|
|
16773
|
+
mode,
|
|
16774
|
+
input.anchor,
|
|
16775
|
+
input.content,
|
|
16776
|
+
occurrence,
|
|
16777
|
+
input.ifExists ?? "skip",
|
|
16778
|
+
detectNewlineStyle(current2.content)
|
|
16779
|
+
);
|
|
16780
|
+
if (insertResult.skipped) {
|
|
16781
|
+
return {
|
|
16782
|
+
mode,
|
|
16783
|
+
filePath: current2.filePath,
|
|
16784
|
+
encoding: current2.encoding,
|
|
16785
|
+
anchor: insertResult.anchor,
|
|
16786
|
+
occurrence: insertResult.occurrence,
|
|
16787
|
+
anchorMatches: insertResult.anchorMatches,
|
|
16788
|
+
inserted: false,
|
|
16789
|
+
skipped: true,
|
|
16790
|
+
bytesRead: current2.bytesRead,
|
|
16791
|
+
bytesWritten: 0
|
|
16792
|
+
};
|
|
16793
|
+
}
|
|
16794
|
+
const buffer2 = import_iconv_lite.default.encode(insertResult.outputText, current2.encoding);
|
|
16795
|
+
await fs2.writeFile(current2.filePath, buffer2);
|
|
16796
|
+
return {
|
|
16797
|
+
mode,
|
|
16798
|
+
filePath: current2.filePath,
|
|
16799
|
+
encoding: current2.encoding,
|
|
16800
|
+
anchor: insertResult.anchor,
|
|
16801
|
+
occurrence: insertResult.occurrence,
|
|
16802
|
+
anchorMatches: insertResult.anchorMatches,
|
|
16803
|
+
inserted: true,
|
|
16804
|
+
skipped: false,
|
|
16805
|
+
bytesRead: current2.bytesRead,
|
|
16806
|
+
bytesWritten: buffer2.byteLength
|
|
16807
|
+
};
|
|
16808
|
+
}
|
|
16809
|
+
assertReplaceArguments(input);
|
|
16685
16810
|
if (input.oldString.length === 0) {
|
|
16686
16811
|
throw createGbkError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16687
16812
|
}
|
|
@@ -16698,14 +16823,15 @@ async function replaceGbkFileText(input) {
|
|
|
16698
16823
|
}
|
|
16699
16824
|
const current = await readWholeGbkTextFile(resolved);
|
|
16700
16825
|
const scope = resolveEditScope(current.content, normalizedInput);
|
|
16701
|
-
const occurrencesBefore = countOccurrences(scope.selectedText,
|
|
16826
|
+
const occurrencesBefore = countOccurrences(scope.selectedText, input.oldString);
|
|
16702
16827
|
if (!replaceAll && occurrencesBefore === 0) {
|
|
16703
|
-
const loose = tryLooseBlockReplace(scope.selectedText,
|
|
16828
|
+
const loose = tryLooseBlockReplace(scope.selectedText, input.oldString, input.newString);
|
|
16704
16829
|
if (loose !== null) {
|
|
16705
16830
|
const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
|
|
16706
16831
|
const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
|
|
16707
16832
|
await fs2.writeFile(current.filePath, buffer2);
|
|
16708
16833
|
return {
|
|
16834
|
+
mode: "replace",
|
|
16709
16835
|
filePath: current.filePath,
|
|
16710
16836
|
encoding: current.encoding,
|
|
16711
16837
|
replacements: 1,
|
|
@@ -16717,20 +16843,21 @@ async function replaceGbkFileText(input) {
|
|
|
16717
16843
|
}
|
|
16718
16844
|
if (replaceAll) {
|
|
16719
16845
|
if (occurrencesBefore === 0) {
|
|
16720
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
16846
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, input.oldString));
|
|
16721
16847
|
}
|
|
16722
16848
|
} else if (occurrencesBefore === 0) {
|
|
16723
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
16849
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, input.oldString));
|
|
16724
16850
|
} else if (occurrencesBefore > 1) {
|
|
16725
|
-
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${
|
|
16851
|
+
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
|
|
16726
16852
|
}
|
|
16727
16853
|
const fileNewlineStyle = detectNewlineStyle(current.content);
|
|
16728
|
-
const alignedNewString = fileNewlineStyle === "crlf" ?
|
|
16729
|
-
const replaced = replaceAll ? scope.selectedText.split(
|
|
16854
|
+
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(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
|
|
16730
16856
|
const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
|
|
16731
16857
|
const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
|
|
16732
16858
|
await fs2.writeFile(current.filePath, buffer);
|
|
16733
16859
|
return {
|
|
16860
|
+
mode: "replace",
|
|
16734
16861
|
filePath: current.filePath,
|
|
16735
16862
|
encoding: current.encoding,
|
|
16736
16863
|
replacements: replaceAll ? occurrencesBefore : 1,
|
|
@@ -16764,11 +16891,20 @@ Recommended workflow for large files (when gbk_read returned truncated=true):
|
|
|
16764
16891
|
3. gbk_edit(oldString=<content without prefixes>, newString=<new content>)
|
|
16765
16892
|
|
|
16766
16893
|
For large files, use 'startLine'/'endLine' or 'startAnchor'/'endAnchor' to narrow the search scope
|
|
16767
|
-
and avoid false matches. Scoped edits also improve performance on very large files
|
|
16894
|
+
and avoid false matches. Scoped edits also improve performance on very large files.
|
|
16895
|
+
|
|
16896
|
+
Insert mode:
|
|
16897
|
+
- Use mode=insertAfter or mode=insertBefore with anchor/content
|
|
16898
|
+
- This is recommended when the intent is "insert after label" instead of exact replacement.`,
|
|
16768
16899
|
args: {
|
|
16769
16900
|
filePath: tool.schema.string().describe("Target file path"),
|
|
16770
|
-
|
|
16771
|
-
|
|
16901
|
+
mode: tool.schema.enum(["replace", "insertAfter", "insertBefore"]).optional().describe("Edit mode, default replace"),
|
|
16902
|
+
oldString: tool.schema.string().optional().describe("Exact text to replace in replace mode"),
|
|
16903
|
+
newString: tool.schema.string().optional().describe("Replacement text in replace mode"),
|
|
16904
|
+
anchor: tool.schema.string().optional().describe("Anchor text used by insertAfter/insertBefore"),
|
|
16905
|
+
content: tool.schema.string().optional().describe("Inserted content used by insertAfter/insertBefore"),
|
|
16906
|
+
occurrence: tool.schema.number().int().positive().optional().describe("1-based anchor occurrence for insert mode"),
|
|
16907
|
+
ifExists: tool.schema.enum(["allow", "skip", "error"]).optional().describe("What to do when inserted content already exists at target position"),
|
|
16772
16908
|
replaceAll: tool.schema.boolean().optional().describe("Replace all occurrences (default: false, requires unique match)"),
|
|
16773
16909
|
startLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based start line (inclusive)"),
|
|
16774
16910
|
endLine: tool.schema.union([tool.schema.number().int().positive(), tool.schema.literal(-1)]).optional().describe("Restrict edit scope to 1-based end line (inclusive)"),
|
|
@@ -16779,6 +16915,25 @@ and avoid false matches. Scoped edits also improve performance on very large fil
|
|
|
16779
16915
|
},
|
|
16780
16916
|
async execute(args, context) {
|
|
16781
16917
|
const result = await replaceGbkFileText({ ...args, context });
|
|
16918
|
+
const isReplace = !("mode" in result) || result.mode === "replace";
|
|
16919
|
+
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
|
+
context.metadata({
|
|
16921
|
+
title,
|
|
16922
|
+
metadata: {
|
|
16923
|
+
filePath: result.filePath,
|
|
16924
|
+
encoding: result.encoding,
|
|
16925
|
+
mode: isReplace ? "replace" : result.mode,
|
|
16926
|
+
replacements: isReplace ? result.replacements : void 0,
|
|
16927
|
+
occurrencesBefore: isReplace ? result.occurrencesBefore : void 0,
|
|
16928
|
+
anchor: isReplace ? void 0 : result.anchor,
|
|
16929
|
+
occurrence: isReplace ? void 0 : result.occurrence,
|
|
16930
|
+
anchorMatches: isReplace ? void 0 : result.anchorMatches,
|
|
16931
|
+
inserted: isReplace ? void 0 : result.inserted,
|
|
16932
|
+
skipped: isReplace ? void 0 : result.skipped,
|
|
16933
|
+
bytesRead: result.bytesRead,
|
|
16934
|
+
bytesWritten: result.bytesWritten
|
|
16935
|
+
}
|
|
16936
|
+
});
|
|
16782
16937
|
return JSON.stringify(result, null, 2);
|
|
16783
16938
|
}
|
|
16784
16939
|
});
|
|
@@ -16632,6 +16632,19 @@ Workflow when truncated=true:
|
|
|
16632
16632
|
},
|
|
16633
16633
|
async execute(args, context) {
|
|
16634
16634
|
const result = await readGbkFile({ ...args, context });
|
|
16635
|
+
const lineRange = `${result.startLine}-${result.endLine}`;
|
|
16636
|
+
context.metadata({
|
|
16637
|
+
title: `GBK \u8BFB\u53D6 ${result.encoding.toUpperCase()} ${lineRange}`,
|
|
16638
|
+
metadata: {
|
|
16639
|
+
filePath: result.filePath,
|
|
16640
|
+
encoding: result.encoding,
|
|
16641
|
+
lineRange,
|
|
16642
|
+
totalLines: result.totalLines,
|
|
16643
|
+
newlineStyle: result.newlineStyle,
|
|
16644
|
+
truncated: result.truncated,
|
|
16645
|
+
tail: result.tail
|
|
16646
|
+
}
|
|
16647
|
+
});
|
|
16635
16648
|
return JSON.stringify(result, null, 2);
|
|
16636
16649
|
}
|
|
16637
16650
|
});
|
|
@@ -16454,6 +16454,15 @@ Workflow for large files:
|
|
|
16454
16454
|
},
|
|
16455
16455
|
async execute(args, context) {
|
|
16456
16456
|
const result = await searchGbkFile({ ...args, context });
|
|
16457
|
+
context.metadata({
|
|
16458
|
+
title: `GBK \u641C\u7D22 ${result.encoding.toUpperCase()} ${result.matchCount} \u547D\u4E2D`,
|
|
16459
|
+
metadata: {
|
|
16460
|
+
filePath: result.filePath,
|
|
16461
|
+
encoding: result.encoding,
|
|
16462
|
+
totalLines: result.totalLines,
|
|
16463
|
+
matchCount: result.matchCount
|
|
16464
|
+
}
|
|
16465
|
+
});
|
|
16457
16466
|
return JSON.stringify(result, null, 2);
|
|
16458
16467
|
}
|
|
16459
16468
|
});
|
|
@@ -16432,6 +16432,18 @@ var gbk_write_default = tool({
|
|
|
16432
16432
|
},
|
|
16433
16433
|
async execute(args, context) {
|
|
16434
16434
|
const result = await writeGbkFile({ ...args, context });
|
|
16435
|
+
const action = result.appended ? "\u8FFD\u52A0" : result.overwritten ? "\u8986\u76D6" : "\u5199\u5165";
|
|
16436
|
+
context.metadata({
|
|
16437
|
+
title: `GBK ${action} ${result.encoding.toUpperCase()}`,
|
|
16438
|
+
metadata: {
|
|
16439
|
+
filePath: result.filePath,
|
|
16440
|
+
encoding: result.encoding,
|
|
16441
|
+
created: result.created,
|
|
16442
|
+
overwritten: result.overwritten,
|
|
16443
|
+
appended: result.appended ?? false,
|
|
16444
|
+
bytesWritten: result.bytesWritten
|
|
16445
|
+
}
|
|
16446
|
+
});
|
|
16435
16447
|
return JSON.stringify(result, null, 2);
|
|
16436
16448
|
}
|
|
16437
16449
|
});
|