@movemama/opencode-legacy 1.0.1 → 1.0.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @movemama/opencode-legacy
2
2
 
3
- OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt` 编辑和 legacy 规则路由场景。
3
+ OpenCode legacy 文本处理插件,默认更偏向 `GBK` 文本兼容,同时支持手动指定 `GB2312`、脚本型 `.txt` 编辑和 legacy 规则路由场景。
4
4
 
5
5
  当前主链路已经改为纯 JS 编码实现,不再依赖系统外部 `iconv.exe`。
6
6
 
@@ -72,6 +72,68 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
72
72
 
73
73
  这让插件可以按场景决定更合适的编辑策略,而不只是按编码读写。
74
74
 
75
+ ## 自定义扩展规则
76
+
77
+ 如果用户希望让其他文件类型也走 `GBK` / `GB2312` 的 legacy 读写链路,当前可以通过项目级规则扩展实现。
78
+
79
+ 推荐在当前项目放置:
80
+
81
+ - `<worktree>/.opencode/legacy-rules.json`
82
+
83
+ 例如:
84
+
85
+ ```json
86
+ {
87
+ "rules": [
88
+ {
89
+ "glob": "**/*.{npc,msg,dialog}",
90
+ "encoding": "gbk",
91
+ "strict": true,
92
+ "tool": "txt-gb2312",
93
+ "priority": 50,
94
+ "profile": "txt-gb2312-safe",
95
+ "editStrategy": "exact-first",
96
+ "fallbackMode": "legacy-safe-replace",
97
+ "scriptMarkers": []
98
+ }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ 字段说明:
104
+
105
+ - `glob`:匹配要走 legacy 读写链路的文件范围
106
+ - `encoding`:指定文件使用的编码,例如 `gbk`、`gb2312`
107
+ - `strict`:是否使用严格模式;通常脚本类文本建议保持 `true`
108
+ - `tool`:建议使用的处理器名称,当前常见值为 `txt-gb2312` 或 `legacy-text`
109
+ - `priority`:规则优先级,数值越大越优先匹配
110
+ - `profile`:场景配置名,用于帮助策略层判断文件类型与处理方式
111
+ - `editStrategy`:主编辑策略,例如 `exact-first`、`widget-field`
112
+ - `fallbackMode`:主策略失败后的回退方式,例如 `legacy-safe-replace`、`widget-field-update`
113
+ - `scriptMarkers`:用于辅助识别脚本型文件或 DSL 结构的关键标记数组
114
+
115
+ 如果客户只是想让某类新文件按中文常见编码读写,通常建议优先使用 `GBK`;如果确实需要严格限制在 `GB2312`,也可以手动指定。最小配置通常只需要先关心:
116
+
117
+ - `glob`
118
+ - `encoding`
119
+ - `strict`
120
+ - `tool`
121
+ - `priority`
122
+
123
+ 其余字段可以先参考现有示例再逐步细化。
124
+
125
+ 当前规则加载优先级为:
126
+
127
+ 1. `<worktree>/.opencode/legacy-rules.json`
128
+ 2. `<worktree>/legacy-rules.json`
129
+ 3. `~/.config/opencode/legacy-rules.json`
130
+ 4. 包内 `legacy-rules.json`
131
+
132
+ 这意味着用户现在既可以:
133
+
134
+ - 在 `~/.config/opencode/legacy-rules.json` 中定义全局通用扩展规则
135
+ - 又可以在单个项目里通过 `<worktree>/.opencode/legacy-rules.json` 或 `<worktree>/legacy-rules.json` 做更高优先级覆盖
136
+
75
137
  ## 自动策略层
76
138
 
77
139
  当前 `edit` 已经接入自动策略层。
@@ -80,6 +142,8 @@ OpenCode legacy 文本处理插件,面向 `GB2312` 文本、脚本型 `.txt`
80
142
  - `classic-tag` 文本可根据 `profile` 和 `scriptMarkers` 判断为更偏 block / line-normalized 的策略
81
143
  - `rich-ui-dsl` 文本会优先识别 `<Text|...>` / `<Button|...>` 组件,并在 exact 失败后尝试 widget 字段级更新
82
144
  - 失败时会返回当前策略、格式族和 fallback 提示,便于继续排障
145
+ - 编辑成功时也会返回:检测格式、修改策略、修改次数、命中位置,以及旧片段 / 新片段摘要,便于像内置 `edit` 一样快速确认改动位置
146
+ - 插件还会通过 `tool.execute.after` 对 legacy edit 结果做统一包装,补充一致的标题与 metadata,方便后续继续往内置 `edit` 体验靠拢
83
147
 
84
148
  ## 支持的格式族
85
149
 
package/legacy-rules.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "_comment": "rules 为实际生效规则;examples 为示例模板,不会自动生效。新增文件类型时,优先复制 examples 中最接近的一条到 rules。tool 可选值当前建议使用 txt-gb2312 或 legacy-text",
2
+ "_comment": "rules 为实际生效规则;examples 为示例模板,不会自动生效。新增文件类型时,优先复制 examples 中最接近的一条到 rules。tool 可选值当前建议使用 txt-gb2312 或 legacy-text。默认 .txt 规则现已优先使用 gbk,以获得更好的中文兼容性。",
3
3
  "rules": [
4
4
  {
5
5
  "glob": "**/*.txt",
6
- "encoding": "gb2312",
6
+ "encoding": "gbk",
7
7
  "strict": true,
8
8
  "tool": "txt-gb2312",
9
9
  "priority": 10,
@@ -14,7 +14,7 @@
14
14
  },
15
15
  {
16
16
  "glob": "**/*.txt",
17
- "encoding": "gb2312",
17
+ "encoding": "gbk",
18
18
  "strict": true,
19
19
  "tool": "txt-gb2312",
20
20
  "priority": 20,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@movemama/opencode-legacy",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "OpenCode legacy text processing plugin for GB2312 and script-safe editing workflows.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/plugin-meta.js CHANGED
@@ -10,6 +10,6 @@ export function getPluginMeta() {
10
10
  'legacy_write',
11
11
  'legacy_edit',
12
12
  ],
13
- hookNames: ['experimental.chat.system.transform', 'tool.execute.before'],
13
+ hookNames: ['experimental.chat.system.transform', 'tool.execute.before', 'tool.execute.after'],
14
14
  }
15
15
  }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
 
4
4
  const EDIT_GUARDED_TOOLS = new Set(['edit', 'write', 'script-edit', 'legacy_edit', 'legacy_write'])
5
+ const LEGACY_EDIT_FEEDBACK_TOOLS = new Set(['edit', 'legacy_edit', 'script-edit'])
5
6
 
6
7
  function normalizeLineEndings(content) {
7
8
  return content.replace(/\r\n/g, '\n').trim()
@@ -72,5 +73,21 @@ export function createAgentsRuleHooks(input = {}) {
72
73
  throw new Error('当前会话尚未完成 AGENTS.md 规则注入,已禁止编辑类工具执行。请先让插件读取全局与项目 AGENTS.md。')
73
74
  }
74
75
  },
76
+ 'tool.execute.after': async (hookInput, output) => {
77
+ if (!LEGACY_EDIT_FEEDBACK_TOOLS.has(hookInput.tool)) {
78
+ return
79
+ }
80
+
81
+ if (typeof output?.output !== 'string' || !output.output.includes('修改策略:')) {
82
+ return
83
+ }
84
+
85
+ output.title = 'legacy edit'
86
+ output.metadata = {
87
+ ...(output.metadata || {}),
88
+ kind: 'legacy-edit-feedback',
89
+ tool: hookInput.tool,
90
+ }
91
+ },
75
92
  }
76
93
  }
package/tools/edit.js CHANGED
@@ -4,6 +4,7 @@ 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 { buildEditFeedback } from './legacy-edit-feedback.js'
7
8
  import { matchLegacyRule } from './legacy-router.mjs'
8
9
  import { loadLegacyRules } from './legacy-rules-loader.js'
9
10
  import { chooseEditStrategy } from './legacy-strategy.js'
@@ -34,7 +35,14 @@ export default tool({
34
35
  const result = applyLegacyEdit(content, args.oldString, args.newString, Boolean(args.replaceAll))
35
36
 
36
37
  if (result.changed) {
37
- return writeTool.execute({ filePath, content: result.content }, context)
38
+ const writeMessage = await writeTool.execute({ filePath, content: result.content }, context)
39
+ return buildEditFeedback({
40
+ writeMessage,
41
+ detectedFamily: strategy.detectedFamily,
42
+ strategy: 'exact-first',
43
+ originalContent: content,
44
+ updatedContent: result.content,
45
+ })
38
46
  }
39
47
 
40
48
  const fallbackResult = applyFallbackEditChain({
@@ -45,7 +53,14 @@ export default tool({
45
53
  })
46
54
 
47
55
  if (fallbackResult.changed) {
48
- return writeTool.execute({ filePath, content: fallbackResult.content }, context)
56
+ const writeMessage = await writeTool.execute({ filePath, content: fallbackResult.content }, context)
57
+ return buildEditFeedback({
58
+ writeMessage,
59
+ detectedFamily: strategy.detectedFamily,
60
+ strategy: fallbackResult.strategy,
61
+ originalContent: content,
62
+ updatedContent: fallbackResult.content,
63
+ })
49
64
  }
50
65
 
51
66
  throw new Error(
@@ -0,0 +1,68 @@
1
+ function normalizeNewlines(text) {
2
+ return text.replace(/\r\n/g, '\n')
3
+ }
4
+
5
+ function buildPositionLabel(startLine, endLine) {
6
+ if (startLine === endLine) {
7
+ return `${startLine}`
8
+ }
9
+
10
+ return `${startLine}-${endLine}`
11
+ }
12
+
13
+ function summarizeChangedBlock(original, updated) {
14
+ const originalLines = normalizeNewlines(original).split('\n')
15
+ const updatedLines = normalizeNewlines(updated).split('\n')
16
+
17
+ let prefix = 0
18
+ while (
19
+ prefix < originalLines.length
20
+ && prefix < updatedLines.length
21
+ && originalLines[prefix] === updatedLines[prefix]
22
+ ) {
23
+ prefix += 1
24
+ }
25
+
26
+ let suffix = 0
27
+ while (
28
+ suffix < originalLines.length - prefix
29
+ && suffix < updatedLines.length - prefix
30
+ && originalLines[originalLines.length - 1 - suffix] === updatedLines[updatedLines.length - 1 - suffix]
31
+ ) {
32
+ suffix += 1
33
+ }
34
+
35
+ const originalEnd = Math.max(prefix, originalLines.length - suffix)
36
+ const updatedEnd = Math.max(prefix, updatedLines.length - suffix)
37
+ const oldBlock = originalLines.slice(prefix, originalEnd)
38
+ const newBlock = updatedLines.slice(prefix, updatedEnd)
39
+ const startLine = prefix + 1
40
+ const endLine = Math.max(prefix + oldBlock.length, prefix + newBlock.length)
41
+
42
+ return {
43
+ changeCount: 1,
44
+ position: buildPositionLabel(startLine, endLine),
45
+ oldSnippet: oldBlock.join('\n'),
46
+ newSnippet: newBlock.join('\n'),
47
+ }
48
+ }
49
+
50
+ export function buildEditFeedback({
51
+ writeMessage,
52
+ detectedFamily,
53
+ strategy,
54
+ originalContent,
55
+ updatedContent,
56
+ }) {
57
+ const summary = summarizeChangedBlock(originalContent, updatedContent)
58
+
59
+ return [
60
+ writeMessage,
61
+ `检测格式:${detectedFamily}`,
62
+ `修改策略:${strategy}`,
63
+ `修改次数:${summary.changeCount}`,
64
+ `命中位置:${summary.position}`,
65
+ `旧片段:${summary.oldSnippet}`,
66
+ `新片段:${summary.newSnippet}`,
67
+ ].join('\n')
68
+ }
@@ -55,7 +55,7 @@ function globToRegExp(glob) {
55
55
 
56
56
  export function createDefaultLegacyRules() {
57
57
  return [
58
- { glob: '**/*.txt', encoding: 'gb2312', strict: true },
58
+ { glob: '**/*.txt', encoding: 'gbk', strict: true },
59
59
  { glob: '**/*.{ini,cfg,dat}', encoding: 'gbk', strict: false },
60
60
  ];
61
61
  }
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
 
4
4
  import { createDefaultLegacyRules } from './legacy-router.mjs'
5
- import { getBundledLegacyRulesPath } from './opencode-paths.mjs'
5
+ import { getBundledLegacyRulesPath, getGlobalLegacyRulesPath } from './opencode-paths.mjs'
6
6
 
7
7
  export function loadLegacyRules(worktree) {
8
8
  return loadLegacyRulesWithMeta(worktree).rules
@@ -12,6 +12,7 @@ export function loadLegacyRulesWithMeta(worktree) {
12
12
  const candidates = [
13
13
  { filePath: path.join(worktree, '.opencode', 'legacy-rules.json'), source: 'project:.opencode' },
14
14
  { filePath: path.join(worktree, 'legacy-rules.json'), source: 'project:root' },
15
+ { filePath: getGlobalLegacyRulesPath(), source: 'global:config' },
15
16
  { filePath: getBundledLegacyRulesPath(), source: 'bundled:default' },
16
17
  ]
17
18