@movemama/opencode-legacy 0.1.0 → 0.1.1
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 +53 -0
- package/index.js +2 -0
- package/legacy-rules.json +10 -2
- package/package.json +3 -2
- package/tools/edit.js +7 -22
- package/tools/legacy-rules-loader.js +35 -0
- package/tools/legacy-status.js +47 -0
- package/tools/legacy-strategy.js +13 -0
- package/tools/read.js +2 -19
- package/tools/write.js +2 -19
- package/tools/edit.ts +0 -64
- package/tools/legacy.ts +0 -230
- package/tools/read.ts +0 -213
- package/tools/script-edit.ts +0 -59
- package/tools/write.ts +0 -67
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt` 编辑和 legacy 规则路由场景。
|
|
4
4
|
|
|
5
|
+
当前主链路已经改为纯 JS 编码实现,不再依赖系统外部 `iconv.exe`。
|
|
6
|
+
|
|
5
7
|
## 安装
|
|
6
8
|
|
|
7
9
|
在 OpenCode 配置中加入:
|
|
@@ -17,6 +19,8 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
|
|
|
17
19
|
|
|
18
20
|
首次未安装的用户会在 OpenCode 启动时自动安装当前 `latest` 版本。后续更新可由用户自行处理。
|
|
19
21
|
|
|
22
|
+
如果刚安装后没有生效,重新启动一次 OpenCode 即可让 npm 插件完成加载。
|
|
23
|
+
|
|
20
24
|
## 当前提供的工具
|
|
21
25
|
|
|
22
26
|
- `read`
|
|
@@ -26,6 +30,7 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
|
|
|
26
30
|
- `legacy_read`
|
|
27
31
|
- `legacy_write`
|
|
28
32
|
- `legacy_edit`
|
|
33
|
+
- `legacy_status`
|
|
29
34
|
|
|
30
35
|
其中同名工具 `read`、`write`、`edit` 会覆盖 OpenCode 内置工具,实现 legacy 路由。
|
|
31
36
|
|
|
@@ -39,6 +44,54 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
|
|
|
39
44
|
2. `<worktree>/legacy-rules.json`
|
|
40
45
|
3. 包内 `legacy-rules.json`
|
|
41
46
|
|
|
47
|
+
规则字段现在除编码外,还支持:
|
|
48
|
+
|
|
49
|
+
- `profile`
|
|
50
|
+
- `editStrategy`
|
|
51
|
+
- `fallbackMode`
|
|
52
|
+
- `scriptMarkers`
|
|
53
|
+
|
|
54
|
+
这让插件可以按场景决定更合适的编辑策略,而不只是按编码读写。
|
|
55
|
+
|
|
56
|
+
## 自动策略层
|
|
57
|
+
|
|
58
|
+
当前 `edit` 已经接入自动策略层。
|
|
59
|
+
|
|
60
|
+
- 普通文本默认走 `exact-first`
|
|
61
|
+
- 脚本型文本可根据 `profile` 和 `scriptMarkers` 判断为 `block-first`
|
|
62
|
+
- 失败时会返回当前策略和 fallback 提示,便于继续排障
|
|
63
|
+
|
|
64
|
+
## 诊断工具
|
|
65
|
+
|
|
66
|
+
可以使用 `legacy_status` 查看:
|
|
67
|
+
|
|
68
|
+
- 插件版本
|
|
69
|
+
- 规则来源
|
|
70
|
+
- 命中规则
|
|
71
|
+
- profile
|
|
72
|
+
- 建议策略
|
|
73
|
+
- fallback 模式
|
|
74
|
+
- 是否被判定为脚本型文件
|
|
75
|
+
|
|
76
|
+
## 如何确认已加载成功
|
|
77
|
+
|
|
78
|
+
最简单的方法是读取一个命中规则的 `.txt` 文件。
|
|
79
|
+
|
|
80
|
+
如果插件已接管成功,返回内容中会出现:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
--- Legacy 读取回执 ---
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
这说明 `read` 工具已经由插件覆盖并进入 legacy 规则链路。
|
|
87
|
+
|
|
88
|
+
也可以直接调用 `legacy_status` 检查当前文件会命中哪条规则、建议使用哪种策略。
|
|
89
|
+
|
|
90
|
+
## 兼容说明
|
|
91
|
+
|
|
92
|
+
- `txt-gb2312-tool.mjs` 目前仅保留为兼容遗留文件,不再是主链路依赖
|
|
93
|
+
- 当前主运行时入口以 `.js/.mjs` 文件为准
|
|
94
|
+
|
|
42
95
|
## 发布
|
|
43
96
|
|
|
44
97
|
```bash
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import readTool from './tools/read.js'
|
|
|
2
2
|
import writeTool from './tools/write.js'
|
|
3
3
|
import editTool from './tools/edit.js'
|
|
4
4
|
import scriptEditTool from './tools/script-edit.js'
|
|
5
|
+
import legacyStatusTool from './tools/legacy-status.js'
|
|
5
6
|
import { edit as legacyEditTool, read as legacyReadTool, write as legacyWriteTool } from './tools/legacy.js'
|
|
6
7
|
|
|
7
8
|
export const OpenCodeLegacyPlugin = async () => ({
|
|
@@ -13,6 +14,7 @@ export const OpenCodeLegacyPlugin = async () => ({
|
|
|
13
14
|
legacy_read: legacyReadTool,
|
|
14
15
|
legacy_write: legacyWriteTool,
|
|
15
16
|
legacy_edit: legacyEditTool,
|
|
17
|
+
legacy_status: legacyStatusTool,
|
|
16
18
|
},
|
|
17
19
|
})
|
|
18
20
|
|
package/legacy-rules.json
CHANGED
|
@@ -6,14 +6,22 @@
|
|
|
6
6
|
"encoding": "gb2312",
|
|
7
7
|
"strict": true,
|
|
8
8
|
"tool": "txt-gb2312",
|
|
9
|
-
"priority": 10
|
|
9
|
+
"priority": 10,
|
|
10
|
+
"profile": "txt-gb2312-safe",
|
|
11
|
+
"editStrategy": "exact-first",
|
|
12
|
+
"fallbackMode": "legacy-safe-replace",
|
|
13
|
+
"scriptMarkers": ["#ACT", "@main", "@全体补偿页面"]
|
|
10
14
|
},
|
|
11
15
|
{
|
|
12
16
|
"glob": "**/*.{ini,cfg,dat}",
|
|
13
17
|
"encoding": "gbk",
|
|
14
18
|
"strict": false,
|
|
15
19
|
"tool": "legacy-text",
|
|
16
|
-
"priority": 1
|
|
20
|
+
"priority": 1,
|
|
21
|
+
"profile": "config-gbk",
|
|
22
|
+
"editStrategy": "exact-first",
|
|
23
|
+
"fallbackMode": "legacy-safe-replace",
|
|
24
|
+
"scriptMarkers": []
|
|
17
25
|
}
|
|
18
26
|
]
|
|
19
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@movemama/opencode-legacy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenCode legacy text processing plugin for GB2312 and script-safe editing workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"plugin",
|
|
21
21
|
"legacy",
|
|
22
22
|
"gb2312",
|
|
23
|
-
"
|
|
23
|
+
"gbk",
|
|
24
|
+
"script-edit"
|
|
24
25
|
],
|
|
25
26
|
"license": "MIT",
|
|
26
27
|
"publishConfig": {
|
package/tools/edit.js
CHANGED
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin'
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import readTool from './read.js'
|
|
5
5
|
import writeTool from './write.js'
|
|
6
6
|
import { applyLegacyEdit } from './legacy-edit-core.mjs'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
function loadLegacyRules(worktree) {
|
|
11
|
-
const candidates = [
|
|
12
|
-
path.join(worktree, '.opencode', 'legacy-rules.json'),
|
|
13
|
-
path.join(worktree, 'legacy-rules.json'),
|
|
14
|
-
getBundledLegacyRulesPath(),
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
for (const filePath of candidates) {
|
|
18
|
-
if (existsSync(filePath)) {
|
|
19
|
-
return JSON.parse(readFileSync(filePath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return createDefaultLegacyRules()
|
|
24
|
-
}
|
|
7
|
+
import { matchLegacyRule } from './legacy-router.mjs'
|
|
8
|
+
import { loadLegacyRules } from './legacy-rules-loader.js'
|
|
9
|
+
import { chooseEditStrategy } from './legacy-strategy.js'
|
|
25
10
|
|
|
26
11
|
function resolvePath(filePath, worktree) {
|
|
27
12
|
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
@@ -42,13 +27,13 @@ export default tool({
|
|
|
42
27
|
}
|
|
43
28
|
|
|
44
29
|
const rules = loadLegacyRules(context.worktree)
|
|
45
|
-
matchLegacyRule(filePath, rules)
|
|
46
|
-
|
|
47
30
|
const content = await readTool.execute({ filePath, raw: true }, context)
|
|
31
|
+
const matchedRule = matchLegacyRule(filePath, rules)
|
|
32
|
+
const strategy = chooseEditStrategy({ filePath, content, matchedRule })
|
|
48
33
|
const result = applyLegacyEdit(content, args.oldString, args.newString, Boolean(args.replaceAll))
|
|
49
34
|
|
|
50
35
|
if (!result.changed) {
|
|
51
|
-
throw new Error(result.error)
|
|
36
|
+
throw new Error(`${result.error} | strategy=${strategy.strategy} | fallback=${strategy.fallbackMode}`)
|
|
52
37
|
}
|
|
53
38
|
|
|
54
39
|
return writeTool.execute({ filePath, content: result.content }, context)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { createDefaultLegacyRules } from './legacy-router.mjs'
|
|
5
|
+
import { getBundledLegacyRulesPath } from './opencode-paths.mjs'
|
|
6
|
+
|
|
7
|
+
export function loadLegacyRules(worktree) {
|
|
8
|
+
return loadLegacyRulesWithMeta(worktree).rules
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadLegacyRulesWithMeta(worktree) {
|
|
12
|
+
const candidates = [
|
|
13
|
+
{ filePath: path.join(worktree, '.opencode', 'legacy-rules.json'), source: 'project:.opencode' },
|
|
14
|
+
{ filePath: path.join(worktree, 'legacy-rules.json'), source: 'project:root' },
|
|
15
|
+
{ filePath: getBundledLegacyRulesPath(), source: 'bundled:default' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
const { filePath, source } = candidate
|
|
20
|
+
if (!existsSync(filePath)) {
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
25
|
+
return {
|
|
26
|
+
rules: parsed.rules ?? createDefaultLegacyRules(),
|
|
27
|
+
source,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
rules: createDefaultLegacyRules(),
|
|
33
|
+
source: 'generated:fallback',
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import readTool from './read.js'
|
|
6
|
+
import { matchLegacyRule } from './legacy-router.mjs'
|
|
7
|
+
import { loadLegacyRulesWithMeta } from './legacy-rules-loader.js'
|
|
8
|
+
import { chooseEditStrategy } from './legacy-strategy.js'
|
|
9
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
10
|
+
|
|
11
|
+
function resolvePath(filePath, worktree) {
|
|
12
|
+
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default tool({
|
|
16
|
+
description: '输出当前 legacy 规则来源、命中规则与策略建议',
|
|
17
|
+
args: {
|
|
18
|
+
filePath: tool.schema.string().describe('目标文件路径'),
|
|
19
|
+
},
|
|
20
|
+
async execute(args, context) {
|
|
21
|
+
const filePath = resolvePath(args.filePath, context.worktree)
|
|
22
|
+
const { rules, source } = loadLegacyRulesWithMeta(context.worktree)
|
|
23
|
+
const matchedRule = matchLegacyRule(filePath, rules)
|
|
24
|
+
|
|
25
|
+
let content = ''
|
|
26
|
+
if (existsSync(filePath)) {
|
|
27
|
+
content = await readTool.execute({ filePath, raw: true }, context)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const strategy = chooseEditStrategy({
|
|
31
|
+
filePath,
|
|
32
|
+
content,
|
|
33
|
+
matchedRule,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
`legacy 插件版本:${pkg.version}`,
|
|
38
|
+
`文件路径:${filePath}`,
|
|
39
|
+
`规则来源:${source}`,
|
|
40
|
+
`命中规则:${matchedRule ? matchedRule.glob : '未命中'}`,
|
|
41
|
+
`profile:${strategy.profile}`,
|
|
42
|
+
`建议策略:${strategy.strategy}`,
|
|
43
|
+
`fallback:${strategy.fallbackMode}`,
|
|
44
|
+
`脚本文件:${strategy.isScriptLike ? '是' : '否'}`,
|
|
45
|
+
].join('\n')
|
|
46
|
+
},
|
|
47
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function chooseEditStrategy({ filePath, content, matchedRule }) {
|
|
2
|
+
const markers = matchedRule?.scriptMarkers ?? []
|
|
3
|
+
const hasMarker = markers.some((marker) => content.includes(marker))
|
|
4
|
+
const looksLikeScript = hasMarker || /(^|\n)@\w+/.test(content) || content.includes('#ACT')
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
filePath,
|
|
8
|
+
profile: matchedRule?.profile ?? 'default',
|
|
9
|
+
strategy: matchedRule?.editStrategy ?? (looksLikeScript ? 'block-first' : 'exact-first'),
|
|
10
|
+
fallbackMode: matchedRule?.fallbackMode ?? (looksLikeScript ? 'suggest-script-edit' : 'legacy-safe-replace'),
|
|
11
|
+
isScriptLike: looksLikeScript,
|
|
12
|
+
}
|
|
13
|
+
}
|
package/tools/read.js
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
3
|
import path from 'node:path'
|
|
5
|
-
import {
|
|
6
|
-
import { getBundledLegacyRulesPath } from './opencode-paths.mjs'
|
|
4
|
+
import { matchLegacyRule } from './legacy-router.mjs'
|
|
7
5
|
import { decodeLegacyBuffer } from './legacy-codec.js'
|
|
8
|
-
|
|
9
|
-
function loadLegacyRules(worktree) {
|
|
10
|
-
const candidates = [
|
|
11
|
-
path.join(worktree, '.opencode', 'legacy-rules.json'),
|
|
12
|
-
path.join(worktree, 'legacy-rules.json'),
|
|
13
|
-
getBundledLegacyRulesPath(),
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
for (const filePath of candidates) {
|
|
17
|
-
if (existsSync(filePath)) {
|
|
18
|
-
return JSON.parse(readFileSync(filePath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return createDefaultLegacyRules()
|
|
23
|
-
}
|
|
6
|
+
import { loadLegacyRules } from './legacy-rules-loader.js'
|
|
24
7
|
|
|
25
8
|
function resolvePath(filePath, worktree) {
|
|
26
9
|
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
package/tools/write.js
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin'
|
|
2
2
|
import { writeFile } from 'node:fs/promises'
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
3
|
import path from 'node:path'
|
|
5
|
-
import {
|
|
6
|
-
import { getBundledLegacyRulesPath } from './opencode-paths.mjs'
|
|
4
|
+
import { matchLegacyRule } from './legacy-router.mjs'
|
|
7
5
|
import { encodeLegacyText } from './legacy-codec.js'
|
|
8
|
-
|
|
9
|
-
function loadLegacyRules(worktree) {
|
|
10
|
-
const candidates = [
|
|
11
|
-
path.join(worktree, '.opencode', 'legacy-rules.json'),
|
|
12
|
-
path.join(worktree, 'legacy-rules.json'),
|
|
13
|
-
getBundledLegacyRulesPath(),
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
for (const filePath of candidates) {
|
|
17
|
-
if (existsSync(filePath)) {
|
|
18
|
-
return JSON.parse(readFileSync(filePath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return createDefaultLegacyRules()
|
|
23
|
-
}
|
|
6
|
+
import { loadLegacyRules } from './legacy-rules-loader.js'
|
|
24
7
|
|
|
25
8
|
function resolvePath(filePath, worktree) {
|
|
26
9
|
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
package/tools/edit.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { tool } from '@opencode-ai/plugin'
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import readTool from './read.ts'
|
|
5
|
-
import writeTool from './write.ts'
|
|
6
|
-
import { applyLegacyEdit } from './legacy-edit-core.mjs'
|
|
7
|
-
import { createDefaultLegacyRules, matchLegacyRule } from './legacy-router.mjs'
|
|
8
|
-
|
|
9
|
-
function loadLegacyRules(worktree: string) {
|
|
10
|
-
const projectConfigPath = path.join(worktree, '.opencode', 'legacy-rules.json')
|
|
11
|
-
const globalConfigPath = 'C:/Users/Administrator/.config/opencode/legacy-rules.json'
|
|
12
|
-
|
|
13
|
-
if (existsSync(projectConfigPath)) {
|
|
14
|
-
return JSON.parse(readFileSync(projectConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (existsSync(globalConfigPath)) {
|
|
18
|
-
return JSON.parse(readFileSync(globalConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return createDefaultLegacyRules()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function resolvePath(filePath: string, worktree: string) {
|
|
25
|
-
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export default tool({
|
|
29
|
-
description: '编辑文件,命中 legacy 规则时自动按对应编码处理',
|
|
30
|
-
args: {
|
|
31
|
-
filePath: tool.schema.string().describe('文件路径'),
|
|
32
|
-
oldString: tool.schema.string().describe('要替换的旧文本'),
|
|
33
|
-
newString: tool.schema.string().describe('要写入的新文本'),
|
|
34
|
-
replaceAll: tool.schema.boolean().optional().describe('是否全部替换'),
|
|
35
|
-
},
|
|
36
|
-
async execute(args, context) {
|
|
37
|
-
const filePath = resolvePath(args.filePath, context.worktree)
|
|
38
|
-
if (!existsSync(filePath)) {
|
|
39
|
-
throw new Error(`文件不存在: ${filePath}`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const rules = loadLegacyRules(context.worktree)
|
|
43
|
-
const matched = matchLegacyRule(filePath, rules)
|
|
44
|
-
if (matched?.tool === 'txt-gb2312') {
|
|
45
|
-
const content = await readTool.execute({ filePath, raw: true }, context)
|
|
46
|
-
const result = applyLegacyEdit(content, args.oldString, args.newString, Boolean(args.replaceAll))
|
|
47
|
-
|
|
48
|
-
if (!result.changed) {
|
|
49
|
-
throw new Error(result.error)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return writeTool.execute({ filePath, content: result.content }, context)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const content = await readTool.execute({ filePath, raw: true }, context)
|
|
56
|
-
const result = applyLegacyEdit(content, args.oldString, args.newString, Boolean(args.replaceAll))
|
|
57
|
-
|
|
58
|
-
if (!result.changed) {
|
|
59
|
-
throw new Error(result.error)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return writeTool.execute({ filePath, content: result.content }, context)
|
|
63
|
-
},
|
|
64
|
-
})
|
package/tools/legacy.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { tool } from '@opencode-ai/plugin'
|
|
2
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { existsSync } from 'node:fs'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
import { spawnSync } from 'node:child_process'
|
|
7
|
-
import { applyLegacyEdit } from './legacy-edit-core.mjs'
|
|
8
|
-
import { getGlobalToolPath, getKnownIconvCandidates } from './opencode-paths.mjs'
|
|
9
|
-
|
|
10
|
-
let cachedNodeCommand = null
|
|
11
|
-
let cachedIconvPath = null
|
|
12
|
-
|
|
13
|
-
function normalizeWindowsPath(filePath) {
|
|
14
|
-
return filePath.replace(/\\/g, '/')
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function getDerivedIconvCandidates() {
|
|
18
|
-
const pathValue = process.env.Path || process.env.PATH || ''
|
|
19
|
-
const parts = pathValue.split(';').map((item) => item.trim()).filter(Boolean)
|
|
20
|
-
const derived = []
|
|
21
|
-
|
|
22
|
-
for (const part of parts) {
|
|
23
|
-
const normalized = normalizeWindowsPath(part)
|
|
24
|
-
if (normalized.toLowerCase().endsWith('/git/cmd')) {
|
|
25
|
-
derived.push(normalized.replace(/\/cmd$/i, '/usr/bin/iconv.exe'))
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return derived
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function resolveTxtToolPath(context) {
|
|
33
|
-
const projectToolPath = path.join(context.worktree, 'tools', 'txt-gb2312-tool.mjs')
|
|
34
|
-
if (existsSync(projectToolPath)) {
|
|
35
|
-
return projectToolPath
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return getGlobalToolPath('txt-gb2312-tool.mjs')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function resolveNodeCommand() {
|
|
42
|
-
if (cachedNodeCommand) {
|
|
43
|
-
return cachedNodeCommand
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const candidates = [process.env.OPENCODE_NODE_PATH, process.execPath, 'node'].filter(Boolean)
|
|
47
|
-
|
|
48
|
-
for (const candidate of candidates) {
|
|
49
|
-
const probe = spawnSync(candidate, ['-e', 'process.stdout.write(process.release?.name === "node" ? "node-ok" : "not-node")'], {
|
|
50
|
-
encoding: 'utf8',
|
|
51
|
-
})
|
|
52
|
-
const output = `${probe.stdout || ''}${probe.stderr || ''}`.trim()
|
|
53
|
-
if (probe.status === 0 && output === 'node-ok') {
|
|
54
|
-
cachedNodeCommand = candidate
|
|
55
|
-
return cachedNodeCommand
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
throw new Error('未找到可用的 Node.js 命令,无法执行 txt-gb2312-tool.mjs')
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function resolveIconvPath() {
|
|
63
|
-
if (cachedIconvPath) {
|
|
64
|
-
return cachedIconvPath
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const candidates = [
|
|
68
|
-
'iconv',
|
|
69
|
-
process.env.OPENCODE_ICONV_PATH,
|
|
70
|
-
process.env.ICONV_PATH,
|
|
71
|
-
...getKnownIconvCandidates(),
|
|
72
|
-
...getDerivedIconvCandidates(),
|
|
73
|
-
].filter(Boolean)
|
|
74
|
-
|
|
75
|
-
for (const candidate of candidates) {
|
|
76
|
-
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8' })
|
|
77
|
-
if (probe.status === 0) {
|
|
78
|
-
cachedIconvPath = candidate
|
|
79
|
-
return cachedIconvPath
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
throw new Error('iconv 不可用,无法处理 legacy 文件')
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function runNode(scriptArgs, cwd, extraEnv = {}) {
|
|
87
|
-
const nodeCommand = resolveNodeCommand()
|
|
88
|
-
const result = spawnSync(nodeCommand, scriptArgs, {
|
|
89
|
-
cwd,
|
|
90
|
-
encoding: 'utf8',
|
|
91
|
-
env: { ...process.env, ...extraEnv },
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
if (result.status !== 0) {
|
|
95
|
-
const stderr = (result.stderr || '').trim()
|
|
96
|
-
const stdout = (result.stdout || '').trim()
|
|
97
|
-
throw new Error(
|
|
98
|
-
[
|
|
99
|
-
'legacy 工具执行失败',
|
|
100
|
-
`nodeCommand=${nodeCommand}`,
|
|
101
|
-
`cwd=${cwd}`,
|
|
102
|
-
`status=${result.status}`,
|
|
103
|
-
`stderr=${stderr || '<empty>'}`,
|
|
104
|
-
`stdout=${stdout || '<empty>'}`,
|
|
105
|
-
].join(' | '),
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return result.stdout
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function runIconv(iconvPath, args) {
|
|
113
|
-
const result = spawnSync(iconvPath, args, { encoding: 'buffer' })
|
|
114
|
-
if (result.status !== 0) {
|
|
115
|
-
throw new Error(result.stderr?.toString('utf8') || 'iconv 执行失败')
|
|
116
|
-
}
|
|
117
|
-
return result.stdout
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function readGb2312File(filePath) {
|
|
121
|
-
const iconvPath = resolveIconvPath()
|
|
122
|
-
const output = runIconv(iconvPath, ['-f', 'GB2312', '-t', 'UTF-8', filePath])
|
|
123
|
-
return output.toString('utf8')
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export const read = tool({
|
|
127
|
-
description: '按 legacy 编码读取文件',
|
|
128
|
-
args: {
|
|
129
|
-
filePath: tool.schema.string().describe('文件路径'),
|
|
130
|
-
encoding: tool.schema.string().describe('legacy 编码'),
|
|
131
|
-
strict: tool.schema.boolean().describe('是否严格模式'),
|
|
132
|
-
},
|
|
133
|
-
async execute(args, context) {
|
|
134
|
-
const filePath = path.isAbsolute(args.filePath)
|
|
135
|
-
? args.filePath
|
|
136
|
-
: path.join(context.worktree, args.filePath)
|
|
137
|
-
|
|
138
|
-
if (args.encoding.toLowerCase() === 'gb2312') {
|
|
139
|
-
return readGb2312File(filePath)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const iconvPath = resolveIconvPath()
|
|
143
|
-
const output = runIconv(iconvPath, ['-f', args.encoding, '-t', 'UTF-8', filePath])
|
|
144
|
-
return output.toString('utf8')
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
export const write = tool({
|
|
149
|
-
description: '按 legacy 编码写入文件',
|
|
150
|
-
args: {
|
|
151
|
-
filePath: tool.schema.string().describe('文件路径'),
|
|
152
|
-
content: tool.schema.string().describe('UTF-8 内容'),
|
|
153
|
-
encoding: tool.schema.string().describe('legacy 编码'),
|
|
154
|
-
strict: tool.schema.boolean().describe('是否严格模式'),
|
|
155
|
-
},
|
|
156
|
-
async execute(args, context) {
|
|
157
|
-
const filePath = path.isAbsolute(args.filePath)
|
|
158
|
-
? args.filePath
|
|
159
|
-
: path.join(context.worktree, args.filePath)
|
|
160
|
-
|
|
161
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), 'legacy-write-'))
|
|
162
|
-
const tempPath = path.join(tempDir, 'content.utf8')
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
await writeFile(tempPath, args.content, 'utf8')
|
|
166
|
-
|
|
167
|
-
if (args.encoding.toLowerCase() === 'gb2312') {
|
|
168
|
-
const iconvPath = resolveIconvPath()
|
|
169
|
-
return runNode([resolveTxtToolPath(context), 'write', filePath, tempPath], context.worktree, {
|
|
170
|
-
OPENCODE_ICONV_PATH: iconvPath,
|
|
171
|
-
ICONV_PATH: iconvPath,
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const iconvPath = resolveIconvPath()
|
|
176
|
-
const converted = runIconv(iconvPath, ['-f', 'UTF-8', '-t', args.encoding, tempPath])
|
|
177
|
-
await writeFile(filePath, converted)
|
|
178
|
-
return `已按 ${args.encoding} 编码写入 ${filePath}`
|
|
179
|
-
} finally {
|
|
180
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
export const edit = tool({
|
|
186
|
-
description: '按 legacy 编码编辑文件',
|
|
187
|
-
args: {
|
|
188
|
-
filePath: tool.schema.string().describe('文件路径'),
|
|
189
|
-
oldString: tool.schema.string().describe('要替换的旧文本'),
|
|
190
|
-
newString: tool.schema.string().describe('要写入的新文本'),
|
|
191
|
-
replaceAll: tool.schema.boolean().describe('是否全部替换'),
|
|
192
|
-
encoding: tool.schema.string().describe('legacy 编码'),
|
|
193
|
-
strict: tool.schema.boolean().describe('是否严格模式'),
|
|
194
|
-
},
|
|
195
|
-
async execute(args, context) {
|
|
196
|
-
const filePath = path.isAbsolute(args.filePath)
|
|
197
|
-
? args.filePath
|
|
198
|
-
: path.join(context.worktree, args.filePath)
|
|
199
|
-
|
|
200
|
-
if (!existsSync(filePath)) {
|
|
201
|
-
throw new Error(`文件不存在: ${filePath}`)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const content = await read.execute(
|
|
205
|
-
{
|
|
206
|
-
filePath,
|
|
207
|
-
encoding: args.encoding,
|
|
208
|
-
strict: args.strict,
|
|
209
|
-
raw: true,
|
|
210
|
-
},
|
|
211
|
-
context,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
const result = applyLegacyEdit(content, args.oldString, args.newString, Boolean(args.replaceAll))
|
|
215
|
-
|
|
216
|
-
if (!result.changed) {
|
|
217
|
-
throw new Error(result.error)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return write.execute(
|
|
221
|
-
{
|
|
222
|
-
filePath,
|
|
223
|
-
content: result.content,
|
|
224
|
-
encoding: args.encoding,
|
|
225
|
-
strict: args.strict,
|
|
226
|
-
},
|
|
227
|
-
context,
|
|
228
|
-
)
|
|
229
|
-
},
|
|
230
|
-
})
|
package/tools/read.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { tool } from '@opencode-ai/plugin'
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
|
-
import { createDefaultLegacyRules, matchLegacyRule } from './legacy-router.mjs'
|
|
7
|
-
import { getGlobalLegacyRulesPath, getKnownIconvCandidates } from './opencode-paths.mjs'
|
|
8
|
-
|
|
9
|
-
let cachedIconvPath = null
|
|
10
|
-
|
|
11
|
-
function loadLegacyRules(worktree) {
|
|
12
|
-
const projectConfigPath = path.join(worktree, '.opencode', 'legacy-rules.json')
|
|
13
|
-
const globalConfigPath = getGlobalLegacyRulesPath()
|
|
14
|
-
|
|
15
|
-
if (existsSync(projectConfigPath)) {
|
|
16
|
-
return JSON.parse(readFileSync(projectConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (existsSync(globalConfigPath)) {
|
|
20
|
-
return JSON.parse(readFileSync(globalConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return createDefaultLegacyRules()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function resolvePath(filePath, worktree) {
|
|
27
|
-
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function normalizeWindowsPath(filePath) {
|
|
31
|
-
return filePath.replace(/\\/g, '/')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getDerivedIconvCandidates() {
|
|
35
|
-
const pathValue = process.env.Path || process.env.PATH || ''
|
|
36
|
-
const parts = pathValue.split(';').map((item) => item.trim()).filter(Boolean)
|
|
37
|
-
const derived = []
|
|
38
|
-
|
|
39
|
-
for (const part of parts) {
|
|
40
|
-
const normalized = normalizeWindowsPath(part)
|
|
41
|
-
if (normalized.toLowerCase().endsWith('/git/cmd')) {
|
|
42
|
-
derived.push(normalized.replace(/\/cmd$/i, '/usr/bin/iconv.exe'))
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return derived
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function resolveIconvPath() {
|
|
50
|
-
if (cachedIconvPath) {
|
|
51
|
-
return cachedIconvPath
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const candidates = [
|
|
55
|
-
'iconv',
|
|
56
|
-
process.env.OPENCODE_ICONV_PATH,
|
|
57
|
-
process.env.ICONV_PATH,
|
|
58
|
-
...getKnownIconvCandidates(),
|
|
59
|
-
...getDerivedIconvCandidates(),
|
|
60
|
-
].filter(Boolean)
|
|
61
|
-
|
|
62
|
-
for (const candidate of candidates) {
|
|
63
|
-
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8' })
|
|
64
|
-
if (probe.status === 0) {
|
|
65
|
-
cachedIconvPath = candidate
|
|
66
|
-
return cachedIconvPath
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
throw new Error('iconv 不可用,无法处理 legacy 文件读取')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function applyOffsetLimit(content, offset, limit) {
|
|
74
|
-
if (offset === undefined && limit === undefined) {
|
|
75
|
-
return content
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (offset !== undefined && offset < 1) {
|
|
79
|
-
throw new Error('offset must be greater than or equal to 1')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const lines = content.split(/\r?\n/)
|
|
83
|
-
const start = offset ? offset - 1 : 0
|
|
84
|
-
const end = limit ? start + limit : lines.length
|
|
85
|
-
return lines.slice(start, end).join('\n')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function applyOffsetLimitFast(content, offset, limit) {
|
|
89
|
-
if (offset === undefined && limit === undefined) {
|
|
90
|
-
return content
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (offset !== undefined && offset < 1) {
|
|
94
|
-
throw new Error('offset must be greater than or equal to 1')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const targetStartLine = offset || 1
|
|
98
|
-
const targetLineCount = limit || Number.MAX_SAFE_INTEGER
|
|
99
|
-
const targetEndLine = targetStartLine + targetLineCount - 1
|
|
100
|
-
|
|
101
|
-
let lineNumber = 1
|
|
102
|
-
let currentStart = 0
|
|
103
|
-
let sliceStart = -1
|
|
104
|
-
let sliceEnd = content.length
|
|
105
|
-
|
|
106
|
-
for (let index = 0; index < content.length; index += 1) {
|
|
107
|
-
const ch = content[index]
|
|
108
|
-
if (ch !== '\n') {
|
|
109
|
-
continue
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (lineNumber === targetStartLine) {
|
|
113
|
-
sliceStart = currentStart
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (lineNumber === targetEndLine) {
|
|
117
|
-
sliceEnd = index
|
|
118
|
-
break
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
lineNumber += 1
|
|
122
|
-
currentStart = index + 1
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (sliceStart === -1) {
|
|
126
|
-
if (lineNumber === targetStartLine) {
|
|
127
|
-
sliceStart = currentStart
|
|
128
|
-
} else {
|
|
129
|
-
return ''
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const result = content.slice(sliceStart, sliceEnd)
|
|
134
|
-
return result.endsWith('\r') ? result.slice(0, -1) : result
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readGb2312File(filePath, offset, limit) {
|
|
138
|
-
const iconvPath = resolveIconvPath()
|
|
139
|
-
const result = spawnSync(iconvPath, ['-f', 'GB2312', '-t', 'UTF-8', filePath], {
|
|
140
|
-
encoding: 'utf8',
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
if (result.status !== 0) {
|
|
144
|
-
throw new Error(result.stderr || 'legacy txt 读取失败')
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return applyOffsetLimitFast(result.stdout, offset, limit)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function withLargeFileNotice(content, offset, limit) {
|
|
151
|
-
if (offset === undefined && limit === undefined) {
|
|
152
|
-
return content
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return [
|
|
156
|
-
'--- 读取回执 ---',
|
|
157
|
-
`读取方式:分段读取`,
|
|
158
|
-
`起始行号:${offset ?? 1}`,
|
|
159
|
-
`读取行数:${limit ?? 'all'}`,
|
|
160
|
-
'说明:如需继续读取剩余内容,请继续增大 offset。',
|
|
161
|
-
content,
|
|
162
|
-
].join('\n')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function withLegacyReadNotice(content, matched, offset, limit, raw = false) {
|
|
166
|
-
if (raw) {
|
|
167
|
-
return content
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const lines = [
|
|
171
|
-
'--- Legacy 读取回执 ---',
|
|
172
|
-
`编码:${matched?.encoding || 'unknown'}`,
|
|
173
|
-
`处理器:${matched?.tool || 'legacy-text'}`,
|
|
174
|
-
`严格模式:${matched?.strict === true ? 'true' : 'false'}`,
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
if (offset !== undefined || limit !== undefined) {
|
|
178
|
-
lines.push(`读取方式:分段读取`)
|
|
179
|
-
lines.push(`起始行号:${offset ?? 1}`)
|
|
180
|
-
lines.push(`读取行数:${limit ?? 'all'}`)
|
|
181
|
-
lines.push('说明:如需继续读取剩余内容,请继续增大 offset。')
|
|
182
|
-
} else {
|
|
183
|
-
lines.push('读取方式:完整读取')
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
lines.push(content)
|
|
187
|
-
return lines.join('\n')
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export default tool({
|
|
191
|
-
description: '读取文件,命中 legacy 规则时自动按对应编码处理',
|
|
192
|
-
args: {
|
|
193
|
-
filePath: tool.schema.string().describe('文件路径'),
|
|
194
|
-
offset: tool.schema.number().optional().describe('起始行号,1 开始'),
|
|
195
|
-
limit: tool.schema.number().optional().describe('最多读取多少行'),
|
|
196
|
-
raw: tool.schema.boolean().optional().describe('内部调用时返回原始内容'),
|
|
197
|
-
},
|
|
198
|
-
async execute(args, context) {
|
|
199
|
-
const filePath = resolvePath(args.filePath, context.worktree)
|
|
200
|
-
const rules = loadLegacyRules(context.worktree)
|
|
201
|
-
const matched = matchLegacyRule(filePath, rules)
|
|
202
|
-
|
|
203
|
-
if (matched?.tool === 'txt-gb2312' || matched?.encoding?.toLowerCase() === 'gb2312') {
|
|
204
|
-
return withLegacyReadNotice(readGb2312File(filePath, args.offset, args.limit), matched, args.offset, args.limit, Boolean(args.raw))
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const content = await readFile(filePath, 'utf8')
|
|
208
|
-
if (args.raw) {
|
|
209
|
-
return applyOffsetLimitFast(content, args.offset, args.limit)
|
|
210
|
-
}
|
|
211
|
-
return withLargeFileNotice(applyOffsetLimitFast(content, args.offset, args.limit), args.offset, args.limit)
|
|
212
|
-
},
|
|
213
|
-
})
|
package/tools/script-edit.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { tool } from '@opencode-ai/plugin'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import readTool from './read.ts'
|
|
5
|
-
import writeTool from './write.ts'
|
|
6
|
-
import {
|
|
7
|
-
insertAfterAnchor,
|
|
8
|
-
insertIntoLabelAfterMarker,
|
|
9
|
-
replaceLabelBlock,
|
|
10
|
-
} from './script-edit-core.mjs'
|
|
11
|
-
|
|
12
|
-
function resolvePath(filePath, worktree) {
|
|
13
|
-
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default tool({
|
|
17
|
-
description: '按标签块或锚点编辑脚本类文本,适合 legacy 脚本文件',
|
|
18
|
-
args: {
|
|
19
|
-
filePath: tool.schema.string().describe('文件路径'),
|
|
20
|
-
mode: tool.schema.enum(['insert_after_anchor', 'insert_into_label_after_marker', 'replace_label_block']).describe('编辑模式'),
|
|
21
|
-
label: tool.schema.string().optional().describe('目标标签名,例如 @全体补偿页面'),
|
|
22
|
-
marker: tool.schema.string().optional().describe('标签块内锚点行,例如 #ACT'),
|
|
23
|
-
anchor: tool.schema.string().optional().describe('普通锚点文本'),
|
|
24
|
-
content: tool.schema.string().describe('要插入或替换的内容'),
|
|
25
|
-
},
|
|
26
|
-
async execute(args, context) {
|
|
27
|
-
const filePath = resolvePath(args.filePath, context.worktree)
|
|
28
|
-
|
|
29
|
-
if (!existsSync(filePath)) {
|
|
30
|
-
throw new Error(`文件不存在: ${filePath}`)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const original = await readTool.execute({ filePath, raw: true }, context)
|
|
34
|
-
let next = original
|
|
35
|
-
|
|
36
|
-
if (args.mode === 'insert_after_anchor') {
|
|
37
|
-
if (!args.anchor) {
|
|
38
|
-
throw new Error('insert_after_anchor 模式缺少 anchor')
|
|
39
|
-
}
|
|
40
|
-
next = insertAfterAnchor(original, args.anchor, args.content)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (args.mode === 'insert_into_label_after_marker') {
|
|
44
|
-
if (!args.label || !args.marker) {
|
|
45
|
-
throw new Error('insert_into_label_after_marker 模式缺少 label 或 marker')
|
|
46
|
-
}
|
|
47
|
-
next = insertIntoLabelAfterMarker(original, args.label, args.marker, args.content)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (args.mode === 'replace_label_block') {
|
|
51
|
-
if (!args.label) {
|
|
52
|
-
throw new Error('replace_label_block 模式缺少 label')
|
|
53
|
-
}
|
|
54
|
-
next = replaceLabelBlock(original, args.label, args.content)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return writeTool.execute({ filePath, content: next }, context)
|
|
58
|
-
},
|
|
59
|
-
})
|
package/tools/write.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { tool } from '@opencode-ai/plugin'
|
|
2
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
import { spawnSync } from 'node:child_process'
|
|
7
|
-
import { createDefaultLegacyRules, matchLegacyRule } from './legacy-router.mjs'
|
|
8
|
-
import { getGlobalLegacyRulesPath, getGlobalToolPath } from './opencode-paths.mjs'
|
|
9
|
-
|
|
10
|
-
function loadLegacyRules(worktree: string) {
|
|
11
|
-
const projectConfigPath = path.join(worktree, '.opencode', 'legacy-rules.json')
|
|
12
|
-
const globalConfigPath = getGlobalLegacyRulesPath()
|
|
13
|
-
|
|
14
|
-
if (existsSync(projectConfigPath)) {
|
|
15
|
-
return JSON.parse(readFileSync(projectConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (existsSync(globalConfigPath)) {
|
|
19
|
-
return JSON.parse(readFileSync(globalConfigPath, 'utf8')).rules ?? createDefaultLegacyRules()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return createDefaultLegacyRules()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function resolvePath(filePath: string, worktree: string) {
|
|
26
|
-
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function writeViaTxtTool(targetPath: string, content: string, worktree: string) {
|
|
30
|
-
const toolPath = getGlobalToolPath('txt-gb2312-tool.mjs')
|
|
31
|
-
return mkdtemp(path.join(tmpdir(), 'global-write-')).then(async (tempDir) => {
|
|
32
|
-
const tempPath = path.join(tempDir, 'content.utf8')
|
|
33
|
-
try {
|
|
34
|
-
await writeFile(tempPath, content, 'utf8')
|
|
35
|
-
const result = spawnSync(process.execPath, [toolPath, 'write', targetPath, tempPath], {
|
|
36
|
-
cwd: worktree,
|
|
37
|
-
encoding: 'utf8',
|
|
38
|
-
})
|
|
39
|
-
if (result.status !== 0) {
|
|
40
|
-
throw new Error(result.stderr || 'legacy txt 写入失败')
|
|
41
|
-
}
|
|
42
|
-
return result.stdout
|
|
43
|
-
} finally {
|
|
44
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export default tool({
|
|
50
|
-
description: '写入文件,命中 legacy 规则时自动按对应编码处理',
|
|
51
|
-
args: {
|
|
52
|
-
filePath: tool.schema.string().describe('文件路径'),
|
|
53
|
-
content: tool.schema.string().describe('文件内容'),
|
|
54
|
-
},
|
|
55
|
-
async execute(args, context) {
|
|
56
|
-
const filePath = resolvePath(args.filePath, context.worktree)
|
|
57
|
-
const rules = loadLegacyRules(context.worktree)
|
|
58
|
-
const matched = matchLegacyRule(filePath, rules)
|
|
59
|
-
|
|
60
|
-
if (matched?.tool === 'txt-gb2312' || matched?.encoding?.toLowerCase() === 'gb2312') {
|
|
61
|
-
return writeViaTxtTool(filePath, args.content, context.worktree)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await writeFile(filePath, args.content, 'utf8')
|
|
65
|
-
return `已写入 ${filePath}`
|
|
66
|
-
},
|
|
67
|
-
})
|