@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,117 @@
|
|
|
1
|
+
# Android-to-HarmonyOS UI Layout Mapping Reference
|
|
2
|
+
|
|
3
|
+
<!-- TODO: Fill in the actual UI layout mapping content -->
|
|
4
|
+
<!-- This reference maps Android layout containers to HarmonyOS ArkUI layout equivalents -->
|
|
5
|
+
<!-- Covers: LinearLayout→Column/Row, FrameLayout→Stack, ConstraintLayout→RelativeContainer, RecyclerView→List+LazyForEach, ScrollView→Scroll, etc. -->
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## 布局容器映射
|
|
9
|
+
|
|
10
|
+
| 安卓部分UI | 对应鸿蒙部分UI | 其他说明 |
|
|
11
|
+
|-----------|---------------|---------|
|
|
12
|
+
| `LinearLayout` (vertical) | `Column` 组件 | 纵向线性布局,支持 `.justifyContent()`、`.alignItems()` |
|
|
13
|
+
| `LinearLayout` (horizontal) | `Row` 组件 | 横向线性布局 |
|
|
14
|
+
| `FrameLayout` | `Stack` 组件 | 层叠布局,支持 `.alignContent()` |
|
|
15
|
+
| `RelativeLayout` | `RelativeContainer` 组件 | 相对布局,通过 `.alignRules()` 设置定位规则 |
|
|
16
|
+
| `ConstraintLayout` | `RelativeContainer` / 自定义布局 | 约束布局,复杂场景可用自定义 `onMeasureSize`/`onPlaceChildren` |
|
|
17
|
+
| `FlexboxLayout` | `Flex` 组件 | 弹性布局,支持 `FlexDirection`/`FlexWrap`/`FlexAlign` 标准 Flexbox |
|
|
18
|
+
| `GridLayout` | `Grid` 组件 | 网格布局,通过 `.columnsTemplate('1fr 1fr 1fr')` 定义列 |
|
|
19
|
+
| `TableLayout` | `Grid` 组件 | 表格布局,使用 `Grid` 组件模拟表格结构 |
|
|
20
|
+
| `CoordinatorLayout` | 组合 `Scroll` + `Column` + 动画 | 无直接对应,需组合模拟 AppBar 折叠效果 |
|
|
21
|
+
| `AppBarLayout` / `CollapsingToolbarLayout` | `Navigation` 标题栏 + `titleMode` | 折叠工具栏,`NavigationTitleMode.Full`→`.Mini` 自动切换 |
|
|
22
|
+
| `ScrollView` | `Scroll` 组件 | 滚动容器,通过 `Scroller` 控制器编程式滚动 |
|
|
23
|
+
| `HorizontalScrollView` | `Scroll` + `ScrollDirection.Horizontal` | 横向滚动 |
|
|
24
|
+
| `NestedScrollView` | `Scroll` + `.nestedScroll()` | 嵌套滚动策略配置 |
|
|
25
|
+
| `ViewPager` / `ViewPager2` | `Swiper` 组件 | 页面滑动切换,支持 `.autoPlay()`、`.indicator()`、`.loop()` |
|
|
26
|
+
| `SwipeRefreshLayout` | `Refresh` 组件 | 下拉刷新,通过 `.onRefreshing()` 回调处理刷新 |
|
|
27
|
+
| `RecyclerView` | `List` 组件 | 高性能列表,使用 `List` + `ForEach`/`LazyForEach` 渲染子组件 |
|
|
28
|
+
| `RecyclerView` (GridLayoutManager) | `Grid` 组件 | 网格布局,通过 `.columnsTemplate()` 设置列数 |
|
|
29
|
+
| `RecyclerView` (StaggeredGridLayoutManager) | `WaterFlow` 组件 | 瀑布流布局 |
|
|
30
|
+
| `ListView` | `List` 组件 | 传统列表,使用 `List` 组件实现 |
|
|
31
|
+
| `GridView` | `Grid` 组件 | 网格视图,使用 `Grid` 组件实现 |
|
|
32
|
+
| `DrawerLayout` / `NavigationView` | `SideBarContainer` 组件 | 侧边栏/抽屉 |
|
|
33
|
+
| `TabLayout` + `ViewPager` | `Tabs` 组件 | 标签页,`Tabs` 整合标签栏和内容页切换 |
|
|
34
|
+
| `BottomNavigationView` | `Tabs` + `BarPosition.End` | 底部导航栏 |
|
|
35
|
+
| `CardView` | `Card` 组件 / `Container` + 样式 | 卡片视图,使用 `Card` 组件或自定义样式实现 |
|
|
36
|
+
| `Space` | `Blank` 组件 | 空白间隔,使用 `Blank` 组件实现 |
|
|
37
|
+
| `FragmentContainerView` | `NavDestination` / `@Component` | 片段容器视图,映射到 `NavDestination` 或自定义组件 |
|
|
38
|
+
| `RecyclerViewFastScroller` | 自定义组件 | 列表快速滚动条,需自定义实现 |
|
|
39
|
+
| `MySearchMenu` (自定义) | `Search` 组件 + 自定义菜单 | 自定义搜索菜单,使用 `Search` 组件并添加自定义菜单 |
|
|
40
|
+
| `MyViewPager` (自定义) | `Swiper` 组件 | 自定义视图分页器,使用 `Swiper` 组件实现 |
|
|
41
|
+
| `MyRecyclerView` (自定义) | `List` 组件 | 自定义 RecyclerView,使用 `List` 组件实现 |
|
|
42
|
+
| `MyTextView` (自定义) | `Text` 组件 | 自定义 TextView,使用 `Text` 组件实现 |
|
|
43
|
+
| `MyAppCompatCheckbox` (自定义) | `Checkbox` 组件 | 自定义 Checkbox,使用 `Checkbox` 组件实现 |
|
|
44
|
+
|
|
45
|
+
## 布局属性映射
|
|
46
|
+
|
|
47
|
+
| 安卓部分UI | 对应鸿蒙部分UI | 其他说明 |
|
|
48
|
+
|-----------|---------------|---------|
|
|
49
|
+
| `layout_weight` | `.layoutWeight()` / `.flexGrow()` | 权重分配 |
|
|
50
|
+
| `padding` / `margin` | `.padding()` / `.margin()` | 支持统一或分别指定 `{ top, right, bottom, left }` |
|
|
51
|
+
| `View.GONE` / `View.INVISIBLE` | `Visibility.None` / `Visibility.Hidden` | `None`=不占位(GONE);`Hidden`=占位不显示(INVISIBLE) |
|
|
52
|
+
| `android:id` | `.id('xxx')` / 组件引用变量 | 组件标识 |
|
|
53
|
+
| `android:background` | `.backgroundColor()` / `.backgroundImage()` | 背景 |
|
|
54
|
+
| `android:alpha` | `.opacity()` | 透明度 0~1 |
|
|
55
|
+
| `android:elevation` | `.shadow({ radius, color, offsetX, offsetY })` | 阴影 |
|
|
56
|
+
| `android:rotation` | `.rotate({ angle: xxx })` | 旋转 |
|
|
57
|
+
| `android:scaleX/Y` | `.scale({ x: xxx, y: xxx })` | 缩放 |
|
|
58
|
+
| `android:translationX/Y` | `.translate({ x: xxx, y: xxx })` | 平移 |
|
|
59
|
+
| `android:clipToOutline` | `.clip(true)` / `.clipShape()` | 裁剪 |
|
|
60
|
+
| `setEnabled(false)` | `.enabled(false)` | 禁用状态 |
|
|
61
|
+
| `android:focusable` | `.focusable(true)` | 可聚焦 |
|
|
62
|
+
| `android:focusableInTouchMode` | `.focusable(true)` + `.focusOnTouch(true)` | 触摸模式下可聚焦 |
|
|
63
|
+
| `android:clickable` | `.enabled(true)` + `.onClick()` | 可点击 |
|
|
64
|
+
| `android:longClickable` | `.gesture(LongPressGesture())` | 可长按 |
|
|
65
|
+
| `android:scrollbars` | `.scrollBar()` | 滚动条 |
|
|
66
|
+
| `android:fadeScrollbars` | `.scrollBar().fade(true)` | 滚动条自动隐藏 |
|
|
67
|
+
| `android:overScrollMode` | `.overScrollMode()` | 过度滚动模式 |
|
|
68
|
+
| `GradientDrawable` | `.linearGradient()` / `.radialGradient()` | 渐变背景 |
|
|
69
|
+
| `CardView` radius | `.borderRadius()` | 圆角 |
|
|
70
|
+
| `GradientDrawable.setStroke()` | `.border({ width, color, radius })` | 边框 |
|
|
71
|
+
| `android:layout_width` / `android:layout_height` | `.width()` / `.height()` | 宽高设置,支持具体数值、百分比、`'match_parent'`/`'wrap_content'` |
|
|
72
|
+
| `android:gravity` | `.alignItems()` / `.justifyContent()` | 内容对齐方式 |
|
|
73
|
+
| `android:layout_gravity` | `.alignSelf()` | 子组件在父容器中的对齐方式 |
|
|
74
|
+
| `android:orientation` (LinearLayout) | `Column` / `Row` | 布局方向 |
|
|
75
|
+
| `android:divider` (LinearLayout) | `Divider` 组件 | 分隔线 |
|
|
76
|
+
| `android:showDividers` (LinearLayout) | `Divider` 组件位置 | 分隔线显示位置 |
|
|
77
|
+
|
|
78
|
+
## 页面结构映射
|
|
79
|
+
|
|
80
|
+
| 安卓部分UI | 对应鸿蒙部分UI | 其他说明 |
|
|
81
|
+
|-----------|---------------|---------|
|
|
82
|
+
| `Activity` | `PageAbility` | 主UI容器 |
|
|
83
|
+
| `Fragment` | `NavDestination` / `@Component` | 通过 Navigation + NavDestination 实现子页面路由 |
|
|
84
|
+
| `Service` | `ServiceAbility` | 后台服务 |
|
|
85
|
+
| `BroadcastReceiver` | `EventHub` + `@Event` | 广播接收器,使用事件订阅机制 |
|
|
86
|
+
| `ContentProvider` | `DataAbility` | 内容提供者 |
|
|
87
|
+
| `Toolbar` / `ActionBar` | `Navigation` 标题栏 | 通过 `.title()` 和 `.menus()` 配置 |
|
|
88
|
+
| `Intent` | `router` 模块 | 页面导航,使用 `router.push()`、`router.replace()` 等 |
|
|
89
|
+
| `Bundle` | `router.push({ params: {} })` | 页面间传递数据 |
|
|
90
|
+
| `onActivityResult` | `router.push()` + 回调 | 页面返回结果 |
|
|
91
|
+
| `startActivityForResult` | `router.push()` + 回调 | 启动页面并获取结果 |
|
|
92
|
+
|
|
93
|
+
## 列表与可滚动组件映射
|
|
94
|
+
|
|
95
|
+
| 安卓部分UI | 对应鸿蒙部分UI | 其他说明 |
|
|
96
|
+
|-----------|---------------|---------|
|
|
97
|
+
| `RecyclerView.Adapter` | `LazyForEach` + `IDataSource` | 数据适配器,实现 `IDataSource` 接口配合 `LazyForEach` 懒加载 |
|
|
98
|
+
| `RecyclerView.ViewHolder` | `@Builder` / `@Component` | 声明式 UI 无需 ViewHolder 模式 |
|
|
99
|
+
| `LinearLayoutManager` | `List` 默认布局 | 纵向列表,`.listDirection(Axis.Horizontal)` 切换横向 |
|
|
100
|
+
| `GridLayoutManager` | `Grid` 组件 | 网格布局管理器 |
|
|
101
|
+
| `StaggeredGridLayoutManager` | `WaterFlow` 组件 | 瀑布流布局管理器 |
|
|
102
|
+
| `ItemTouchHelper` (swipe) | `ListItem.swipeAction()` | 列表项滑动操作菜单 |
|
|
103
|
+
| `ItemTouchHelper` (drag) | `ListItem.dragable()` | 列表项拖拽排序 |
|
|
104
|
+
| `DiffUtil` | `IDataSource` 的 `DataChangeListener` | 差异通知,`onDataAdd`/`onDataDelete`/`onDataChange` |
|
|
105
|
+
| `PagerAdapter` / `FragmentStateAdapter` | `Swiper` + `ForEach`/`LazyForEach` | 无需 Adapter 模式,直接循环渲染子页面 |
|
|
106
|
+
| `ListAdapter` | `LazyForEach` + `IDataSource` | 带 DiffUtil 的适配器 |
|
|
107
|
+
| `ArrayAdapter` | `ForEach` + 数组 | 数组适配器 |
|
|
108
|
+
| `BaseAdapter` | `ForEach` / `LazyForEach` | 基础适配器 |
|
|
109
|
+
|
|
110
|
+
## 迁移注意事项
|
|
111
|
+
|
|
112
|
+
1. **范式转换**:Android 使用命令式 UI(XML 布局 + Java/Kotlin 操作);ArkUI 使用声明式 UI(ArkTS 组件树 + 状态驱动渲染)
|
|
113
|
+
2. **Adapter 模式消除**:`RecyclerView.Adapter`、`PagerAdapter` 等被 `ForEach`/`LazyForEach` + `IDataSource` 替代
|
|
114
|
+
3. **Fragment → NavDestination**:Fragment 模块化 UI 能力映射到 `Navigation` + `NavDestination` 路由
|
|
115
|
+
4. **布局属性调整**:Android 的布局属性需要转换为 ArkUI 的链式调用语法
|
|
116
|
+
5. **性能优化**:使用 `LazyForEach` 进行懒加载,避免一次性渲染大量数据
|
|
117
|
+
6. **响应式布局**:使用 `Flex` 和 `Grid` 组件实现响应式布局,适应不同屏幕尺寸
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
应用页面导航工具 —— 使用 PhoneAgent 按指定路径操作设备,导航到目标页面。
|
|
5
|
+
|
|
6
|
+
支持 Android (ADB) 和 HarmonyOS (HDC) 两种设备类型。
|
|
7
|
+
|
|
8
|
+
依赖: pip install openpyxl phone-agent
|
|
9
|
+
运行前需确保: 设备已连接 (HDC/ADB)、模型 API 可用
|
|
10
|
+
|
|
11
|
+
用法:
|
|
12
|
+
# 自由 prompt 导航
|
|
13
|
+
python app_feature_verify.py --device adb --app Gallery --package com.example.gallery --prompt "打开相册,进入设置页面"
|
|
14
|
+
|
|
15
|
+
# 按功能路径导航
|
|
16
|
+
python app_feature_verify.py --app 简单图库 --package com.simplemobiletools.gallery.pro --task "L1相册详情L2更多L3筛选媒体文件"
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# 强制设置 UTF-8 编码以避免 Windows GBK 编码问题
|
|
26
|
+
if sys.platform == 'win32':
|
|
27
|
+
import io
|
|
28
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
29
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Excel 功能清单解析
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def parse_app_feature(
|
|
37
|
+
file_path: str | Path | None = None,
|
|
38
|
+
sheet_name: str | int = 0,
|
|
39
|
+
) -> list[dict]:
|
|
40
|
+
"""解析功能清单 Excel,返回功能清单列表。"""
|
|
41
|
+
import openpyxl
|
|
42
|
+
|
|
43
|
+
file_path = Path(file_path)
|
|
44
|
+
if not file_path.exists():
|
|
45
|
+
raise FileNotFoundError(f"文件不存在: {file_path}")
|
|
46
|
+
|
|
47
|
+
wb = openpyxl.load_workbook(file_path, read_only=True, data_only=True)
|
|
48
|
+
|
|
49
|
+
if isinstance(sheet_name, int):
|
|
50
|
+
sheet = wb.worksheets[sheet_name]
|
|
51
|
+
else:
|
|
52
|
+
sheet = wb[sheet_name]
|
|
53
|
+
|
|
54
|
+
rows = list(sheet.iter_rows(values_only=True))
|
|
55
|
+
wb.close()
|
|
56
|
+
|
|
57
|
+
if not rows:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
headers = [str(h) if h is not None else f"col_{i}" for i, h in enumerate(rows[0])]
|
|
61
|
+
|
|
62
|
+
result = []
|
|
63
|
+
for row in rows[1:]:
|
|
64
|
+
row_dict = {}
|
|
65
|
+
for i, val in enumerate(row):
|
|
66
|
+
key = headers[i] if i < len(headers) else f"col_{i}"
|
|
67
|
+
row_dict[key] = val
|
|
68
|
+
result.append(row_dict)
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Markdown 功能清单解析
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def parse_app_feature_md(file_path: str | Path) -> list[dict]:
|
|
78
|
+
"""解析 Markdown 测试用例文件,返回场景列表。"""
|
|
79
|
+
file_path = Path(file_path)
|
|
80
|
+
if not file_path.exists():
|
|
81
|
+
raise FileNotFoundError(f"文件不存在: {file_path}")
|
|
82
|
+
|
|
83
|
+
text = file_path.read_text(encoding="utf-8")
|
|
84
|
+
lines = text.splitlines()
|
|
85
|
+
|
|
86
|
+
results: list[dict] = []
|
|
87
|
+
current_feature = ""
|
|
88
|
+
current_scenario: dict | None = None
|
|
89
|
+
collecting_sub_items: str | None = None
|
|
90
|
+
|
|
91
|
+
for line in lines:
|
|
92
|
+
stripped = line.strip()
|
|
93
|
+
|
|
94
|
+
feat_match = re.match(r"^##\s+Feature:\s*(.+)$", stripped)
|
|
95
|
+
if feat_match:
|
|
96
|
+
if current_scenario is not None:
|
|
97
|
+
results.append(current_scenario)
|
|
98
|
+
current_scenario = None
|
|
99
|
+
current_feature = feat_match.group(1).strip()
|
|
100
|
+
collecting_sub_items = None
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
scen_match = re.match(r"^###\s+Scenario:\s*(.+)$", stripped)
|
|
104
|
+
if scen_match:
|
|
105
|
+
if current_scenario is not None:
|
|
106
|
+
results.append(current_scenario)
|
|
107
|
+
scenario_name = scen_match.group(1).strip()
|
|
108
|
+
feature_name = re.sub(r"^FEA-\d+\s*", "", current_feature)
|
|
109
|
+
current_scenario = {
|
|
110
|
+
"feature": current_feature,
|
|
111
|
+
"scenario": scenario_name,
|
|
112
|
+
"preconditions": [],
|
|
113
|
+
"actions": [],
|
|
114
|
+
"expectations": [],
|
|
115
|
+
"L1": feature_name,
|
|
116
|
+
"L2": scenario_name,
|
|
117
|
+
"L3": "",
|
|
118
|
+
}
|
|
119
|
+
collecting_sub_items = None
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if current_scenario is None:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
if re.match(r"^-\s*前置条件[::]", stripped):
|
|
126
|
+
collecting_sub_items = "preconditions"
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
action_match = re.match(r"^-\s*动作[::]\s*(.+)$", stripped)
|
|
130
|
+
if action_match:
|
|
131
|
+
collecting_sub_items = None
|
|
132
|
+
current_scenario["actions"].append(action_match.group(1).strip())
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
expect_match = re.match(r"^-\s*预期结果[::]\s*(.+)$", stripped)
|
|
136
|
+
if expect_match:
|
|
137
|
+
collecting_sub_items = None
|
|
138
|
+
current_scenario["expectations"].append(expect_match.group(1).strip())
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
sub_match = re.match(r"^\s+-\s+(.+)$", line)
|
|
142
|
+
if sub_match and collecting_sub_items == "preconditions":
|
|
143
|
+
current_scenario["preconditions"].append(sub_match.group(1).strip())
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
if stripped and not stripped.startswith("-"):
|
|
147
|
+
collecting_sub_items = None
|
|
148
|
+
|
|
149
|
+
if current_scenario is not None:
|
|
150
|
+
results.append(current_scenario)
|
|
151
|
+
|
|
152
|
+
return results
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# 应用注册
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def register_app(app_name: str, package: str, device_type: str, ability: str = "EntryAbility") -> None:
|
|
160
|
+
"""将应用注册到 phone-agent 的配置中。"""
|
|
161
|
+
if device_type == "hdc":
|
|
162
|
+
from phone_agent.config import apps_harmonyos
|
|
163
|
+
apps_harmonyos.APP_PACKAGES[app_name] = package
|
|
164
|
+
apps_harmonyos.APP_ABILITIES[package] = ability
|
|
165
|
+
else:
|
|
166
|
+
from phone_agent.config import apps
|
|
167
|
+
apps.APP_PACKAGES[app_name] = package
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# 功能路径提取
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def _get_feature_path(item: dict) -> tuple[str, str]:
|
|
175
|
+
"""从功能项提取层级路径和叶子节点名称。"""
|
|
176
|
+
keys = [k for k in ("L1", "L2", "L3") if k in item]
|
|
177
|
+
if not keys:
|
|
178
|
+
keys = [k for k in ("一级", "二级", "三级") if k in item]
|
|
179
|
+
if not keys:
|
|
180
|
+
keys = list(item.keys())[:3]
|
|
181
|
+
|
|
182
|
+
parts = []
|
|
183
|
+
for k in keys:
|
|
184
|
+
v = item.get(k)
|
|
185
|
+
if v is not None and str(v).strip():
|
|
186
|
+
parts.append(str(v).strip())
|
|
187
|
+
|
|
188
|
+
if not parts:
|
|
189
|
+
return "", ""
|
|
190
|
+
return " > ".join(parts), parts[-1]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# 构建导航 prompt
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def _build_navigate_prompt(app_name: str, path_str: str, leaf: str, app_hints: str | None = None) -> str:
|
|
198
|
+
"""构建导航到目标页面的 prompt。"""
|
|
199
|
+
prompt = (
|
|
200
|
+
f"打开{app_name},按照以下路径依次点击导航到目标页面:{path_str}。"
|
|
201
|
+
f"最终目标是到达「{leaf}」所在的页面。"
|
|
202
|
+
"注意:"
|
|
203
|
+
"1. 鸿蒙手机浅色/深色模式切换路径:设置->显示与亮度"
|
|
204
|
+
"2. '更多'组件可能表示为 三横 或者 三点。"
|
|
205
|
+
"3. '设置'组件可能表示为 齿轮。"
|
|
206
|
+
"到达目标页面后请回复'已到达',无法到达请回复'未找到'。"
|
|
207
|
+
)
|
|
208
|
+
if app_hints:
|
|
209
|
+
prompt += f"\n补充提示: {app_hints}"
|
|
210
|
+
return prompt
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# 判断是否到达目标页面
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def _judge_reached(msg: str) -> bool:
|
|
218
|
+
"""根据 agent 回复判断是否到达了目标页面。"""
|
|
219
|
+
if not msg:
|
|
220
|
+
return False
|
|
221
|
+
reached_keywords = ("已到达", "到达", "存在", "找到", "完成", "成功", "已确认", "已进入", "已打开")
|
|
222
|
+
failed_keywords = ("未找到", "不存在", "无法找到", "无法到达", "没有", "失败", "未到达")
|
|
223
|
+
first_line = msg.split('\n')[0]
|
|
224
|
+
if any(kw in first_line for kw in failed_keywords):
|
|
225
|
+
return False
|
|
226
|
+
if any(kw in first_line for kw in reached_keywords):
|
|
227
|
+
return True
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
# phone-agent runtime patches (idempotent)
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def _apply_phone_agent_patches():
|
|
236
|
+
"""Monkey-patch phone_agent bugs so the skill works on an unpatched install."""
|
|
237
|
+
if getattr(_apply_phone_agent_patches, '_done', False):
|
|
238
|
+
return
|
|
239
|
+
_apply_phone_agent_patches._done = True
|
|
240
|
+
|
|
241
|
+
import math
|
|
242
|
+
import time as _time
|
|
243
|
+
|
|
244
|
+
# Patch 1: HarmonyOS swipe — last arg should be velocity (pps), not duration (ms)
|
|
245
|
+
import phone_agent.hdc.device as _hdc
|
|
246
|
+
if not getattr(_hdc.swipe, '_patched', False):
|
|
247
|
+
def _swipe(start_x, start_y, end_x, end_y,
|
|
248
|
+
duration_ms=None, device_id=None, delay=None):
|
|
249
|
+
if delay is None:
|
|
250
|
+
delay = _hdc.TIMING_CONFIG.device.default_swipe_delay
|
|
251
|
+
hdc_prefix = _hdc._get_hdc_prefix(device_id)
|
|
252
|
+
distance = math.sqrt((start_x - end_x) ** 2 + (start_y - end_y) ** 2)
|
|
253
|
+
if duration_ms is None:
|
|
254
|
+
duration_ms = int(distance ** 2 / 1000)
|
|
255
|
+
duration_ms = max(500, min(duration_ms, 1000))
|
|
256
|
+
velocity = int((distance / duration_ms) * 4000)
|
|
257
|
+
velocity = max(500, min(velocity, 40000))
|
|
258
|
+
_hdc._run_hdc_command(
|
|
259
|
+
hdc_prefix + [
|
|
260
|
+
"shell", "uitest", "uiInput", "swipe",
|
|
261
|
+
str(start_x), str(start_y), str(end_x), str(end_y),
|
|
262
|
+
str(velocity),
|
|
263
|
+
],
|
|
264
|
+
capture_output=True,
|
|
265
|
+
)
|
|
266
|
+
_time.sleep(delay)
|
|
267
|
+
_swipe._patched = True
|
|
268
|
+
_hdc.swipe = _swipe
|
|
269
|
+
|
|
270
|
+
# Patch 2: parse_action — regex fallback when do() contains unescaped quotes
|
|
271
|
+
import phone_agent.actions.handler as _handler
|
|
272
|
+
if not getattr(_handler.parse_action, '_patched', False):
|
|
273
|
+
_orig_parse = _handler.parse_action
|
|
274
|
+
def _parse_action(response):
|
|
275
|
+
try:
|
|
276
|
+
return _orig_parse(response)
|
|
277
|
+
except ValueError as e:
|
|
278
|
+
if 'Failed to parse do() action' in str(e):
|
|
279
|
+
m = re.search(
|
|
280
|
+
r'do\s*\(\s*action\s*=\s*"([^"]*)"\s*,\s*message\s*="(.*)"\s*\)\s*$',
|
|
281
|
+
response.strip(), re.DOTALL,
|
|
282
|
+
)
|
|
283
|
+
if m:
|
|
284
|
+
msg = m.group(2)
|
|
285
|
+
msg = msg.replace("\\n", "\x0a").replace("\\t", "\x09").replace("\\r", "\x0d")
|
|
286
|
+
msg = msg.replace('\\"', '"')
|
|
287
|
+
return {"_metadata": "do", "action": m.group(1), "message": msg}
|
|
288
|
+
raise
|
|
289
|
+
_parse_action._patched = True
|
|
290
|
+
_handler.parse_action = _parse_action
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# 核心导航逻辑
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def navigate(
|
|
298
|
+
model_config,
|
|
299
|
+
app_name: str,
|
|
300
|
+
package: str,
|
|
301
|
+
device_type: str = "hdc",
|
|
302
|
+
prompt: str | None = None,
|
|
303
|
+
single_task: str | None = None,
|
|
304
|
+
feature_file: str | Path = "",
|
|
305
|
+
sheet_name: int = 0,
|
|
306
|
+
offset: int = 1,
|
|
307
|
+
limit: int | None = None,
|
|
308
|
+
max_steps: int = 30,
|
|
309
|
+
verbose: bool = True,
|
|
310
|
+
app_hints: str | None = None,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""按指定路径操作设备导航到目标页面,输出是否到达。"""
|
|
313
|
+
_apply_phone_agent_patches()
|
|
314
|
+
register_app(app_name, package, device_type)
|
|
315
|
+
|
|
316
|
+
from phone_agent import PhoneAgent
|
|
317
|
+
from phone_agent.agent import AgentConfig
|
|
318
|
+
from phone_agent.device_factory import set_device_type, DeviceType
|
|
319
|
+
|
|
320
|
+
agent_config = AgentConfig(max_steps=max_steps, verbose=verbose)
|
|
321
|
+
set_device_type(DeviceType.HDC if device_type == "hdc" else DeviceType.ADB)
|
|
322
|
+
agent = PhoneAgent(model_config=model_config, agent_config=agent_config)
|
|
323
|
+
|
|
324
|
+
# 收集要导航的目标列表: [(prompt_text, display_label), ...]
|
|
325
|
+
tasks: list[tuple[str, str]] = []
|
|
326
|
+
|
|
327
|
+
if prompt:
|
|
328
|
+
full_prompt = f"打开{app_name},{prompt}"
|
|
329
|
+
tasks.append((full_prompt, prompt))
|
|
330
|
+
elif single_task:
|
|
331
|
+
level_parts = re.split(r'(L[123])', single_task)
|
|
332
|
+
item = {}
|
|
333
|
+
for j in range(1, len(level_parts) - 1, 2):
|
|
334
|
+
key = level_parts[j]
|
|
335
|
+
val = level_parts[j + 1].strip()
|
|
336
|
+
if val:
|
|
337
|
+
item[key] = val
|
|
338
|
+
path_str, leaf = _get_feature_path(item)
|
|
339
|
+
if path_str:
|
|
340
|
+
tasks.append((_build_navigate_prompt(app_name, path_str, leaf, app_hints), path_str))
|
|
341
|
+
else:
|
|
342
|
+
feature_path = Path(feature_file)
|
|
343
|
+
if feature_path.suffix.lower() == ".md":
|
|
344
|
+
features = parse_app_feature_md(file_path=feature_file)
|
|
345
|
+
else:
|
|
346
|
+
features = parse_app_feature(file_path=feature_file, sheet_name=sheet_name)
|
|
347
|
+
if offset > 1:
|
|
348
|
+
features = features[offset - 1:]
|
|
349
|
+
if limit is not None:
|
|
350
|
+
features = features[:limit]
|
|
351
|
+
for item in features:
|
|
352
|
+
path_str, leaf = _get_feature_path(item)
|
|
353
|
+
if path_str:
|
|
354
|
+
tasks.append((_build_navigate_prompt(app_name, path_str, leaf, app_hints), path_str))
|
|
355
|
+
|
|
356
|
+
for i, (task_prompt, label) in enumerate(tasks):
|
|
357
|
+
if len(tasks) > 1:
|
|
358
|
+
print(f"\n[{i + 1}/{len(tasks)}] 导航: {label}")
|
|
359
|
+
else:
|
|
360
|
+
print(f"导航: {label}")
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
result_msg = agent.run(task_prompt)
|
|
364
|
+
reached = _judge_reached(result_msg)
|
|
365
|
+
except Exception:
|
|
366
|
+
reached = False
|
|
367
|
+
|
|
368
|
+
if reached:
|
|
369
|
+
print("到达目标页面")
|
|
370
|
+
else:
|
|
371
|
+
print("未找到目标页面")
|
|
372
|
+
|
|
373
|
+
if i < len(tasks) - 1:
|
|
374
|
+
agent.reset()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
# CLI 入口
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
def main():
|
|
382
|
+
parser = argparse.ArgumentParser(
|
|
383
|
+
description="按指定路径操作设备导航到目标页面(支持 Android / HarmonyOS)"
|
|
384
|
+
)
|
|
385
|
+
parser.add_argument("--device", choices=["hdc", "adb"], default="hdc",
|
|
386
|
+
help="设备类型: hdc=HarmonyOS, adb=Android(默认 hdc)")
|
|
387
|
+
parser.add_argument("--app", type=str, required=True, help="应用名称")
|
|
388
|
+
parser.add_argument("--package", type=str, required=True, help="应用包名")
|
|
389
|
+
parser.add_argument("--task", type=str, default=None,
|
|
390
|
+
help="功能路径,格式: 'L1xxxL2xxxL3xxx'")
|
|
391
|
+
parser.add_argument("--prompt", type=str, default=None,
|
|
392
|
+
help="直接注入给 Agent 的自然语言导航指令")
|
|
393
|
+
parser.add_argument("--feature", type=str, default=None,
|
|
394
|
+
help="功能清单文件路径(.xlsx / .md)")
|
|
395
|
+
parser.add_argument("--limit", type=int, default=None, help="仅导航前 N 条")
|
|
396
|
+
parser.add_argument("--offset", type=int, default=1, help="从第几条开始(1-based)")
|
|
397
|
+
parser.add_argument("--sheet", type=int, default=0, help="Excel 工作表索引")
|
|
398
|
+
parser.add_argument("--api-key", type=str, default="", help="智谱 API Key")
|
|
399
|
+
parser.add_argument("--max-steps", type=int, default=30, help="Agent 最大步数")
|
|
400
|
+
parser.add_argument("--app-hints", type=str, default=None, help="应用上下文提示")
|
|
401
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="减少输出")
|
|
402
|
+
args = parser.parse_args()
|
|
403
|
+
|
|
404
|
+
if not args.prompt and not args.task and not args.feature:
|
|
405
|
+
parser.error("必须指定 --feature、--task 或 --prompt 之一")
|
|
406
|
+
|
|
407
|
+
api_key = args.api_key or os.environ.get("GLM_API_KEY", "")
|
|
408
|
+
if not api_key and not args.quiet:
|
|
409
|
+
print("提示: 未设置 API Key,请通过 --api-key 或环境变量 GLM_API_KEY 配置")
|
|
410
|
+
print("继续运行可能因鉴权失败而报错\n")
|
|
411
|
+
|
|
412
|
+
from phone_agent.model import ModelConfig
|
|
413
|
+
|
|
414
|
+
model_config = ModelConfig(
|
|
415
|
+
base_url="https://open.bigmodel.cn/api/paas/v4",
|
|
416
|
+
api_key=api_key,
|
|
417
|
+
model_name="autoglm-phone",
|
|
418
|
+
temperature=0.1,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
device_label = "ADB (Android)" if args.device == "adb" else "HDC (HarmonyOS)"
|
|
422
|
+
print(f"设备: {device_label}, 应用: {args.app} ({args.package})")
|
|
423
|
+
print("-" * 50)
|
|
424
|
+
|
|
425
|
+
navigate(
|
|
426
|
+
model_config=model_config,
|
|
427
|
+
app_name=args.app,
|
|
428
|
+
package=args.package,
|
|
429
|
+
device_type=args.device,
|
|
430
|
+
prompt=args.prompt,
|
|
431
|
+
single_task=args.task,
|
|
432
|
+
feature_file=args.feature or "",
|
|
433
|
+
sheet_name=args.sheet,
|
|
434
|
+
offset=args.offset,
|
|
435
|
+
limit=args.limit,
|
|
436
|
+
max_steps=args.max_steps,
|
|
437
|
+
verbose=not args.quiet,
|
|
438
|
+
app_hints=args.app_hints,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
if __name__ == "__main__":
|
|
443
|
+
sys.exit(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Create a timestamped output directory under `capture_output_dir`:
|
|
2
|
+
```
|
|
3
|
+
task_dir = capture_output_dir / "task_{timestamp}"
|
|
4
|
+
android_capture_dir = task_dir / "android_page_{i}_{name}"
|
|
5
|
+
hmos_capture_dir = task_dir / "hmos_page_{i}_{name}"
|
|
6
|
+
```
|
|
7
|
+
Each paired hmos-android page-pair should have the same `i`
|
|
8
|
+
|
|
9
|
+
## Navigation prompt rules
|
|
10
|
+
|
|
11
|
+
Each page's `android_nav_path` / `hmos_nav_path` is used as the `--prompt` for `app_feature_verify.py`.
|
|
12
|
+
The `--prompt` mode auto-prepends "打开{app_name}," so do not include "打开{app_name}" in the prompt.
|
|
13
|
+
|
|
14
|
+
## Navigate to Android page and Capture (If navigated Success)
|
|
15
|
+
```bash
|
|
16
|
+
PYTHONIOENCODING=utf-8 python ./app_feature_verify.py \
|
|
17
|
+
--device adb \
|
|
18
|
+
--app "{android.app_name}" \
|
|
19
|
+
--package "{android.package}" \
|
|
20
|
+
--prompt "{pages[i].android_nav_path}" \
|
|
21
|
+
--api-key "{glm_api_key}" \
|
|
22
|
+
--max-steps 15
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Capture Android page** (if navigation succeeded)
|
|
26
|
+
```bash
|
|
27
|
+
PYTHONIOENCODING=utf-8 python ./page_capture.py --device adb -o "{android_capture_dir}"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Navigate & Capture HarmonyOS Pages
|
|
31
|
+
|
|
32
|
+
Same as Andorid but with `--device hdc` and HarmonyOS config values. Directory pattern:
|
|
33
|
+
```
|
|
34
|
+
hmos_capture_dir = task_dir / "hmos_page_{i}_{name}"
|
|
35
|
+
```
|
|
36
|
+
For pages missed in harmony OS app, navigation will certainly fail — `hmos_capture_dir` will be empty, which is expected. Skip navigation for these pages and leave the directory empty.
|
|
37
|
+
|