@harmonyos-arkts/d2h 0.0.0-stage → 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.
Files changed (74) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +7 -0
  3. package/README.md +168 -2
  4. package/agents/android-to-hmos-00-orchestrator.md +312 -0
  5. package/agents/d2h.md +168 -0
  6. package/bin/install-opencode.mjs +51 -0
  7. package/install-opencode.sh +160 -0
  8. package/opencode/agents.json +16 -0
  9. package/package.json +16 -4
  10. package/schemas/android-source-manifest.schema.json +25 -0
  11. package/schemas/checkpoint-provenance.schema.json +67 -0
  12. package/schemas/final-acceptance.schema.json +29 -0
  13. package/schemas/managed-evidence-index.schema.json +15 -0
  14. package/schemas/managed-evidence.schema.json +114 -0
  15. package/schemas/migration-config.schema.json +51 -0
  16. package/schemas/migration-report-index.schema.json +39 -0
  17. package/schemas/migration-report-item.schema.json +51 -0
  18. package/schemas/migration-report-summary.schema.json +25 -0
  19. package/schemas/migration-status.schema.json +202 -0
  20. package/schemas/preflight.schema.json +60 -0
  21. package/schemas/source-order-audit.schema.json +16 -0
  22. package/schemas/source-provenance-event.schema.json +19 -0
  23. package/schemas/spec-app-shard.schema.json +45 -0
  24. package/schemas/spec-fact-corrections-shard.schema.json +13 -0
  25. package/schemas/spec-features-shard.schema.json +43 -0
  26. package/schemas/spec-index.schema.json +26 -0
  27. package/schemas/spec-interactions-shard.schema.json +40 -0
  28. package/schemas/spec-page.schema.json +98 -0
  29. package/schemas/spec-pages-index.schema.json +1 -0
  30. package/schemas/spec-unresolved-shard.schema.json +1 -0
  31. package/schemas/task-envelope.schema.json +55 -0
  32. package/schemas/task-plan.schema.json +123 -0
  33. package/skills/android2hmos_resources_convert/SKILL.md +162 -0
  34. package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -0
  35. package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -0
  36. package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -0
  37. package/skills/appgraph-rule-audit/SKILL.md +58 -0
  38. package/skills/appgraph-rule-audit/references/audit-contract.md +47 -0
  39. package/skills/appgraph-rule-audit/references/recommendation-schema.md +41 -0
  40. package/skills/appgraph-rule-audit/schemas/rule-opportunities.schema.json +125 -0
  41. package/skills/arkts-app-identity/SKILL.md +238 -0
  42. package/skills/arkts-i18n/SKILL.md +496 -0
  43. package/skills/arkts-i18n/evals/evals.json +84 -0
  44. package/skills/arkts-i18n/references/code-examples.md +302 -0
  45. package/skills/arkts-i18n/references/common-pitfalls.md +391 -0
  46. package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -0
  47. package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -0
  48. package/skills/arkts-i18n/references/language-codes.md +104 -0
  49. package/skills/arkts-i18n/references/resource-file-structure.md +775 -0
  50. package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -0
  51. package/skills/arkts-i18n/references/v1-compat.md +244 -0
  52. package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -0
  53. package/skills/arkts-icon-sizing/SKILL.md +211 -0
  54. package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -0
  55. package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -0
  56. package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -0
  57. package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -0
  58. package/skills/arkts-mvvm-architecture/SKILL.md +613 -0
  59. package/skills/harmonyos-migration-playbook/SKILL.md +56 -0
  60. package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -0
  61. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -0
  62. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -0
  63. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -0
  64. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -0
  65. package/skills/harmonyos-migration-playbook/references/ui-automation.md +52 -0
  66. package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -0
  67. package/skills/maintaining-migration-report/SKILL.md +155 -0
  68. package/skills/native-library-substitution/SKILL.md +385 -0
  69. package/skills/native-library-substitution/references/native-library-substitution.json +56906 -0
  70. package/skills/native-library-substitution/references/native-library-substitution.md +163 -0
  71. package/skills/preparing-migration-workspace/SKILL.md +124 -0
  72. package/skills/preparing-migration-workspace/toolchain.json +43 -0
  73. package/skills/reviewing-migration-process/SKILL.md +62 -0
  74. package/src/install-opencode.mjs +108 -0
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: arkts-icon-sizing
3
+ description: >-
4
+ 检测并修复 Android→HarmonyOS UI 迁移中,因丢失 `.width()/.height()` 而导致渲染尺寸错误或比例
5
+ 失真的 ArkUI/HarmonyOS `Image` 组件。转换器保留了 `.objectFit()` 却丢掉了尺寸,使 `resources/base/media`
6
+ 里的位图图标按**原始像素尺寸**渲染(通常大 3 倍,非方形的还会被拉伸)。当用户反馈图标/图片在鸿蒙页面
7
+ (主页、我的、弹窗、列表项——任何位置)上「太大 / 太小 / 被拉伸 / 被压扁 / 比例异常 / 图标变形 / 显示太大 /
8
+ icons look off」,或要求「统一检查/修复各页图标比例」,或刚做完 a2h / ArkUI UI 迁移(大量 Android ImageView
9
+ 用了 wrap_content)时触发。内置「审计→测量→修复」三步流水线:用源位图的像素尺寸 ÷ 密度还原每个图标的正确
10
+ vp 尺寸,同时保留真实宽高比。即使用户只说“图标比例不对”而未说明原因也应使用。适用于 HarmonyOS 工程、Android→HarmonyOS 迁移和 CI。
11
+ ---
12
+
13
+ # ArkUI 图标尺寸修复(Android→HarmonyOS 迁移修复)
14
+
15
+ ## 输入与统一结果
16
+
17
+ 输入包括 HarmonyOS 工程、当前请求允许修改的范围,以及可用的 Android `res/` 根。缺少 Android raster 来源时允许使用显式密度兜底,但必须在结果中标记假设。始终执行同一套审计、测量、修复和复审流程,不识别或修改外部编排状态。
18
+
19
+ 返回统一领域结果:
20
+
21
+ ```json
22
+ {
23
+ "status": "completed | partial | failed",
24
+ "changedFiles": [],
25
+ "sourceFiles": [],
26
+ "evidence": [],
27
+ "domainChecks": [{"name": "", "status": "passed | failed", "evidence": []}],
28
+ "unresolved": [{"kind": "", "blocking": true, "location": "", "reason": ""}],
29
+ "verificationRequests": [{"type": "build | test | device | review", "scope": [], "reason": ""}],
30
+ "suggestedRemediation": [],
31
+ "details": {"auditReport": "", "fixedCount": 0}
32
+ }
33
+ ```
34
+
35
+ `status` 只表示图标审计和修复的领域状态。动态源、无 raster 和密度兜底项写入 `unresolved`;构建与设备视觉核对写入 `verificationRequests`,不在本 Skill 内选择仓库级命令。
36
+
37
+ ## 问题与成因
38
+
39
+ 在 Android 中,`android:layout_width/height="wrap_content"` 的 `ImageView` 会按 drawable 的**标称 dp**
40
+ 尺寸渲染。一张只放在 `*-xxhdpi`(3 倍密度)目录的位图,磁盘上可能是 `90×90 px`,显示为 `30×30 dp`。
41
+
42
+ Android→HarmonyOS UI 转换器常常复刻了 `ImageView` 的 `.objectFit(...)`,却**丢掉了 `.width()/.height()`**
43
+ (因为 `wrap_content` 没有固定数值可抄)。位图被原样拷进 HarmonyOS 的 `resources/base/media`——而**该目录无密度
44
+ 限定符**——于是 ArkUI 按 `Image` 的**字面像素尺寸(当成 vp)**渲染。那张 `90×90 px` 的图标现在显示为 `90×90 vp`,
45
+ 即**大了 3 倍**。更糟的是,非方形图标(`45×23` 的电池、`44×26` 的箭头)在没有显式尺寸时保持原始像素框,一旦
46
+ 某根轴被布局约束触碰,宽高比就明显失真——这就是用户看到的「比例异常 / 图标变形」。
47
+
48
+ 修复办法是把每个欠约束的 `Image` 还原成它的**标称 vp 尺寸**:
49
+
50
+ ```
51
+ 标称 vp = 源位图像素 ÷ 源密度倍数
52
+ ```
53
+
54
+ 宽和高**分别独立**相除,这正是还原正确宽高比的关键。已经带显式 `.width()/.height()` 的图标是正常的——**不要**
55
+ 碰它们,也**不要**用「把位图挪进带密度限定的目录」这种全局手段去修(那会把本来正确的图标缩小)。缺陷只针对
56
+ **那一部分丢了尺寸的 `Image`**。
57
+
58
+ ## 何时使用
59
+
60
+ - 用户反馈图标/图片在鸿蒙页面上过大、过小、被拉伸或被压扁——尤其刚做完 a2h / ArkUI UI 迁移之后。
61
+ - 用户要求「统一检查/修复图标比例」。
62
+ - 做迁移 QA,想横扫每个页面排查这一类缺陷。
63
+
64
+ 不属于本 skill:文本截断(`arkts-text-truncation`)、大字体布局溢出(`arkts-large-font`)、深色模式图标变色
65
+ (`arkts-dark-mode`)、生成全新 UI(`arkts-component-builder`)。那些是不同的问题。
66
+
67
+ ## 两种用法
68
+
69
+ 本 skill 有两条路径,共用同一套确定性逻辑:
70
+
71
+ | 路径 | 入口 | 是否有人工审核 | 适用 |
72
+ |---|---|---|---|
73
+ | **A. 全自动(受控入口)** | `icon_autofix.py` | 无 | 请求范围已明确、Android 资源来源可靠时;CI / 无人值守批量修复 |
74
+ | **B. 人工三步流** | `icon_audit.py` → `icon_dims.py` → `icon_fix.py` | 有(审 `icon_plan.json` 后再 `--apply`) | 人工 ad-hoc 排查,想逐步检查中间产物 |
75
+
76
+ 运行前先读取当前工作区状态并记录本次请求允许修改的文件。修复会就地改源文件,不得覆盖、暂存或回滚用户及其他工作的已有改动;需要恢复时只撤销本次脚本明确列出的修改。
77
+
78
+ ---
79
+
80
+ ## 用法 A:全自动(headless 单入口)
81
+
82
+ ```bash
83
+ # 最佳:让 Android 源逐图决定密度(多模块 → 传所有 res 根)
84
+ python <skill>/scripts/icon_autofix.py --apply \
85
+ --android-res <android>/app/src/main/res <android>/base/src/main/res \
86
+ --output-json icon_autofix.json
87
+
88
+ # 无 Android 源 → 用工程单一密度桶兜底(默认 3 = xxhdpi)
89
+ python <skill>/scripts/icon_autofix.py --apply --density 3 --output-json icon_autofix.json
90
+ ```
91
+
92
+ 一次跑完 `审计 → 测量 → 修复 --apply → 复审`,**无任何人工 gate**,只输出一个机读 JSON
93
+ (`passed / scanned / fixed_count / fixed[] / dynamic_unresolved[] / no_raster_unresolved[] / residual_static[]`)。
94
+ **从 HarmonyOS 工程根目录运行**(脚本自动发现 `*/src/main/ets` 与 `resources/base/media`)。
95
+
96
+ **为什么无人工审核也安全:** 尺寸是 `像素 ÷ 密度` 的确定性计算;只动「无尺寸 / 单尺寸无 aspectRatio」的
97
+ `Image`,绝不碰已带尺寸的;幂等(重复跑第二次 `fixed_count=0`);输出的修改清单可用于逐项审阅和定向恢复。**建议传 `--android-res`
98
+ 以获得逐图精确密度**,缺省回退 `--density 3`。动态源图标 / 无 raster 的项**只记录、不修改**(见用法 B 第 4 步)。
99
+
100
+ ---
101
+
102
+ ## 用法 B:人工三步流
103
+
104
+ 三个脚本按序运行,通过两个 JSON 文件(`icon_report.json`、`icon_plan.json`)通信,每一步落盘前都可检查。
105
+
106
+ ### 1. 审计 — 找出每一个欠约束的 Image
107
+
108
+ ```bash
109
+ python <skill>/scripts/icon_audit.py
110
+ ```
111
+
112
+ 自动发现 `*/src/main/ets` 根并写出 `icon_report.json`。它打印两份清单:**静态**欠约束图标
113
+ (`$r('app.media.xxx')`,可自动修)与**动态源**图片(`Image(this.foo())`、三元、`@Builder` 参数——需手工解析,
114
+ 见下)。`ImageFit.Fill` 用法单列供人工复核:`Fill` 会拉伸填满,通常是**可拉伸背景图形**(`shape_*_bg` 弹窗横幅)
115
+ 的有意为之,除非真有图标用了 `Fill`,否则放着别动。
116
+
117
+ 被标记 = 修饰链**无 width 且无 height**(`NO-DIMS`)或**只有其中一个且无 `aspectRatio`**
118
+ (`ONE-DIM-NO-ASPECT`)。`width('100%') + height('100%')` 视为有意的满铺,不标记。
119
+
120
+ ### 2. 测量 — 还原每个图标的正确 vp 尺寸
121
+
122
+ ```bash
123
+ # 最佳:让 Android 源逐图决定密度(多模块 → 传所有 res 根)
124
+ python <skill>/scripts/icon_dims.py --report icon_report.json \
125
+ --android-res <android>/app/src/main/res <android>/base/src/main/res
126
+
127
+ # 无 Android 源 → 用工程单一密度桶兜底(默认 3 = xxhdpi)
128
+ python <skill>/scripts/icon_dims.py --report icon_report.json --density 3
129
+ ```
130
+
131
+ 对每个被标记的静态图标,读取 HarmonyOS media 文件(webp/png/jpg/svg)的像素尺寸,除以密度倍数,写出
132
+ `icon_plan.json`(`{icon: {w, h, px, density, src}}`)。
133
+
134
+ **落地前务必审 `icon_plan.json`。** 核对 `src` 列是真实密度目录(如 `mipmap-xxhdpi`),且算出的 vp 尺寸对一个
135
+ 图标而言合理(小图标个/十位数,插图更大)。若大量图标显示 `assumed Nx`,说明没找到 Android 源——确认工程实际密度
136
+ 或补 `--android-res`。
137
+
138
+ 标为 **UNRESOLVED** 的图标没有 raster(属动态引用、纯 SVG `<shape>` 转换或矢量图)——手工处理(下一节)。
139
+
140
+ ### 3. 修复 — 插入尺寸(先 dry-run)
141
+
142
+ ```bash
143
+ python <skill>/scripts/icon_fix.py --plan icon_plan.json --report icon_report.json # 预览
144
+ python <skill>/scripts/icon_fix.py --plan icon_plan.json --report icon_report.json --apply # 落盘
145
+ ```
146
+
147
+ 逐处在 `Image(...)` 开头之后插入:
148
+ - **NO-DIMS** → `.width(W)` `.height(H)`(还原的标称 vp;固定尺寸、真实比例)。
149
+ - **ONE-DIM** → 仅 `.aspectRatio(W/H)`——保留有意的那一维(如满宽横幅的 `.width('100%')`),只修比例。
150
+
151
+ 它实时重扫,所以即使前面的插入移动了行号也安全。动态图片只报告,绝不自动改。
152
+
153
+ ### 4. 手工解析动态源图片
154
+
155
+ 审计/修复读不出 `Image(this.vipEditRes())` 或 `Image(cond ? $r('app.media.a') : $r('app.media.b'))` 的尺寸。
156
+ 对每一处:
157
+
158
+ 1. 读代码找出它能解析到的具体资源名(顺着方法/三元跟踪)。勾选类开关通常指向两张同尺寸资源。
159
+ 2. 查这些名字:`python <skill>/scripts/icon_dims.py --media <media_dir> --icons ic_a,ic_b`。
160
+ 3. 手工给 `Image(...)` 加 `.width(W).height(H)`(若已设一维则用 `.aspectRatio()`)。`@Builder tabItem(icon: Resource)`
161
+ 会让所有调用方共享一个尺寸——在 Builder 的 `Image` 上设一次即可。
162
+
163
+ ### 5. 校验
164
+
165
+ ```bash
166
+ python <skill>/scripts/icon_audit.py # 静态被标记数应为 0
167
+ ```
168
+
169
+ 把构建请求和设备端 UI 核对请求写入 `verificationRequests`。静态审计通过只说明确定性脚本未发现残留静态项,不能证明工程可编译或视觉已经一致。审计结果只应剩下已记录的动态项、无 raster 项和 `Fill` 背景。
170
+
171
+ ## 决策规则与边界
172
+
173
+ - **密度是逐图的,不是全局的。** 传了 `--android-res` 时 `icon_dims.py`/`icon_autofix.py` 读每个图标的真实源目录。
174
+ 多数 app 只发一个桶(xxhdpi=3),但别假设——通过 `src` 列核实。倍数表:ldpi .75,mdpi 1,hdpi 1.5,xhdpi 2,
175
+ xxhdpi 3,xxxhdpi 4。
176
+ - **方形 SVG 不会失真。** 方形 viewBox 的 `<shape>`/矢量图按标称 vp 渲染、比例正确——它不会「比例错」,只可能
177
+ 「尺寸错」。若这种 SVG 渲染得过大(如 `100×100` viewBox 显示成 100vp,而本意是小图标),查 **Android drawable 的
178
+ `android:width/height`** 取本意 dp 并显式设上。否则优先级低。
179
+ - **`shape_*_bg` 上的 `ImageFit.Fill`** 是可拉伸横幅背景——放着别动。只在真图标的盒子比例 ≠ 图标比例时才管 `Fill`。
180
+ - **别碰已带尺寸的图标。** 重点就是欠约束的那部分;全局改资源密度会破坏本来正确的图标。
181
+ - **多模块 Android 工程**把共享资源放在 `base`/`common` 模块——把每个 `*/src/main/res` 根都传给 `--android-res`,
182
+ 密度查找才能找到它们。
183
+
184
+ ## 脚本
185
+
186
+ | 脚本 | 职责 |
187
+ |---|---|
188
+ | `scripts/icon_audit.py` | 扫 `.ets` → `icon_report.json`(被标记静态 + 动态 + Fill)。导出 `discover_roots()` / `audit()`。 |
189
+ | `scripts/icon_dims.py` | `report` → `icon_plan.json`:标称 vp = media 像素 ÷ 源密度。导出 `build_plan()`;亦支持 `--icons` 临时查询。 |
190
+ | `scripts/icon_fix.py` | `report`+`plan` → 插入 `.width/.height`(NO-DIMS)或 `.aspectRatio`(ONE-DIM)。导出 `apply_fixes()`;默认 dry-run,`--apply` 落盘。 |
191
+ | `scripts/icon_autofix.py` | **全自动单入口**:审计→测量→修复→复审一次跑完,输出单个 JSON;由调用方或 CI 在输入边界明确时调用。 |
192
+
193
+ 四者均为纯 Python 标准库(无依赖),支持 `--help`,从工程根运行时自动发现 ets 根与 `resources/base/media`。
194
+
195
+ ## 结果组装
196
+
197
+ 1. 将脚本 JSON 和复审报告写入 `evidence`。
198
+ 2. 将实际修改文件写入 `changedFiles`,Android raster 来源写入 `sourceFiles`。
199
+ 3. 将静态复审写入 `domainChecks`。
200
+ 4. 将动态源、无 raster 和密度兜底项写入 `unresolved`。
201
+ 5. 将构建和相关页面的设备视觉核对写入 `verificationRequests`。
202
+
203
+ ## 可选领域协作
204
+
205
+ 下列 Skill 存在时可以协作,但不改变本 Skill 的输入、流程或统一结果:
206
+
207
+ - **android2hmos_resources_convert**:资源/资产侧转换(res/ → resources/、SVG 坐标/格式修复);本 skill 修的是 `.ets` 消费侧。
208
+ - **arkts-mvvm-architecture**:页面组件结构和 ArkTS/ArkUI 编码约束。
209
+ - **harmonyos-migration-playbook**:设备 UI 自动化、截图和页面对照方式。
210
+
211
+ 本 Skill 不暂存或提交文件、不更新迁移报告、不创建 checkpoint、不调用 `complete`。调用方自行消费统一结果。
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """第 1/3 步 —— 审计 ArkUI (.ets) 中欠约束尺寸的 Image() 组件。
4
+
5
+ 成因
6
+ Android UI 迁移到 HarmonyOS/ArkUI 时,转换器常保留 `.objectFit(...)` 却
7
+ 丢掉用了 `android:layout_width/height="wrap_content"` 的 ImageView 的
8
+ `.width()/.height()`。位图被拷进 `resources/base/media`(无密度限定符),
9
+ 于是 HarmonyOS 按其原始像素尺寸当作 vp 渲染 —— 图标被放大(按源密度倍数,
10
+ 如 xxhdpi = 3 倍),非方形图标还会比例失真。
11
+
12
+ 会标记什么(一个 Image 的修饰链满足)
13
+ - 既无 width 又无 height 且无 size() -> kind = NO-DIMS
14
+ - 恰好只有 width/height 之一,且无 aspectRatio -> kind = ONE-DIM-NO-ASPECT
15
+ (width('100%') + height('100%') = 满铺,视为有意为之。)
16
+ 同时列出 ImageFit.Fill 用法(可能拉伸)供人工复核。
17
+
18
+ 输出
19
+ 打印摘要并写出 JSON 报告(默认 icon_report.json),供 icon_dims.py /
20
+ icon_fix.py / icon_autofix.py 消费。
21
+
22
+ 用法
23
+ python icon_audit.py # 自动发现 */src/main/ets 根
24
+ python icon_audit.py --roots entry/src/main/ets/pages entry/.../components
25
+ python icon_audit.py --json icon_report.json
26
+ """
27
+ import os, re, json, argparse
28
+
29
+
30
+ def discover_roots():
31
+ """找出每一个 */src/main/ets 目录(entry + 任意 HSP/HAR 模块)。"""
32
+ roots, skip = [], {'node_modules', 'oh_modules', 'build', '.git', '.hvigor', '.idea'}
33
+ for base, dirs, _ in os.walk('.'):
34
+ dirs[:] = [d for d in dirs if d not in skip]
35
+ if base.replace('\\', '/').endswith('src/main/ets'):
36
+ roots.append(base)
37
+ return sorted(set(roots))
38
+
39
+
40
+ def chain_window(lines, i):
41
+ """第 i 行 Image 的修饰链 = 紧随其后、缩进更深的那些行。"""
42
+ base = len(lines[i]) - len(lines[i].lstrip())
43
+ out, j = [], i + 1
44
+ while j < len(lines):
45
+ ln = lines[j]
46
+ if ln.strip() == '':
47
+ j += 1
48
+ continue
49
+ if (len(ln) - len(ln.lstrip())) <= base:
50
+ break
51
+ out.append(ln)
52
+ j += 1
53
+ return '\n'.join(out)
54
+
55
+
56
+ def audit(roots):
57
+ flagged, fill_used, full_bleed, scanned = [], [], 0, 0
58
+ for root in roots:
59
+ for dp, _, files in os.walk(root):
60
+ for fn in files:
61
+ if not fn.endswith('.ets'):
62
+ continue
63
+ path = os.path.join(dp, fn).replace('\\', '/')
64
+ lines = open(path, encoding='utf-8').read().split('\n')
65
+ for i, ln in enumerate(lines):
66
+ st = ln.lstrip()
67
+ # 跳过注释行,避免被注释掉的 Image()/ImageView 引用误命中
68
+ if st.startswith('*') or st.startswith('//') or st.startswith('/*'):
69
+ continue
70
+ code = ln.split('//')[0]
71
+ if not re.search(r'\bImage\(', code):
72
+ continue
73
+ scanned += 1
74
+ win = chain_window(lines, i)
75
+ has_w, has_h = '.width(' in win, '.height(' in win
76
+ has_size = '.size(' in win
77
+ has_aspect = '.aspectRatio(' in win
78
+ has_weight = '.layoutWeight(' in win
79
+ w100, h100 = ".width('100%')" in win, ".height('100%')" in win
80
+ fitm = re.search(r'ImageFit\.(\w+)', win)
81
+ fit = fitm.group(1) if fitm else None
82
+ iconm = re.search(r"(?:app|sys)\.media\.([A-Za-z0-9_]+)", code)
83
+ icon = iconm.group(1) if iconm else None
84
+ if 'ImageFit.Fill' in win:
85
+ fill_used.append({'file': path, 'line': i + 1, 'icon': icon})
86
+ if w100 and h100:
87
+ full_bleed += 1
88
+ continue
89
+ ok = has_size or (has_w and has_h) or \
90
+ ((has_w ^ has_h) and (has_aspect or has_weight))
91
+ if ok:
92
+ continue
93
+ flagged.append({
94
+ 'file': path, 'line': i + 1, 'icon': icon,
95
+ 'dynamic': icon is None,
96
+ 'kind': 'NO-DIMS' if not (has_w or has_h) else 'ONE-DIM-NO-ASPECT',
97
+ 'fit': fit, 'expr': code.strip()[:60],
98
+ })
99
+ return {'scanned': scanned, 'flagged': flagged,
100
+ 'fill': fill_used, 'full_bleed_count': full_bleed}
101
+
102
+
103
+ def main():
104
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
105
+ ap.add_argument('--roots', nargs='*', help='要扫描的 ets 目录(默认:自动发现 */src/main/ets)')
106
+ ap.add_argument('--json', default='icon_report.json', help='报告输出路径')
107
+ a = ap.parse_args()
108
+ roots = a.roots or discover_roots()
109
+ if not roots:
110
+ print('未找到 ets 根。请从 HarmonyOS 工程根运行,或用 --roots 指定。')
111
+ return
112
+ print('扫描根目录:')
113
+ for r in roots:
114
+ print(' ' + r)
115
+ rep = audit(roots)
116
+ print(f'\n== 扫描 {rep["scanned"]} 个 Image() | 标记 {len(rep["flagged"])} '
117
+ f'| Fill {len(rep["fill"])} | 满铺 {rep["full_bleed_count"]} ==\n')
118
+ stat = [f for f in rep['flagged'] if not f['dynamic']]
119
+ dyn = [f for f in rep['flagged'] if f['dynamic']]
120
+ print(f'-- 欠约束的静态图标({len(stat)})--')
121
+ for f in sorted(stat, key=lambda x: (x['file'], x['line'])):
122
+ print(f" [{f['kind']:18}] fit={f['fit'] or '-':8} {os.path.basename(f['file'])}:{f['line']} {f['icon']}")
123
+ print(f'\n-- 动态源图片({len(dyn)})-> 手工解析资源(见 SKILL.md)--')
124
+ for f in sorted(dyn, key=lambda x: (x['file'], x['line'])):
125
+ print(f" {os.path.basename(f['file'])}:{f['line']} {f['expr']}")
126
+ json.dump(rep, open(a.json, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
127
+ print(f'\n已写出 {a.json} -> 下一步:python icon_dims.py --report {a.json}')
128
+
129
+
130
+ if __name__ == '__main__':
131
+ main()
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """全自动单入口 —— 一次跑完 审计→测量→修复→复审,输出单个机读 JSON,无任何人工 gate。
4
+
5
+ 由调用方或 CI 在输入边界明确时调用;也可直接使用 --apply。
6
+
7
+ 为什么无人工审核也安全:
8
+ 尺寸 = 像素 ÷ 密度 是确定性计算;只动「无尺寸 / 单尺寸无 aspectRatio」的 Image,
9
+ 绝不碰已带尺寸的;幂等(重复跑第二次 fixed_count=0);调用者负责保护已有工作区改动。
10
+ 动态源图片 / 无 raster 项只记录、不修改,由调用方判断其是否阻塞当前请求。
11
+
12
+ 用法
13
+ python icon_autofix.py --apply --output-json icon_autofix.json
14
+ python icon_autofix.py --apply --android-res <a>/app/src/main/res <a>/base/src/main/res \
15
+ --output-json icon_autofix.json
16
+ python icon_autofix.py --apply --density 3 --output-json icon_autofix.json # 无 Android 源
17
+ """
18
+ import os, json, argparse
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # 允许从本脚本目录直接 import 同级模块
23
+ sys.path.insert(0, str(Path(__file__).parent))
24
+ from icon_audit import discover_roots, audit
25
+ from icon_dims import find_media_dir, build_plan
26
+ from icon_fix import apply_fixes
27
+
28
+
29
+ def autofix(android_res, density, apply=True):
30
+ """审计→测量→修复→复审,返回结构化结果 dict。"""
31
+ roots = discover_roots()
32
+ if not roots:
33
+ return {'passed': True, 'skipped': True, 'reason': '未找到 */src/main/ets 根'}
34
+ report = audit(roots)
35
+ media = find_media_dir()
36
+ names = sorted({f['icon'] for f in report['flagged'] if f.get('icon')})
37
+ if media:
38
+ plan, no_raster = build_plan(names, media, android_res, density)
39
+ else:
40
+ plan, no_raster = {}, names
41
+ applied, dynamic = apply_fixes(plan, report, apply=apply)
42
+ # 复审:验证收敛。有 raster 却仍被标记的静态图标 = 异常
43
+ report2 = audit(roots)
44
+ residual = [f for f in report2['flagged']
45
+ if not f['dynamic'] and f.get('icon') in plan]
46
+ return {
47
+ 'passed': True, # 自愈永不阻断;执行异常时由外层 main 置 False
48
+ 'media': media,
49
+ 'scanned': report['scanned'],
50
+ 'flagged_before': len(report['flagged']),
51
+ 'flagged_after': len(report2['flagged']),
52
+ 'fixed_count': len(applied),
53
+ 'fixed': applied,
54
+ 'dynamic_unresolved': dynamic, # Image(this.foo()) / 三元等,记录不修
55
+ 'no_raster_unresolved': no_raster, # 纯 svg shape / 矢量图,记录不修
56
+ 'fill_for_review': report.get('fill', []),
57
+ 'residual_static': [{'file': f['file'], 'line': f['line'], 'icon': f['icon']} for f in residual],
58
+ 'density_source': ('android-res' if android_res else f'fallback {density}x'),
59
+ }
60
+
61
+
62
+ def main():
63
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
64
+ ap.add_argument('--project-root', default='.', help='HarmonyOS 工程根(脚本会 chdir 到此)')
65
+ ap.add_argument('--android-res', nargs='*', default=[], help='Android res 根,用于逐图密度(缺省回退 --density)')
66
+ ap.add_argument('--density', type=float, default=3.0, help='回退密度倍数(默认 3 = xxhdpi)')
67
+ ap.add_argument('--apply', action='store_true', help='落盘(默认 dry-run,仅计算)')
68
+ ap.add_argument('--output-json', required=True, help='结果 JSON 输出路径')
69
+ a = ap.parse_args()
70
+
71
+ out_path = Path(a.output_json).resolve() # chdir 前先解析为绝对路径
72
+ android_res = [str(Path(r).resolve()) for r in a.android_res] or None
73
+ if a.project_root and a.project_root != '.':
74
+ os.chdir(a.project_root)
75
+
76
+ try:
77
+ out = autofix(android_res, a.density, apply=a.apply)
78
+ except Exception as e:
79
+ out = {'passed': False, 'error': str(e)}
80
+
81
+ out_path.parent.mkdir(parents=True, exist_ok=True)
82
+ out_path.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding='utf-8')
83
+ brief = {k: out.get(k) for k in ('passed', 'fixed_count', 'dynamic_unresolved', 'density_source', 'skipped', 'error')}
84
+ print(json.dumps({k: v for k, v in brief.items() if v is not None}, ensure_ascii=False))
85
+
86
+
87
+ if __name__ == '__main__':
88
+ main()
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """第 2/3 步 —— 还原每个被标记图标的正确 vp 尺寸。
4
+
5
+ 公式
6
+ HarmonyOS 中 `resources/base/media`(无密度限定符)里的位图按其字面像素尺寸
7
+ 当作 vp 渲染。源位图来自某个 Android 密度桶(如 drawable-xxhdpi /
8
+ mipmap-xxhdpi = 3 倍)。因此图标被设计显示的尺寸是:
9
+ 标称 vp = 源像素 / 源密度倍数
10
+ 我们从 HarmonyOS media 文件读像素尺寸,从 Android 源目录名取倍数。宽和高
11
+ 分别独立相除,所以真实宽高比得以保留(这正是修复「失真 / 比例错」的关键)。
12
+
13
+ 倍数从何而来
14
+ 1. 给了 --android-res 时,由图标的源目录(如 mipmap-xxhdpi)决定。Android
15
+ 工程可能多模块 —— 把每个 res 根都传进来(如 app/src/main/res 与
16
+ base/src/main/res)。
17
+ 2. 否则回退到 --density(默认 3.0 = xxhdpi,现代 app 最常见的单一桶)。输出
18
+ 会标出 assumed 项,便于人工核对。
19
+
20
+ 输出
21
+ 打印表格并写出 icon_plan.json {icon: {w,h,px,density,src}}。
22
+ 找不到 raster 的图标(动态引用、纯 SVG shape、矢量图)列为 UNRESOLVED 供手工处理。
23
+
24
+ 用法
25
+ python icon_dims.py --report icon_report.json \
26
+ --media entry/src/main/resources/base/media \
27
+ --android-res /path/app/src/main/res /path/base/src/main/res
28
+ python icon_dims.py --report icon_report.json --density 3 # 无 Android 源
29
+ python icon_dims.py --media .../media --icons ic_a,ic_b # 临时查询
30
+ """
31
+ import os, re, json, struct, glob, argparse
32
+
33
+ DENSITY = {'ldpi': 0.75, 'mdpi': 1.0, 'tvdpi': 1.33, 'hdpi': 1.5,
34
+ 'xhdpi': 2.0, 'xxhdpi': 3.0, 'xxxhdpi': 4.0}
35
+
36
+
37
+ def webp_size(p):
38
+ d = open(p, 'rb').read(40)
39
+ if d[:4] != b'RIFF' or d[8:12] != b'WEBP':
40
+ return None
41
+ f = d[12:16]
42
+ if f == b'VP8 ':
43
+ return struct.unpack('<H', d[26:28])[0] & 0x3fff, struct.unpack('<H', d[28:30])[0] & 0x3fff
44
+ if f == b'VP8L':
45
+ b = d[21:25]
46
+ v = b[0] | b[1] << 8 | b[2] << 16 | b[3] << 24
47
+ return (v & 0x3fff) + 1, ((v >> 14) & 0x3fff) + 1
48
+ if f == b'VP8X':
49
+ return (d[24] | d[25] << 8 | d[26] << 16) + 1, (d[27] | d[28] << 8 | d[29] << 16) + 1
50
+ return None
51
+
52
+
53
+ def png_size(p):
54
+ d = open(p, 'rb').read(24)
55
+ return struct.unpack('>II', d[16:24])
56
+
57
+
58
+ def svg_size(p):
59
+ h = open(p, encoding='utf-8', errors='ignore').read(400)
60
+ m = re.search(r'width="([0-9.]+)"[^>]*height="([0-9.]+)"', h)
61
+ if m:
62
+ return float(m.group(1)), float(m.group(2))
63
+ m = re.search(r'viewBox="[\d.]+ [\d.]+ ([\d.]+) ([\d.]+)"', h)
64
+ return (float(m.group(1)), float(m.group(2))) if m else None
65
+
66
+
67
+ def media_size(media_dir, name):
68
+ for ext, fn in (('.webp', webp_size), ('.png', png_size), ('.jpg', png_size), ('.svg', svg_size)):
69
+ p = os.path.join(media_dir, name + ext)
70
+ if os.path.exists(p):
71
+ try:
72
+ return fn(p), ext
73
+ except Exception:
74
+ return None, ext
75
+ return None, None
76
+
77
+
78
+ def src_multiplier(res_roots, name):
79
+ best = None
80
+ for root in res_roots:
81
+ for hit in glob.glob(f'{root}/*/{name}.*'):
82
+ folder = os.path.basename(os.path.dirname(hit))
83
+ for k, v in DENSITY.items():
84
+ if folder.endswith('-' + k) or folder.endswith(k):
85
+ if best is None or v > best[0]:
86
+ best = (v, folder)
87
+ return best
88
+
89
+
90
+ def find_media_dir():
91
+ # 排除非工程目录(skill 模板 / 依赖 / 构建产物 / 隐藏目录),避免 os.walk 误入
92
+ # .claude/skills/.../template/AppScope/resources/base/media 等 fixture。
93
+ skip = {'.git', '.hvigor', '.idea', '.claude', '.cxt',
94
+ 'oh_modules', 'node_modules', 'build', 'template'}
95
+ candidates = []
96
+ for base, dirs, _ in os.walk('.'):
97
+ dirs[:] = [d for d in dirs if d not in skip and not d.startswith('.')]
98
+ if base.replace('\\', '/').endswith('resources/base/media'):
99
+ candidates.append(base)
100
+ if not candidates:
101
+ return None
102
+ # 优先工程模块媒体(*/src/main/resources/base/media)而非 app 级 AppScope 或其它
103
+ for c in candidates:
104
+ if '/src/main/resources/' in c.replace('\\', '/'):
105
+ return c
106
+ return candidates[0]
107
+
108
+
109
+ def build_plan(names, media, android_res, density):
110
+ """对一组图标名,逐个 媒体像素 ÷ 源密度 算出标称 vp,返回 (plan, unresolved)。
111
+
112
+ 被 icon_dims.main() 与 icon_autofix.py 共用。
113
+ names : 图标名列表(不含扩展名)
114
+ media : HarmonyOS base/media 目录
115
+ android_res : Android res 根列表(空/None → 用 density 回退)
116
+ density : 回退密度倍数
117
+ 返回 plan = {icon: {w,h,px,density,src,ext}};unresolved = 找不到 raster 的图标名列表。
118
+ """
119
+ plan, unresolved = {}, []
120
+ for ic in names:
121
+ sz, ext = media_size(media, ic)
122
+ if sz is None:
123
+ unresolved.append(ic)
124
+ continue
125
+ mult = src_multiplier(android_res, ic) if android_res else None
126
+ m = mult[0] if mult else density
127
+ src = mult[1] if mult else f'assumed {density}x'
128
+ w, h = sz
129
+ vw, vh = max(1, round(w / m)), max(1, round(h / m))
130
+ plan[ic] = {'w': vw, 'h': vh, 'px': [round(w), round(h)], 'density': m, 'src': src, 'ext': ext}
131
+ return plan, unresolved
132
+
133
+
134
+ def main():
135
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
136
+ ap.add_argument('--report', default='icon_report.json')
137
+ ap.add_argument('--media', help='HarmonyOS base/media 目录(默认:自动发现)')
138
+ ap.add_argument('--android-res', nargs='*', default=[], help='用于密度查找的 Android res 根')
139
+ ap.add_argument('--density', type=float, default=3.0, help='回退密度倍数(默认 3 = xxhdpi)')
140
+ ap.add_argument('--icons', help='逗号分隔的图标名,临时查询(替代 --report)')
141
+ ap.add_argument('--json', default='icon_plan.json')
142
+ a = ap.parse_args()
143
+
144
+ media = a.media or find_media_dir()
145
+ if not media:
146
+ print('未找到 resources/base/media —— 请用 --media 指定。')
147
+ return
148
+
149
+ if a.icons:
150
+ names = [n.strip() for n in a.icons.split(',') if n.strip()]
151
+ else:
152
+ rep = json.load(open(a.report, encoding='utf-8'))
153
+ names = sorted({f['icon'] for f in rep['flagged'] if f.get('icon')})
154
+
155
+ print(f"media: {media}")
156
+ print(f"密度来源: {a.android_res or f'回退 {a.density}x'}\n")
157
+ print(f"{'图标':40} {'像素':12} {'密度':16} {'-> vp (w x h)'}")
158
+ print('-' * 86)
159
+ plan, unresolved = build_plan(names, media, a.android_res or None, a.density)
160
+ for ic in names:
161
+ if ic in plan:
162
+ p = plan[ic]
163
+ px = f"{p['px'][0]}x{p['px'][1]}"
164
+ print(f"{ic:40} {px:12} {p['src']:16} -> {p['w']} x {p['h']}")
165
+ else:
166
+ label = '(无 raster)'
167
+ blank = ''
168
+ print(f"{ic:40} {label:12} {blank:16} -> 手工处理(动态 / svg shape / 矢量图)")
169
+
170
+ json.dump(plan, open(a.json, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
171
+ print(f'\n已写出 {a.json}({len(plan)} 个图标)。')
172
+ if unresolved:
173
+ print(f'UNRESOLVED({len(unresolved)})—— 手工解析资源后,用 --icons <名字> 重跑来测尺寸:'
174
+ f'{", ".join(unresolved)}')
175
+ print(f'请审核 icon_plan.json,然后:python icon_fix.py --plan {a.json} --report {a.report}(加 --apply 落盘)')
176
+
177
+
178
+ if __name__ == '__main__':
179
+ main()