@harmonyos-arkts/d2h 0.1.10 → 0.1.12

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.
Files changed (126) hide show
  1. package/.claude-plugin/marketplace.json +15 -13
  2. package/.claude-plugin/plugin.json +13 -7
  3. package/AGENTS.md +5 -5
  4. package/README.md +212 -212
  5. package/agents/android-to-hmos-00-orchestrator.md +318 -318
  6. package/agents/d2h.md +157 -157
  7. package/dist/bin/install-opencode.js +30 -30
  8. package/docs/environment-check.md +86 -86
  9. package/install-opencode.sh +160 -160
  10. package/opencode/agents.json +16 -16
  11. package/opencode/vscode-d2h-package.json +7 -7
  12. package/opencode/vscode-d2h-plugin.mjs +29 -29
  13. package/package.json +54 -54
  14. package/schemas/android-source-manifest.schema.json +25 -25
  15. package/schemas/checkpoint-provenance.schema.json +67 -67
  16. package/schemas/final-acceptance.schema.json +29 -29
  17. package/schemas/managed-evidence-index.schema.json +15 -15
  18. package/schemas/managed-evidence.schema.json +114 -114
  19. package/schemas/migration-config.schema.json +51 -51
  20. package/schemas/migration-report-index.schema.json +39 -39
  21. package/schemas/migration-report-item.schema.json +51 -51
  22. package/schemas/migration-report-summary.schema.json +25 -25
  23. package/schemas/migration-status.schema.json +202 -202
  24. package/schemas/preflight.schema.json +60 -60
  25. package/schemas/source-order-audit.schema.json +16 -16
  26. package/schemas/source-provenance-event.schema.json +19 -19
  27. package/schemas/spec-app-shard.schema.json +45 -45
  28. package/schemas/spec-fact-corrections-shard.schema.json +13 -13
  29. package/schemas/spec-features-shard.schema.json +43 -43
  30. package/schemas/spec-index.schema.json +26 -26
  31. package/schemas/spec-interactions-shard.schema.json +40 -40
  32. package/schemas/spec-page.schema.json +98 -98
  33. package/schemas/spec-pages-index.schema.json +1 -1
  34. package/schemas/spec-unresolved-shard.schema.json +1 -1
  35. package/schemas/task-envelope.schema.json +109 -109
  36. package/schemas/task-plan.schema.json +341 -341
  37. package/scripts/check-environment.mjs +320 -320
  38. package/skills/android2hmos_resources_convert/SKILL.md +180 -180
  39. package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
  40. package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
  41. package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
  42. package/skills/arkts-app-identity/SKILL.md +238 -238
  43. package/skills/arkts-i18n/SKILL.md +496 -496
  44. package/skills/arkts-i18n/evals/evals.json +84 -84
  45. package/skills/arkts-i18n/references/code-examples.md +302 -302
  46. package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
  47. package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
  48. package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
  49. package/skills/arkts-i18n/references/language-codes.md +104 -104
  50. package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
  51. package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
  52. package/skills/arkts-i18n/references/v1-compat.md +244 -244
  53. package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
  54. package/skills/arkts-icon-sizing/SKILL.md +211 -211
  55. package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
  56. package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
  57. package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
  58. package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
  59. package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
  60. package/skills/d2h-hypium-forge/SKILL.md +309 -309
  61. package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
  62. package/skills/d2h-hypium-forge/references/official-api.md +135 -135
  63. package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
  64. package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
  65. package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
  66. package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
  67. package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
  68. package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
  69. package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
  70. package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
  71. package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
  72. package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
  73. package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
  74. package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
  75. package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
  76. package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
  77. package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
  78. package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
  79. package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
  80. package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
  81. package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
  82. package/skills/d2h-page-develop/SKILL.md +112 -112
  83. package/skills/d2h-page-develop/agents/openai.yaml +7 -7
  84. package/skills/d2h-page-spec-generate/SKILL.md +206 -206
  85. package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
  86. package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
  87. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
  88. package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
  89. package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
  90. package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
  91. package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
  92. package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
  93. package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
  94. package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
  95. package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
  96. package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
  97. package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
  98. package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
  99. package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
  100. package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
  101. package/skills/d2h-utg-static-generate/SKILL.md +73 -73
  102. package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
  103. package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
  104. package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
  105. package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
  106. package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
  107. package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
  108. package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
  109. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
  110. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
  111. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
  112. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
  113. package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
  114. package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
  115. package/skills/maintaining-migration-report/SKILL.md +144 -144
  116. package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
  117. package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
  118. package/skills/managing-d2h-toolchain/SKILL.md +24 -24
  119. package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
  120. package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
  121. package/skills/managing-d2h-toolchain/toolchain.json +16 -16
  122. package/skills/native-library-substitution/SKILL.md +384 -384
  123. package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
  124. package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
  125. package/skills/preparing-migration-workspace/SKILL.md +131 -131
  126. package/skills/reviewing-migration-process/SKILL.md +62 -62
@@ -1,348 +1,348 @@
1
- # 硬编码字符串扫描与迁移工具
2
-
3
- > 辅助扫描项目中硬编码字符串并迁移到 resources 的工具和脚本
4
-
5
- ---
6
-
7
- ## 扫描工具汇总
8
-
9
- ### 1. rg(ripgrep)快速扫描
10
-
11
- ```bash
12
- # 安装:Windows 使用 winget install Andersonby.rg 或 choco install ripgrep
13
-
14
- # 扫描 Text() 硬编码
15
- rg "Text\(['\"]" --type ets -n -o
16
-
17
- # 扫描 Button() 硬编码
18
- rg "Button\(['\"]" --type ets -n -o
19
-
20
- # 扫描 showToast() 硬编码
21
- rg "showToast\(['\"]" --type ets -n -o
22
-
23
- # 扫描 AlertDialog 硬编码
24
- rg "AlertDialog" --type ets -n -o
25
-
26
- # 扫描 promptAction 硬编码
27
- rg "promptAction\." --type ets -n
28
- ```
29
-
30
- ### 2. PowerShell 扫描脚本
31
-
32
- ```powershell
33
- # 扫描所有 .ets 文件中的硬编码字符串
34
- Get-ChildItem -Recurse -Filter "*.ets" | ForEach-Object {
35
- $content = Get-Content $_.FullName -Raw
36
- # 匹配单引号字符串
37
- if ($content -match "Text\(['\']([^'\)]+)['\']") {
38
- Write-Host "$($_.FullName): $($matches[1])"
39
- }
40
- }
41
- ```
42
-
43
- ### 3. Node.js 扫描脚本
44
-
45
- ```javascript
46
- // scan-hardcoded-strings.js
47
- const fs = require('fs');
48
- const path = require('path');
49
-
50
- const patterns = [
51
- /Text\(['"]([^'"]+)['"]\)/g,
52
- /Button\(['"]([^'"]+)['"]\)/g,
53
- /\.title\(['"]([^'"]+)['"]\)/g,
54
- /\.placeholder\(['"]([^'"]+)['"]\)/g,
55
- /\.promptText\(['"]([^'"]+)['"]\)/g,
56
- /showToast\(['"]([^'"]+)['"]\)/g,
57
- ];
58
-
59
- function scanFile(filePath) {
60
- const content = fs.readFileSync(filePath, 'utf-8');
61
- const results = [];
62
-
63
- for (const pattern of patterns) {
64
- let match;
65
- while ((match = pattern.exec(content)) !== null) {
66
- const str = match[1];
67
- // 过滤
68
- if (str.length > 1 && /[\u4e00-\u9fa5a-zA-Z]/.test(str)) {
69
- results.push({
70
- file: filePath,
71
- line: content.substring(0, match.index).split('\n').length,
72
- pattern: match[0],
73
- value: str
74
- });
75
- }
76
- }
77
- }
78
- return results;
79
- }
80
-
81
- function scanDirectory(dir) {
82
- const results = [];
83
- const files = fs.readdirSync(dir, { withFileTypes: true });
84
-
85
- for (const file of files) {
86
- const fullPath = path.join(dir, file.name);
87
- if (file.isDirectory() && !file.name.startsWith('.')) {
88
- results.push(...scanDirectory(fullPath));
89
- } else if (file.name.endsWith('.ets')) {
90
- results.push(...scanFile(fullPath));
91
- }
92
- }
93
- return results;
94
- }
95
-
96
- const results = scanDirectory('./entry/src/main/ets');
97
- console.log(JSON.stringify(results, null, 2));
98
- ```
99
-
100
- ---
101
-
102
- ## 过滤规则
103
-
104
- ### 必须排除的模式
105
-
106
- ```javascript
107
- // 排除 $r() 引用
108
- const filtered = results.filter(r => !r.value.includes('$r'));
109
-
110
- // 排除纯数字
111
- const filtered = results.filter(r => !/^\d+$/.test(r.value));
112
-
113
- // 排除单字符
114
- const filtered = results.filter(r => r.value.length > 1);
115
-
116
- // 排除 URL
117
- const filtered = results.filter(r => !/^https?:\/\//.test(r.value));
118
-
119
- // 排除技术术语
120
- const filtered = results.filter(r => !/^<[^>]+>$/.test(r.value)); // HTML 标签
121
- const filtered = results.filter(r => !/^\{[^}]+\}$/.test(r.value)); // 变量引用
122
-
123
- // 排除正则表达式
124
- const filtered = results.filter(r => !/^[\[\]\{\}\(\)\.\?\+\*\^\\]$/.test(r.value));
125
- ```
126
-
127
- ---
128
-
129
- ## key 生成算法
130
-
131
- ```javascript
132
- function generateKey(str) {
133
- // 1. 转小写
134
- let key = str.toLowerCase();
135
-
136
- // 2. 移除特殊字符
137
- key = key.replace(/[^a-z0-9\u4e00-\u9fa5]/g, '_');
138
-
139
- // 3. 移除连续下划线
140
- key = key.replace(/_+/g, '_');
141
-
142
- // 4. 移除首尾下划线
143
- key = key.replace(/^_|_$/g, '');
144
-
145
- // 5. 截断过长 key
146
- if (key.length > 30) {
147
- key = key.substring(0, 30);
148
- }
149
-
150
- return key || 'unknown';
151
- }
152
-
153
- // 示例
154
- console.log(generateKey('确认')); // confirm
155
- console.log(generateKey('确定要删除吗?')); // 确定要删除吗
156
- console.log(generateKey('Search Files')); // search_files
157
- ```
158
-
159
- ---
160
-
161
- ## 迁移报告模板
162
-
163
- ```markdown
164
- # 硬编码字符串迁移报告
165
-
166
- ## 扫描时间
167
- 2024-xx-xx
168
-
169
- ## 扫描范围
170
- - entry/src/main/ets/**/*.ets
171
-
172
- ## 发现数量
173
- - 总计:XX 个硬编码字符串
174
- - 已迁移:XX 个
175
- - 待处理:XX 个
176
-
177
- ## 按文件分布
178
- | 文件 | 数量 |
179
- |------|------|
180
- | page1.ets | 10 |
181
- | page2.ets | 5 |
182
- | dialog1.ets | 3 |
183
-
184
- ## 待迁移列表
185
-
186
- | # | 原文本 | 建议 key | 优先级 |
187
- |---|--------|---------|--------|
188
- | 1 | 确认 | confirm | P0 |
189
- | 2 | 取消 | cancel | P0 |
190
- | 3 | 保存成功 | toast_save_success | P1 |
191
-
192
- ## 已完成迁移
193
-
194
- | # | 原文本 | key | 文件 |
195
- |---|--------|-----|------|
196
- | 1 | 设置 | title_settings | SettingsPage.ets |
197
- | 2 | 搜索 | hint_search | SearchBar.ets |
198
- ```
199
-
200
- ---
201
-
202
- ## 批量替换脚本
203
-
204
- ```javascript
205
- // migrate-to-resources.js
206
- const fs = require('fs');
207
- const path = require('path');
208
-
209
- // 迁移映射表(从扫描结果生成)
210
- const migrationMap = {
211
- '确认': 'app.string.confirm',
212
- '取消': 'app.string.cancel',
213
- '保存': 'app.string.save',
214
- '删除': 'app.string.delete',
215
- '设置': 'app.string.title_settings',
216
- '搜索': 'app.string.hint_search',
217
- };
218
-
219
- function migrateFile(filePath) {
220
- let content = fs.readFileSync(filePath, 'utf-8');
221
- let modified = false;
222
-
223
- for (const [oldStr, newRef] of Object.entries(migrationMap)) {
224
- // 替换 Text('xxx')
225
- const textPattern = new RegExp(`Text\\(['"]${escapeRegExp(oldStr)}['"]\\)`, 'g');
226
- if (textPattern.test(content)) {
227
- content = content.replace(textPattern, `Text(\$r('${newRef}'))`);
228
- modified = true;
229
- }
230
-
231
- // 替换 Button('xxx')
232
- const buttonPattern = new RegExp(`Button\\(['"]${escapeRegExp(oldStr)}['"]\\)`, 'g');
233
- if (buttonPattern.test(content)) {
234
- content = content.replace(buttonPattern, `Button(\$r('${newRef}'))`);
235
- modified = true;
236
- }
237
- }
238
-
239
- if (modified) {
240
- fs.writeFileSync(filePath, content);
241
- console.log(`Migrated: ${filePath}`);
242
- }
243
- }
244
-
245
- function escapeRegExp(string) {
246
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
247
- }
248
-
249
- function migrateDirectory(dir) {
250
- const files = fs.readdirSync(dir, { withFileTypes: true });
251
-
252
- for (const file of files) {
253
- const fullPath = path.join(dir, file.name);
254
- if (file.isDirectory() && !file.name.startsWith('.')) {
255
- migrateDirectory(fullPath);
256
- } else if (file.name.endsWith('.ets')) {
257
- migrateFile(fullPath);
258
- }
259
- }
260
- }
261
-
262
- migrateDirectory('./entry/src/main/ets');
263
- console.log('Migration complete!');
264
- ```
265
-
266
- ---
267
-
268
- ## string.json 更新脚本
269
-
270
- ```javascript
271
- // update-string-json.js
272
- const fs = require('fs');
273
-
274
- // 新增的 key-value 对
275
- const newEntries = [
276
- { name: 'confirm', value: 'Confirm' },
277
- { name: 'cancel', value: 'Cancel' },
278
- { name: 'save', value: 'Save' },
279
- { name: 'delete', value: 'Delete' },
280
- ];
281
-
282
- function updateStringJson(filePath) {
283
- const content = fs.readFileSync(filePath, 'utf-8');
284
- const json = JSON.parse(content);
285
-
286
- // 检查是否已存在
287
- const existingNames = json.string.map(s => s.name);
288
- const toAdd = newEntries.filter(e => !existingNames.includes(e.name));
289
-
290
- if (toAdd.length > 0) {
291
- json.string.push(...toAdd);
292
- fs.writeFileSync(filePath, JSON.stringify(json, null, 2));
293
- console.log(`Updated: ${filePath} (+${toAdd.length} entries)`);
294
- }
295
- }
296
-
297
- // 更新 base 和 en_US
298
- updateStringJson('./entry/src/main/resources/base/element/string.json');
299
- updateStringJson('./entry/src/main/resources/en_US/element/string.json');
300
-
301
- console.log('Done!');
302
- ```
303
-
304
- ---
305
-
306
- ## 使用流程
307
-
308
- ### 完整迁移流程
309
-
310
- ```
311
- 1. 扫描阶段
312
- ├── 运行 scan-hardcoded-strings.js
313
- ├── 生成迁移报告
314
- └── 人工审核
315
-
316
- 2. 准备阶段
317
- ├── 创建 migrationMap
318
- └── 验证 key 不冲突
319
-
320
- 3. 迁移阶段
321
- ├── 运行 update-string-json.js(更新资源文件)
322
- ├── 运行 migrate-to-resources.js(替换代码)
323
- └── 人工检查修改
324
-
325
- 4. 验证阶段
326
- ├── 编译项目
327
- ├── 运行应用检查
328
- └── 如有问题,回滚修改
329
- ```
330
-
331
- ### 推荐工具链
332
-
333
- | 阶段 | 工具 | 说明 |
334
- |------|------|------|
335
- | 扫描 | rg / grep | 快速定位 |
336
- | 分析 | Node.js 脚本 | 生成报告 |
337
- | 迁移 | VS Code 多文件替换 | 人工确认 |
338
- | 验证 | DevEco Studio 编译 | 检查错误 |
339
-
340
- ---
341
-
342
- ## 注意事项
343
-
344
- 1. **先备份**:迁移前先 commit 或备份
345
- 2. **小步快跑**:分批次迁移,每次只处理 10-20 个
346
- 3. **编译验证**:每批次迁移后立即编译检查
347
- 4. **人工审核**:自动化替换后需要人工复核
348
- 5. **key 冲突**:避免重复 key,如已存在则复用
1
+ # 硬编码字符串扫描与迁移工具
2
+
3
+ > 辅助扫描项目中硬编码字符串并迁移到 resources 的工具和脚本
4
+
5
+ ---
6
+
7
+ ## 扫描工具汇总
8
+
9
+ ### 1. rg(ripgrep)快速扫描
10
+
11
+ ```bash
12
+ # 安装:Windows 使用 winget install Andersonby.rg 或 choco install ripgrep
13
+
14
+ # 扫描 Text() 硬编码
15
+ rg "Text\(['\"]" --type ets -n -o
16
+
17
+ # 扫描 Button() 硬编码
18
+ rg "Button\(['\"]" --type ets -n -o
19
+
20
+ # 扫描 showToast() 硬编码
21
+ rg "showToast\(['\"]" --type ets -n -o
22
+
23
+ # 扫描 AlertDialog 硬编码
24
+ rg "AlertDialog" --type ets -n -o
25
+
26
+ # 扫描 promptAction 硬编码
27
+ rg "promptAction\." --type ets -n
28
+ ```
29
+
30
+ ### 2. PowerShell 扫描脚本
31
+
32
+ ```powershell
33
+ # 扫描所有 .ets 文件中的硬编码字符串
34
+ Get-ChildItem -Recurse -Filter "*.ets" | ForEach-Object {
35
+ $content = Get-Content $_.FullName -Raw
36
+ # 匹配单引号字符串
37
+ if ($content -match "Text\(['\']([^'\)]+)['\']") {
38
+ Write-Host "$($_.FullName): $($matches[1])"
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### 3. Node.js 扫描脚本
44
+
45
+ ```javascript
46
+ // scan-hardcoded-strings.js
47
+ const fs = require('fs');
48
+ const path = require('path');
49
+
50
+ const patterns = [
51
+ /Text\(['"]([^'"]+)['"]\)/g,
52
+ /Button\(['"]([^'"]+)['"]\)/g,
53
+ /\.title\(['"]([^'"]+)['"]\)/g,
54
+ /\.placeholder\(['"]([^'"]+)['"]\)/g,
55
+ /\.promptText\(['"]([^'"]+)['"]\)/g,
56
+ /showToast\(['"]([^'"]+)['"]\)/g,
57
+ ];
58
+
59
+ function scanFile(filePath) {
60
+ const content = fs.readFileSync(filePath, 'utf-8');
61
+ const results = [];
62
+
63
+ for (const pattern of patterns) {
64
+ let match;
65
+ while ((match = pattern.exec(content)) !== null) {
66
+ const str = match[1];
67
+ // 过滤
68
+ if (str.length > 1 && /[\u4e00-\u9fa5a-zA-Z]/.test(str)) {
69
+ results.push({
70
+ file: filePath,
71
+ line: content.substring(0, match.index).split('\n').length,
72
+ pattern: match[0],
73
+ value: str
74
+ });
75
+ }
76
+ }
77
+ }
78
+ return results;
79
+ }
80
+
81
+ function scanDirectory(dir) {
82
+ const results = [];
83
+ const files = fs.readdirSync(dir, { withFileTypes: true });
84
+
85
+ for (const file of files) {
86
+ const fullPath = path.join(dir, file.name);
87
+ if (file.isDirectory() && !file.name.startsWith('.')) {
88
+ results.push(...scanDirectory(fullPath));
89
+ } else if (file.name.endsWith('.ets')) {
90
+ results.push(...scanFile(fullPath));
91
+ }
92
+ }
93
+ return results;
94
+ }
95
+
96
+ const results = scanDirectory('./entry/src/main/ets');
97
+ console.log(JSON.stringify(results, null, 2));
98
+ ```
99
+
100
+ ---
101
+
102
+ ## 过滤规则
103
+
104
+ ### 必须排除的模式
105
+
106
+ ```javascript
107
+ // 排除 $r() 引用
108
+ const filtered = results.filter(r => !r.value.includes('$r'));
109
+
110
+ // 排除纯数字
111
+ const filtered = results.filter(r => !/^\d+$/.test(r.value));
112
+
113
+ // 排除单字符
114
+ const filtered = results.filter(r => r.value.length > 1);
115
+
116
+ // 排除 URL
117
+ const filtered = results.filter(r => !/^https?:\/\//.test(r.value));
118
+
119
+ // 排除技术术语
120
+ const filtered = results.filter(r => !/^<[^>]+>$/.test(r.value)); // HTML 标签
121
+ const filtered = results.filter(r => !/^\{[^}]+\}$/.test(r.value)); // 变量引用
122
+
123
+ // 排除正则表达式
124
+ const filtered = results.filter(r => !/^[\[\]\{\}\(\)\.\?\+\*\^\\]$/.test(r.value));
125
+ ```
126
+
127
+ ---
128
+
129
+ ## key 生成算法
130
+
131
+ ```javascript
132
+ function generateKey(str) {
133
+ // 1. 转小写
134
+ let key = str.toLowerCase();
135
+
136
+ // 2. 移除特殊字符
137
+ key = key.replace(/[^a-z0-9\u4e00-\u9fa5]/g, '_');
138
+
139
+ // 3. 移除连续下划线
140
+ key = key.replace(/_+/g, '_');
141
+
142
+ // 4. 移除首尾下划线
143
+ key = key.replace(/^_|_$/g, '');
144
+
145
+ // 5. 截断过长 key
146
+ if (key.length > 30) {
147
+ key = key.substring(0, 30);
148
+ }
149
+
150
+ return key || 'unknown';
151
+ }
152
+
153
+ // 示例
154
+ console.log(generateKey('确认')); // confirm
155
+ console.log(generateKey('确定要删除吗?')); // 确定要删除吗
156
+ console.log(generateKey('Search Files')); // search_files
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 迁移报告模板
162
+
163
+ ```markdown
164
+ # 硬编码字符串迁移报告
165
+
166
+ ## 扫描时间
167
+ 2024-xx-xx
168
+
169
+ ## 扫描范围
170
+ - entry/src/main/ets/**/*.ets
171
+
172
+ ## 发现数量
173
+ - 总计:XX 个硬编码字符串
174
+ - 已迁移:XX 个
175
+ - 待处理:XX 个
176
+
177
+ ## 按文件分布
178
+ | 文件 | 数量 |
179
+ |------|------|
180
+ | page1.ets | 10 |
181
+ | page2.ets | 5 |
182
+ | dialog1.ets | 3 |
183
+
184
+ ## 待迁移列表
185
+
186
+ | # | 原文本 | 建议 key | 优先级 |
187
+ |---|--------|---------|--------|
188
+ | 1 | 确认 | confirm | P0 |
189
+ | 2 | 取消 | cancel | P0 |
190
+ | 3 | 保存成功 | toast_save_success | P1 |
191
+
192
+ ## 已完成迁移
193
+
194
+ | # | 原文本 | key | 文件 |
195
+ |---|--------|-----|------|
196
+ | 1 | 设置 | title_settings | SettingsPage.ets |
197
+ | 2 | 搜索 | hint_search | SearchBar.ets |
198
+ ```
199
+
200
+ ---
201
+
202
+ ## 批量替换脚本
203
+
204
+ ```javascript
205
+ // migrate-to-resources.js
206
+ const fs = require('fs');
207
+ const path = require('path');
208
+
209
+ // 迁移映射表(从扫描结果生成)
210
+ const migrationMap = {
211
+ '确认': 'app.string.confirm',
212
+ '取消': 'app.string.cancel',
213
+ '保存': 'app.string.save',
214
+ '删除': 'app.string.delete',
215
+ '设置': 'app.string.title_settings',
216
+ '搜索': 'app.string.hint_search',
217
+ };
218
+
219
+ function migrateFile(filePath) {
220
+ let content = fs.readFileSync(filePath, 'utf-8');
221
+ let modified = false;
222
+
223
+ for (const [oldStr, newRef] of Object.entries(migrationMap)) {
224
+ // 替换 Text('xxx')
225
+ const textPattern = new RegExp(`Text\\(['"]${escapeRegExp(oldStr)}['"]\\)`, 'g');
226
+ if (textPattern.test(content)) {
227
+ content = content.replace(textPattern, `Text(\$r('${newRef}'))`);
228
+ modified = true;
229
+ }
230
+
231
+ // 替换 Button('xxx')
232
+ const buttonPattern = new RegExp(`Button\\(['"]${escapeRegExp(oldStr)}['"]\\)`, 'g');
233
+ if (buttonPattern.test(content)) {
234
+ content = content.replace(buttonPattern, `Button(\$r('${newRef}'))`);
235
+ modified = true;
236
+ }
237
+ }
238
+
239
+ if (modified) {
240
+ fs.writeFileSync(filePath, content);
241
+ console.log(`Migrated: ${filePath}`);
242
+ }
243
+ }
244
+
245
+ function escapeRegExp(string) {
246
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
247
+ }
248
+
249
+ function migrateDirectory(dir) {
250
+ const files = fs.readdirSync(dir, { withFileTypes: true });
251
+
252
+ for (const file of files) {
253
+ const fullPath = path.join(dir, file.name);
254
+ if (file.isDirectory() && !file.name.startsWith('.')) {
255
+ migrateDirectory(fullPath);
256
+ } else if (file.name.endsWith('.ets')) {
257
+ migrateFile(fullPath);
258
+ }
259
+ }
260
+ }
261
+
262
+ migrateDirectory('./entry/src/main/ets');
263
+ console.log('Migration complete!');
264
+ ```
265
+
266
+ ---
267
+
268
+ ## string.json 更新脚本
269
+
270
+ ```javascript
271
+ // update-string-json.js
272
+ const fs = require('fs');
273
+
274
+ // 新增的 key-value 对
275
+ const newEntries = [
276
+ { name: 'confirm', value: 'Confirm' },
277
+ { name: 'cancel', value: 'Cancel' },
278
+ { name: 'save', value: 'Save' },
279
+ { name: 'delete', value: 'Delete' },
280
+ ];
281
+
282
+ function updateStringJson(filePath) {
283
+ const content = fs.readFileSync(filePath, 'utf-8');
284
+ const json = JSON.parse(content);
285
+
286
+ // 检查是否已存在
287
+ const existingNames = json.string.map(s => s.name);
288
+ const toAdd = newEntries.filter(e => !existingNames.includes(e.name));
289
+
290
+ if (toAdd.length > 0) {
291
+ json.string.push(...toAdd);
292
+ fs.writeFileSync(filePath, JSON.stringify(json, null, 2));
293
+ console.log(`Updated: ${filePath} (+${toAdd.length} entries)`);
294
+ }
295
+ }
296
+
297
+ // 更新 base 和 en_US
298
+ updateStringJson('./entry/src/main/resources/base/element/string.json');
299
+ updateStringJson('./entry/src/main/resources/en_US/element/string.json');
300
+
301
+ console.log('Done!');
302
+ ```
303
+
304
+ ---
305
+
306
+ ## 使用流程
307
+
308
+ ### 完整迁移流程
309
+
310
+ ```
311
+ 1. 扫描阶段
312
+ ├── 运行 scan-hardcoded-strings.js
313
+ ├── 生成迁移报告
314
+ └── 人工审核
315
+
316
+ 2. 准备阶段
317
+ ├── 创建 migrationMap
318
+ └── 验证 key 不冲突
319
+
320
+ 3. 迁移阶段
321
+ ├── 运行 update-string-json.js(更新资源文件)
322
+ ├── 运行 migrate-to-resources.js(替换代码)
323
+ └── 人工检查修改
324
+
325
+ 4. 验证阶段
326
+ ├── 编译项目
327
+ ├── 运行应用检查
328
+ └── 如有问题,回滚修改
329
+ ```
330
+
331
+ ### 推荐工具链
332
+
333
+ | 阶段 | 工具 | 说明 |
334
+ |------|------|------|
335
+ | 扫描 | rg / grep | 快速定位 |
336
+ | 分析 | Node.js 脚本 | 生成报告 |
337
+ | 迁移 | VS Code 多文件替换 | 人工确认 |
338
+ | 验证 | DevEco Studio 编译 | 检查错误 |
339
+
340
+ ---
341
+
342
+ ## 注意事项
343
+
344
+ 1. **先备份**:迁移前先 commit 或备份
345
+ 2. **小步快跑**:分批次迁移,每次只处理 10-20 个
346
+ 3. **编译验证**:每批次迁移后立即编译检查
347
+ 4. **人工审核**:自动化替换后需要人工复核
348
+ 5. **key 冲突**:避免重复 key,如已存在则复用