@movemama/opencode-legacy 0.1.1 → 1.0.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 +52 -2
- package/index.js +21 -12
- package/legacy-rules.json +11 -0
- package/package.json +1 -1
- package/plugin-meta.js +1 -0
- package/tools/agents-rules.js +76 -0
- package/tools/edit.js +17 -3
- package/tools/legacy-format-detector.js +21 -0
- package/tools/legacy-status.js +4 -0
- package/tools/legacy-strategy.js +11 -2
- package/tools/legacy-widget-edit.js +227 -0
package/README.md
CHANGED
|
@@ -4,6 +4,25 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
|
|
|
4
4
|
|
|
5
5
|
当前主链路已经改为纯 JS 编码实现,不再依赖系统外部 `iconv.exe`。
|
|
6
6
|
|
|
7
|
+
## 规则注入
|
|
8
|
+
|
|
9
|
+
插件现在会在每轮对话自动读取并注入以下规则文件:
|
|
10
|
+
|
|
11
|
+
1. 全局 `AGENTS.md`:根据当前环境变量 `USERPROFILE` 或 `HOME` 动态解析到 `~/.config/opencode/AGENTS.md`
|
|
12
|
+
2. 项目 `AGENTS.md`:根据当前打开项目的 `worktree` 动态解析到 `<worktree>/AGENTS.md`
|
|
13
|
+
|
|
14
|
+
注入后的硬规则会进入系统上下文,用于约束代理优先读取规则、完整读取目标文件、以及统一输出回执。
|
|
15
|
+
|
|
16
|
+
另外,插件已为以下编辑类工具增加前置守卫:
|
|
17
|
+
|
|
18
|
+
- `edit`
|
|
19
|
+
- `write`
|
|
20
|
+
- `script-edit`
|
|
21
|
+
- `legacy_edit`
|
|
22
|
+
- `legacy_write`
|
|
23
|
+
|
|
24
|
+
如果当前会话尚未完成 `AGENTS.md` 规则注入,上述工具会直接拒绝执行,以避免代理在未读取规则的情况下改文件。
|
|
25
|
+
|
|
7
26
|
## 安装
|
|
8
27
|
|
|
9
28
|
在 OpenCode 配置中加入:
|
|
@@ -58,8 +77,24 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
|
|
|
58
77
|
当前 `edit` 已经接入自动策略层。
|
|
59
78
|
|
|
60
79
|
- 普通文本默认走 `exact-first`
|
|
61
|
-
-
|
|
62
|
-
-
|
|
80
|
+
- `classic-tag` 文本可根据 `profile` 和 `scriptMarkers` 判断为更偏 block / line-normalized 的策略
|
|
81
|
+
- `rich-ui-dsl` 文本会优先识别 `<Text|...>` / `<Button|...>` 组件,并在 exact 失败后尝试 widget 字段级更新
|
|
82
|
+
- 失败时会返回当前策略、格式族和 fallback 提示,便于继续排障
|
|
83
|
+
|
|
84
|
+
## 支持的格式族
|
|
85
|
+
|
|
86
|
+
- `classic-tag`
|
|
87
|
+
- 例如 `[@main]`、`<领取/@领取>`、`<关闭/@exit>`
|
|
88
|
+
- 当前已支持:菜单行宽松匹配、标签块替换、跳转目标校验
|
|
89
|
+
- `rich-ui-dsl`
|
|
90
|
+
- 例如 `<Text|x=...|text=...>`、`<Button|...|link=@...>`
|
|
91
|
+
- 当前已支持:多行 widget 定位、字段级更新、children 字段保留
|
|
92
|
+
|
|
93
|
+
当前这两类是第一批重点支持对象。
|
|
94
|
+
|
|
95
|
+
- `general-code`
|
|
96
|
+
- 例如 C++ / C# / 强语法代码文本
|
|
97
|
+
- 当前仅做安全识别与保守回退,不做激进结构改写
|
|
63
98
|
|
|
64
99
|
## 诊断工具
|
|
65
100
|
|
|
@@ -68,6 +103,7 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
|
|
|
68
103
|
- 插件版本
|
|
69
104
|
- 规则来源
|
|
70
105
|
- 命中规则
|
|
106
|
+
- 格式族
|
|
71
107
|
- profile
|
|
72
108
|
- 建议策略
|
|
73
109
|
- fallback 模式
|
|
@@ -87,6 +123,20 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
|
|
|
87
123
|
|
|
88
124
|
也可以直接调用 `legacy_status` 检查当前文件会命中哪条规则、建议使用哪种策略。
|
|
89
125
|
|
|
126
|
+
如果检测为 `general-code`,当前插件会优先保持保守,不会像 rich-ui / classic-tag 那样做特化结构修改;这部分为未来语法感知层预留空间。
|
|
127
|
+
|
|
128
|
+
如果检测为 `classic-tag`,当前插件已能在 exact 失败后继续尝试:
|
|
129
|
+
|
|
130
|
+
- 菜单行 token 级匹配
|
|
131
|
+
- 标签块局部替换
|
|
132
|
+
- 跳转标签存在性检查
|
|
133
|
+
|
|
134
|
+
如果检测为 `rich-ui-dsl`,当前插件已能在 exact 失败后继续尝试:
|
|
135
|
+
|
|
136
|
+
- widget 字段级更新
|
|
137
|
+
- 多行 widget 定位
|
|
138
|
+
- 保留 `children={...}` 容器字段
|
|
139
|
+
|
|
90
140
|
## 兼容说明
|
|
91
141
|
|
|
92
142
|
- `txt-gb2312-tool.mjs` 目前仅保留为兼容遗留文件,不再是主链路依赖
|
package/index.js
CHANGED
|
@@ -4,18 +4,27 @@ import editTool from './tools/edit.js'
|
|
|
4
4
|
import scriptEditTool from './tools/script-edit.js'
|
|
5
5
|
import legacyStatusTool from './tools/legacy-status.js'
|
|
6
6
|
import { edit as legacyEditTool, read as legacyReadTool, write as legacyWriteTool } from './tools/legacy.js'
|
|
7
|
+
import { createAgentsRuleHooks } from './tools/agents-rules.js'
|
|
7
8
|
|
|
8
|
-
export const OpenCodeLegacyPlugin = async () =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
export const OpenCodeLegacyPlugin = async (context, options = {}) => {
|
|
10
|
+
const agentsRuleHooks = createAgentsRuleHooks({
|
|
11
|
+
worktree: context.worktree,
|
|
12
|
+
globalAgentsPath: options.globalAgentsPath,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
tool: {
|
|
17
|
+
read: readTool,
|
|
18
|
+
write: writeTool,
|
|
19
|
+
edit: editTool,
|
|
20
|
+
'script-edit': scriptEditTool,
|
|
21
|
+
legacy_read: legacyReadTool,
|
|
22
|
+
legacy_write: legacyWriteTool,
|
|
23
|
+
legacy_edit: legacyEditTool,
|
|
24
|
+
legacy_status: legacyStatusTool,
|
|
25
|
+
},
|
|
26
|
+
...agentsRuleHooks,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
export default OpenCodeLegacyPlugin
|
package/legacy-rules.json
CHANGED
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
"fallbackMode": "legacy-safe-replace",
|
|
13
13
|
"scriptMarkers": ["#ACT", "@main", "@全体补偿页面"]
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
"glob": "**/*.txt",
|
|
17
|
+
"encoding": "gb2312",
|
|
18
|
+
"strict": true,
|
|
19
|
+
"tool": "txt-gb2312",
|
|
20
|
+
"priority": 20,
|
|
21
|
+
"profile": "rich-ui-dsl",
|
|
22
|
+
"editStrategy": "widget-field",
|
|
23
|
+
"fallbackMode": "widget-field-update",
|
|
24
|
+
"scriptMarkers": ["<Text|", "<Button|", "<Img|", "<Layout|"]
|
|
25
|
+
},
|
|
15
26
|
{
|
|
16
27
|
"glob": "**/*.{ini,cfg,dat}",
|
|
17
28
|
"encoding": "gbk",
|
package/package.json
CHANGED
package/plugin-meta.js
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const EDIT_GUARDED_TOOLS = new Set(['edit', 'write', 'script-edit', 'legacy_edit', 'legacy_write'])
|
|
5
|
+
|
|
6
|
+
function normalizeLineEndings(content) {
|
|
7
|
+
return content.replace(/\r\n/g, '\n').trim()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getDefaultGlobalAgentsPath() {
|
|
11
|
+
const home = process.env.USERPROFILE || process.env.HOME || ''
|
|
12
|
+
return path.join(home, '.config', 'opencode', 'AGENTS.md')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readAgentsFileIfExists(filePath) {
|
|
16
|
+
if (!filePath || !existsSync(filePath)) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return normalizeLineEndings(readFileSync(filePath, 'utf8'))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildInjectedRulesBlock(entries) {
|
|
24
|
+
const sections = entries
|
|
25
|
+
.filter((entry) => entry.content)
|
|
26
|
+
.map((entry) => [`## ${entry.label}`, entry.content].join('\n'))
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
'[AGENTS Rules Bootstrap]',
|
|
30
|
+
'你必须在本轮严格遵守以下规则优先级:系统 > 开发者 > 全局 AGENTS.md > 项目 AGENTS.md > 用户临时偏好。',
|
|
31
|
+
'硬性要求:分析或编辑前先读取规则与目标文件完整内容;文件过长时连续分段读取;上下文不足前不得做结构判断或编辑判断。',
|
|
32
|
+
'硬性要求:所有用户可见输出必须使用简体中文;受限时必须说明受限原因、影响范围、备选方案;回复末尾必须输出规则执行回执。',
|
|
33
|
+
...sections,
|
|
34
|
+
].join('\n\n')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createAgentsRuleHooks(input = {}) {
|
|
38
|
+
const globalAgentsPath = input.globalAgentsPath || getDefaultGlobalAgentsPath()
|
|
39
|
+
const projectAgentsPath = path.join(input.worktree, 'AGENTS.md')
|
|
40
|
+
const sessionBootstrapState = new Set()
|
|
41
|
+
|
|
42
|
+
function loadRules() {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
label: 'Global AGENTS.md',
|
|
46
|
+
path: globalAgentsPath,
|
|
47
|
+
content: readAgentsFileIfExists(globalAgentsPath),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: 'Project AGENTS.md',
|
|
51
|
+
path: projectAgentsPath,
|
|
52
|
+
content: readAgentsFileIfExists(projectAgentsPath),
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
'experimental.chat.system.transform': async (hookInput, output) => {
|
|
59
|
+
const entries = loadRules()
|
|
60
|
+
const injected = buildInjectedRulesBlock(entries)
|
|
61
|
+
output.system.push(injected)
|
|
62
|
+
if (hookInput?.sessionID) {
|
|
63
|
+
sessionBootstrapState.add(hookInput.sessionID)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
'tool.execute.before': async (hookInput) => {
|
|
67
|
+
if (!EDIT_GUARDED_TOOLS.has(hookInput.tool)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!sessionBootstrapState.has(hookInput.sessionID)) {
|
|
72
|
+
throw new Error('当前会话尚未完成 AGENTS.md 规则注入,已禁止编辑类工具执行。请先让插件读取全局与项目 AGENTS.md。')
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
package/tools/edit.js
CHANGED
|
@@ -7,6 +7,7 @@ import { applyLegacyEdit } from './legacy-edit-core.mjs'
|
|
|
7
7
|
import { matchLegacyRule } from './legacy-router.mjs'
|
|
8
8
|
import { loadLegacyRules } from './legacy-rules-loader.js'
|
|
9
9
|
import { chooseEditStrategy } from './legacy-strategy.js'
|
|
10
|
+
import { applyFallbackEditChain } from './legacy-widget-edit.js'
|
|
10
11
|
|
|
11
12
|
function resolvePath(filePath, worktree) {
|
|
12
13
|
return path.isAbsolute(filePath) ? filePath : path.join(worktree, filePath)
|
|
@@ -32,10 +33,23 @@ export default tool({
|
|
|
32
33
|
const strategy = chooseEditStrategy({ filePath, content, matchedRule })
|
|
33
34
|
const result = applyLegacyEdit(content, args.oldString, args.newString, Boolean(args.replaceAll))
|
|
34
35
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
36
|
+
if (result.changed) {
|
|
37
|
+
return writeTool.execute({ filePath, content: result.content }, context)
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
const fallbackResult = applyFallbackEditChain({
|
|
41
|
+
content,
|
|
42
|
+
oldString: args.oldString,
|
|
43
|
+
newString: args.newString,
|
|
44
|
+
detectedFamily: strategy.detectedFamily,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (fallbackResult.changed) {
|
|
48
|
+
return writeTool.execute({ filePath, content: fallbackResult.content }, context)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error(
|
|
52
|
+
`${result.error} | strategy=${strategy.strategy} | detected=${strategy.detectedFamily} | fallback=${strategy.fallbackMode}`,
|
|
53
|
+
)
|
|
40
54
|
},
|
|
41
55
|
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function detectLegacyFormat(content) {
|
|
2
|
+
const normalized = content.replace(/\r\n/g, '\n')
|
|
3
|
+
|
|
4
|
+
if (/<(?:Text|Button|Img|Layout|COUNTDOWN)\|/i.test(normalized)) {
|
|
5
|
+
return { family: 'rich-ui-dsl' }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (
|
|
9
|
+
/(using\s+[A-Z][\w.]*;|namespace\s+\w+|#include\s+[<"]|class\s+\w+\s*[{:]|public\s+(class|struct|interface))/m.test(
|
|
10
|
+
normalized,
|
|
11
|
+
)
|
|
12
|
+
) {
|
|
13
|
+
return { family: 'general-code' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (/^\[@.+\]$/m.test(normalized) || /<[^<>]+\/@[^<>]+>/.test(normalized)) {
|
|
17
|
+
return { family: 'classic-tag' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { family: 'plain-text' }
|
|
21
|
+
}
|
package/tools/legacy-status.js
CHANGED
|
@@ -38,10 +38,14 @@ export default tool({
|
|
|
38
38
|
`文件路径:${filePath}`,
|
|
39
39
|
`规则来源:${source}`,
|
|
40
40
|
`命中规则:${matchedRule ? matchedRule.glob : '未命中'}`,
|
|
41
|
+
`格式族:${strategy.detectedFamily}`,
|
|
41
42
|
`profile:${strategy.profile}`,
|
|
42
43
|
`建议策略:${strategy.strategy}`,
|
|
43
44
|
`fallback:${strategy.fallbackMode}`,
|
|
44
45
|
`脚本文件:${strategy.isScriptLike ? '是' : '否'}`,
|
|
46
|
+
strategy.detectedFamily === 'classic-tag' ? 'classic-tag 能力:菜单行宽松匹配、标签块替换、跳转目标校验。' : '',
|
|
47
|
+
strategy.detectedFamily === 'rich-ui-dsl' ? 'rich-ui-dsl 能力:widget 字段级编辑、多行 widget 定位、children 字段保留。' : '',
|
|
48
|
+
strategy.detectedFamily === 'general-code' ? '提示:general-code 当前仅提供保守兼容,后续建议接入语法感知层。' : '提示:当前格式已接入分层编辑策略。',
|
|
45
49
|
].join('\n')
|
|
46
50
|
},
|
|
47
51
|
})
|
package/tools/legacy-strategy.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { detectLegacyFormat } from './legacy-format-detector.js'
|
|
2
|
+
|
|
1
3
|
export function chooseEditStrategy({ filePath, content, matchedRule }) {
|
|
4
|
+
const detected = detectLegacyFormat(content)
|
|
2
5
|
const markers = matchedRule?.scriptMarkers ?? []
|
|
3
6
|
const hasMarker = markers.some((marker) => content.includes(marker))
|
|
4
7
|
const looksLikeScript = hasMarker || /(^|\n)@\w+/.test(content) || content.includes('#ACT')
|
|
8
|
+
const profile = matchedRule?.profile ?? (detected.family === 'rich-ui-dsl' ? 'rich-ui-dsl' : detected.family === 'classic-tag' ? 'classic-tag' : 'default')
|
|
9
|
+
const strategy = matchedRule?.editStrategy
|
|
10
|
+
?? (detected.family === 'rich-ui-dsl' ? 'widget-field'
|
|
11
|
+
: looksLikeScript ? 'block-first'
|
|
12
|
+
: 'exact-first')
|
|
5
13
|
|
|
6
14
|
return {
|
|
7
15
|
filePath,
|
|
8
|
-
profile
|
|
9
|
-
|
|
16
|
+
profile,
|
|
17
|
+
detectedFamily: detected.family,
|
|
18
|
+
strategy,
|
|
10
19
|
fallbackMode: matchedRule?.fallbackMode ?? (looksLikeScript ? 'suggest-script-edit' : 'legacy-safe-replace'),
|
|
11
20
|
isScriptLike: looksLikeScript,
|
|
12
21
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
function splitWidgetLine(line) {
|
|
2
|
+
if (!line.startsWith('<') || !line.endsWith('>') || !line.includes('|')) {
|
|
3
|
+
return null
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const body = line.slice(1, -1)
|
|
7
|
+
const parts = body.split('|')
|
|
8
|
+
const kind = parts.shift()
|
|
9
|
+
return { kind, parts }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function partsToFields(parts) {
|
|
13
|
+
const fields = {}
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
const separatorIndex = part.indexOf('=')
|
|
16
|
+
if (separatorIndex === -1) {
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
const key = part.slice(0, separatorIndex)
|
|
20
|
+
const value = part.slice(separatorIndex + 1)
|
|
21
|
+
fields[key] = value
|
|
22
|
+
}
|
|
23
|
+
return fields
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function splitClassicMenuTokens(line) {
|
|
27
|
+
const tokens = line.match(/<[^<>]+\/@[^<>]+>/g)
|
|
28
|
+
return tokens ?? []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findClassicTagBlocks(content) {
|
|
32
|
+
const lines = content.split('\n')
|
|
33
|
+
const blocks = []
|
|
34
|
+
|
|
35
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
36
|
+
if (!/^\[@.+\]$/.test(lines[index].trim())) {
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let end = lines.length - 1
|
|
41
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
42
|
+
if (/^\[@.+\]$/.test(lines[cursor].trim())) {
|
|
43
|
+
end = cursor - 1
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
blocks.push({ start: index, end, label: lines[index].trim() })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { lines, blocks }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateClassicTagLinks(content) {
|
|
55
|
+
const labels = new Set((content.match(/^\[@.+\]$/gm) ?? []).map((item) => item.trim()))
|
|
56
|
+
const links = content.match(/@[^>\s\]}|]+/g) ?? []
|
|
57
|
+
const missing = [...new Set(links.filter((link) => link !== '@exit' && !labels.has(`[${link}]`)))]
|
|
58
|
+
return {
|
|
59
|
+
valid: missing.length === 0,
|
|
60
|
+
missing,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function updateWidgetField(line, updates) {
|
|
65
|
+
const parsed = splitWidgetLine(line)
|
|
66
|
+
if (!parsed) {
|
|
67
|
+
throw new Error('不是可识别的 widget 行')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const nextParts = parsed.parts.map((part) => {
|
|
71
|
+
const separatorIndex = part.indexOf('=')
|
|
72
|
+
if (separatorIndex === -1) {
|
|
73
|
+
return part
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const key = part.slice(0, separatorIndex)
|
|
77
|
+
if (!(key in updates)) {
|
|
78
|
+
return part
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `${key}=${updates[key]}`
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return `<${parsed.kind}|${nextParts.join('|')}>`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function updateWidgetBySignature(content, oldWidgetLine, newText) {
|
|
88
|
+
const target = splitWidgetLine(oldWidgetLine)
|
|
89
|
+
if (!target) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const targetFields = partsToFields(target.parts)
|
|
94
|
+
const lines = content.split('\n')
|
|
95
|
+
const hitIndex = lines.findIndex((line) => {
|
|
96
|
+
const parsed = splitWidgetLine(line)
|
|
97
|
+
if (!parsed || parsed.kind !== target.kind) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fields = partsToFields(parsed.parts)
|
|
102
|
+
return ['x', 'y', 'text', 'link'].every((key) => !targetFields[key] || fields[key] === targetFields[key])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (hitIndex === -1) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines[hitIndex] = updateWidgetField(lines[hitIndex], { text: newText })
|
|
110
|
+
return lines.join('\n')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function updateClassicMenuLine(content, oldString, newString) {
|
|
114
|
+
const oldTokens = splitClassicMenuTokens(oldString)
|
|
115
|
+
const newTokens = splitClassicMenuTokens(newString)
|
|
116
|
+
if (oldTokens.length === 0 || oldTokens.length !== newTokens.length) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines = content.split('\n')
|
|
121
|
+
const hitIndex = lines.findIndex((line) => {
|
|
122
|
+
const lineTokens = splitClassicMenuTokens(line)
|
|
123
|
+
if (lineTokens.length !== oldTokens.length) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
return oldTokens.every((token, index) => lineTokens[index] === token)
|
|
127
|
+
|| oldTokens.every((token) => lineTokens.includes(token))
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (hitIndex === -1) {
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lineTokens = splitClassicMenuTokens(lines[hitIndex])
|
|
135
|
+
let nextLine = lines[hitIndex]
|
|
136
|
+
for (let index = 0; index < oldTokens.length; index += 1) {
|
|
137
|
+
if (lineTokens.includes(oldTokens[index])) {
|
|
138
|
+
nextLine = nextLine.replace(oldTokens[index], newTokens[index])
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
lines[hitIndex] = nextLine
|
|
142
|
+
return lines.join('\n')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function updateClassicTagBlock(content, oldString, newString) {
|
|
146
|
+
const oldLabelMatch = oldString.match(/^\[@.+\]$/m)
|
|
147
|
+
if (!oldLabelMatch) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { lines, blocks } = findClassicTagBlocks(content)
|
|
152
|
+
const target = blocks.find((block) => block.label === oldLabelMatch[0].trim())
|
|
153
|
+
if (!target) {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const nextLines = [...lines]
|
|
158
|
+
const replacementStart = target.start + 1
|
|
159
|
+
const replacementEnd = target.end
|
|
160
|
+
nextLines.splice(replacementStart, replacementEnd - replacementStart + 1, newString)
|
|
161
|
+
return nextLines.join('\n')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function applyFallbackEditChain({ content, oldString, newString, detectedFamily }) {
|
|
165
|
+
if (content.includes(oldString)) {
|
|
166
|
+
return {
|
|
167
|
+
changed: true,
|
|
168
|
+
content: content.replace(oldString, newString),
|
|
169
|
+
strategy: 'exact-first',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (detectedFamily === 'rich-ui-dsl') {
|
|
174
|
+
const updated = updateWidgetBySignature(content, oldString, newString)
|
|
175
|
+
if (updated) {
|
|
176
|
+
return {
|
|
177
|
+
changed: true,
|
|
178
|
+
content: updated,
|
|
179
|
+
strategy: 'widget-field',
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (/<Text\|/.test(content) && content.includes(`text=${oldString}`)) {
|
|
184
|
+
return {
|
|
185
|
+
changed: true,
|
|
186
|
+
content: updateWidgetField(content, { text: newString }),
|
|
187
|
+
strategy: 'widget-field',
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (detectedFamily === 'classic-tag') {
|
|
193
|
+
const blockUpdated = updateClassicTagBlock(content, oldString, newString)
|
|
194
|
+
if (blockUpdated) {
|
|
195
|
+
return {
|
|
196
|
+
changed: true,
|
|
197
|
+
content: blockUpdated,
|
|
198
|
+
strategy: 'classic-tag-block',
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const updated = updateClassicMenuLine(content, oldString, newString)
|
|
203
|
+
if (updated) {
|
|
204
|
+
return {
|
|
205
|
+
changed: true,
|
|
206
|
+
content: updated,
|
|
207
|
+
strategy: 'classic-tag-line-normalized',
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const normalizedContent = content.replace(/[ \t]+/g, ' ')
|
|
212
|
+
const normalizedOld = oldString.replace(/[ \t]+/g, ' ')
|
|
213
|
+
if (normalizedContent.includes(normalizedOld)) {
|
|
214
|
+
return {
|
|
215
|
+
changed: true,
|
|
216
|
+
content: normalizedContent.replace(normalizedOld, newString),
|
|
217
|
+
strategy: 'classic-tag-line-normalized',
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
changed: false,
|
|
224
|
+
content,
|
|
225
|
+
strategy: 'no-match',
|
|
226
|
+
}
|
|
227
|
+
}
|