@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.
- package/README.md +141 -124
- package/agents/build-fixer.md +1 -0
- package/agents/code-review-fix.md +1 -0
- package/agents/code-reviewer.md +1 -0
- package/agents/logic-coding.md +1 -0
- package/agents/logic-context-builder.md +1 -0
- package/agents/review-fixer.md +1 -0
- package/agents/self-test-fixer.md +1 -0
- package/agents/self-tester.md +260 -233
- package/agents/spec-generator.md +1 -0
- package/agents/test-tools/autotest/README.md +223 -0
- package/agents/test-tools/autotest/config.yaml.example +58 -0
- package/agents/test-tools/autotest/pyproject.toml +16 -0
- package/agents/test-tools/autotest/report_tool.py +759 -0
- package/agents/test-tools/autotest/self_test_runner.py +773 -0
- package/agents/test-tools/autotest/testcases_schema.md +143 -0
- package/agents/test-tools/autotest/testcases_tool.py +215 -0
- package/agents/test-tools/autotest/uv.lock +3156 -0
- package/agents/test-tools/harmony_autotest-0.1.0-py3-none-any.whl +0 -0
- package/agents/test-tools/hypium-6.1.0.210-py3-none-any.whl +0 -0
- package/agents/test-tools/hypium_mcp-0.6.5-py3-none-any.whl +0 -0
- package/agents/test-tools/xdevice-6.1.0.210-py3-none-any.whl +0 -0
- package/agents/test-tools/xdevice_devicetest-6.1.0.210-py3-none-any.whl +0 -0
- package/agents/test-tools/xdevice_ohos-6.1.0.210-py3-none-any.whl +0 -0
- package/dist/cli/config-store.js +27 -2
- package/dist/cli/config.js +17 -6
- package/dist/cli/index.js +3 -2
- package/dist/cli/init.js +135 -22
- package/dist/cli/mcp.js +2 -2
- package/dist/context/index.js +165 -69
- package/package.json +59 -60
- package/skills/code-dev-review-fix/SKILL.md +279 -0
- package/skills/code-dev-review-fix-workspace/evals/evals.json +56 -0
- package/skills/code-dev-review-fix-workspace/iteration-1/routing-results.md +23 -0
- package/skills/convert_pipeline/SKILL.md +423 -439
- package/skills/hmos-resources-convert/SKILL.md +623 -0
- package/skills/hmos-resources-convert/evals/evals.json +171 -0
- package/skills/hmos-resources-convert/references/conversion-rules.md +663 -0
- package/skills/hmos-resources-convert/references/dependency-analysis-rules.md +388 -0
- package/skills/hmos-resources-convert/references/resource-mapping-rules.md +457 -0
- package/skills/hmos-resources-convert/references/xml-drawable-to-svg-rules.md +513 -0
- package/skills/hmos-resources-convert/template/AppScope/app.json5 +10 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +8 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +7 -0
- package/skills/hmos-resources-convert/template/build-profile.json5 +42 -0
- package/skills/hmos-resources-convert/template/code-linter.json5 +32 -0
- package/skills/hmos-resources-convert/template/entry/build-profile.json5 +33 -0
- package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +6 -0
- package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +23 -0
- package/skills/hmos-resources-convert/template/entry/oh-package.json5 +10 -0
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +48 -0
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +16 -0
- package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +23 -0
- package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +55 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +8 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +8 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +16 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +7 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +3 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +5 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +8 -0
- package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +2 -0
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +35 -0
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +5 -0
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +16 -0
- package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +5 -0
- package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +33 -0
- package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +23 -0
- package/skills/hmos-resources-convert/template/hvigorfile.ts +6 -0
- package/skills/hmos-resources-convert/template/oh-package-lock.json5 +28 -0
- package/skills/hmos-resources-convert/template/oh-package.json5 +10 -0
- package/skills/hmos-resources-convert/tools/apktool.bat +85 -0
- package/skills/hmos-resources-convert/tools/apktool_3.0.1.jar +0 -0
- package/skills/hmos-ui-align/SKILL.md +182 -0
- package/skills/hmos-ui-align/config-example.json +11 -0
- package/skills/hmos-ui-align/config.json +11 -0
- package/skills/hmos-ui-align/diff_analysis.md +53 -0
- package/skills/hmos-ui-align/page_align.md +62 -0
- package/skills/hmos-ui-align/readme.md +231 -0
- package/skills/hmos-ui-align/references/Comparison_Template.md +2 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/skills/hmos-ui-align/references/UI_Analysis_Template.md +4 -0
- package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2535 -0
- package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
- package/skills/hmos-ui-align/references/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
- package/skills/hmos-ui-align/scripts/app_feature_verify.py +443 -0
- package/skills/hmos-ui-align/scripts/navigation-capure.md +37 -0
- package/skills/hmos-ui-align/scripts/page_capture.py +592 -0
- package/skills/hmos-ui-align-batch/SKILL.md +99 -0
- package/skills/hmos-ui-align-batch/references/conversion-procedure.md +180 -0
- package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2535 -0
- package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
- package/skills/hmos-ui-align-batch/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/skills/hmos-ui-align-batch/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
- 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
- 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
- package/skills/hmos-ui-align-batch/scripts/android_parse_fast.py +1606 -0
- package/skills/self-test/SKILL.md +369 -0
- package/skills/self-test/readme.md +309 -0
- package/skills/spec-generator-skill/SKILL.md +332 -0
- package/skills/spec-generator-skill/references/android-platform-tokens.md +105 -0
- package/skills/spec-generator-skill/references/spec-sample-1.md +78 -0
- package/skills/spec-generator-skill/references/spec-sample-2.md +58 -0
- package/skills/spec-generator-skill/references/spec-sample-3.md +116 -0
- package/skills/spec-generator-skill/references/step4-report-template.md +33 -0
- package/agents/self-test-setup.md +0 -165
- package/dist/context/resources/sdkConfig.json +0 -24
- 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.
|