@harmonyos-arkts/d2h 0.1.20 → 0.1.22

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 (152) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -0
  3. package/agents/android-to-hmos-00-orchestrator.md +3 -315
  4. package/agents/d2h.md +53 -38
  5. package/contracts/workflow/artifacts.md +22 -0
  6. package/contracts/workflow/delivery.md +54 -0
  7. package/contracts/workflow/dispatch.md +26 -0
  8. package/contracts/workflow/execution.md +125 -0
  9. package/contracts/workflow/generation.md +40 -0
  10. package/contracts/workflow/orchestration.md +49 -0
  11. package/contracts/workflow/overview.md +58 -0
  12. package/contracts/workflow/plan.md +119 -0
  13. package/contracts/workflow/scope.md +151 -0
  14. package/dist/bin/install-opencode.js +34 -15
  15. package/dist/src/install-codex.js +131 -0
  16. package/dist/src/install-opencode.js +5 -5
  17. package/dist/src/workflow.js +54 -0
  18. package/docs/architecture-plan-guide.zh-CN.md +70 -0
  19. package/docs/incremental-spec-evaluation-design.md +670 -0
  20. package/docs/spec-plan-guide.md +226 -0
  21. package/docs/workflow-decoupling.md +59 -0
  22. package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
  23. package/opencode/interactive-tools/browser-host.ts +110 -0
  24. package/opencode/interactive-tools/core.ts +242 -0
  25. package/opencode/interactive-tools/demo-workflow.ts +55 -0
  26. package/opencode/interactive-tools/demo.mjs +37 -0
  27. package/opencode/interactive-tools/package-lock.json +1876 -0
  28. package/opencode/interactive-tools/package.json +23 -0
  29. package/opencode/interactive-tools/panel.ts +40 -0
  30. package/opencode/interactive-tools/prepare-demo.mjs +17 -0
  31. package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
  32. package/opencode/interactive-tools/progress.ts +118 -0
  33. package/opencode/interactive-tools/server-legacy.mjs +2 -0
  34. package/opencode/interactive-tools/server.mjs +40 -0
  35. package/opencode/interactive-tools/tui.tsx +230 -0
  36. package/package.json +9 -1
  37. package/schemas/native-libs-analysis-report.schema.json +695 -0
  38. package/schemas/native-libs-verify-report.schema.json +566 -0
  39. package/schemas/spec-native-libs-shard.schema.json +110 -0
  40. package/skills/arkts-icon-sizing/SKILL.md +0 -10
  41. package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
  42. package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
  43. package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
  44. package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
  45. package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
  46. package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
  47. package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
  48. package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
  49. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
  50. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
  51. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
  52. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
  53. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
  54. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
  55. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
  56. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
  57. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
  58. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
  59. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
  60. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
  61. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
  62. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
  63. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
  64. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
  65. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
  66. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
  67. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
  68. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
  69. package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
  70. package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
  71. package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
  72. package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
  73. package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
  74. package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
  75. package/skills/d2h-app-solution/SKILL.md +198 -0
  76. package/skills/d2h-app-solution/agents/openai.yaml +4 -0
  77. package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
  78. package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
  79. package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
  80. package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
  81. package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
  82. package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
  83. package/skills/d2h-architecture-plan/SKILL.md +35 -0
  84. package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
  85. package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
  86. package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
  87. package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
  88. package/skills/d2h-architecture-plan/references/source-review.md +50 -0
  89. package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
  90. package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
  91. package/skills/d2h-dashboard/SKILL.md +56 -0
  92. package/skills/d2h-product-family-migration/SKILL.md +103 -0
  93. package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
  94. package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
  95. package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
  96. package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
  97. package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
  98. package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
  99. package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
  100. package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
  101. package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
  102. package/skills/d2h-spec-develop/SKILL.md +41 -0
  103. package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
  104. package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
  105. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
  106. package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
  107. package/skills/d2h-spec-generate/references/from-plan.md +19 -0
  108. package/skills/d2h-spec-generate/references/spec-content.md +34 -0
  109. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
  110. package/skills/d2h-spec-plan/SKILL.md +38 -0
  111. package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
  112. package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
  113. package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
  114. package/skills/d2h-spec-plan/references/planning-method.md +46 -0
  115. package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
  116. package/skills/d2h-spec-plan/references/work-packets.md +61 -0
  117. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
  118. package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
  119. package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
  120. package/skills/d2h-utg-static-generate/SKILL.md +2 -2
  121. package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
  122. package/skills/managing-d2h-toolchain/toolchain.json +1 -1
  123. package/skills/preparing-migration-workspace/SKILL.md +32 -2
  124. package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
  125. package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
  126. package/workflow/artifact_bindings.py +224 -0
  127. package/workflow/assets/dashboard.html +88 -0
  128. package/workflow/build_batch_report.py +8 -0
  129. package/workflow/collect_code_metrics.py +75 -0
  130. package/workflow/dashboard_overview.py +109 -0
  131. package/workflow/delivery_view.py +82 -0
  132. package/workflow/feature_plan.py +234 -0
  133. package/workflow/page_batches.py +223 -0
  134. package/workflow/spec_generation.py +120 -0
  135. package/workflow/spec_runtime.py +890 -0
  136. package/workflow/work_packets.py +85 -0
  137. package/skills/d2h-page-develop/SKILL.md +0 -126
  138. package/skills/d2h-page-develop/agents/openai.yaml +0 -7
  139. package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
  140. package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
  141. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
  142. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
  143. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
  144. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
  145. package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
  146. package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
  147. package/skills/native-library-substitution/SKILL.md +0 -385
  148. package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
  149. package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
  150. package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
  151. package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
  152. package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: d2h-native-library-verify
3
+ description: Android 功能场景 → 鸿蒙 so/a 能力验证(按功能,从 so/a 导出符号检测能力,非 .d.ts 声明)。从 Android 源码提取功能场景(或消费 analyse 产出的 feature-manifest.json),用 llvm-nm 提取鸿蒙 so/a 的导出符号(.so 动态符号 / .a 全局符号),据此检测真实能力(avcodec/avformat/ff_libx264_encoder/ff_rtmp_protocol/FFAudioPlayer/OpenSSL/LAME 等)——有符号才判有能力,不编造;对每个功能场景判定可实现/部分实现/无法实现,NAPI 可基于真实符号自己写桥接,生成可编译运行的鸿蒙测试 demo(Hypium 断言用例);连真机时自动触发「真机验证门禁」(准备 demo → 编译 → 签名 → 安装 → 运行 → 采集证据),报告三态标注(✅通过 / ❌失败 / ⚠未真机验证)。当用户要求「验证鸿蒙 so 能不能满足 Android 功能」「无头文件/index 文件校验 so 能力」「JNI 转 NAPI」「生成能力验证报告/鸿蒙测试 demo」时触发。
4
+ ---
5
+
6
+ # Android 功能场景 → 鸿蒙 so/a 能力验证(按功能,从 so 导出符号检测能力)
7
+
8
+ ## 核心理念
9
+
10
+ 不按 Android 每个 JNI API 做 1:1 映射,也不依赖 `.d.ts` 声明文件(声明可能和实际 so 不符)。
11
+ 能力判定的唯一依据是 **so/a 的导出符号表**——这是"事实",不是声明、不是 AI 编造。
12
+
13
+ ```
14
+ Android 用 so/a 实现的功能(7 个场景)
15
+ → 用 llvm-nm 提取鸿蒙 so/a 导出符号(.so 动态符号 / .a 全局符号)
16
+ → 从符号检测真实能力(avcodec_/avformat_/ff_libx264_encoder/ff_rtmp_protocol/FFAudioPlayer/OpenSSL…)
17
+ → 能力 ⊆ 功能所需(requires)? → 可实现/部分实现/无法实现
18
+ ├─ 能实现 → NAPI 可基于真实符号自己写桥接 → 生成 Hypium demo
19
+ └─ 不能实现 → 真正无法实现(so 未导出对应符号)
20
+ → 连真机 → 自动触发「真机验证门禁」(编译+安装+运行+采集 14 个用例)
21
+ ```
22
+
23
+ ## 用法
24
+
25
+ ```bash
26
+ # 能力验证 + NAPI 方案 + demo 测试用例(--dts 仅作参考,可选)
27
+ python3 scripts/verify-ohos-native.py --android <工程> --ohos <so或.a> \
28
+ --manifest <feature-manifest.json>
29
+
30
+ # 连真机时自动触发真机验证门禁(无需 --device,自动 hdc list targets 检测)
31
+ python3 scripts/verify-ohos-native.py --android <工程> --ohos <so> \
32
+ --manifest <manifest.json> --media-source <带media图标的工程>
33
+
34
+ # 显式指定设备 + demo 工程
35
+ python3 scripts/verify-ohos-native.py ... --device <序列号> --demo <demo工程> --media-source <工程>
36
+ ```
37
+
38
+ `--ohos` 可传单个文件或目录;目录会收集 `.so` 和 `.a`。
39
+
40
+ ## 能力检测(核心)
41
+
42
+ 用 `llvm-nm`(DevEco SDK 自带,`find_elf_nm()` 自动定位)提取导出符号:
43
+
44
+ | 库类型 | 命令 | 提取什么 |
45
+ |---|---|---|
46
+ | `.so` | `llvm-nm -D --defined-only --extern-only` | 动态导出符号(.dynsym) |
47
+ | `.a` | `llvm-nm --defined-only` | 全局定义符号(类型大写 T/D/B/R;ar 归档无动态符号表) |
48
+
49
+ `CAPABILITY_RULES`(31 项)定义「能力名 → 符号名正则」。命中任一符号即证明 so/a 真实具备该能力:
50
+
51
+ ```
52
+ FFmpeg:avcodec_* avformat_* avfilter_* swscale_* swresample_* avio_*
53
+ 编码器:H.264(ff_libx264_encoder/x264_*) AAC(ff_aac_encoder/faacEnc) AV1(ff_libaom/libaom) MP3(lame_*)
54
+ 协议/封装:RTMP RTSP HTTP FLV MP4 MPEG-TS HLS
55
+ NAPI:FFAudioPlayer FFAudioWriter FFAbortSignal FFmpeg
56
+ OpenSSL:AES RSA X509 TLS/SSL EVP SHA BN CRYPTO ossl
57
+ 其他:AMR zlib libcurl
58
+ ```
59
+
60
+ 实测(FFmpegAndroid):`libffmpeg.so` 26753 符号→31 项能力;`libx264.a`→H.264;`libfaac.a`→AAC。
61
+
62
+ ## 判定模型
63
+
64
+ 每个功能场景在 `FEATURE_PLANS` 里声明 `requires`(所需能力),由 `judge_feature()` 用检测到的能力比对:
65
+
66
+ - **可实现**:requires 全部命中 so 导出符号
67
+ - **部分实现**:部分命中(如 VideoPlayer 滤镜命中、视频渲染未命中)
68
+ - **无法实现**:requires 全不命中(如「视频播放/渲染」「音频采样回调」so 未导出对应符号)
69
+
70
+ **不编造**:`视频播放/渲染`、`音频采样回调` 等能力不在 `CAPABILITY_RULES`(so 确实无此符号),检测不到就诚实判「无法实现」。plan 里的 NAPI 名用 `verify_plan_napi()` 验证对应能力是否被符号证实——`✓ 符号证实` 才可写桥接,`✗ 未证实` 不能编造。
71
+
72
+ ## 真机验证门禁(连真机自动触发)
73
+
74
+ 静态判定是「推断」,真机验证才是「证据」。verify 脚本自动 `hdc list targets` 检测设备——**连真机就自动触发真机验证门禁**,不连就诚实标注「未真机验证」。
75
+
76
+ 门禁三态(报告顶部卡片 + `summary["真机验证门禁"]`):
77
+
78
+ | 状态 | 条件 | 卡片 |
79
+ |---|---|---|
80
+ | ✅ 通过 | 设备连接 + 全部用例通过 | 绿色 `14/14 真机验证通过` |
81
+ | ❌ 失败 | 设备连接 + 有用例失败 | 红色 `N/14 真机验证失败` |
82
+ | ⚠ 未验证 | 无设备连接 | 黄色 `— 未真机验证` |
83
+
84
+ 自动编排链路(`device-verify.py`):
85
+
86
+ ```
87
+ ① 准备 demo 工程(复制模板 + 补 media 图标 + 生成测试 wav + 集成测试用例 + 替换待测 so)
88
+ ② ohpm install @sj/ffmpeg
89
+ ③ hvigorw 编译 app + ohosTest(模板自带签名材料,自动签名)
90
+ ④ hdc install 两个 HAP
91
+ ⑤ aa test 运行 Hypium 用例
92
+ ⑥ hilog 采集 NATIVE_TEST → run.log → 回填报告(期望 vs 真实结果)
93
+ ```
94
+
95
+ ## 产出
96
+
97
+ - `capability-report.html/json` — 能力验证报告(so 符号→能力清单 + 功能判定 + 方案 + 真机验证门禁状态)
98
+ - `napi-plans/*.md` — 每功能场景一份 NAPI 实现方案(含 so 符号验证标记)
99
+ - `demo-tests/FFmpegVerify.test.ets` — 按功能场景组织的 Hypium 测试用例(文件产物断言)
100
+ - `verify-evidence/run.log` — 真机运行日志(`[NATIVE_TEST]` 格式,供门禁判定)
101
+
102
+
103
+ ## 参考文件
104
+
105
+ - [scripts/verify-ohos-native.py](scripts/verify-ohos-native.py) — 主脚本(能力检测 + 判定 + demo 生成 + 真机验证门禁编排)
106
+ - [scripts/device-verify.py](scripts/device-verify.py) — 真机验证编排(准备 demo → 编译 → 安装 → 运行 → 采集)
107
+ - [scripts/native_common.py](scripts/native_common.py) — 共享模块(工具发现 + 能力检测 + demangle + 前缀聚类)
108
+ - [references/demo-project-template/](references/demo-project-template/) — demo 工程完整模板
109
+ - [references/demo-build-sign-run.md](references/demo-build-sign-run.md) — 构建签名运行流程
110
+ - [references/hypium-test-template.md](references/hypium-test-template.md) — Hypium 用例模板
111
+ - [references/demo-template.md](references/demo-template.md) — demo 生成规范
@@ -0,0 +1,67 @@
1
+ # Demo 构建 · CLI 自动签名 · 部署运行(无头环境)
2
+
3
+ 本文件固化 `d2h-native-library-verify` demo 验证的**无 DevEco Studio 交互**构建/签名/运行链路。每一步都在真实环境验证过。
4
+
5
+ ## 前置
6
+
7
+ - DevEco Studio 已安装(提供 hvigor 工具链 + SDK + 签名工具)
8
+ - 鸿蒙真机/模拟器已连接:`hdc list targets` 可见设备
9
+ - demo 工程已按 [demo-template.md](demo-template.md) 建好(含 `@sj/ffmpeg` 等 ohpm 依赖、验证页)
10
+
11
+ ## 一、构建(hvigor headless)
12
+
13
+ DevEco 的 hvigor 插件不在 ohpm 仓库,必须用 DevEco 自带的 hvigor 入口:
14
+
15
+ ```bash
16
+ export DEVECO_SDK_HOME="/Applications/DevEco-Studio.app/Contents/sdk"
17
+ node "/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js" \
18
+ --mode module -p product=default assembleHap
19
+ ```
20
+
21
+ 产物:`entry/build/default/outputs/default/entry-default-unsigned.hap`
22
+
23
+ ## 二、CLI 自动签名(关键)
24
+
25
+ 鸿蒙安装强制签名(未签名 HAP 报 `no signature file` code 9568320)。**无头复用的正确姿势**:
26
+
27
+ DevEco 自动签名材料在本机 `~/.ohos/config/<项目名>_<hash>.{cer,p12,p7b}`,**绑定 bundleName + 设备 UDID**。三要素:
28
+
29
+ | 材料字段 | 说明 |
30
+ |----------|------|
31
+ | `certpath` (.cer) / `storeFile` (.p12) | 调试证书(keyAlias 固定 `debugKey`) |
32
+ | `profile` (.p7b) | Provisioning Profile,**绑定 bundleName + 设备** |
33
+ | `keyPassword` / `storePassword` | DevEco 加密存储的密码,hvigor 本地可解密(**直接复用原文,勿改**) |
34
+
35
+ 复用步骤:
36
+
37
+ 1. 找本机任一已自动签名的工程(如 `ProtobufDemoOhos`)的 `build-profile.json5`,完整复制其 `signingConfigs` 数组;
38
+ 2. 把该数组填入 demo 工程的 `build-profile.json5` 的 `"app.signingConfigs"`;
39
+ 3. 把 demo 的 `AppScope/app.json5` 的 `bundleName` 改成与 `.p7b` profile 绑定的 bundleName(否则安装时签名校验失败);
40
+ 4. 重新 `assembleHap` → 产物变为 `entry-default-signed.hap`。
41
+
42
+ > 注意:keyPassword/storePassword 是 16 进制密文(`0000001b...` 开头),**不要试图解密或改写**,hvigor 签名任务会本地解密。
43
+
44
+ ## 三、安装与启动
45
+
46
+ ```bash
47
+ hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
48
+ hdc -t <设备序列号> shell aa start -a EntryAbility -b <bundleName>
49
+ ```
50
+
51
+ ## 四、运行采集(无 UI 交互)
52
+
53
+ 让验证页在 `aboutToAppear` 自动执行全部用例(见 [demo-template.md](demo-template.md)),并把 `EntryAbility.loadContent` 直接指向验证页,则启动即触发:
54
+
55
+ ```bash
56
+ hdc -t <设备> shell hilog -r # 清空日志
57
+ hdc -t <设备> shell aa start -a EntryAbility -b <bundleName>
58
+ sleep 5
59
+ hdc -t <设备> shell "hilog -x" | grep NATIVE_VERIFY # hilog -x 转存当前缓冲后退出
60
+ ```
61
+
62
+ 把采集到的日志文件交给 `verify-ohos-native.py --collect-diff` 生成 `functional_diffs[]` 草稿。
63
+
64
+ ## 五、真实链路验证记录(FFmpegAndroid → @sj/ffmpeg)
65
+
66
+ - 构建 → CLI 签名 → 安装 → 启动 → 采集,全程无 DevEco Studio 交互
67
+ - 采集结果:`ffmpeg -version` = **8.0.1**、`ffprobe -version` = **8.0.1**(Android 侧基线 n6.0.1-109)→ 版本差异写入功能差异清单
@@ -0,0 +1 @@
1
+ {"app":{"bundleName":"com.example.protobufdemo","vendor":"example","versionCode":1000000,"versionName":"1.0.0","icon":"$media:app_icon","label":"$string:app_name"}}
@@ -0,0 +1 @@
1
+ {"string":[{"name":"app_name","value":"FFmpegVerify"}]}
@@ -0,0 +1,25 @@
1
+ # Demo 工程完整模板
2
+
3
+ 重建 demo 工程时**必须一次性复制全部文件**——不允许"只写测试不写验证页"的部分构建。
4
+
5
+ ## 必须同时存在的文件清单(缺一不可)
6
+
7
+ | 文件 | 作用 | 漏掉的症状 |
8
+ |------|------|-----------|
9
+ | `EntryAbility.ets` → loadContent **pages/NativeVerifyDemo** | 启动直接进验证页 | 打开只显示 Index 文字 |
10
+ | `NativeVerifyDemo.ets` | 14 项验证卡片(V2 状态、红绿结果) | 无界面 |
11
+ | `main_pages.json` 含 **pages/NativeVerifyDemo** | 页面注册 | 路由报错 |
12
+ | `FFmpegVerify.test.ets` | Hypium 断言测试 | `aa test` 无用例 |
13
+ | `List.test.ets` | 测试聚合 | 测试不执行 |
14
+ | `ohosTest/module.json5` + `entry/build-profile.json5` 含 `ohosTest` target | 测试 HAP 产出 | 不生成测试 HAP |
15
+
16
+ ## 重建步骤(原子操作,不可拆分)
17
+
18
+ ```bash
19
+ # 1. 复制全部模板文件到目标目录
20
+ # 2. ohpm install @sj/ffmpeg
21
+ # 3. 替换 oh_modules 里的 so 为待测 so
22
+ # 4. hvigorw assembleHap(app)
23
+ # 5. hvigorw --mode module -p module=entry@ohosTest assembleHap(test)
24
+ # 6. CLI 签名 → 安装两个 HAP → aa test
25
+ ```
@@ -0,0 +1,24 @@
1
+ {
2
+ "app":{"signingConfigs":[
3
+ {
4
+ // 签名材料占位:真机验证时按 references/demo-build-sign-run.md 从本机
5
+ // ~/.ohos/config/<已自动签名工程>.{cer,p12,p7b} 复制(DevEco 自动签名材料,
6
+ // 绑定 bundleName + 设备 UDID),bundleName 需与 .p7b profile 一致。
7
+ name: 'default',
8
+ type: 'HarmonyOS',
9
+ material: {
10
+ certpath: '<SIGNING_DIR>/<project>.cer',
11
+ keyAlias: 'debugKey',
12
+ keyPassword: '<KEY_PASSWORD>',
13
+ profile: '<SIGNING_DIR>/<project>.p7b',
14
+ signAlg: 'SHA256withECDSA',
15
+ storeFile: '<SIGNING_DIR>/<project>.p12',
16
+ storePassword: '<STORE_PASSWORD>',
17
+ },
18
+ },
19
+ ],
20
+ "products":[{"name":"default","signingConfig":"default","targetSdkVersion":"5.0.5(17)","compatibleSdkVersion":"5.0.0(12)","runtimeOS":"HarmonyOS",
21
+ "buildOption":{"strictMode":{"caseSensitiveCheck":true,"useNormalizedOHMUrl":true}}}],
22
+ "buildModeSet":[{"name":"debug"},{"name":"release"}]},
23
+ "modules":[{"name":"entry","srcPath":"./entry","targets":[{"name":"default","applyToProducts":["default"]}]}]
24
+ }
@@ -0,0 +1 @@
1
+ {"apiType":"stageMode","buildOption":{},"buildOptionSet":[{"name":"release","arkOptions":{"obfuscation":{"ruleOptions":{"enable":false}}}}],"targets":[{"name":"default"},{"name":"ohosTest"}]}
@@ -0,0 +1,2 @@
1
+ import { hapTasks } from '@ohos/hvigor-ohos-plugin';
2
+ export default { system: hapTasks, plugins: [] }
@@ -0,0 +1 @@
1
+ {"name":"entry","version":"1.0.0","dependencies":{},"devDependencies":{"@ohos/hypium":"1.0.25"}}
@@ -0,0 +1,5 @@
1
+ import { UIAbility } from '@kit.AbilityKit';
2
+ import { window } from '@kit.ArkUI';
3
+ export default class EntryAbility extends UIAbility {
4
+ onWindowStageCreate(ws: window.WindowStage) { ws.loadContent('pages/NativeVerifyDemo'); }
5
+ }
@@ -0,0 +1,3 @@
1
+ @Entry
2
+ @Component
3
+ struct Index { build() { Column(){Text('FFmpegVerify').fontSize(24)}.justifyContent(FlexAlign.Center).width('100%').height('100%') } }
@@ -0,0 +1,276 @@
1
+ import { FFmpeg, FFAbortController, FFAudioPlayer } from '@sj/ffmpeg';
2
+ import { common } from '@kit.AbilityKit';
3
+ import { fileIo as fs } from '@kit.CoreFileKit';
4
+
5
+ const BUILD_TS = 'v2-full-14cases';
6
+
7
+ @ObservedV2
8
+ class ApiCase {
9
+ @Trace title: string;
10
+ @Trace scenario: string;
11
+ @Trace expected: string;
12
+ @Trace actual: string;
13
+ @Trace status: string; // pass / fail / pending
14
+ constructor(title: string, scenario: string, expected: string) {
15
+ this.title = title;
16
+ this.scenario = scenario;
17
+ this.expected = expected;
18
+ this.actual = '-';
19
+ this.status = 'pending';
20
+ }
21
+ }
22
+
23
+ @Entry
24
+ @ComponentV2
25
+ struct NativeVerifyDemo {
26
+ @Local cases: ApiCase[] = [
27
+ new ApiCase('01 命令执行', 'FFmpegCmd.handle → FFmpeg.execute', '含 ffmpeg version'),
28
+ new ApiCase('02 探测执行', 'FFmpegCmd.handleProbe → FFmpeg.execute', '含 ffprobe version'),
29
+ new ApiCase('03 任务取消', 'FFmpegCmd.cancelTaskJni → FFAbortController', 'Promise 中断'),
30
+ new ApiCase('04 音频播放', 'AudioPlayer.native_play → FFAudioPlayer', 'playWhenReady=true'),
31
+ new ApiCase('05 获取时长', 'AudioPlayer.native_get_duration → FFAudioPlayer', 'duration>0(wav 为 2000ms)'),
32
+ new ApiCase('06 获取位置', 'AudioPlayer.native_get_position → FFAudioPlayer', 'currentTime>=0'),
33
+ new ApiCase('07 重新播放', 'AudioPlayer.native_again → FFAudioPlayer', 'seek(0) 后可播放'),
34
+ new ApiCase('08 视频转码', 'VideoPlayer.executeTranscode → FFmpeg.execute', 'm4a 产物文件生成'),
35
+ new ApiCase('09 重跑命令', 'VideoPlayer.again → FFmpeg.execute', '重复执行输出相同'),
36
+ new ApiCase('10 播放暂停', 'VideoPlayer.playAudio → FFAudioPlayer', 'pause 后 playWhenReady=false'),
37
+ new ApiCase('11 数据源探测', 'Retriever.native_setDataSource → ffprobe', '输出非空'),
38
+ new ApiCase('12 元数据提取', 'Retriever.native_extractMetadata → ffprobe', '输出含 format'),
39
+ new ApiCase('13 截帧', 'Retriever.native_getFrameAtTime → ffmpeg', 'frame.jpg 生成'),
40
+ new ApiCase('14 缩放截帧', 'Retriever.native_getScaleFrameAtTime → ffmpeg', '32x32 frame_small.jpg 生成'),
41
+ ];
42
+ @Local wav: string = '';
43
+ @Local ready: boolean = false;
44
+
45
+ aboutToAppear(): void {
46
+ const ctx = getContext(this) as common.UIAbilityContext;
47
+ this.setup(ctx).then(() => {
48
+ this.wav = `${ctx.filesDir}/ffmpeg_test.wav`;
49
+ this.ready = true;
50
+ this.runAll(ctx);
51
+ });
52
+ }
53
+
54
+ async setup(ctx: common.UIAbilityContext): Promise<void> {
55
+ try {
56
+ const raw = await ctx.resourceManager.getRawFileContent('ffmpeg_test.wav');
57
+ const f = fs.openSync(`${ctx.filesDir}/ffmpeg_test.wav`, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
58
+ fs.writeSync(f.fd, raw.buffer);
59
+ fs.closeSync(f);
60
+ } catch (e) {
61
+ console.info(`[NATIVE_VERIFY] setup: ${JSON.stringify(e)}`);
62
+ }
63
+ }
64
+
65
+ runAll(ctx: common.UIAbilityContext): void {
66
+ this.exec(0, ['ffmpeg', '-version']);
67
+ this.exec(1, ['ffprobe', '-version']);
68
+ this.abort(2);
69
+ this.audio(3, 'play');
70
+ this.audio(4, 'duration');
71
+ this.audio(5, 'position');
72
+ this.audio(6, 'replay');
73
+ this.transcode(7);
74
+ this.exec(8, ['ffmpeg', '-version']);
75
+ this.audio(9, 'pause');
76
+ this.probe(10, false);
77
+ this.probe(11, true);
78
+ this.frame(12, false);
79
+ setTimeout(() => { this.frame(13, true); }, 3000);
80
+ }
81
+
82
+ exec(i: number, cmd: string[]): void {
83
+ this.cases[i].actual = '运行中...';
84
+ let out = '';
85
+ FFmpeg.execute(cmd, {
86
+ logCallback: (m: string) => { out += m; },
87
+ outputCallback: (m: string) => { out += m; },
88
+ }).then(() => {
89
+ const ok = out.includes('version');
90
+ this.cases[i].actual = out.slice(0, 60);
91
+ this.cases[i].status = ok ? 'pass' : 'fail';
92
+ }).catch((e: Error) => {
93
+ this.cases[i].actual = '异常: ' + e.message;
94
+ this.cases[i].status = 'fail';
95
+ });
96
+ }
97
+
98
+ abort(i: number): void {
99
+ this.cases[i].actual = '运行中...';
100
+ const c = new FFAbortController();
101
+ FFmpeg.execute(['ffmpeg', '-i', this.wav, `${this.wav}.abort.m4a`, '-y'],
102
+ { signal: c.signal, logCallback: (m: string) => {} })
103
+ .then(() => {
104
+ this.cases[i].actual = '✗ 未被中断';
105
+ this.cases[i].status = 'fail';
106
+ }).catch(() => {
107
+ this.cases[i].actual = '✓ 已中断';
108
+ this.cases[i].status = 'pass';
109
+ });
110
+ setTimeout(() => { c.abort(); }, 300);
111
+ }
112
+
113
+ audio(i: number, mode: string): void {
114
+ this.cases[i].actual = '运行中...';
115
+ const p = new FFAudioPlayer();
116
+ p.url = `file://${this.wav}`;
117
+ p.prepare();
118
+ setTimeout(() => {
119
+ if (mode === 'duration') {
120
+ this.cases[i].actual = `duration=${p.duration}ms`;
121
+ this.cases[i].status = p.duration > 0 ? 'pass' : 'fail';
122
+ } else if (mode === 'position') {
123
+ p.play();
124
+ setTimeout(() => {
125
+ this.cases[i].actual = `position=${p.currentTime}ms`;
126
+ this.cases[i].status = p.currentTime >= 0 ? 'pass' : 'fail';
127
+ p.pause();
128
+ }, 500);
129
+ } else if (mode === 'pause') {
130
+ p.play();
131
+ setTimeout(() => {
132
+ p.pause();
133
+ this.cases[i].actual = `paused=${!p.playWhenReady}`;
134
+ this.cases[i].status = !p.playWhenReady ? 'pass' : 'fail';
135
+ }, 500);
136
+ } else if (mode === 'replay') {
137
+ p.play();
138
+ setTimeout(() => {
139
+ p.seek(0);
140
+ p.play();
141
+ this.cases[i].actual = `replaying=${p.playWhenReady}`;
142
+ this.cases[i].status = p.playWhenReady ? 'pass' : 'fail';
143
+ p.pause();
144
+ }, 500);
145
+ } else {
146
+ p.play();
147
+ setTimeout(() => {
148
+ this.cases[i].actual = `playing=${p.playWhenReady}`;
149
+ this.cases[i].status = p.playWhenReady ? 'pass' : 'fail';
150
+ p.pause();
151
+ }, 800);
152
+ }
153
+ }, 1500);
154
+ }
155
+
156
+ transcode(i: number): void {
157
+ this.cases[i].actual = '运行中...';
158
+ FFmpeg.execute(['ffmpeg', '-i', this.wav, `${this.wav}.m4a`, '-y'],
159
+ { logCallback: (m: string) => {} })
160
+ .then(() => {
161
+ let ok = false;
162
+ try { fs.accessSync(`${this.wav}.m4a`); ok = true; } catch (e) { ok = false; }
163
+ this.cases[i].actual = ok ? '✓ m4a 已生成' : '✗ 未生成';
164
+ this.cases[i].status = ok ? 'pass' : 'fail';
165
+ }).catch((e: Error) => {
166
+ this.cases[i].actual = '异常: ' + e.message;
167
+ this.cases[i].status = 'fail';
168
+ });
169
+ }
170
+
171
+ probe(i: number, showFormat: boolean): void {
172
+ this.cases[i].actual = '运行中...';
173
+ let out = '';
174
+ FFmpeg.execute(['ffprobe', '-v', 'info', '-of', 'json', '-show_format', '-i', this.wav], {
175
+ logCallback: (m: string) => { out += m; },
176
+ outputCallback: (m: string) => { out += m; },
177
+ }).then(() => {
178
+ this.cases[i].actual = out.slice(0, 80);
179
+ if (showFormat) {
180
+ this.cases[i].status = out.includes('format') ? 'pass' : 'fail';
181
+ } else {
182
+ this.cases[i].status = out.length > 0 ? 'pass' : 'fail';
183
+ }
184
+ }).catch((e: Error) => {
185
+ this.cases[i].actual = '异常: ' + e.message;
186
+ this.cases[i].status = 'fail';
187
+ });
188
+ }
189
+
190
+ frame(i: number, scaled: boolean): void {
191
+ this.cases[i].actual = '运行中...';
192
+ const gen = FFmpeg.execute(['ffmpeg', '-f', 'lavfi', '-i', 'color=c=red:s=64x64:d=1', `${this.wav}.gen.mp4`, '-y'],
193
+ { logCallback: (m: string) => {} });
194
+ gen.then(() => {
195
+ const args = scaled
196
+ ? ['ffmpeg', '-ss', '0', '-i', `${this.wav}.gen.mp4`, '-frames:v', '1', '-vf', 'scale=32:32', `${this.wav}.fs.jpg`, '-y']
197
+ : ['ffmpeg', '-ss', '0', '-i', `${this.wav}.gen.mp4`, '-frames:v', '1', `${this.wav}.f.jpg`, '-y'];
198
+ FFmpeg.execute(args, { logCallback: (m: string) => {} })
199
+ .then(() => {
200
+ const f = scaled ? `${this.wav}.fs.jpg` : `${this.wav}.f.jpg`;
201
+ let ok = false;
202
+ try { fs.accessSync(f); ok = true; } catch (e) { ok = false; }
203
+ this.cases[i].actual = ok ? '✓ 截图已生成' : '✗ 未生成';
204
+ this.cases[i].status = ok ? 'pass' : 'fail';
205
+ })
206
+ .catch((e: Error) => {
207
+ this.cases[i].actual = '异常(截帧): ' + e.message;
208
+ this.cases[i].status = 'fail';
209
+ });
210
+ }).catch((e: Error) => {
211
+ this.cases[i].actual = '异常(生成视频): ' + e.message;
212
+ this.cases[i].status = 'fail';
213
+ });
214
+ }
215
+
216
+ @Builder
217
+ card(i: number) {
218
+ Column({ space: 4 }) {
219
+ Row({ space: 8 }) {
220
+ Text(this.cases[i].title).fontSize(14).fontWeight(FontWeight.Bold).layoutWeight(1)
221
+ if (this.cases[i].status === 'pass') {
222
+ Text('✓').fontSize(16).fontColor(Color.Green)
223
+ } else if (this.cases[i].status === 'fail') {
224
+ Text('✗').fontSize(16).fontColor(Color.Red)
225
+ } else {
226
+ Text('…').fontSize(14).fontColor(Color.Gray)
227
+ }
228
+ }
229
+ Text(this.cases[i].scenario).fontSize(9).fontColor(Color.Gray).maxLines(1)
230
+ Text('期望: ' + this.cases[i].expected).fontSize(10).fontColor(Color.Blue).maxLines(1)
231
+ Text('实际: ' + this.cases[i].actual)
232
+ .fontSize(10)
233
+ .fontColor(
234
+ (this.cases[i].actual.startsWith('✗') || this.cases[i].actual.startsWith('异常'))
235
+ ? Color.Red : Color.Gray)
236
+ .maxLines(2)
237
+ }
238
+ .width('100%')
239
+ .padding(10)
240
+ .backgroundColor(this.cases[i].status === 'fail' ? '#FFF0F0' : '#F5F5F5')
241
+ .borderRadius(8)
242
+ .onClick(() => {
243
+ if (!this.ready) { return; }
244
+ const ctx = getContext(this) as common.UIAbilityContext;
245
+ const map: Record<number, () => void> = {
246
+ 0: () => this.exec(0, ['ffmpeg', '-version']),
247
+ 1: () => this.exec(1, ['ffprobe', '-version']),
248
+ 2: () => this.abort(2),
249
+ 3: () => this.audio(3, 'play'),
250
+ 4: () => this.audio(4, 'duration'),
251
+ 5: () => this.audio(5, 'position'),
252
+ 6: () => this.audio(6, 'replay'),
253
+ 7: () => this.transcode(7),
254
+ 8: () => this.exec(8, ['ffmpeg', '-version']),
255
+ 9: () => this.audio(9, 'pause'),
256
+ 10: () => this.probe(10, false),
257
+ 11: () => this.probe(11, true),
258
+ 12: () => this.frame(12, false),
259
+ 13: () => this.frame(13, true),
260
+ };
261
+ if (map[i]) { map[i](); }
262
+ })
263
+ }
264
+
265
+ build() {
266
+ Scroll() {
267
+ Column({ space: 6 }) {
268
+ Text('FFmpeg so 功能验证 ' + BUILD_TS).fontSize(18).fontWeight(FontWeight.Bold)
269
+ Text('14 项 verify · 加载自动执行 · 点击卡片重跑').fontSize(10).fontColor(Color.Gray)
270
+ ForEach(this.cases, (c: ApiCase, i: number) => {
271
+ this.card(i)
272
+ }, (c: ApiCase) => c.title)
273
+ }.width('100%').padding(10)
274
+ }.width('100%').height('100%')
275
+ }
276
+ }
@@ -0,0 +1 @@
1
+ {"module":{"name":"entry","type":"entry","description":"$string:m","mainElement":"EntryAbility","deviceTypes":["phone"],"deliveryWithInstall":true,"installationFree":false,"pages":"$profile:main_pages","abilities":[{"name":"EntryAbility","srcEntry":"./ets/entryability/EntryAbility.ets","description":"$string:m","icon":"$media:layered_image","label":"$string:m","startWindowIcon":"$media:startIcon","startWindowBackground":"$color:bg","exported":true,"skills":[{"entities":["entity.system.home"],"actions":["ohos.want.action.home"]}]}]}}