@movemama/opencode-legacy 0.1.0
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 +19 -0
- package/legacy-rules.json +19 -0
- package/package.json +36 -0
- package/plugin-meta.js +14 -0
- package/tools/edit.js +56 -0
- package/tools/edit.ts +64 -0
- package/tools/grep.js +210 -0
- package/tools/legacy-codec.js +13 -0
- package/tools/legacy-edit-core.mjs +134 -0
- package/tools/legacy-router.mjs +149 -0
- package/tools/legacy-search-core.mjs +84 -0
- package/tools/legacy.js +78 -0
- package/tools/legacy.ts +230 -0
- package/tools/opencode-paths.mjs +41 -0
- package/tools/read.js +148 -0
- package/tools/read.ts +213 -0
- package/tools/script-edit-core.mjs +126 -0
- package/tools/script-edit.js +59 -0
- package/tools/script-edit.ts +59 -0
- package/tools/txt-gb2312-tool.mjs +392 -0
- package/tools/write.js +53 -0
- package/tools/write.ts +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @movemama/opencode-legacy
|
|
2
|
+
|
|
3
|
+
OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt` 编辑和 legacy 规则路由场景。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
在 OpenCode 配置中加入:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"$schema": "https://opencode.ai/config.json",
|
|
12
|
+
"plugin": [
|
|
13
|
+
"@movemama/opencode-legacy@latest"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
首次未安装的用户会在 OpenCode 启动时自动安装当前 `latest` 版本。后续更新可由用户自行处理。
|
|
19
|
+
|
|
20
|
+
## 当前提供的工具
|
|
21
|
+
|
|
22
|
+
- `read`
|
|
23
|
+
- `write`
|
|
24
|
+
- `edit`
|
|
25
|
+
- `script-edit`
|
|
26
|
+
- `legacy_read`
|
|
27
|
+
- `legacy_write`
|
|
28
|
+
- `legacy_edit`
|
|
29
|
+
|
|
30
|
+
其中同名工具 `read`、`write`、`edit` 会覆盖 OpenCode 内置工具,实现 legacy 路由。
|
|
31
|
+
|
|
32
|
+
## 规则文件
|
|
33
|
+
|
|
34
|
+
包内自带 `legacy-rules.json` 作为默认规则。
|
|
35
|
+
|
|
36
|
+
加载优先级:
|
|
37
|
+
|
|
38
|
+
1. `<worktree>/.opencode/legacy-rules.json`
|
|
39
|
+
2. `<worktree>/legacy-rules.json`
|
|
40
|
+
3. 包内 `legacy-rules.json`
|
|
41
|
+
|
|
42
|
+
## 发布
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm login
|
|
46
|
+
npm publish --access public
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 开发测试
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm test
|
|
53
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import readTool from './tools/read.js'
|
|
2
|
+
import writeTool from './tools/write.js'
|
|
3
|
+
import editTool from './tools/edit.js'
|
|
4
|
+
import scriptEditTool from './tools/script-edit.js'
|
|
5
|
+
import { edit as legacyEditTool, read as legacyReadTool, write as legacyWriteTool } from './tools/legacy.js'
|
|
6
|
+
|
|
7
|
+
export const OpenCodeLegacyPlugin = async () => ({
|
|
8
|
+
tool: {
|
|
9
|
+
read: readTool,
|
|
10
|
+
write: writeTool,
|
|
11
|
+
edit: editTool,
|
|
12
|
+
'script-edit': scriptEditTool,
|
|
13
|
+
legacy_read: legacyReadTool,
|
|
14
|
+
legacy_write: legacyWriteTool,
|
|
15
|
+
legacy_edit: legacyEditTool,
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default OpenCodeLegacyPlugin
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "rules 为实际生效规则;examples 为示例模板,不会自动生效。新增文件类型时,优先复制 examples 中最接近的一条到 rules。tool 可选值当前建议使用 txt-gb2312 或 legacy-text。",
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"glob": "**/*.txt",
|
|
6
|
+
"encoding": "gb2312",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"tool": "txt-gb2312",
|
|
9
|
+
"priority": 10
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"glob": "**/*.{ini,cfg,dat}",
|
|
13
|
+
"encoding": "gbk",
|
|
14
|
+
"strict": false,
|
|
15
|
+
"tool": "legacy-text",
|
|
16
|
+
"priority": 1
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@movemama/opencode-legacy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode legacy text processing plugin for GB2312 and script-safe editing workflows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"exports": "./index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"plugin-meta.js",
|
|
11
|
+
"legacy-rules.json",
|
|
12
|
+
"tools",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test tests/*.test.mjs"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"opencode",
|
|
20
|
+
"plugin",
|
|
21
|
+
"legacy",
|
|
22
|
+
"gb2312",
|
|
23
|
+
"iconv"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@opencode-ai/plugin": "1.3.3",
|
|
34
|
+
"iconv-lite": "^0.7.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/plugin-meta.js
ADDED
package/tools/edit.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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.js'
|
|
5
|
+
import writeTool from './write.js'
|
|
6
|
+
import { applyLegacyEdit } from './legacy-edit-core.mjs'
|
|
7
|
+
import { createDefaultLegacyRules, matchLegacyRule } from './legacy-router.mjs'
|
|
8
|
+
import { getBundledLegacyRulesPath } from './opencode-paths.mjs'
|
|
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
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolvePath(filePath, worktree) {
|
|
27
|
+
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default tool({
|
|
31
|
+
description: '编辑文件,命中 legacy 规则时自动按对应编码处理',
|
|
32
|
+
args: {
|
|
33
|
+
filePath: tool.schema.string().describe('文件路径'),
|
|
34
|
+
oldString: tool.schema.string().describe('要替换的旧文本'),
|
|
35
|
+
newString: tool.schema.string().describe('要写入的新文本'),
|
|
36
|
+
replaceAll: tool.schema.boolean().optional().describe('是否全部替换'),
|
|
37
|
+
},
|
|
38
|
+
async execute(args, context) {
|
|
39
|
+
const filePath = resolvePath(args.filePath, context.worktree)
|
|
40
|
+
if (!existsSync(filePath)) {
|
|
41
|
+
throw new Error(`文件不存在: ${filePath}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rules = loadLegacyRules(context.worktree)
|
|
45
|
+
matchLegacyRule(filePath, rules)
|
|
46
|
+
|
|
47
|
+
const content = await readTool.execute({ filePath, raw: true }, context)
|
|
48
|
+
const result = applyLegacyEdit(content, args.oldString, args.newString, Boolean(args.replaceAll))
|
|
49
|
+
|
|
50
|
+
if (!result.changed) {
|
|
51
|
+
throw new Error(result.error)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return writeTool.execute({ filePath, content: result.content }, context)
|
|
55
|
+
},
|
|
56
|
+
})
|
package/tools/edit.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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/grep.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin'
|
|
2
|
+
import { readdir, 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 { searchContentLines, searchContentLinesChunked } from './legacy-search-core.mjs'
|
|
8
|
+
import { getGlobalLegacyRulesPath, getGlobalToolPath } from './opencode-paths.mjs'
|
|
9
|
+
|
|
10
|
+
function loadLegacyRules(worktree) {
|
|
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(inputPath, worktree) {
|
|
26
|
+
if (!inputPath || inputPath === '.') {
|
|
27
|
+
return worktree
|
|
28
|
+
}
|
|
29
|
+
return path.isAbsolute(inputPath) ? inputPath : path.join(worktree, inputPath)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePath(filePath) {
|
|
33
|
+
return filePath.replace(/\\/g, '/')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function globToRegExp(glob) {
|
|
37
|
+
const normalized = normalizePath(glob)
|
|
38
|
+
let pattern = '^'
|
|
39
|
+
|
|
40
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
41
|
+
const ch = normalized[index]
|
|
42
|
+
if (ch === '*') {
|
|
43
|
+
const next = normalized[index + 1]
|
|
44
|
+
if (next === '*') {
|
|
45
|
+
pattern += '.*'
|
|
46
|
+
index += 1
|
|
47
|
+
} else {
|
|
48
|
+
pattern += '[^/]*'
|
|
49
|
+
}
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (ch === '?') {
|
|
53
|
+
pattern += '.'
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (ch === '{') {
|
|
57
|
+
const closeIndex = normalized.indexOf('}', index)
|
|
58
|
+
if (closeIndex > index) {
|
|
59
|
+
const group = normalized
|
|
60
|
+
.slice(index + 1, closeIndex)
|
|
61
|
+
.split(',')
|
|
62
|
+
.map((part) => part.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
63
|
+
.join('|')
|
|
64
|
+
pattern += `(${group})`
|
|
65
|
+
index = closeIndex
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if ('\\.[]{}()+-^$|'.includes(ch)) {
|
|
70
|
+
pattern += `\\${ch}`
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
pattern += ch
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pattern += '$'
|
|
77
|
+
return new RegExp(pattern, 'i')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function collectFiles(rootPath, includePattern) {
|
|
81
|
+
const results = []
|
|
82
|
+
const includeRegex = includePattern ? globToRegExp(includePattern) : null
|
|
83
|
+
|
|
84
|
+
async function walk(currentPath) {
|
|
85
|
+
const entries = await readdir(currentPath, { withFileTypes: true })
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
await walk(fullPath)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const normalized = normalizePath(fullPath)
|
|
94
|
+
if (!includeRegex || includeRegex.test(normalized)) {
|
|
95
|
+
results.push(fullPath)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (existsSync(rootPath) && path.extname(rootPath)) {
|
|
101
|
+
if (!includeRegex || includeRegex.test(normalizePath(rootPath))) {
|
|
102
|
+
return [rootPath]
|
|
103
|
+
}
|
|
104
|
+
return []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await walk(rootPath)
|
|
108
|
+
return results
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readViaTxtTool(filePath, worktree) {
|
|
112
|
+
const toolPath = getGlobalToolPath('txt-gb2312-tool.mjs')
|
|
113
|
+
const result = spawnSync(process.execPath, [toolPath, 'read', filePath], {
|
|
114
|
+
cwd: worktree,
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
throw new Error(result.stderr || 'legacy txt 搜索读取失败')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result.stdout
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readSearchableContent(filePath, matched, worktree) {
|
|
126
|
+
if (matched?.tool === 'txt-gb2312' || matched?.encoding?.toLowerCase() === 'gb2312') {
|
|
127
|
+
return readViaTxtTool(filePath, worktree)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return readFile(filePath, 'utf8')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function searchLargeAware(content, pattern, options = {}) {
|
|
134
|
+
const threshold = options.chunkThreshold || 1024 * 1024
|
|
135
|
+
if (content.length > threshold) {
|
|
136
|
+
return searchContentLinesChunked(content, pattern, options)
|
|
137
|
+
}
|
|
138
|
+
return searchContentLines(content, pattern, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default tool({
|
|
142
|
+
description: '搜索文件内容,命中 legacy 规则时按对应编码读取后再搜索',
|
|
143
|
+
args: {
|
|
144
|
+
path: tool.schema.string().optional().describe('搜索根路径'),
|
|
145
|
+
pattern: tool.schema.string().describe('搜索模式'),
|
|
146
|
+
include: tool.schema.string().optional().describe('文件 glob 过滤'),
|
|
147
|
+
caseSensitive: tool.schema.boolean().optional().describe('是否区分大小写'),
|
|
148
|
+
fixedStrings: tool.schema.boolean().optional().describe('是否按普通字符串匹配'),
|
|
149
|
+
wholeWord: tool.schema.boolean().optional().describe('是否按整词匹配'),
|
|
150
|
+
maxResults: tool.schema.number().optional().describe('最多返回多少条命中结果'),
|
|
151
|
+
page: tool.schema.number().optional().describe('结果页码,从 1 开始'),
|
|
152
|
+
},
|
|
153
|
+
async execute(args, context) {
|
|
154
|
+
const rules = loadLegacyRules(context.worktree)
|
|
155
|
+
const rootPath = resolvePath(args.path || '.', context.worktree)
|
|
156
|
+
const files = await collectFiles(rootPath, args.include)
|
|
157
|
+
const outputs = []
|
|
158
|
+
const maxResults = args.maxResults || 200
|
|
159
|
+
const page = args.page || 1
|
|
160
|
+
const startIndex = (page - 1) * maxResults
|
|
161
|
+
const endIndex = startIndex + maxResults
|
|
162
|
+
let seen = 0
|
|
163
|
+
|
|
164
|
+
for (const filePath of files) {
|
|
165
|
+
const matched = matchLegacyRule(filePath, rules)
|
|
166
|
+
const content = await readSearchableContent(filePath, matched, context.worktree)
|
|
167
|
+
const matches = searchLargeAware(content, args.pattern, {
|
|
168
|
+
caseSensitive: Boolean(args.caseSensitive),
|
|
169
|
+
fixedStrings: Boolean(args.fixedStrings),
|
|
170
|
+
wholeWord: Boolean(args.wholeWord),
|
|
171
|
+
chunkThreshold: 1024 * 512,
|
|
172
|
+
chunkSize: 1024 * 256,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
for (const item of matches) {
|
|
176
|
+
if (seen >= startIndex && outputs.length < maxResults) {
|
|
177
|
+
outputs.push(`${filePath}:${item.line}:${item.text}`)
|
|
178
|
+
}
|
|
179
|
+
seen += 1
|
|
180
|
+
if (seen >= endIndex) {
|
|
181
|
+
if (outputs.length >= maxResults) {
|
|
182
|
+
return [
|
|
183
|
+
'--- 搜索回执 ---',
|
|
184
|
+
`页码:${page}`,
|
|
185
|
+
`每页上限:${maxResults}`,
|
|
186
|
+
`本页结果数:${outputs.length}`,
|
|
187
|
+
`已达到本页上限,请增大 page 查看后续结果。`,
|
|
188
|
+
...outputs,
|
|
189
|
+
].join('\n')
|
|
190
|
+
}
|
|
191
|
+
return [
|
|
192
|
+
'--- 搜索回执 ---',
|
|
193
|
+
`页码:${page}`,
|
|
194
|
+
`每页上限:${maxResults}`,
|
|
195
|
+
`本页结果数:${outputs.length}`,
|
|
196
|
+
...outputs,
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return [
|
|
203
|
+
'--- 搜索回执 ---',
|
|
204
|
+
`页码:${page}`,
|
|
205
|
+
`每页上限:${maxResults}`,
|
|
206
|
+
`本页结果数:${outputs.length}`,
|
|
207
|
+
...outputs,
|
|
208
|
+
].join('\n')
|
|
209
|
+
},
|
|
210
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import iconv from 'iconv-lite'
|
|
2
|
+
|
|
3
|
+
export function decodeLegacyBuffer(buffer, encoding) {
|
|
4
|
+
return iconv.decode(buffer, encoding)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function encodeLegacyText(text, encoding) {
|
|
8
|
+
return iconv.encode(text, encoding)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function encodingExists(encoding) {
|
|
12
|
+
return iconv.encodingExists(encoding)
|
|
13
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
function normalizeNewlines(text) {
|
|
2
|
+
return text.replace(/\r\n/g, '\n')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function trimRightSpaces(text) {
|
|
6
|
+
return text.replace(/[ \t]+$/g, '')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function splitNormalizedLines(text) {
|
|
10
|
+
return normalizeNewlines(text).split('\n')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function canonicalLine(text) {
|
|
14
|
+
return trimRightSpaces(text)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function trimTrailingEmptyLines(lines) {
|
|
18
|
+
const copy = [...lines]
|
|
19
|
+
while (copy.length > 0 && canonicalLine(copy[copy.length - 1]) === '') {
|
|
20
|
+
copy.pop()
|
|
21
|
+
}
|
|
22
|
+
return copy
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function denormalizeNewlines(text, newline) {
|
|
26
|
+
if (newline === '\r\n') {
|
|
27
|
+
return text.replace(/\n/g, '\r\n')
|
|
28
|
+
}
|
|
29
|
+
return text
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectNewline(text) {
|
|
33
|
+
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getNearestContext(content, oldString) {
|
|
37
|
+
const lines = content.split(/\r?\n/)
|
|
38
|
+
const oldLines = normalizeNewlines(oldString).split('\n').filter(Boolean)
|
|
39
|
+
const firstToken = oldLines[0] || oldString.trim()
|
|
40
|
+
|
|
41
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
42
|
+
if (lines[index].includes(firstToken)) {
|
|
43
|
+
return lines.slice(index, index + 4).join('\n')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines.slice(0, 4).join('\n')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildEditFailureMessage(content, oldString) {
|
|
51
|
+
return [
|
|
52
|
+
'未找到需要替换的文本。',
|
|
53
|
+
'oldString 必须与文件实际内容完全对应。',
|
|
54
|
+
'最接近的上下文:',
|
|
55
|
+
getNearestContext(content, oldString),
|
|
56
|
+
].join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tryLooseBlockReplace(normalizedContent, normalizedOld, normalizedNew) {
|
|
60
|
+
const contentLines = splitNormalizedLines(normalizedContent)
|
|
61
|
+
const oldLines = trimTrailingEmptyLines(splitNormalizedLines(normalizedOld))
|
|
62
|
+
const newLines = splitNormalizedLines(normalizedNew)
|
|
63
|
+
|
|
64
|
+
if (oldLines.length === 0) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let start = 0; start < contentLines.length; start += 1) {
|
|
69
|
+
let contentIndex = start
|
|
70
|
+
let oldIndex = 0
|
|
71
|
+
|
|
72
|
+
while (oldIndex < oldLines.length && contentIndex < contentLines.length) {
|
|
73
|
+
const expected = canonicalLine(oldLines[oldIndex])
|
|
74
|
+
const actual = canonicalLine(contentLines[contentIndex])
|
|
75
|
+
|
|
76
|
+
if (expected === actual) {
|
|
77
|
+
oldIndex += 1
|
|
78
|
+
contentIndex += 1
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (actual === '') {
|
|
83
|
+
contentIndex += 1
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (oldIndex === oldLines.length) {
|
|
91
|
+
const replaced = [
|
|
92
|
+
...contentLines.slice(0, start),
|
|
93
|
+
...newLines,
|
|
94
|
+
...contentLines.slice(contentIndex),
|
|
95
|
+
].join('\n')
|
|
96
|
+
|
|
97
|
+
return replaced
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function applyLegacyEdit(content, oldString, newString, replaceAll = false) {
|
|
105
|
+
const newline = detectNewline(content)
|
|
106
|
+
const normalizedContent = normalizeNewlines(content)
|
|
107
|
+
const normalizedOld = normalizeNewlines(oldString)
|
|
108
|
+
const normalizedNew = normalizeNewlines(newString)
|
|
109
|
+
|
|
110
|
+
let nextNormalized = replaceAll
|
|
111
|
+
? normalizedContent.split(normalizedOld).join(normalizedNew)
|
|
112
|
+
: normalizedContent.replace(normalizedOld, normalizedNew)
|
|
113
|
+
|
|
114
|
+
if (normalizedContent === nextNormalized && !replaceAll) {
|
|
115
|
+
const looseReplaced = tryLooseBlockReplace(normalizedContent, normalizedOld, normalizedNew)
|
|
116
|
+
if (looseReplaced !== null) {
|
|
117
|
+
nextNormalized = looseReplaced
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (normalizedContent === nextNormalized) {
|
|
122
|
+
return {
|
|
123
|
+
changed: false,
|
|
124
|
+
content,
|
|
125
|
+
error: buildEditFailureMessage(content, oldString),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
changed: true,
|
|
131
|
+
content: denormalizeNewlines(nextNormalized, newline),
|
|
132
|
+
error: null,
|
|
133
|
+
}
|
|
134
|
+
}
|