android2harmony 0.1.6 → 0.1.8

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 (227) hide show
  1. package/agents/api-analyzer.md +113 -0
  2. package/agents/app-action.md +122 -0
  3. package/agents/code-reviewer.md +191 -295
  4. package/agents/coder.md +377 -0
  5. package/agents/self-tester.md +8 -8
  6. package/agents/spec-designer.md +56 -0
  7. package/package.json +1 -1
  8. package/skills/a2h-app-req-gen/SKILL.md +174 -0
  9. package/skills/a2h-app-req-gen/references/spec-handoff.md +99 -0
  10. package/skills/a2h-app-req-gen/references/subagent-render.md +115 -0
  11. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/build.gradle.kts +41 -0
  12. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.jar +0 -0
  13. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.properties +8 -0
  14. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle.properties +3 -0
  15. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew +251 -0
  16. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew.bat +94 -0
  17. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/settings.gradle.kts +17 -0
  18. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawAnalysisDetector.java +1911 -0
  19. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawIssueRegistry.java +37 -0
  20. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawJson.java +146 -0
  21. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawLocation.java +96 -0
  22. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawRecordWriter.java +227 -0
  23. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +1 -0
  24. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/dev/reqgen/analyzer/jvm/raw-record-v1.schema.json +93 -0
  25. package/skills/a2h-app-req-gen/scripts/reqgen/assets/dashboard.html +151 -0
  26. package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-lint.init.gradle +60 -0
  27. package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-model.init.gradle +204 -0
  28. package/skills/a2h-app-req-gen/scripts/reqgen/package.json +14 -0
  29. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/backend.js +323 -0
  30. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/cli.js +811 -0
  31. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/gap-audit-link.js +62 -0
  32. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/index.js +66 -0
  33. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/merge.js +605 -0
  34. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/name-trace.js +153 -0
  35. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/orchestrate.js +1324 -0
  36. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/output-parse.js +50 -0
  37. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/overview.js +10 -0
  38. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/pool.js +23 -0
  39. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/post-merge-fix.js +146 -0
  40. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt-loader.js +51 -0
  41. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt.js +967 -0
  42. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompts.md +301 -0
  43. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/serve.js +78 -0
  44. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/short-id.js +46 -0
  45. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/snapshot.js +267 -0
  46. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/types.js +1 -0
  47. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/validate.js +122 -0
  48. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/callback-call-sites.js +62 -0
  49. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/build.js +1291 -0
  50. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/index.js +1 -0
  51. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/area-ownership.js +392 -0
  52. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/build.js +1995 -0
  53. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/callback-flow.js +277 -0
  54. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/component-variants.js +725 -0
  55. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/index.js +15 -0
  56. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/index.js +12 -0
  57. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/policy.js +469 -0
  58. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/traceability.js +468 -0
  59. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/build.js +1066 -0
  60. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/index.js +1 -0
  61. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android-interaction-registry.js +174 -0
  62. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android.js +1062 -0
  63. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/area-titles.js +678 -0
  64. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/builder.js +350 -0
  65. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-api-registry.js +395 -0
  66. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-call-sites.js +2008 -0
  67. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose.js +9059 -0
  68. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/dynamic-repeated-call-sites.js +529 -0
  69. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/index.js +1 -0
  70. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/interaction-effects.js +1149 -0
  71. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/jvm.js +6272 -0
  72. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/normalize.js +33 -0
  73. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/pending-intents.js +565 -0
  74. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/resource-id.js +42 -0
  75. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/route-state-variants.js +781 -0
  76. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/state.js +234 -0
  77. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/types.js +1 -0
  78. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/extract.js +40 -0
  79. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/index.js +6 -0
  80. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/manifest.js +513 -0
  81. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/resources.js +398 -0
  82. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/types.js +1 -0
  83. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/views.js +447 -0
  84. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/xml-parser.js +175 -0
  85. package/skills/a2h-app-req-gen/scripts/reqgen/src/cli.js +398 -0
  86. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/area-graph.js +1 -0
  87. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/artifact.js +1 -0
  88. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/common.js +16 -0
  89. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/diagnostic.js +10 -0
  90. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/gap-audit.js +14 -0
  91. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/index.js +11 -0
  92. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/name-traceability.js +26 -0
  93. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/raw-fact.js +40 -0
  94. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/req-model.js +1 -0
  95. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/run.js +19 -0
  96. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/semantic-completeness.js +11 -0
  97. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/source-facts.js +1 -0
  98. package/skills/a2h-app-req-gen/scripts/reqgen/src/core/output-guard.js +41 -0
  99. package/skills/a2h-app-req-gen/scripts/reqgen/src/core/stable-id.js +92 -0
  100. package/skills/a2h-app-req-gen/scripts/reqgen/src/dashboard-cli.js +169 -0
  101. package/skills/a2h-app-req-gen/scripts/reqgen/src/diagnostics.js +104 -0
  102. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/dump.js +1028 -0
  103. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/index.js +1 -0
  104. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump-cli.js +251 -0
  105. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/artifacts.js +49 -0
  106. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/index.js +18 -0
  107. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/quality-report.js +125 -0
  108. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/render.js +353 -0
  109. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/types.js +2 -0
  110. package/skills/a2h-app-req-gen/scripts/reqgen/src/graph/paths.js +474 -0
  111. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/index.js +15 -0
  112. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/protocol.js +408 -0
  113. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/runner.js +452 -0
  114. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/index.js +8 -0
  115. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/output.js +165 -0
  116. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/run-stage1.js +891 -0
  117. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/discovery.js +299 -0
  118. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-model.js +358 -0
  119. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-runner.js +165 -0
  120. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/resource-profile.js +60 -0
  121. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/types.js +1 -0
  122. package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff-cli.js +151 -0
  123. package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff.js +1004 -0
  124. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-cli.js +109 -0
  125. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-input.js +419 -0
  126. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-name-trace-input.js +95 -0
  127. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/area-graph.js +444 -0
  128. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/index.js +7 -0
  129. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-model.js +580 -0
  130. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-text.js +195 -0
  131. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/semantic-area-graph.js +1431 -0
  132. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/types.js +1 -0
  133. package/skills/a2h-app-req-gen/scripts/reqgen-audit.mjs +298 -0
  134. package/skills/a2h-app-req-gen/scripts/reqgen-selfcheck.mjs +555 -0
  135. package/skills/a2h-code-review/SKILL.md +379 -0
  136. package/skills/{hmos-integration-test → a2h-integration-test}/README.md +6 -6
  137. package/skills/{hmos-integration-test → a2h-integration-test}/SKILL.md +6 -6
  138. package/skills/{hmos-integration-test → a2h-integration-test}/scripts/report-tool.mjs +1 -1
  139. package/skills/a2h-spec-design/SKILL.md +330 -0
  140. package/skills/a2h-spec-design/scripts/inspect_spec_design_inputs.mjs +207 -0
  141. package/skills/a2h-spec-design/scripts/move_spec_technical_reference.mjs +198 -0
  142. package/skills/a2h-spec-design/scripts/validate_spec_design_output.mjs +256 -0
  143. package/skills/a2h-spec-generate/SKILL.md +696 -0
  144. package/skills/a2h-spec-generate/references/trace-template.md +115 -0
  145. package/skills/a2h-spec-generate/scripts/parse_requirements.mjs +537 -0
  146. package/skills/a2h-spec-generate/template/REQ.txt +22 -0
  147. package/skills/a2h-spec-generate/template/REQ.xlsx +0 -0
  148. package/skills/a2h-spec-implement/SKILL.md +548 -0
  149. package/skills/a2h-spec-implement/references/harmony-pitfalls.md +186 -0
  150. package/skills/a2h-spec-implement/references/l1-unit-test.md +185 -0
  151. package/skills/a2h-spec-implement/references/l3-e2e-uitest.md +167 -0
  152. package/skills/a2h-spec-implement/references/test-core.md +132 -0
  153. package/skills/a2h-spec-implement/rules/arkts/arkts-standards.md +184 -0
  154. package/skills/a2h-spec-implement/rules/arkts/conventions/coding-style.md +96 -0
  155. package/skills/a2h-spec-implement/rules/arkts/conventions/security.md +126 -0
  156. package/skills/a2h-spec-implement/rules/arkts/language/arkts-rules.md +650 -0
  157. package/skills/a2h-spec-implement/rules/arkts/language/arkui-structure-rules.md +173 -0
  158. package/skills/a2h-spec-implement/rules/arkts/language/ts-to-arkts-rewrites.md +58 -0
  159. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md +648 -0
  160. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md +2089 -0
  161. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md +1033 -0
  162. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md +1183 -0
  163. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md +576 -0
  164. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md +297 -0
  165. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md +395 -0
  166. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md +903 -0
  167. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
  168. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md +1178 -0
  169. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
  170. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +355 -0
  171. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md +11 -0
  172. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md +216 -0
  173. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md +442 -0
  174. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md +169 -0
  175. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md +763 -0
  176. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md +2088 -0
  177. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md +1258 -0
  178. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Once/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md +175 -0
  179. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Param/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md +850 -0
  180. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md +862 -0
  181. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md +110 -0
  182. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md +301 -0
  183. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md +1411 -0
  184. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +1392 -0
  185. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/getTarget/346/216/245/345/217/243/357/274/232/350/216/267/345/217/226/347/212/266/346/200/201/347/256/241/347/220/206/346/241/206/346/236/266/344/273/243/347/220/206/345/211/215/347/232/204/345/216/237/345/247/213/345/257/271/350/261/241.md +288 -0
  186. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/makeObserved/346/216/245/345/217/243/357/274/232/345/260/206/351/235/236/350/247/202/345/257/237/346/225/260/346/215/256/345/217/230/344/270/272/345/217/257/350/247/202/345/257/237/346/225/260/346/215/256.md +768 -0
  187. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/222/214V2/346/267/267/347/224/250/346/214/207/345/257/274/357/274/210API version 19/345/217/212/344/271/213/345/220/216/357/274/211.md" +829 -0
  188. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206/346/246/202/350/277/260.md +184 -0
  189. package/skills/a2h-spec-implement/rules/arkts/ui/component-cookbook.md +431 -0
  190. package/skills/a2h-spec-implement/rules/arkts/ui/state-management.md +152 -0
  191. package/skills/a2h-spec-implement/rules/arkts/ui/ui-quality.md +67 -0
  192. package/skills/a2h-spec-implement/rules/arkts/ui/ui-runtime-diagnosis.md +32 -0
  193. package/skills/a2h-spec-implement/rules/pipeline/build/package-set-collection.md +43 -0
  194. package/skills/a2h-spec-implement/scripts/_lib/common.mjs +97 -0
  195. package/skills/a2h-spec-implement/scripts/_lib/config.mjs +70 -0
  196. package/skills/a2h-spec-implement/scripts/_lib/lessons_cli.mjs +48 -0
  197. package/skills/a2h-spec-implement/scripts/build_lessons.mjs +24 -0
  198. package/skills/a2h-spec-implement/scripts/code_pattern_lessons.mjs +26 -0
  199. package/skills/a2h-spec-implement/scripts/device_ui.mjs +433 -0
  200. package/skills/a2h-spec-implement/scripts/device_ui_core.mjs +895 -0
  201. package/skills/a2h-spec-implement/scripts/ledger/aggregate_run.mjs +207 -0
  202. package/skills/a2h-spec-implement/scripts/manifest/emit_summary_line.mjs +37 -0
  203. package/skills/a2h-spec-implement/scripts/manifest/finalize_run.mjs +115 -0
  204. package/skills/a2h-spec-implement/scripts/manifest/render_final_summary.mjs +78 -0
  205. package/skills/a2h-spec-implement/scripts/manifest/root_mirror.mjs +137 -0
  206. package/skills/a2h-spec-implement/scripts/package.json +12 -0
  207. package/skills/a2h-spec-implement/scripts/state_propagation_check.mjs +676 -0
  208. package/skills/a2h-spec-implement/scripts/test/l2_run.mjs +244 -0
  209. package/skills/a2h-spec-implement/scripts/test/tp_coverage.mjs +231 -0
  210. package/skills/a2h-spec-implement/scripts/ui_memory.mjs +161 -0
  211. package/skills/a2h-spec-implement/scripts/utils/ensure_app.mjs +134 -0
  212. package/skills/a2h-ui-transfer/SKILL.md +20 -9
  213. package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +80 -0
  214. package/skills/hmos-convert-pipeline/SKILL.md +7 -5
  215. package/skills/hmos-incremental-ui-align/README.md +7 -7
  216. package/skills/hmos-incremental-ui-align/SKILL.md +1 -1
  217. package/skills/hmos-incremental-ui-align/page_align.md +1 -1
  218. package/skills/hmos-spec-generate/SKILL.md +2 -2
  219. package/skills/hmos-spec-generate/scripts/parse_requirements.mjs +537 -0
  220. package/skills/hmos-fix-build-errors/SKILL.md +0 -266
  221. package/skills/hmos-fix-build-errors/references/arkts-strict-patterns.md +0 -219
  222. package/skills/hmos-fix-build-errors/references/known-patterns.md +0 -157
  223. package/skills/hmos-fix-build-errors/references/rdb-entity-pattern.md +0 -131
  224. package/skills/hmos-spec-generate/scripts/parse_requirements.ts +0 -515
  225. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/resolve-metadata-tool.mjs +0 -0
  226. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/self-test-runner.mjs +0 -0
  227. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/testcases-tool.mjs +0 -0
@@ -0,0 +1,895 @@
1
+ // device_ui_core.mjs — HarmonyOS device UI driver core (hypium-driver backend, single shared implementation).
2
+ //
3
+ // All entry points share this module; logic is written once:
4
+ // device_ui.mjs CLI (one-shot + serve daemon): args + single-line JSON stdout contract
5
+ // test/l2_run.mjs L2 e2e runner: in-process calls (no per-step subprocess round-trip)
6
+ // All intelligence is host-side (dump JSON → active layer → selector match → coordinate action);
7
+ // hypium-driver is a dumb "fetch tree + inject" channel; the device-side agent is the official uitest channel.
8
+ //
9
+ // No-console-window rule: every spawn/spawnSync uses windowsHide:true; the daemon stays resident with a
10
+ // hidden console (CREATE_NO_WINDOW semantics), inherited by its children (hdc etc.) — no cmd popups, ever.
11
+ import { spawnSync } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
+ import { getKey } from "./_lib/config.mjs";
17
+ import { HMOS_SKIP_TYPES as SKIP_TYPES, HMOS_SKIP_BUNDLES as SKIP_BUNDLES } from "./_lib/common.mjs";
18
+
19
+ // --------------------------------------------------------------------------
20
+ // stdout discipline: hypium-driver may log to stdout, polluting the single-line JSON contract.
21
+ // Entry processes call captureStdout() before importing the driver: real stdout is reserved for emit; everything else is rerouted to stderr.
22
+ // --------------------------------------------------------------------------
23
+ let _realWrite = null;
24
+ export function captureStdout() {
25
+ if (_realWrite) return;
26
+ _realWrite = process.stdout.write.bind(process.stdout);
27
+ process.stdout.write = (chunk, enc, cb) => process.stderr.write(chunk, enc, cb);
28
+ console.log = (...a) => console.error(...a);
29
+ }
30
+ export function writeStdoutLine(s) {
31
+ (_realWrite || process.stdout.write.bind(process.stdout))(s + "\n");
32
+ }
33
+ export function warn(msg) {
34
+ try { process.stderr.write(`[device_ui] ${msg}\n`); } catch { /* ignore */ }
35
+ }
36
+
37
+ export class DeviceUIError extends Error {
38
+ // Expected selector/argument errors: one-shot maps to fail(), daemon/l3 to an error dict (process keeps running)
39
+ constructor(reason, extra = {}) { super(reason); this.reason = reason; this.extra = extra; }
40
+ }
41
+
42
+ // --------------------------------------------------------------------------
43
+ // hdc lookup and invocation: prefer PATH, else derive from conf.yaml DEVECO_PATH/DEVECO_SDK_HOME.
44
+ // The driver talks to the hdc server (TCP 8710) instead of spawning hdc; one `hdc list targets`
45
+ // run both locates the binary and starts the hdc server — called before connect.
46
+ // --------------------------------------------------------------------------
47
+ const HDC_NAMES = os.platform() === "win32" ? ["hdc.exe", "hdc"] : ["hdc"];
48
+ function dirHasHdc(d) {
49
+ try { return HDC_NAMES.some((n) => fs.statSync(path.join(d, n)).isFile()); } catch { return false; }
50
+ }
51
+ export function ensureHdcOnPath() {
52
+ const sep = path.delimiter;
53
+ for (const d of (process.env.PATH || "").split(sep)) if (d && dirHasHdc(d)) return true;
54
+ const deveco = getKey("DEVECO_PATH", "");
55
+ const sdk = getKey("DEVECO_SDK_HOME", "") || (deveco ? path.join(deveco, "sdk") : "");
56
+ const cands = [];
57
+ if (sdk) {
58
+ try {
59
+ for (const sub of fs.readdirSync(sdk)) cands.push(path.join(sdk, sub, "openharmony", "toolchains"));
60
+ } catch { /* sdk missing */ }
61
+ cands.push(path.join(sdk, "openharmony", "toolchains"));
62
+ }
63
+ if (deveco) cands.push(path.join(deveco, "tools", "hdc"));
64
+ for (const d of cands.sort()) {
65
+ if (dirHasHdc(d)) { process.env.PATH = d + sep + (process.env.PATH || ""); return true; }
66
+ }
67
+ return false;
68
+ }
69
+ export function hdcRun(device, args, timeoutMs = 15000) {
70
+ // Every hdc subprocess goes through here: windowsHide pinned (no-console-window rule)
71
+ const full = [...(device ? ["-t", device] : []), ...args];
72
+ return spawnSync("hdc", full, {
73
+ encoding: "utf8", timeout: timeoutMs, windowsHide: true,
74
+ });
75
+ }
76
+
77
+ // --------------------------------------------------------------------------
78
+ // view tree parsing (dumpLayout emits JSON)
79
+ // --------------------------------------------------------------------------
80
+ // SKIP_TYPES / SKIP_BUNDLES (HarmonyOS system-UI denylist) are single-sourced in _lib/common.mjs,
81
+ // imported above — edit the denylist there, not here.
82
+
83
+ export function parseXyBounds(s) {
84
+ const nums = (s || "").match(/-?\d+/g);
85
+ if (!nums || nums.length !== 4) return null;
86
+ const [x1, y1, x2, y2] = nums.map(Number);
87
+ return [[x1, y1, x2, y2], [Math.floor((x1 + x2) / 2), Math.floor((y1 + y2) / 2)]];
88
+ }
89
+
90
+ export function collectElements(tree, includeAll, includeBound = false) {
91
+ const out = [];
92
+ const walk = (node) => {
93
+ const a = node.attributes || {};
94
+ const t = a.type || "", bundle = a.bundleName || "";
95
+ if (SKIP_TYPES.has(t) || SKIP_BUNDLES.has(bundle)) {
96
+ for (const c of node.children || []) walk(c);
97
+ return;
98
+ }
99
+ const bc = parseXyBounds(a.bounds || "");
100
+ const text = a.text || "", desc = a.description || "", key = a.key || "";
101
+ const clickable = a.clickable === "true";
102
+ const scrollable = a.scrollable === "true";
103
+ if (includeAll || text || desc || clickable || scrollable) {
104
+ const e = { type: t, text, center: bc ? bc[1] : null, clickable };
105
+ if (includeBound && bc) e.bound = bc[0]; // [x1,y1,x2,y2] exact pixel box (for geometry comparators)
106
+ if (desc) e.desc = desc;
107
+ if (key) e.key = key;
108
+ if (scrollable) e.scrollable = true;
109
+ // State attributes, sparsely exposed: only non-default values appear (saves tokens) — fills assertion blind spots
110
+ if (a.checked === "true") e.checked = true;
111
+ if (a.selected === "true") e.selected = true;
112
+ if (a.focused === "true") e.focused = true;
113
+ if (a.enabled === "false") e.enabled = false;
114
+ out.push(e);
115
+ }
116
+ for (const c of node.children || []) walk(c);
117
+ };
118
+ walk(tree);
119
+ return out;
120
+ }
121
+
122
+ // Interactive overlays (Dialog/Sheet/Popup/Menu). ArkUI overlay stack: push appends as the last sibling under root; last-in shows on top
123
+ const OVERLAY_TYPES = ["Dialog", "Sheet", "Popup", "Menu"];
124
+
125
+ export function resolveActiveLayer(tree) {
126
+ // Active surface = topmost overlay subtree (last in document order, no overlay ancestor); else the whole page
127
+ const tops = [];
128
+ const walk = (node, hasAnc) => {
129
+ const t = (node.attributes || {}).type || "";
130
+ const isOverlay = OVERLAY_TYPES.some((k) => t.includes(k));
131
+ if (isOverlay && !hasAnc) tops.push(node);
132
+ for (const c of node.children || []) walk(c, hasAnc || isOverlay);
133
+ };
134
+ walk(tree, false);
135
+ return tops.length ? [tops[tops.length - 1], tops.length] : [tree, 0];
136
+ }
137
+
138
+ // --------------------------------------------------------------------------
139
+ // Selectors (host-side matching; single source of truth shared with observe)
140
+ // Precedence: exact text > contains substring > regex > starts > ends > key > desc > type
141
+ // --------------------------------------------------------------------------
142
+ const SELECTOR_KEYS = ["text", "contains", "regex", "starts", "ends", "key", "desc", "type"];
143
+
144
+ export function hasSelector(opts) {
145
+ return SELECTOR_KEYS.some((k) => opts[k] != null);
146
+ }
147
+ export function selectorDesc(opts) {
148
+ for (const k of SELECTOR_KEYS) if (opts[k] != null) return `${k}=${JSON.stringify(opts[k])}`;
149
+ return null;
150
+ }
151
+ export function matches(e, opts) {
152
+ const txt = e.text || "";
153
+ if (opts.text != null) return txt === opts.text;
154
+ if (opts.contains != null) return txt.includes(opts.contains);
155
+ if (opts.regex != null) { try { return new RegExp(opts.regex).test(txt); } catch { throw new DeviceUIError(`非法正则: ${opts.regex}`); } }
156
+ if (opts.starts != null) return txt.startsWith(opts.starts);
157
+ if (opts.ends != null) return txt.endsWith(opts.ends);
158
+ if (opts.key != null) return (e.key || "") === opts.key;
159
+ if (opts.desc != null) return (e.desc || "") === opts.desc;
160
+ if (opts.type != null) return (e.type || "") === opts.type;
161
+ return true;
162
+ }
163
+
164
+ async function dumpTree(d) {
165
+ // Dump the current ui-tree to a temp file, parse and return (for tap/input resolution, not persisted; observe writes its own ui_tree.json)
166
+ const p = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "uitree_")), "tree.json");
167
+ try {
168
+ await d.dumpLayout(p);
169
+ return JSON.parse(fs.readFileSync(p, "utf8"));
170
+ } finally {
171
+ try { fs.rmSync(path.dirname(p), { recursive: true, force: true }); } catch { /* ignore */ }
172
+ }
173
+ }
174
+
175
+ export async function resolveOne(d, opts, action, includeBound = false) {
176
+ // Selector → single target element (with center).
177
+ // Native fast path (~43ms vs ~900ms full host tree dump, 30x; closes on-device, no host tree walk):
178
+ // a unique findComponents hit yields the center directly. 0/≥2 hits/desc/--nth/bound needed →
179
+ // fall back to the authoritative host path (miss confirmation, active-layer disambiguation +
180
+ // ghost filtering, bound extraction) — most of the win without losing correctness.
181
+ // The narrow edge case (unique hit on a background ghost under an overlay) is caught by the pipeline's per-step expect gate.
182
+ const desc = selectorDesc(opts);
183
+ if (desc == null) throw new DeviceUIError(`${action} 需要选择器(--text/--contains/--regex/--starts/--ends/--key/--desc/--type)或 --xy`);
184
+ if (!includeBound && opts.desc == null && opts.nth == null) {
185
+ try {
186
+ const comps = (await d.findComponents(byFromSelector(d.__pkg, opts))) || [];
187
+ if (comps.length === 1) {
188
+ const c = await comps[0].getBoundsCenter();
189
+ return [{ center: [c.x ?? c.width, c.y ?? c.height] }, desc];
190
+ }
191
+ // 0 or ≥2 → fall back to host (authoritative: confirm miss / active-layer disambiguation / ghost filtering / list candidates)
192
+ } catch (e) { if (e instanceof DeviceUIError) { /* byFromSelector rejects desc; fall back to host */ } /* native RPC errors also fall back to host */ }
193
+ }
194
+ const [scope] = resolveActiveLayer(await dumpTree(d));
195
+ const cands = collectElements(scope, true, includeBound).filter((e) => matches(e, opts) && e.center);
196
+ if (!cands.length) {
197
+ throw new DeviceUIError(`选择器未命中(活跃层): ${desc}`, {
198
+ hint: "先 observe 看屏幕,或换选择器/用 --xy(--raw observe 看全树)" });
199
+ }
200
+ if (cands.length > 1 && opts.nth == null) {
201
+ const info = cands.slice(0, 10).map((c) => ({ text: c.text, center: c.center, clickable: c.clickable }));
202
+ throw new DeviceUIError(`命中 ${cands.length} 个: ${desc}`, { candidates: info, hint: "加 --nth N(0 起)" });
203
+ }
204
+ const idx = opts.nth || 0;
205
+ if (idx >= cands.length) throw new DeviceUIError(`--nth ${idx} 越界,仅 ${cands.length} 个命中`);
206
+ return [cands[idx], desc];
207
+ }
208
+
209
+ // --------------------------------------------------------------------------
210
+ // hypium-driver loading (lazy: pure-hdc commands like devices/density don't need it)
211
+ //
212
+ // Resolution order mirrors a2h-integration-test's batch_runner lookup:
213
+ // 1. bare specifier — repo dev copy, after `npm install` under scripts/
214
+ // 2. global npm root — `<root>/hypium-driver`, imported by absolute file URL
215
+ // (a bare specifier only walks up node_modules from this file, so it can
216
+ // never see a global install)
217
+ // 3. `npm install -g hypium-driver@<range from scripts/package.json>`, then retry 2
218
+ //
219
+ // Step 3 is what makes the plugin edition work at all: shipped inside DevEco Code the
220
+ // scripts live under node_modules/@deveco-test/deveco-code/vendor/android2harmony/skills/…,
221
+ // where there is no node_modules ancestor to resolve from and no sane place to
222
+ // `npm install` in situ (a reinstall of the host wipes it). The plugin therefore keeps
223
+ // `dependencies` empty and the driver self-heals into the global root at first use.
224
+ // --------------------------------------------------------------------------
225
+ const DRIVER_PKG = "hypium-driver";
226
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
227
+
228
+ // Global npm root(s) where `npm install -g` places packages.
229
+ let _globalRootsCache = null;
230
+ function globalNodeModulesRoots() {
231
+ if (_globalRootsCache) return _globalRootsCache;
232
+ const roots = new Set();
233
+ try {
234
+ const r = spawnSync("npm", ["root", "-g"], { encoding: "utf-8", timeout: 15000, windowsHide: true, shell: true });
235
+ const p = (r.stdout || "").trim();
236
+ if (r.status === 0 && p) roots.add(p);
237
+ } catch { /* npm not on PATH or timed out — fall through to the heuristics */ }
238
+ // Heuristic fallbacks in case `npm root -g` is unavailable.
239
+ const home = os.homedir();
240
+ if (process.platform === "win32") {
241
+ if (process.env.APPDATA) roots.add(path.join(process.env.APPDATA, "npm", "node_modules"));
242
+ } else {
243
+ roots.add("/usr/local/lib/node_modules");
244
+ roots.add("/usr/lib/node_modules");
245
+ roots.add(path.join(home, ".npm-global", "lib", "node_modules"));
246
+ }
247
+ _globalRootsCache = [...roots].filter(Boolean);
248
+ return _globalRootsCache;
249
+ }
250
+
251
+ // The pinned range lives in scripts/package.json — sole source of truth, so the
252
+ // auto-install can never drift to a `latest` that lacks a fix we depend on.
253
+ //
254
+ // The version reaches the command line through `shell: true` (unavoidable: since
255
+ // CVE-2024-27980 Node refuses to spawn a `.cmd` — which is what npm is on Windows —
256
+ // without a shell), so cmd.exe gets a crack at it: `^` is cmd's escape character and
257
+ // `|` / `&` in a range like `^6 || ^7` would be read as operators. Only a bare semver
258
+ // is therefore allowed through; anything else degrades to the unversioned package name
259
+ // (npm resolves `latest`) rather than handing metacharacters to the shell.
260
+ const SEMVER_EXACT = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
261
+ function driverInstallSpec() {
262
+ let range = "";
263
+ try {
264
+ const pkg = JSON.parse(fs.readFileSync(path.join(HERE, "package.json"), "utf-8"));
265
+ range = ((pkg.dependencies && pkg.dependencies[DRIVER_PKG]) || "").trim();
266
+ } catch { /* unreadable package.json — install unversioned */ }
267
+ // `^6.1.210` / `~6.1.210` / `=6.1.210` → `6.1.210`; a real multi-comparator range → dropped.
268
+ const pinned = range.replace(/^[\^~=v\s]+/, "");
269
+ return SEMVER_EXACT.test(pinned) ? `${DRIVER_PKG}@${pinned}` : DRIVER_PKG;
270
+ }
271
+
272
+ // Absolute entry file of a package dir, per its package.json (exports["."] → main → index.js).
273
+ function packageEntry(dir) {
274
+ let pkg;
275
+ try { pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8")); } catch { return ""; }
276
+ const pick = (v) => {
277
+ if (typeof v === "string") return v;
278
+ if (v && typeof v === "object") return pick(v.import || v.module || v.require || v.default || v.node);
279
+ return "";
280
+ };
281
+ const rel = pick(pkg.exports && pkg.exports["."] !== undefined ? pkg.exports["."] : pkg.exports) || pkg.main || "index.js";
282
+ const abs = path.join(dir, rel);
283
+ return fs.existsSync(abs) ? abs : "";
284
+ }
285
+
286
+ function resolveDriverEntry() {
287
+ for (const root of globalNodeModulesRoots()) {
288
+ const entry = packageEntry(path.join(root, DRIVER_PKG));
289
+ if (entry) return entry;
290
+ }
291
+ return "";
292
+ }
293
+
294
+ function installDriverGlobally() {
295
+ const spec = driverInstallSpec();
296
+ warn(`hypium-driver 未找到,自动安装(npm install -g ${spec})...`);
297
+ try {
298
+ const r = spawnSync("npm", ["install", "-g", spec], {
299
+ encoding: "utf-8", timeout: 180000, windowsHide: true, shell: true, stdio: ["ignore", "pipe", "pipe"],
300
+ });
301
+ if (r.status === 0) { warn(`hypium-driver 安装完成: ${spec}`); return { ok: true, detail: "" }; }
302
+ const tail = ((r.stderr || "") + (r.stdout || "")).trim().slice(-400);
303
+ warn(`npm install 退出码=${r.status}: ${tail}`);
304
+ return { ok: false, detail: tail };
305
+ } catch (e) {
306
+ const detail = String(e.message || e).slice(0, 300);
307
+ warn(`npm install 异常: ${detail}`);
308
+ return { ok: false, detail };
309
+ }
310
+ }
311
+
312
+ let _pkg = null;
313
+ async function loadDriverPkg() {
314
+ if (_pkg) return _pkg;
315
+ // 1. bare specifier (repo dev copy with scripts/node_modules)
316
+ try { _pkg = await import(DRIVER_PKG); return _pkg; } catch { /* not resolvable from here — try the global root */ }
317
+ // 2. global npm root
318
+ let entry = resolveDriverEntry();
319
+ let install = null;
320
+ // 3. auto-install, then re-resolve (drop the cache: the install may have created the root)
321
+ if (!entry) {
322
+ install = installDriverGlobally();
323
+ _globalRootsCache = null;
324
+ entry = resolveDriverEntry();
325
+ }
326
+ if (!entry) {
327
+ throw new DeviceUIError(
328
+ install && !install.ok
329
+ ? `hypium-driver 自动安装失败(可能是网络或全局目录权限);请手动执行 \`npm install -g ${driverInstallSpec()}\`(Node ≥20.11.1)`
330
+ : `npm install 报告成功但 hypium-driver 仍未定位到;请检查 npm 全局目录(\`npm root -g\`)权限或路径`,
331
+ { detail: (install && install.detail) || "" });
332
+ }
333
+ try {
334
+ _pkg = await import(pathToFileURL(entry).href);
335
+ } catch (e) {
336
+ throw new DeviceUIError(`hypium-driver 加载失败: ${entry}`, { detail: String(e.message || e).slice(0, 200) });
337
+ }
338
+ return _pkg;
339
+ }
340
+
341
+ export async function connect(device, workDir) {
342
+ ensureHdcOnPath();
343
+ hdcRun(device, ["list", "targets"], 10000); // also starts the hdc server (driver uses TCP 8710)
344
+ const pkg = await loadDriverPkg();
345
+ // Runtime artifacts (daemon.json/calls.jsonl/ui_tree etc.) are confined to <work-dir>/.hypium/
346
+ let wd = workDir;
347
+ if (!wd) {
348
+ wd = fs.mkdtempSync(path.join(os.tmpdir(), "device_ui_"));
349
+ warn(`未传 --work-dir:运行杂物落到 ${wd}(传 --work-dir 可让它们随本次运行落盘)`);
350
+ }
351
+ const base = path.join(wd, ".hypium");
352
+ fs.mkdirSync(base, { recursive: true });
353
+ try { process.chdir(base); } catch { /* a bad dir must not block connect */ }
354
+ const caps = device ? { deviceSn: device } : {};
355
+ const driver = await pkg.UiDriver.connect(caps);
356
+ driver.__pkg = pkg; // carry KeyCode etc. constants with the driver so the action layer avoids re-imports
357
+ return driver;
358
+ }
359
+
360
+ export async function disconnect(d) {
361
+ try { await d.disconnect(); } catch { /* best-effort release */ }
362
+ }
363
+
364
+ // --------------------------------------------------------------------------
365
+ // Device self-heal: clear accumulated forwards → wake + keep-awake + start uitest singleton.
366
+ // (Known daemon leftovers are handled by device_ui.mjs killDaemonByPid via pid; no separate engine seizes the device.)
367
+ // --------------------------------------------------------------------------
368
+ export function cleanStaleFport(device) {
369
+ // hypium opens a new local tcp→device uitest port forward per connection and never reclaims it; stale = remote contains "uitest" or ≥3 forwards to the same remote
370
+ try {
371
+ const out = hdcRun(device, ["fport", "ls"]);
372
+ const rows = [], remoteCount = {};
373
+ for (const line of (out.stdout || "").split(/\r?\n/)) {
374
+ const eps = line.split(/\s+/).filter((t) => t.startsWith("tcp:") || t.startsWith("localabstract:"));
375
+ if (eps.length >= 2) {
376
+ rows.push([eps[0], eps[1]]);
377
+ remoteCount[eps[1]] = (remoteCount[eps[1]] || 0) + 1;
378
+ }
379
+ }
380
+ for (const [local, remote] of rows) {
381
+ if (remote.includes("uitest") || remoteCount[remote] >= 3) {
382
+ hdcRun(device, ["fport", "rm", local, remote], 10000);
383
+ }
384
+ }
385
+ } catch { /* best-effort */ }
386
+ }
387
+
388
+ export function warmDevice(device) {
389
+ // Wake + keep screen on (an override, undone by restoreScreenTimeout at session end) + start
390
+ // uitest singleton daemon: OSBase init hoisted to once per session
391
+ for (const tail of [["shell", "power-shell", "wakeup"],
392
+ ["shell", "power-shell", "timeout", "-o", "86400000"],
393
+ ["shell", "uitest", "start-daemon", "singleness"]]) {
394
+ try { hdcRun(device, tail); } catch { /* best-effort */ }
395
+ }
396
+ }
397
+
398
+ export function restoreScreenTimeout(device) {
399
+ // Undo warmDevice's screen-off override (power-shell keeps the pre-override value itself; -r
400
+ // restores it). Crash paths may leave the override; the next warm/restore cycle clears it.
401
+ try { hdcRun(device, ["shell", "power-shell", "timeout", "-r"]); } catch { /* best-effort */ }
402
+ }
403
+
404
+ export function selfHealDevice(device) {
405
+ cleanStaleFport(device);
406
+ warmDevice(device);
407
+ }
408
+
409
+ export async function connectWithReclaim(device, workDir) {
410
+ try {
411
+ return await connect(device, workDir);
412
+ } catch (first) {
413
+ if (first instanceof DeviceUIError) throw first; // missing-dependency class; self-heal won't help
414
+ selfHealDevice(device);
415
+ await new Promise((r) => setTimeout(r, 2000));
416
+ try {
417
+ return await connect(device, workDir);
418
+ } catch (again) {
419
+ throw new DeviceUIError("连接设备失败:已清残留持有者 + 预热 uitest 仍无法连接(息屏/无设备/agent 异常)。"
420
+ + `重试错误: ${String(again.message || again).slice(0, 160)};首错: ${String(first.message || first).slice(0, 160)}`);
421
+ }
422
+ }
423
+ }
424
+
425
+ // --------------------------------------------------------------------------
426
+ // Pure hdc commands (no driver connection)
427
+ // --------------------------------------------------------------------------
428
+ export function doDevices() {
429
+ ensureHdcOnPath();
430
+ const p = hdcRun(null, ["list", "targets"], 15000);
431
+ if (p.error) return { ok: false, error: "hdc 不在 PATH(且无法由 conf.yaml 的 DEVECO_PATH 定位);无法枚举设备" };
432
+ if (p.status !== 0) return { ok: false, error: `hdc list targets 失败(rc=${p.status}): ${(p.stderr || "").trim().slice(0, 300)}` };
433
+ const devs = (p.stdout || "").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.includes("Empty"));
434
+ return { ok: true, devices: devs, count: devs.length };
435
+ }
436
+
437
+ export function probeDensity(device) {
438
+ // px↔vp scale factor (= densityDPI/160): from the "Density:" line of hidumper DisplayManagerService
439
+ ensureHdcOnPath();
440
+ try {
441
+ const out = hdcRun(device, ["shell", "hidumper", "-s", "DisplayManagerService", "-a", "-a"], 15000);
442
+ const m = /Density:\s*([\d.]+)/.exec(out.stdout || "");
443
+ if (m) return parseFloat(m[1]);
444
+ } catch { /* best-effort */ }
445
+ return null;
446
+ }
447
+
448
+ // --------------------------------------------------------------------------
449
+ // Action layer (pure functions: driver d + opts → result dict; shared by one-shot/daemon/l3)
450
+ // --------------------------------------------------------------------------
451
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
452
+
453
+ export async function displaySize(d) {
454
+ const v = await d.getDisplaySize();
455
+ return [v.x ?? v.width, v.y ?? v.height];
456
+ }
457
+ async function currentAppArr(d) {
458
+ const s = await d.currentApp(); // "bundle/ability"
459
+ const i = String(s).indexOf("/");
460
+ return i >= 0 ? [s.slice(0, i), s.slice(i + 1)] : [String(s), ""];
461
+ }
462
+
463
+ export function resolveOut(opts) {
464
+ // observe/screenshot output dir: never cwd. Absolute -o as-is; relative joins --work-dir; default <work-dir>/.hypium/; neither → temp dir + warning
465
+ const out = opts.out, wd = opts.workDir;
466
+ if (out) {
467
+ if (path.isAbsolute(out)) return path.normalize(out);
468
+ if (wd) return path.normalize(path.join(wd, out));
469
+ const td = fs.mkdtempSync(path.join(os.tmpdir(), "device_ui_obs_"));
470
+ warn(`observe/screenshot 收到相对 -o ${JSON.stringify(out)} 且无 --work-dir,落到临时目录 ${td}`);
471
+ return td;
472
+ }
473
+ if (wd) return path.normalize(path.join(wd, ".hypium"));
474
+ const td = fs.mkdtempSync(path.join(os.tmpdir(), "device_ui_obs_"));
475
+ warn(`observe/screenshot 未传 -o 且无 --work-dir,ui_tree/screenshot 落到临时目录 ${td}`);
476
+ return td;
477
+ }
478
+
479
+ function probeMainAbility(device, bundle) {
480
+ // Probe the main ability via bm dump (the driver's startApp auto-match fails for some apps; observed with com.xuncorp.sp)
481
+ try {
482
+ const out = hdcRun(device, ["shell", `bm dump -n ${bundle}`], 15000);
483
+ const m = /"mainElement"\s*:\s*"([^"]+)"/.exec(out.stdout || "")
484
+ || /"mainAbility"\s*:\s*"([^"]+)"/.exec(out.stdout || "");
485
+ return m ? m[1] : null;
486
+ } catch { return null; }
487
+ }
488
+
489
+ export async function doLaunch(d, opts) {
490
+ if (opts.restart) {
491
+ try { await d.stopApp(opts.bundle); } catch (e) { warn(`restart 时 stopApp 失败(继续启动): ${String(e.message || e).slice(0, 160)}`); }
492
+ await sleep(500);
493
+ }
494
+ try {
495
+ await d.startApp(opts.bundle, opts.ability || undefined);
496
+ } catch (e) {
497
+ const ability = !opts.ability && probeMainAbility(opts.device, opts.bundle);
498
+ if (!ability) throw e;
499
+ warn(`startApp 自动匹配失败,按 bm dump mainElement 重试: ${ability}`);
500
+ await d.startApp(opts.bundle, ability);
501
+ }
502
+ await sleep(1200);
503
+ return { ok: true, action: "launch", bundle: opts.bundle, current_app: await currentAppArr(d) };
504
+ }
505
+
506
+ export async function doObserve(d, opts) {
507
+ const outDir = resolveOut(opts);
508
+ fs.mkdirSync(outDir, { recursive: true });
509
+ const treePath = path.normalize(path.join(outDir, "ui_tree.json"));
510
+ await d.dumpLayout(treePath);
511
+ const tree = JSON.parse(fs.readFileSync(treePath, "utf8"));
512
+ const [active, overlays] = resolveActiveLayer(tree);
513
+ const scope = opts.raw ? tree : active;
514
+ const includeBound = !!opts.bound;
515
+ const actionable = collectElements(scope, !!opts.all, includeBound);
516
+ const density = includeBound ? probeDensity(opts.device) : null;
517
+ const result = {
518
+ ok: true, current_app: await currentAppArr(d), display_size: await displaySize(d),
519
+ count: actionable.length, dialog: overlays > 0, tree_file: treePath,
520
+ };
521
+ if (density != null) result.density = density;
522
+ if (overlays >= 2) result.overlays = overlays; // multi-overlay "transitioning" frame; keep evidence for tiebreak review
523
+ let elements;
524
+ if (hasSelector(opts)) {
525
+ elements = collectElements(scope, true, includeBound).filter((e) => matches(e, opts));
526
+ result.find = selectorDesc(opts);
527
+ result.matched = elements.length;
528
+ } else {
529
+ elements = actionable;
530
+ }
531
+ if (opts.limit != null && elements.length > opts.limit) {
532
+ result.truncated = elements.length;
533
+ elements = elements.slice(0, opts.limit);
534
+ }
535
+ result.elements = elements;
536
+ if (opts.screenshot) {
537
+ const shot = path.normalize(path.join(outDir, "screenshot.jpeg"));
538
+ result.screenshot = await d.screenCap(shot);
539
+ }
540
+ return result;
541
+ }
542
+
543
+ export async function doTap(d, opts) {
544
+ if (opts.xy) {
545
+ await d.click(opts.xy[0], opts.xy[1]);
546
+ return { ok: true, action: "tap", center: opts.xy };
547
+ }
548
+ const [el, desc] = await resolveOne(d, opts, "tap");
549
+ await d.click(el.center[0], el.center[1]);
550
+ return { ok: true, action: "tap", matched: desc, center: el.center };
551
+ }
552
+
553
+ export async function doDoubleTap(d, opts) {
554
+ if (opts.xy) {
555
+ await d.doubleClick(opts.xy[0], opts.xy[1]);
556
+ return { ok: true, action: "double-tap", center: opts.xy };
557
+ }
558
+ const [el, desc] = await resolveOne(d, opts, "double-tap");
559
+ await d.doubleClick(el.center[0], el.center[1]);
560
+ return { ok: true, action: "double-tap", matched: desc, center: el.center };
561
+ }
562
+
563
+ export async function doLongPress(d, opts) {
564
+ let center = opts.xy, desc;
565
+ if (!center) { const [e, dd] = await resolveOne(d, opts, "long-press"); center = e.center; desc = dd; }
566
+ const [x, y] = center;
567
+ if (opts.pressMs) {
568
+ // longClickAt's duration param fails over RPC on agent v1.1.9 (observed); same-point swipeHold gives a controllable duration
569
+ await d.swipeHold(x, y, x, y, 600, opts.pressMs);
570
+ } else {
571
+ await d.longClickAt({ x, y });
572
+ }
573
+ return { ok: true, action: "long-press", ...(desc ? { matched: desc } : {}), center,
574
+ ...(opts.pressMs ? { press_ms: opts.pressMs } : {}) };
575
+ }
576
+
577
+ export async function doInput(d, opts) {
578
+ let center = opts.xy, desc;
579
+ if (!center) { const [e, dd] = await resolveOne(d, opts, "input"); center = e.center; desc = dd; }
580
+ await d.inputText({ x: center[0], y: center[1] }, opts.value);
581
+ return { ok: true, action: "input", ...(desc ? { matched: desc } : {}), center, value: opts.value };
582
+ }
583
+
584
+ export async function doSwipe(d, opts) {
585
+ if (opts.from && opts.to) { // exact two-point swipe
586
+ await d.swipe(opts.from[0], opts.from[1], opts.to[0], opts.to[1], opts.speed || undefined);
587
+ return { ok: true, action: "swipe", from: opts.from, to: opts.to };
588
+ }
589
+ const [w, h] = await displaySize(d);
590
+ // Directional: full-screen area + percent (distance is a 0-100 percentage, default 60)
591
+ const percent = Math.min(1, Math.max(0.05, (opts.distance ?? 60) / 100));
592
+ await d.Screen.swipe(0, 0, w, h, opts.dir, percent, opts.speed || 5000);
593
+ return { ok: true, action: "swipe", dir: opts.dir };
594
+ }
595
+
596
+ export async function doDrag(d, opts) {
597
+ if (!opts.from || !opts.to) throw new DeviceUIError("drag 需要 --from X Y 与 --to X Y(选择器由调用方先 observe 解析为坐标)");
598
+ await d.drag(opts.from[0], opts.from[1], opts.to[0], opts.to[1], opts.speed || undefined);
599
+ return { ok: true, action: "drag", from: opts.from, to: opts.to };
600
+ }
601
+
602
+ export async function doFling(d, opts) {
603
+ const [w, h] = await displaySize(d);
604
+ await d.Screen.fling(0, 0, w, h, opts.dir, opts.speed || 7500);
605
+ return { ok: true, action: "fling", dir: opts.dir };
606
+ }
607
+
608
+ export async function doPinch(d, opts) {
609
+ let area = opts.area; // [x1,y1,x2,y2]
610
+ if (!area && hasSelector(opts)) {
611
+ const [el] = await resolveOne(d, opts, "pinch", true);
612
+ if (!el.bound) throw new DeviceUIError("pinch 目标元素无 bound(不可作捏合区域)");
613
+ area = el.bound;
614
+ }
615
+ if (!area) { // default: center half of the screen
616
+ const [w, h] = await displaySize(d);
617
+ area = [Math.floor(w / 4), Math.floor(h / 4), Math.floor((3 * w) / 4), Math.floor((3 * h) / 4)];
618
+ }
619
+ const [x1, y1, x2, y2] = area;
620
+ const percent = opts.scale ?? 0.5, speed = opts.speed || 2500;
621
+ const fn = opts.mode === "out" ? "pinchOpen" : "pinchClose";
622
+ await d.Screen[fn](x1, y1, x2 - x1, y2 - y1, percent, speed);
623
+ return { ok: true, action: "pinch", mode: opts.mode, area, scale: percent };
624
+ }
625
+
626
+ export async function doKey(d, opts) {
627
+ const pkg = d.__pkg || {};
628
+ let name;
629
+ if (opts.back) { await d.pressBack(); name = "Back"; }
630
+ else if (opts.home) { await d.pressHome(); name = "Home"; }
631
+ else if (opts.power) { await d.pressKey(pkg.KeyCode?.POWER ?? 18); name = "Power"; }
632
+ else if (opts.combo) { await d.triggerCombineKeys(...opts.combo); name = `combo:${opts.combo.join("+")}`; }
633
+ else if (opts.code != null) { await d.pressKey(opts.code); name = `code:${opts.code}`; }
634
+ else throw new DeviceUIError("key 需要 --back/--home/--power/--code/--combo");
635
+ return { ok: true, action: "key", name };
636
+ }
637
+
638
+ export async function doRotate(d, opts) {
639
+ const map = { 0: 0, 90: 1, 180: 2, 270: 3 };
640
+ const rot = map[opts.deg];
641
+ if (rot == null) throw new DeviceUIError("rotate 需要 --deg 0|90|180|270");
642
+ await d.Screen.setDisplayRotation(rot);
643
+ return { ok: true, action: "rotate", deg: opts.deg };
644
+ }
645
+
646
+ // --------------------------------------------------------------------------
647
+ // By-path actions (clear-text/scroll-until need a device-side component handle, so they use the
648
+ // official By — the sole exemption from the dump main path; uniqueness guard via findComponents count)
649
+ // --------------------------------------------------------------------------
650
+ function byFromSelector(pkg, opts) {
651
+ const { BY, MatchPattern } = pkg;
652
+ if (opts.text != null) return BY.text(opts.text);
653
+ if (opts.contains != null) return BY.text(opts.contains, MatchPattern.CONTAINS);
654
+ if (opts.regex != null) return BY.text(opts.regex, MatchPattern.REGEXP);
655
+ if (opts.starts != null) return BY.text(opts.starts, MatchPattern.STARTS_WITH);
656
+ if (opts.ends != null) return BY.text(opts.ends, MatchPattern.ENDS_WITH);
657
+ if (opts.key != null) return BY.key(opts.key);
658
+ if (opts.type != null) return BY.type(opts.type);
659
+ if (opts.desc != null) throw new DeviceUIError("该动作走设备侧 By 寻址,node 版 By 无 description 匹配器——换 --text/--key/--type");
660
+ throw new DeviceUIError("需要选择器(--text/--contains/--regex/--starts/--ends/--key/--type)");
661
+ }
662
+
663
+ async function findUniqueComponent(d, opts, action) {
664
+ const by = byFromSelector(d.__pkg, opts);
665
+ const comps = (await d.findComponents(by)) || [];
666
+ if (!comps.length) throw new DeviceUIError(`选择器未命中(By): ${selectorDesc(opts)}`,
667
+ { hint: "先 observe 看屏幕,或换选择器" });
668
+ if (comps.length > 1 && opts.nth == null) {
669
+ throw new DeviceUIError(`命中 ${comps.length} 个: ${selectorDesc(opts)}`, { hint: "加 --nth N(0 起)" });
670
+ }
671
+ const idx = opts.nth || 0;
672
+ if (idx >= comps.length) throw new DeviceUIError(`--nth ${idx} 越界,仅 ${comps.length} 个命中`);
673
+ return comps[idx];
674
+ }
675
+
676
+ export async function doClearText(d, opts) {
677
+ const comp = await findUniqueComponent(d, opts, "clear-text");
678
+ await comp.clearText();
679
+ return { ok: true, action: "clear-text", matched: selectorDesc(opts) };
680
+ }
681
+
682
+ export async function doScrollUntil(d, opts) {
683
+ // Scroll until target visible: container (default = first scrollable container).scrollSearch(target)
684
+ const pkg = d.__pkg;
685
+ const targetBy = byFromSelector(pkg, opts);
686
+ let container;
687
+ if (opts.containerKey != null || opts.containerType != null) {
688
+ container = await findUniqueComponent(d,
689
+ { key: opts.containerKey, type: opts.containerType, nth: opts.containerNth }, "scroll-until.container");
690
+ } else {
691
+ const cands = (await d.findComponents(pkg.BY.scrollable(true))) || [];
692
+ if (!cands.length) throw new DeviceUIError("scroll-until: 屏上无可滚动容器");
693
+ container = cands[opts.containerNth || 0];
694
+ if (!container) throw new DeviceUIError(`scroll-until: --container-nth 越界,仅 ${cands.length} 个容器`);
695
+ }
696
+ let found;
697
+ try {
698
+ found = await container.scrollSearch(targetBy);
699
+ } catch (e) { // driver throws "Fail to resolve object [ScrollSearchComp…]" on not-found (observed); translate to a stable error
700
+ if (String(e.message || e).includes("ScrollSearchComp")) found = null;
701
+ else throw e;
702
+ }
703
+ if (!found) throw new DeviceUIError(`scroll-until 滚完未命中: ${selectorDesc(opts)}`,
704
+ { hint: "确认目标文字存在于该滚动容器内" });
705
+ const c = await found.getBoundsCenter();
706
+ return { ok: true, action: "scroll-until", matched: selectorDesc(opts), center: [c.x ?? c.width, c.y ?? c.height] };
707
+ }
708
+
709
+ export async function doGesture(d, opts) {
710
+ // Custom trajectories (single/multi finger): tracks = [[{x,y,ms?}|[x,y,ms?], ...], ...]; first point of each track is down, the rest move_to
711
+ const pkg = d.__pkg;
712
+ const tracks = opts.tracks;
713
+ if (!Array.isArray(tracks) || !tracks.length || !tracks.every((t) => Array.isArray(t) && t.length >= 2)) {
714
+ throw new DeviceUIError("gesture 需要 tracks:每轨 ≥2 个点,形如 [[[x,y,ms?],…],…]");
715
+ }
716
+ const actions = tracks.map((track) => {
717
+ const pa = new pkg.PointAction(opts.samplingMs || undefined);
718
+ track.forEach((p, i) => {
719
+ const pt = Array.isArray(p) ? { x: p[0], y: p[1], ms: p[2] } : p;
720
+ if (i === 0) pa.down({ x: pt.x, y: pt.y }, pt.ms || undefined);
721
+ else pa.move_to({ x: pt.x, y: pt.y }, pt.ms || undefined);
722
+ });
723
+ return pa;
724
+ });
725
+ const payload = actions.length === 1 ? actions[0] : pkg.PointAction.mergeMultiPointAction(actions);
726
+ await d.injectMultiPointerAction(payload, opts.speed || undefined);
727
+ return { ok: true, action: "gesture", tracks: tracks.length,
728
+ points: tracks.reduce((n, t) => n + t.length, 0) };
729
+ }
730
+
731
+ // --------------------------------------------------------------------------
732
+ // Extended commands (round out the hypium-driver capability surface; the agent can only use what's exposed as a command)
733
+ // A) Driver-backed (need driver d; registered in ACTIONS)
734
+ // --------------------------------------------------------------------------
735
+ export async function doWaitIdle(d, opts) {
736
+ // Wait for the UI to settle (returns after idle_ms without refresh, or on timeout).
737
+ // Default idle 1000ms — smaller values RPC-flake (measured; the contract warns against <1000).
738
+ await d.waitForIdle((opts.idleMs ?? 1000) / 1000, (opts.timeoutMs ?? 10000) / 1000);
739
+ return { ok: true, action: "wait-idle", idle_ms: opts.idleMs ?? 1000 };
740
+ }
741
+
742
+ export async function doUnlock(d) {
743
+ await d.wakeUpDisplay();
744
+ const locked = await d.isDisplayLocked().catch(() => null);
745
+ if (locked !== false) await d.unlock();
746
+ return { ok: true, action: "unlock", was_locked: locked };
747
+ }
748
+
749
+ export async function doWake(d) {
750
+ await d.wakeUpDisplay();
751
+ return { ok: true, action: "wake", display_on: await d.isDisplayOn().catch(() => null) };
752
+ }
753
+
754
+ export async function doInfo(d, opts) {
755
+ // Device/screen info in one call. Every probe is timeout-raced: on a flaky channel a driver
756
+ // RPC can hang (never rejects) — that must degrade the field to null, not hang the whole
757
+ // command (measured 2min+ per hang). displaySize included: it was the un-guarded first await.
758
+ const g = (p, fb = null) => Promise.race([p.catch(() => fb), sleep(8000).then(() => fb)]);
759
+ const [w, h] = await g(displaySize(d), [null, null]);
760
+ return { ok: true, action: "info",
761
+ device_type: await g(d.getDeviceType()), model: await g(d.getDeviceModel()),
762
+ api_level: await g(d.getApiLevel()), system_version: await g(d.getSystemVersion()),
763
+ display_size: [w, h], density: probeDensity(opts.device),
764
+ rotation: await g(d.getDisplayRotation()), display_on: await g(d.isDisplayOn()),
765
+ display_locked: await g(d.isDisplayLocked()) };
766
+ }
767
+
768
+ const RESIZE_DIR = { left: 0, right: 1, up: 2, down: 3, "left-up": 4, "left-down": 5, "right-up": 6, "right-down": 7 };
769
+
770
+ export async function doWindow(d, opts) {
771
+ // Find a window (--bundle/--title/--focused; default = currently focused) and optionally apply a window op (multi-window/PC)
772
+ const filter = {};
773
+ if (opts.wbundle != null) filter.bundleName = opts.wbundle;
774
+ if (opts.title != null) filter.title = opts.title;
775
+ filter.focused = opts.focused != null ? opts.focused : (opts.wbundle == null && opts.title == null);
776
+ const win = d.findWindow(filter);
777
+ if (!(await win.exist?.().catch(() => true)) && win.exist) throw new DeviceUIError("window 未命中(换 --bundle/--title/--focused)");
778
+ const g = async (p) => p.catch(() => null);
779
+ if (opts.wop === "resize") await win.resize(opts.w, opts.h, RESIZE_DIR[opts.dir ?? "right-down"] ?? 7);
780
+ else if (opts.wop === "maximize") await win.maximize();
781
+ else if (opts.wop === "minimize") await win.minimize();
782
+ else if (opts.wop === "close") await win.close();
783
+ else if (opts.wop === "focus") await win.focus();
784
+ else if (opts.wop === "move") await win.moveTo(opts.x, opts.y);
785
+ const b = await g(win.getBounds());
786
+ return { ok: true, action: "window", op: opts.wop || "info",
787
+ bundle: await g(win.getBundleName()), bounds: b ? [b.left ?? b.x, b.top ?? b.y, b.right ?? (b.x + b.width), b.bottom ?? (b.y + b.height)] : null,
788
+ focused: await g(win.isFocused()), active: await g(win.isActive()) };
789
+ }
790
+
791
+ export async function doRecord(d, opts) {
792
+ // Screen recording start/stop (daemon only: start and stop are two separate calls, the driver session must stay alive)
793
+ if (opts.rop === "start") { await d.Screen.startRecordingScreen(); return { ok: true, action: "record", op: "start" }; }
794
+ if (opts.rop === "stop") {
795
+ const outDir = resolveOut(opts);
796
+ fs.mkdirSync(outDir, { recursive: true });
797
+ const mp4 = path.normalize(path.join(outDir, "record.mp4"));
798
+ await d.Screen.stopRecordingScreen({ mp4 });
799
+ return { ok: true, action: "record", op: "stop", path: mp4 };
800
+ }
801
+ throw new DeviceUIError("record 需要 --op start 或 --op stop");
802
+ }
803
+
804
+ export async function doLaunchUri(d, opts) {
805
+ // Implicit launch by action/uri
806
+ await d.startAppImplicit(opts.uaction || undefined, opts.uri || undefined, opts.extra || undefined);
807
+ await sleep(1200);
808
+ return { ok: true, action: "launch-uri", current_app: await currentAppArr(d) };
809
+ }
810
+
811
+ export async function doApp(d, opts) {
812
+ // Thin app-management wrapper (install/uninstall/clear/info/running)
813
+ if (opts.aop === "info") return { ok: true, action: "app", op: "info", info: await d.getAppInfo(opts.bundle) };
814
+ if (opts.aop === "running") return { ok: true, action: "app", op: "running", running: await d.isAppRunning(opts.bundle) };
815
+ if (opts.aop === "install") { await d.installApp(opts.path); return { ok: true, action: "app", op: "install", path: opts.path }; }
816
+ if (opts.aop === "uninstall") { await d.uninstallApp(opts.bundle); return { ok: true, action: "app", op: "uninstall", bundle: opts.bundle }; }
817
+ if (opts.aop === "clear") { await d.clearAppData(opts.bundle); return { ok: true, action: "app", op: "clear", bundle: opts.bundle }; }
818
+ throw new DeviceUIError("app 需要 --op info/running/install/uninstall/clear");
819
+ }
820
+
821
+ // --------------------------------------------------------------------------
822
+ // B) Pure hdc (no driver needed; CLI/daemon run these before connecting, skipping connect overhead)
823
+ // --------------------------------------------------------------------------
824
+ export function doShell(opts) {
825
+ ensureHdcOnPath();
826
+ const out = hdcRun(opts.device, ["shell", opts.cmd], (opts.timeoutMs ?? 60000));
827
+ if (out.error) return { ok: false, error: `hdc 不可用: ${out.error.message}` };
828
+ return { ok: out.status === 0 || out.status == null, action: "shell",
829
+ stdout: (out.stdout || "").trim(), stderr: (out.stderr || "").trim() || undefined, rc: out.status };
830
+ }
831
+
832
+ export function doHdc(opts) {
833
+ ensureHdcOnPath();
834
+ const out = hdcRun(opts.device, opts.args || [], (opts.timeoutMs ?? 60000));
835
+ if (out.error) return { ok: false, error: `hdc 不可用: ${out.error.message}` };
836
+ return { ok: out.status === 0 || out.status == null, action: "hdc",
837
+ stdout: (out.stdout || "").trim(), stderr: (out.stderr || "").trim() || undefined, rc: out.status };
838
+ }
839
+
840
+ export function doHilog(opts) {
841
+ // Snapshot the current hilog buffer (hilog -x dumps and exits, non-blocking); supports --grep filter + --lines cap.
842
+ // The pipeline's hilog-marker collection relies on this (snapshot right after an action, grep the marker).
843
+ ensureHdcOnPath();
844
+ const out = hdcRun(opts.device, ["shell", "hilog", "-x"], (opts.timeoutMs ?? 30000));
845
+ if (out.error) return { ok: false, error: `hdc 不可用: ${out.error.message}` };
846
+ let lines = (out.stdout || "").split(/\r?\n/);
847
+ if (opts.grep) { try { const re = new RegExp(opts.grep); lines = lines.filter((l) => re.test(l)); } catch { lines = lines.filter((l) => l.includes(opts.grep)); } }
848
+ const n = opts.lines ?? 200;
849
+ const matched = lines.length;
850
+ return { ok: true, action: "hilog", grep: opts.grep, matched, lines: lines.slice(-n) };
851
+ }
852
+
853
+ export function doToast(opts) {
854
+ // Toast detection (hypium-driver has no Toast API; grep toast events from a hilog snapshot — best-effort, OS-version dependent).
855
+ ensureHdcOnPath();
856
+ const out = hdcRun(opts.device, ["shell", "hilog", "-x"], (opts.timeoutMs ?? 30000));
857
+ if (out.error) return { ok: false, error: `hdc 不可用: ${out.error.message}` };
858
+ const re = /toast/i;
859
+ const hits = (out.stdout || "").split(/\r?\n/).filter((l) => re.test(l));
860
+ return { ok: true, action: "toast", note: "best-effort via hilog(无原生 Toast API)",
861
+ matched: hits.length, lines: hits.slice(-30) };
862
+ }
863
+
864
+ export function doFile(opts) {
865
+ // Device file transfer (hdc file send/recv; cheaper for one-shots than driver.pushFile)
866
+ ensureHdcOnPath();
867
+ if (opts.fop === "push") {
868
+ const out = hdcRun(opts.device, ["file", "send", opts.local, opts.remote], (opts.timeoutMs ?? 60000));
869
+ return { ok: out.status === 0, action: "file", op: "push", local: opts.local, remote: opts.remote, out: (out.stdout || out.stderr || "").trim() };
870
+ }
871
+ if (opts.fop === "pull") {
872
+ const out = hdcRun(opts.device, ["file", "recv", opts.remote, opts.local], (opts.timeoutMs ?? 60000));
873
+ return { ok: out.status === 0, action: "file", op: "pull", remote: opts.remote, local: opts.local, out: (out.stdout || out.stderr || "").trim() };
874
+ }
875
+ throw new DeviceUIError("file 需要 --op push 或 --op pull");
876
+ }
877
+
878
+ // Pure hdc command set (no driver; CLI/daemon run these before connect)
879
+ export const HDC_ACTIONS = { shell: doShell, hdc: doHdc, hilog: doHilog, toast: doToast, file: doFile };
880
+
881
+ // Action dispatch table (needs driver; shared by daemon and l3; CLI one-shot uses the same table)
882
+ export const ACTIONS = {
883
+ launch: doLaunch, observe: doObserve, tap: doTap, "double-tap": doDoubleTap,
884
+ "long-press": doLongPress, input: doInput, "clear-text": doClearText, swipe: doSwipe,
885
+ drag: doDrag, fling: doFling, pinch: doPinch, "scroll-until": doScrollUntil,
886
+ gesture: doGesture, key: doKey, rotate: doRotate,
887
+ "wait-idle": doWaitIdle, unlock: doUnlock, wake: doWake, info: doInfo,
888
+ window: doWindow, record: doRecord, "launch-uri": doLaunchUri, app: doApp,
889
+ };
890
+
891
+ export async function runAction(d, cmd, opts) {
892
+ const fn = ACTIONS[cmd];
893
+ if (!fn) throw new DeviceUIError(`不支持的命令: ${cmd}`);
894
+ return fn(d, opts);
895
+ }