@buaa_smat/hometrans 0.1.0 → 0.1.2

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 (136) hide show
  1. package/README.md +141 -124
  2. package/agents/build-fixer.md +1 -0
  3. package/agents/code-review-fix.md +1 -0
  4. package/agents/code-reviewer.md +1 -0
  5. package/agents/logic-coding.md +1 -0
  6. package/agents/logic-context-builder.md +1 -0
  7. package/agents/review-fixer.md +1 -0
  8. package/agents/self-test-fixer.md +1 -0
  9. package/agents/self-tester.md +260 -233
  10. package/agents/spec-generator.md +1 -0
  11. package/agents/test-tools/autotest/README.md +223 -0
  12. package/agents/test-tools/autotest/config.yaml.example +58 -0
  13. package/agents/test-tools/autotest/pyproject.toml +16 -0
  14. package/agents/test-tools/autotest/report_tool.py +759 -0
  15. package/agents/test-tools/autotest/self_test_runner.py +773 -0
  16. package/agents/test-tools/autotest/testcases_schema.md +143 -0
  17. package/agents/test-tools/autotest/testcases_tool.py +215 -0
  18. package/agents/test-tools/autotest/uv.lock +3156 -0
  19. package/agents/test-tools/harmony_autotest-0.1.0-py3-none-any.whl +0 -0
  20. package/agents/test-tools/hypium-6.1.0.210-py3-none-any.whl +0 -0
  21. package/agents/test-tools/hypium_mcp-0.6.5-py3-none-any.whl +0 -0
  22. package/agents/test-tools/xdevice-6.1.0.210-py3-none-any.whl +0 -0
  23. package/agents/test-tools/xdevice_devicetest-6.1.0.210-py3-none-any.whl +0 -0
  24. package/agents/test-tools/xdevice_ohos-6.1.0.210-py3-none-any.whl +0 -0
  25. package/dist/cli/config-store.js +27 -2
  26. package/dist/cli/config.js +17 -6
  27. package/dist/cli/index.js +3 -2
  28. package/dist/cli/init.js +135 -22
  29. package/dist/cli/mcp.js +2 -2
  30. package/dist/context/index.js +165 -69
  31. package/package.json +59 -60
  32. package/skills/code-dev-review-fix/SKILL.md +279 -0
  33. package/skills/code-dev-review-fix-workspace/evals/evals.json +56 -0
  34. package/skills/code-dev-review-fix-workspace/iteration-1/routing-results.md +23 -0
  35. package/skills/convert_pipeline/SKILL.md +423 -439
  36. package/skills/hmos-resources-convert/SKILL.md +623 -0
  37. package/skills/hmos-resources-convert/evals/evals.json +171 -0
  38. package/skills/hmos-resources-convert/references/conversion-rules.md +663 -0
  39. package/skills/hmos-resources-convert/references/dependency-analysis-rules.md +388 -0
  40. package/skills/hmos-resources-convert/references/resource-mapping-rules.md +457 -0
  41. package/skills/hmos-resources-convert/references/xml-drawable-to-svg-rules.md +513 -0
  42. package/skills/hmos-resources-convert/template/AppScope/app.json5 +10 -0
  43. package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +8 -0
  44. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
  45. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
  46. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +7 -0
  47. package/skills/hmos-resources-convert/template/build-profile.json5 +42 -0
  48. package/skills/hmos-resources-convert/template/code-linter.json5 +32 -0
  49. package/skills/hmos-resources-convert/template/entry/build-profile.json5 +33 -0
  50. package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +6 -0
  51. package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +23 -0
  52. package/skills/hmos-resources-convert/template/entry/oh-package.json5 +10 -0
  53. package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +48 -0
  54. package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +16 -0
  55. package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +23 -0
  56. package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +55 -0
  57. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +8 -0
  58. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +8 -0
  59. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +16 -0
  60. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
  61. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
  62. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +7 -0
  63. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
  64. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +3 -0
  65. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +5 -0
  66. package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +8 -0
  67. package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +2 -0
  68. package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +35 -0
  69. package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +5 -0
  70. package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +16 -0
  71. package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +5 -0
  72. package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +33 -0
  73. package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +23 -0
  74. package/skills/hmos-resources-convert/template/hvigorfile.ts +6 -0
  75. package/skills/hmos-resources-convert/template/oh-package-lock.json5 +28 -0
  76. package/skills/hmos-resources-convert/template/oh-package.json5 +10 -0
  77. package/skills/hmos-resources-convert/tools/apktool.bat +85 -0
  78. package/skills/hmos-resources-convert/tools/apktool_3.0.1.jar +0 -0
  79. package/skills/hmos-ui-align/SKILL.md +182 -0
  80. package/skills/hmos-ui-align/config-example.json +11 -0
  81. package/skills/hmos-ui-align/config.json +11 -0
  82. package/skills/hmos-ui-align/diff_analysis.md +53 -0
  83. package/skills/hmos-ui-align/page_align.md +62 -0
  84. package/skills/hmos-ui-align/readme.md +231 -0
  85. package/skills/hmos-ui-align/references/Comparison_Template.md +2 -0
  86. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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
  87. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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
  88. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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
  89. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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
  90. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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
  91. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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
  92. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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
  93. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/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
  94. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/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
  95. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/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
  96. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217V1.md +911 -0
  97. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
  98. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/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
  99. package/skills/hmos-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243//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
  100. package/skills/hmos-ui-align/references/UI_Analysis_Template.md +4 -0
  101. package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2535 -0
  102. package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
  103. package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
  104. package/skills/hmos-ui-align/scripts/app_feature_verify.py +443 -0
  105. package/skills/hmos-ui-align/scripts/navigation-capure.md +37 -0
  106. package/skills/hmos-ui-align/scripts/page_capture.py +592 -0
  107. package/skills/hmos-ui-align-batch/SKILL.md +99 -0
  108. package/skills/hmos-ui-align-batch/references/conversion-procedure.md +180 -0
  109. package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2535 -0
  110. package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
  111. package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
  112. package/skills/hmos-ui-align-batch/references/mvvm/@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
  113. package/skills/hmos-ui-align-batch/references/mvvm/@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
  114. package/skills/hmos-ui-align-batch/references/mvvm/@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
  115. package/skills/hmos-ui-align-batch/references/mvvm/@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
  116. package/skills/hmos-ui-align-batch/references/mvvm/@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
  117. package/skills/hmos-ui-align-batch/references/mvvm/@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
  118. package/skills/hmos-ui-align-batch/references/mvvm/@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
  119. package/skills/hmos-ui-align-batch/references/mvvm/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
  120. package/skills/hmos-ui-align-batch/references/mvvm/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
  121. package/skills/hmos-ui-align-batch/references/mvvm/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
  122. package/skills/hmos-ui-align-batch/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
  123. package/skills/hmos-ui-align-batch/references/mvvm/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
  124. package/skills/hmos-ui-align-batch/references/mvvm//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
  125. package/skills/hmos-ui-align-batch/scripts/android_parse_fast.py +1606 -0
  126. package/skills/self-test/SKILL.md +369 -0
  127. package/skills/self-test/readme.md +309 -0
  128. package/skills/spec-generator-skill/SKILL.md +332 -0
  129. package/skills/spec-generator-skill/references/android-platform-tokens.md +105 -0
  130. package/skills/spec-generator-skill/references/spec-sample-1.md +78 -0
  131. package/skills/spec-generator-skill/references/spec-sample-2.md +58 -0
  132. package/skills/spec-generator-skill/references/spec-sample-3.md +116 -0
  133. package/skills/spec-generator-skill/references/step4-report-template.md +33 -0
  134. package/agents/self-test-setup.md +0 -165
  135. package/dist/context/resources/sdkConfig.json +0 -24
  136. package/src/context/resources/sdkConfig.json +0 -24
@@ -0,0 +1,592 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 统一页面采集工具 —— 单次采集当前设备页面的 View Tree (XML)、截图,支持滚动采集。
5
+
6
+ 支持 Android (ADB) 和 HarmonyOS (HDC) 两种设备。
7
+
8
+ 用法:
9
+ # HarmonyOS
10
+ python page_capture.py --device hdc -o ./output
11
+
12
+ # Android
13
+ python page_capture.py --device adb -o ./output --serial emulator-5554
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ import time
23
+ import xml.etree.ElementTree as ET
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+
27
+ # 强制设置 UTF-8 编码以避免 Windows GBK 编码问题
28
+ if sys.platform == 'win32':
29
+ import io
30
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
31
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Constants
35
+ # ---------------------------------------------------------------------------
36
+ SCROLL_DURATION_MS = 300
37
+ HDC_SWIPE_SPEED = 600 # pixels/second for hdc uitest uiInput swipe
38
+ IDLE_POLL_INTERVAL = 0.2
39
+ IDLE_TIMEOUT = 5
40
+
41
+ SKIP_RESOURCE_ID_PATTERNS = [
42
+ "statusBarBackground",
43
+ "navigationBarBackground",
44
+ "action_bar_container",
45
+ "status_bar",
46
+ "navigation_bar",
47
+ ]
48
+
49
+ # HarmonyOS: skip system UI node types / bundles
50
+ HMOS_SKIP_TYPES = {"WindowScene", "__Common__", "EffectComponent", "metaballNode"}
51
+ HMOS_SKIP_BUNDLES = {"com.huawei.systemui", "com.ohos.systemui", "com.huawei.android.launcher"}
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # ADB helpers
56
+ # ---------------------------------------------------------------------------
57
+ def adb(*args, serial=None, timeout=15) -> str:
58
+ cmd = ["adb"]
59
+ if serial:
60
+ cmd += ["-s", serial]
61
+ cmd += list(args)
62
+ try:
63
+ result = subprocess.run(cmd, capture_output=True, timeout=timeout,
64
+ encoding="utf-8", errors="replace")
65
+ return (result.stdout or "").strip()
66
+ except subprocess.TimeoutExpired:
67
+ print(f" [WARN] adb timeout: {' '.join(args)}")
68
+ return ""
69
+
70
+
71
+ def adb_shell(*args, serial=None, timeout=15) -> str:
72
+ return adb("shell", *args, serial=serial, timeout=timeout)
73
+
74
+
75
+ def adb_wait_idle(serial=None):
76
+ deadline = time.time() + IDLE_TIMEOUT
77
+ prev_focus = None
78
+ interval = IDLE_POLL_INTERVAL
79
+ while time.time() < deadline:
80
+ time.sleep(interval)
81
+ interval = min(interval * 1.5, 0.5)
82
+ focus_out = adb_shell("dumpsys", "window", serial=serial)
83
+ current_focus = ""
84
+ for line in focus_out.splitlines():
85
+ if "mCurrentFocus" in line:
86
+ current_focus = line.strip()
87
+ break
88
+ if current_focus and current_focus == prev_focus:
89
+ return
90
+ prev_focus = current_focus
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # HDC helpers
95
+ # ---------------------------------------------------------------------------
96
+ def hdc(*args, timeout=15) -> str:
97
+ cmd = ["hdc"] + list(args)
98
+ try:
99
+ result = subprocess.run(cmd, shell=True, capture_output=True,
100
+ text=True, timeout=timeout)
101
+ if result.returncode != 0 and result.stderr.strip():
102
+ print(f" [WARN] hdc stderr: {result.stderr.strip()}")
103
+ return (result.stdout or "").strip()
104
+ except subprocess.TimeoutExpired:
105
+ print(f" [WARN] hdc timeout: {' '.join(args)}")
106
+ return ""
107
+
108
+
109
+ def hdc_shell(*args, timeout=15) -> str:
110
+ return hdc("shell", *args, timeout=timeout)
111
+
112
+
113
+ def hdc_wait_idle():
114
+ time.sleep(1)
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # XML utilities (shared)
119
+ # ---------------------------------------------------------------------------
120
+ def find_scrollable_containers(xml_content: str) -> list[dict]:
121
+ containers = []
122
+ if not xml_content:
123
+ return containers
124
+ try:
125
+ root = ET.fromstring(xml_content)
126
+ except ET.ParseError:
127
+ return containers
128
+ for node in root.iter("node"):
129
+ if node.get("scrollable") != "true":
130
+ continue
131
+ cls = node.get("class", "")
132
+ if cls in {"android.widget.HorizontalScrollView", "HorizontalScrollView"}:
133
+ continue
134
+ bounds_str = node.get("bounds", "")
135
+ m = re.findall(r"\d+", bounds_str)
136
+ if len(m) != 4:
137
+ continue
138
+ x1, y1, x2, y2 = map(int, m)
139
+ if y2 - y1 < 200:
140
+ continue
141
+ containers.append({
142
+ "bounds": (x1, y1, x2, y2),
143
+ "class": cls,
144
+ "resource-id": node.get("resource-id", ""),
145
+ })
146
+ containers.sort(
147
+ key=lambda c: (c["bounds"][2] - c["bounds"][0]) * (c["bounds"][3] - c["bounds"][1]),
148
+ reverse=True,
149
+ )
150
+ return containers
151
+
152
+
153
+ def _extract_node_keys(xml_content: str) -> set[tuple]:
154
+ keys = set()
155
+ if not xml_content:
156
+ return keys
157
+ try:
158
+ root = ET.fromstring(xml_content)
159
+ except ET.ParseError:
160
+ return keys
161
+ for node in root.iter("node"):
162
+ keys.add((
163
+ node.get("class", ""),
164
+ node.get("resource-id", ""),
165
+ node.get("text", ""),
166
+ node.get("content-desc", ""),
167
+ node.get("bounds", ""),
168
+ ))
169
+ return keys
170
+
171
+
172
+ def parse_bounds_center(bounds: str):
173
+ m = re.findall(r"\d+", bounds)
174
+ if len(m) == 4:
175
+ x1, y1, x2, y2 = map(int, m)
176
+ return ((x1 + x2) // 2, (y1 + y2) // 2)
177
+ return None
178
+
179
+
180
+ def get_clickable_elements(xml_content: str) -> list[dict]:
181
+ elements = []
182
+ if not xml_content:
183
+ return elements
184
+ try:
185
+ root = ET.fromstring(xml_content)
186
+
187
+ listview_child_set = set()
188
+ for node in root.iter("node"):
189
+ cls = node.get("class", "")
190
+ if "ListView" in cls or "GridView" in cls or "List" == cls:
191
+ for child in node:
192
+ listview_child_set.add(child)
193
+
194
+ def extract_text(node):
195
+ text = node.get("text", "")
196
+ if text:
197
+ return text
198
+ for child in node.iter("node"):
199
+ t = child.get("text", "")
200
+ if t:
201
+ return t
202
+ return ""
203
+
204
+ for node in root.iter("node"):
205
+ if node.get("enabled") != "true":
206
+ continue
207
+ is_clickable = node.get("clickable") == "true"
208
+ is_long_clickable = node.get("long-clickable") == "true"
209
+ is_listview_item = node in listview_child_set
210
+ is_checkable = node.get("checkable") == "true"
211
+ if not (is_clickable or is_long_clickable or is_listview_item or is_checkable):
212
+ continue
213
+ bounds = node.get("bounds", "")
214
+ text = node.get("text", "")
215
+ if not text:
216
+ text = extract_text(node)
217
+ base_info = {
218
+ "class": node.get("class", ""),
219
+ "text": text,
220
+ "resource-id": node.get("resource-id", ""),
221
+ "content-desc": node.get("content-desc", ""),
222
+ "bounds": bounds,
223
+ "center": parse_bounds_center(bounds),
224
+ }
225
+ if is_clickable or is_listview_item or is_checkable:
226
+ elements.append({**base_info, "long-clickable": False})
227
+ if is_long_clickable:
228
+ elements.append({**base_info, "long-clickable": True})
229
+ except ET.ParseError as e:
230
+ print(f" [WARN] XML parse error: {e}")
231
+ return elements
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # HarmonyOS: JSON → XML conversion
236
+ # ---------------------------------------------------------------------------
237
+ def hmos_to_xml(node, parent=None):
238
+ attrs = node.get("attributes", {})
239
+ t = attrs.get("type", "")
240
+ bundle = attrs.get("bundleName", "")
241
+ if t in HMOS_SKIP_TYPES:
242
+ return None
243
+ if bundle in HMOS_SKIP_BUNDLES:
244
+ return None
245
+ xml_attrs = {
246
+ "class": t,
247
+ "resource-id": attrs.get("key", ""),
248
+ "text": attrs.get("text", ""),
249
+ "content-desc": attrs.get("description", ""),
250
+ "clickable": attrs.get("clickable", "false"),
251
+ "long-clickable": attrs.get("longClickable", "false"),
252
+ "scrollable": attrs.get("scrollable", "false"),
253
+ "enabled": attrs.get("enabled", "true"),
254
+ "focused": attrs.get("focused", "false"),
255
+ "bounds": attrs.get("bounds", ""),
256
+ "package": bundle,
257
+ }
258
+ el = ET.SubElement(parent, "node", xml_attrs) if parent is not None \
259
+ else ET.Element("node", xml_attrs)
260
+ for child in node.get("children", []):
261
+ hmos_to_xml(child, el)
262
+ return el
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Android capture
267
+ # ---------------------------------------------------------------------------
268
+ def _adb_dump_xml(output_dir: Path, serial=None) -> str:
269
+ remote_path = "/sdcard/ui_dump.xml"
270
+ adb_shell("rm -f /sdcard/ui_dump.xml && uiautomator dump /sdcard/ui_dump.xml",
271
+ serial=serial, timeout=20)
272
+ for _ in range(6):
273
+ check = adb_shell("ls", remote_path, serial=serial)
274
+ if remote_path in check:
275
+ break
276
+ time.sleep(0.2)
277
+ xml_content = adb_shell("cat", remote_path, serial=serial, timeout=15)
278
+ if xml_content:
279
+ (output_dir / "view.xml").write_text(xml_content, encoding="utf-8")
280
+ return xml_content
281
+
282
+
283
+ def _adb_dump_xml_quick(serial=None) -> str:
284
+ remote_path = "/sdcard/ui_dump.xml"
285
+ adb_shell("rm -f /sdcard/ui_dump.xml && uiautomator dump /sdcard/ui_dump.xml",
286
+ serial=serial, timeout=20)
287
+ for _ in range(6):
288
+ check = adb_shell("ls", remote_path, serial=serial)
289
+ if remote_path in check:
290
+ break
291
+ time.sleep(0.2)
292
+ return adb_shell("cat", remote_path, serial=serial, timeout=15)
293
+
294
+
295
+ def _adb_screenshot(output_dir: Path, filename="screenshot.png", serial=None):
296
+ local_path = output_dir / filename
297
+ cmd = ["adb"]
298
+ if serial:
299
+ cmd += ["-s", serial]
300
+ cmd += ["exec-out", "screencap", "-p"]
301
+ try:
302
+ result = subprocess.run(cmd, capture_output=True, timeout=15)
303
+ if result.stdout:
304
+ local_path.write_bytes(result.stdout)
305
+ except subprocess.TimeoutExpired:
306
+ print(f" [WARN] screenshot timeout")
307
+
308
+
309
+ def _adb_get_activity(serial=None) -> dict:
310
+ info = {}
311
+ act_out = adb_shell("dumpsys", "activity", "activities", serial=serial)
312
+ win_out = adb_shell("dumpsys", "window", serial=serial)
313
+ for line in act_out.splitlines():
314
+ if re.search(r"ResumedActivity|topActivity", line, re.IGNORECASE):
315
+ info["resumed_activity"] = line.strip()
316
+ break
317
+ for line in win_out.splitlines():
318
+ if "mCurrentFocus" in line:
319
+ info["current_focus"] = line.strip()
320
+ break
321
+ if "mFocusedApp" in line:
322
+ info.setdefault("focused_app", line.strip())
323
+ return info
324
+
325
+
326
+ def _adb_scroll_and_capture(output_dir: Path, container_bounds, max_scrolls, initial_xml, serial=None):
327
+ x1, y1, x2, y2 = container_bounds
328
+ cx = (x1 + x2) // 2
329
+ height = y2 - y1
330
+ swipe_from_y = y1 + int(height * 0.7)
331
+ swipe_to_y = y1 + int(height * 0.3)
332
+
333
+ xml_list = []
334
+ prev_keys = _extract_node_keys(initial_xml) if initial_xml else None
335
+ scroll_count = 0
336
+
337
+ for i in range(max_scrolls):
338
+ adb_shell("input", "swipe",
339
+ str(cx), str(swipe_from_y), str(cx), str(swipe_to_y),
340
+ str(SCROLL_DURATION_MS), serial=serial)
341
+ adb_wait_idle(serial)
342
+
343
+ new_xml = _adb_dump_xml_quick(serial=serial)
344
+ new_keys = _extract_node_keys(new_xml)
345
+
346
+ if prev_keys is not None and new_keys == prev_keys:
347
+ print(f" Reached bottom after {i + 1} scroll(s)")
348
+ break
349
+ prev_keys = new_keys
350
+ scroll_count += 1
351
+ xml_list.append(new_xml)
352
+
353
+ xml_path = output_dir / f"view_scroll_{i + 1}.xml"
354
+ xml_path.write_text(new_xml, encoding="utf-8")
355
+
356
+ _adb_screenshot(output_dir, f"screenshot_scroll_{i + 1}.png", serial=serial)
357
+ print(f" Scroll {i + 1}: XML + screenshot saved")
358
+
359
+ if scroll_count > 0:
360
+ print(f" Restoring initial position: scrolling back {scroll_count} time(s)...")
361
+ for i in range(scroll_count):
362
+ adb_shell("input", "swipe",
363
+ str(cx), str(swipe_to_y), str(cx), str(swipe_from_y),
364
+ str(SCROLL_DURATION_MS), serial=serial)
365
+ adb_wait_idle(serial)
366
+
367
+ return xml_list
368
+
369
+
370
+ def capture_android(output_dir: Path, serial=None, max_scrolls=10):
371
+ output_dir.mkdir(parents=True, exist_ok=True)
372
+ print("[1/4] Dumping view XML...")
373
+ xml_content = _adb_dump_xml(output_dir, serial=serial)
374
+ if not xml_content:
375
+ print("[ERR] Failed to dump XML")
376
+ return
377
+
378
+ print("[2/4] Taking screenshot...")
379
+ _adb_screenshot(output_dir, serial=serial)
380
+
381
+ print("[3/4] Checking scrollable containers...")
382
+ containers = find_scrollable_containers(xml_content)
383
+ all_xml_contents = [xml_content]
384
+ if containers:
385
+ container = containers[0]
386
+ print(f" Found scrollable: {container['class']} {container.get('resource-id', '')}")
387
+ scroll_xmls = _adb_scroll_and_capture(
388
+ output_dir, container["bounds"], max_scrolls, xml_content, serial=serial,
389
+ )
390
+ all_xml_contents.extend(scroll_xmls)
391
+ print(f" Scroll capture done: {len(scroll_xmls)} scroll(s)")
392
+ else:
393
+ print(" No scrollable container found")
394
+
395
+ print("[4/4] Building meta.json...")
396
+ activity_info = _adb_get_activity(serial=serial)
397
+
398
+ seen_clickable = set()
399
+ clickable = []
400
+ for idx, xc in enumerate(all_xml_contents):
401
+ for elem in get_clickable_elements(xc):
402
+ key = (
403
+ elem.get("resource-id", ""),
404
+ elem.get("text", ""),
405
+ elem.get("content-desc", ""),
406
+ elem.get("class", ""),
407
+ elem.get("bounds", ""),
408
+ elem.get("long-clickable", False),
409
+ )
410
+ if key not in seen_clickable:
411
+ seen_clickable.add(key)
412
+ elem["scroll_index"] = idx
413
+ clickable.append(elem)
414
+
415
+ meta = {
416
+ "timestamp": datetime.now().isoformat(),
417
+ "device": "adb",
418
+ "activity_info": activity_info,
419
+ "clickable_count": len(clickable),
420
+ "clickable_elements": clickable,
421
+ }
422
+ (output_dir / "meta.json").write_text(
423
+ json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8",
424
+ )
425
+
426
+ print(f"\nDone! Files saved to {output_dir}")
427
+
428
+
429
+ # ---------------------------------------------------------------------------
430
+ # HarmonyOS capture
431
+ # ---------------------------------------------------------------------------
432
+ def _hdc_dump_layout(output_dir: Path) -> tuple[str, str]:
433
+ """Dump layout JSON, convert to XML. Returns (xml_string, page_name)."""
434
+ out = hdc_shell("uitest dumpLayout")
435
+ match = re.search(r"saved to:(.+\.json)", out)
436
+ if not match:
437
+ raise RuntimeError(f"Cannot parse dumpLayout output: {out}")
438
+ remote_json = match.group(1).strip()
439
+
440
+ local_json = str(output_dir / "_tmp_layout.json")
441
+ hdc("file", "recv", remote_json, local_json)
442
+
443
+ with open(local_json, "r", encoding="utf-8") as f:
444
+ data = json.load(f)
445
+
446
+ page_name = ""
447
+ if data.get("children"):
448
+ page_name = data["children"][0].get("attributes", {}).get("pagePath", "")
449
+ page_name = page_name.split("/")[-1] if page_name else "unknown"
450
+
451
+ root_el = hmos_to_xml(data)
452
+ tree = ET.ElementTree(root_el)
453
+ ET.indent(tree, space=" ")
454
+
455
+ xml_path = output_dir / "view.xml"
456
+ tree.write(str(xml_path), encoding="unicode", xml_declaration=True)
457
+
458
+ xml_str = ET.tostring(root_el, encoding="unicode")
459
+
460
+ os.remove(local_json)
461
+ return xml_str, page_name
462
+
463
+
464
+ def _hdc_dump_layout_quick(output_dir: Path) -> str:
465
+ """Dump layout and return XML string without saving to view.xml."""
466
+ out = hdc_shell("uitest dumpLayout")
467
+ match = re.search(r"saved to:(.+\.json)", out)
468
+ if not match:
469
+ return ""
470
+ remote_json = match.group(1).strip()
471
+
472
+ local_json = str(output_dir / "_tmp_layout.json")
473
+ hdc("file", "recv", remote_json, local_json)
474
+
475
+ try:
476
+ with open(local_json, "r", encoding="utf-8") as f:
477
+ data = json.load(f)
478
+ root_el = hmos_to_xml(data)
479
+ xml_str = ET.tostring(root_el, encoding="unicode") if root_el else ""
480
+ finally:
481
+ if os.path.exists(local_json):
482
+ os.remove(local_json)
483
+ return xml_str
484
+
485
+
486
+ def _hdc_screenshot(output_dir: Path, filename="screenshot.jpeg"):
487
+ remote_img = "/data/local/tmp/screenshot.jpeg"
488
+ hdc_shell(f"snapshot_display -f {remote_img}")
489
+ local_path = output_dir / filename
490
+ hdc("file", "recv", remote_img, str(local_path))
491
+
492
+
493
+ def _hdc_scroll_and_capture(output_dir: Path, container_bounds, max_scrolls, initial_xml):
494
+ x1, y1, x2, y2 = container_bounds
495
+ cx = (x1 + x2) // 2
496
+ height = y2 - y1
497
+ swipe_from_y = y1 + int(height * 0.7)
498
+ swipe_to_y = y1 + int(height * 0.3)
499
+
500
+ xml_list = []
501
+ prev_keys = _extract_node_keys(initial_xml) if initial_xml else None
502
+ scroll_count = 0
503
+
504
+ for i in range(max_scrolls):
505
+ hdc_shell(
506
+ "uitest", "uiInput", "swipe",
507
+ str(cx), str(swipe_from_y), str(cx), str(swipe_to_y),
508
+ str(HDC_SWIPE_SPEED),
509
+ )
510
+ hdc_wait_idle()
511
+
512
+ new_xml = _hdc_dump_layout_quick(output_dir)
513
+ new_keys = _extract_node_keys(new_xml)
514
+
515
+ if prev_keys is not None and new_keys == prev_keys:
516
+ print(f" Reached bottom after {i + 1} scroll(s)")
517
+ break
518
+ prev_keys = new_keys
519
+ scroll_count += 1
520
+ xml_list.append(new_xml)
521
+
522
+ xml_path = output_dir / f"view_scroll_{i + 1}.xml"
523
+ xml_path.write_text(new_xml, encoding="utf-8")
524
+
525
+ _hdc_screenshot(output_dir, f"screenshot_scroll_{i + 1}.jpeg")
526
+ print(f" Scroll {i + 1}: XML + screenshot saved")
527
+
528
+ if scroll_count > 0:
529
+ print(f" Restoring initial position: scrolling back {scroll_count} time(s)...")
530
+ for i in range(scroll_count):
531
+ hdc_shell(
532
+ "uitest", "uiInput", "swipe",
533
+ str(cx), str(swipe_to_y), str(cx), str(swipe_from_y),
534
+ str(HDC_SWIPE_SPEED),
535
+ )
536
+ hdc_wait_idle()
537
+
538
+ return xml_list
539
+
540
+
541
+ def capture_hdc(output_dir: Path, max_scrolls=10):
542
+ output_dir.mkdir(parents=True, exist_ok=True)
543
+ print("[1/3] Dumping layout (JSON -> XML)...")
544
+ xml_content, page_name = _hdc_dump_layout(output_dir)
545
+ if not xml_content:
546
+ print("[ERR] Failed to dump layout")
547
+ return
548
+ print(f" Page: {page_name}")
549
+
550
+ print("[2/3] Taking screenshot...")
551
+ _hdc_screenshot(output_dir)
552
+
553
+ print("[3/3] Checking scrollable containers...")
554
+ containers = find_scrollable_containers(xml_content)
555
+ if containers:
556
+ container = containers[0]
557
+ print(f" Found scrollable: {container['class']} {container.get('resource-id', '')}")
558
+ scroll_xmls = _hdc_scroll_and_capture(
559
+ output_dir, container["bounds"], max_scrolls, xml_content,
560
+ )
561
+ print(f" Scroll capture done: {len(scroll_xmls)} scroll(s)")
562
+ else:
563
+ print(" No scrollable container found")
564
+
565
+ print(f"\nDone! Files saved to {output_dir}")
566
+
567
+
568
+ # ---------------------------------------------------------------------------
569
+ # CLI
570
+ # ---------------------------------------------------------------------------
571
+ def main():
572
+ parser = argparse.ArgumentParser(description="Capture current page: view tree XML + screenshots (Android / HarmonyOS)")
573
+ parser.add_argument("--device", choices=["hdc", "adb"], default="hdc",
574
+ help="Device type: hdc=HarmonyOS, adb=Android (default: hdc)")
575
+ parser.add_argument("--output-dir", "-o", type=str, required=True,
576
+ help="Output directory for captured files")
577
+ parser.add_argument("--max-scrolls", type=int, default=10,
578
+ help="Max scroll count per container (default: 10)")
579
+ parser.add_argument("--serial", type=str, default=None,
580
+ help="ADB device serial (optional, for multi-device)")
581
+ args = parser.parse_args()
582
+
583
+ output_dir = Path(args.output_dir)
584
+
585
+ if args.device == "adb":
586
+ capture_android(output_dir, serial=args.serial, max_scrolls=args.max_scrolls)
587
+ else:
588
+ capture_hdc(output_dir, max_scrolls=args.max_scrolls)
589
+
590
+
591
+ if __name__ == "__main__":
592
+ sys.exit(main())
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: hmos-ui-align-batch
3
+ description: Batch-convert multiple Android Activity UI snapshots to HarmonyOS ArkUI (ArkTS) pages. Use when the user wants to migrate Android UI pages to HarmonyOS in bulk, port multiple Activity screens to ArkTS, or run an Android-to-HarmonyOS UI conversion across a folder of page snapshots (page_NNNN_ActivityName). Triggers on phrases like "把安卓页面迁移到鸿蒙", "Android UI 转鸿蒙", "批量转 ArkTS", "hmos-ui-align_batch", or any request that supplies an Android project path + Harmony project path + a directory of page snapshots.
4
+ ---
5
+
6
+ # hmos-ui-align_batch — Android → HarmonyOS UI Batch Conversion
7
+
8
+ Batch-convert a set of Android page snapshots (each snapshot is a `page_NNNN_ActivityName/` directory containing `meta.json` + `view.xml` + optional `screenshot.png`) into HarmonyOS ArkTS pages following MVVM architecture.
9
+
10
+ ## Step 1 — Parse User Request
11
+
12
+ Extract 4 paths from the user's message (natural language, no fixed format):
13
+
14
+ | Variable | Meaning | Typical Phrases |
15
+ |---|---|---|
16
+ | `android_project_dir` | Android source project root | "Android project path", "安卓项目", "source project" |
17
+ | `harmony_project_dir` | HarmonyOS target project root | "target Harmony project", "鸿蒙项目", "output project" |
18
+ | `ui_info_root` | **Parent directory containing all `page_NNNN_*` subdirectories** | "page screenshots and view tree", "page snapshots", "page folder" |
19
+ | `pages` (optional) | User-explicitly listed page subset | When user lists `1. .../page_0001_X` ... take these |
20
+
21
+ **`references_dir` and MVVM document directories always use skill-bundled relative paths** — **do not** accept user-provided overrides:
22
+
23
+ - mappings: `./references/mappings/`
24
+ - mvvm: `./references/mvvm/`
25
+
26
+ If any required path cannot be extracted from the user's message, **ask the user** — do not guess.
27
+
28
+ ## Step 2 — Resource Conversion
29
+
30
+ Invoke the `hmos-resources-convert` skill with the following parameters:
31
+ 1. **Android project path** = `android_project_dir`
32
+ 2. **HarmonyOS project output path** = `harmony_project_dir`
33
+ 3. **resource_mapping_path** = generate the resource mapping document under `harmony_project_dir` at `${harmony_project_dir}/resource_mapping.md`
34
+
35
+ This skill batch-converts Android resources (strings, colors, drawables, images, etc.) to HarmonyOS resource format and generates the mapping document.
36
+
37
+ ## Step 3 — Page Exploration
38
+
39
+ 1. Check if `ui_info_root` already contains `page_*` subdirectories. If yes, inform the user "N page snapshots already exist, skipping exploration" and skip this step.
40
+ 2. Otherwise, determine the target App package name (`package`). If the user didn't provide it, extract the `package` attribute from `AndroidManifest.xml` in `android_project_dir`; if still unavailable, ask the user.
41
+ 3. Execute with Bash:
42
+ ```
43
+ python ./scripts/android_parse_fast.py --package <package_name> --output <ui_info_root_absolute_path>
44
+ ```
45
+ This script connects to the Android emulator via ADB, BFS-traverses all reachable pages in the App, and for each page saves `screenshot.png`, `view.xml`, `meta.json` into `page_NNNN_ActivityName/` subdirectories. It also generates `index.json` and `report.html` under `ui_info_root`.
46
+ 4. After execution, confirm that `index.json` has been generated under `ui_info_root`. If not, report an error and abort.
47
+
48
+ ## Step 4 — Discover Pages
49
+
50
+ If the user did not explicitly list pages: use Glob `page_*_*` under `ui_info_root` to find all page subdirectories, sorted by name (`page_0001_*` first).
51
+
52
+ If the user explicitly listed pages (e.g. "1. .../page_0001_MainActivity ... 19. .../page_0019_MainActivity"): use the user's list.
53
+
54
+ After discovery, create a task for each page via TaskCreate to track progress.
55
+
56
+ ## Step 5 — Per-Page Conversion
57
+
58
+ **For each page**, invoke the Agent tool (`subagent_type: general-purpose`) to perform conversion. The prompt must include:
59
+
60
+ 1. The page's `ui_info` absolute path (`{ui_info_root}/page_NNNN_ActivityName`)
61
+ 2. `android_project_dir`, `harmony_project_dir`
62
+ 3. Absolute paths for mappings and mvvm directories (skill-bundled)
63
+ 4. **Copy the full contents of `references/conversion-procedure.md` as the sub-agent's work instructions** (use Read to read it and paste into the prompt)
64
+
65
+ Why use sub-agents: single-page conversion reads many mapping/MVVM documents + Android source code. Sub-agents isolate context to avoid overwhelming the main session.
66
+
67
+ After receiving a sub-agent's conversion report, append a brief summary to a cumulative report and mark the corresponding task as completed.
68
+
69
+ **Execute serially** — do not parallelize (multiple sub-agents modifying the same Harmony project simultaneously will conflict).
70
+
71
+ ## Step 6 — Unified Build Fix
72
+
73
+ After all page conversions are complete, call the `hmos_fix_build_errors` skill **only once at the end** (passing `harmony_project_dir`) to make the entire project compile. Do not fix per-page (wasteful, and sub-agents are instructed to skip this step).
74
+
75
+ ## Step 7 — Summary Report
76
+
77
+ Output an overview:
78
+
79
+ ```
80
+ ## Batch Conversion Overview
81
+ - Pages processed: N
82
+ - Successful: M Failed/partial: K
83
+ - Final build status: SUCCESS / PARTIAL
84
+
85
+ ### Per-Page Results
86
+ | Page | Activity | Output File | Status |
87
+ |---|---|---|---|
88
+ | 0001 | MainActivity | pages/MainPage.ets | ✓ |
89
+ | ... | | | |
90
+
91
+ ### Manual Follow-up TODOs
92
+ - (Aggregate mocked items / unimplemented navigation / business logic from sub-reports)
93
+ ```
94
+
95
+ ## Key Constraints
96
+
97
+ - **Never** let a per-page sub-agent call `hmos_fix_build_errors`; do it once in Step 6 only.
98
+ - If the user's `harmony_project_dir` lacks entry/resources subdirectories or the target project doesn't exist, stop and ask — do not generate code.
99
+ - If input paths are Windows paths (with backslashes), pass them as-is to sub-agents; internal Read/Glob support them.