@harmonyos-arkts/d2h 0.1.20 → 0.1.21

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 (151) 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-page-develop → d2h-spec-develop}/SKILL.md +143 -126
  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/agents/openai.yaml +0 -7
  138. package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
  139. package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
  140. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
  141. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
  142. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
  143. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
  144. package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
  145. package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
  146. package/skills/native-library-substitution/SKILL.md +0 -385
  147. package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
  148. package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
  149. package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
  150. package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
  151. package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
@@ -0,0 +1,1099 @@
1
+ #!/usr/bin/env python3
2
+ """Android 功能场景 → 鸿蒙 so/a 能力验证(按功能,从 so/a 导出符号检测能力)。
3
+
4
+ 理念:
5
+ 不按 Android 每个 JNI API 做 1:1 映射,也不依赖 .d.ts 声明(声明可能与实际 so 不符)。
6
+ 能力判定的唯一依据是 so/a 的导出符号表(llvm-nm)——这是「事实」,不是声明、不是编造:
7
+ · 用 llvm-nm 提取 so/a 导出符号(.so 动态符号 / .a 全局符号)
8
+ · 从符号检测真实能力(avcodec_/avformat_/ff_libx264_encoder/ff_rtmp_protocol/FFAudioPlayer…)
9
+ · 能力 ⊆ 功能所需(requires)→ 可实现/部分实现/无法实现
10
+ · 能实现 → NAPI 可基于真实符号自己写桥接,生成 Hypium demo 真机验证
11
+ · 不能实现 → 真正无法实现(so 未导出对应符号)
12
+
13
+ 流程:
14
+ ① 加载 analyse 产出的功能清单(feature-manifest.json)或直接扫描 Android 源码
15
+ ② llvm-nm 提取 so/a 导出符号 → detect_capabilities 检测能力(非 .d.ts)
16
+ ③ 每个功能场景:judge_feature(requires, detected) 判定可实现/部分实现/无法实现
17
+ ④ 生成 Hypium 测试用例(用 so 真实能力实现功能,文件产物断言)
18
+ ⑤ 输出:能力验证报告(HTML+JSON)+ 每场景 NAPI 方案(.md)+ demo 测试用例
19
+
20
+ 用法:
21
+ python3 verify-ohos-native.py --android <工程> --ohos <so或.a文件或目录> \
22
+ [--manifest <feature-manifest.json>] [--dts <Index.d.ts>] [--output <目录>]
23
+ """
24
+ import argparse, json, os, re, sys, subprocess, html as _html
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+ import native_common as nc
28
+ from native_common import (run, resolve_tool, find_elf_nm, find_llvm_readelf,
29
+ find_cxxfilt, demangle_symbol, classify_feature,
30
+ detect_capabilities, CAPABILITY_RULES, TOOL_INSTALL_HINTS,
31
+ prefix_meaning, esc)
32
+
33
+ _e = esc
34
+
35
+ __version__ = "4.0.0"
36
+
37
+ # ==================== Android 源码扫描 ====================
38
+ JAVA_NATIVE_RE = re.compile(r"\bnative\s+[\w<>\[\],\s]*?(\w+)\s*\(")
39
+ KOTLIN_EXTERNAL_RE = re.compile(r"\bexternal\s+fun\s+(\w+)\s*\(")
40
+ PACKAGE_RE = re.compile(r"^\s*package\s+([\w.]+)", re.MULTILINE)
41
+ LOAD_LIB_RE = re.compile(r'System\.loadLibrary\("([^"]+)"\)')
42
+
43
+ def scan_android_jni(root):
44
+ results = []
45
+ for dirpath, dirnames, filenames in os.walk(root):
46
+ dirnames[:] = [d for d in dirnames if d not in (".git", ".gradle", "build", ".idea", "node_modules")]
47
+ for fn in filenames:
48
+ if not fn.endswith((".java", ".kt")):
49
+ continue
50
+ path = os.path.join(dirpath, fn)
51
+ try:
52
+ content = open(path, encoding="utf-8", errors="replace").read(1 << 20)
53
+ except OSError:
54
+ continue
55
+ pkg_m = PACKAGE_RE.search(content)
56
+ pkg = pkg_m.group(1) + "." if pkg_m else ""
57
+ stem = os.path.splitext(fn)[0]
58
+ full_class = pkg + stem
59
+ ext = os.path.splitext(fn)[1]
60
+ native_methods = []
61
+ for lineno, line in enumerate(content.splitlines(), 1):
62
+ m = JAVA_NATIVE_RE.search(line) if ext == ".java" else KOTLIN_EXTERNAL_RE.search(line)
63
+ if m:
64
+ native_methods.append({"method": m.group(1), "line": lineno})
65
+ if native_methods:
66
+ results.append({
67
+ "class": full_class, "file": fn, "path": path,
68
+ "load_libraries": LOAD_LIB_RE.findall(content),
69
+ "native_methods": native_methods,
70
+ "feature": classify_feature(full_class) or "通用",
71
+ })
72
+ return results
73
+
74
+ # ==================== 鸿蒙 so/a 分析 ====================
75
+ def get_so_exports(so_path, nm_tool):
76
+ return nc.get_lib_exports(so_path, nm_tool)
77
+
78
+ def get_so_napi_apis(so_path, nm_tool):
79
+ exports = get_so_exports(so_path, nm_tool)
80
+ is_napi = any("napi_" in s or s.startswith("napi") for s in exports)
81
+ return {"is_napi": is_napi, "export_count": len(exports)}
82
+
83
+ def check_android_residue(so_path, nm_tool, readelf=None):
84
+ if readelf is None:
85
+ readelf = resolve_tool(["llvm-readelf", "readelf"])
86
+ dyn = run([readelf, "-d", "-W", so_path]) if readelf else ""
87
+ needed = re.findall(r"\(NEEDED\)\s+Shared library:\s*\[([^\]]+)\]", dyn)
88
+ android_libs = [l for l in needed if l in ("liblog.so", "libandroid.so", "libnativehelper.so", "libmediandk.so")]
89
+ exports = get_so_exports(so_path, nm_tool) if nm_tool else set()
90
+ jni_exports = [s for s in exports if s.startswith("Java_")]
91
+ ndk1_count = sum(1 for s in exports if "__ndk1" in s)
92
+ issues = []
93
+ if android_libs:
94
+ issues.append({"level": "fail", "msg": f"依赖 Android 专有库: {', '.join(android_libs)}"})
95
+ if jni_exports:
96
+ issues.append({"level": "fail", "msg": f"JNI 导出残留 {len(jni_exports)} 个(鸿蒙无 ART)"})
97
+ if ndk1_count > 0:
98
+ issues.append({"level": "warn", "msg": f"__ndk1 符号 {ndk1_count} 个(C++ ABI)"})
99
+ return issues, needed
100
+
101
+ # ==================== 鸿蒙 .d.ts NAPI 提取 ====================
102
+ def extract_napi_from_dts(dts_path):
103
+ if not dts_path or not os.path.isfile(dts_path):
104
+ return {}
105
+ try:
106
+ content = open(dts_path, encoding="utf-8", errors="replace").read(1 << 20)
107
+ except OSError:
108
+ return {}
109
+ content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
110
+ content = re.sub(r"//[^\n]*", "", content)
111
+
112
+ apis = {}
113
+ pat = re.compile(r"export\s+(namespace|class|interface|enum)\s+(\w+)")
114
+ i = 0
115
+ while True:
116
+ m = pat.search(content, i)
117
+ if not m:
118
+ break
119
+ kind, name = m.group(1), m.group(2)
120
+ open_brace = content.find("{", m.end())
121
+ if open_brace == -1:
122
+ i = m.end()
123
+ continue
124
+ depth = 0
125
+ j = open_brace
126
+ while j < len(content):
127
+ if content[j] == "{":
128
+ depth += 1
129
+ elif content[j] == "}":
130
+ depth -= 1
131
+ if depth == 0:
132
+ break
133
+ j += 1
134
+ body = content[open_brace + 1:j]
135
+ i = j + 1
136
+ if kind == "namespace":
137
+ for fm in re.finditer(r"export\s+function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*([^;{]+))?;", body):
138
+ fname, params, ret = fm.group(1), fm.group(2).strip(), (fm.group(3) or "void").strip()
139
+ apis[f"{name}.{fname}"] = f"{fname}({params}): {ret}"
140
+ elif kind == "class":
141
+ for mm in re.finditer(r"(?:public\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*([^;{]+))?;", body):
142
+ mname, params = mm.group(1), mm.group(2).strip()
143
+ if mname in ("get", "set", "constructor"):
144
+ continue
145
+ apis[f"{name}.{mname}"] = f"{mname}({params})" + (f": {mm.group(3).strip()}" if mm.group(3) else "")
146
+ for pm in re.finditer(r"(?:public\s+)?(?:get|set)\s+(\w+)\s*\(\)\s*:\s*([^;]+);", body):
147
+ apis[f"{name}.{pm.group(1)}"] = f"{pm.group(1)}: {pm.group(2).strip()}"
148
+ return apis
149
+
150
+ def find_dts_for_so(so_path):
151
+ dirs = [os.path.dirname(so_path)]
152
+ p = os.path.dirname(so_path)
153
+ for _ in range(5):
154
+ parent = os.path.dirname(p)
155
+ if parent == p:
156
+ break
157
+ dirs.append(parent)
158
+ p = parent
159
+ for d in dirs:
160
+ try:
161
+ entries = os.listdir(d)
162
+ except OSError:
163
+ continue
164
+ for e in entries:
165
+ if e.endswith(".d.ts") or e.endswith(".d.ets"):
166
+ return os.path.join(d, e)
167
+ for sub in ("types", "libffmpeg", "cpp"):
168
+ subdir = os.path.join(d, sub)
169
+ if os.path.isdir(subdir):
170
+ for dp2, _, fns in os.walk(subdir):
171
+ if "node_modules" in dp2 or "oh_modules" in dp2:
172
+ continue
173
+ for fn in fns:
174
+ if fn.endswith((".d.ts", ".d.ets")):
175
+ return os.path.join(dp2, fn)
176
+ return None
177
+
178
+ # ==================== 功能场景能力方案 ====================
179
+ # 每个功能场景:Android 用 so 实现的功能 → 所需底层能力(requires)。
180
+ # requires 里的能力名对应 CAPABILITY_RULES;判定完全由 so 导出符号检测结果决定:
181
+ # requires 全命中 → 可实现;部分命中 → 部分实现;全不命中 → 无法实现。
182
+ # 「视频播放/渲染」「音频采样回调」等能力不在 CAPABILITY_RULES(so 未导出),故检测不到 → 判为无法实现。
183
+ FEATURE_PLANS = {
184
+ "媒体格式探测(MediaRetriever)": {
185
+ "usage": "提取媒体元数据、生成音频波形图、提取视频帧缩略图",
186
+ "requires": ["封装/解封装", "音视频编解码"],
187
+ "plan": [
188
+ ("提取元数据", "FFmpeg.execute", "ffprobe -v info -of json -show_format -show_streams -i <file>"),
189
+ ("音频波形图", "FFmpeg.execute", "ffmpeg -i <file> -filter_complex showwavespic=s=800x400 <out.png>"),
190
+ ("视频帧提取", "FFmpeg.execute", "ffmpeg -ss <t> -i <file> -frames:v 1 <out.jpg>"),
191
+ ("缩放帧提取", "FFmpeg.execute", "ffmpeg -ss <t> -i <file> -frames:v 1 -vf scale=<w>:<h> <out.jpg>"),
192
+ ],
193
+ "note": "setSurface(视频渲染表面)非本场景核心,且需 XComponent,不纳入本功能验证",
194
+ },
195
+ "视频剪辑处理(FFmpegCmd)": {
196
+ "usage": "执行 ffmpeg/ffprobe 命令(剪辑、转码、探测)",
197
+ "requires": ["命令执行 NAPI", "音视频编解码", "封装/解封装"],
198
+ "plan": [
199
+ ("执行命令", "FFmpeg.execute", "FFmpeg.execute(['ffmpeg', '-i', in, out, '-y'])"),
200
+ ("探测命令", "FFmpeg.execute", "FFmpeg.execute(['ffprobe', '-i', in])"),
201
+ ("取消任务", "FFAbortController.abort", "FFmpeg.execute(cmds, { signal: ctrl.signal }); ctrl.abort()"),
202
+ ],
203
+ },
204
+ "音频播放(AudioPlayer)": {
205
+ "usage": "播放音频(播放/时长/位置/重播)",
206
+ "requires": ["音频播放 NAPI"],
207
+ "plan": [
208
+ ("播放", "FFAudioPlayer.play", "player.url = ...; player.prepare(); player.play()"),
209
+ ("时长", "FFAudioPlayer.duration", "player.duration"),
210
+ ("位置", "FFAudioPlayer.currentTime", "player.currentTime"),
211
+ ("重播", "FFAudioPlayer.seek", "player.seek(0); player.play()"),
212
+ ],
213
+ },
214
+ "直播推流(LivePusher)": {
215
+ "usage": "音视频实时推流到 RTMP 服务器(H.264/AAC 编码)",
216
+ "requires": ["H.264 编码", "AAC 编码", "RTMP 协议", "FLV 封装"],
217
+ "plan": [
218
+ ("RTMP 推流", "FFmpeg.execute",
219
+ "ffmpeg -re -i <input> -c:v libx264 -preset fast -c:a aac -f flv <rtmp://...>"),
220
+ ("H.264 编码", "FFmpeg.execute", "ffmpeg -i <in> -c:v libx264 <out.mp4>"),
221
+ ("AAC 编码", "FFmpeg.execute", "ffmpeg -i <in> -c:a aac <out.m4a>"),
222
+ ("FLV 封装", "FFmpeg.execute", "ffmpeg -i <in> -c:v libx264 -f flv <out.flv>"),
223
+ ],
224
+ "note": "libfaac.a/libx264.a 为 Android 侧编码器静态库;鸿蒙 so 内置 libx264/aac 编码器与 flv 封装,真机验证可用",
225
+ },
226
+ "视频滤镜预览(VideoPlayer)": {
227
+ "usage": "视频转码、滤镜处理、音频播放",
228
+ "requires": ["滤镜", "视频渲染"], # 滤镜命中;视频渲染 so 未导出 → 部分实现
229
+ "plan": [
230
+ ("转码", "FFmpeg.execute", "ffmpeg -i <in> -c:v libx264 <out.mp4>"),
231
+ ("滤镜处理", "FFmpeg.execute", "ffmpeg -i <in> -vf <filter> <out.mp4>"),
232
+ ("音频播放", "FFAudioPlayer", "FFAudioPlayer 播放音轨"),
233
+ ],
234
+ "note": "filter 离线处理可用(avfilter 库存在);实时预览需解码帧 + 渲染表面,so 未导出渲染 NAPI,需 XComponent 自建渲染管线",
235
+ },
236
+ "视频播放(NextPlayer引擎)": {
237
+ "usage": "视频解码 + 播放 + 渲染(25 个播放器引擎 JNI)",
238
+ "requires": ["视频播放/渲染"], # so 未导出视频播放/渲染 NAPI → 无法实现
239
+ "plan": [],
240
+ "note": "so 仅导出音频播放(FFAudioPlayer),无视频解码帧 + 渲染 NAPI;需系统 @ohos.multimedia.media.AVPlayer 替代,或用 avcodec 软解 + XComponent 自建渲染管线",
241
+ },
242
+ "音频可视化(FrankVisualizer)": {
243
+ "usage": "捕获实时音频 PCM 数据做波形/频谱可视化",
244
+ "requires": ["音频采样回调"], # so 未导出 PCM 采样回调 → 无法实现
245
+ "plan": [],
246
+ "note": "so 的 FFAudioPlayer 不暴露 PCM、FFAudioWriter 只写不读;需 @ohos.multimedia.audio.AudioRenderer on('audioData') 回调自实现 FFT",
247
+ },
248
+ }
249
+
250
+ VERDICT_LABEL = {
251
+ "可实现": "✓ 可实现",
252
+ "部分实现": "◐ 部分实现",
253
+ "需验证": "⚠ 需验证",
254
+ "无法实现": "✗ 无法实现",
255
+ }
256
+
257
+ def judge_feature(requires, detected):
258
+ """根据 so 导出符号检测到的能力,判定功能场景能否实现。"""
259
+ met = [r for r in requires if r in detected]
260
+ missing = [r for r in requires if r not in detected]
261
+ if not missing:
262
+ return "可实现", f"所需能力全部由 so 导出符号证实:{', '.join(requires)}"
263
+ if met:
264
+ return "部分实现", f"已证实能力:{', '.join(met)};缺失(so 未导出):{', '.join(missing)}"
265
+ return "无法实现", f"所需能力均未在 so 导出符号中检测到:{', '.join(missing)}"
266
+
267
+ # ==================== 功能场景匹配 ====================
268
+ def match_feature_plan(feature):
269
+ """按功能场景名精确匹配能力方案;未命中用通用推断。"""
270
+ if feature in FEATURE_PLANS:
271
+ return FEATURE_PLANS[feature]
272
+ f = feature.lower()
273
+ if any(k in f for k in ["推流", "live", "rtmp"]):
274
+ return {"usage": feature, "requires": ["RTMP 协议", "FLV 封装", "H.264 编码", "AAC 编码"], "plan": []}
275
+ if any(k in f for k in ["播放", "player", "video"]):
276
+ return {"usage": feature, "requires": ["视频播放/渲染"], "plan": []}
277
+ if any(k in f for k in ["可视化", "visual", "fft"]):
278
+ return {"usage": feature, "requires": ["音频采样回调"], "plan": []}
279
+ return {"usage": feature, "requires": ["命令执行 NAPI"], "plan": []}
280
+
281
+ # ==================== NAPI 名验证(对照 so 导出符号,非 .d.ts)====================
282
+ # plan 里的 NAPI 接口名 → 对应的能力(能力从 so 导出符号检测)
283
+ NAPI_CLASS_TO_CAPABILITY = {
284
+ "FFmpeg": "命令执行 NAPI",
285
+ "FFAudioPlayer": "音频播放 NAPI",
286
+ "FFAudioWriter": "音频写入 NAPI",
287
+ "FFAbortController": "任务取消 NAPI",
288
+ "FFAbortSignal": "任务取消 NAPI",
289
+ }
290
+
291
+ def verify_plan_napi(plan, detected):
292
+ """验证 plan 每个步骤的 NAPI 名对应的能力是否被 so 导出符号证实。
293
+
294
+ 返回 {napi_name: status},status:
295
+ declared —— 对应能力已被 so 导出符号检测到(真实存在)
296
+ missing —— so 导出符号中无对应类/接口(不能写 NAPI 桥接,不能编造)
297
+ """
298
+ verified = {}
299
+ for step, napi, code in (plan or []):
300
+ cls = napi.split(".")[0]
301
+ cap = NAPI_CLASS_TO_CAPABILITY.get(cls, cls)
302
+ verified[napi] = "declared" if cap in detected else "missing"
303
+ return verified
304
+
305
+
306
+ # ==================== Hypium 测试用例生成 ====================
307
+ def generate_hypium_case(feature, verdict, reason=""):
308
+ """为功能场景生成 Hypium 断言用例(用鸿蒙 so NAPI 实现功能)。"""
309
+ name = re.sub(r"[^\w]", "_", feature)
310
+ if verdict == "无法实现":
311
+ return {
312
+ "test_name": f"{name}_no_ohos_api",
313
+ "code": None,
314
+ "assert": "unverifiable",
315
+ "comment": reason,
316
+ }
317
+ # 可实现/部分实现/需验证 → 生成可运行的测试代码(占位,由 demo 生成器填入)
318
+ return {
319
+ "test_name": f"{name}_capability",
320
+ "code": None,
321
+ "assert": "runtime",
322
+ }
323
+
324
+ HYPIUM_HEADER = '''import { describe, it, expect, beforeAll } from '@ohos/hypium';
325
+ import { FFmpeg, FFAbortController, FFAudioPlayer } from '@sj/ffmpeg';
326
+ import { abilityDelegatorRegistry } from '@kit.TestKit';
327
+ import { common } from '@kit.AbilityKit';
328
+ import { fileIo as fs } from '@kit.CoreFileKit';
329
+
330
+ let filesDir = '';
331
+ let wav = '';
332
+
333
+ function run(cmd: string[]): Promise<string> {
334
+ return new Promise<string>((resolve) => {
335
+ let out = '';
336
+ FFmpeg.execute(cmd, {
337
+ logCallback: (msg: string) => { out += msg; },
338
+ outputCallback: (msg: string) => { out += msg; },
339
+ }).then(() => resolve(out)).catch((e: Error) => resolve('ERR:' + e.message));
340
+ });
341
+ }
342
+
343
+ function fileExists(p: string): boolean {
344
+ try { fs.accessSync(p); return true; } catch (e) { return false; }
345
+ }
346
+ '''
347
+
348
+ # 每个功能场景 → Hypium 测试用例代码(用鸿蒙 so NAPI 实现功能并断言)
349
+ HYPIUM_SCENARIO_TESTS = {
350
+ "视频剪辑处理(FFmpegCmd)": '''
351
+ // ==== 视频剪辑处理(FFmpegCmd) — 可实现:FFmpeg.execute 命令执行 + 文件产物 ====
352
+ it('FFmpegCmd_handle_executes', 0, async () => {
353
+ await run(['ffmpeg', '-f', 'lavfi', '-i', 'color=c=red:s=64x64:d=1', `${filesDir}/handle_test.mp4`, '-y']);
354
+ const ok = fileExists(`${filesDir}/handle_test.mp4`);
355
+ console.info(`[NATIVE_TEST] FFmpegCmd_handle ok=${ok}`);
356
+ expect(ok).assertTrue();
357
+ });
358
+ it('FFmpegCmd_handleProbe_executes', 0, async () => {
359
+ const out = await run(['ffprobe', '-i', wav]);
360
+ console.info(`[NATIVE_TEST] FFmpegCmd_handleProbe out=${out.slice(0, 60)}`);
361
+ expect(out.length).assertLarger(0);
362
+ });
363
+ it('FFmpegCmd_cancelTask_abort', 0, async () => {
364
+ const c = new FFAbortController();
365
+ const p = FFmpeg.execute(['ffmpeg', '-f', 'lavfi', '-i', 'testsrc=duration=10:size=320x240:rate=30', '-c:v', 'libx264', `${filesDir}/abort.mp4`, '-y'],
366
+ { signal: c.signal, logCallback: (msg: string) => {} });
367
+ setTimeout(() => { c.abort(); }, 500);
368
+ let aborted = false;
369
+ try { await p; } catch (err) { aborted = true; }
370
+ console.info(`[NATIVE_TEST] FFmpegCmd_cancelTask aborted=${aborted}`);
371
+ expect(aborted).assertTrue();
372
+ });
373
+ ''',
374
+ "音频播放(AudioPlayer)": '''
375
+ // ==== 音频播放(AudioPlayer) — 可实现:FFAudioPlayer ====
376
+ it('AudioPlayer_play', 0, async () => {
377
+ const p = new FFAudioPlayer();
378
+ p.url = `file://${wav}`;
379
+ p.prepare();
380
+ await new Promise<void>((r) => setTimeout(r, 1500));
381
+ p.play();
382
+ await new Promise<void>((r) => setTimeout(r, 800));
383
+ const playing = p.playWhenReady;
384
+ p.pause();
385
+ console.info(`[NATIVE_TEST] AudioPlayer_play playing=${playing}`);
386
+ expect(playing).assertTrue();
387
+ });
388
+ it('AudioPlayer_duration', 0, async () => {
389
+ const p = new FFAudioPlayer();
390
+ p.url = `file://${wav}`;
391
+ p.prepare();
392
+ await new Promise<void>((r) => setTimeout(r, 1500));
393
+ console.info(`[NATIVE_TEST] AudioPlayer_duration duration=${p.duration}`);
394
+ expect(p.duration).assertLarger(0);
395
+ });
396
+ it('AudioPlayer_position', 0, async () => {
397
+ const p = new FFAudioPlayer();
398
+ p.url = `file://${wav}`;
399
+ p.prepare();
400
+ await new Promise<void>((r) => setTimeout(r, 1500));
401
+ p.play();
402
+ await new Promise<void>((r) => setTimeout(r, 500));
403
+ const pos = p.currentTime;
404
+ p.pause();
405
+ console.info(`[NATIVE_TEST] AudioPlayer_position pos=${pos}`);
406
+ expect(pos).assertLargerOrEqual(0);
407
+ });
408
+ it('AudioPlayer_again_replay', 0, async () => {
409
+ const p = new FFAudioPlayer();
410
+ p.url = `file://${wav}`;
411
+ p.prepare();
412
+ await new Promise<void>((r) => setTimeout(r, 1500));
413
+ p.play();
414
+ await new Promise<void>((r) => setTimeout(r, 500));
415
+ p.seek(0);
416
+ p.play();
417
+ const playing = p.playWhenReady;
418
+ p.pause();
419
+ console.info(`[NATIVE_TEST] AudioPlayer_again_replay playing=${playing}`);
420
+ expect(playing).assertTrue();
421
+ });
422
+ ''',
423
+ "媒体格式探测(MediaRetriever)": '''
424
+ // ==== 媒体格式探测(MediaRetriever) — 可实现:FFmpeg.execute 跑 ffprobe/ffmpeg ====
425
+ it('MediaRetriever_extractMetadata', 0, async () => {
426
+ const out = await run(['ffprobe', '-v', 'info', '-of', 'json', '-show_format', '-i', wav]);
427
+ console.info(`[NATIVE_TEST] MediaRetriever_metadata len=${out.length}`);
428
+ expect(out).assertContain('format');
429
+ });
430
+ it('MediaRetriever_getFrameAtTime', 0, async () => {
431
+ await run(['ffmpeg', '-f', 'lavfi', '-i', 'color=c=red:s=64x64:d=1', `${filesDir}/gen.mp4`, '-y']);
432
+ await run(['ffmpeg', '-ss', '0', '-i', `${filesDir}/gen.mp4`, '-frames:v', '1', `${filesDir}/frame.jpg`, '-y']);
433
+ let ok = false;
434
+ try { fs.accessSync(`${filesDir}/frame.jpg`); ok = true; } catch (e) { ok = false; }
435
+ console.info(`[NATIVE_TEST] MediaRetriever_frame ok=${ok}`);
436
+ expect(ok).assertTrue();
437
+ });
438
+ it('MediaRetriever_getScaleFrameAtTime', 0, async () => {
439
+ await run(['ffmpeg', '-ss', '0', '-i', `${filesDir}/gen.mp4`, '-frames:v', '1',
440
+ '-vf', 'scale=32:32', `${filesDir}/frame_small.jpg`, '-y']);
441
+ let ok = false;
442
+ try { fs.accessSync(`${filesDir}/frame_small.jpg`); ok = true; } catch (e) { ok = false; }
443
+ console.info(`[NATIVE_TEST] MediaRetriever_scaleFrame ok=${ok}`);
444
+ expect(ok).assertTrue();
445
+ });
446
+ ''',
447
+ "视频滤镜预览(VideoPlayer)": '''
448
+ // ==== 视频滤镜预览(VideoPlayer) — 部分实现:转码可用,filter 实时预览需渲染管线 ====
449
+ it('VideoPlayer_executeTranscode', 0, async () => {
450
+ const out = `${filesDir}/t.m4a`;
451
+ await run(['ffmpeg', '-i', wav, out, '-y']);
452
+ let ok = false;
453
+ try { fs.accessSync(out); ok = true; } catch (e) { ok = false; }
454
+ console.info(`[NATIVE_TEST] VideoPlayer_transcode ok=${ok}`);
455
+ expect(ok).assertTrue();
456
+ });
457
+ ''',
458
+ "直播推流(LivePusher)": '''
459
+ // ==== 直播推流(LivePusher) — 需验证:用文件产物验证 H.264/AAC 编码 + FLV 封装 ====
460
+ it('LivePusher_check_h264_encoder', 0, async () => {
461
+ await run(['ffmpeg', '-f', 'lavfi', '-i', 'color=c=red:s=64x64:d=1', '-c:v', 'libx264', `${filesDir}/h264.mp4`, '-y']);
462
+ const ok = fileExists(`${filesDir}/h264.mp4`);
463
+ console.info(`[NATIVE_TEST] LivePusher_h264 ok=${ok}`);
464
+ expect(ok).assertTrue();
465
+ });
466
+ it('LivePusher_check_aac_encoder', 0, async () => {
467
+ await run(['ffmpeg', '-i', wav, '-c:a', 'aac', `${filesDir}/aac.m4a`, '-y']);
468
+ const ok = fileExists(`${filesDir}/aac.m4a`);
469
+ console.info(`[NATIVE_TEST] LivePusher_aac ok=${ok}`);
470
+ expect(ok).assertTrue();
471
+ });
472
+ it('LivePusher_check_flv_muxer', 0, async () => {
473
+ await run(['ffmpeg', '-f', 'lavfi', '-i', 'color=c=red:s=64x64:d=1', '-c:v', 'libx264', `${filesDir}/test.flv`, '-y']);
474
+ const ok = fileExists(`${filesDir}/test.flv`);
475
+ console.info(`[NATIVE_TEST] LivePusher_flv ok=${ok}`);
476
+ expect(ok).assertTrue();
477
+ });
478
+ ''',
479
+ }
480
+
481
+ HYPIUM_BEFOREALL = '''
482
+ export default function ffmpegVerifyTest() {
483
+ describe('FFmpegVerify', () => {
484
+ beforeAll(async () => {
485
+ const delegator = abilityDelegatorRegistry.getAbilityDelegator();
486
+ const ability = await delegator.getCurrentTopAbility();
487
+ const ctx = ability.context as common.UIAbilityContext;
488
+ filesDir = ctx.filesDir;
489
+ wav = `${filesDir}/ffmpeg_test.wav`;
490
+ const raw = await ctx.resourceManager.getRawFileContent('ffmpeg_test.wav');
491
+ const f = fs.openSync(wav, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
492
+ fs.writeSync(f.fd, raw.buffer);
493
+ fs.closeSync(f);
494
+ });
495
+ '''
496
+
497
+ # 每个功能场景 → Hypium 测试用例的期望结果(key 与 console.info 的 [NATIVE_TEST] 短名一致)
498
+ TEST_EXPECTATIONS = {
499
+ "MediaRetriever_metadata": "ffprobe 输出含 format 元数据",
500
+ "MediaRetriever_frame": "生成帧图片 frame.jpg",
501
+ "MediaRetriever_scaleFrame": "生成缩放帧图片",
502
+ "LivePusher_h264": "libx264 编码生成 H.264 文件",
503
+ "LivePusher_aac": "aac 编码生成音频文件",
504
+ "LivePusher_flv": "FLV 封装生成文件",
505
+ "FFmpegCmd_handle": "ffmpeg 命令执行生成文件",
506
+ "FFmpegCmd_handleProbe": "ffprobe 输出探测信息",
507
+ "FFmpegCmd_cancelTask": "abort 中断任务 aborted=true",
508
+ "VideoPlayer_transcode": "转码生成 m4a 文件",
509
+ "AudioPlayer_play": "播放状态 playing=true",
510
+ "AudioPlayer_duration": "时长 duration > 0",
511
+ "AudioPlayer_position": "位置 currentTime >= 0",
512
+ "AudioPlayer_again_replay": "重播后 playing=true",
513
+ }
514
+
515
+ def generate_hypium_test_file(scenarios):
516
+ """生成按功能场景组织的 Hypium 测试文件(FFmpegVerify.test.ets)。"""
517
+ lines = [HYPIUM_HEADER]
518
+ lines.append(HYPIUM_BEFOREALL)
519
+ for s in scenarios:
520
+ code = HYPIUM_SCENARIO_TESTS.get(s["feature"])
521
+ if code:
522
+ lines.append(code)
523
+ else:
524
+ v = s["verdict"]
525
+ if v == "无法实现":
526
+ lines.append(f" // ==== {s['feature']} — 无法实现:{s['reason']} ====")
527
+ lines.append(f" // unverifiable 登记:鸿蒙 so 未导出对应 NAPI 能力;{s.get('note','')}")
528
+ lines.append("")
529
+ else:
530
+ lines.append(f" // ==== {s['feature']} — {v}:无预置测试代码 ====")
531
+ lines.append("")
532
+ lines.append(" });")
533
+ lines.append("}")
534
+ return "\n".join(lines)
535
+
536
+ # ==================== 真实运行证据 ====================
537
+ def parse_evidence(log_path):
538
+ """解析运行日志 run.log,返回每个测试的结果 + 汇总。"""
539
+ if not log_path or not os.path.isfile(log_path):
540
+ return None
541
+ try:
542
+ content = open(log_path, encoding="utf-8", errors="replace").read()
543
+ except OSError:
544
+ return None
545
+ results = {}
546
+ for m in re.finditer(r"\[NATIVE_TEST\]\s+(\w+)\s+(.+)", content):
547
+ results[m.group(1)] = m.group(2).strip()
548
+ m = re.search(r"Tests run:\s*(\d+),\s*Failure:\s*(\d+),\s*Error:\s*(\d+),\s*Pass:\s*(\d+)", content)
549
+ summary = None
550
+ if m:
551
+ summary = {"run": int(m.group(1)), "failure": int(m.group(2)),
552
+ "error": int(m.group(3)), "pass": int(m.group(4))}
553
+ return {"tests": results, "summary": summary}
554
+
555
+ # ==================== NAPI 方案(Markdown)====================
556
+ def render_napi_plan_md(feature, plan, libs, jni_methods):
557
+ lines = [f"# {feature} — NAPI 实现方案", ""]
558
+ lines.append(f"> 判定:**{VERDICT_LABEL[plan['verdict']]}**")
559
+ lines.append("")
560
+ lines.append(f"- **Android 功能用途**:{plan['usage']}")
561
+ lines.append(f"- **涉及的 so/a**:{', '.join(libs) if libs else '—'}")
562
+ lines.append(f"- **判定依据**:{plan['reason']}")
563
+ lines.append("")
564
+ if plan["capabilities"]:
565
+ lines.append("## 依赖的能力(so 导出符号已证实)")
566
+ for c in plan["capabilities"]:
567
+ lines.append(f"- {c}")
568
+ lines.append("")
569
+ if plan["plan"]:
570
+ lines.append("## 实现步骤(NAPI 名经 so 符号验证)")
571
+ lines.append("")
572
+ lines.append("| 步骤 | 鸿蒙 NAPI | so 符号验证 | 用法 |")
573
+ lines.append("|------|-----------|-----------|------|")
574
+ napi_verified = plan.get("napi_verified", {})
575
+ for step, napi, code in plan["plan"]:
576
+ v = napi_verified.get(napi, "")
577
+ mark = {"declared": "✓ 符号证实", "missing": "✗ 未证实"}.get(v, "—")
578
+ lines.append(f"| {step} | `{napi}` | {mark} | `{code}` |")
579
+ lines.append("")
580
+ if plan.get("note"):
581
+ lines.append(f"## 说明")
582
+ lines.append(plan["note"])
583
+ lines.append("")
584
+ if jni_methods:
585
+ lines.append("## 对应的 Android JNI 面")
586
+ for jm in jni_methods:
587
+ lines.append(f"- `{jm['class'].split('.')[-1]}.{jm['method']}`")
588
+ return "\n".join(lines)
589
+
590
+ # ==================== HTML ====================
591
+ HTML_CSS = """<style>
592
+ body{font-family:-apple-system,"PingFang SC",sans-serif;margin:0;background:#f6f7f9;color:#1a1a2e}
593
+ .hd{background:linear-gradient(135deg,#1a2a4a,#2d4a7a);color:#fff;padding:22px 30px}
594
+ .hd h1{margin:0 0 6px;font-size:21px}
595
+ .hd .m{font-size:12px;opacity:.85}
596
+ .ct{max-width:1200px;margin:18px auto;padding:0 14px}
597
+ details{background:#fff;border-radius:10px;margin-bottom:10px;box-shadow:0 1px 3px rgba(0,0,0,.08);overflow:hidden}
598
+ summary{padding:14px 16px;cursor:pointer;font-weight:600;font-size:15px}
599
+ .bd{padding:0 16px 14px}
600
+ table{width:100%;border-collapse:collapse;font-size:13px;margin:6px 0}
601
+ th,td{text-align:left;padding:6px 10px;border-bottom:1px solid #eee;vertical-align:top}
602
+ th{background:#fafafa;font-size:12px;color:#555}
603
+ .b{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600}
604
+ .bg{background:#e6f7e6;color:#1a7a1a}.br{background:#fdeaea;color:#c0392b}.by{background:#fff7e0;color:#9a6d00}.bl{background:#e8f0fe;color:#1a56c4}
605
+ .tag{display:inline-block;margin:2px 3px 2px 0;padding:2px 8px;border-radius:4px;font-size:11px;background:#f0f2f5}
606
+ code{background:#f0f2f5;padding:1px 5px;border-radius:4px;font-size:12px;word-break:break-all;overflow-wrap:anywhere}
607
+ pre{background:#f6f8fa;padding:10px;border-radius:6px;font-size:12px;overflow-x:auto}
608
+ .mu{color:#888;font-size:12px}
609
+ .cards{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px}
610
+ .card{background:#fff;border-radius:10px;padding:12px 18px;box-shadow:0 1px 3px rgba(0,0,0,.08);min-width:110px}
611
+ .card .n{font-size:26px;font-weight:700}
612
+ .card .l{font-size:11px;color:#666}
613
+ .tabbar{display:flex;gap:6px;overflow-x:auto;margin:0 auto 14px;max-width:1200px;padding:0 14px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
614
+ .tabbar::-webkit-scrollbar{display:none}
615
+ .tabbtn{flex:0 0 auto;padding:8px 18px;border-radius:18px;border:1px solid #d0d5dd;background:#fff;font-size:13px;cursor:pointer;white-space:nowrap;color:#333;font-weight:500}
616
+ .tabbtn:hover{background:#f0f4ff}
617
+ .tabbtn.active{background:#1a56c4;color:#fff;border-color:#1a56c4}
618
+ .tab-content{display:none}
619
+ .tab-content.active{display:block}
620
+ .filterbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0 auto 14px;max-width:1200px;padding:0 14px}
621
+ .searchbox{flex:1;min-width:200px;padding:8px 14px;border-radius:18px;border:1px solid #d0d5dd;font-size:13px;outline:none}
622
+ .searchbox:focus{border-color:#1a56c4}
623
+ .filterbtn{padding:6px 14px;border-radius:16px;border:1px solid #d0d5dd;background:#fff;font-size:12px;cursor:pointer;color:#555;white-space:nowrap}
624
+ .filterbtn.active{background:#1a56c4;color:#fff;border-color:#1a56c4}
625
+ details.scene-detail.hidden{display:none}
626
+ </style>"""
627
+
628
+ HTML_JS = """<script>
629
+ function switchTab(idx) {
630
+ var btns = document.querySelectorAll('.tabbtn');
631
+ var contents = document.querySelectorAll('.tab-content');
632
+ for (var i = 0; i < btns.length; i++) {
633
+ btns[i].classList.toggle('active', i === idx);
634
+ }
635
+ for (var j = 0; j < contents.length; j++) {
636
+ contents[j].classList.toggle('active', j === idx);
637
+ }
638
+ applyFilter();
639
+ }
640
+
641
+ var currentFilter = '全部';
642
+
643
+ function setFilter(btn) {
644
+ currentFilter = btn.getAttribute('data-verdict');
645
+ var btns = document.querySelectorAll('.filterbtn');
646
+ for (var i = 0; i < btns.length; i++) {
647
+ btns[i].classList.toggle('active', btns[i].getAttribute('data-verdict') === currentFilter);
648
+ }
649
+ applyFilter();
650
+ }
651
+
652
+ function applyFilter() {
653
+ var kw = (document.getElementById('searchbox').value || '').toLowerCase();
654
+ var items = document.querySelectorAll('details.scene-detail');
655
+ for (var i = 0; i < items.length; i++) {
656
+ var it = items[i];
657
+ var verdict = it.getAttribute('data-verdict');
658
+ var text = it.textContent.toLowerCase();
659
+ var matchV = currentFilter === '全部' || verdict === currentFilter;
660
+ var matchK = !kw || text.indexOf(kw) >= 0;
661
+ it.classList.toggle('hidden', !(matchV && matchK));
662
+ }
663
+ }
664
+ </script>"""
665
+
666
+ def _verdict_badge(verdict):
667
+ return {
668
+ "可实现": '<span class="b bg">✓ 可实现</span>',
669
+ "部分实现": '<span class="b by">◐ 部分实现</span>',
670
+ "需验证": '<span class="b bl">⚠ 需验证</span>',
671
+ "无法实现": '<span class="b br">✗ 无法实现</span>',
672
+ }.get(verdict, f'<span class="b">{_e(verdict)}</span>')
673
+
674
+ def render_html(payload):
675
+ ohos_libs = payload.get("ohos_libs", {})
676
+ rules_desc = {r["name"]: r["desc"] for r in payload.get("capability_rules", [])}
677
+ lib_names = list(ohos_libs.keys())
678
+
679
+ def scenario_table(scs):
680
+ out = ['<details open><summary>📋 功能能力判定</summary><div class="bd">',
681
+ '<table><tr><th>功能场景</th><th>Android 用途</th><th>判定</th><th>已证实能力</th><th>无法实现的能力</th><th>JNI 数</th></tr>']
682
+ for s in scs:
683
+ ok_caps = " ".join(f'<span class="tag" style="background:#e6f7e6;color:#1a7a1a">{_e(c)}</span>' for c in s.get("capabilities", [])) or '<span class="mu">—</span>'
684
+ miss_caps = " ".join(f'<span class="tag" style="background:#fdeaea;color:#c0392b">{_e(c)}</span>' for c in s.get("missing_capabilities", [])) or '<span class="mu">—</span>'
685
+ out.append(f'<tr><td><b>{_e(s["feature"])}</b></td>'
686
+ f'<td>{_e(s["usage"])}</td>'
687
+ f'<td>{_verdict_badge(s["verdict"])}</td>'
688
+ f'<td>{ok_caps}</td><td>{miss_caps}</td><td>{s["total_jni"]}</td></tr>')
689
+ out.append('</table></div></details>')
690
+ return "\n".join(out)
691
+
692
+ def scenario_details(scs):
693
+ out = []
694
+ for s in scs:
695
+ out.append(f'<details class="scene-detail" data-verdict="{_e(s["verdict"])}">'
696
+ f'<summary>{_e(s["feature"])} {_verdict_badge(s["verdict"])} '
697
+ f'<span class="mu">({s["total_jni"]} JNI · {"、".join(s["libs"]) if s["libs"] else "无 so"})</span></summary><div class="bd">')
698
+ out.append(f'<p><b>判定依据</b>:{_e(s["reason"])}</p>')
699
+ if s.get("capabilities"):
700
+ out.append('<p><b>已证实能力</b>:' + " ".join(f'<span class="b bg">{_e(c)}</span>' for c in s["capabilities"]) + '</p>')
701
+ if s.get("missing_capabilities"):
702
+ out.append('<p><b>无法实现的能力</b>:' + " ".join(f'<span class="b br">{_e(c)}</span>' for c in s["missing_capabilities"]) + '</p>')
703
+ if s["plan"]:
704
+ out.append('<table><tr><th>功能步骤</th><th>鸿蒙 NAPI(so 符号验证)</th><th>实现用法</th></tr>')
705
+ napi_verified = s.get("napi_verified", {})
706
+ for step, napi, code in s["plan"]:
707
+ v = napi_verified.get(napi, "")
708
+ badge = {"declared": '<span class="b bg">✓ 符号证实</span>',
709
+ "missing": '<span class="b br">✗ 未证实</span>'}.get(v, '<span class="mu">—</span>')
710
+ out.append(f'<tr><td><b>{_e(step)}</b></td>'
711
+ f'<td><code>{_e(napi)}</code><br>{badge}</td>'
712
+ f'<td><code>{_e(code)}</code></td></tr>')
713
+ out.append('</table>')
714
+ if s.get("note"):
715
+ out.append(f'<p class="mu">说明:{_e(s["note"])}</p>')
716
+ if s["jni_methods"]:
717
+ out.append('<details><summary>对应的 Android JNI 面</summary><div class="bd"><ul>')
718
+ for jm in s["jni_methods"]:
719
+ out.append(f'<li><code>{_e(jm["class"].split(".")[-1])}.{_e(jm["method"])}</code> <span class="mu">({",".join(jm.get("libs", []))})</span></li>')
720
+ out.append('</ul></div></details>')
721
+ out.append('</div></details>')
722
+ return "\n".join(out)
723
+
724
+ def capabilities(lib_info):
725
+ lib_detected = lib_info.get("detected", {})
726
+ cxxfilt = find_cxxfilt()
727
+ out = ['<details open><summary>🧩 能力清单</summary><div class="bd">']
728
+ for r in lib_info.get("android_residue", []):
729
+ color = "#c0392b" if r.get("level") == "fail" else "#9a6d00"
730
+ out.append(f'<p class="mu" style="color:{color}">⚠ {_e(r.get("msg", ""))}</p>')
731
+ if lib_detected:
732
+ out.append('<table><tr><th>能力</th><th>命中符号(证据)</th><th>说明</th></tr>')
733
+ for cap_name, symbol in lib_detected.items():
734
+ demangled = demangle_symbol(symbol, cxxfilt)
735
+ short = demangled if len(demangled) <= 60 else demangled[:60] + "…"
736
+ out.append(f'<tr><td><b>{_e(cap_name)}</b></td>'
737
+ f'<td><code title="{_e(symbol)}">{_e(short)}</code></td>'
738
+ f'<td>{_e(rules_desc.get(cap_name, ""))}</td></tr>')
739
+ out.append('</table>')
740
+ else:
741
+ out.append('<p class="mu">未检测到已知能力</p>')
742
+ # 未识别符号前缀聚类(自动发现未纳入规则的库)
743
+ clusters = lib_info.get("unknown_clusters", [])
744
+ if clusters:
745
+ out.append('<details><summary>🔍 未纳入规则的符号前缀(可能的能力线索)</summary><div class="bd">')
746
+ tags = " ".join(
747
+ f'<span class="tag">{_e(p)}{"(" + _e(prefix_meaning(p)) + ")" if prefix_meaning(p) else ""} ×{c}</span>'
748
+ for p, c in clusters)
749
+ out.append(f'<p class="mu">{tags}</p>')
750
+ out.append('</div></details>')
751
+ out.append('</div></details>')
752
+ return "\n".join(out)
753
+
754
+ h = ['<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8">',
755
+ '<title>Android 功能 → 鸿蒙 so 能力验证报告</title>', HTML_CSS, HTML_JS,
756
+ '</head><body><div class="hd">',
757
+ '<h1>Android 功能场景 → 鸿蒙 so 能力验证报告</h1>',
758
+ '<div class="m">tool %s · 按 so/a 划分能力验证(非逐 API 映射)</div></div>' % _e(payload.get("tool_version"))]
759
+
760
+ # 全局卡片
761
+ h.append('<div class="ct"><div class="cards">')
762
+ for label, val, color in [("so/a 文件", len(lib_names), "#1a56c4")]:
763
+ h.append(f'<div class="card"><div class="n" style="color:{color}">{val}</div><div class="l">{label}</div></div>')
764
+ # 真机验证门禁卡片(通过/失败/未进行 三态)
765
+ ev = payload.get("evidence")
766
+ gate = payload.get("summary", {}).get("真机验证门禁", "未进行")
767
+ if ev and ev.get("summary"):
768
+ es = ev["summary"]
769
+ if es["pass"] == es["run"] and es["run"] > 0:
770
+ g_color, g_label = "#1a7a1a", "✅ 真机验证通过"
771
+ else:
772
+ g_color, g_label = "#c0392b", "❌ 真机验证失败"
773
+ h.append(f'<div class="card"><div class="n" style="color:{g_color}">{es["pass"]}/{es["run"]}</div>'
774
+ f'<div class="l">{g_label}</div></div>')
775
+ else:
776
+ h.append(f'<div class="card"><div class="n" style="color:#9a6d00">—</div>'
777
+ f'<div class="l">⚠ 未真机验证</div></div>')
778
+ h.append('</div>')
779
+
780
+ # Tab 栏:每个 so/a
781
+ h.append('<div class="tabbar">')
782
+ for i, name in enumerate(lib_names):
783
+ active = ' active' if i == 0 else ''
784
+ h.append(f'<button class="tabbtn{active}" onclick="switchTab({i})">📦 {_e(name)}</button>')
785
+ h.append('</div>')
786
+
787
+ # 搜索 + 筛选(作用于当前 tab 的场景详情)
788
+ h.append('<div class="filterbar">')
789
+ h.append('<input id="searchbox" class="searchbox" type="text" placeholder="搜索功能场景 / JNI 方法..." oninput="applyFilter()">')
790
+ h.append('<button class="filterbtn active" data-verdict="全部" onclick="setFilter(this)">全部</button>')
791
+ for v in ("可实现", "部分实现", "无法实现"):
792
+ h.append(f'<button class="filterbtn" data-verdict="{v}" onclick="setFilter(this)">{v}</button>')
793
+ h.append('</div>')
794
+
795
+ # 每个 so/a tab:功能判定 + 场景详情 + 能力清单
796
+ for i, name in enumerate(lib_names):
797
+ lib_info = ohos_libs[name]
798
+ lib_scenarios = lib_info.get("scenarios", [])
799
+ active = ' active' if i == 0 else ''
800
+ h.append(f'<div class="tab-content{active}">')
801
+ h.append(f'<h3 style="margin:4px 0 10px">📦 {_e(name)}</h3>')
802
+ h.append(scenario_table(lib_scenarios))
803
+ h.append(scenario_details(lib_scenarios))
804
+ h.append(capabilities(lib_info))
805
+ h.append('</div>')
806
+
807
+ # 真实运行验证证据(始终显示,合并验证)
808
+ ev = payload.get("evidence")
809
+ if ev and ev.get("tests"):
810
+ h.append('<details><summary>🔬 真实运行验证证据(Hypium 测试)</summary><div class="bd">')
811
+ h.append('<table><tr><th>测试用例</th><th>期望结果</th><th>真实结果</th></tr>')
812
+ for name, result in sorted(ev["tests"].items()):
813
+ expected = TEST_EXPECTATIONS.get(name, "—")
814
+ actual = result if len(result) <= 64 else result[:64] + "…"
815
+ h.append(f'<tr><td><code>{_e(name)}</code></td>'
816
+ f'<td>{_e(expected)}</td>'
817
+ f'<td><code>{_e(actual)}</code></td></tr>')
818
+ h.append('</table></div></details>')
819
+
820
+ h.append('</div></body></html>')
821
+ return "\n".join(h)
822
+
823
+
824
+ # ==================== 主流程 ====================
825
+ def main():
826
+ parser = argparse.ArgumentParser(description="Android 功能场景 → 鸿蒙 so/a 能力验证(按功能,非按 API)")
827
+ parser.add_argument("--android", required=True, help="Android 工程目录")
828
+ parser.add_argument("--ohos", required=True, help="鸿蒙 so/a 文件或目录")
829
+ parser.add_argument("--manifest", help="analyse 产出的 feature-manifest.json(可选,有则直接用)")
830
+ parser.add_argument("--dts", help="鸿蒙 so 的 .d.ts 声明文件(可选,自动查找)")
831
+ parser.add_argument("--output", help="输出目录(默认 <工程名>-native-reports/)")
832
+ parser.add_argument("--device", help="真机验证:设备序列号(触发编译+安装+运行+采集证据)")
833
+ parser.add_argument("--demo", help="真机验证:demo 工程目录(默认 <输出目录>/../<工程名>-verify-demo)")
834
+ parser.add_argument("--media-source", help="真机验证:提供 media 图标的工程目录(如 cppdemoHarmony)")
835
+ parser.add_argument("--json", action="store_true")
836
+ args = parser.parse_args()
837
+
838
+ android_root = os.path.abspath(args.android)
839
+ ohos_path = os.path.abspath(args.ohos)
840
+ if not os.path.isdir(android_root):
841
+ print("Android 工程不存在:", android_root)
842
+ return 2
843
+
844
+ # 收集鸿蒙 so/a
845
+ ohos_libs = {}
846
+ if os.path.isfile(ohos_path):
847
+ ohos_libs[os.path.basename(ohos_path)] = {"path": ohos_path}
848
+ else:
849
+ for dp, dns, fns in os.walk(ohos_path):
850
+ for fn in fns:
851
+ if fn.endswith((".so", ".a")) and (dp == ohos_path or "arm64" in dp or "ohos" in dp or fn in os.listdir(ohos_path)):
852
+ if fn not in ohos_libs:
853
+ ohos_libs[fn] = {"path": os.path.join(dp, fn)}
854
+
855
+ if not ohos_libs:
856
+ print("未找到鸿蒙 .so/.a 文件")
857
+ return 2
858
+
859
+ # 校验能解析 ELF 的 nm 工具(能力检测的核心依赖,缺失直接报错)
860
+ nm_tool = find_elf_nm()
861
+ if not nm_tool:
862
+ print("错误:未找到能解析 ELF 的 llvm-nm / GNU nm 工具")
863
+ print("安装方法:")
864
+ print(" ① DevEco Studio(推荐):https://developer.huawei.com/consumer/cn/deveco-studio/")
865
+ print(" (装好后脚本自动探测 /Applications/DevEco-Studio.app/Contents/sdk,或设置 OHOS_SDK_HOME / DEVECO_SDK_HOME)")
866
+ print(" ② macOS:brew install llvm")
867
+ print(" ③ Linux:sudo apt install binutils")
868
+ return 2
869
+
870
+ # readelf 用于 Android 残留检查(辅助,缺失仅告警不中断)
871
+ readelf = find_llvm_readelf()
872
+ if not readelf:
873
+ print("警告:未找到 llvm-readelf/readelf,跳过 Android 残留检查")
874
+ print(f" 安装方法:{TOOL_INSTALL_HINTS['llvm-readelf']}")
875
+
876
+ all_exports = set()
877
+ for name, info in ohos_libs.items():
878
+ exports = get_so_exports(info["path"], nm_tool)
879
+ info["exports"] = exports
880
+ info["export_count"] = len(exports)
881
+ info["is_napi"] = any("napi_" in s or s.startswith("napi") for s in exports)
882
+ info["detected"] = detect_capabilities(exports)
883
+ info["capability_count"] = len(info["detected"])
884
+ info["unknown_clusters"] = nc.cluster_unknown_symbols(exports, 15)
885
+ all_exports |= exports
886
+ issues, needed = check_android_residue(info["path"], nm_tool, readelf)
887
+ info["android_residue"] = issues
888
+ info["needed"] = needed
889
+
890
+ # 合并所有 so/a 的能力(用于功能场景判定)
891
+ detected = detect_capabilities(all_exports)
892
+ print(f"共 {len(ohos_libs)} 个 so/a,导出符号 {len(all_exports)} 个 → 合并能力 {len(detected)} 项")
893
+ for name, info in ohos_libs.items():
894
+ print(f" {name}: {info['export_count']} 符号 → {info['capability_count']} 项能力")
895
+
896
+ # .d.ts 仅作参考(可选),不作为能力判定依据
897
+ dts_path = args.dts
898
+ if not dts_path:
899
+ first_so = next(iter(ohos_libs.values()))["path"]
900
+ dts_path = find_dts_for_so(first_so)
901
+ napi_interfaces = extract_napi_from_dts(dts_path) if dts_path else {}
902
+ if dts_path:
903
+ print(f"参考 .d.ts: {dts_path}({len(napi_interfaces)} 接口,仅参考)")
904
+
905
+ # 加载功能场景
906
+ jni_to_libs = {}
907
+ if args.manifest and os.path.isfile(args.manifest):
908
+ manifest = json.load(open(args.manifest))
909
+ features = manifest.get("features", [])
910
+ for f in features:
911
+ for jm in f.get("jni_methods", []):
912
+ jni_to_libs[f'{jm.get("class","")}.{jm.get("method","")}'] = jm.get("libs", []) or f.get("involved_libs", [])
913
+ print(f"从 analyse 产物加载 {len(features)} 个功能场景")
914
+ else:
915
+ features = None
916
+ print("直接扫描 Android 源码...")
917
+
918
+ scenario_jni = {}
919
+ if features:
920
+ for f in features:
921
+ scenario_jni.setdefault(f["feature"], []).extend(f.get("jni_methods", []))
922
+ else:
923
+ for cls in scan_android_jni(android_root):
924
+ feat = cls["feature"] or "通用"
925
+ for nm in cls["native_methods"]:
926
+ scenario_jni.setdefault(feat, []).append(
927
+ {"class": cls["class"], "method": nm["method"]})
928
+
929
+ # 功能场景定义(feat_name → jni_methods + libs + plan)
930
+ scene_defs = []
931
+ for feat_name in sorted(scenario_jni):
932
+ jni_methods = scenario_jni[feat_name]
933
+ libs = sorted({l for jm in jni_methods for l in (jm.get("libs") or jni_to_libs.get(f'{jm["class"]}.{jm["method"]}', []))})
934
+ plan = match_feature_plan(feat_name)
935
+ scene_defs.append((feat_name, jni_methods, libs, plan))
936
+
937
+ # 合并所有 so/a 的能力(用于 napi-plans / demo 生成)
938
+ detected = detect_capabilities(all_exports)
939
+
940
+ # 合并场景判定(供 JSON + napi-plans + demo)
941
+ scenarios = []
942
+ for feat_name, jni_methods, libs, plan in scene_defs:
943
+ verdict, reason = judge_feature(plan.get("requires", []), detected)
944
+ scenarios.append({
945
+ "feature": feat_name, "usage": plan["usage"], "verdict": verdict, "reason": reason,
946
+ "requires": plan.get("requires", []),
947
+ "capabilities": [r for r in plan.get("requires", []) if r in detected],
948
+ "missing_capabilities": [r for r in plan.get("requires", []) if r not in detected],
949
+ "plan": plan["plan"], "napi_verified": verify_plan_napi(plan["plan"], detected),
950
+ "note": plan.get("note", ""), "total_jni": len(jni_methods),
951
+ "jni_methods": jni_methods, "libs": libs,
952
+ "hypium": generate_hypium_case(feat_name, verdict, reason),
953
+ })
954
+
955
+ # 每个 so/a 单独判定功能场景(按 so 划分,非合并能力)
956
+ for lib_name, lib_info in ohos_libs.items():
957
+ lib_detected = lib_info.get("detected", {})
958
+ lib_scenarios = []
959
+ for feat_name, jni_methods, libs, plan in scene_defs:
960
+ v, reason = judge_feature(plan.get("requires", []), lib_detected)
961
+ lib_scenarios.append({
962
+ "feature": feat_name, "usage": plan["usage"], "verdict": v, "reason": reason,
963
+ "requires": plan.get("requires", []),
964
+ "capabilities": [r for r in plan.get("requires", []) if r in lib_detected],
965
+ "missing_capabilities": [r for r in plan.get("requires", []) if r not in lib_detected],
966
+ "plan": plan["plan"], "napi_verified": verify_plan_napi(plan["plan"], lib_detected),
967
+ "note": plan.get("note", ""), "total_jni": len(jni_methods),
968
+ "jni_methods": jni_methods, "libs": libs,
969
+ })
970
+ lib_info["scenarios"] = lib_scenarios
971
+
972
+ ok = sum(1 for s in scenarios if s["verdict"] == "可实现")
973
+ partial = sum(1 for s in scenarios if s["verdict"] == "部分实现")
974
+ verify = sum(1 for s in scenarios if s["verdict"] == "需验证")
975
+ fail = len(scenarios) - ok - partial - verify
976
+
977
+ # 输出目录(提前确定,供 evidence 读取)
978
+ out_dir = os.path.abspath(args.output) if args.output else os.path.join(
979
+ os.path.dirname(android_root), os.path.basename(android_root) + "-native-reports")
980
+ os.makedirs(out_dir, exist_ok=True)
981
+
982
+ # Hypium 测试用例(提前生成,真机验证需要)
983
+ demo_dir = os.path.join(out_dir, "demo-tests")
984
+ os.makedirs(demo_dir, exist_ok=True)
985
+ test_ets = os.path.join(demo_dir, "FFmpegVerify.test.ets")
986
+ with open(test_ets, "w") as f:
987
+ f.write(generate_hypium_test_file(scenarios))
988
+
989
+ # 真机验证门禁:自动检测设备,连真机则自动触发(也可用 --device 显式指定)
990
+ device = args.device
991
+ if device in (None, "auto"):
992
+ devices = nc.list_devices()
993
+ if devices:
994
+ device = devices[0]
995
+ print(f"检测到设备 {device},自动加入真机验证门禁")
996
+ if device:
997
+ dv_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "device-verify.py")
998
+ demo_proj = args.demo or os.path.join(os.path.dirname(out_dir), os.path.basename(android_root) + "-verify-demo")
999
+ first_so = next(iter(ohos_libs.values()))["path"]
1000
+ run_log = os.path.join(out_dir, "verify-evidence", "run.log")
1001
+ cmd = [sys.executable, dv_script, "--demo", demo_proj, "--test-ets", test_ets,
1002
+ "--so", first_so, "--output", run_log]
1003
+ if device != "auto":
1004
+ cmd += ["--device", device]
1005
+ # media 图标来源:显式指定,或自动在同级目录找带 media 的鸿蒙工程
1006
+ media_source = args.media_source
1007
+ if not media_source:
1008
+ parent = os.path.dirname(android_root)
1009
+ for cand in ("cppdemoHarmony", "ProtobufDemoOhos", "CalculatorExampleOhos"):
1010
+ p = os.path.join(parent, cand)
1011
+ if os.path.isdir(os.path.join(p, "AppScope", "resources", "base", "media")):
1012
+ media_source = p
1013
+ break
1014
+ if media_source:
1015
+ cmd += ["--media-source", media_source]
1016
+ print("=" * 72)
1017
+ print("真机验证门禁:编译 demo → 签名 → 安装 → 运行 14 个 Hypium 用例 → 采集证据")
1018
+ print("=" * 72)
1019
+ subprocess.run(cmd)
1020
+ else:
1021
+ print("未检测到设备,跳过真机验证门禁(报告将标注「未真机验证」)")
1022
+
1023
+ # 真实运行证据(run.log)
1024
+ evidence = parse_evidence(os.path.join(out_dir, "verify-evidence", "run.log"))
1025
+
1026
+ # 真机验证门禁状态
1027
+ device_gate = "未进行"
1028
+ if evidence and evidence.get("summary"):
1029
+ es = evidence["summary"]
1030
+ if es["pass"] == es["run"] and es["run"] > 0:
1031
+ device_gate = f"通过({es['pass']}/{es['run']})"
1032
+ else:
1033
+ device_gate = f"失败({es['pass']}/{es['run']})"
1034
+
1035
+ payload = {
1036
+ "tool_version": __version__,
1037
+ "android_root": android_root,
1038
+ "ohos_libs": {k: {"path": v["path"], "is_napi": v.get("is_napi"),
1039
+ "export_count": v.get("export_count", 0),
1040
+ "detected": v.get("detected", {}),
1041
+ "capability_count": v.get("capability_count", 0),
1042
+ "scenarios": v.get("scenarios", []),
1043
+ "unknown_clusters": v.get("unknown_clusters", []),
1044
+ "android_residue": v.get("android_residue", [])} for k, v in ohos_libs.items()},
1045
+ "dts_path": dts_path,
1046
+ "napi_interface_count": len(napi_interfaces),
1047
+ "so_export_count": len(all_exports),
1048
+ "detected_capabilities": detected,
1049
+ "capability_rules": [{"name": n, "pattern": p, "desc": d} for n, p, d in CAPABILITY_RULES],
1050
+ "scenarios": scenarios,
1051
+ "evidence": evidence,
1052
+ "summary": {
1053
+ "total_scenarios": len(scenarios),
1054
+ "可实现": ok, "部分实现": partial, "需验证": verify, "无法实现": fail,
1055
+ "真机验证门禁": device_gate,
1056
+ },
1057
+ }
1058
+
1059
+ # 输出
1060
+ os.makedirs(out_dir, exist_ok=True)
1061
+
1062
+ json_path = os.path.join(out_dir, "capability-report.json")
1063
+ with open(json_path, "w") as f:
1064
+ json.dump(payload, f, ensure_ascii=False, indent=2)
1065
+
1066
+ html_path = os.path.join(out_dir, "capability-report.html")
1067
+ with open(html_path, "w") as f:
1068
+ f.write(render_html(payload))
1069
+
1070
+ # 每场景 NAPI 方案 .md
1071
+ plans_dir = os.path.join(out_dir, "napi-plans")
1072
+ os.makedirs(plans_dir, exist_ok=True)
1073
+ for s in scenarios:
1074
+ md = render_napi_plan_md(s["feature"], s, s["libs"], s["jni_methods"])
1075
+ fname = re.sub(r"[^\w\u4e00-\u9fff]+", "-", s["feature"]).strip("-") + ".md"
1076
+ with open(os.path.join(plans_dir, fname), "w") as f:
1077
+ f.write(md)
1078
+
1079
+ # 控制台摘要
1080
+ print("=" * 72)
1081
+ print("Android 功能场景 → 鸿蒙 so/a 能力验证 (tool %s)" % __version__)
1082
+ print("功能场景: %d | ✓ 可实现: %d | ◐ 部分: %d | ⚠ 需验证: %d | ✗ 无法实现: %d" % (
1083
+ len(scenarios), ok, partial, verify, fail))
1084
+ print("JSON: %s" % json_path)
1085
+ print("HTML: %s" % html_path)
1086
+ print("NAPI 方案: %s/" % plans_dir)
1087
+ print("Hypium 测试: %s" % test_ets)
1088
+ print("=" * 72)
1089
+ for s in scenarios:
1090
+ print(f" {VERDICT_LABEL[s['verdict']]:<10} {s['feature']} ({s['total_jni']} JNI)")
1091
+ print("=" * 72)
1092
+
1093
+ if args.json:
1094
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
1095
+ return 0
1096
+
1097
+
1098
+ if __name__ == "__main__":
1099
+ sys.exit(main())