@xiuchang-midscene/android 1.6.2 → 2.0.1
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/dist/es/cli.mjs +438 -30
- package/dist/es/index.mjs +435 -27
- package/dist/es/mcp-server.mjs +438 -30
- package/dist/lib/cli.js +440 -32
- package/dist/lib/index.js +435 -27
- package/dist/lib/mcp-server.js +440 -32
- package/dist/types/index.d.ts +27 -0
- package/dist/types/mcp-server.d.ts +27 -0
- package/package.json +2 -1
- package/src/app-knowledge/screenshots/fliggy/fliggy-qwen/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/flight-detail/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/flight-list/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/flight-order/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/home/home-page-annotated.jpg +0 -0
- package/src/app-knowledge/screenshots/fliggy/hotel-detail/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/hotel-list/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/hotel-order/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/search-default/search-default-annotated.jpg +0 -0
- package/src/app-knowledge/screenshots/fliggy/search-result/search-result-annotated.jpg +0 -0
- package/src/app-knowledge/screenshots/fliggy/search-result/search-result-quickfilter-annotated.jpg +0 -0
- package/src/app-knowledge/screenshots/fliggy/search-sug/search-sug-annotated.jpg +0 -0
- package/src/app-knowledge/screenshots/fliggy/ticket-detail/.gitkeep +0 -0
- package/src/app-knowledge/screenshots/fliggy/ticket-order/.gitkeep +0 -0
package/dist/es/index.mjs
CHANGED
|
@@ -130,7 +130,7 @@ var __webpack_modules__ = {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
resolveServerBinPath() {
|
|
133
|
-
const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
|
|
133
|
+
const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(import.meta.url).resolve('@xiuchang-midscene/android/package.json');
|
|
134
134
|
return node_path__rspack_import_2["default"].join(node_path__rspack_import_2["default"].dirname(androidPkgJson), 'bin', 'scrcpy-server');
|
|
135
135
|
}
|
|
136
136
|
getFfmpegPath() {
|
|
@@ -1376,7 +1376,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1376
1376
|
async ensureYadb() {
|
|
1377
1377
|
if (!this.yadbPushed) {
|
|
1378
1378
|
const adb = await this.getAdb();
|
|
1379
|
-
const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
|
|
1379
|
+
const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@xiuchang-midscene/android/package.json');
|
|
1380
1380
|
const yadbBin = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'yadb');
|
|
1381
1381
|
await adb.push(yadbBin, '/data/local/tmp');
|
|
1382
1382
|
this.yadbPushed = true;
|
|
@@ -1424,25 +1424,31 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1424
1424
|
IME_STRATEGY_ADB_KEYBOARD
|
|
1425
1425
|
];
|
|
1426
1426
|
}
|
|
1427
|
-
if (effectiveFallback?.length)
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1427
|
+
if (effectiveFallback?.length) {
|
|
1428
|
+
const MAX_ROUNDS = 2;
|
|
1429
|
+
const totalAttempts = effectiveFallback.length * MAX_ROUNDS;
|
|
1430
|
+
let succeeded = false;
|
|
1431
|
+
for(let i = 0; i < totalAttempts; i++){
|
|
1432
|
+
const strategy = effectiveFallback[i % effectiveFallback.length];
|
|
1433
|
+
const isLast = i === totalAttempts - 1;
|
|
1434
|
+
debugDevice(`[IME fallback] trying strategy: ${strategy} (attempt ${i + 1}/${totalAttempts})`);
|
|
1435
|
+
await this._typeWithStrategy(text, strategy, adb);
|
|
1436
|
+
if (isLast) {
|
|
1437
|
+
debugDevice('[IME fallback] all attempts exhausted');
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
await sleep(800);
|
|
1441
|
+
const success = await this.inputVerifyFn(text);
|
|
1442
|
+
if (success) {
|
|
1443
|
+
debugDevice(`[IME fallback] strategy '${strategy}' succeeded`);
|
|
1444
|
+
succeeded = true;
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
debugDevice(`[IME fallback] strategy '${strategy}' failed, clearing field and trying next`);
|
|
1448
|
+
await adb.clearTextField(100);
|
|
1441
1449
|
}
|
|
1442
|
-
debugDevice(`[IME fallback]
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
else {
|
|
1450
|
+
debugDevice(`[IME fallback] done, succeeded=${succeeded}`);
|
|
1451
|
+
} else {
|
|
1446
1452
|
const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
|
|
1447
1453
|
await this._typeWithStrategy(text, IME_STRATEGY, adb);
|
|
1448
1454
|
}
|
|
@@ -1769,6 +1775,346 @@ const createPlatformActions = (device)=>({
|
|
|
1769
1775
|
}
|
|
1770
1776
|
})
|
|
1771
1777
|
});
|
|
1778
|
+
const fliggyKnowledge = `---
|
|
1779
|
+
name: fliggy-app-guide
|
|
1780
|
+
description: >
|
|
1781
|
+
飞猪 App 页面结构、功能入口和操作指南。适用于 iOS、Android、鸿蒙全端。
|
|
1782
|
+
当需要在飞猪 App 上进行任何操作时,优先阅读此 Skill 了解页面结构和导航路径。
|
|
1783
|
+
包含首页、搜索(猪搜)、飞猪千问(问一问)、结果页等核心页面的布局和操作方式。
|
|
1784
|
+
包含门票、酒店、机票下单链路的页面描述和操作规则。
|
|
1785
|
+
|
|
1786
|
+
Trigger keywords: 飞猪, fliggy, 猪搜, 飞猪千问, 问一问, 飞猪搜索, 飞猪app, 商卡, 结果页, 默认页, 品专, SUG, 联想词, 底纹词, 金刚词, 景点货架, POI, 门票, 酒店, 机票, 下单, 预订
|
|
1787
|
+
---
|
|
1788
|
+
|
|
1789
|
+
# 飞猪 App 操作指南(全端通用)
|
|
1790
|
+
|
|
1791
|
+
> 本指南适用于 iOS、Android、鸿蒙端的飞猪 App 操作。
|
|
1792
|
+
> 在执行任何飞猪 App 相关任务前,先参考本文档了解页面结构和导航路径。
|
|
1793
|
+
|
|
1794
|
+
## App 版本区分
|
|
1795
|
+
|
|
1796
|
+
- 手机上可能有两个"飞猪旅行" App:**测试包**和**正式包**
|
|
1797
|
+
- 区分方式:测试包图标右上角有"测试版"标记,正式包没有
|
|
1798
|
+
- **默认打开正式包**,除非用户明确要求打开测试包
|
|
1799
|
+
|
|
1800
|
+
## 首页(即"推荐"页)
|
|
1801
|
+
|
|
1802
|
+
- 飞猪 App 首页是所有操作的默认起点
|
|
1803
|
+
- 除非用户明确要求"继续操作"或指定其他页面,否则每次任务都先回到首页
|
|
1804
|
+
|
|
1805
|
+
### 顶部导航栏(从左到右)
|
|
1806
|
+
|
|
1807
|
+
| 导航项 | 说明 | 别名 |
|
|
1808
|
+
|---|---|---|
|
|
1809
|
+
| 飞猪千问 | 飞猪 AI 助手,可以对话提问 | 也叫"问一问" |
|
|
1810
|
+
| 推荐 | 飞猪 App 首页(默认停留页) | 首页 |
|
|
1811
|
+
| [城市名] | 基于定位显示的城市(如"杭州") | 目的地推荐 |
|
|
1812
|
+
|
|
1813
|
+
### 搜索区域
|
|
1814
|
+
|
|
1815
|
+
- **搜索框**:内有滚动词(称为"底纹词"),点击搜索框进入搜索页,这个搜索叫"飞猪搜索"(简称"猪搜")
|
|
1816
|
+
- **搜索按钮**:搜索框右侧,点击可以用当前滚动到的"底纹词"直接搜索
|
|
1817
|
+
- **热词**:搜索框下方,推荐的搜索关键词
|
|
1818
|
+
|
|
1819
|
+
### 类目入口(热词下方)
|
|
1820
|
+
|
|
1821
|
+
用户要求"进入XX类目"时,优先从这里进入。包括:
|
|
1822
|
+
- 酒店、机票、火车票、汽车票、景点门票、旅游、用车、签证、邮轮、特价机票等
|
|
1823
|
+
|
|
1824
|
+
### 中间内容区
|
|
1825
|
+
|
|
1826
|
+
- 活动 Banner(轮播图)
|
|
1827
|
+
- 推荐内容(酒店、旅游产品推荐卡片)
|
|
1828
|
+
|
|
1829
|
+
### 底部 Tab 导航栏
|
|
1830
|
+
|
|
1831
|
+
- 首页 | 消息 | 行程 | 我的
|
|
1832
|
+
|
|
1833
|
+
### 首页弹窗处理
|
|
1834
|
+
|
|
1835
|
+
当 App 不在后台(冷启动)时,首页可能出现以下弹窗,需要先处理:
|
|
1836
|
+
|
|
1837
|
+
1. **全屏广告**:覆盖整个屏幕的推广广告
|
|
1838
|
+
- 处理方式:点击右上角"跳过"按钮,或等待几秒自动消失
|
|
1839
|
+
2. **领券弹窗**:弹出的优惠券领取提示
|
|
1840
|
+
- 处理方式:点击弹窗上的 X 按钮关闭
|
|
1841
|
+
|
|
1842
|
+
**操作顺序**:打开 App → 处理弹窗(如有) → 确认在首页 → 执行任务
|
|
1843
|
+
|
|
1844
|
+
## 默认页(从首页点击搜索框进入)
|
|
1845
|
+
|
|
1846
|
+
入口:首页 → 点击搜索框
|
|
1847
|
+
|
|
1848
|
+
页面布局(从上到下):
|
|
1849
|
+
|
|
1850
|
+
1. **搜索框**:
|
|
1851
|
+
- 内有灰色的"底纹词"(从首页进入时滚动停留的词,进入默认页后不再滚动)
|
|
1852
|
+
- 未输入内容时显示底纹词,输入内容后底纹词消失
|
|
1853
|
+
- 输入内容后点击搜索 → 进入搜索结果页(简称"结果页")
|
|
1854
|
+
- 输入过程中,搜索框下方会动态出现 **SUG(联想词)**,对用户输入内容进行补全
|
|
1855
|
+
2. **金刚词**:搜索框下方的推荐词,点击可直接搜索
|
|
1856
|
+
3. **搜索历史**:之前搜索过的词,点击也会跳转到结果页
|
|
1857
|
+
4. **榜单**:搜索历史下方,包含一些榜单内容,可左右滑动,一般有 3-4 个
|
|
1858
|
+
|
|
1859
|
+
## SUG(联想词)
|
|
1860
|
+
|
|
1861
|
+
- 用户在搜索框输入过程中,搜索框下方会动态显示联想补全词
|
|
1862
|
+
- 出现场景:
|
|
1863
|
+
1. 默认页搜索框输入时
|
|
1864
|
+
2. 搜索结果页点击搜索框输入时
|
|
1865
|
+
- SUG 内容可点击,不同类型的 SUG 会进入不同的页面
|
|
1866
|
+
|
|
1867
|
+
## 搜索结果页(结果页)
|
|
1868
|
+
|
|
1869
|
+
入口:默认页 → 输入内容点击搜索 / 点击金刚词 / 点击搜索历史
|
|
1870
|
+
|
|
1871
|
+
页面布局(从上到下):
|
|
1872
|
+
|
|
1873
|
+
1. **顶部搜索框**:
|
|
1874
|
+
- 点击可以不退出当前页面重新搜索
|
|
1875
|
+
- 左侧:返回按钮(回到上一页,很多页面都有)
|
|
1876
|
+
- 右侧:"我的收藏"(收藏夹,首页也有入口)
|
|
1877
|
+
2. **品专**(可能出现):
|
|
1878
|
+
- 广告位,不一定每次都有,只有特定的搜索词才会有投放
|
|
1879
|
+
3. **筛选**:品专下方的筛选条件
|
|
1880
|
+
4. **商品卡片(商卡)**:
|
|
1881
|
+
- 左上角标注商卡类型(如"景点"、"酒店"等)
|
|
1882
|
+
- 景点也叫 POI
|
|
1883
|
+
- 左侧:图片;右侧:标题、标签、价格
|
|
1884
|
+
- 点击商卡 → 进入商品详情页(但景点商卡点击进入的是"景点货架",待补充)
|
|
1885
|
+
|
|
1886
|
+
## 飞猪千问(问一问)
|
|
1887
|
+
|
|
1888
|
+
- **入口**:首页顶部导航栏最左侧的"飞猪千问"
|
|
1889
|
+
- 这是飞猪的 AI 助手对话页面,可以输入问题进行对话
|
|
1890
|
+
- **注意**:飞猪千问 ≠ 飞猪搜索(猪搜)
|
|
1891
|
+
- 飞猪千问:从顶部导航栏点击"飞猪千问"进入,是 AI 对话页面
|
|
1892
|
+
- 飞猪搜索:从搜索框进入,是商品搜索结果页
|
|
1893
|
+
|
|
1894
|
+
### 千问页面布局
|
|
1895
|
+
|
|
1896
|
+
1. **顶部导航栏**:
|
|
1897
|
+
- 左侧 icon:历史会话(查看之前的对话记录)
|
|
1898
|
+
- 右侧 icon:新建会话(开始一个新的对话)
|
|
1899
|
+
2. **中间对话区域**:显示 AI 对话内容,包含推荐问题卡片
|
|
1900
|
+
3. **底部输入框**:输入问题并发送消息
|
|
1901
|
+
|
|
1902
|
+
### 操作流程
|
|
1903
|
+
|
|
1904
|
+
- 在底部输入框输入问题 → 点击发送 → 等待 AI 回复完成
|
|
1905
|
+
- 回复内容可能包含文字、卡片、推荐商品等
|
|
1906
|
+
|
|
1907
|
+
## 门票下单链路
|
|
1908
|
+
|
|
1909
|
+
### 门票列表页
|
|
1910
|
+
|
|
1911
|
+
在上方的搜索框里可以搜索对应的景点
|
|
1912
|
+
|
|
1913
|
+
### 门票详情页
|
|
1914
|
+
|
|
1915
|
+
- 页面描述
|
|
1916
|
+
- 顶部是图片或者相关介绍视频,下方是预订、评价、推荐tab
|
|
1917
|
+
- 预订tab
|
|
1918
|
+
- 左侧是是可以可以快速锚定的索引,包括门票、联票或者更多套餐
|
|
1919
|
+
- 购买的话点击景点门票,此时会有不同的sku,成人票、儿童票等,选择其中一个开始预订。此时会直接调到下单页或者唤起一个浮层。在浮层里从上到下依次选择对应的选项,比如种类、日期等
|
|
1920
|
+
- 选择日期应该点日历图标来选择
|
|
1921
|
+
|
|
1922
|
+
### 门票下单页
|
|
1923
|
+
|
|
1924
|
+
- 页面描述
|
|
1925
|
+
- 在该页面确保联系电话是否正确填写
|
|
1926
|
+
- 如果要填写出行人信息,请确保出行人信息右侧的勾选框已经勾选
|
|
1927
|
+
- **⚠️ 支付时唤起收银台即可,不要真正支付,否则会对用户的资产造成损失!!!**
|
|
1928
|
+
|
|
1929
|
+
## 酒店下单链路
|
|
1930
|
+
|
|
1931
|
+
### 飞猪首页 for 酒店
|
|
1932
|
+
|
|
1933
|
+
- 页面描述
|
|
1934
|
+
- 头部搜索框和搜索按钮
|
|
1935
|
+
- 头部我的收藏按钮
|
|
1936
|
+
- 顶部金刚区域, 有各行业的入口, 一般为图片+文字,如酒店文案+酒店logo
|
|
1937
|
+
- 该页面操作限制规则:
|
|
1938
|
+
- 酒店应点击金刚区域的酒店按钮所在金刚(它在金刚区域第一个,在机票金刚的前一个), 点击即可进入酒店首页
|
|
1939
|
+
|
|
1940
|
+
### 酒店首页
|
|
1941
|
+
|
|
1942
|
+
- 页面描述
|
|
1943
|
+
- 顶部4个tab: 国内(默认是被选中的)、国际、钟点房、民宿, 购买酒店流程中, 一般不需要切换点击
|
|
1944
|
+
- 国内tab下有五行,第一行左侧城市选择按钮(一般有城市名显示, 如: 北京/杭州/上海)、右侧关键字输入栏
|
|
1945
|
+
- 国内tab下有五行,第二行左侧酒店入住日期,右侧酒店离店日期
|
|
1946
|
+
- 国内tab下有五行,第三行左侧是房间数/成人数/儿童数选择按钮(点击出现浮层,可以修改房间数/成人数/儿童数),右侧是星级选择按钮(点击出现浮层,可以修改每间每晚价格/星级)
|
|
1947
|
+
- 国内tab下有五行,第四行是特殊关键字标签行,横向显示多个关键字标签,点击单个关键字标签,可以跳转带默认关键搜索词的酒店列表页
|
|
1948
|
+
- 国内tab下有五行,第五行是搜索酒店按钮,一般通过点击搜索酒店按钮,跳转酒店列表页。
|
|
1949
|
+
- 国内tab下的五行下方,是酒店猜你喜欢卡片列表。
|
|
1950
|
+
- 其他不常用按钮不做过多赘述
|
|
1951
|
+
- 页面操作限制规则
|
|
1952
|
+
- 只点击酒店详情页中的某一张房型卡片,不点击其他元素
|
|
1953
|
+
|
|
1954
|
+
### 酒店列表页
|
|
1955
|
+
|
|
1956
|
+
- 页面描述
|
|
1957
|
+
- 顶部搜索栏左侧返回按钮,中间搜索栏(包含城市名称/酒店入离日期/房间数+人数/关键词栏(文字可能为空)),右侧地图icon按钮及三个点按钮
|
|
1958
|
+
- 搜索栏下方是筛选栏(包含推荐排序按钮、位置距离、价格/星级、筛选等按钮)
|
|
1959
|
+
- 筛选栏下方是卡片列表,由多张酒店卡片上下排列而成,点击卡片可以进入酒店详情页
|
|
1960
|
+
- 其他不常用按钮不做过多赘述
|
|
1961
|
+
- 页面操作限制规则
|
|
1962
|
+
- 只点击国内tab下第五行搜索酒店按钮
|
|
1963
|
+
|
|
1964
|
+
### 酒店详情页
|
|
1965
|
+
|
|
1966
|
+
- 页面描述:
|
|
1967
|
+
- 顶部导航栏(导航栏一般为透明色)及按钮(按钮主要包括左侧返回按钮、右侧搜索Icon按钮、收藏星星Icon按钮、转发Icon按钮、三个点Icon的更多按钮)
|
|
1968
|
+
- 顶部导航栏下方是多媒体头图容器,支持横向滚动的视频、图片。
|
|
1969
|
+
- 头图容器下方是酒店标题组件(上下排布,上面是酒店标题文字,下方是酒店文字标签)
|
|
1970
|
+
- 酒店标题组件下方是一排三按钮(左中右三部分区块构成,左侧是酒店评分,中间是装修信息,右侧是地址信息)
|
|
1971
|
+
- 一排三按钮下方是会员及广告栏
|
|
1972
|
+
- 会员及广告栏下方是入离日期栏(包含左侧酒店入住离店日期、右侧房间数及人员数)
|
|
1973
|
+
- 入离日期栏下方是酒店房型卡片列表(一个由酒店房型卡片组成的支持上下滑动的卡片列表,每张卡片左侧是图片,右侧是上下多行文字描述,右下角带价格显示及预订按钮)
|
|
1974
|
+
- 其他不常用按钮不做过多赘述
|
|
1975
|
+
- 页面操作限制规则:
|
|
1976
|
+
- 当用户要下单某个房型时,应该先找到目标房型卡片(比如:大床房),可以先点击快捷筛选按钮,选择对应房型,再点击房型卡片中的 "订" 或者 "抢" 按钮操作进入下单页
|
|
1977
|
+
|
|
1978
|
+
### 酒店下单页
|
|
1979
|
+
|
|
1980
|
+
- 页面描述
|
|
1981
|
+
- 头部有房型信息、入离日期等
|
|
1982
|
+
- 中间有入驻人信息和手机号
|
|
1983
|
+
- 底部有支付按钮和价格
|
|
1984
|
+
- **⚠️ 页面操作限制规则:判断到达该页面后,即可返回END结束,不要真实操作下单。**
|
|
1985
|
+
|
|
1986
|
+
### AI酒店组件校验列表页
|
|
1987
|
+
|
|
1988
|
+
- 页面描述:
|
|
1989
|
+
- 顶部导航栏左侧返回按钮,中间文字标题"AI酒店组件校验列表"
|
|
1990
|
+
- 导航栏下方是一个列表页,由多个列表项的行组件组成,每个行组件左侧是icon,中间是上下两行文字,右侧是一个右箭头
|
|
1991
|
+
- 其他不常用按钮不做过多赘述
|
|
1992
|
+
- 页面操作限制规则
|
|
1993
|
+
- 可以点击单个列表项行组件进行跳转
|
|
1994
|
+
- 一次只可以点击单个行组件
|
|
1995
|
+
|
|
1996
|
+
## 机票下单链路
|
|
1997
|
+
|
|
1998
|
+
### 飞猪首页 for 机票
|
|
1999
|
+
|
|
2000
|
+
- 页面描述
|
|
2001
|
+
- 头部搜索框和搜索按钮
|
|
2002
|
+
- 头部我的收藏按钮
|
|
2003
|
+
- 顶部金刚区域, 有各行业的入口, 一般为图片+文字,如机票文案+飞机logo
|
|
2004
|
+
- 该页面操作限制规则:
|
|
2005
|
+
- 机票应点击金刚区域的飞机按钮所在金刚(它在金刚区域第二个,酒店金刚后,火车票金刚前), 点击即可进入机票首页
|
|
2006
|
+
|
|
2007
|
+
### 机票首页
|
|
2008
|
+
|
|
2009
|
+
- 页面描述:
|
|
2010
|
+
- 顶部4个tab: 机票/特价机票(默认)、火车票、汽车票、接送/租车, 购买机票流程中, 一般不需要切换点击
|
|
2011
|
+
- 顶部3个tab切换行程类型: 单程(默认), 多程, 往返, 如果没有特别说明, 一般是单程机票购买流程
|
|
2012
|
+
- **注意**:出发城市、到达城市位于页面左右两侧, 并且在搜索按钮前, OCR结果一般因为在左右两侧所有分为两个独立的元素, 在搜索按钮后, 可能存在出发城市-到达城市相关的快捷操作文案, 这不是入口, 在candidate_elements, 以及locate里都不要选择那些快捷操作文案
|
|
2013
|
+
- 出发城市: 一般有城市名(在页面左侧), 如: 北京
|
|
2014
|
+
- 到达城市: 一般有城市名(在页面右侧), 如: 杭州
|
|
2015
|
+
- 搜索机票: 按钮, 点击进入机票列表页
|
|
2016
|
+
- 其他不常用按钮不做过多赘述
|
|
2017
|
+
|
|
2018
|
+
### 机票首页-城市选择页
|
|
2019
|
+
|
|
2020
|
+
- 页面描述: 城市选择页是机票首页的弹窗, 从点击出发地、目的地进入, 主要特征是一个遮罩在机票首页的半弹窗, 顶部有单目的地、多目的地的切换tab、中间有国内、国际城市的切换tab、主体内容是各个热门城市、地区的选项
|
|
2021
|
+
- 该页面操作限制规则:
|
|
2022
|
+
1. 机票首页, 点击默认出发地、或目的地, 进入城市选择页
|
|
2023
|
+
2. 返回Input, 参数是点击的搜索框以及输入的机场/城市内容
|
|
2024
|
+
3. 选择对应的机场/城市
|
|
2025
|
+
|
|
2026
|
+
### 机票列表页
|
|
2027
|
+
|
|
2028
|
+
- 页面描述:
|
|
2029
|
+
- 机票日期选择器: 页面顶部, 点击可切换日期, 会刷新列表
|
|
2030
|
+
- 机票列表, 点击进入不同机票详情
|
|
2031
|
+
- 页面操作限制规则
|
|
2032
|
+
- 机票日期选择器 (如无特殊要求, 请不要操作这部分): 如果最终目标中没有明确说明, 请不要在这里操作切换日期
|
|
2033
|
+
- 机票列表(请在此部分选择航班, 进入机票详情页): 一般展示航班的出发时间/机场、到达时间/机场, 以及机票价格, 如果最终目标提到了对应信息请选择对应的航班, 如果没有说明请任选一个进入机票详情页
|
|
2034
|
+
|
|
2035
|
+
### 机票详情页
|
|
2036
|
+
|
|
2037
|
+
- 页面描述:
|
|
2038
|
+
- 头部信息: 展示机票航班信息, 只做展示都不可点击
|
|
2039
|
+
- 仓位切换tab: 默认经济舱, 可点击切换成公务/头等舱
|
|
2040
|
+
- 航班报价列表: 只有 "订" (完全一致, 不包含: 预订, 下订单等其他字)关键字的才可点击, 点击后进入填单页
|
|
2041
|
+
- 页面操作限制规则
|
|
2042
|
+
- 弹窗处理逻辑: 点击订后, 如果出现弹窗, 请点击继续购买相关的选项
|
|
2043
|
+
|
|
2044
|
+
### 机票下单页
|
|
2045
|
+
|
|
2046
|
+
- 页面描述:
|
|
2047
|
+
- 头部信息: 展示机票航班信息, 只做展示都不可点击
|
|
2048
|
+
- 选择完乘机人后点击底部的按钮就行下单。按钮可能是"去支付"或者"预订"
|
|
2049
|
+
- 支付按钮
|
|
2050
|
+
- **⚠️ 该页面操作限制规则:判断到达该页面后,即可返回END结束,不要真实操作下单。**`;
|
|
2051
|
+
const debug = (0, logger_.getDebug)('android:app-knowledge');
|
|
2052
|
+
const MAX_SCREENSHOTS_PER_PAGE = 2;
|
|
2053
|
+
const packageConfigMap = {
|
|
2054
|
+
'com.taobao.trip': {
|
|
2055
|
+
knowledge: fliggyKnowledge,
|
|
2056
|
+
screenshotDirName: 'fliggy',
|
|
2057
|
+
defaultPage: 'home',
|
|
2058
|
+
pageIds: [
|
|
2059
|
+
'home',
|
|
2060
|
+
'search-default',
|
|
2061
|
+
'search-sug',
|
|
2062
|
+
'search-result',
|
|
2063
|
+
'fliggy-qwen',
|
|
2064
|
+
'ticket-detail',
|
|
2065
|
+
'hotel-list',
|
|
2066
|
+
'hotel-detail',
|
|
2067
|
+
'flight-list',
|
|
2068
|
+
'flight-detail',
|
|
2069
|
+
'ticket-order',
|
|
2070
|
+
'hotel-order',
|
|
2071
|
+
'flight-order'
|
|
2072
|
+
]
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
function getKnowledgeForPackage(packageName) {
|
|
2076
|
+
const config = packageConfigMap[packageName];
|
|
2077
|
+
if (config) {
|
|
2078
|
+
debug('found knowledge for package: %s', packageName);
|
|
2079
|
+
return config.knowledge;
|
|
2080
|
+
}
|
|
2081
|
+
debug('no knowledge found for package: %s', packageName);
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
function getScreenshotsForPage(packageName, pageId) {
|
|
2085
|
+
const config = packageConfigMap[packageName];
|
|
2086
|
+
if (!config) {
|
|
2087
|
+
debug('no config for package: %s, skipping screenshots', packageName);
|
|
2088
|
+
return [];
|
|
2089
|
+
}
|
|
2090
|
+
const pageDir = external_node_path_.join(__dirname, 'screenshots', config.screenshotDirName, pageId);
|
|
2091
|
+
if (!external_node_fs_.existsSync(pageDir)) {
|
|
2092
|
+
debug('screenshot directory not found: %s', pageDir);
|
|
2093
|
+
return [];
|
|
2094
|
+
}
|
|
2095
|
+
try {
|
|
2096
|
+
const files = external_node_fs_.readdirSync(pageDir);
|
|
2097
|
+
const imageFiles = files.filter((f)=>/\.(jpg|jpeg|png)$/i.test(f)).map((f)=>external_node_path_.join(pageDir, f));
|
|
2098
|
+
if (0 === imageFiles.length) {
|
|
2099
|
+
debug('no image files in directory: %s', pageDir);
|
|
2100
|
+
return [];
|
|
2101
|
+
}
|
|
2102
|
+
const result = imageFiles.slice(0, MAX_SCREENSHOTS_PER_PAGE);
|
|
2103
|
+
debug('found %d screenshot(s) for %s/%s (returning %d)', imageFiles.length, packageName, pageId, result.length);
|
|
2104
|
+
return result;
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
debug('error reading screenshot directory %s: %s', pageDir, err);
|
|
2107
|
+
return [];
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function getDefaultPageForPackage(packageName) {
|
|
2111
|
+
const config = packageConfigMap[packageName];
|
|
2112
|
+
return config?.defaultPage ?? null;
|
|
2113
|
+
}
|
|
2114
|
+
function getPageIdsForPackage(packageName) {
|
|
2115
|
+
const config = packageConfigMap[packageName];
|
|
2116
|
+
return config?.pageIds ?? [];
|
|
2117
|
+
}
|
|
1772
2118
|
const defaultAppNameMapping = {
|
|
1773
2119
|
微信: 'com.tencent.mm',
|
|
1774
2120
|
QQ: 'com.tencent.mobileqq',
|
|
@@ -1972,17 +2318,63 @@ class AndroidAgent extends Agent {
|
|
|
1972
2318
|
command
|
|
1973
2319
|
});
|
|
1974
2320
|
}
|
|
2321
|
+
loadAppKnowledge(packageName) {
|
|
2322
|
+
if (this.lastKnowledgePackageName === packageName) return void debugAgent('knowledge already loaded for package: %s, skipping', packageName);
|
|
2323
|
+
const knowledge = getKnowledgeForPackage(packageName);
|
|
2324
|
+
if (knowledge) {
|
|
2325
|
+
this.setAIActContext(knowledge);
|
|
2326
|
+
debugAgent('loaded app knowledge for package: %s', packageName);
|
|
2327
|
+
} else debugAgent('no app knowledge available for package: %s', packageName);
|
|
2328
|
+
const pageIds = getPageIdsForPackage(packageName);
|
|
2329
|
+
if (pageIds.length > 0) {
|
|
2330
|
+
this.availablePageIds = pageIds;
|
|
2331
|
+
this.referenceScreenshotProvider = async (pageId)=>{
|
|
2332
|
+
const effectivePageId = pageId ?? getDefaultPageForPackage(packageName);
|
|
2333
|
+
if (!effectivePageId) return [];
|
|
2334
|
+
return this.resolveReferenceScreenshots(effectivePageId, packageName);
|
|
2335
|
+
};
|
|
2336
|
+
debugAgent('screenshot knowledge provider set for package: %s (%d pages)', packageName, pageIds.length);
|
|
2337
|
+
} else {
|
|
2338
|
+
this.availablePageIds = void 0;
|
|
2339
|
+
this.referenceScreenshotProvider = void 0;
|
|
2340
|
+
}
|
|
2341
|
+
this.lastKnowledgePackageName = packageName;
|
|
2342
|
+
}
|
|
2343
|
+
async resolveReferenceScreenshots(pageId, packageName) {
|
|
2344
|
+
const screenshotPaths = getScreenshotsForPage(packageName, pageId);
|
|
2345
|
+
if (!screenshotPaths.length) return [];
|
|
2346
|
+
return Promise.all(screenshotPaths.map(async (p, i)=>{
|
|
2347
|
+
const base64 = await external_node_fs_.promises.readFile(p, {
|
|
2348
|
+
encoding: 'base64'
|
|
2349
|
+
});
|
|
2350
|
+
const ext = external_node_path_.extname(p).slice(1) || 'jpeg';
|
|
2351
|
+
return {
|
|
2352
|
+
name: `${pageId}-${i + 1}`,
|
|
2353
|
+
url: `data:image/${ext};base64,${base64}`
|
|
2354
|
+
};
|
|
2355
|
+
}));
|
|
2356
|
+
}
|
|
2357
|
+
async detectAndLoadAppKnowledge() {
|
|
2358
|
+
try {
|
|
2359
|
+
const dumpsysOutput = await this.runAdbShell('dumpsys activity activities | grep -E "mResumedActivity|mTopActivityRecord"');
|
|
2360
|
+
const packageName = parseForegroundPackageName(dumpsysOutput);
|
|
2361
|
+
if (packageName) this.loadAppKnowledge(packageName);
|
|
2362
|
+
else debugAgent('could not detect foreground app package name');
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
debugAgent('failed to detect foreground app for knowledge injection:', error);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
1975
2367
|
createActionWrapper(name) {
|
|
1976
2368
|
const action = this.wrapActionInActionSpace(name);
|
|
1977
2369
|
return (...args)=>action(args[0]);
|
|
1978
2370
|
}
|
|
1979
2371
|
constructor(device, opts){
|
|
1980
|
-
super(device, opts), agent_define_property(this, "back", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "recentApps", void 0), agent_define_property(this, "appNameMapping", void 0);
|
|
2372
|
+
super(device, opts), agent_define_property(this, "back", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "recentApps", void 0), agent_define_property(this, "appNameMapping", void 0), agent_define_property(this, "lastKnowledgePackageName", void 0);
|
|
1981
2373
|
this.appNameMapping = mergeAndNormalizeAppNameMapping(defaultAppNameMapping, opts?.appNameMapping);
|
|
1982
2374
|
device.setAppNameMapping(this.appNameMapping);
|
|
1983
2375
|
device.inputVerifyFn = async (text)=>{
|
|
1984
2376
|
try {
|
|
1985
|
-
return await this.aiBoolean(`the currently focused input field
|
|
2377
|
+
return await this.aiBoolean(`the currently focused input field (text box) shows "${text}" as its actual typed value — ignore any IME candidate bar, autocomplete dropdown, or search suggestion list below the field; only check the input field itself`);
|
|
1986
2378
|
} catch {
|
|
1987
2379
|
return false;
|
|
1988
2380
|
}
|
|
@@ -1992,6 +2384,17 @@ class AndroidAgent extends Agent {
|
|
|
1992
2384
|
this.recentApps = this.createActionWrapper('AndroidRecentAppsButton');
|
|
1993
2385
|
}
|
|
1994
2386
|
}
|
|
2387
|
+
function parseForegroundPackageName(dumpsysOutput) {
|
|
2388
|
+
const patterns = [
|
|
2389
|
+
/mResumedActivity.*?\s([a-zA-Z][a-zA-Z0-9_.]*)\//,
|
|
2390
|
+
/mTopActivityRecord.*?\s([a-zA-Z][a-zA-Z0-9_.]*)\//
|
|
2391
|
+
];
|
|
2392
|
+
for (const pattern of patterns){
|
|
2393
|
+
const match = dumpsysOutput.match(pattern);
|
|
2394
|
+
if (match?.[1]) return match[1];
|
|
2395
|
+
}
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
1995
2398
|
async function agentFromAdbDevice(deviceId, opts) {
|
|
1996
2399
|
if (!deviceId) {
|
|
1997
2400
|
const devices = await getConnectedDevices();
|
|
@@ -2001,9 +2404,11 @@ async function agentFromAdbDevice(deviceId, opts) {
|
|
|
2001
2404
|
}
|
|
2002
2405
|
const device = new AndroidDevice(deviceId, opts || {});
|
|
2003
2406
|
await device.connect();
|
|
2004
|
-
|
|
2407
|
+
const agent = new AndroidAgent(device, opts);
|
|
2408
|
+
await agent.detectAndLoadAppKnowledge();
|
|
2409
|
+
return agent;
|
|
2005
2410
|
}
|
|
2006
|
-
const
|
|
2411
|
+
const mcp_tools_debug = (0, logger_.getDebug)('mcp:android-tools');
|
|
2007
2412
|
class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
2008
2413
|
createTemporaryDevice() {
|
|
2009
2414
|
return new AndroidDevice('temp-for-action-space', {});
|
|
@@ -2013,12 +2418,15 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
|
2013
2418
|
try {
|
|
2014
2419
|
await this.agent.destroy?.();
|
|
2015
2420
|
} catch (error) {
|
|
2016
|
-
|
|
2421
|
+
mcp_tools_debug('Failed to destroy agent during cleanup:', error);
|
|
2017
2422
|
}
|
|
2018
2423
|
this.agent = void 0;
|
|
2019
2424
|
}
|
|
2020
|
-
if (this.agent)
|
|
2021
|
-
|
|
2425
|
+
if (this.agent) {
|
|
2426
|
+
await this.agent.detectAndLoadAppKnowledge();
|
|
2427
|
+
return this.agent;
|
|
2428
|
+
}
|
|
2429
|
+
mcp_tools_debug('Creating Android agent with deviceId:', deviceId || 'auto-detect');
|
|
2022
2430
|
const agent = await agentFromAdbDevice(deviceId, {
|
|
2023
2431
|
autoDismissKeyboard: false
|
|
2024
2432
|
});
|