mobile-mcp-ai 2.7.0__py3-none-any.whl → 2.7.4__py3-none-any.whl
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.
- mobile_mcp/core/basic_tools_lite.py +584 -68
- mobile_mcp/mcp_tools/mcp_server.py +107 -136
- {mobile_mcp_ai-2.7.0.dist-info → mobile_mcp_ai-2.7.4.dist-info}/METADATA +1 -1
- {mobile_mcp_ai-2.7.0.dist-info → mobile_mcp_ai-2.7.4.dist-info}/RECORD +8 -8
- {mobile_mcp_ai-2.7.0.dist-info → mobile_mcp_ai-2.7.4.dist-info}/WHEEL +1 -1
- {mobile_mcp_ai-2.7.0.dist-info → mobile_mcp_ai-2.7.4.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.7.0.dist-info → mobile_mcp_ai-2.7.4.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.7.0.dist-info → mobile_mcp_ai-2.7.4.dist-info}/top_level.txt +0 -0
|
@@ -158,6 +158,33 @@ class BasicMobileToolsLite:
|
|
|
158
158
|
return info.get('package')
|
|
159
159
|
except Exception:
|
|
160
160
|
return None
|
|
161
|
+
|
|
162
|
+
def _normalize_resource_id(self, resource_id: str) -> str:
|
|
163
|
+
"""标准化 resource-id,支持前端只传简写 id 时自动补全包名
|
|
164
|
+
|
|
165
|
+
约定:
|
|
166
|
+
- Android:
|
|
167
|
+
- 如果传入的是完整 id(包含 ':' 或 '/'),直接返回
|
|
168
|
+
- 如果是简写(如 'qylt_search_input_layout'),自动补全为
|
|
169
|
+
'{package}:id/{resource_id}',package 优先使用 target_package,
|
|
170
|
+
否则使用当前前台应用包名
|
|
171
|
+
- iOS: 直接原样返回
|
|
172
|
+
"""
|
|
173
|
+
# iOS 不做处理,保持与 WDA 一致
|
|
174
|
+
if self._is_ios():
|
|
175
|
+
return resource_id
|
|
176
|
+
|
|
177
|
+
# 已经是完整 id 或者包含路径信息时,不再修改
|
|
178
|
+
if ":" in resource_id or "/" in resource_id:
|
|
179
|
+
return resource_id
|
|
180
|
+
|
|
181
|
+
# 尝试获取包名:优先使用目标应用包名,其次当前前台应用
|
|
182
|
+
package = getattr(self, "target_package", None) or self._get_current_package()
|
|
183
|
+
if not package:
|
|
184
|
+
# 没有包名信息时,回退为原值,避免误拼接错误包名
|
|
185
|
+
return resource_id
|
|
186
|
+
|
|
187
|
+
return f"{package}:id/{resource_id}"
|
|
161
188
|
|
|
162
189
|
def _check_app_switched(self) -> Dict:
|
|
163
190
|
"""检查是否已跳出目标应用
|
|
@@ -1286,13 +1313,13 @@ class BasicMobileToolsLite:
|
|
|
1286
1313
|
page_texts = self._get_page_texts(10)
|
|
1287
1314
|
return {"success": True, "page_texts": page_texts}
|
|
1288
1315
|
|
|
1289
|
-
#
|
|
1316
|
+
# 选择器失败,用控件中心坐标点兜底
|
|
1290
1317
|
if bounds:
|
|
1291
1318
|
x = (bounds[0] + bounds[2]) // 2
|
|
1292
1319
|
y = (bounds[1] + bounds[3]) // 2
|
|
1293
1320
|
self.client.u2.click(x, y)
|
|
1294
1321
|
time.sleep(0.3)
|
|
1295
|
-
self._record_click('
|
|
1322
|
+
self._record_click('coords', f"{x},{y}", x_pct, y_pct,
|
|
1296
1323
|
element_desc=text)
|
|
1297
1324
|
# 验证逻辑
|
|
1298
1325
|
if verify:
|
|
@@ -1346,12 +1373,14 @@ class BasicMobileToolsLite:
|
|
|
1346
1373
|
except Exception as e:
|
|
1347
1374
|
return {"success": True, "verified": False, "hint": f"验证异常: {e}"}
|
|
1348
1375
|
|
|
1349
|
-
def _find_element_in_tree(self, text: str, position: Optional[str] = None) -> Optional[Dict]:
|
|
1350
|
-
"""在 XML
|
|
1376
|
+
def _find_element_in_tree(self, text: str, position: Optional[str] = None, exact_match: bool = True) -> Optional[Dict]:
|
|
1377
|
+
"""在 XML 树中查找指定文本的元素,优先返回可点击的元素
|
|
1351
1378
|
|
|
1352
1379
|
Args:
|
|
1353
1380
|
text: 要查找的文本
|
|
1354
1381
|
position: 位置信息,用于在有多个相同文案时筛选
|
|
1382
|
+
exact_match: 是否精确匹配。True=优先精确匹配(用于定位元素如点击),
|
|
1383
|
+
False=只进行包含匹配(用于验证元素)
|
|
1355
1384
|
"""
|
|
1356
1385
|
try:
|
|
1357
1386
|
xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
@@ -1383,26 +1412,40 @@ class BasicMobileToolsLite:
|
|
|
1383
1412
|
attr_type = None
|
|
1384
1413
|
attr_value = None
|
|
1385
1414
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1415
|
+
if exact_match:
|
|
1416
|
+
# 精确匹配模式(用于定位元素):优先精确匹配
|
|
1417
|
+
# 精确匹配 text
|
|
1418
|
+
if elem_text == text:
|
|
1419
|
+
is_match = True
|
|
1420
|
+
attr_type = 'text'
|
|
1421
|
+
attr_value = text
|
|
1422
|
+
# 精确匹配 content-desc
|
|
1423
|
+
elif elem_desc == text:
|
|
1424
|
+
is_match = True
|
|
1425
|
+
attr_type = 'description'
|
|
1426
|
+
attr_value = text
|
|
1427
|
+
# 精确匹配找不到时,再尝试包含匹配(作为兜底)
|
|
1428
|
+
elif text in elem_text:
|
|
1429
|
+
is_match = True
|
|
1430
|
+
attr_type = 'textContains'
|
|
1431
|
+
attr_value = text
|
|
1432
|
+
# 包含匹配 content-desc
|
|
1433
|
+
elif text in elem_desc:
|
|
1434
|
+
is_match = True
|
|
1435
|
+
attr_type = 'descriptionContains'
|
|
1436
|
+
attr_value = text
|
|
1437
|
+
else:
|
|
1438
|
+
# 包含匹配模式(用于验证元素):只进行包含匹配
|
|
1439
|
+
# 包含匹配 text
|
|
1440
|
+
if text in elem_text:
|
|
1441
|
+
is_match = True
|
|
1442
|
+
attr_type = 'textContains'
|
|
1443
|
+
attr_value = text
|
|
1444
|
+
# 包含匹配 content-desc
|
|
1445
|
+
elif text in elem_desc:
|
|
1446
|
+
is_match = True
|
|
1447
|
+
attr_type = 'descriptionContains'
|
|
1448
|
+
attr_value = text
|
|
1406
1449
|
|
|
1407
1450
|
if is_match and bounds:
|
|
1408
1451
|
# 计算元素的中心点坐标
|
|
@@ -1421,6 +1464,17 @@ class BasicMobileToolsLite:
|
|
|
1421
1464
|
if not matched_elements:
|
|
1422
1465
|
return None
|
|
1423
1466
|
|
|
1467
|
+
# 精确匹配模式下,优先返回精确匹配的元素(text/description),再返回包含匹配的元素
|
|
1468
|
+
if exact_match:
|
|
1469
|
+
exact_matches = [m for m in matched_elements if m['attr_type'] in ['text', 'description']]
|
|
1470
|
+
contains_matches = [m for m in matched_elements if m['attr_type'] in ['textContains', 'descriptionContains']]
|
|
1471
|
+
# 如果有精确匹配,优先使用精确匹配的结果
|
|
1472
|
+
if exact_matches:
|
|
1473
|
+
matched_elements = exact_matches + contains_matches
|
|
1474
|
+
# 如果没有精确匹配,使用包含匹配的结果
|
|
1475
|
+
else:
|
|
1476
|
+
matched_elements = contains_matches
|
|
1477
|
+
|
|
1424
1478
|
# 如果有位置信息,根据位置筛选
|
|
1425
1479
|
if position and len(matched_elements) > 1:
|
|
1426
1480
|
position_lower = position.lower()
|
|
@@ -1459,6 +1513,7 @@ class BasicMobileToolsLite:
|
|
|
1459
1513
|
}
|
|
1460
1514
|
|
|
1461
1515
|
# 没有位置信息时,优先返回可点击的元素
|
|
1516
|
+
# 由于前面已经排序(精确匹配在前),这里会优先返回精确匹配且可点击的元素
|
|
1462
1517
|
for match in matched_elements:
|
|
1463
1518
|
if match['clickable']:
|
|
1464
1519
|
return {
|
|
@@ -1504,17 +1559,23 @@ class BasicMobileToolsLite:
|
|
|
1504
1559
|
else:
|
|
1505
1560
|
return {"success": False, "msg": "iOS未初始化"}
|
|
1506
1561
|
else:
|
|
1507
|
-
|
|
1562
|
+
normalized_id = self._normalize_resource_id(resource_id)
|
|
1563
|
+
elem = self.client.u2(resourceId=normalized_id)
|
|
1508
1564
|
if elem.exists(timeout=0.5):
|
|
1509
1565
|
count = elem.count
|
|
1510
1566
|
if index < count:
|
|
1511
1567
|
elem[index].click()
|
|
1512
1568
|
time.sleep(0.3)
|
|
1513
|
-
|
|
1569
|
+
# 记录时同时保留原始入参和实际使用的 id 信息
|
|
1570
|
+
self._record_click('id', normalized_id, element_desc=resource_id)
|
|
1514
1571
|
return {"success": True}
|
|
1515
1572
|
else:
|
|
1516
1573
|
return {"success": False, "msg": f"索引{index}超出范围(共{count}个)"}
|
|
1517
|
-
return {
|
|
1574
|
+
return {
|
|
1575
|
+
"success": False,
|
|
1576
|
+
"fallback": "vision",
|
|
1577
|
+
"msg": f"未找到ID'{resource_id}' (实际匹配: '{normalized_id}')"
|
|
1578
|
+
}
|
|
1518
1579
|
except Exception as e:
|
|
1519
1580
|
return {"success": False, "msg": str(e)}
|
|
1520
1581
|
|
|
@@ -1777,13 +1838,20 @@ class BasicMobileToolsLite:
|
|
|
1777
1838
|
return {"success": True}
|
|
1778
1839
|
return {"success": False, "msg": f"未找到'{resource_id}'"}
|
|
1779
1840
|
else:
|
|
1780
|
-
|
|
1841
|
+
normalized_id = self._normalize_resource_id(resource_id)
|
|
1842
|
+
elem = self.client.u2(resourceId=normalized_id)
|
|
1781
1843
|
if elem.exists(timeout=0.5):
|
|
1782
1844
|
elem.long_click(duration=duration)
|
|
1783
1845
|
time.sleep(0.3)
|
|
1784
|
-
self._record_long_press('id',
|
|
1785
|
-
return {
|
|
1786
|
-
|
|
1846
|
+
self._record_long_press('id', normalized_id, duration, element_desc=resource_id)
|
|
1847
|
+
return {
|
|
1848
|
+
"success": True,
|
|
1849
|
+
"message": f"✅ 长按成功: {resource_id} (实际匹配: {normalized_id}) 持续 {duration}s"
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
"success": False,
|
|
1853
|
+
"msg": f"未找到'{resource_id}' (实际匹配: '{normalized_id}')"
|
|
1854
|
+
}
|
|
1787
1855
|
except Exception as e:
|
|
1788
1856
|
return {"success": False, "message": f"❌ 长按失败: {e}"}
|
|
1789
1857
|
|
|
@@ -1833,7 +1901,8 @@ class BasicMobileToolsLite:
|
|
|
1833
1901
|
}
|
|
1834
1902
|
return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
|
|
1835
1903
|
else:
|
|
1836
|
-
|
|
1904
|
+
normalized_id = self._normalize_resource_id(resource_id)
|
|
1905
|
+
elements = self.client.u2(resourceId=normalized_id)
|
|
1837
1906
|
|
|
1838
1907
|
# 检查是否存在
|
|
1839
1908
|
if elements.exists(timeout=0.5):
|
|
@@ -1843,7 +1912,7 @@ class BasicMobileToolsLite:
|
|
|
1843
1912
|
if count == 1:
|
|
1844
1913
|
elements.set_text(text)
|
|
1845
1914
|
time.sleep(0.3)
|
|
1846
|
-
self._record_input(text, 'id',
|
|
1915
|
+
self._record_input(text, 'id', normalized_id)
|
|
1847
1916
|
|
|
1848
1917
|
# 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
|
|
1849
1918
|
app_check = self._check_app_switched()
|
|
@@ -1851,7 +1920,7 @@ class BasicMobileToolsLite:
|
|
|
1851
1920
|
if app_check['switched']:
|
|
1852
1921
|
return_result = self._return_to_target_app()
|
|
1853
1922
|
|
|
1854
|
-
msg = f"✅ 输入成功: '{text}'"
|
|
1923
|
+
msg = f"✅ 输入成功: '{text}' (id: {resource_id}, 实际匹配: {normalized_id})"
|
|
1855
1924
|
if app_check['switched']:
|
|
1856
1925
|
msg += f"\n{app_check['message']}"
|
|
1857
1926
|
if return_result:
|
|
@@ -2071,13 +2140,16 @@ class BasicMobileToolsLite:
|
|
|
2071
2140
|
|
|
2072
2141
|
# ==================== 导航操作 ====================
|
|
2073
2142
|
|
|
2074
|
-
async def swipe(self, direction: str, y: Optional[int] = None, y_percent: Optional[float] = None
|
|
2143
|
+
async def swipe(self, direction: str, y: Optional[int] = None, y_percent: Optional[float] = None,
|
|
2144
|
+
distance: Optional[int] = None, distance_percent: Optional[float] = None) -> Dict:
|
|
2075
2145
|
"""滑动屏幕
|
|
2076
2146
|
|
|
2077
2147
|
Args:
|
|
2078
2148
|
direction: 滑动方向 (up/down/left/right)
|
|
2079
2149
|
y: 左右滑动时指定的高度坐标(像素)
|
|
2080
2150
|
y_percent: 左右滑动时指定的高度百分比 (0-100)
|
|
2151
|
+
distance: 横向滑动时指定的滑动距离(像素),仅用于 left/right
|
|
2152
|
+
distance_percent: 横向滑动时指定的滑动距离百分比 (0-100),仅用于 left/right
|
|
2081
2153
|
"""
|
|
2082
2154
|
try:
|
|
2083
2155
|
if self._is_ios():
|
|
@@ -2104,20 +2176,53 @@ class BasicMobileToolsLite:
|
|
|
2104
2176
|
swipe_y = y
|
|
2105
2177
|
else:
|
|
2106
2178
|
swipe_y = center_y
|
|
2179
|
+
|
|
2180
|
+
# 计算横向滑动距离
|
|
2181
|
+
if distance_percent is not None:
|
|
2182
|
+
if not (0 <= distance_percent <= 100):
|
|
2183
|
+
return {"success": False, "message": f"❌ distance_percent 必须在 0-100 之间: {distance_percent}"}
|
|
2184
|
+
swipe_distance = int(width * distance_percent / 100)
|
|
2185
|
+
elif distance is not None:
|
|
2186
|
+
if distance <= 0:
|
|
2187
|
+
return {"success": False, "message": f"❌ distance 必须大于 0: {distance}"}
|
|
2188
|
+
if distance > width:
|
|
2189
|
+
return {"success": False, "message": f"❌ distance 不能超过屏幕宽度 ({width}): {distance}"}
|
|
2190
|
+
swipe_distance = distance
|
|
2191
|
+
else:
|
|
2192
|
+
# 默认滑动距离:屏幕宽度的 60%(从 0.8 到 0.2)
|
|
2193
|
+
swipe_distance = int(width * 0.6)
|
|
2194
|
+
|
|
2195
|
+
# 计算起始和结束位置
|
|
2196
|
+
if direction == 'left':
|
|
2197
|
+
# 从右向左滑动:起始点在右侧,结束点在左侧
|
|
2198
|
+
# 确保起始点不超出屏幕右边界
|
|
2199
|
+
start_x = min(center_x + swipe_distance // 2, width - 10)
|
|
2200
|
+
end_x = start_x - swipe_distance
|
|
2201
|
+
# 确保结束点不超出屏幕左边界
|
|
2202
|
+
if end_x < 10:
|
|
2203
|
+
end_x = 10
|
|
2204
|
+
start_x = min(end_x + swipe_distance, width - 10)
|
|
2205
|
+
else: # right
|
|
2206
|
+
# 从左向右滑动:起始点在左侧,结束点在右侧
|
|
2207
|
+
# 确保起始点不超出屏幕左边界
|
|
2208
|
+
start_x = max(center_x - swipe_distance // 2, 10)
|
|
2209
|
+
end_x = start_x + swipe_distance
|
|
2210
|
+
# 确保结束点不超出屏幕右边界
|
|
2211
|
+
if end_x > width - 10:
|
|
2212
|
+
end_x = width - 10
|
|
2213
|
+
start_x = max(end_x - swipe_distance, 10)
|
|
2214
|
+
|
|
2215
|
+
x1, y1, x2, y2 = start_x, swipe_y, end_x, swipe_y
|
|
2107
2216
|
else:
|
|
2108
2217
|
swipe_y = center_y
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
if direction not in swipe_map:
|
|
2118
|
-
return {"success": False, "message": f"❌ 不支持的方向: {direction}"}
|
|
2119
|
-
|
|
2120
|
-
x1, y1, x2, y2 = swipe_map[direction]
|
|
2218
|
+
# 纵向滑动保持原有逻辑
|
|
2219
|
+
swipe_map = {
|
|
2220
|
+
'up': (center_x, int(height * 0.8), center_x, int(height * 0.2)),
|
|
2221
|
+
'down': (center_x, int(height * 0.2), center_x, int(height * 0.8)),
|
|
2222
|
+
}
|
|
2223
|
+
if direction not in swipe_map:
|
|
2224
|
+
return {"success": False, "message": f"❌ 不支持的方向: {direction}"}
|
|
2225
|
+
x1, y1, x2, y2 = swipe_map[direction]
|
|
2121
2226
|
|
|
2122
2227
|
if self._is_ios():
|
|
2123
2228
|
ios_client.wda.swipe(x1, y1, x2, y2)
|
|
@@ -2138,10 +2243,21 @@ class BasicMobileToolsLite:
|
|
|
2138
2243
|
# 构建返回消息
|
|
2139
2244
|
msg = f"✅ 滑动成功: {direction}"
|
|
2140
2245
|
if direction in ['left', 'right']:
|
|
2246
|
+
msg_parts = []
|
|
2141
2247
|
if y_percent is not None:
|
|
2142
|
-
|
|
2248
|
+
msg_parts.append(f"高度: {y_percent}% = {swipe_y}px")
|
|
2143
2249
|
elif y is not None:
|
|
2144
|
-
|
|
2250
|
+
msg_parts.append(f"高度: {y}px")
|
|
2251
|
+
|
|
2252
|
+
if distance_percent is not None:
|
|
2253
|
+
msg_parts.append(f"距离: {distance_percent}% = {swipe_distance}px")
|
|
2254
|
+
elif distance is not None:
|
|
2255
|
+
msg_parts.append(f"距离: {distance}px")
|
|
2256
|
+
else:
|
|
2257
|
+
msg_parts.append(f"距离: 默认 {swipe_distance}px")
|
|
2258
|
+
|
|
2259
|
+
if msg_parts:
|
|
2260
|
+
msg += f" ({', '.join(msg_parts)})"
|
|
2145
2261
|
|
|
2146
2262
|
# 如果检测到应用跳转,添加警告和返回结果
|
|
2147
2263
|
if app_check['switched']:
|
|
@@ -2206,6 +2322,170 @@ class BasicMobileToolsLite:
|
|
|
2206
2322
|
self.operation_history.append(record)
|
|
2207
2323
|
return {"success": True}
|
|
2208
2324
|
|
|
2325
|
+
async def drag_progress_bar(self, direction: str = "right", distance_percent: float = 30.0,
|
|
2326
|
+
y_percent: Optional[float] = None, y: Optional[int] = None) -> Dict:
|
|
2327
|
+
"""智能拖动进度条
|
|
2328
|
+
|
|
2329
|
+
自动检测进度条是否可见:
|
|
2330
|
+
- 如果进度条已显示,直接拖动(无需先点击播放区域)
|
|
2331
|
+
- 如果进度条未显示,先点击播放区域显示控制栏,再拖动
|
|
2332
|
+
|
|
2333
|
+
Args:
|
|
2334
|
+
direction: 拖动方向,'left'(倒退)或 'right'(前进),默认 'right'
|
|
2335
|
+
distance_percent: 拖动距离百分比 (0-100),默认 30%
|
|
2336
|
+
y_percent: 进度条的垂直位置百分比 (0-100),如果未指定则自动检测
|
|
2337
|
+
y: 进度条的垂直位置坐标(像素),如果未指定则自动检测
|
|
2338
|
+
"""
|
|
2339
|
+
try:
|
|
2340
|
+
import xml.etree.ElementTree as ET
|
|
2341
|
+
import re
|
|
2342
|
+
|
|
2343
|
+
if self._is_ios():
|
|
2344
|
+
return {"success": False, "message": "❌ iOS 暂不支持,请使用 mobile_swipe"}
|
|
2345
|
+
|
|
2346
|
+
if direction not in ['left', 'right']:
|
|
2347
|
+
return {"success": False, "message": f"❌ 拖动方向必须是 'left' 或 'right': {direction}"}
|
|
2348
|
+
|
|
2349
|
+
screen_width, screen_height = self.client.u2.window_size()
|
|
2350
|
+
|
|
2351
|
+
# 获取 XML 查找进度条
|
|
2352
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
2353
|
+
root = ET.fromstring(xml_string)
|
|
2354
|
+
|
|
2355
|
+
progress_bar_found = False
|
|
2356
|
+
progress_bar_y = None
|
|
2357
|
+
progress_bar_y_percent = None
|
|
2358
|
+
|
|
2359
|
+
# 查找进度条元素(SeekBar、ProgressBar)
|
|
2360
|
+
for elem in root.iter():
|
|
2361
|
+
class_name = elem.attrib.get('class', '')
|
|
2362
|
+
resource_id = elem.attrib.get('resource-id', '')
|
|
2363
|
+
bounds_str = elem.attrib.get('bounds', '')
|
|
2364
|
+
|
|
2365
|
+
# 检查是否是进度条
|
|
2366
|
+
is_progress_bar = (
|
|
2367
|
+
'SeekBar' in class_name or
|
|
2368
|
+
'ProgressBar' in class_name or
|
|
2369
|
+
'progress' in resource_id.lower() or
|
|
2370
|
+
'seek' in resource_id.lower()
|
|
2371
|
+
)
|
|
2372
|
+
|
|
2373
|
+
if is_progress_bar and bounds_str:
|
|
2374
|
+
# 解析 bounds 获取进度条位置
|
|
2375
|
+
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
|
|
2376
|
+
if match:
|
|
2377
|
+
x1, y1, x2, y2 = map(int, match.groups())
|
|
2378
|
+
center_y = (y1 + y2) // 2
|
|
2379
|
+
progress_bar_y = center_y
|
|
2380
|
+
progress_bar_y_percent = round(center_y / screen_height * 100, 1)
|
|
2381
|
+
progress_bar_found = True
|
|
2382
|
+
break
|
|
2383
|
+
|
|
2384
|
+
# 如果未找到进度条,尝试点击播放区域显示控制栏
|
|
2385
|
+
if not progress_bar_found:
|
|
2386
|
+
# 点击屏幕中心显示控制栏
|
|
2387
|
+
center_x, center_y = screen_width // 2, screen_height // 2
|
|
2388
|
+
self.client.u2.click(center_x, center_y)
|
|
2389
|
+
time.sleep(0.5)
|
|
2390
|
+
|
|
2391
|
+
# 再次查找进度条
|
|
2392
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
2393
|
+
root = ET.fromstring(xml_string)
|
|
2394
|
+
|
|
2395
|
+
for elem in root.iter():
|
|
2396
|
+
class_name = elem.attrib.get('class', '')
|
|
2397
|
+
resource_id = elem.attrib.get('resource-id', '')
|
|
2398
|
+
bounds_str = elem.attrib.get('bounds', '')
|
|
2399
|
+
|
|
2400
|
+
is_progress_bar = (
|
|
2401
|
+
'SeekBar' in class_name or
|
|
2402
|
+
'ProgressBar' in class_name or
|
|
2403
|
+
'progress' in resource_id.lower() or
|
|
2404
|
+
'seek' in resource_id.lower()
|
|
2405
|
+
)
|
|
2406
|
+
|
|
2407
|
+
if is_progress_bar and bounds_str:
|
|
2408
|
+
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
|
|
2409
|
+
if match:
|
|
2410
|
+
x1, y1, x2, y2 = map(int, match.groups())
|
|
2411
|
+
center_y = (y1 + y2) // 2
|
|
2412
|
+
progress_bar_y = center_y
|
|
2413
|
+
progress_bar_y_percent = round(center_y / screen_height * 100, 1)
|
|
2414
|
+
progress_bar_found = True
|
|
2415
|
+
break
|
|
2416
|
+
|
|
2417
|
+
# 确定使用的高度位置
|
|
2418
|
+
if y_percent is not None:
|
|
2419
|
+
swipe_y = int(screen_height * y_percent / 100)
|
|
2420
|
+
used_y_percent = y_percent
|
|
2421
|
+
elif y is not None:
|
|
2422
|
+
swipe_y = y
|
|
2423
|
+
used_y_percent = round(y / screen_height * 100, 1)
|
|
2424
|
+
elif progress_bar_found:
|
|
2425
|
+
swipe_y = progress_bar_y
|
|
2426
|
+
used_y_percent = progress_bar_y_percent
|
|
2427
|
+
else:
|
|
2428
|
+
# 默认使用屏幕底部附近(进度条常见位置)
|
|
2429
|
+
swipe_y = int(screen_height * 0.91)
|
|
2430
|
+
used_y_percent = 91.0
|
|
2431
|
+
|
|
2432
|
+
# 计算滑动距离
|
|
2433
|
+
swipe_distance = int(screen_width * distance_percent / 100)
|
|
2434
|
+
|
|
2435
|
+
# 计算起始和结束位置
|
|
2436
|
+
center_x = screen_width // 2
|
|
2437
|
+
if direction == 'left':
|
|
2438
|
+
start_x = min(center_x + swipe_distance // 2, screen_width - 10)
|
|
2439
|
+
end_x = start_x - swipe_distance
|
|
2440
|
+
if end_x < 10:
|
|
2441
|
+
end_x = 10
|
|
2442
|
+
start_x = min(end_x + swipe_distance, screen_width - 10)
|
|
2443
|
+
else: # right
|
|
2444
|
+
start_x = max(center_x - swipe_distance // 2, 10)
|
|
2445
|
+
end_x = start_x + swipe_distance
|
|
2446
|
+
if end_x > screen_width - 10:
|
|
2447
|
+
end_x = screen_width - 10
|
|
2448
|
+
start_x = max(end_x - swipe_distance, 10)
|
|
2449
|
+
|
|
2450
|
+
# 执行拖动
|
|
2451
|
+
self.client.u2.swipe(start_x, swipe_y, end_x, swipe_y, duration=0.5)
|
|
2452
|
+
time.sleep(0.3)
|
|
2453
|
+
|
|
2454
|
+
# 记录操作
|
|
2455
|
+
self._record_swipe(direction)
|
|
2456
|
+
|
|
2457
|
+
# 检查应用是否跳转
|
|
2458
|
+
app_check = self._check_app_switched()
|
|
2459
|
+
return_result = None
|
|
2460
|
+
if app_check['switched']:
|
|
2461
|
+
return_result = self._return_to_target_app()
|
|
2462
|
+
|
|
2463
|
+
# 构建返回消息
|
|
2464
|
+
msg = f"✅ 进度条拖动成功: {direction} (高度: {used_y_percent}%, 距离: {distance_percent}%)"
|
|
2465
|
+
if not progress_bar_found:
|
|
2466
|
+
msg += "\n💡 已自动点击播放区域显示控制栏"
|
|
2467
|
+
else:
|
|
2468
|
+
msg += "\n💡 进度条已显示,直接拖动"
|
|
2469
|
+
|
|
2470
|
+
if app_check['switched']:
|
|
2471
|
+
msg += f"\n{app_check['message']}"
|
|
2472
|
+
if return_result and return_result.get('success'):
|
|
2473
|
+
msg += f"\n{return_result['message']}"
|
|
2474
|
+
|
|
2475
|
+
return {
|
|
2476
|
+
"success": True,
|
|
2477
|
+
"message": msg,
|
|
2478
|
+
"progress_bar_found": progress_bar_found,
|
|
2479
|
+
"y_percent": used_y_percent,
|
|
2480
|
+
"distance_percent": distance_percent,
|
|
2481
|
+
"direction": direction,
|
|
2482
|
+
"app_check": app_check,
|
|
2483
|
+
"return_to_app": return_result
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
except Exception as e:
|
|
2487
|
+
return {"success": False, "message": f"❌ 拖动进度条失败: {e}"}
|
|
2488
|
+
|
|
2209
2489
|
# ==================== 应用管理 ====================
|
|
2210
2490
|
|
|
2211
2491
|
async def launch_app(self, package_name: str) -> Dict:
|
|
@@ -2369,6 +2649,26 @@ class BasicMobileToolsLite:
|
|
|
2369
2649
|
'carrier', 'operator', 'sim_', 'mobile_signal'
|
|
2370
2650
|
}
|
|
2371
2651
|
|
|
2652
|
+
# 系统控件关键词(厂商系统UI元素,对测试没有意义,直接过滤)
|
|
2653
|
+
SYSTEM_WIDGET_KEYWORDS = {
|
|
2654
|
+
'system_icon', 'systemicon', 'system_image', 'systemimage',
|
|
2655
|
+
'vivo_', 'vivo_superx', 'superx', 'super_x',
|
|
2656
|
+
'miui_', 'miui_system', 'huawei_', 'emui_',
|
|
2657
|
+
'oppo_', 'coloros_', 'oneplus_', 'realme_',
|
|
2658
|
+
'samsung_', 'oneui_', 'com.android.systemui',
|
|
2659
|
+
'system_ui', 'systemui', 'navigation_bar', 'navigationbar'
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
# 系统弹窗交互文本(如果元素包含这些文本,即使 resource_id 匹配系统控件,也不过滤)
|
|
2663
|
+
# 这些是系统弹窗(权限请求、系统对话框等)的常见按钮文本
|
|
2664
|
+
SYSTEM_DIALOG_INTERACTIVE_TEXTS = {
|
|
2665
|
+
'允许', '拒绝', '确定', '取消', '同意', '不同意',
|
|
2666
|
+
'允许访问', '拒绝访问', '始终允许', '仅在使用时允许',
|
|
2667
|
+
'确定', '取消', '是', '否', '好', '知道了',
|
|
2668
|
+
'Allow', 'Deny', 'OK', 'Cancel', 'Yes', 'No',
|
|
2669
|
+
'Accept', 'Reject', 'Grant', 'Deny'
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2372
2672
|
# Token 优化:构建精简元素(只返回非空字段)
|
|
2373
2673
|
def build_compact_element(resource_id, text, content_desc, bounds, likely_click, class_name):
|
|
2374
2674
|
"""只返回有值的字段,节省 token"""
|
|
@@ -2412,6 +2712,41 @@ class BasicMobileToolsLite:
|
|
|
2412
2712
|
if any(keyword in resource_id_lower for keyword in STATUS_BAR_KEYWORDS):
|
|
2413
2713
|
continue
|
|
2414
2714
|
|
|
2715
|
+
# 1.6 过滤系统控件(厂商系统UI元素,对测试没有意义)
|
|
2716
|
+
# 例外:如果元素有明确的交互文本(系统弹窗按钮),不过滤
|
|
2717
|
+
if resource_id:
|
|
2718
|
+
resource_id_lower = resource_id.lower()
|
|
2719
|
+
|
|
2720
|
+
# 检查是否是系统弹窗的交互按钮(有明确的交互文本)
|
|
2721
|
+
is_system_dialog_button = (
|
|
2722
|
+
text in SYSTEM_DIALOG_INTERACTIVE_TEXTS or
|
|
2723
|
+
content_desc in SYSTEM_DIALOG_INTERACTIVE_TEXTS
|
|
2724
|
+
)
|
|
2725
|
+
|
|
2726
|
+
# 特殊处理:android:id/ 开头的元素
|
|
2727
|
+
if 'android:id/' in resource_id_lower:
|
|
2728
|
+
# android:id/button1, android:id/button2 等是系统弹窗按钮,应该保留
|
|
2729
|
+
# 只过滤特定的系统UI容器元素
|
|
2730
|
+
android_system_ids_to_filter = [
|
|
2731
|
+
'android:id/statusbarbackground',
|
|
2732
|
+
'android:id/navigationbarbackground'
|
|
2733
|
+
]
|
|
2734
|
+
# 如果是系统弹窗按钮(有交互文本)或者是按钮类ID,保留
|
|
2735
|
+
if (is_system_dialog_button or
|
|
2736
|
+
'button' in resource_id_lower or
|
|
2737
|
+
resource_id_lower not in [id.lower() for id in android_system_ids_to_filter]):
|
|
2738
|
+
# 保留,不过滤
|
|
2739
|
+
pass
|
|
2740
|
+
else:
|
|
2741
|
+
# 过滤系统UI容器
|
|
2742
|
+
continue
|
|
2743
|
+
else:
|
|
2744
|
+
# 非 android:id/ 开头的元素,检查是否匹配系统控件关键词
|
|
2745
|
+
# 如果是系统弹窗按钮(有交互文本),不过滤
|
|
2746
|
+
if not is_system_dialog_button:
|
|
2747
|
+
if any(keyword in resource_id_lower for keyword in SYSTEM_WIDGET_KEYWORDS):
|
|
2748
|
+
continue
|
|
2749
|
+
|
|
2415
2750
|
# 2. 检查是否是功能控件(直接保留)
|
|
2416
2751
|
if class_name in FUNCTIONAL_WIDGETS:
|
|
2417
2752
|
# 使用启发式判断可点击性(替代不准确的 clickable 属性)
|
|
@@ -2754,7 +3089,7 @@ class BasicMobileToolsLite:
|
|
|
2754
3089
|
except Exception as e:
|
|
2755
3090
|
return {"success": False, "msg": str(e)}
|
|
2756
3091
|
|
|
2757
|
-
def close_popup(self) -> Dict:
|
|
3092
|
+
def close_popup(self, popup_detected: bool = None, popup_bounds: tuple = None) -> Dict:
|
|
2758
3093
|
"""智能关闭弹窗(改进版)
|
|
2759
3094
|
|
|
2760
3095
|
核心改进:先检测弹窗区域,再在弹窗范围内查找关闭按钮
|
|
@@ -2768,6 +3103,10 @@ class BasicMobileToolsLite:
|
|
|
2768
3103
|
适配策略:
|
|
2769
3104
|
- X 按钮可能在任意位置(上下左右都支持)
|
|
2770
3105
|
- 使用百分比坐标记录,跨分辨率兼容
|
|
3106
|
+
|
|
3107
|
+
Args:
|
|
3108
|
+
popup_detected: 可选,AI已识别到弹窗时为True,跳过弹窗检测
|
|
3109
|
+
popup_bounds: 可选,弹窗边界 (x1, y1, x2, y2),如果AI已识别到弹窗区域可传入
|
|
2771
3110
|
"""
|
|
2772
3111
|
try:
|
|
2773
3112
|
import re
|
|
@@ -2788,25 +3127,40 @@ class BasicMobileToolsLite:
|
|
|
2788
3127
|
close_desc_keywords = ['关闭', 'close', 'dismiss', 'cancel', '跳过']
|
|
2789
3128
|
|
|
2790
3129
|
close_candidates = []
|
|
2791
|
-
|
|
3130
|
+
all_clickable_elements = [] # 所有可点击元素(用于兜底策略)
|
|
3131
|
+
popup_confidence = 0.0
|
|
2792
3132
|
|
|
2793
3133
|
# 解析 XML
|
|
2794
3134
|
try:
|
|
2795
3135
|
root = ET.fromstring(xml_string)
|
|
2796
3136
|
all_elements = list(root.iter())
|
|
2797
3137
|
|
|
2798
|
-
# =====
|
|
2799
|
-
popup_bounds
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3138
|
+
# ===== 第一步:检测弹窗区域(如果AI未传入完整弹窗信息)=====
|
|
3139
|
+
if popup_bounds is None:
|
|
3140
|
+
# 无论popup_detected是否传入,都需要检测bounds来定位弹窗区域
|
|
3141
|
+
detected_bounds, detected_confidence = self._detect_popup_with_confidence(
|
|
3142
|
+
root, screen_width, screen_height
|
|
3143
|
+
)
|
|
3144
|
+
popup_bounds = detected_bounds
|
|
3145
|
+
popup_confidence = detected_confidence
|
|
3146
|
+
|
|
3147
|
+
# 如果AI未传入popup_detected,根据检测结果判断
|
|
3148
|
+
if popup_detected is None:
|
|
3149
|
+
popup_detected = popup_bounds is not None and popup_confidence >= 0.6
|
|
3150
|
+
# 如果AI传入了popup_detected=True,但检测不到bounds,仍然使用AI的判断
|
|
3151
|
+
elif popup_detected and popup_bounds is None:
|
|
3152
|
+
# AI说有问题但检测不到,可能是检测算法不够准确,信任AI的判断
|
|
3153
|
+
popup_detected = True
|
|
3154
|
+
popup_confidence = 0.7 # 降低置信度,因为检测不到bounds
|
|
3155
|
+
else:
|
|
3156
|
+
# AI已传入popup_bounds,直接使用
|
|
3157
|
+
if popup_detected is None:
|
|
3158
|
+
# 有bounds就认为有弹窗
|
|
3159
|
+
popup_detected = True
|
|
3160
|
+
popup_confidence = 0.8 # AI识别到的弹窗,置信度较高
|
|
2805
3161
|
|
|
2806
|
-
#
|
|
2807
|
-
#
|
|
2808
|
-
if not popup_detected:
|
|
2809
|
-
return {"success": True, "popup": False}
|
|
3162
|
+
# 【重要修复】如果没有检测到弹窗区域,只搜索有明确关闭特征的元素(文本、resource-id等)
|
|
3163
|
+
# 避免误点击普通页面的右上角图标
|
|
2810
3164
|
|
|
2811
3165
|
# ===== 第二步:在弹窗范围内查找关闭按钮 =====
|
|
2812
3166
|
for idx, elem in enumerate(all_elements):
|
|
@@ -2815,6 +3169,7 @@ class BasicMobileToolsLite:
|
|
|
2815
3169
|
bounds_str = elem.attrib.get('bounds', '')
|
|
2816
3170
|
class_name = elem.attrib.get('class', '')
|
|
2817
3171
|
clickable = elem.attrib.get('clickable', 'false') == 'true'
|
|
3172
|
+
resource_id = elem.attrib.get('resource-id', '')
|
|
2818
3173
|
|
|
2819
3174
|
if not bounds_str:
|
|
2820
3175
|
continue
|
|
@@ -2830,8 +3185,22 @@ class BasicMobileToolsLite:
|
|
|
2830
3185
|
center_x = (x1 + x2) // 2
|
|
2831
3186
|
center_y = (y1 + y2) // 2
|
|
2832
3187
|
|
|
3188
|
+
# 收集所有可点击元素(用于兜底策略:当只有一个可点击元素时点击它)
|
|
3189
|
+
if clickable:
|
|
3190
|
+
all_clickable_elements.append({
|
|
3191
|
+
'bounds': bounds_str,
|
|
3192
|
+
'center_x': center_x,
|
|
3193
|
+
'center_y': center_y,
|
|
3194
|
+
'width': width,
|
|
3195
|
+
'height': height,
|
|
3196
|
+
'text': text,
|
|
3197
|
+
'content_desc': content_desc,
|
|
3198
|
+
'resource_id': resource_id,
|
|
3199
|
+
'class_name': class_name
|
|
3200
|
+
})
|
|
3201
|
+
|
|
2833
3202
|
# 如果检测到弹窗区域,检查元素是否在弹窗范围内或附近
|
|
2834
|
-
in_popup =
|
|
3203
|
+
in_popup = False
|
|
2835
3204
|
popup_edge_bonus = 0
|
|
2836
3205
|
is_floating_close = False # 是否是浮动关闭按钮(在弹窗外部上方)
|
|
2837
3206
|
if popup_bounds:
|
|
@@ -2872,6 +3241,20 @@ class BasicMobileToolsLite:
|
|
|
2872
3241
|
# 浮动关闭按钮(在弹窗上方外侧)给予高额加分
|
|
2873
3242
|
if is_floating_close:
|
|
2874
3243
|
popup_edge_bonus += 5.0 # 大幅加分
|
|
3244
|
+
elif not popup_detected:
|
|
3245
|
+
# 没有检测到弹窗时,只处理有明确关闭特征的元素
|
|
3246
|
+
# 检查是否有明确的关闭特征(文本、resource-id、content-desc)
|
|
3247
|
+
has_explicit_close_feature = (
|
|
3248
|
+
text in close_texts or
|
|
3249
|
+
any(kw in content_desc.lower() for kw in close_desc_keywords) or
|
|
3250
|
+
'close' in resource_id.lower() or
|
|
3251
|
+
'dismiss' in resource_id.lower() or
|
|
3252
|
+
'cancel' in resource_id.lower()
|
|
3253
|
+
)
|
|
3254
|
+
if not has_explicit_close_feature:
|
|
3255
|
+
continue # 没有明确关闭特征,跳过
|
|
3256
|
+
# 有明确关闭特征时,允许处理
|
|
3257
|
+
in_popup = True
|
|
2875
3258
|
|
|
2876
3259
|
if not in_popup:
|
|
2877
3260
|
continue
|
|
@@ -2945,6 +3328,33 @@ class BasicMobileToolsLite:
|
|
|
2945
3328
|
pass
|
|
2946
3329
|
|
|
2947
3330
|
if not close_candidates:
|
|
3331
|
+
# 兜底策略:如果检测到弹窗但未找到关闭按钮,且页面元素很少(只有1个可点击元素),直接点击它
|
|
3332
|
+
if popup_detected and popup_bounds and len(all_clickable_elements) == 1:
|
|
3333
|
+
single_element = all_clickable_elements[0]
|
|
3334
|
+
self.client.u2.click(single_element['center_x'], single_element['center_y'])
|
|
3335
|
+
time.sleep(0.5)
|
|
3336
|
+
|
|
3337
|
+
# 检查应用是否跳转
|
|
3338
|
+
app_check = self._check_app_switched()
|
|
3339
|
+
return_result = None
|
|
3340
|
+
if app_check['switched']:
|
|
3341
|
+
return_result = self._return_to_target_app()
|
|
3342
|
+
|
|
3343
|
+
# 记录操作
|
|
3344
|
+
rel_x = single_element['center_x'] / screen_width
|
|
3345
|
+
rel_y = single_element['center_y'] / screen_height
|
|
3346
|
+
self._record_click('percent', f"{round(rel_x * 100, 1)}%,{round(rel_y * 100, 1)}%",
|
|
3347
|
+
round(rel_x * 100, 1), round(rel_y * 100, 1),
|
|
3348
|
+
element_desc="唯一可点击元素(弹窗兜底)")
|
|
3349
|
+
|
|
3350
|
+
result = {"success": True, "clicked": True, "method": "single_clickable_fallback"}
|
|
3351
|
+
if app_check['switched']:
|
|
3352
|
+
result["switched"] = True
|
|
3353
|
+
if return_result:
|
|
3354
|
+
result["returned"] = return_result['success']
|
|
3355
|
+
return result
|
|
3356
|
+
|
|
3357
|
+
# 如果没有找到关闭按钮,且不满足兜底条件,返回fallback
|
|
2948
3358
|
if popup_detected and popup_bounds:
|
|
2949
3359
|
return {"success": False, "fallback": "vision", "popup": True}
|
|
2950
3360
|
return {"success": True, "popup": False}
|
|
@@ -3062,6 +3472,15 @@ class BasicMobileToolsLite:
|
|
|
3062
3472
|
resource_id = elem.attrib.get('resource-id', '')
|
|
3063
3473
|
clickable = elem.attrib.get('clickable', 'false') == 'true'
|
|
3064
3474
|
|
|
3475
|
+
# 检查是否是关闭按钮
|
|
3476
|
+
is_close_button = (
|
|
3477
|
+
'close' in resource_id.lower() or
|
|
3478
|
+
'dismiss' in resource_id.lower() or
|
|
3479
|
+
'cancel' in resource_id.lower() or
|
|
3480
|
+
'×' in elem.attrib.get('text', '') or
|
|
3481
|
+
'X' in elem.attrib.get('text', '')
|
|
3482
|
+
)
|
|
3483
|
+
|
|
3065
3484
|
all_elements.append({
|
|
3066
3485
|
'idx': idx,
|
|
3067
3486
|
'bounds': (x1, y1, x2, y2),
|
|
@@ -3074,6 +3493,7 @@ class BasicMobileToolsLite:
|
|
|
3074
3493
|
'clickable': clickable,
|
|
3075
3494
|
'center_x': (x1 + x2) // 2,
|
|
3076
3495
|
'center_y': (y1 + y2) // 2,
|
|
3496
|
+
'is_close_button': is_close_button,
|
|
3077
3497
|
})
|
|
3078
3498
|
|
|
3079
3499
|
if not all_elements:
|
|
@@ -3082,6 +3502,8 @@ class BasicMobileToolsLite:
|
|
|
3082
3502
|
# 弹窗检测关键词
|
|
3083
3503
|
dialog_class_keywords = ['Dialog', 'Popup', 'Alert', 'Modal', 'BottomSheet', 'PopupWindow']
|
|
3084
3504
|
dialog_id_keywords = ['dialog', 'popup', 'alert', 'modal', 'bottom_sheet', 'overlay', 'mask']
|
|
3505
|
+
# 广告弹窗关键词(全屏广告、激励视频等)
|
|
3506
|
+
ad_popup_keywords = ['ad_close', 'ad_button', 'full_screen', 'interstitial', 'reward', 'close_icon', 'close_btn']
|
|
3085
3507
|
|
|
3086
3508
|
popup_candidates = []
|
|
3087
3509
|
has_mask_layer = False
|
|
@@ -3112,6 +3534,59 @@ class BasicMobileToolsLite:
|
|
|
3112
3534
|
if y1 < 50:
|
|
3113
3535
|
continue
|
|
3114
3536
|
|
|
3537
|
+
# 【非弹窗特征】如果元素包含底部导航栏(底部tab),则不是弹窗
|
|
3538
|
+
# 底部导航栏通常在屏幕底部,高度约100-200像素
|
|
3539
|
+
if y2 > screen_height * 0.85:
|
|
3540
|
+
# 检查是否包含tab相关的resource-id或class
|
|
3541
|
+
if 'tab' in resource_id.lower() or 'Tab' in class_name or 'navigation' in resource_id.lower():
|
|
3542
|
+
continue # 跳过底部导航栏
|
|
3543
|
+
|
|
3544
|
+
# 【非弹窗特征】如果元素包含顶部搜索栏,则不是弹窗
|
|
3545
|
+
if y1 < screen_height * 0.15:
|
|
3546
|
+
if 'search' in resource_id.lower() or 'Search' in class_name:
|
|
3547
|
+
continue # 跳过顶部搜索栏
|
|
3548
|
+
|
|
3549
|
+
# 先检查是否有强弹窗特征(用于后续判断)
|
|
3550
|
+
has_strong_popup_feature = (
|
|
3551
|
+
any(kw in class_name for kw in dialog_class_keywords) or
|
|
3552
|
+
any(kw in resource_id.lower() for kw in dialog_id_keywords) or
|
|
3553
|
+
any(kw in resource_id.lower() for kw in ad_popup_keywords) # 广告弹窗关键词
|
|
3554
|
+
)
|
|
3555
|
+
|
|
3556
|
+
# 检查是否有子元素是关闭按钮(作为弹窗特征)
|
|
3557
|
+
has_close_button_child = False
|
|
3558
|
+
elem_bounds = elem['bounds']
|
|
3559
|
+
for other_elem in all_elements:
|
|
3560
|
+
if other_elem['idx'] == elem['idx']:
|
|
3561
|
+
continue
|
|
3562
|
+
if other_elem['is_close_button']:
|
|
3563
|
+
# 检查关闭按钮是否在这个元素范围内
|
|
3564
|
+
ox1, oy1, ox2, oy2 = other_elem['bounds']
|
|
3565
|
+
ex1, ey1, ex2, ey2 = elem_bounds
|
|
3566
|
+
if ex1 <= ox1 and ey1 <= oy1 and ex2 >= ox2 and ey2 >= oy2:
|
|
3567
|
+
has_close_button_child = True
|
|
3568
|
+
break
|
|
3569
|
+
|
|
3570
|
+
# 【非弹窗特征】如果元素包含明显的页面内容特征,则不是弹窗
|
|
3571
|
+
# 检查是否包含视频播放器、内容列表等页面元素
|
|
3572
|
+
page_content_keywords = ['video', 'player', 'recycler', 'list', 'scroll', 'viewpager', 'fragment']
|
|
3573
|
+
if any(kw in resource_id.lower() or kw in class_name.lower() for kw in page_content_keywords):
|
|
3574
|
+
# 如果面积很大且没有强弹窗特征,则不是弹窗
|
|
3575
|
+
if area_ratio > 0.6 and not has_strong_popup_feature:
|
|
3576
|
+
continue
|
|
3577
|
+
|
|
3578
|
+
# 【非弹窗特征】如果元素面积过大(接近全屏),即使居中也不应该是弹窗
|
|
3579
|
+
# 真正的弹窗通常不会超过屏幕的60%
|
|
3580
|
+
# 对于面积 > 0.6 的元素,如果没有强特征,直接跳过(避免误判首页内容区域)
|
|
3581
|
+
if area_ratio > 0.6 and not has_strong_popup_feature:
|
|
3582
|
+
continue # 跳过大面积非弹窗元素(接近全屏的内容区域,如首页视频播放区域)
|
|
3583
|
+
|
|
3584
|
+
# 对于面积 > 0.7 的元素,即使有强特征也要更严格
|
|
3585
|
+
if area_ratio > 0.7:
|
|
3586
|
+
# 需要非常强的特征才认为是弹窗
|
|
3587
|
+
if not has_strong_popup_feature:
|
|
3588
|
+
continue
|
|
3589
|
+
|
|
3115
3590
|
confidence = 0.0
|
|
3116
3591
|
|
|
3117
3592
|
# 【强特征】class 名称包含弹窗关键词 (+0.5)
|
|
@@ -3122,19 +3597,46 @@ class BasicMobileToolsLite:
|
|
|
3122
3597
|
if any(kw in resource_id.lower() for kw in dialog_id_keywords):
|
|
3123
3598
|
confidence += 0.4
|
|
3124
3599
|
|
|
3600
|
+
# 【强特征】resource-id 包含广告弹窗关键词 (+0.4)
|
|
3601
|
+
if any(kw in resource_id.lower() for kw in ad_popup_keywords):
|
|
3602
|
+
confidence += 0.4
|
|
3603
|
+
|
|
3604
|
+
# 【强特征】包含关闭按钮作为子元素 (+0.3)
|
|
3605
|
+
if has_close_button_child:
|
|
3606
|
+
confidence += 0.3
|
|
3607
|
+
|
|
3125
3608
|
# 【中等特征】居中显示 (+0.2)
|
|
3609
|
+
# 但如果没有强特征,降低权重
|
|
3126
3610
|
center_x = elem['center_x']
|
|
3127
3611
|
center_y = elem['center_y']
|
|
3128
3612
|
is_centered_x = abs(center_x - screen_width / 2) < screen_width * 0.15
|
|
3129
3613
|
is_centered_y = abs(center_y - screen_height / 2) < screen_height * 0.25
|
|
3614
|
+
|
|
3615
|
+
has_strong_feature = (
|
|
3616
|
+
any(kw in class_name for kw in dialog_class_keywords) or
|
|
3617
|
+
any(kw in resource_id.lower() for kw in dialog_id_keywords) or
|
|
3618
|
+
any(kw in resource_id.lower() for kw in ad_popup_keywords) or
|
|
3619
|
+
has_close_button_child
|
|
3620
|
+
)
|
|
3621
|
+
|
|
3130
3622
|
if is_centered_x and is_centered_y:
|
|
3131
|
-
|
|
3623
|
+
if has_strong_feature:
|
|
3624
|
+
confidence += 0.2
|
|
3625
|
+
else:
|
|
3626
|
+
confidence += 0.1 # 没有强特征时降低权重
|
|
3132
3627
|
elif is_centered_x:
|
|
3133
|
-
|
|
3628
|
+
if has_strong_feature:
|
|
3629
|
+
confidence += 0.1
|
|
3630
|
+
else:
|
|
3631
|
+
confidence += 0.05 # 没有强特征时降低权重
|
|
3134
3632
|
|
|
3135
3633
|
# 【中等特征】非全屏但有一定大小 (+0.15)
|
|
3634
|
+
# 但如果没有强特征,降低权重
|
|
3136
3635
|
if 0.15 < area_ratio < 0.75:
|
|
3137
|
-
|
|
3636
|
+
if has_strong_feature:
|
|
3637
|
+
confidence += 0.15
|
|
3638
|
+
else:
|
|
3639
|
+
confidence += 0.08 # 没有强特征时降低权重
|
|
3138
3640
|
|
|
3139
3641
|
# 【弱特征】XML 顺序靠后(在视图层级上层)(+0.1)
|
|
3140
3642
|
if elem['idx'] > len(all_elements) * 0.5:
|
|
@@ -3161,8 +3663,22 @@ class BasicMobileToolsLite:
|
|
|
3161
3663
|
popup_candidates.sort(key=lambda x: (x['confidence'], x['idx']), reverse=True)
|
|
3162
3664
|
best = popup_candidates[0]
|
|
3163
3665
|
|
|
3164
|
-
#
|
|
3165
|
-
|
|
3666
|
+
# 更严格的阈值:只有置信度 >= 0.7 才返回弹窗
|
|
3667
|
+
# 如果没有强特征(class或resource-id包含弹窗关键词),需要更高的置信度
|
|
3668
|
+
has_strong_feature = (
|
|
3669
|
+
any(kw in best['class'] for kw in dialog_class_keywords) or
|
|
3670
|
+
any(kw in best['resource_id'].lower() for kw in dialog_id_keywords) or
|
|
3671
|
+
any(kw in best['resource_id'].lower() for kw in ad_popup_keywords)
|
|
3672
|
+
)
|
|
3673
|
+
|
|
3674
|
+
if has_strong_feature:
|
|
3675
|
+
# 有强特征时,阈值0.7
|
|
3676
|
+
threshold = 0.7
|
|
3677
|
+
else:
|
|
3678
|
+
# 没有强特征时,阈值0.85(更严格)
|
|
3679
|
+
threshold = 0.85
|
|
3680
|
+
|
|
3681
|
+
if best['confidence'] >= threshold:
|
|
3166
3682
|
return best['bounds'], best['confidence']
|
|
3167
3683
|
|
|
3168
3684
|
return None, best['confidence']
|
|
@@ -204,115 +204,6 @@ class MobileMCPServer:
|
|
|
204
204
|
|
|
205
205
|
return "android"
|
|
206
206
|
|
|
207
|
-
def _open_new_chat(self, message: str = "继续执行飞书用例", delay: float = 5) -> dict:
|
|
208
|
-
"""
|
|
209
|
-
打开新Chat窗口并发送消息
|
|
210
|
-
|
|
211
|
-
原理:用后台线程延迟执行键盘操作,避免打断当前AI响应
|
|
212
|
-
跨平台统一用 pyautogui + pyperclip,配合系统API激活窗口
|
|
213
|
-
"""
|
|
214
|
-
import threading
|
|
215
|
-
import platform
|
|
216
|
-
|
|
217
|
-
def delayed_action():
|
|
218
|
-
import time
|
|
219
|
-
time.sleep(delay) # 等待当前响应结束
|
|
220
|
-
|
|
221
|
-
try:
|
|
222
|
-
system = platform.system()
|
|
223
|
-
|
|
224
|
-
if system == "Darwin": # ========== macOS: 纯 AppleScript(无需额外安装)==========
|
|
225
|
-
import subprocess
|
|
226
|
-
|
|
227
|
-
# AppleScript: 激活Cursor → Cmd+L聚焦Chat → Cmd+N新建 → 粘贴 → 回车
|
|
228
|
-
script = f'''
|
|
229
|
-
-- 设置剪贴板(支持中文)
|
|
230
|
-
set the clipboard to "{message}"
|
|
231
|
-
|
|
232
|
-
-- 激活 Cursor
|
|
233
|
-
tell application "Cursor" to activate
|
|
234
|
-
delay 0.5
|
|
235
|
-
|
|
236
|
-
tell application "System Events"
|
|
237
|
-
tell process "Cursor"
|
|
238
|
-
-- Cmd+L 聚焦 Chat
|
|
239
|
-
keystroke "l" using command down
|
|
240
|
-
delay 0.5
|
|
241
|
-
-- Cmd+N 新建对话
|
|
242
|
-
keystroke "n" using command down
|
|
243
|
-
delay 1
|
|
244
|
-
-- Cmd+V 粘贴
|
|
245
|
-
keystroke "v" using command down
|
|
246
|
-
delay 0.3
|
|
247
|
-
-- Enter 发送
|
|
248
|
-
key code 36
|
|
249
|
-
end tell
|
|
250
|
-
end tell
|
|
251
|
-
'''
|
|
252
|
-
result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True)
|
|
253
|
-
if result.returncode == 0:
|
|
254
|
-
print(f"[open_new_chat] macOS成功: {message}", file=sys.stderr)
|
|
255
|
-
else:
|
|
256
|
-
print(f"[open_new_chat] AppleScript错误: {result.stderr}", file=sys.stderr)
|
|
257
|
-
|
|
258
|
-
elif system == "Windows": # ========== Windows: 需要 pip install pyautogui pyperclip ==========
|
|
259
|
-
import pyautogui
|
|
260
|
-
import pyperclip
|
|
261
|
-
|
|
262
|
-
# 尝试激活 Cursor 窗口
|
|
263
|
-
try:
|
|
264
|
-
import pygetwindow as gw
|
|
265
|
-
windows = gw.getWindowsWithTitle('Cursor')
|
|
266
|
-
if windows:
|
|
267
|
-
windows[0].activate()
|
|
268
|
-
time.sleep(0.5)
|
|
269
|
-
except ImportError:
|
|
270
|
-
pass
|
|
271
|
-
|
|
272
|
-
pyautogui.PAUSE = 0.3
|
|
273
|
-
|
|
274
|
-
# Ctrl+L 聚焦 Chat → Ctrl+N 新建
|
|
275
|
-
pyautogui.hotkey('ctrl', 'l')
|
|
276
|
-
time.sleep(0.5)
|
|
277
|
-
pyautogui.hotkey('ctrl', 'n')
|
|
278
|
-
time.sleep(1)
|
|
279
|
-
|
|
280
|
-
# 粘贴消息
|
|
281
|
-
pyperclip.copy(message)
|
|
282
|
-
pyautogui.hotkey('ctrl', 'v')
|
|
283
|
-
time.sleep(0.3)
|
|
284
|
-
pyautogui.press('enter')
|
|
285
|
-
|
|
286
|
-
print(f"[open_new_chat] Windows成功: {message}", file=sys.stderr)
|
|
287
|
-
|
|
288
|
-
else: # Linux: 写信号文件
|
|
289
|
-
from pathlib import Path
|
|
290
|
-
signal_file = Path(__file__).parent.parent / ".new_chat_signal"
|
|
291
|
-
signal_file.write_text(message)
|
|
292
|
-
print(f"[open_new_chat] Linux: 已写入信号文件", file=sys.stderr)
|
|
293
|
-
|
|
294
|
-
except ImportError as e:
|
|
295
|
-
# Windows 缺少依赖时写信号文件
|
|
296
|
-
from pathlib import Path
|
|
297
|
-
signal_file = Path(__file__).parent.parent / ".new_chat_signal"
|
|
298
|
-
signal_file.write_text(message)
|
|
299
|
-
print(f"[open_new_chat] Windows缺少依赖,请运行: pip install pyautogui pyperclip", file=sys.stderr)
|
|
300
|
-
|
|
301
|
-
except Exception as e:
|
|
302
|
-
# 写错误日志
|
|
303
|
-
import sys
|
|
304
|
-
print(f"[open_new_chat] 错误: {e}", file=sys.stderr)
|
|
305
|
-
|
|
306
|
-
# 后台线程执行,不阻塞MCP响应
|
|
307
|
-
thread = threading.Thread(target=delayed_action, daemon=True)
|
|
308
|
-
thread.start()
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
"success": True,
|
|
312
|
-
"message": f"⏰ {delay}秒后将打开新Chat并发送: {message}",
|
|
313
|
-
"tip": "请不要手动操作,等待自动执行"
|
|
314
|
-
}
|
|
315
|
-
|
|
316
207
|
def get_tools(self):
|
|
317
208
|
"""注册 MCP 工具"""
|
|
318
209
|
tools = []
|
|
@@ -603,18 +494,82 @@ class MobileMCPServer:
|
|
|
603
494
|
# ==================== 导航操作 ====================
|
|
604
495
|
tools.append(Tool(
|
|
605
496
|
name="mobile_swipe",
|
|
606
|
-
description="👆
|
|
497
|
+
description="👆 滑动屏幕。方向:up/down/left/right\n\n"
|
|
498
|
+
"🎯 适用场景:\n"
|
|
499
|
+
"- 滑动页面(列表、页面切换)\n"
|
|
500
|
+
"- 拖动进度条/滑块(SeekBar、ProgressBar)\n"
|
|
501
|
+
"- 滑动选择器(Picker、Slider)\n\n"
|
|
502
|
+
"💡 左右滑动时,可指定高度坐标或百分比:\n"
|
|
503
|
+
"- y: 指定高度坐标(像素)\n"
|
|
504
|
+
"- y_percent: 指定高度百分比 (0-100)\n"
|
|
505
|
+
"- 两者都未指定时,使用屏幕中心高度\n"
|
|
506
|
+
"- 📌 拖动进度条时,使用进度条的 Y 位置(百分比或像素)\n\n"
|
|
507
|
+
"💡 横向滑动(left/right)时,可指定滑动距离:\n"
|
|
508
|
+
"- distance: 滑动距离(像素)\n"
|
|
509
|
+
"- distance_percent: 滑动距离百分比 (0-100)\n"
|
|
510
|
+
"- 两者都未指定时,使用默认距离(屏幕宽度的 60%)\n"
|
|
511
|
+
"- 📌 拖动进度条时,distance_percent 控制拖动幅度\n\n"
|
|
512
|
+
"💡 拖动进度条示例:\n"
|
|
513
|
+
"- 倒退:direction='left', y_percent=91(进度条位置), distance_percent=30\n"
|
|
514
|
+
"- 前进:direction='right', y_percent=91, distance_percent=30\n\n"
|
|
515
|
+
"⚠️ **推荐使用 mobile_drag_progress_bar 拖动进度条**(自动检测进度条位置,无需手动指定)",
|
|
607
516
|
inputSchema={
|
|
608
517
|
"type": "object",
|
|
609
518
|
"properties": {
|
|
610
519
|
"direction": {"type": "string", "enum": ["up", "down", "left", "right"], "description": "方向"},
|
|
611
|
-
"y": {"type": "integer", "description": "
|
|
612
|
-
"y_percent": {"type": "number", "description": "
|
|
520
|
+
"y": {"type": "integer", "description": "左右滑动时指定的高度坐标(像素)"},
|
|
521
|
+
"y_percent": {"type": "number", "description": "左右滑动时指定的高度百分比 (0-100)"},
|
|
522
|
+
"distance": {"type": "integer", "description": "横向滑动时指定的滑动距离(像素),仅用于 left/right"},
|
|
523
|
+
"distance_percent": {"type": "number", "description": "横向滑动时指定的滑动距离百分比 (0-100),仅用于 left/right"}
|
|
613
524
|
},
|
|
614
525
|
"required": ["direction"]
|
|
615
526
|
}
|
|
616
527
|
))
|
|
617
528
|
|
|
529
|
+
tools.append(Tool(
|
|
530
|
+
name="mobile_drag_progress_bar",
|
|
531
|
+
description="🎯 智能拖动进度条(⭐⭐ 推荐用于拖动视频/音频进度条)\n\n"
|
|
532
|
+
"✅ **自动检测进度条是否可见**:\n"
|
|
533
|
+
"- 如果进度条已显示,直接拖动(无需先点击播放区域)\n"
|
|
534
|
+
"- 如果进度条未显示,自动点击播放区域显示控制栏,再拖动\n\n"
|
|
535
|
+
"🎯 优势:\n"
|
|
536
|
+
"- 自动检测进度条位置,无需手动指定 y_percent\n"
|
|
537
|
+
"- 智能判断是否需要显示控制栏\n"
|
|
538
|
+
"- 使用 swipe 拖动,更稳定可靠\n\n"
|
|
539
|
+
"💡 参数说明:\n"
|
|
540
|
+
"- direction: 'left'(倒退)或 'right'(前进),默认 'right'\n"
|
|
541
|
+
"- distance_percent: 拖动距离百分比 (0-100),默认 30%\n"
|
|
542
|
+
"- y_percent: 进度条位置(可选,未指定则自动检测)\n"
|
|
543
|
+
"- y: 进度条位置坐标(可选,未指定则自动检测)\n\n"
|
|
544
|
+
"📋 使用示例:\n"
|
|
545
|
+
"- 前进30%:mobile_drag_progress_bar(direction='right', distance_percent=30)\n"
|
|
546
|
+
"- 倒退30%:mobile_drag_progress_bar(direction='left', distance_percent=30)\n"
|
|
547
|
+
"- 前进到指定位置:先点击进度条位置,或使用 mobile_swipe",
|
|
548
|
+
inputSchema={
|
|
549
|
+
"type": "object",
|
|
550
|
+
"properties": {
|
|
551
|
+
"direction": {
|
|
552
|
+
"type": "string",
|
|
553
|
+
"enum": ["left", "right"],
|
|
554
|
+
"description": "拖动方向:'left'(倒退)或 'right'(前进),默认 'right'"
|
|
555
|
+
},
|
|
556
|
+
"distance_percent": {
|
|
557
|
+
"type": "number",
|
|
558
|
+
"description": "拖动距离百分比 (0-100),默认 30%"
|
|
559
|
+
},
|
|
560
|
+
"y_percent": {
|
|
561
|
+
"type": "number",
|
|
562
|
+
"description": "进度条的垂直位置百分比 (0-100),可选,未指定则自动检测"
|
|
563
|
+
},
|
|
564
|
+
"y": {
|
|
565
|
+
"type": "integer",
|
|
566
|
+
"description": "进度条的垂直位置坐标(像素),可选,未指定则自动检测"
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
"required": []
|
|
570
|
+
}
|
|
571
|
+
))
|
|
572
|
+
|
|
618
573
|
tools.append(Tool(
|
|
619
574
|
name="mobile_press_key",
|
|
620
575
|
description="⌨️ 按键:home/back/enter/search。",
|
|
@@ -730,6 +685,8 @@ class MobileMCPServer:
|
|
|
730
685
|
- 如果没有弹窗 → 直接返回"无弹窗",不执行任何操作
|
|
731
686
|
- 如果有弹窗 → 自动查找并点击关闭按钮
|
|
732
687
|
|
|
688
|
+
💡 【优化】如果已通过list_elements识别到弹窗,可传入popup_detected=true跳过重复检测
|
|
689
|
+
|
|
733
690
|
✅ 适用场景:
|
|
734
691
|
- 启动应用后检测并关闭可能出现的弹窗
|
|
735
692
|
- 页面跳转后检测并关闭弹窗
|
|
@@ -745,7 +702,21 @@ class MobileMCPServer:
|
|
|
745
702
|
tools.append(Tool(
|
|
746
703
|
name="mobile_close_popup",
|
|
747
704
|
description=desc_close_popup,
|
|
748
|
-
inputSchema={
|
|
705
|
+
inputSchema={
|
|
706
|
+
"type": "object",
|
|
707
|
+
"properties": {
|
|
708
|
+
"popup_detected": {
|
|
709
|
+
"type": "boolean",
|
|
710
|
+
"description": "可选,如果已通过list_elements识别到弹窗,传入true可跳过重复检测"
|
|
711
|
+
},
|
|
712
|
+
"popup_bounds": {
|
|
713
|
+
"type": "array",
|
|
714
|
+
"items": {"type": "number"},
|
|
715
|
+
"description": "可选,弹窗边界[x1, y1, x2, y2],如果已识别到弹窗区域可传入"
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
"required": []
|
|
719
|
+
}
|
|
749
720
|
))
|
|
750
721
|
|
|
751
722
|
tools.append(Tool(
|
|
@@ -896,20 +867,6 @@ class MobileMCPServer:
|
|
|
896
867
|
}
|
|
897
868
|
))
|
|
898
869
|
|
|
899
|
-
# ==================== Cursor自动化 ====================
|
|
900
|
-
tools.append(Tool(
|
|
901
|
-
name="mobile_open_new_chat",
|
|
902
|
-
description="🔄 打开新Chat继续执行。执行完10个用例后调用,会延迟5秒后按Cmd+T打开新会话并输入继续命令。",
|
|
903
|
-
inputSchema={
|
|
904
|
-
"type": "object",
|
|
905
|
-
"properties": {
|
|
906
|
-
"message": {"type": "string", "description": "新会话中要发送的消息", "default": "继续执行飞书用例"},
|
|
907
|
-
"delay": {"type": "number", "description": "延迟秒数(等待当前响应结束)", "default": 5}
|
|
908
|
-
},
|
|
909
|
-
"required": []
|
|
910
|
-
}
|
|
911
|
-
))
|
|
912
|
-
|
|
913
870
|
return tools
|
|
914
871
|
|
|
915
872
|
async def handle_tool_call(self, name: str, arguments: dict):
|
|
@@ -1047,7 +1004,18 @@ class MobileMCPServer:
|
|
|
1047
1004
|
result = await self.tools.swipe(
|
|
1048
1005
|
arguments["direction"],
|
|
1049
1006
|
y=arguments.get("y"),
|
|
1050
|
-
y_percent=arguments.get("y_percent")
|
|
1007
|
+
y_percent=arguments.get("y_percent"),
|
|
1008
|
+
distance=arguments.get("distance"),
|
|
1009
|
+
distance_percent=arguments.get("distance_percent")
|
|
1010
|
+
)
|
|
1011
|
+
return [TextContent(type="text", text=self.format_response(result))]
|
|
1012
|
+
|
|
1013
|
+
elif name == "mobile_drag_progress_bar":
|
|
1014
|
+
result = await self.tools.drag_progress_bar(
|
|
1015
|
+
direction=arguments.get("direction", "right"),
|
|
1016
|
+
distance_percent=arguments.get("distance_percent", 30.0),
|
|
1017
|
+
y_percent=arguments.get("y_percent"),
|
|
1018
|
+
y=arguments.get("y")
|
|
1051
1019
|
)
|
|
1052
1020
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
1053
1021
|
|
|
@@ -1091,7 +1059,17 @@ class MobileMCPServer:
|
|
|
1091
1059
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
1092
1060
|
|
|
1093
1061
|
elif name == "mobile_close_popup":
|
|
1094
|
-
|
|
1062
|
+
popup_detected = arguments.get("popup_detected")
|
|
1063
|
+
popup_bounds = arguments.get("popup_bounds")
|
|
1064
|
+
# 如果传入了popup_bounds,转换为tuple
|
|
1065
|
+
if popup_bounds and isinstance(popup_bounds, list) and len(popup_bounds) == 4:
|
|
1066
|
+
popup_bounds = tuple(popup_bounds)
|
|
1067
|
+
elif popup_bounds:
|
|
1068
|
+
popup_bounds = None # 格式不正确,忽略
|
|
1069
|
+
result = self.tools.close_popup(
|
|
1070
|
+
popup_detected=popup_detected,
|
|
1071
|
+
popup_bounds=popup_bounds
|
|
1072
|
+
)
|
|
1095
1073
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
1096
1074
|
|
|
1097
1075
|
elif name == "mobile_assert_text":
|
|
@@ -1174,13 +1152,6 @@ class MobileMCPServer:
|
|
|
1174
1152
|
result = {"success": False, "error": "请提供 x_percent/y_percent 或 screenshot_path/x/y/width/height"}
|
|
1175
1153
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
1176
1154
|
|
|
1177
|
-
# Cursor自动化:打开新Chat
|
|
1178
|
-
elif name == "mobile_open_new_chat":
|
|
1179
|
-
message = arguments.get("message", "继续执行飞书用例")
|
|
1180
|
-
delay = arguments.get("delay", 5)
|
|
1181
|
-
result = self._open_new_chat(message, delay)
|
|
1182
|
-
return [TextContent(type="text", text=self.format_response(result))]
|
|
1183
|
-
|
|
1184
1155
|
else:
|
|
1185
1156
|
return [TextContent(type="text", text=f"❌ 未知工具: {name}")]
|
|
1186
1157
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
mobile_mcp/__init__.py,sha256=sQJZTL_sxQFzmcS7jOtS2AHCfUySz40vhX96N6u1qy4,816
|
|
2
2
|
mobile_mcp/config.py,sha256=-xSl9vahp3EFAA97P1ahcnQC-HHAFvccGHpnFAXeKHU,5841
|
|
3
3
|
mobile_mcp/core/__init__.py,sha256=ndMy-cLAIsQDG5op7gM_AIplycqZSZPWEkec1pEhvEY,170
|
|
4
|
-
mobile_mcp/core/basic_tools_lite.py,sha256=
|
|
4
|
+
mobile_mcp/core/basic_tools_lite.py,sha256=tclg7m7STWaWwSizAhK-MeZhsvboKqjKo_t5ZKEt0n4,221624
|
|
5
5
|
mobile_mcp/core/device_manager.py,sha256=xG5DoeNFs45pl-FTEhEWblqVwxtFK-FmVEGlNL6EqRI,8798
|
|
6
6
|
mobile_mcp/core/dynamic_config.py,sha256=Ja1n1pfb0HspGByqk2_A472mYVniKmGtNEWyjUjmgK8,9811
|
|
7
7
|
mobile_mcp/core/ios_client_wda.py,sha256=Nq9WxevhTWpVpolM-Ymp-b0nUQV3tXLFszmJHbDC4wA,18770
|
|
@@ -19,14 +19,14 @@ mobile_mcp/core/utils/logger.py,sha256=XXQAHUwT1jc70pq_tYFmL6f_nKrFlYm3hcgl-5RYR
|
|
|
19
19
|
mobile_mcp/core/utils/operation_history_manager.py,sha256=gi8S8HJAMqvkUrY7_-kVbko3Xt7c4GAUziEujRd-N-Y,4792
|
|
20
20
|
mobile_mcp/core/utils/smart_wait.py,sha256=N5wKTUYrNWPruBILqrAjpvtso8Z3GRWCfMIR_aZxPLg,8649
|
|
21
21
|
mobile_mcp/mcp_tools/__init__.py,sha256=xkro8Rwqv_55YlVyhh-3DgRFSsLE3h1r31VIb3bpM6E,143
|
|
22
|
-
mobile_mcp/mcp_tools/mcp_server.py,sha256=
|
|
22
|
+
mobile_mcp/mcp_tools/mcp_server.py,sha256=nEb_chFF_1lN__4nMei2yPzVgzlDeYK67p-KJVta-VU,53521
|
|
23
23
|
mobile_mcp/utils/__init__.py,sha256=8EH0i7UGtx1y_j_GEgdN-cZdWn2sRtZSEOLlNF9HRnY,158
|
|
24
24
|
mobile_mcp/utils/logger.py,sha256=Sqq2Nr0Y4p03erqcrbYKVPCGiFaNGHMcE_JwCkeOfU4,3626
|
|
25
25
|
mobile_mcp/utils/xml_formatter.py,sha256=uwTRb3vLbqhT8O-udzWT7s7LsV-DyDUz2DkofD3hXOE,4556
|
|
26
26
|
mobile_mcp/utils/xml_parser.py,sha256=QhL8CWbdmNDzmBLjtx6mEnjHgMFZzJeHpCL15qfXSpI,3926
|
|
27
|
-
mobile_mcp_ai-2.7.
|
|
28
|
-
mobile_mcp_ai-2.7.
|
|
29
|
-
mobile_mcp_ai-2.7.
|
|
30
|
-
mobile_mcp_ai-2.7.
|
|
31
|
-
mobile_mcp_ai-2.7.
|
|
32
|
-
mobile_mcp_ai-2.7.
|
|
27
|
+
mobile_mcp_ai-2.7.4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
28
|
+
mobile_mcp_ai-2.7.4.dist-info/METADATA,sha256=L46BIHR90WVEk5KCH4WvCmP6kM2H3XnLETW3w2wFg8Y,11505
|
|
29
|
+
mobile_mcp_ai-2.7.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
30
|
+
mobile_mcp_ai-2.7.4.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
|
|
31
|
+
mobile_mcp_ai-2.7.4.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
|
|
32
|
+
mobile_mcp_ai-2.7.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|