mobile-mcp-ai 2.6.4__py3-none-any.whl → 2.6.6__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/config.py +0 -32
- mobile_mcp/core/basic_tools_lite.py +445 -271
- mobile_mcp/mcp_tools/mcp_server.py +306 -216
- {mobile_mcp_ai-2.6.4.dist-info → mobile_mcp_ai-2.6.6.dist-info}/METADATA +30 -17
- {mobile_mcp_ai-2.6.4.dist-info → mobile_mcp_ai-2.6.6.dist-info}/RECORD +9 -9
- {mobile_mcp_ai-2.6.4.dist-info → mobile_mcp_ai-2.6.6.dist-info}/WHEEL +1 -1
- {mobile_mcp_ai-2.6.4.dist-info → mobile_mcp_ai-2.6.6.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.6.4.dist-info → mobile_mcp_ai-2.6.6.dist-info/licenses}/LICENSE +0 -0
- {mobile_mcp_ai-2.6.4.dist-info → mobile_mcp_ai-2.6.6.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
- 核心功能精简
|
|
9
9
|
- 保留 pytest 脚本生成
|
|
10
10
|
- 支持操作历史记录
|
|
11
|
-
- Token 优化模式(省钱)
|
|
12
11
|
"""
|
|
13
12
|
|
|
14
13
|
import asyncio
|
|
@@ -18,19 +17,6 @@ from pathlib import Path
|
|
|
18
17
|
from typing import Dict, List, Optional
|
|
19
18
|
from datetime import datetime
|
|
20
19
|
|
|
21
|
-
# Token 优化配置(只精简格式,不限制数量,确保准确度)
|
|
22
|
-
try:
|
|
23
|
-
from mobile_mcp.config import Config
|
|
24
|
-
TOKEN_OPTIMIZATION = Config.TOKEN_OPTIMIZATION_ENABLED
|
|
25
|
-
MAX_ELEMENTS = Config.MAX_ELEMENTS_RETURN
|
|
26
|
-
MAX_SOM_ELEMENTS = Config.MAX_SOM_ELEMENTS_RETURN
|
|
27
|
-
COMPACT_RESPONSE = Config.COMPACT_RESPONSE
|
|
28
|
-
except ImportError:
|
|
29
|
-
TOKEN_OPTIMIZATION = True
|
|
30
|
-
MAX_ELEMENTS = 0 # 0 = 不限制
|
|
31
|
-
MAX_SOM_ELEMENTS = 0 # 0 = 不限制
|
|
32
|
-
COMPACT_RESPONSE = True
|
|
33
|
-
|
|
34
20
|
|
|
35
21
|
class BasicMobileToolsLite:
|
|
36
22
|
"""精简版移动端工具"""
|
|
@@ -349,7 +335,7 @@ class BasicMobileToolsLite:
|
|
|
349
335
|
size = ios_client.wda.window_size()
|
|
350
336
|
screen_width, screen_height = size[0], size[1]
|
|
351
337
|
else:
|
|
352
|
-
return {"success": False, "
|
|
338
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
353
339
|
else:
|
|
354
340
|
self.client.u2.screenshot(str(temp_path))
|
|
355
341
|
info = self.client.u2.info
|
|
@@ -400,14 +386,22 @@ class BasicMobileToolsLite:
|
|
|
400
386
|
|
|
401
387
|
cropped_size = final_path.stat().st_size
|
|
402
388
|
|
|
403
|
-
# 返回结果
|
|
404
389
|
return {
|
|
405
390
|
"success": True,
|
|
406
391
|
"screenshot_path": str(final_path),
|
|
392
|
+
"screen_width": screen_width,
|
|
393
|
+
"screen_height": screen_height,
|
|
407
394
|
"image_width": img.width,
|
|
408
395
|
"image_height": img.height,
|
|
409
396
|
"crop_offset_x": crop_offset_x,
|
|
410
|
-
"crop_offset_y": crop_offset_y
|
|
397
|
+
"crop_offset_y": crop_offset_y,
|
|
398
|
+
"file_size": f"{cropped_size/1024:.1f}KB",
|
|
399
|
+
"message": f"🔍 局部截图已保存: {final_path}\n"
|
|
400
|
+
f"📐 裁剪区域: ({crop_offset_x}, {crop_offset_y}) 起,{img.width}x{img.height} 像素\n"
|
|
401
|
+
f"📦 文件大小: {cropped_size/1024:.0f}KB\n"
|
|
402
|
+
f"🎯 【坐标换算】AI 返回坐标 (x, y) 后:\n"
|
|
403
|
+
f" 实际屏幕坐标 = ({crop_offset_x} + x, {crop_offset_y} + y)\n"
|
|
404
|
+
f" 或直接调用 mobile_click_at_coords(x, y, crop_offset_x={crop_offset_x}, crop_offset_y={crop_offset_y})"
|
|
411
405
|
}
|
|
412
406
|
|
|
413
407
|
# ========== 情况2:全屏压缩截图 ==========
|
|
@@ -460,14 +454,24 @@ class BasicMobileToolsLite:
|
|
|
460
454
|
compressed_size = final_path.stat().st_size
|
|
461
455
|
saved_percent = (1 - compressed_size / original_size) * 100
|
|
462
456
|
|
|
463
|
-
# 返回结果
|
|
464
457
|
return {
|
|
465
458
|
"success": True,
|
|
466
459
|
"screenshot_path": str(final_path),
|
|
467
|
-
"
|
|
468
|
-
"
|
|
469
|
-
"original_img_width": original_img_width,
|
|
470
|
-
"original_img_height": original_img_height
|
|
460
|
+
"screen_width": screen_width,
|
|
461
|
+
"screen_height": screen_height,
|
|
462
|
+
"original_img_width": original_img_width, # 截图原始宽度
|
|
463
|
+
"original_img_height": original_img_height, # 截图原始高度
|
|
464
|
+
"image_width": image_width, # 压缩后宽度(AI 看到的)
|
|
465
|
+
"image_height": image_height, # 压缩后高度(AI 看到的)
|
|
466
|
+
"original_size": f"{original_size/1024:.1f}KB",
|
|
467
|
+
"compressed_size": f"{compressed_size/1024:.1f}KB",
|
|
468
|
+
"saved_percent": f"{saved_percent:.0f}%",
|
|
469
|
+
"message": f"📸 截图已保存: {final_path}\n"
|
|
470
|
+
f"📐 原始尺寸: {original_img_width}x{original_img_height} → 压缩后: {image_width}x{image_height}\n"
|
|
471
|
+
f"📦 已压缩: {original_size/1024:.0f}KB → {compressed_size/1024:.0f}KB (省 {saved_percent:.0f}%)\n"
|
|
472
|
+
f"⚠️ 【坐标转换】AI 返回坐标后,请传入:\n"
|
|
473
|
+
f" image_width={image_width}, image_height={image_height},\n"
|
|
474
|
+
f" original_img_width={original_img_width}, original_img_height={original_img_height}"
|
|
471
475
|
}
|
|
472
476
|
|
|
473
477
|
# ========== 情况3:全屏不压缩截图 ==========
|
|
@@ -481,12 +485,21 @@ class BasicMobileToolsLite:
|
|
|
481
485
|
final_path = self.screenshot_dir / filename
|
|
482
486
|
temp_path.rename(final_path)
|
|
483
487
|
|
|
484
|
-
#
|
|
488
|
+
# 不压缩时,用截图实际尺寸(可能和 screen_width 不同)
|
|
485
489
|
return {
|
|
486
490
|
"success": True,
|
|
487
491
|
"screenshot_path": str(final_path),
|
|
488
|
-
"
|
|
489
|
-
"
|
|
492
|
+
"screen_width": screen_width,
|
|
493
|
+
"screen_height": screen_height,
|
|
494
|
+
"original_img_width": img.width, # 截图实际尺寸
|
|
495
|
+
"original_img_height": img.height,
|
|
496
|
+
"image_width": img.width, # 未压缩,和原图一样
|
|
497
|
+
"image_height": img.height,
|
|
498
|
+
"file_size": f"{original_size/1024:.1f}KB",
|
|
499
|
+
"message": f"📸 截图已保存: {final_path}\n"
|
|
500
|
+
f"📐 截图尺寸: {img.width}x{img.height}\n"
|
|
501
|
+
f"📦 文件大小: {original_size/1024:.0f}KB(未压缩)\n"
|
|
502
|
+
f"💡 未压缩,坐标可直接使用"
|
|
490
503
|
}
|
|
491
504
|
except ImportError:
|
|
492
505
|
# 如果没有 PIL,回退到原始方式(不压缩)
|
|
@@ -526,7 +539,7 @@ class BasicMobileToolsLite:
|
|
|
526
539
|
size = ios_client.wda.window_size()
|
|
527
540
|
screen_width, screen_height = size[0], size[1]
|
|
528
541
|
else:
|
|
529
|
-
return {"success": False, "
|
|
542
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
530
543
|
else:
|
|
531
544
|
self.client.u2.screenshot(str(temp_path))
|
|
532
545
|
info = self.client.u2.info
|
|
@@ -640,16 +653,26 @@ class BasicMobileToolsLite:
|
|
|
640
653
|
result = {
|
|
641
654
|
"success": True,
|
|
642
655
|
"screenshot_path": str(final_path),
|
|
656
|
+
"screen_width": screen_width,
|
|
657
|
+
"screen_height": screen_height,
|
|
643
658
|
"image_width": img_width,
|
|
644
659
|
"image_height": img_height,
|
|
645
|
-
"grid_size": grid_size
|
|
660
|
+
"grid_size": grid_size,
|
|
661
|
+
"message": f"📸 网格截图已保存: {final_path}\n"
|
|
662
|
+
f"📐 尺寸: {img_width}x{img_height}\n"
|
|
663
|
+
f"📏 网格间距: {grid_size}px"
|
|
646
664
|
}
|
|
647
665
|
|
|
648
666
|
if popup_info:
|
|
649
|
-
result["
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
667
|
+
result["popup_detected"] = True
|
|
668
|
+
result["popup_bounds"] = popup_info["bounds"]
|
|
669
|
+
result["close_button_hints"] = close_positions
|
|
670
|
+
result["message"] += f"\n🎯 检测到弹窗: {popup_info['bounds']}"
|
|
671
|
+
result["message"] += f"\n💡 可能的关闭按钮位置(绿色圆圈标注):"
|
|
672
|
+
for pos in close_positions:
|
|
673
|
+
result["message"] += f"\n {pos['priority']}. {pos['name']}: ({pos['x']}, {pos['y']})"
|
|
674
|
+
else:
|
|
675
|
+
result["popup_detected"] = False
|
|
653
676
|
|
|
654
677
|
return result
|
|
655
678
|
|
|
@@ -686,7 +709,7 @@ class BasicMobileToolsLite:
|
|
|
686
709
|
size = ios_client.wda.window_size()
|
|
687
710
|
screen_width, screen_height = size[0], size[1]
|
|
688
711
|
else:
|
|
689
|
-
return {"success": False, "
|
|
712
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
690
713
|
else:
|
|
691
714
|
self.client.u2.screenshot(str(temp_path))
|
|
692
715
|
info = self.client.u2.info
|
|
@@ -846,15 +869,38 @@ class BasicMobileToolsLite:
|
|
|
846
869
|
img.save(str(final_path), "JPEG", quality=85)
|
|
847
870
|
temp_path.unlink()
|
|
848
871
|
|
|
849
|
-
#
|
|
872
|
+
# 构建元素列表文字
|
|
873
|
+
elements_text = "\n".join([
|
|
874
|
+
f" [{e['index']}] {e['desc']} → ({e['center'][0]}, {e['center'][1]})"
|
|
875
|
+
for e in som_elements[:15] # 只显示前15个
|
|
876
|
+
])
|
|
877
|
+
if len(som_elements) > 15:
|
|
878
|
+
elements_text += f"\n ... 还有 {len(som_elements) - 15} 个元素"
|
|
879
|
+
|
|
880
|
+
# 构建弹窗提示文字
|
|
881
|
+
hints_text = ""
|
|
882
|
+
if popup_bounds:
|
|
883
|
+
hints_text = f"\n🎯 检测到弹窗区域(蓝色边框)\n"
|
|
884
|
+
hints_text += f" 如需关闭弹窗,请观察图片中的 X 按钮位置\n"
|
|
885
|
+
hints_text += f" 然后使用 mobile_click_by_percent(x%, y%) 点击"
|
|
886
|
+
|
|
850
887
|
return {
|
|
851
888
|
"success": True,
|
|
852
889
|
"screenshot_path": str(final_path),
|
|
853
890
|
"screen_width": screen_width,
|
|
854
891
|
"screen_height": screen_height,
|
|
892
|
+
"image_width": img_width,
|
|
893
|
+
"image_height": img_height,
|
|
855
894
|
"element_count": len(som_elements),
|
|
895
|
+
"elements": som_elements,
|
|
856
896
|
"popup_detected": popup_bounds is not None,
|
|
857
|
-
"
|
|
897
|
+
"popup_bounds": f"[{popup_bounds[0]},{popup_bounds[1]}][{popup_bounds[2]},{popup_bounds[3]}]" if popup_bounds else None,
|
|
898
|
+
"message": f"📸 SoM 截图已保存: {final_path}\n"
|
|
899
|
+
f"🏷️ 已标注 {len(som_elements)} 个可点击元素\n"
|
|
900
|
+
f"📋 元素列表:\n{elements_text}{hints_text}\n\n"
|
|
901
|
+
f"💡 使用方法:\n"
|
|
902
|
+
f" - 点击标注元素:mobile_click_by_som(编号)\n"
|
|
903
|
+
f" - 点击任意位置:mobile_click_by_percent(x%, y%)"
|
|
858
904
|
}
|
|
859
905
|
|
|
860
906
|
except ImportError:
|
|
@@ -935,6 +981,7 @@ class BasicMobileToolsLite:
|
|
|
935
981
|
|
|
936
982
|
return {
|
|
937
983
|
"success": True,
|
|
984
|
+
"message": f"✅ 已点击 [{index}] {target['desc']} → ({cx}, {cy})\n💡 建议:再次截图确认操作是否成功",
|
|
938
985
|
"clicked": {
|
|
939
986
|
"index": index,
|
|
940
987
|
"desc": target['desc'],
|
|
@@ -968,7 +1015,7 @@ class BasicMobileToolsLite:
|
|
|
968
1015
|
size = ios_client.wda.window_size()
|
|
969
1016
|
width, height = size[0], size[1]
|
|
970
1017
|
else:
|
|
971
|
-
return {"success": False, "
|
|
1018
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
972
1019
|
else:
|
|
973
1020
|
self.client.u2.screenshot(str(screenshot_path))
|
|
974
1021
|
info = self.client.u2.info
|
|
@@ -1046,7 +1093,7 @@ class BasicMobileToolsLite:
|
|
|
1046
1093
|
size = ios_client.wda.window_size()
|
|
1047
1094
|
screen_width, screen_height = size[0], size[1]
|
|
1048
1095
|
else:
|
|
1049
|
-
return {"success": False, "
|
|
1096
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
1050
1097
|
else:
|
|
1051
1098
|
info = self.client.u2.info
|
|
1052
1099
|
screen_width = info.get('displayWidth', 0)
|
|
@@ -1161,14 +1208,14 @@ class BasicMobileToolsLite:
|
|
|
1161
1208
|
size = ios_client.wda.window_size()
|
|
1162
1209
|
width, height = size[0], size[1]
|
|
1163
1210
|
else:
|
|
1164
|
-
return {"success": False, "
|
|
1211
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
1165
1212
|
else:
|
|
1166
1213
|
info = self.client.u2.info
|
|
1167
1214
|
width = info.get('displayWidth', 0)
|
|
1168
1215
|
height = info.get('displayHeight', 0)
|
|
1169
1216
|
|
|
1170
1217
|
if width == 0 or height == 0:
|
|
1171
|
-
return {"success": False, "
|
|
1218
|
+
return {"success": False, "message": "❌ 无法获取屏幕尺寸"}
|
|
1172
1219
|
|
|
1173
1220
|
# 第2步:百分比转像素坐标
|
|
1174
1221
|
# 公式:像素 = 屏幕尺寸 × (百分比 / 100)
|
|
@@ -1189,6 +1236,9 @@ class BasicMobileToolsLite:
|
|
|
1189
1236
|
|
|
1190
1237
|
return {
|
|
1191
1238
|
"success": True,
|
|
1239
|
+
"message": f"✅ 百分比点击成功: ({x_percent}%, {y_percent}%) → 像素({x}, {y})",
|
|
1240
|
+
"screen_size": {"width": width, "height": height},
|
|
1241
|
+
"percent": {"x": x_percent, "y": y_percent},
|
|
1192
1242
|
"pixel": {"x": x, "y": y}
|
|
1193
1243
|
}
|
|
1194
1244
|
except Exception as e:
|
|
@@ -1214,12 +1264,10 @@ class BasicMobileToolsLite:
|
|
|
1214
1264
|
if elem.exists:
|
|
1215
1265
|
elem.click()
|
|
1216
1266
|
time.sleep(0.3)
|
|
1267
|
+
# 使用标准记录格式
|
|
1217
1268
|
self._record_click('text', text, element_desc=text, locator_attr='text')
|
|
1218
|
-
return {"success": True}
|
|
1219
|
-
|
|
1220
|
-
return {"success": False, "fallback": "vision", "msg": f"未找到'{text}',用截图点击"}
|
|
1221
|
-
else:
|
|
1222
|
-
return {"success": False, "msg": "iOS未初始化"}
|
|
1269
|
+
return {"success": True, "message": f"✅ 点击成功: '{text}'"}
|
|
1270
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1223
1271
|
else:
|
|
1224
1272
|
# 获取屏幕尺寸用于计算百分比
|
|
1225
1273
|
screen_width, screen_height = self.client.u2.window_size()
|
|
@@ -1240,15 +1288,17 @@ class BasicMobileToolsLite:
|
|
|
1240
1288
|
x_pct = round(cx / screen_width * 100, 1)
|
|
1241
1289
|
y_pct = round(cy / screen_height * 100, 1)
|
|
1242
1290
|
|
|
1243
|
-
#
|
|
1291
|
+
# 如果有位置参数,直接使用坐标点击(避免 u2 选择器匹配到错误的元素)
|
|
1244
1292
|
if position and bounds:
|
|
1245
1293
|
x = (bounds[0] + bounds[2]) // 2
|
|
1246
1294
|
y = (bounds[1] + bounds[3]) // 2
|
|
1247
1295
|
self.client.u2.click(x, y)
|
|
1248
1296
|
time.sleep(0.3)
|
|
1297
|
+
position_info = f" ({position})" if position else ""
|
|
1298
|
+
# 虽然用坐标点击,但记录时仍使用文本定位(脚本更稳定)
|
|
1249
1299
|
self._record_click('text', attr_value, x_pct, y_pct,
|
|
1250
|
-
element_desc=f"{text}
|
|
1251
|
-
return {"success": True}
|
|
1300
|
+
element_desc=f"{text}{position_info}", locator_attr=attr_type)
|
|
1301
|
+
return {"success": True, "message": f"✅ 点击成功(坐标定位): '{text}'{position_info} @ ({x},{y})"}
|
|
1252
1302
|
|
|
1253
1303
|
# 没有位置参数时,使用选择器定位
|
|
1254
1304
|
if attr_type == 'text':
|
|
@@ -1265,24 +1315,27 @@ class BasicMobileToolsLite:
|
|
|
1265
1315
|
if elem and elem.exists(timeout=1):
|
|
1266
1316
|
elem.click()
|
|
1267
1317
|
time.sleep(0.3)
|
|
1318
|
+
position_info = f" ({position})" if position else ""
|
|
1319
|
+
# 使用标准记录格式:文本定位
|
|
1268
1320
|
self._record_click('text', attr_value, x_pct, y_pct,
|
|
1269
1321
|
element_desc=text, locator_attr=attr_type)
|
|
1270
|
-
return {"success": True}
|
|
1322
|
+
return {"success": True, "message": f"✅ 点击成功({attr_type}): '{text}'{position_info}"}
|
|
1271
1323
|
|
|
1272
|
-
#
|
|
1324
|
+
# 如果选择器失败,用坐标兜底
|
|
1273
1325
|
if bounds:
|
|
1274
1326
|
x = (bounds[0] + bounds[2]) // 2
|
|
1275
1327
|
y = (bounds[1] + bounds[3]) // 2
|
|
1276
1328
|
self.client.u2.click(x, y)
|
|
1277
1329
|
time.sleep(0.3)
|
|
1330
|
+
position_info = f" ({position})" if position else ""
|
|
1331
|
+
# 选择器失败,用百分比作为兜底
|
|
1278
1332
|
self._record_click('percent', f"{x_pct}%,{y_pct}%", x_pct, y_pct,
|
|
1279
|
-
element_desc=text)
|
|
1280
|
-
return {"success": True}
|
|
1333
|
+
element_desc=f"{text}{position_info}")
|
|
1334
|
+
return {"success": True, "message": f"✅ 点击成功(坐标兜底): '{text}'{position_info} @ ({x},{y})"}
|
|
1281
1335
|
|
|
1282
|
-
|
|
1283
|
-
return {"success": False, "fallback": "vision", "msg": f"未找到'{text}',用截图点击"}
|
|
1336
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1284
1337
|
except Exception as e:
|
|
1285
|
-
return {"success": False, "
|
|
1338
|
+
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
1286
1339
|
|
|
1287
1340
|
def _find_element_in_tree(self, text: str, position: Optional[str] = None) -> Optional[Dict]:
|
|
1288
1341
|
"""在 XML 树中查找包含指定文本的元素,优先返回可点击的元素
|
|
@@ -1421,8 +1474,15 @@ class BasicMobileToolsLite:
|
|
|
1421
1474
|
return None
|
|
1422
1475
|
|
|
1423
1476
|
def click_by_id(self, resource_id: str, index: int = 0) -> Dict:
|
|
1424
|
-
"""通过 resource-id
|
|
1477
|
+
"""通过 resource-id 点击(支持点击第 N 个元素)
|
|
1478
|
+
|
|
1479
|
+
Args:
|
|
1480
|
+
resource_id: 元素的 resource-id
|
|
1481
|
+
index: 第几个元素(从 0 开始),默认 0 表示第一个
|
|
1482
|
+
"""
|
|
1425
1483
|
try:
|
|
1484
|
+
index_desc = f"[{index}]" if index > 0 else ""
|
|
1485
|
+
|
|
1426
1486
|
if self._is_ios():
|
|
1427
1487
|
ios_client = self._get_ios_client()
|
|
1428
1488
|
if ios_client and hasattr(ios_client, 'wda'):
|
|
@@ -1430,31 +1490,33 @@ class BasicMobileToolsLite:
|
|
|
1430
1490
|
if not elem.exists:
|
|
1431
1491
|
elem = ios_client.wda(name=resource_id)
|
|
1432
1492
|
if elem.exists:
|
|
1493
|
+
# 获取所有匹配的元素
|
|
1433
1494
|
elements = elem.find_elements()
|
|
1434
1495
|
if index < len(elements):
|
|
1435
1496
|
elements[index].click()
|
|
1436
1497
|
time.sleep(0.3)
|
|
1437
|
-
|
|
1438
|
-
|
|
1498
|
+
# 使用标准记录格式
|
|
1499
|
+
self._record_click('id', resource_id, element_desc=f"{resource_id}{index_desc}")
|
|
1500
|
+
return {"success": True, "message": f"✅ 点击成功: {resource_id}{index_desc}"}
|
|
1439
1501
|
else:
|
|
1440
|
-
return {"success": False, "
|
|
1441
|
-
return {"success": False, "
|
|
1442
|
-
else:
|
|
1443
|
-
return {"success": False, "msg": "iOS未初始化"}
|
|
1502
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {len(elements)} 个元素,但请求索引 {index}"}
|
|
1503
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1444
1504
|
else:
|
|
1445
1505
|
elem = self.client.u2(resourceId=resource_id)
|
|
1446
1506
|
if elem.exists(timeout=0.5):
|
|
1507
|
+
# 获取匹配元素数量
|
|
1447
1508
|
count = elem.count
|
|
1448
1509
|
if index < count:
|
|
1449
1510
|
elem[index].click()
|
|
1450
1511
|
time.sleep(0.3)
|
|
1451
|
-
|
|
1452
|
-
|
|
1512
|
+
# 使用标准记录格式
|
|
1513
|
+
self._record_click('id', resource_id, element_desc=f"{resource_id}{index_desc}")
|
|
1514
|
+
return {"success": True, "message": f"✅ 点击成功: {resource_id}{index_desc}" + (f" (共 {count} 个)" if count > 1 else "")}
|
|
1453
1515
|
else:
|
|
1454
|
-
return {"success": False, "
|
|
1455
|
-
return {"success": False, "
|
|
1516
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {count} 个元素,但请求索引 {index}"}
|
|
1517
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1456
1518
|
except Exception as e:
|
|
1457
|
-
return {"success": False, "
|
|
1519
|
+
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
1458
1520
|
|
|
1459
1521
|
# ==================== 长按操作 ====================
|
|
1460
1522
|
|
|
@@ -1488,7 +1550,7 @@ class BasicMobileToolsLite:
|
|
|
1488
1550
|
size = ios_client.wda.window_size()
|
|
1489
1551
|
screen_width, screen_height = size[0], size[1]
|
|
1490
1552
|
else:
|
|
1491
|
-
return {"success": False, "
|
|
1553
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
1492
1554
|
else:
|
|
1493
1555
|
info = self.client.u2.info
|
|
1494
1556
|
screen_width = info.get('displayWidth', 0)
|
|
@@ -1541,11 +1603,23 @@ class BasicMobileToolsLite:
|
|
|
1541
1603
|
|
|
1542
1604
|
if converted:
|
|
1543
1605
|
if conversion_type == "crop_offset":
|
|
1544
|
-
return {
|
|
1606
|
+
return {
|
|
1607
|
+
"success": True,
|
|
1608
|
+
"message": f"✅ 长按成功: ({x}, {y}) 持续 {duration}s\n"
|
|
1609
|
+
f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
|
|
1610
|
+
}
|
|
1545
1611
|
else:
|
|
1546
|
-
return {
|
|
1612
|
+
return {
|
|
1613
|
+
"success": True,
|
|
1614
|
+
"message": f"✅ 长按成功: ({x}, {y}) 持续 {duration}s\n"
|
|
1615
|
+
f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n"
|
|
1616
|
+
f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
|
|
1617
|
+
}
|
|
1547
1618
|
else:
|
|
1548
|
-
return {
|
|
1619
|
+
return {
|
|
1620
|
+
"success": True,
|
|
1621
|
+
"message": f"✅ 长按成功: ({x}, {y}) 持续 {duration}s [相对位置: {x_percent}%, {y_percent}%]"
|
|
1622
|
+
}
|
|
1549
1623
|
except Exception as e:
|
|
1550
1624
|
return {"success": False, "message": f"❌ 长按失败: {e}"}
|
|
1551
1625
|
|
|
@@ -1574,14 +1648,14 @@ class BasicMobileToolsLite:
|
|
|
1574
1648
|
size = ios_client.wda.window_size()
|
|
1575
1649
|
width, height = size[0], size[1]
|
|
1576
1650
|
else:
|
|
1577
|
-
return {"success": False, "
|
|
1651
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
1578
1652
|
else:
|
|
1579
1653
|
info = self.client.u2.info
|
|
1580
1654
|
width = info.get('displayWidth', 0)
|
|
1581
1655
|
height = info.get('displayHeight', 0)
|
|
1582
1656
|
|
|
1583
1657
|
if width == 0 or height == 0:
|
|
1584
|
-
return {"success": False, "
|
|
1658
|
+
return {"success": False, "message": "❌ 无法获取屏幕尺寸"}
|
|
1585
1659
|
|
|
1586
1660
|
# 第2步:百分比转像素坐标
|
|
1587
1661
|
x = int(width * x_percent / 100)
|
|
@@ -1603,7 +1677,13 @@ class BasicMobileToolsLite:
|
|
|
1603
1677
|
self._record_long_press('percent', f"{x_percent}%,{y_percent}%", duration,
|
|
1604
1678
|
x_percent, y_percent, element_desc=f"百分比({x_percent}%,{y_percent}%)")
|
|
1605
1679
|
|
|
1606
|
-
return {
|
|
1680
|
+
return {
|
|
1681
|
+
"success": True,
|
|
1682
|
+
"message": f"✅ 百分比长按成功: ({x_percent}%, {y_percent}%) → 像素({x}, {y}) 持续 {duration}s",
|
|
1683
|
+
"screen_size": {"width": width, "height": height},
|
|
1684
|
+
"percent": {"x": x_percent, "y": y_percent},
|
|
1685
|
+
"pixel": {"x": x, "y": y},
|
|
1686
|
+
"duration": duration
|
|
1607
1687
|
}
|
|
1608
1688
|
except Exception as e:
|
|
1609
1689
|
return {"success": False, "message": f"❌ 百分比长按失败: {e}"}
|
|
@@ -1633,8 +1713,8 @@ class BasicMobileToolsLite:
|
|
|
1633
1713
|
ios_client.wda.swipe(x, y, x, y, duration=duration)
|
|
1634
1714
|
time.sleep(0.3)
|
|
1635
1715
|
self._record_long_press('text', text, duration, element_desc=text, locator_attr='text')
|
|
1636
|
-
return {"success": True}
|
|
1637
|
-
return {"success": False, "
|
|
1716
|
+
return {"success": True, "message": f"✅ 长按成功: '{text}' 持续 {duration}s"}
|
|
1717
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1638
1718
|
else:
|
|
1639
1719
|
# 获取屏幕尺寸用于计算百分比
|
|
1640
1720
|
screen_width, screen_height = self.client.u2.window_size()
|
|
@@ -1672,7 +1752,7 @@ class BasicMobileToolsLite:
|
|
|
1672
1752
|
time.sleep(0.3)
|
|
1673
1753
|
self._record_long_press('text', attr_value, duration, x_pct, y_pct,
|
|
1674
1754
|
element_desc=text, locator_attr=attr_type)
|
|
1675
|
-
return {"success": True}
|
|
1755
|
+
return {"success": True, "message": f"✅ 长按成功({attr_type}): '{text}' 持续 {duration}s"}
|
|
1676
1756
|
|
|
1677
1757
|
# 如果选择器失败,用坐标兜底
|
|
1678
1758
|
if bounds:
|
|
@@ -1682,9 +1762,9 @@ class BasicMobileToolsLite:
|
|
|
1682
1762
|
time.sleep(0.3)
|
|
1683
1763
|
self._record_long_press('percent', f"{x_pct}%,{y_pct}%", duration, x_pct, y_pct,
|
|
1684
1764
|
element_desc=text)
|
|
1685
|
-
return {"success": True}
|
|
1765
|
+
return {"success": True, "message": f"✅ 长按成功(坐标兜底): '{text}' @ ({x},{y}) 持续 {duration}s"}
|
|
1686
1766
|
|
|
1687
|
-
return {"success": False, "
|
|
1767
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1688
1768
|
except Exception as e:
|
|
1689
1769
|
return {"success": False, "message": f"❌ 长按失败: {e}"}
|
|
1690
1770
|
|
|
@@ -1712,8 +1792,8 @@ class BasicMobileToolsLite:
|
|
|
1712
1792
|
ios_client.wda.swipe(x, y, x, y, duration=duration)
|
|
1713
1793
|
time.sleep(0.3)
|
|
1714
1794
|
self._record_long_press('id', resource_id, duration, element_desc=resource_id)
|
|
1715
|
-
return {"success": True}
|
|
1716
|
-
return {"success": False, "
|
|
1795
|
+
return {"success": True, "message": f"✅ 长按成功: {resource_id} 持续 {duration}s"}
|
|
1796
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1717
1797
|
else:
|
|
1718
1798
|
elem = self.client.u2(resourceId=resource_id)
|
|
1719
1799
|
if elem.exists(timeout=0.5):
|
|
@@ -1721,7 +1801,7 @@ class BasicMobileToolsLite:
|
|
|
1721
1801
|
time.sleep(0.3)
|
|
1722
1802
|
self._record_long_press('id', resource_id, duration, element_desc=resource_id)
|
|
1723
1803
|
return {"success": True, "message": f"✅ 长按成功: {resource_id} 持续 {duration}s"}
|
|
1724
|
-
return {"success": False, "
|
|
1804
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1725
1805
|
except Exception as e:
|
|
1726
1806
|
return {"success": False, "message": f"❌ 长按失败: {e}"}
|
|
1727
1807
|
|
|
@@ -1978,8 +2058,15 @@ class BasicMobileToolsLite:
|
|
|
1978
2058
|
x_percent = round(x / screen_width * 100, 1) if screen_width > 0 else 0
|
|
1979
2059
|
y_percent = round(y / screen_height * 100, 1) if screen_height > 0 else 0
|
|
1980
2060
|
|
|
1981
|
-
|
|
1982
|
-
|
|
2061
|
+
self._record_operation(
|
|
2062
|
+
'input',
|
|
2063
|
+
x=x,
|
|
2064
|
+
y=y,
|
|
2065
|
+
x_percent=x_percent,
|
|
2066
|
+
y_percent=y_percent,
|
|
2067
|
+
ref=f"coords_{x}_{y}",
|
|
2068
|
+
text=text
|
|
2069
|
+
)
|
|
1983
2070
|
|
|
1984
2071
|
# 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
|
|
1985
2072
|
app_check = self._check_app_switched()
|
|
@@ -2009,13 +2096,16 @@ class BasicMobileToolsLite:
|
|
|
2009
2096
|
|
|
2010
2097
|
# ==================== 导航操作 ====================
|
|
2011
2098
|
|
|
2012
|
-
async def swipe(self, direction: str, y: Optional[int] = None, y_percent: Optional[float] = None
|
|
2099
|
+
async def swipe(self, direction: str, y: Optional[int] = None, y_percent: Optional[float] = None,
|
|
2100
|
+
distance: Optional[int] = None, distance_percent: Optional[float] = None) -> Dict:
|
|
2013
2101
|
"""滑动屏幕
|
|
2014
2102
|
|
|
2015
2103
|
Args:
|
|
2016
2104
|
direction: 滑动方向 (up/down/left/right)
|
|
2017
2105
|
y: 左右滑动时指定的高度坐标(像素)
|
|
2018
2106
|
y_percent: 左右滑动时指定的高度百分比 (0-100)
|
|
2107
|
+
distance: 横向滑动时指定的滑动距离(像素),仅用于 left/right
|
|
2108
|
+
distance_percent: 横向滑动时指定的滑动距离百分比 (0-100),仅用于 left/right
|
|
2019
2109
|
"""
|
|
2020
2110
|
try:
|
|
2021
2111
|
if self._is_ios():
|
|
@@ -2024,7 +2114,7 @@ class BasicMobileToolsLite:
|
|
|
2024
2114
|
size = ios_client.wda.window_size()
|
|
2025
2115
|
width, height = size[0], size[1]
|
|
2026
2116
|
else:
|
|
2027
|
-
return {"success": False, "
|
|
2117
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
2028
2118
|
else:
|
|
2029
2119
|
width, height = self.client.u2.window_size()
|
|
2030
2120
|
|
|
@@ -2042,20 +2132,53 @@ class BasicMobileToolsLite:
|
|
|
2042
2132
|
swipe_y = y
|
|
2043
2133
|
else:
|
|
2044
2134
|
swipe_y = center_y
|
|
2135
|
+
|
|
2136
|
+
# 计算横向滑动距离
|
|
2137
|
+
if distance_percent is not None:
|
|
2138
|
+
if not (0 <= distance_percent <= 100):
|
|
2139
|
+
return {"success": False, "message": f"❌ distance_percent 必须在 0-100 之间: {distance_percent}"}
|
|
2140
|
+
swipe_distance = int(width * distance_percent / 100)
|
|
2141
|
+
elif distance is not None:
|
|
2142
|
+
if distance <= 0:
|
|
2143
|
+
return {"success": False, "message": f"❌ distance 必须大于 0: {distance}"}
|
|
2144
|
+
if distance > width:
|
|
2145
|
+
return {"success": False, "message": f"❌ distance 不能超过屏幕宽度 ({width}): {distance}"}
|
|
2146
|
+
swipe_distance = distance
|
|
2147
|
+
else:
|
|
2148
|
+
# 默认滑动距离:屏幕宽度的 60%(从 0.8 到 0.2)
|
|
2149
|
+
swipe_distance = int(width * 0.6)
|
|
2150
|
+
|
|
2151
|
+
# 计算起始和结束位置
|
|
2152
|
+
if direction == 'left':
|
|
2153
|
+
# 从右向左滑动:起始点在右侧,结束点在左侧
|
|
2154
|
+
# 确保起始点不超出屏幕右边界
|
|
2155
|
+
start_x = min(center_x + swipe_distance // 2, width - 10)
|
|
2156
|
+
end_x = start_x - swipe_distance
|
|
2157
|
+
# 确保结束点不超出屏幕左边界
|
|
2158
|
+
if end_x < 10:
|
|
2159
|
+
end_x = 10
|
|
2160
|
+
start_x = min(end_x + swipe_distance, width - 10)
|
|
2161
|
+
else: # right
|
|
2162
|
+
# 从左向右滑动:起始点在左侧,结束点在右侧
|
|
2163
|
+
# 确保起始点不超出屏幕左边界
|
|
2164
|
+
start_x = max(center_x - swipe_distance // 2, 10)
|
|
2165
|
+
end_x = start_x + swipe_distance
|
|
2166
|
+
# 确保结束点不超出屏幕右边界
|
|
2167
|
+
if end_x > width - 10:
|
|
2168
|
+
end_x = width - 10
|
|
2169
|
+
start_x = max(end_x - swipe_distance, 10)
|
|
2170
|
+
|
|
2171
|
+
x1, y1, x2, y2 = start_x, swipe_y, end_x, swipe_y
|
|
2045
2172
|
else:
|
|
2046
2173
|
swipe_y = center_y
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
if direction not in swipe_map:
|
|
2056
|
-
return {"success": False, "message": f"❌ 不支持的方向: {direction}"}
|
|
2057
|
-
|
|
2058
|
-
x1, y1, x2, y2 = swipe_map[direction]
|
|
2174
|
+
# 纵向滑动保持原有逻辑
|
|
2175
|
+
swipe_map = {
|
|
2176
|
+
'up': (center_x, int(height * 0.8), center_x, int(height * 0.2)),
|
|
2177
|
+
'down': (center_x, int(height * 0.2), center_x, int(height * 0.8)),
|
|
2178
|
+
}
|
|
2179
|
+
if direction not in swipe_map:
|
|
2180
|
+
return {"success": False, "message": f"❌ 不支持的方向: {direction}"}
|
|
2181
|
+
x1, y1, x2, y2 = swipe_map[direction]
|
|
2059
2182
|
|
|
2060
2183
|
if self._is_ios():
|
|
2061
2184
|
ios_client.wda.swipe(x1, y1, x2, y2)
|
|
@@ -2076,10 +2199,21 @@ class BasicMobileToolsLite:
|
|
|
2076
2199
|
# 构建返回消息
|
|
2077
2200
|
msg = f"✅ 滑动成功: {direction}"
|
|
2078
2201
|
if direction in ['left', 'right']:
|
|
2202
|
+
msg_parts = []
|
|
2079
2203
|
if y_percent is not None:
|
|
2080
|
-
|
|
2204
|
+
msg_parts.append(f"高度: {y_percent}% = {swipe_y}px")
|
|
2081
2205
|
elif y is not None:
|
|
2082
|
-
|
|
2206
|
+
msg_parts.append(f"高度: {y}px")
|
|
2207
|
+
|
|
2208
|
+
if distance_percent is not None:
|
|
2209
|
+
msg_parts.append(f"距离: {distance_percent}% = {swipe_distance}px")
|
|
2210
|
+
elif distance is not None:
|
|
2211
|
+
msg_parts.append(f"距离: {distance}px")
|
|
2212
|
+
else:
|
|
2213
|
+
msg_parts.append(f"距离: 默认 {swipe_distance}px")
|
|
2214
|
+
|
|
2215
|
+
if msg_parts:
|
|
2216
|
+
msg += f" ({', '.join(msg_parts)})"
|
|
2083
2217
|
|
|
2084
2218
|
# 如果检测到应用跳转,添加警告和返回结果
|
|
2085
2219
|
if app_check['switched']:
|
|
@@ -2120,22 +2254,22 @@ class BasicMobileToolsLite:
|
|
|
2120
2254
|
ios_client.wda.send_keys('\n')
|
|
2121
2255
|
elif ios_key == 'home':
|
|
2122
2256
|
ios_client.wda.home()
|
|
2123
|
-
return {"success": True}
|
|
2124
|
-
return {"success": False, "
|
|
2257
|
+
return {"success": True, "message": f"✅ 按键成功: {key}"}
|
|
2258
|
+
return {"success": False, "message": f"❌ iOS 不支持: {key}"}
|
|
2125
2259
|
else:
|
|
2126
2260
|
keycode = key_map.get(key.lower())
|
|
2127
2261
|
if keycode:
|
|
2128
2262
|
self.client.u2.shell(f'input keyevent {keycode}')
|
|
2129
2263
|
self._record_key(key)
|
|
2130
|
-
return {"success": True}
|
|
2131
|
-
return {"success": False, "
|
|
2264
|
+
return {"success": True, "message": f"✅ 按键成功: {key}"}
|
|
2265
|
+
return {"success": False, "message": f"❌ 不支持的按键: {key}"}
|
|
2132
2266
|
except Exception as e:
|
|
2133
2267
|
return {"success": False, "message": f"❌ 按键失败: {e}"}
|
|
2134
2268
|
|
|
2135
2269
|
def wait(self, seconds: float) -> Dict:
|
|
2136
2270
|
"""等待指定时间"""
|
|
2137
2271
|
time.sleep(seconds)
|
|
2138
|
-
return {"success": True}
|
|
2272
|
+
return {"success": True, "message": f"✅ 已等待 {seconds} 秒"}
|
|
2139
2273
|
|
|
2140
2274
|
# ==================== 应用管理 ====================
|
|
2141
2275
|
|
|
@@ -2164,7 +2298,10 @@ class BasicMobileToolsLite:
|
|
|
2164
2298
|
|
|
2165
2299
|
self._record_operation('launch_app', package_name=package_name)
|
|
2166
2300
|
|
|
2167
|
-
return {
|
|
2301
|
+
return {
|
|
2302
|
+
"success": True,
|
|
2303
|
+
"message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3 秒让页面加载\n📱 已设置应用状态监测"
|
|
2304
|
+
}
|
|
2168
2305
|
except Exception as e:
|
|
2169
2306
|
return {"success": False, "message": f"❌ 启动失败: {e}"}
|
|
2170
2307
|
|
|
@@ -2177,9 +2314,9 @@ class BasicMobileToolsLite:
|
|
|
2177
2314
|
ios_client.wda.app_terminate(package_name)
|
|
2178
2315
|
else:
|
|
2179
2316
|
self.client.u2.app_stop(package_name)
|
|
2180
|
-
return {"success": True}
|
|
2317
|
+
return {"success": True, "message": f"✅ 已终止: {package_name}"}
|
|
2181
2318
|
except Exception as e:
|
|
2182
|
-
return {"success": False, "
|
|
2319
|
+
return {"success": False, "message": f"❌ 终止失败: {e}"}
|
|
2183
2320
|
|
|
2184
2321
|
def list_apps(self, filter_keyword: str = "") -> Dict:
|
|
2185
2322
|
"""列出已安装应用"""
|
|
@@ -2291,26 +2428,6 @@ class BasicMobileToolsLite:
|
|
|
2291
2428
|
'_shadow', 'shadow_', '_divider', 'divider_', '_line', 'line_'
|
|
2292
2429
|
}
|
|
2293
2430
|
|
|
2294
|
-
# Token 优化:构建精简元素(只返回非空字段)
|
|
2295
|
-
def build_compact_element(resource_id, text, content_desc, bounds, likely_click, class_name):
|
|
2296
|
-
"""只返回有值的字段,节省 token"""
|
|
2297
|
-
item = {}
|
|
2298
|
-
if resource_id:
|
|
2299
|
-
# 精简 resource_id,只保留最后一段
|
|
2300
|
-
item['id'] = resource_id.split('/')[-1] if '/' in resource_id else resource_id
|
|
2301
|
-
if text:
|
|
2302
|
-
item['text'] = text
|
|
2303
|
-
if content_desc:
|
|
2304
|
-
item['desc'] = content_desc
|
|
2305
|
-
if bounds:
|
|
2306
|
-
item['bounds'] = bounds
|
|
2307
|
-
if likely_click:
|
|
2308
|
-
item['click'] = True # 启发式判断可点击
|
|
2309
|
-
# class 精简:只保留关键类型
|
|
2310
|
-
if class_name in ('EditText', 'TextInput', 'Button', 'ImageButton', 'CheckBox', 'Switch'):
|
|
2311
|
-
item['type'] = class_name
|
|
2312
|
-
return item
|
|
2313
|
-
|
|
2314
2431
|
result = []
|
|
2315
2432
|
for elem in elements:
|
|
2316
2433
|
# 获取元素属性
|
|
@@ -2330,11 +2447,14 @@ class BasicMobileToolsLite:
|
|
|
2330
2447
|
|
|
2331
2448
|
# 2. 检查是否是功能控件(直接保留)
|
|
2332
2449
|
if class_name in FUNCTIONAL_WIDGETS:
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2450
|
+
result.append({
|
|
2451
|
+
'resource_id': resource_id,
|
|
2452
|
+
'text': text,
|
|
2453
|
+
'content_desc': content_desc,
|
|
2454
|
+
'bounds': bounds,
|
|
2455
|
+
'clickable': clickable,
|
|
2456
|
+
'class': class_name
|
|
2457
|
+
})
|
|
2338
2458
|
continue
|
|
2339
2459
|
|
|
2340
2460
|
# 3. 检查是否是容器控件
|
|
@@ -2347,10 +2467,14 @@ class BasicMobileToolsLite:
|
|
|
2347
2467
|
# 所有属性都是默认值,过滤掉
|
|
2348
2468
|
continue
|
|
2349
2469
|
# 有业务ID或其他有意义属性,保留
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2470
|
+
result.append({
|
|
2471
|
+
'resource_id': resource_id,
|
|
2472
|
+
'text': text,
|
|
2473
|
+
'content_desc': content_desc,
|
|
2474
|
+
'bounds': bounds,
|
|
2475
|
+
'clickable': clickable,
|
|
2476
|
+
'class': class_name
|
|
2477
|
+
})
|
|
2354
2478
|
continue
|
|
2355
2479
|
|
|
2356
2480
|
# 4. 检查是否是装饰类控件
|
|
@@ -2367,21 +2491,14 @@ class BasicMobileToolsLite:
|
|
|
2367
2491
|
continue
|
|
2368
2492
|
|
|
2369
2493
|
# 6. 其他情况:有意义的元素保留
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
# 仅在用户明确设置 MAX_ELEMENTS_RETURN 时才截断
|
|
2378
|
-
truncated = result[:MAX_ELEMENTS]
|
|
2379
|
-
truncated.append({
|
|
2380
|
-
'_truncated': True,
|
|
2381
|
-
'_total': len(result),
|
|
2382
|
-
'_shown': MAX_ELEMENTS
|
|
2494
|
+
result.append({
|
|
2495
|
+
'resource_id': resource_id,
|
|
2496
|
+
'text': text,
|
|
2497
|
+
'content_desc': content_desc,
|
|
2498
|
+
'bounds': bounds,
|
|
2499
|
+
'clickable': clickable,
|
|
2500
|
+
'class': class_name
|
|
2383
2501
|
})
|
|
2384
|
-
return truncated
|
|
2385
2502
|
|
|
2386
2503
|
return result
|
|
2387
2504
|
except Exception as e:
|
|
@@ -2418,68 +2535,6 @@ class BasicMobileToolsLite:
|
|
|
2418
2535
|
|
|
2419
2536
|
return True
|
|
2420
2537
|
|
|
2421
|
-
def _is_likely_clickable(self, class_name: str, resource_id: str, text: str,
|
|
2422
|
-
content_desc: str, clickable: bool, bounds: str) -> bool:
|
|
2423
|
-
"""
|
|
2424
|
-
启发式判断元素是否可能可点击
|
|
2425
|
-
|
|
2426
|
-
Android 的 clickable 属性经常不准确,因为:
|
|
2427
|
-
1. 点击事件可能设置在父容器上
|
|
2428
|
-
2. 使用 onTouchListener 而不是 onClick
|
|
2429
|
-
3. RecyclerView item 通过 ItemClickListener 处理
|
|
2430
|
-
|
|
2431
|
-
此方法通过多种规则推断元素的真实可点击性
|
|
2432
|
-
"""
|
|
2433
|
-
# 规则1:clickable=true 肯定可点击
|
|
2434
|
-
if clickable:
|
|
2435
|
-
return True
|
|
2436
|
-
|
|
2437
|
-
# 规则2:特定类型的控件通常可点击
|
|
2438
|
-
TYPICALLY_CLICKABLE = {
|
|
2439
|
-
'Button', 'ImageButton', 'CheckBox', 'RadioButton', 'Switch',
|
|
2440
|
-
'ToggleButton', 'FloatingActionButton', 'Chip', 'TabView',
|
|
2441
|
-
'EditText', 'TextInput', # 输入框可点击获取焦点
|
|
2442
|
-
}
|
|
2443
|
-
if class_name in TYPICALLY_CLICKABLE:
|
|
2444
|
-
return True
|
|
2445
|
-
|
|
2446
|
-
# 规则3:resource_id 包含可点击关键词
|
|
2447
|
-
if resource_id:
|
|
2448
|
-
id_lower = resource_id.lower()
|
|
2449
|
-
CLICK_KEYWORDS = [
|
|
2450
|
-
'btn', 'button', 'click', 'tap', 'submit', 'confirm',
|
|
2451
|
-
'cancel', 'close', 'back', 'next', 'prev', 'more',
|
|
2452
|
-
'action', 'link', 'menu', 'tab', 'item', 'cell',
|
|
2453
|
-
'card', 'avatar', 'icon', 'entry', 'option', 'arrow'
|
|
2454
|
-
]
|
|
2455
|
-
for kw in CLICK_KEYWORDS:
|
|
2456
|
-
if kw in id_lower:
|
|
2457
|
-
return True
|
|
2458
|
-
|
|
2459
|
-
# 规则4:content_desc 包含可点击暗示
|
|
2460
|
-
if content_desc:
|
|
2461
|
-
desc_lower = content_desc.lower()
|
|
2462
|
-
CLICK_HINTS = ['点击', '按钮', '关闭', '返回', '更多', 'click', 'tap', 'button', 'close']
|
|
2463
|
-
for hint in CLICK_HINTS:
|
|
2464
|
-
if hint in desc_lower:
|
|
2465
|
-
return True
|
|
2466
|
-
|
|
2467
|
-
# 规则5:有 resource_id 或 content_desc 的小图标可能可点击
|
|
2468
|
-
# (纯 ImageView 不加判断,误判率太高)
|
|
2469
|
-
if class_name in ('ImageView', 'Image') and (resource_id or content_desc) and bounds:
|
|
2470
|
-
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
|
|
2471
|
-
if match:
|
|
2472
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
2473
|
-
w, h = x2 - x1, y2 - y1
|
|
2474
|
-
# 小图标(20-100px)更可能是按钮
|
|
2475
|
-
if 20 <= w <= 100 and 20 <= h <= 100:
|
|
2476
|
-
return True
|
|
2477
|
-
|
|
2478
|
-
# 规则6:移除(TextView 误判率太高,只依赖上面的规则)
|
|
2479
|
-
# 如果有 clickable=true 或 ID/desc 中有关键词,前面的规则已经覆盖
|
|
2480
|
-
|
|
2481
|
-
return False
|
|
2482
|
-
|
|
2483
2538
|
def find_close_button(self) -> Dict:
|
|
2484
2539
|
"""智能查找关闭按钮(不点击,只返回位置)
|
|
2485
2540
|
|
|
@@ -2493,7 +2548,7 @@ class BasicMobileToolsLite:
|
|
|
2493
2548
|
import re
|
|
2494
2549
|
|
|
2495
2550
|
if self._is_ios():
|
|
2496
|
-
return {"success": False, "
|
|
2551
|
+
return {"success": False, "message": "iOS 暂不支持,请使用截图+坐标点击"}
|
|
2497
2552
|
|
|
2498
2553
|
# 获取屏幕尺寸
|
|
2499
2554
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
@@ -2504,14 +2559,6 @@ class BasicMobileToolsLite:
|
|
|
2504
2559
|
import xml.etree.ElementTree as ET
|
|
2505
2560
|
root = ET.fromstring(xml_string)
|
|
2506
2561
|
|
|
2507
|
-
# 🔴 先检测是否有弹窗,避免误识别普通页面的按钮
|
|
2508
|
-
popup_bounds, popup_confidence = self._detect_popup_with_confidence(
|
|
2509
|
-
root, screen_width, screen_height
|
|
2510
|
-
)
|
|
2511
|
-
|
|
2512
|
-
if popup_bounds is None or popup_confidence < 0.5:
|
|
2513
|
-
return {"success": True, "popup": False}
|
|
2514
|
-
|
|
2515
2562
|
# 关闭按钮特征
|
|
2516
2563
|
close_texts = ['×', 'X', 'x', '关闭', '取消', 'close', 'Close', '跳过', '知道了', '我知道了']
|
|
2517
2564
|
candidates = []
|
|
@@ -2613,16 +2660,27 @@ class BasicMobileToolsLite:
|
|
|
2613
2660
|
candidates.sort(key=lambda x: x['score'], reverse=True)
|
|
2614
2661
|
best = candidates[0]
|
|
2615
2662
|
|
|
2616
|
-
# Token 优化:只返回最必要的信息
|
|
2617
2663
|
return {
|
|
2618
2664
|
"success": True,
|
|
2619
|
-
"
|
|
2620
|
-
"
|
|
2621
|
-
|
|
2665
|
+
"message": f"✅ 找到可能的关闭按钮",
|
|
2666
|
+
"best_candidate": {
|
|
2667
|
+
"reason": best['reason'],
|
|
2668
|
+
"center": {"x": best['center_x'], "y": best['center_y']},
|
|
2669
|
+
"percent": {"x": best['x_percent'], "y": best['y_percent']},
|
|
2670
|
+
"bounds": best['bounds'],
|
|
2671
|
+
"size": best['size'],
|
|
2672
|
+
"score": best['score']
|
|
2673
|
+
},
|
|
2674
|
+
"click_command": f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})",
|
|
2675
|
+
"other_candidates": [
|
|
2676
|
+
{"reason": c['reason'], "percent": f"({c['x_percent']}%, {c['y_percent']}%)", "score": c['score']}
|
|
2677
|
+
for c in candidates[1:4]
|
|
2678
|
+
] if len(candidates) > 1 else [],
|
|
2679
|
+
"screen_size": {"width": screen_width, "height": screen_height}
|
|
2622
2680
|
}
|
|
2623
2681
|
|
|
2624
2682
|
except Exception as e:
|
|
2625
|
-
return {"success": False, "
|
|
2683
|
+
return {"success": False, "message": f"❌ 查找关闭按钮失败: {e}"}
|
|
2626
2684
|
|
|
2627
2685
|
def close_popup(self) -> Dict:
|
|
2628
2686
|
"""智能关闭弹窗(改进版)
|
|
@@ -2645,7 +2703,7 @@ class BasicMobileToolsLite:
|
|
|
2645
2703
|
|
|
2646
2704
|
# 获取屏幕尺寸
|
|
2647
2705
|
if self._is_ios():
|
|
2648
|
-
return {"success": False, "
|
|
2706
|
+
return {"success": False, "message": "iOS 暂不支持,请使用截图+坐标点击"}
|
|
2649
2707
|
|
|
2650
2708
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
2651
2709
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
@@ -2673,11 +2731,6 @@ class BasicMobileToolsLite:
|
|
|
2673
2731
|
# 如果置信度不够高,记录但继续尝试查找关闭按钮
|
|
2674
2732
|
popup_detected = popup_bounds is not None and popup_confidence >= 0.6
|
|
2675
2733
|
|
|
2676
|
-
# 🔴 关键检查:如果没有检测到弹窗区域,直接返回"无弹窗"
|
|
2677
|
-
# 避免误点击普通页面上的"关闭"、"取消"等按钮
|
|
2678
|
-
if not popup_detected:
|
|
2679
|
-
return {"success": True, "popup": False}
|
|
2680
|
-
|
|
2681
2734
|
# ===== 第二步:在弹窗范围内查找关闭按钮 =====
|
|
2682
2735
|
for idx, elem in enumerate(all_elements):
|
|
2683
2736
|
text = elem.attrib.get('text', '')
|
|
@@ -2815,9 +2868,86 @@ class BasicMobileToolsLite:
|
|
|
2815
2868
|
pass
|
|
2816
2869
|
|
|
2817
2870
|
if not close_candidates:
|
|
2871
|
+
# 如果检测到高置信度的弹窗区域,先尝试点击常见的关闭按钮位置
|
|
2818
2872
|
if popup_detected and popup_bounds:
|
|
2819
|
-
|
|
2820
|
-
|
|
2873
|
+
px1, py1, px2, py2 = popup_bounds
|
|
2874
|
+
popup_width = px2 - px1
|
|
2875
|
+
popup_height = py2 - py1
|
|
2876
|
+
|
|
2877
|
+
# 【优化】X按钮有三种常见位置:
|
|
2878
|
+
# 1. 弹窗内靠近顶部边界(内嵌X按钮)- 最常见
|
|
2879
|
+
# 2. 弹窗边界上方(浮动X按钮)
|
|
2880
|
+
# 3. 弹窗正下方(底部关闭按钮)
|
|
2881
|
+
offset_x = max(60, int(popup_width * 0.07)) # 宽度7%
|
|
2882
|
+
offset_y_above = max(35, int(popup_height * 0.025)) # 高度2.5%,在边界之上
|
|
2883
|
+
offset_y_near = max(45, int(popup_height * 0.03)) # 高度3%,紧贴顶边界内侧
|
|
2884
|
+
|
|
2885
|
+
try_positions = [
|
|
2886
|
+
# 【最高优先级】弹窗内紧贴顶部边界
|
|
2887
|
+
(px2 - offset_x, py1 + offset_y_near, "弹窗右上角"),
|
|
2888
|
+
# 弹窗边界上方(浮动X按钮)
|
|
2889
|
+
(px2 - offset_x, py1 - offset_y_above, "弹窗右上浮"),
|
|
2890
|
+
# 弹窗正下方中间(底部关闭按钮)
|
|
2891
|
+
((px1 + px2) // 2, py2 + max(50, int(popup_height * 0.04)), "弹窗下方中间"),
|
|
2892
|
+
# 弹窗正上方中间
|
|
2893
|
+
((px1 + px2) // 2, py1 - 40, "弹窗正上方"),
|
|
2894
|
+
]
|
|
2895
|
+
|
|
2896
|
+
for try_x, try_y, position_name in try_positions:
|
|
2897
|
+
if 0 <= try_x <= screen_width and 0 <= try_y <= screen_height:
|
|
2898
|
+
self.client.u2.click(try_x, try_y)
|
|
2899
|
+
time.sleep(0.3)
|
|
2900
|
+
|
|
2901
|
+
# 🎯 关键步骤:检查应用是否跳转,如果跳转说明弹窗去除失败,需要返回目标应用
|
|
2902
|
+
app_check = self._check_app_switched()
|
|
2903
|
+
return_result = None
|
|
2904
|
+
|
|
2905
|
+
if app_check['switched']:
|
|
2906
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
2907
|
+
return_result = self._return_to_target_app()
|
|
2908
|
+
|
|
2909
|
+
# 尝试后截图,让 AI 判断是否成功
|
|
2910
|
+
screenshot_result = self.take_screenshot("尝试关闭后")
|
|
2911
|
+
|
|
2912
|
+
msg = f"✅ 已尝试点击常见关闭按钮位置"
|
|
2913
|
+
if app_check['switched']:
|
|
2914
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
2915
|
+
if return_result:
|
|
2916
|
+
if return_result['success']:
|
|
2917
|
+
msg += f"\n{return_result['message']}"
|
|
2918
|
+
else:
|
|
2919
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
2920
|
+
|
|
2921
|
+
return {
|
|
2922
|
+
"success": True,
|
|
2923
|
+
"message": msg,
|
|
2924
|
+
"tried_positions": [p[2] for p in try_positions],
|
|
2925
|
+
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2926
|
+
"app_check": app_check,
|
|
2927
|
+
"return_to_app": return_result,
|
|
2928
|
+
"tip": "请查看截图确认弹窗是否已关闭。如果还在,可手动分析截图找到关闭按钮位置。"
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
# 没有检测到弹窗区域,截图让 AI 分析
|
|
2932
|
+
screenshot_result = self.take_screenshot(description="页面截图", compress=True)
|
|
2933
|
+
|
|
2934
|
+
return {
|
|
2935
|
+
"success": False,
|
|
2936
|
+
"message": "❌ 未检测到弹窗区域,已截图供 AI 分析",
|
|
2937
|
+
"action_required": "请查看截图找到关闭按钮,调用 mobile_click_at_coords 点击",
|
|
2938
|
+
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2939
|
+
"screen_size": {"width": screen_width, "height": screen_height},
|
|
2940
|
+
"image_size": {
|
|
2941
|
+
"width": screenshot_result.get("image_width", screen_width),
|
|
2942
|
+
"height": screenshot_result.get("image_height", screen_height)
|
|
2943
|
+
},
|
|
2944
|
+
"original_size": {
|
|
2945
|
+
"width": screenshot_result.get("original_img_width", screen_width),
|
|
2946
|
+
"height": screenshot_result.get("original_img_height", screen_height)
|
|
2947
|
+
},
|
|
2948
|
+
"search_areas": ["弹窗右上角", "弹窗正上方", "弹窗下方中间", "屏幕右上角"],
|
|
2949
|
+
"time_warning": "⚠️ 截图分析期间弹窗可能自动消失。如果是定时弹窗,建议等待其自动消失。"
|
|
2950
|
+
}
|
|
2821
2951
|
|
|
2822
2952
|
# 按得分排序,取最可能的
|
|
2823
2953
|
close_candidates.sort(key=lambda x: x['score'], reverse=True)
|
|
@@ -2835,22 +2965,62 @@ class BasicMobileToolsLite:
|
|
|
2835
2965
|
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
2836
2966
|
return_result = self._return_to_target_app()
|
|
2837
2967
|
|
|
2838
|
-
#
|
|
2839
|
-
self.
|
|
2840
|
-
|
|
2841
|
-
|
|
2968
|
+
# 点击后截图,让 AI 判断是否成功
|
|
2969
|
+
screenshot_result = self.take_screenshot("关闭弹窗后")
|
|
2970
|
+
|
|
2971
|
+
# 记录操作(使用百分比,跨设备兼容)
|
|
2972
|
+
self._record_operation(
|
|
2973
|
+
'click',
|
|
2974
|
+
x=best['center_x'],
|
|
2975
|
+
y=best['center_y'],
|
|
2976
|
+
x_percent=best['x_percent'],
|
|
2977
|
+
y_percent=best['y_percent'],
|
|
2978
|
+
screen_width=screen_width,
|
|
2979
|
+
screen_height=screen_height,
|
|
2980
|
+
ref=f"close_popup_{best['position']}"
|
|
2981
|
+
)
|
|
2842
2982
|
|
|
2843
|
-
#
|
|
2844
|
-
|
|
2983
|
+
# 构建返回消息
|
|
2984
|
+
msg = f"✅ 已点击关闭按钮 ({best['position']}): ({best['center_x']}, {best['center_y']})"
|
|
2845
2985
|
if app_check['switched']:
|
|
2846
|
-
|
|
2986
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
2847
2987
|
if return_result:
|
|
2848
|
-
|
|
2988
|
+
if return_result['success']:
|
|
2989
|
+
msg += f"\n{return_result['message']}"
|
|
2990
|
+
else:
|
|
2991
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
2849
2992
|
|
|
2850
|
-
|
|
2993
|
+
# 返回候选按钮列表,让 AI 看截图判断
|
|
2994
|
+
# 如果弹窗还在,AI 可以选择点击其他候选按钮
|
|
2995
|
+
return {
|
|
2996
|
+
"success": True,
|
|
2997
|
+
"message": msg,
|
|
2998
|
+
"clicked": {
|
|
2999
|
+
"position": best['position'],
|
|
3000
|
+
"match_type": best['match_type'],
|
|
3001
|
+
"coords": (best['center_x'], best['center_y']),
|
|
3002
|
+
"percent": (best['x_percent'], best['y_percent'])
|
|
3003
|
+
},
|
|
3004
|
+
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
3005
|
+
"popup_detected": popup_detected,
|
|
3006
|
+
"popup_confidence": popup_confidence if popup_bounds else 0,
|
|
3007
|
+
"popup_bounds": f"[{popup_bounds[0]},{popup_bounds[1]}][{popup_bounds[2]},{popup_bounds[3]}]" if popup_detected else None,
|
|
3008
|
+
"app_check": app_check,
|
|
3009
|
+
"return_to_app": return_result,
|
|
3010
|
+
"other_candidates": [
|
|
3011
|
+
{
|
|
3012
|
+
"position": c['position'],
|
|
3013
|
+
"type": c['match_type'],
|
|
3014
|
+
"coords": (c['center_x'], c['center_y']),
|
|
3015
|
+
"percent": (c['x_percent'], c['y_percent'])
|
|
3016
|
+
}
|
|
3017
|
+
for c in close_candidates[1:4] # 返回其他3个候选,AI 可以选择
|
|
3018
|
+
],
|
|
3019
|
+
"tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates 中的其他位置"
|
|
3020
|
+
}
|
|
2851
3021
|
|
|
2852
3022
|
except Exception as e:
|
|
2853
|
-
return {"success": False, "
|
|
3023
|
+
return {"success": False, "message": f"❌ 关闭弹窗失败: {e}"}
|
|
2854
3024
|
|
|
2855
3025
|
def _get_position_name(self, rel_x: float, rel_y: float) -> str:
|
|
2856
3026
|
"""根据相对坐标获取位置名称"""
|
|
@@ -3751,28 +3921,10 @@ class BasicMobileToolsLite:
|
|
|
3751
3921
|
try:
|
|
3752
3922
|
import xml.etree.ElementTree as ET
|
|
3753
3923
|
|
|
3754
|
-
# ========== 第
|
|
3924
|
+
# ========== 第1步:控件树查找关闭按钮 ==========
|
|
3755
3925
|
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
3756
3926
|
root = ET.fromstring(xml_string)
|
|
3757
3927
|
|
|
3758
|
-
screen_width = self.client.u2.info.get('displayWidth', 1440)
|
|
3759
|
-
screen_height = self.client.u2.info.get('displayHeight', 3200)
|
|
3760
|
-
|
|
3761
|
-
popup_bounds, popup_confidence = self._detect_popup_with_confidence(
|
|
3762
|
-
root, screen_width, screen_height
|
|
3763
|
-
)
|
|
3764
|
-
|
|
3765
|
-
# 如果没有检测到弹窗,直接返回"无弹窗"
|
|
3766
|
-
if popup_bounds is None or popup_confidence < 0.5:
|
|
3767
|
-
result["success"] = True
|
|
3768
|
-
result["method"] = None
|
|
3769
|
-
result["message"] = "ℹ️ 当前页面未检测到弹窗,无需关闭"
|
|
3770
|
-
result["popup_detected"] = False
|
|
3771
|
-
result["popup_confidence"] = popup_confidence
|
|
3772
|
-
return result
|
|
3773
|
-
|
|
3774
|
-
# ========== 第1步:控件树查找关闭按钮 ==========
|
|
3775
|
-
|
|
3776
3928
|
# 关闭按钮的常见特征
|
|
3777
3929
|
close_keywords = ['关闭', '跳过', '×', 'X', 'x', 'close', 'skip', '取消']
|
|
3778
3930
|
close_content_desc = ['关闭', '跳过', 'close', 'skip', 'dismiss']
|
|
@@ -3851,6 +4003,12 @@ class BasicMobileToolsLite:
|
|
|
3851
4003
|
cx, cy = best['center']
|
|
3852
4004
|
bounds = best['bounds']
|
|
3853
4005
|
|
|
4006
|
+
# 点击前截图(用于自动学习)
|
|
4007
|
+
pre_screenshot = None
|
|
4008
|
+
if auto_learn:
|
|
4009
|
+
pre_result = self.take_screenshot(description="关闭前", compress=False)
|
|
4010
|
+
pre_screenshot = pre_result.get("screenshot_path")
|
|
4011
|
+
|
|
3854
4012
|
# 点击(click_at_coords 内部已包含应用状态检查和自动返回)
|
|
3855
4013
|
click_result = self.click_at_coords(cx, cy)
|
|
3856
4014
|
time.sleep(0.5)
|
|
@@ -3880,11 +4038,17 @@ class BasicMobileToolsLite:
|
|
|
3880
4038
|
result["message"] = msg
|
|
3881
4039
|
result["app_check"] = app_check
|
|
3882
4040
|
result["return_to_app"] = return_result
|
|
3883
|
-
|
|
4041
|
+
|
|
4042
|
+
# 自动学习:检查这个 X 是否已在模板库,不在就添加
|
|
4043
|
+
if auto_learn and pre_screenshot:
|
|
4044
|
+
learn_result = self._auto_learn_template(pre_screenshot, bounds)
|
|
4045
|
+
if learn_result:
|
|
4046
|
+
result["learned_template"] = learn_result
|
|
4047
|
+
result["message"] += f"\n📚 自动学习: {learn_result}"
|
|
3884
4048
|
|
|
3885
4049
|
return result
|
|
3886
4050
|
|
|
3887
|
-
# ========== 第2
|
|
4051
|
+
# ========== 第2步:模板匹配 ==========
|
|
3888
4052
|
screenshot_path = None
|
|
3889
4053
|
try:
|
|
3890
4054
|
from .template_matcher import TemplateMatcher
|
|
@@ -3903,14 +4067,16 @@ class BasicMobileToolsLite:
|
|
|
3903
4067
|
x_pct = best["percent"]["x"]
|
|
3904
4068
|
y_pct = best["percent"]["y"]
|
|
3905
4069
|
|
|
3906
|
-
#
|
|
4070
|
+
# 点击(click_by_percent 内部已包含应用状态检查和自动返回)
|
|
3907
4071
|
click_result = self.click_by_percent(x_pct, y_pct)
|
|
3908
4072
|
time.sleep(0.5)
|
|
3909
4073
|
|
|
4074
|
+
# 🎯 再次检查应用状态(确保弹窗去除没有导致应用跳转)
|
|
3910
4075
|
app_check = self._check_app_switched()
|
|
3911
4076
|
return_result = None
|
|
3912
4077
|
|
|
3913
4078
|
if app_check['switched']:
|
|
4079
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
3914
4080
|
return_result = self._return_to_target_app()
|
|
3915
4081
|
|
|
3916
4082
|
result["success"] = True
|
|
@@ -3921,9 +4087,12 @@ class BasicMobileToolsLite:
|
|
|
3921
4087
|
f" 位置: ({x_pct:.1f}%, {y_pct:.1f}%)"
|
|
3922
4088
|
|
|
3923
4089
|
if app_check['switched']:
|
|
3924
|
-
msg += f"\n⚠️
|
|
4090
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
3925
4091
|
if return_result:
|
|
3926
|
-
|
|
4092
|
+
if return_result['success']:
|
|
4093
|
+
msg += f"\n{return_result['message']}"
|
|
4094
|
+
else:
|
|
4095
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
3927
4096
|
|
|
3928
4097
|
result["message"] = msg
|
|
3929
4098
|
result["app_check"] = app_check
|
|
@@ -3935,12 +4104,17 @@ class BasicMobileToolsLite:
|
|
|
3935
4104
|
except Exception:
|
|
3936
4105
|
pass # 模板匹配失败,继续下一步
|
|
3937
4106
|
|
|
3938
|
-
# ========== 第3
|
|
4107
|
+
# ========== 第3步:返回截图供 AI 分析 ==========
|
|
4108
|
+
if not screenshot_path:
|
|
4109
|
+
screenshot_result = self.take_screenshot(description="需要AI分析", compress=True)
|
|
4110
|
+
|
|
3939
4111
|
result["success"] = False
|
|
3940
|
-
result["fallback"] = "vision"
|
|
3941
4112
|
result["method"] = None
|
|
3942
|
-
result["
|
|
3943
|
-
|
|
4113
|
+
result["message"] = "❌ 控件树和模板匹配都未找到关闭按钮\n" \
|
|
4114
|
+
"📸 已截图,请 AI 分析图片中的 X 按钮位置\n" \
|
|
4115
|
+
"💡 找到后使用 mobile_click_by_percent(x%, y%) 点击"
|
|
4116
|
+
result["screenshot"] = screenshot_result if not screenshot_path else {"screenshot_path": screenshot_path}
|
|
4117
|
+
result["need_ai_analysis"] = True
|
|
3944
4118
|
|
|
3945
4119
|
return result
|
|
3946
4120
|
|