mobile-mcp-ai 2.6.5__py3-none-any.whl → 2.6.7__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 +575 -385
- mobile_mcp/mcp_tools/mcp_server.py +345 -241
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/METADATA +1 -1
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/RECORD +9 -9
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.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,13 +1236,15 @@ 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:
|
|
1195
1245
|
return {"success": False, "message": f"❌ 百分比点击失败: {e}"}
|
|
1196
1246
|
|
|
1197
|
-
def click_by_text(self, text: str, timeout: float = 3.0, position: Optional[str] = None
|
|
1198
|
-
verify: Optional[str] = None) -> Dict:
|
|
1247
|
+
def click_by_text(self, text: str, timeout: float = 3.0, position: Optional[str] = None) -> Dict:
|
|
1199
1248
|
"""通过文本点击 - 先查 XML 树,再精准匹配
|
|
1200
1249
|
|
|
1201
1250
|
Args:
|
|
@@ -1204,7 +1253,6 @@ class BasicMobileToolsLite:
|
|
|
1204
1253
|
position: 位置信息,当有多个相同文案时使用。支持:
|
|
1205
1254
|
- 垂直方向: "top"/"upper"/"上", "bottom"/"lower"/"下", "middle"/"center"/"中"
|
|
1206
1255
|
- 水平方向: "left"/"左", "right"/"右", "center"/"中"
|
|
1207
|
-
verify: 可选,点击后验证的文本。如果指定,会检查该文本是否出现在页面上
|
|
1208
1256
|
"""
|
|
1209
1257
|
try:
|
|
1210
1258
|
if self._is_ios():
|
|
@@ -1216,17 +1264,10 @@ class BasicMobileToolsLite:
|
|
|
1216
1264
|
if elem.exists:
|
|
1217
1265
|
elem.click()
|
|
1218
1266
|
time.sleep(0.3)
|
|
1267
|
+
# 使用标准记录格式
|
|
1219
1268
|
self._record_click('text', text, element_desc=text, locator_attr='text')
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
return self._verify_after_click(verify, ios=True)
|
|
1223
|
-
# 返回页面文本摘要,方便确认页面变化
|
|
1224
|
-
page_texts = self._get_page_texts(10)
|
|
1225
|
-
return {"success": True, "page_texts": page_texts}
|
|
1226
|
-
# 控件树找不到,提示用视觉识别
|
|
1227
|
-
return {"success": False, "fallback": "vision", "msg": f"未找到'{text}',用截图点击"}
|
|
1228
|
-
else:
|
|
1229
|
-
return {"success": False, "msg": "iOS未初始化"}
|
|
1269
|
+
return {"success": True, "message": f"✅ 点击成功: '{text}'"}
|
|
1270
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1230
1271
|
else:
|
|
1231
1272
|
# 获取屏幕尺寸用于计算百分比
|
|
1232
1273
|
screen_width, screen_height = self.client.u2.window_size()
|
|
@@ -1247,20 +1288,17 @@ class BasicMobileToolsLite:
|
|
|
1247
1288
|
x_pct = round(cx / screen_width * 100, 1)
|
|
1248
1289
|
y_pct = round(cy / screen_height * 100, 1)
|
|
1249
1290
|
|
|
1250
|
-
#
|
|
1291
|
+
# 如果有位置参数,直接使用坐标点击(避免 u2 选择器匹配到错误的元素)
|
|
1251
1292
|
if position and bounds:
|
|
1252
1293
|
x = (bounds[0] + bounds[2]) // 2
|
|
1253
1294
|
y = (bounds[1] + bounds[3]) // 2
|
|
1254
1295
|
self.client.u2.click(x, y)
|
|
1255
1296
|
time.sleep(0.3)
|
|
1297
|
+
position_info = f" ({position})" if position else ""
|
|
1298
|
+
# 虽然用坐标点击,但记录时仍使用文本定位(脚本更稳定)
|
|
1256
1299
|
self._record_click('text', attr_value, x_pct, y_pct,
|
|
1257
|
-
element_desc=f"{text}
|
|
1258
|
-
|
|
1259
|
-
if verify:
|
|
1260
|
-
return self._verify_after_click(verify)
|
|
1261
|
-
# 返回页面文本摘要
|
|
1262
|
-
page_texts = self._get_page_texts(10)
|
|
1263
|
-
return {"success": True, "page_texts": page_texts}
|
|
1300
|
+
element_desc=f"{text}{position_info}", locator_attr=attr_type)
|
|
1301
|
+
return {"success": True, "message": f"✅ 点击成功(坐标定位): '{text}'{position_info} @ ({x},{y})"}
|
|
1264
1302
|
|
|
1265
1303
|
# 没有位置参数时,使用选择器定位
|
|
1266
1304
|
if attr_type == 'text':
|
|
@@ -1277,74 +1315,27 @@ class BasicMobileToolsLite:
|
|
|
1277
1315
|
if elem and elem.exists(timeout=1):
|
|
1278
1316
|
elem.click()
|
|
1279
1317
|
time.sleep(0.3)
|
|
1318
|
+
position_info = f" ({position})" if position else ""
|
|
1319
|
+
# 使用标准记录格式:文本定位
|
|
1280
1320
|
self._record_click('text', attr_value, x_pct, y_pct,
|
|
1281
1321
|
element_desc=text, locator_attr=attr_type)
|
|
1282
|
-
|
|
1283
|
-
if verify:
|
|
1284
|
-
return self._verify_after_click(verify)
|
|
1285
|
-
# 返回页面文本摘要
|
|
1286
|
-
page_texts = self._get_page_texts(10)
|
|
1287
|
-
return {"success": True, "page_texts": page_texts}
|
|
1322
|
+
return {"success": True, "message": f"✅ 点击成功({attr_type}): '{text}'{position_info}"}
|
|
1288
1323
|
|
|
1289
|
-
#
|
|
1324
|
+
# 如果选择器失败,用坐标兜底
|
|
1290
1325
|
if bounds:
|
|
1291
1326
|
x = (bounds[0] + bounds[2]) // 2
|
|
1292
1327
|
y = (bounds[1] + bounds[3]) // 2
|
|
1293
1328
|
self.client.u2.click(x, y)
|
|
1294
1329
|
time.sleep(0.3)
|
|
1330
|
+
position_info = f" ({position})" if position else ""
|
|
1331
|
+
# 选择器失败,用百分比作为兜底
|
|
1295
1332
|
self._record_click('percent', f"{x_pct}%,{y_pct}%", x_pct, y_pct,
|
|
1296
|
-
element_desc=text)
|
|
1297
|
-
|
|
1298
|
-
if verify:
|
|
1299
|
-
return self._verify_after_click(verify)
|
|
1300
|
-
# 返回页面文本摘要
|
|
1301
|
-
page_texts = self._get_page_texts(10)
|
|
1302
|
-
return {"success": True, "page_texts": page_texts}
|
|
1333
|
+
element_desc=f"{text}{position_info}")
|
|
1334
|
+
return {"success": True, "message": f"✅ 点击成功(坐标兜底): '{text}'{position_info} @ ({x},{y})"}
|
|
1303
1335
|
|
|
1304
|
-
|
|
1305
|
-
return {"success": False, "fallback": "vision", "msg": f"未找到'{text}',用截图点击"}
|
|
1306
|
-
except Exception as e:
|
|
1307
|
-
return {"success": False, "msg": str(e)}
|
|
1308
|
-
|
|
1309
|
-
def _verify_after_click(self, verify_text: str, ios: bool = False, timeout: float = 2.0) -> Dict:
|
|
1310
|
-
"""点击后验证期望文本是否出现
|
|
1311
|
-
|
|
1312
|
-
Args:
|
|
1313
|
-
verify_text: 期望出现的文本
|
|
1314
|
-
ios: 是否是 iOS 设备
|
|
1315
|
-
timeout: 验证超时时间
|
|
1316
|
-
|
|
1317
|
-
Returns:
|
|
1318
|
-
{"success": True, "verified": True/False, "hint": "..."}
|
|
1319
|
-
"""
|
|
1320
|
-
time.sleep(0.5) # 等待页面更新
|
|
1321
|
-
|
|
1322
|
-
try:
|
|
1323
|
-
if ios:
|
|
1324
|
-
ios_client = self._get_ios_client()
|
|
1325
|
-
if ios_client and hasattr(ios_client, 'wda'):
|
|
1326
|
-
exists = ios_client.wda(name=verify_text).exists or \
|
|
1327
|
-
ios_client.wda(label=verify_text).exists
|
|
1328
|
-
else:
|
|
1329
|
-
exists = False
|
|
1330
|
-
else:
|
|
1331
|
-
# Android: 检查文本或包含文本
|
|
1332
|
-
exists = self.client.u2(text=verify_text).exists(timeout=timeout) or \
|
|
1333
|
-
self.client.u2(textContains=verify_text).exists(timeout=0.5) or \
|
|
1334
|
-
self.client.u2(description=verify_text).exists(timeout=0.5)
|
|
1335
|
-
|
|
1336
|
-
if exists:
|
|
1337
|
-
return {"success": True, "verified": True}
|
|
1338
|
-
else:
|
|
1339
|
-
# 验证失败,提示可以截图确认
|
|
1340
|
-
return {
|
|
1341
|
-
"success": True, # 点击本身成功
|
|
1342
|
-
"verified": False,
|
|
1343
|
-
"expect": verify_text,
|
|
1344
|
-
"hint": "验证失败,可截图确认"
|
|
1345
|
-
}
|
|
1336
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1346
1337
|
except Exception as e:
|
|
1347
|
-
return {"success":
|
|
1338
|
+
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
1348
1339
|
|
|
1349
1340
|
def _find_element_in_tree(self, text: str, position: Optional[str] = None) -> Optional[Dict]:
|
|
1350
1341
|
"""在 XML 树中查找包含指定文本的元素,优先返回可点击的元素
|
|
@@ -1483,8 +1474,15 @@ class BasicMobileToolsLite:
|
|
|
1483
1474
|
return None
|
|
1484
1475
|
|
|
1485
1476
|
def click_by_id(self, resource_id: str, index: int = 0) -> Dict:
|
|
1486
|
-
"""通过 resource-id
|
|
1477
|
+
"""通过 resource-id 点击(支持点击第 N 个元素)
|
|
1478
|
+
|
|
1479
|
+
Args:
|
|
1480
|
+
resource_id: 元素的 resource-id
|
|
1481
|
+
index: 第几个元素(从 0 开始),默认 0 表示第一个
|
|
1482
|
+
"""
|
|
1487
1483
|
try:
|
|
1484
|
+
index_desc = f"[{index}]" if index > 0 else ""
|
|
1485
|
+
|
|
1488
1486
|
if self._is_ios():
|
|
1489
1487
|
ios_client = self._get_ios_client()
|
|
1490
1488
|
if ios_client and hasattr(ios_client, 'wda'):
|
|
@@ -1492,31 +1490,33 @@ class BasicMobileToolsLite:
|
|
|
1492
1490
|
if not elem.exists:
|
|
1493
1491
|
elem = ios_client.wda(name=resource_id)
|
|
1494
1492
|
if elem.exists:
|
|
1493
|
+
# 获取所有匹配的元素
|
|
1495
1494
|
elements = elem.find_elements()
|
|
1496
1495
|
if index < len(elements):
|
|
1497
1496
|
elements[index].click()
|
|
1498
1497
|
time.sleep(0.3)
|
|
1499
|
-
|
|
1500
|
-
|
|
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}"}
|
|
1501
1501
|
else:
|
|
1502
|
-
return {"success": False, "
|
|
1503
|
-
return {"success": False, "
|
|
1504
|
-
else:
|
|
1505
|
-
return {"success": False, "msg": "iOS未初始化"}
|
|
1502
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {len(elements)} 个元素,但请求索引 {index}"}
|
|
1503
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1506
1504
|
else:
|
|
1507
1505
|
elem = self.client.u2(resourceId=resource_id)
|
|
1508
1506
|
if elem.exists(timeout=0.5):
|
|
1507
|
+
# 获取匹配元素数量
|
|
1509
1508
|
count = elem.count
|
|
1510
1509
|
if index < count:
|
|
1511
1510
|
elem[index].click()
|
|
1512
1511
|
time.sleep(0.3)
|
|
1513
|
-
|
|
1514
|
-
|
|
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 "")}
|
|
1515
1515
|
else:
|
|
1516
|
-
return {"success": False, "
|
|
1517
|
-
return {"success": False, "
|
|
1516
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {count} 个元素,但请求索引 {index}"}
|
|
1517
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1518
1518
|
except Exception as e:
|
|
1519
|
-
return {"success": False, "
|
|
1519
|
+
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
1520
1520
|
|
|
1521
1521
|
# ==================== 长按操作 ====================
|
|
1522
1522
|
|
|
@@ -1550,7 +1550,7 @@ class BasicMobileToolsLite:
|
|
|
1550
1550
|
size = ios_client.wda.window_size()
|
|
1551
1551
|
screen_width, screen_height = size[0], size[1]
|
|
1552
1552
|
else:
|
|
1553
|
-
return {"success": False, "
|
|
1553
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
1554
1554
|
else:
|
|
1555
1555
|
info = self.client.u2.info
|
|
1556
1556
|
screen_width = info.get('displayWidth', 0)
|
|
@@ -1603,11 +1603,23 @@ class BasicMobileToolsLite:
|
|
|
1603
1603
|
|
|
1604
1604
|
if converted:
|
|
1605
1605
|
if conversion_type == "crop_offset":
|
|
1606
|
-
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
|
+
}
|
|
1607
1611
|
else:
|
|
1608
|
-
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
|
+
}
|
|
1609
1618
|
else:
|
|
1610
|
-
return {
|
|
1619
|
+
return {
|
|
1620
|
+
"success": True,
|
|
1621
|
+
"message": f"✅ 长按成功: ({x}, {y}) 持续 {duration}s [相对位置: {x_percent}%, {y_percent}%]"
|
|
1622
|
+
}
|
|
1611
1623
|
except Exception as e:
|
|
1612
1624
|
return {"success": False, "message": f"❌ 长按失败: {e}"}
|
|
1613
1625
|
|
|
@@ -1636,14 +1648,14 @@ class BasicMobileToolsLite:
|
|
|
1636
1648
|
size = ios_client.wda.window_size()
|
|
1637
1649
|
width, height = size[0], size[1]
|
|
1638
1650
|
else:
|
|
1639
|
-
return {"success": False, "
|
|
1651
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
1640
1652
|
else:
|
|
1641
1653
|
info = self.client.u2.info
|
|
1642
1654
|
width = info.get('displayWidth', 0)
|
|
1643
1655
|
height = info.get('displayHeight', 0)
|
|
1644
1656
|
|
|
1645
1657
|
if width == 0 or height == 0:
|
|
1646
|
-
return {"success": False, "
|
|
1658
|
+
return {"success": False, "message": "❌ 无法获取屏幕尺寸"}
|
|
1647
1659
|
|
|
1648
1660
|
# 第2步:百分比转像素坐标
|
|
1649
1661
|
x = int(width * x_percent / 100)
|
|
@@ -1665,7 +1677,13 @@ class BasicMobileToolsLite:
|
|
|
1665
1677
|
self._record_long_press('percent', f"{x_percent}%,{y_percent}%", duration,
|
|
1666
1678
|
x_percent, y_percent, element_desc=f"百分比({x_percent}%,{y_percent}%)")
|
|
1667
1679
|
|
|
1668
|
-
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
|
|
1669
1687
|
}
|
|
1670
1688
|
except Exception as e:
|
|
1671
1689
|
return {"success": False, "message": f"❌ 百分比长按失败: {e}"}
|
|
@@ -1695,8 +1713,8 @@ class BasicMobileToolsLite:
|
|
|
1695
1713
|
ios_client.wda.swipe(x, y, x, y, duration=duration)
|
|
1696
1714
|
time.sleep(0.3)
|
|
1697
1715
|
self._record_long_press('text', text, duration, element_desc=text, locator_attr='text')
|
|
1698
|
-
return {"success": True}
|
|
1699
|
-
return {"success": False, "
|
|
1716
|
+
return {"success": True, "message": f"✅ 长按成功: '{text}' 持续 {duration}s"}
|
|
1717
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1700
1718
|
else:
|
|
1701
1719
|
# 获取屏幕尺寸用于计算百分比
|
|
1702
1720
|
screen_width, screen_height = self.client.u2.window_size()
|
|
@@ -1734,7 +1752,7 @@ class BasicMobileToolsLite:
|
|
|
1734
1752
|
time.sleep(0.3)
|
|
1735
1753
|
self._record_long_press('text', attr_value, duration, x_pct, y_pct,
|
|
1736
1754
|
element_desc=text, locator_attr=attr_type)
|
|
1737
|
-
return {"success": True}
|
|
1755
|
+
return {"success": True, "message": f"✅ 长按成功({attr_type}): '{text}' 持续 {duration}s"}
|
|
1738
1756
|
|
|
1739
1757
|
# 如果选择器失败,用坐标兜底
|
|
1740
1758
|
if bounds:
|
|
@@ -1744,9 +1762,9 @@ class BasicMobileToolsLite:
|
|
|
1744
1762
|
time.sleep(0.3)
|
|
1745
1763
|
self._record_long_press('percent', f"{x_pct}%,{y_pct}%", duration, x_pct, y_pct,
|
|
1746
1764
|
element_desc=text)
|
|
1747
|
-
return {"success": True}
|
|
1765
|
+
return {"success": True, "message": f"✅ 长按成功(坐标兜底): '{text}' @ ({x},{y}) 持续 {duration}s"}
|
|
1748
1766
|
|
|
1749
|
-
return {"success": False, "
|
|
1767
|
+
return {"success": False, "message": f"❌ 文本不存在: {text}"}
|
|
1750
1768
|
except Exception as e:
|
|
1751
1769
|
return {"success": False, "message": f"❌ 长按失败: {e}"}
|
|
1752
1770
|
|
|
@@ -1774,8 +1792,8 @@ class BasicMobileToolsLite:
|
|
|
1774
1792
|
ios_client.wda.swipe(x, y, x, y, duration=duration)
|
|
1775
1793
|
time.sleep(0.3)
|
|
1776
1794
|
self._record_long_press('id', resource_id, duration, element_desc=resource_id)
|
|
1777
|
-
return {"success": True}
|
|
1778
|
-
return {"success": False, "
|
|
1795
|
+
return {"success": True, "message": f"✅ 长按成功: {resource_id} 持续 {duration}s"}
|
|
1796
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1779
1797
|
else:
|
|
1780
1798
|
elem = self.client.u2(resourceId=resource_id)
|
|
1781
1799
|
if elem.exists(timeout=0.5):
|
|
@@ -1783,7 +1801,7 @@ class BasicMobileToolsLite:
|
|
|
1783
1801
|
time.sleep(0.3)
|
|
1784
1802
|
self._record_long_press('id', resource_id, duration, element_desc=resource_id)
|
|
1785
1803
|
return {"success": True, "message": f"✅ 长按成功: {resource_id} 持续 {duration}s"}
|
|
1786
|
-
return {"success": False, "
|
|
1804
|
+
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1787
1805
|
except Exception as e:
|
|
1788
1806
|
return {"success": False, "message": f"❌ 长按失败: {e}"}
|
|
1789
1807
|
|
|
@@ -2040,8 +2058,15 @@ class BasicMobileToolsLite:
|
|
|
2040
2058
|
x_percent = round(x / screen_width * 100, 1) if screen_width > 0 else 0
|
|
2041
2059
|
y_percent = round(y / screen_height * 100, 1) if screen_height > 0 else 0
|
|
2042
2060
|
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
+
)
|
|
2045
2070
|
|
|
2046
2071
|
# 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
|
|
2047
2072
|
app_check = self._check_app_switched()
|
|
@@ -2071,13 +2096,16 @@ class BasicMobileToolsLite:
|
|
|
2071
2096
|
|
|
2072
2097
|
# ==================== 导航操作 ====================
|
|
2073
2098
|
|
|
2074
|
-
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:
|
|
2075
2101
|
"""滑动屏幕
|
|
2076
2102
|
|
|
2077
2103
|
Args:
|
|
2078
2104
|
direction: 滑动方向 (up/down/left/right)
|
|
2079
2105
|
y: 左右滑动时指定的高度坐标(像素)
|
|
2080
2106
|
y_percent: 左右滑动时指定的高度百分比 (0-100)
|
|
2107
|
+
distance: 横向滑动时指定的滑动距离(像素),仅用于 left/right
|
|
2108
|
+
distance_percent: 横向滑动时指定的滑动距离百分比 (0-100),仅用于 left/right
|
|
2081
2109
|
"""
|
|
2082
2110
|
try:
|
|
2083
2111
|
if self._is_ios():
|
|
@@ -2086,7 +2114,7 @@ class BasicMobileToolsLite:
|
|
|
2086
2114
|
size = ios_client.wda.window_size()
|
|
2087
2115
|
width, height = size[0], size[1]
|
|
2088
2116
|
else:
|
|
2089
|
-
return {"success": False, "
|
|
2117
|
+
return {"success": False, "message": "❌ iOS 客户端未初始化"}
|
|
2090
2118
|
else:
|
|
2091
2119
|
width, height = self.client.u2.window_size()
|
|
2092
2120
|
|
|
@@ -2104,20 +2132,53 @@ class BasicMobileToolsLite:
|
|
|
2104
2132
|
swipe_y = y
|
|
2105
2133
|
else:
|
|
2106
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
|
|
2107
2172
|
else:
|
|
2108
2173
|
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]
|
|
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]
|
|
2121
2182
|
|
|
2122
2183
|
if self._is_ios():
|
|
2123
2184
|
ios_client.wda.swipe(x1, y1, x2, y2)
|
|
@@ -2138,10 +2199,21 @@ class BasicMobileToolsLite:
|
|
|
2138
2199
|
# 构建返回消息
|
|
2139
2200
|
msg = f"✅ 滑动成功: {direction}"
|
|
2140
2201
|
if direction in ['left', 'right']:
|
|
2202
|
+
msg_parts = []
|
|
2141
2203
|
if y_percent is not None:
|
|
2142
|
-
|
|
2204
|
+
msg_parts.append(f"高度: {y_percent}% = {swipe_y}px")
|
|
2143
2205
|
elif y is not None:
|
|
2144
|
-
|
|
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)})"
|
|
2145
2217
|
|
|
2146
2218
|
# 如果检测到应用跳转,添加警告和返回结果
|
|
2147
2219
|
if app_check['switched']:
|
|
@@ -2182,22 +2254,22 @@ class BasicMobileToolsLite:
|
|
|
2182
2254
|
ios_client.wda.send_keys('\n')
|
|
2183
2255
|
elif ios_key == 'home':
|
|
2184
2256
|
ios_client.wda.home()
|
|
2185
|
-
return {"success": True}
|
|
2186
|
-
return {"success": False, "
|
|
2257
|
+
return {"success": True, "message": f"✅ 按键成功: {key}"}
|
|
2258
|
+
return {"success": False, "message": f"❌ iOS 不支持: {key}"}
|
|
2187
2259
|
else:
|
|
2188
2260
|
keycode = key_map.get(key.lower())
|
|
2189
2261
|
if keycode:
|
|
2190
2262
|
self.client.u2.shell(f'input keyevent {keycode}')
|
|
2191
2263
|
self._record_key(key)
|
|
2192
|
-
return {"success": True}
|
|
2193
|
-
return {"success": False, "
|
|
2264
|
+
return {"success": True, "message": f"✅ 按键成功: {key}"}
|
|
2265
|
+
return {"success": False, "message": f"❌ 不支持的按键: {key}"}
|
|
2194
2266
|
except Exception as e:
|
|
2195
2267
|
return {"success": False, "message": f"❌ 按键失败: {e}"}
|
|
2196
2268
|
|
|
2197
2269
|
def wait(self, seconds: float) -> Dict:
|
|
2198
2270
|
"""等待指定时间"""
|
|
2199
2271
|
time.sleep(seconds)
|
|
2200
|
-
return {"success": True}
|
|
2272
|
+
return {"success": True, "message": f"✅ 已等待 {seconds} 秒"}
|
|
2201
2273
|
|
|
2202
2274
|
# ==================== 应用管理 ====================
|
|
2203
2275
|
|
|
@@ -2226,7 +2298,10 @@ class BasicMobileToolsLite:
|
|
|
2226
2298
|
|
|
2227
2299
|
self._record_operation('launch_app', package_name=package_name)
|
|
2228
2300
|
|
|
2229
|
-
return {
|
|
2301
|
+
return {
|
|
2302
|
+
"success": True,
|
|
2303
|
+
"message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3 秒让页面加载\n📱 已设置应用状态监测"
|
|
2304
|
+
}
|
|
2230
2305
|
except Exception as e:
|
|
2231
2306
|
return {"success": False, "message": f"❌ 启动失败: {e}"}
|
|
2232
2307
|
|
|
@@ -2239,9 +2314,9 @@ class BasicMobileToolsLite:
|
|
|
2239
2314
|
ios_client.wda.app_terminate(package_name)
|
|
2240
2315
|
else:
|
|
2241
2316
|
self.client.u2.app_stop(package_name)
|
|
2242
|
-
return {"success": True}
|
|
2317
|
+
return {"success": True, "message": f"✅ 已终止: {package_name}"}
|
|
2243
2318
|
except Exception as e:
|
|
2244
|
-
return {"success": False, "
|
|
2319
|
+
return {"success": False, "message": f"❌ 终止失败: {e}"}
|
|
2245
2320
|
|
|
2246
2321
|
def list_apps(self, filter_keyword: str = "") -> Dict:
|
|
2247
2322
|
"""列出已安装应用"""
|
|
@@ -2353,26 +2428,6 @@ class BasicMobileToolsLite:
|
|
|
2353
2428
|
'_shadow', 'shadow_', '_divider', 'divider_', '_line', 'line_'
|
|
2354
2429
|
}
|
|
2355
2430
|
|
|
2356
|
-
# Token 优化:构建精简元素(只返回非空字段)
|
|
2357
|
-
def build_compact_element(resource_id, text, content_desc, bounds, likely_click, class_name):
|
|
2358
|
-
"""只返回有值的字段,节省 token"""
|
|
2359
|
-
item = {}
|
|
2360
|
-
if resource_id:
|
|
2361
|
-
# 精简 resource_id,只保留最后一段
|
|
2362
|
-
item['id'] = resource_id.split('/')[-1] if '/' in resource_id else resource_id
|
|
2363
|
-
if text:
|
|
2364
|
-
item['text'] = text
|
|
2365
|
-
if content_desc:
|
|
2366
|
-
item['desc'] = content_desc
|
|
2367
|
-
if bounds:
|
|
2368
|
-
item['bounds'] = bounds
|
|
2369
|
-
if likely_click:
|
|
2370
|
-
item['click'] = True # 启发式判断可点击
|
|
2371
|
-
# class 精简:只保留关键类型
|
|
2372
|
-
if class_name in ('EditText', 'TextInput', 'Button', 'ImageButton', 'CheckBox', 'Switch'):
|
|
2373
|
-
item['type'] = class_name
|
|
2374
|
-
return item
|
|
2375
|
-
|
|
2376
2431
|
result = []
|
|
2377
2432
|
for elem in elements:
|
|
2378
2433
|
# 获取元素属性
|
|
@@ -2392,11 +2447,14 @@ class BasicMobileToolsLite:
|
|
|
2392
2447
|
|
|
2393
2448
|
# 2. 检查是否是功能控件(直接保留)
|
|
2394
2449
|
if class_name in FUNCTIONAL_WIDGETS:
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
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
|
+
})
|
|
2400
2458
|
continue
|
|
2401
2459
|
|
|
2402
2460
|
# 3. 检查是否是容器控件
|
|
@@ -2409,10 +2467,14 @@ class BasicMobileToolsLite:
|
|
|
2409
2467
|
# 所有属性都是默认值,过滤掉
|
|
2410
2468
|
continue
|
|
2411
2469
|
# 有业务ID或其他有意义属性,保留
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
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
|
+
})
|
|
2416
2478
|
continue
|
|
2417
2479
|
|
|
2418
2480
|
# 4. 检查是否是装饰类控件
|
|
@@ -2429,72 +2491,19 @@ class BasicMobileToolsLite:
|
|
|
2429
2491
|
continue
|
|
2430
2492
|
|
|
2431
2493
|
# 6. 其他情况:有意义的元素保留
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
# 仅在用户明确设置 MAX_ELEMENTS_RETURN 时才截断
|
|
2440
|
-
truncated = result[:MAX_ELEMENTS]
|
|
2441
|
-
truncated.append({
|
|
2442
|
-
'_truncated': True,
|
|
2443
|
-
'_total': len(result),
|
|
2444
|
-
'_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
|
|
2445
2501
|
})
|
|
2446
|
-
return truncated
|
|
2447
2502
|
|
|
2448
2503
|
return result
|
|
2449
2504
|
except Exception as e:
|
|
2450
2505
|
return [{"error": f"获取元素失败: {e}"}]
|
|
2451
2506
|
|
|
2452
|
-
def _get_page_texts(self, max_count: int = 15) -> List[str]:
|
|
2453
|
-
"""获取页面关键文本列表(用于点击后快速确认页面变化)
|
|
2454
|
-
|
|
2455
|
-
Args:
|
|
2456
|
-
max_count: 最多返回的文本数量
|
|
2457
|
-
|
|
2458
|
-
Returns:
|
|
2459
|
-
页面上的关键文本列表(去重)
|
|
2460
|
-
"""
|
|
2461
|
-
try:
|
|
2462
|
-
if self._is_ios():
|
|
2463
|
-
ios_client = self._get_ios_client()
|
|
2464
|
-
if ios_client and hasattr(ios_client, 'wda'):
|
|
2465
|
-
# iOS: 获取所有 StaticText 的文本
|
|
2466
|
-
elements = ios_client.wda(type='XCUIElementTypeStaticText').find_elements()
|
|
2467
|
-
texts = set()
|
|
2468
|
-
for elem in elements[:50]: # 限制扫描数量
|
|
2469
|
-
try:
|
|
2470
|
-
name = elem.name or elem.label
|
|
2471
|
-
if name and len(name) > 1 and len(name) < 50:
|
|
2472
|
-
texts.add(name)
|
|
2473
|
-
except:
|
|
2474
|
-
pass
|
|
2475
|
-
return list(texts)[:max_count]
|
|
2476
|
-
return []
|
|
2477
|
-
else:
|
|
2478
|
-
# Android: 快速扫描 XML 获取文本
|
|
2479
|
-
xml_string = self.client.u2.dump_hierarchy(compressed=True)
|
|
2480
|
-
import xml.etree.ElementTree as ET
|
|
2481
|
-
root = ET.fromstring(xml_string)
|
|
2482
|
-
|
|
2483
|
-
texts = set()
|
|
2484
|
-
for elem in root.iter():
|
|
2485
|
-
text = elem.get('text', '').strip()
|
|
2486
|
-
desc = elem.get('content-desc', '').strip()
|
|
2487
|
-
# 只收集有意义的文本(长度2-30,非纯数字)
|
|
2488
|
-
for t in [text, desc]:
|
|
2489
|
-
if t and 2 <= len(t) <= 30 and not t.isdigit():
|
|
2490
|
-
texts.add(t)
|
|
2491
|
-
if len(texts) >= max_count * 2: # 收集足够后停止
|
|
2492
|
-
break
|
|
2493
|
-
|
|
2494
|
-
return list(texts)[:max_count]
|
|
2495
|
-
except Exception:
|
|
2496
|
-
return []
|
|
2497
|
-
|
|
2498
2507
|
def _has_business_id(self, resource_id: str) -> bool:
|
|
2499
2508
|
"""
|
|
2500
2509
|
判断resource_id是否是业务相关的ID
|
|
@@ -2526,68 +2535,6 @@ class BasicMobileToolsLite:
|
|
|
2526
2535
|
|
|
2527
2536
|
return True
|
|
2528
2537
|
|
|
2529
|
-
def _is_likely_clickable(self, class_name: str, resource_id: str, text: str,
|
|
2530
|
-
content_desc: str, clickable: bool, bounds: str) -> bool:
|
|
2531
|
-
"""
|
|
2532
|
-
启发式判断元素是否可能可点击
|
|
2533
|
-
|
|
2534
|
-
Android 的 clickable 属性经常不准确,因为:
|
|
2535
|
-
1. 点击事件可能设置在父容器上
|
|
2536
|
-
2. 使用 onTouchListener 而不是 onClick
|
|
2537
|
-
3. RecyclerView item 通过 ItemClickListener 处理
|
|
2538
|
-
|
|
2539
|
-
此方法通过多种规则推断元素的真实可点击性
|
|
2540
|
-
"""
|
|
2541
|
-
# 规则1:clickable=true 肯定可点击
|
|
2542
|
-
if clickable:
|
|
2543
|
-
return True
|
|
2544
|
-
|
|
2545
|
-
# 规则2:特定类型的控件通常可点击
|
|
2546
|
-
TYPICALLY_CLICKABLE = {
|
|
2547
|
-
'Button', 'ImageButton', 'CheckBox', 'RadioButton', 'Switch',
|
|
2548
|
-
'ToggleButton', 'FloatingActionButton', 'Chip', 'TabView',
|
|
2549
|
-
'EditText', 'TextInput', # 输入框可点击获取焦点
|
|
2550
|
-
}
|
|
2551
|
-
if class_name in TYPICALLY_CLICKABLE:
|
|
2552
|
-
return True
|
|
2553
|
-
|
|
2554
|
-
# 规则3:resource_id 包含可点击关键词
|
|
2555
|
-
if resource_id:
|
|
2556
|
-
id_lower = resource_id.lower()
|
|
2557
|
-
CLICK_KEYWORDS = [
|
|
2558
|
-
'btn', 'button', 'click', 'tap', 'submit', 'confirm',
|
|
2559
|
-
'cancel', 'close', 'back', 'next', 'prev', 'more',
|
|
2560
|
-
'action', 'link', 'menu', 'tab', 'item', 'cell',
|
|
2561
|
-
'card', 'avatar', 'icon', 'entry', 'option', 'arrow'
|
|
2562
|
-
]
|
|
2563
|
-
for kw in CLICK_KEYWORDS:
|
|
2564
|
-
if kw in id_lower:
|
|
2565
|
-
return True
|
|
2566
|
-
|
|
2567
|
-
# 规则4:content_desc 包含可点击暗示
|
|
2568
|
-
if content_desc:
|
|
2569
|
-
desc_lower = content_desc.lower()
|
|
2570
|
-
CLICK_HINTS = ['点击', '按钮', '关闭', '返回', '更多', 'click', 'tap', 'button', 'close']
|
|
2571
|
-
for hint in CLICK_HINTS:
|
|
2572
|
-
if hint in desc_lower:
|
|
2573
|
-
return True
|
|
2574
|
-
|
|
2575
|
-
# 规则5:有 resource_id 或 content_desc 的小图标可能可点击
|
|
2576
|
-
# (纯 ImageView 不加判断,误判率太高)
|
|
2577
|
-
if class_name in ('ImageView', 'Image') and (resource_id or content_desc) and bounds:
|
|
2578
|
-
match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
|
|
2579
|
-
if match:
|
|
2580
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
2581
|
-
w, h = x2 - x1, y2 - y1
|
|
2582
|
-
# 小图标(20-100px)更可能是按钮
|
|
2583
|
-
if 20 <= w <= 100 and 20 <= h <= 100:
|
|
2584
|
-
return True
|
|
2585
|
-
|
|
2586
|
-
# 规则6:移除(TextView 误判率太高,只依赖上面的规则)
|
|
2587
|
-
# 如果有 clickable=true 或 ID/desc 中有关键词,前面的规则已经覆盖
|
|
2588
|
-
|
|
2589
|
-
return False
|
|
2590
|
-
|
|
2591
2538
|
def find_close_button(self) -> Dict:
|
|
2592
2539
|
"""智能查找关闭按钮(不点击,只返回位置)
|
|
2593
2540
|
|
|
@@ -2601,7 +2548,7 @@ class BasicMobileToolsLite:
|
|
|
2601
2548
|
import re
|
|
2602
2549
|
|
|
2603
2550
|
if self._is_ios():
|
|
2604
|
-
return {"success": False, "
|
|
2551
|
+
return {"success": False, "message": "iOS 暂不支持,请使用截图+坐标点击"}
|
|
2605
2552
|
|
|
2606
2553
|
# 获取屏幕尺寸
|
|
2607
2554
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
@@ -2612,14 +2559,6 @@ class BasicMobileToolsLite:
|
|
|
2612
2559
|
import xml.etree.ElementTree as ET
|
|
2613
2560
|
root = ET.fromstring(xml_string)
|
|
2614
2561
|
|
|
2615
|
-
# 🔴 先检测是否有弹窗,避免误识别普通页面的按钮
|
|
2616
|
-
popup_bounds, popup_confidence = self._detect_popup_with_confidence(
|
|
2617
|
-
root, screen_width, screen_height
|
|
2618
|
-
)
|
|
2619
|
-
|
|
2620
|
-
if popup_bounds is None or popup_confidence < 0.5:
|
|
2621
|
-
return {"success": True, "popup": False}
|
|
2622
|
-
|
|
2623
2562
|
# 关闭按钮特征
|
|
2624
2563
|
close_texts = ['×', 'X', 'x', '关闭', '取消', 'close', 'Close', '跳过', '知道了', '我知道了']
|
|
2625
2564
|
candidates = []
|
|
@@ -2721,16 +2660,27 @@ class BasicMobileToolsLite:
|
|
|
2721
2660
|
candidates.sort(key=lambda x: x['score'], reverse=True)
|
|
2722
2661
|
best = candidates[0]
|
|
2723
2662
|
|
|
2724
|
-
# Token 优化:只返回最必要的信息
|
|
2725
2663
|
return {
|
|
2726
2664
|
"success": True,
|
|
2727
|
-
"
|
|
2728
|
-
"
|
|
2729
|
-
|
|
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}
|
|
2730
2680
|
}
|
|
2731
2681
|
|
|
2732
2682
|
except Exception as e:
|
|
2733
|
-
return {"success": False, "
|
|
2683
|
+
return {"success": False, "message": f"❌ 查找关闭按钮失败: {e}"}
|
|
2734
2684
|
|
|
2735
2685
|
def close_popup(self) -> Dict:
|
|
2736
2686
|
"""智能关闭弹窗(改进版)
|
|
@@ -2753,7 +2703,7 @@ class BasicMobileToolsLite:
|
|
|
2753
2703
|
|
|
2754
2704
|
# 获取屏幕尺寸
|
|
2755
2705
|
if self._is_ios():
|
|
2756
|
-
return {"success": False, "
|
|
2706
|
+
return {"success": False, "message": "iOS 暂不支持,请使用截图+坐标点击"}
|
|
2757
2707
|
|
|
2758
2708
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
2759
2709
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
@@ -2781,10 +2731,8 @@ class BasicMobileToolsLite:
|
|
|
2781
2731
|
# 如果置信度不够高,记录但继续尝试查找关闭按钮
|
|
2782
2732
|
popup_detected = popup_bounds is not None and popup_confidence >= 0.6
|
|
2783
2733
|
|
|
2784
|
-
#
|
|
2785
|
-
#
|
|
2786
|
-
if not popup_detected:
|
|
2787
|
-
return {"success": True, "popup": False}
|
|
2734
|
+
# 【重要修复】如果没有检测到弹窗区域,只搜索有明确关闭特征的元素(文本、resource-id等)
|
|
2735
|
+
# 避免误点击普通页面的右上角图标
|
|
2788
2736
|
|
|
2789
2737
|
# ===== 第二步:在弹窗范围内查找关闭按钮 =====
|
|
2790
2738
|
for idx, elem in enumerate(all_elements):
|
|
@@ -2793,6 +2741,7 @@ class BasicMobileToolsLite:
|
|
|
2793
2741
|
bounds_str = elem.attrib.get('bounds', '')
|
|
2794
2742
|
class_name = elem.attrib.get('class', '')
|
|
2795
2743
|
clickable = elem.attrib.get('clickable', 'false') == 'true'
|
|
2744
|
+
resource_id = elem.attrib.get('resource-id', '')
|
|
2796
2745
|
|
|
2797
2746
|
if not bounds_str:
|
|
2798
2747
|
continue
|
|
@@ -2809,7 +2758,7 @@ class BasicMobileToolsLite:
|
|
|
2809
2758
|
center_y = (y1 + y2) // 2
|
|
2810
2759
|
|
|
2811
2760
|
# 如果检测到弹窗区域,检查元素是否在弹窗范围内或附近
|
|
2812
|
-
in_popup =
|
|
2761
|
+
in_popup = False
|
|
2813
2762
|
popup_edge_bonus = 0
|
|
2814
2763
|
is_floating_close = False # 是否是浮动关闭按钮(在弹窗外部上方)
|
|
2815
2764
|
if popup_bounds:
|
|
@@ -2850,6 +2799,20 @@ class BasicMobileToolsLite:
|
|
|
2850
2799
|
# 浮动关闭按钮(在弹窗上方外侧)给予高额加分
|
|
2851
2800
|
if is_floating_close:
|
|
2852
2801
|
popup_edge_bonus += 5.0 # 大幅加分
|
|
2802
|
+
elif not popup_detected:
|
|
2803
|
+
# 没有检测到弹窗时,只处理有明确关闭特征的元素
|
|
2804
|
+
# 检查是否有明确的关闭特征(文本、resource-id、content-desc)
|
|
2805
|
+
has_explicit_close_feature = (
|
|
2806
|
+
text in close_texts or
|
|
2807
|
+
any(kw in content_desc.lower() for kw in close_desc_keywords) or
|
|
2808
|
+
'close' in resource_id.lower() or
|
|
2809
|
+
'dismiss' in resource_id.lower() or
|
|
2810
|
+
'cancel' in resource_id.lower()
|
|
2811
|
+
)
|
|
2812
|
+
if not has_explicit_close_feature:
|
|
2813
|
+
continue # 没有明确关闭特征,跳过
|
|
2814
|
+
# 有明确关闭特征时,允许处理
|
|
2815
|
+
in_popup = True
|
|
2853
2816
|
|
|
2854
2817
|
if not in_popup:
|
|
2855
2818
|
continue
|
|
@@ -2923,9 +2886,86 @@ class BasicMobileToolsLite:
|
|
|
2923
2886
|
pass
|
|
2924
2887
|
|
|
2925
2888
|
if not close_candidates:
|
|
2889
|
+
# 如果检测到高置信度的弹窗区域,先尝试点击常见的关闭按钮位置
|
|
2926
2890
|
if popup_detected and popup_bounds:
|
|
2927
|
-
|
|
2928
|
-
|
|
2891
|
+
px1, py1, px2, py2 = popup_bounds
|
|
2892
|
+
popup_width = px2 - px1
|
|
2893
|
+
popup_height = py2 - py1
|
|
2894
|
+
|
|
2895
|
+
# 【优化】X按钮有三种常见位置:
|
|
2896
|
+
# 1. 弹窗内靠近顶部边界(内嵌X按钮)- 最常见
|
|
2897
|
+
# 2. 弹窗边界上方(浮动X按钮)
|
|
2898
|
+
# 3. 弹窗正下方(底部关闭按钮)
|
|
2899
|
+
offset_x = max(60, int(popup_width * 0.07)) # 宽度7%
|
|
2900
|
+
offset_y_above = max(35, int(popup_height * 0.025)) # 高度2.5%,在边界之上
|
|
2901
|
+
offset_y_near = max(45, int(popup_height * 0.03)) # 高度3%,紧贴顶边界内侧
|
|
2902
|
+
|
|
2903
|
+
try_positions = [
|
|
2904
|
+
# 【最高优先级】弹窗内紧贴顶部边界
|
|
2905
|
+
(px2 - offset_x, py1 + offset_y_near, "弹窗右上角"),
|
|
2906
|
+
# 弹窗边界上方(浮动X按钮)
|
|
2907
|
+
(px2 - offset_x, py1 - offset_y_above, "弹窗右上浮"),
|
|
2908
|
+
# 弹窗正下方中间(底部关闭按钮)
|
|
2909
|
+
((px1 + px2) // 2, py2 + max(50, int(popup_height * 0.04)), "弹窗下方中间"),
|
|
2910
|
+
# 弹窗正上方中间
|
|
2911
|
+
((px1 + px2) // 2, py1 - 40, "弹窗正上方"),
|
|
2912
|
+
]
|
|
2913
|
+
|
|
2914
|
+
for try_x, try_y, position_name in try_positions:
|
|
2915
|
+
if 0 <= try_x <= screen_width and 0 <= try_y <= screen_height:
|
|
2916
|
+
self.client.u2.click(try_x, try_y)
|
|
2917
|
+
time.sleep(0.3)
|
|
2918
|
+
|
|
2919
|
+
# 🎯 关键步骤:检查应用是否跳转,如果跳转说明弹窗去除失败,需要返回目标应用
|
|
2920
|
+
app_check = self._check_app_switched()
|
|
2921
|
+
return_result = None
|
|
2922
|
+
|
|
2923
|
+
if app_check['switched']:
|
|
2924
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
2925
|
+
return_result = self._return_to_target_app()
|
|
2926
|
+
|
|
2927
|
+
# 尝试后截图,让 AI 判断是否成功
|
|
2928
|
+
screenshot_result = self.take_screenshot("尝试关闭后")
|
|
2929
|
+
|
|
2930
|
+
msg = f"✅ 已尝试点击常见关闭按钮位置"
|
|
2931
|
+
if app_check['switched']:
|
|
2932
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
2933
|
+
if return_result:
|
|
2934
|
+
if return_result['success']:
|
|
2935
|
+
msg += f"\n{return_result['message']}"
|
|
2936
|
+
else:
|
|
2937
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
2938
|
+
|
|
2939
|
+
return {
|
|
2940
|
+
"success": True,
|
|
2941
|
+
"message": msg,
|
|
2942
|
+
"tried_positions": [p[2] for p in try_positions],
|
|
2943
|
+
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2944
|
+
"app_check": app_check,
|
|
2945
|
+
"return_to_app": return_result,
|
|
2946
|
+
"tip": "请查看截图确认弹窗是否已关闭。如果还在,可手动分析截图找到关闭按钮位置。"
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
# 没有检测到弹窗区域,截图让 AI 分析
|
|
2950
|
+
screenshot_result = self.take_screenshot(description="页面截图", compress=True)
|
|
2951
|
+
|
|
2952
|
+
return {
|
|
2953
|
+
"success": False,
|
|
2954
|
+
"message": "❌ 未检测到弹窗区域,已截图供 AI 分析",
|
|
2955
|
+
"action_required": "请查看截图找到关闭按钮,调用 mobile_click_at_coords 点击",
|
|
2956
|
+
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2957
|
+
"screen_size": {"width": screen_width, "height": screen_height},
|
|
2958
|
+
"image_size": {
|
|
2959
|
+
"width": screenshot_result.get("image_width", screen_width),
|
|
2960
|
+
"height": screenshot_result.get("image_height", screen_height)
|
|
2961
|
+
},
|
|
2962
|
+
"original_size": {
|
|
2963
|
+
"width": screenshot_result.get("original_img_width", screen_width),
|
|
2964
|
+
"height": screenshot_result.get("original_img_height", screen_height)
|
|
2965
|
+
},
|
|
2966
|
+
"search_areas": ["弹窗右上角", "弹窗正上方", "弹窗下方中间", "屏幕右上角"],
|
|
2967
|
+
"time_warning": "⚠️ 截图分析期间弹窗可能自动消失。如果是定时弹窗,建议等待其自动消失。"
|
|
2968
|
+
}
|
|
2929
2969
|
|
|
2930
2970
|
# 按得分排序,取最可能的
|
|
2931
2971
|
close_candidates.sort(key=lambda x: x['score'], reverse=True)
|
|
@@ -2943,22 +2983,62 @@ class BasicMobileToolsLite:
|
|
|
2943
2983
|
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
2944
2984
|
return_result = self._return_to_target_app()
|
|
2945
2985
|
|
|
2946
|
-
#
|
|
2947
|
-
self.
|
|
2948
|
-
|
|
2949
|
-
|
|
2986
|
+
# 点击后截图,让 AI 判断是否成功
|
|
2987
|
+
screenshot_result = self.take_screenshot("关闭弹窗后")
|
|
2988
|
+
|
|
2989
|
+
# 记录操作(使用百分比,跨设备兼容)
|
|
2990
|
+
self._record_operation(
|
|
2991
|
+
'click',
|
|
2992
|
+
x=best['center_x'],
|
|
2993
|
+
y=best['center_y'],
|
|
2994
|
+
x_percent=best['x_percent'],
|
|
2995
|
+
y_percent=best['y_percent'],
|
|
2996
|
+
screen_width=screen_width,
|
|
2997
|
+
screen_height=screen_height,
|
|
2998
|
+
ref=f"close_popup_{best['position']}"
|
|
2999
|
+
)
|
|
2950
3000
|
|
|
2951
|
-
#
|
|
2952
|
-
|
|
3001
|
+
# 构建返回消息
|
|
3002
|
+
msg = f"✅ 已点击关闭按钮 ({best['position']}): ({best['center_x']}, {best['center_y']})"
|
|
2953
3003
|
if app_check['switched']:
|
|
2954
|
-
|
|
3004
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
2955
3005
|
if return_result:
|
|
2956
|
-
|
|
3006
|
+
if return_result['success']:
|
|
3007
|
+
msg += f"\n{return_result['message']}"
|
|
3008
|
+
else:
|
|
3009
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
2957
3010
|
|
|
2958
|
-
|
|
3011
|
+
# 返回候选按钮列表,让 AI 看截图判断
|
|
3012
|
+
# 如果弹窗还在,AI 可以选择点击其他候选按钮
|
|
3013
|
+
return {
|
|
3014
|
+
"success": True,
|
|
3015
|
+
"message": msg,
|
|
3016
|
+
"clicked": {
|
|
3017
|
+
"position": best['position'],
|
|
3018
|
+
"match_type": best['match_type'],
|
|
3019
|
+
"coords": (best['center_x'], best['center_y']),
|
|
3020
|
+
"percent": (best['x_percent'], best['y_percent'])
|
|
3021
|
+
},
|
|
3022
|
+
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
3023
|
+
"popup_detected": popup_detected,
|
|
3024
|
+
"popup_confidence": popup_confidence if popup_bounds else 0,
|
|
3025
|
+
"popup_bounds": f"[{popup_bounds[0]},{popup_bounds[1]}][{popup_bounds[2]},{popup_bounds[3]}]" if popup_detected else None,
|
|
3026
|
+
"app_check": app_check,
|
|
3027
|
+
"return_to_app": return_result,
|
|
3028
|
+
"other_candidates": [
|
|
3029
|
+
{
|
|
3030
|
+
"position": c['position'],
|
|
3031
|
+
"type": c['match_type'],
|
|
3032
|
+
"coords": (c['center_x'], c['center_y']),
|
|
3033
|
+
"percent": (c['x_percent'], c['y_percent'])
|
|
3034
|
+
}
|
|
3035
|
+
for c in close_candidates[1:4] # 返回其他3个候选,AI 可以选择
|
|
3036
|
+
],
|
|
3037
|
+
"tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates 中的其他位置"
|
|
3038
|
+
}
|
|
2959
3039
|
|
|
2960
3040
|
except Exception as e:
|
|
2961
|
-
return {"success": False, "
|
|
3041
|
+
return {"success": False, "message": f"❌ 关闭弹窗失败: {e}"}
|
|
2962
3042
|
|
|
2963
3043
|
def _get_position_name(self, rel_x: float, rel_y: float) -> str:
|
|
2964
3044
|
"""根据相对坐标获取位置名称"""
|
|
@@ -3040,6 +3120,15 @@ class BasicMobileToolsLite:
|
|
|
3040
3120
|
resource_id = elem.attrib.get('resource-id', '')
|
|
3041
3121
|
clickable = elem.attrib.get('clickable', 'false') == 'true'
|
|
3042
3122
|
|
|
3123
|
+
# 检查是否是关闭按钮
|
|
3124
|
+
is_close_button = (
|
|
3125
|
+
'close' in resource_id.lower() or
|
|
3126
|
+
'dismiss' in resource_id.lower() or
|
|
3127
|
+
'cancel' in resource_id.lower() or
|
|
3128
|
+
'×' in elem.attrib.get('text', '') or
|
|
3129
|
+
'X' in elem.attrib.get('text', '')
|
|
3130
|
+
)
|
|
3131
|
+
|
|
3043
3132
|
all_elements.append({
|
|
3044
3133
|
'idx': idx,
|
|
3045
3134
|
'bounds': (x1, y1, x2, y2),
|
|
@@ -3052,6 +3141,7 @@ class BasicMobileToolsLite:
|
|
|
3052
3141
|
'clickable': clickable,
|
|
3053
3142
|
'center_x': (x1 + x2) // 2,
|
|
3054
3143
|
'center_y': (y1 + y2) // 2,
|
|
3144
|
+
'is_close_button': is_close_button,
|
|
3055
3145
|
})
|
|
3056
3146
|
|
|
3057
3147
|
if not all_elements:
|
|
@@ -3060,6 +3150,8 @@ class BasicMobileToolsLite:
|
|
|
3060
3150
|
# 弹窗检测关键词
|
|
3061
3151
|
dialog_class_keywords = ['Dialog', 'Popup', 'Alert', 'Modal', 'BottomSheet', 'PopupWindow']
|
|
3062
3152
|
dialog_id_keywords = ['dialog', 'popup', 'alert', 'modal', 'bottom_sheet', 'overlay', 'mask']
|
|
3153
|
+
# 广告弹窗关键词(全屏广告、激励视频等)
|
|
3154
|
+
ad_popup_keywords = ['ad_close', 'ad_button', 'full_screen', 'interstitial', 'reward', 'close_icon', 'close_btn']
|
|
3063
3155
|
|
|
3064
3156
|
popup_candidates = []
|
|
3065
3157
|
has_mask_layer = False
|
|
@@ -3090,6 +3182,59 @@ class BasicMobileToolsLite:
|
|
|
3090
3182
|
if y1 < 50:
|
|
3091
3183
|
continue
|
|
3092
3184
|
|
|
3185
|
+
# 【非弹窗特征】如果元素包含底部导航栏(底部tab),则不是弹窗
|
|
3186
|
+
# 底部导航栏通常在屏幕底部,高度约100-200像素
|
|
3187
|
+
if y2 > screen_height * 0.85:
|
|
3188
|
+
# 检查是否包含tab相关的resource-id或class
|
|
3189
|
+
if 'tab' in resource_id.lower() or 'Tab' in class_name or 'navigation' in resource_id.lower():
|
|
3190
|
+
continue # 跳过底部导航栏
|
|
3191
|
+
|
|
3192
|
+
# 【非弹窗特征】如果元素包含顶部搜索栏,则不是弹窗
|
|
3193
|
+
if y1 < screen_height * 0.15:
|
|
3194
|
+
if 'search' in resource_id.lower() or 'Search' in class_name:
|
|
3195
|
+
continue # 跳过顶部搜索栏
|
|
3196
|
+
|
|
3197
|
+
# 先检查是否有强弹窗特征(用于后续判断)
|
|
3198
|
+
has_strong_popup_feature = (
|
|
3199
|
+
any(kw in class_name for kw in dialog_class_keywords) or
|
|
3200
|
+
any(kw in resource_id.lower() for kw in dialog_id_keywords) or
|
|
3201
|
+
any(kw in resource_id.lower() for kw in ad_popup_keywords) # 广告弹窗关键词
|
|
3202
|
+
)
|
|
3203
|
+
|
|
3204
|
+
# 检查是否有子元素是关闭按钮(作为弹窗特征)
|
|
3205
|
+
has_close_button_child = False
|
|
3206
|
+
elem_bounds = elem['bounds']
|
|
3207
|
+
for other_elem in all_elements:
|
|
3208
|
+
if other_elem['idx'] == elem['idx']:
|
|
3209
|
+
continue
|
|
3210
|
+
if other_elem['is_close_button']:
|
|
3211
|
+
# 检查关闭按钮是否在这个元素范围内
|
|
3212
|
+
ox1, oy1, ox2, oy2 = other_elem['bounds']
|
|
3213
|
+
ex1, ey1, ex2, ey2 = elem_bounds
|
|
3214
|
+
if ex1 <= ox1 and ey1 <= oy1 and ex2 >= ox2 and ey2 >= oy2:
|
|
3215
|
+
has_close_button_child = True
|
|
3216
|
+
break
|
|
3217
|
+
|
|
3218
|
+
# 【非弹窗特征】如果元素包含明显的页面内容特征,则不是弹窗
|
|
3219
|
+
# 检查是否包含视频播放器、内容列表等页面元素
|
|
3220
|
+
page_content_keywords = ['video', 'player', 'recycler', 'list', 'scroll', 'viewpager', 'fragment']
|
|
3221
|
+
if any(kw in resource_id.lower() or kw in class_name.lower() for kw in page_content_keywords):
|
|
3222
|
+
# 如果面积很大且没有强弹窗特征,则不是弹窗
|
|
3223
|
+
if area_ratio > 0.6 and not has_strong_popup_feature:
|
|
3224
|
+
continue
|
|
3225
|
+
|
|
3226
|
+
# 【非弹窗特征】如果元素面积过大(接近全屏),即使居中也不应该是弹窗
|
|
3227
|
+
# 真正的弹窗通常不会超过屏幕的60%
|
|
3228
|
+
# 对于面积 > 0.6 的元素,如果没有强特征,直接跳过(避免误判首页内容区域)
|
|
3229
|
+
if area_ratio > 0.6 and not has_strong_popup_feature:
|
|
3230
|
+
continue # 跳过大面积非弹窗元素(接近全屏的内容区域,如首页视频播放区域)
|
|
3231
|
+
|
|
3232
|
+
# 对于面积 > 0.7 的元素,即使有强特征也要更严格
|
|
3233
|
+
if area_ratio > 0.7:
|
|
3234
|
+
# 需要非常强的特征才认为是弹窗
|
|
3235
|
+
if not has_strong_popup_feature:
|
|
3236
|
+
continue
|
|
3237
|
+
|
|
3093
3238
|
confidence = 0.0
|
|
3094
3239
|
|
|
3095
3240
|
# 【强特征】class 名称包含弹窗关键词 (+0.5)
|
|
@@ -3100,19 +3245,46 @@ class BasicMobileToolsLite:
|
|
|
3100
3245
|
if any(kw in resource_id.lower() for kw in dialog_id_keywords):
|
|
3101
3246
|
confidence += 0.4
|
|
3102
3247
|
|
|
3248
|
+
# 【强特征】resource-id 包含广告弹窗关键词 (+0.4)
|
|
3249
|
+
if any(kw in resource_id.lower() for kw in ad_popup_keywords):
|
|
3250
|
+
confidence += 0.4
|
|
3251
|
+
|
|
3252
|
+
# 【强特征】包含关闭按钮作为子元素 (+0.3)
|
|
3253
|
+
if has_close_button_child:
|
|
3254
|
+
confidence += 0.3
|
|
3255
|
+
|
|
3103
3256
|
# 【中等特征】居中显示 (+0.2)
|
|
3257
|
+
# 但如果没有强特征,降低权重
|
|
3104
3258
|
center_x = elem['center_x']
|
|
3105
3259
|
center_y = elem['center_y']
|
|
3106
3260
|
is_centered_x = abs(center_x - screen_width / 2) < screen_width * 0.15
|
|
3107
3261
|
is_centered_y = abs(center_y - screen_height / 2) < screen_height * 0.25
|
|
3262
|
+
|
|
3263
|
+
has_strong_feature = (
|
|
3264
|
+
any(kw in class_name for kw in dialog_class_keywords) or
|
|
3265
|
+
any(kw in resource_id.lower() for kw in dialog_id_keywords) or
|
|
3266
|
+
any(kw in resource_id.lower() for kw in ad_popup_keywords) or
|
|
3267
|
+
has_close_button_child
|
|
3268
|
+
)
|
|
3269
|
+
|
|
3108
3270
|
if is_centered_x and is_centered_y:
|
|
3109
|
-
|
|
3271
|
+
if has_strong_feature:
|
|
3272
|
+
confidence += 0.2
|
|
3273
|
+
else:
|
|
3274
|
+
confidence += 0.1 # 没有强特征时降低权重
|
|
3110
3275
|
elif is_centered_x:
|
|
3111
|
-
|
|
3276
|
+
if has_strong_feature:
|
|
3277
|
+
confidence += 0.1
|
|
3278
|
+
else:
|
|
3279
|
+
confidence += 0.05 # 没有强特征时降低权重
|
|
3112
3280
|
|
|
3113
3281
|
# 【中等特征】非全屏但有一定大小 (+0.15)
|
|
3282
|
+
# 但如果没有强特征,降低权重
|
|
3114
3283
|
if 0.15 < area_ratio < 0.75:
|
|
3115
|
-
|
|
3284
|
+
if has_strong_feature:
|
|
3285
|
+
confidence += 0.15
|
|
3286
|
+
else:
|
|
3287
|
+
confidence += 0.08 # 没有强特征时降低权重
|
|
3116
3288
|
|
|
3117
3289
|
# 【弱特征】XML 顺序靠后(在视图层级上层)(+0.1)
|
|
3118
3290
|
if elem['idx'] > len(all_elements) * 0.5:
|
|
@@ -3139,8 +3311,22 @@ class BasicMobileToolsLite:
|
|
|
3139
3311
|
popup_candidates.sort(key=lambda x: (x['confidence'], x['idx']), reverse=True)
|
|
3140
3312
|
best = popup_candidates[0]
|
|
3141
3313
|
|
|
3142
|
-
#
|
|
3143
|
-
|
|
3314
|
+
# 更严格的阈值:只有置信度 >= 0.7 才返回弹窗
|
|
3315
|
+
# 如果没有强特征(class或resource-id包含弹窗关键词),需要更高的置信度
|
|
3316
|
+
has_strong_feature = (
|
|
3317
|
+
any(kw in best['class'] for kw in dialog_class_keywords) or
|
|
3318
|
+
any(kw in best['resource_id'].lower() for kw in dialog_id_keywords) or
|
|
3319
|
+
any(kw in best['resource_id'].lower() for kw in ad_popup_keywords)
|
|
3320
|
+
)
|
|
3321
|
+
|
|
3322
|
+
if has_strong_feature:
|
|
3323
|
+
# 有强特征时,阈值0.7
|
|
3324
|
+
threshold = 0.7
|
|
3325
|
+
else:
|
|
3326
|
+
# 没有强特征时,阈值0.85(更严格)
|
|
3327
|
+
threshold = 0.85
|
|
3328
|
+
|
|
3329
|
+
if best['confidence'] >= threshold:
|
|
3144
3330
|
return best['bounds'], best['confidence']
|
|
3145
3331
|
|
|
3146
3332
|
return None, best['confidence']
|
|
@@ -3859,28 +4045,10 @@ class BasicMobileToolsLite:
|
|
|
3859
4045
|
try:
|
|
3860
4046
|
import xml.etree.ElementTree as ET
|
|
3861
4047
|
|
|
3862
|
-
# ========== 第
|
|
4048
|
+
# ========== 第1步:控件树查找关闭按钮 ==========
|
|
3863
4049
|
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
3864
4050
|
root = ET.fromstring(xml_string)
|
|
3865
4051
|
|
|
3866
|
-
screen_width = self.client.u2.info.get('displayWidth', 1440)
|
|
3867
|
-
screen_height = self.client.u2.info.get('displayHeight', 3200)
|
|
3868
|
-
|
|
3869
|
-
popup_bounds, popup_confidence = self._detect_popup_with_confidence(
|
|
3870
|
-
root, screen_width, screen_height
|
|
3871
|
-
)
|
|
3872
|
-
|
|
3873
|
-
# 如果没有检测到弹窗,直接返回"无弹窗"
|
|
3874
|
-
if popup_bounds is None or popup_confidence < 0.5:
|
|
3875
|
-
result["success"] = True
|
|
3876
|
-
result["method"] = None
|
|
3877
|
-
result["message"] = "ℹ️ 当前页面未检测到弹窗,无需关闭"
|
|
3878
|
-
result["popup_detected"] = False
|
|
3879
|
-
result["popup_confidence"] = popup_confidence
|
|
3880
|
-
return result
|
|
3881
|
-
|
|
3882
|
-
# ========== 第1步:控件树查找关闭按钮 ==========
|
|
3883
|
-
|
|
3884
4052
|
# 关闭按钮的常见特征
|
|
3885
4053
|
close_keywords = ['关闭', '跳过', '×', 'X', 'x', 'close', 'skip', '取消']
|
|
3886
4054
|
close_content_desc = ['关闭', '跳过', 'close', 'skip', 'dismiss']
|
|
@@ -3959,6 +4127,12 @@ class BasicMobileToolsLite:
|
|
|
3959
4127
|
cx, cy = best['center']
|
|
3960
4128
|
bounds = best['bounds']
|
|
3961
4129
|
|
|
4130
|
+
# 点击前截图(用于自动学习)
|
|
4131
|
+
pre_screenshot = None
|
|
4132
|
+
if auto_learn:
|
|
4133
|
+
pre_result = self.take_screenshot(description="关闭前", compress=False)
|
|
4134
|
+
pre_screenshot = pre_result.get("screenshot_path")
|
|
4135
|
+
|
|
3962
4136
|
# 点击(click_at_coords 内部已包含应用状态检查和自动返回)
|
|
3963
4137
|
click_result = self.click_at_coords(cx, cy)
|
|
3964
4138
|
time.sleep(0.5)
|
|
@@ -3988,11 +4162,17 @@ class BasicMobileToolsLite:
|
|
|
3988
4162
|
result["message"] = msg
|
|
3989
4163
|
result["app_check"] = app_check
|
|
3990
4164
|
result["return_to_app"] = return_result
|
|
3991
|
-
|
|
4165
|
+
|
|
4166
|
+
# 自动学习:检查这个 X 是否已在模板库,不在就添加
|
|
4167
|
+
if auto_learn and pre_screenshot:
|
|
4168
|
+
learn_result = self._auto_learn_template(pre_screenshot, bounds)
|
|
4169
|
+
if learn_result:
|
|
4170
|
+
result["learned_template"] = learn_result
|
|
4171
|
+
result["message"] += f"\n📚 自动学习: {learn_result}"
|
|
3992
4172
|
|
|
3993
4173
|
return result
|
|
3994
4174
|
|
|
3995
|
-
# ========== 第2
|
|
4175
|
+
# ========== 第2步:模板匹配 ==========
|
|
3996
4176
|
screenshot_path = None
|
|
3997
4177
|
try:
|
|
3998
4178
|
from .template_matcher import TemplateMatcher
|
|
@@ -4011,14 +4191,16 @@ class BasicMobileToolsLite:
|
|
|
4011
4191
|
x_pct = best["percent"]["x"]
|
|
4012
4192
|
y_pct = best["percent"]["y"]
|
|
4013
4193
|
|
|
4014
|
-
#
|
|
4194
|
+
# 点击(click_by_percent 内部已包含应用状态检查和自动返回)
|
|
4015
4195
|
click_result = self.click_by_percent(x_pct, y_pct)
|
|
4016
4196
|
time.sleep(0.5)
|
|
4017
4197
|
|
|
4198
|
+
# 🎯 再次检查应用状态(确保弹窗去除没有导致应用跳转)
|
|
4018
4199
|
app_check = self._check_app_switched()
|
|
4019
4200
|
return_result = None
|
|
4020
4201
|
|
|
4021
4202
|
if app_check['switched']:
|
|
4203
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
4022
4204
|
return_result = self._return_to_target_app()
|
|
4023
4205
|
|
|
4024
4206
|
result["success"] = True
|
|
@@ -4029,9 +4211,12 @@ class BasicMobileToolsLite:
|
|
|
4029
4211
|
f" 位置: ({x_pct:.1f}%, {y_pct:.1f}%)"
|
|
4030
4212
|
|
|
4031
4213
|
if app_check['switched']:
|
|
4032
|
-
msg += f"\n⚠️
|
|
4214
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
4033
4215
|
if return_result:
|
|
4034
|
-
|
|
4216
|
+
if return_result['success']:
|
|
4217
|
+
msg += f"\n{return_result['message']}"
|
|
4218
|
+
else:
|
|
4219
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
4035
4220
|
|
|
4036
4221
|
result["message"] = msg
|
|
4037
4222
|
result["app_check"] = app_check
|
|
@@ -4043,12 +4228,17 @@ class BasicMobileToolsLite:
|
|
|
4043
4228
|
except Exception:
|
|
4044
4229
|
pass # 模板匹配失败,继续下一步
|
|
4045
4230
|
|
|
4046
|
-
# ========== 第3
|
|
4231
|
+
# ========== 第3步:返回截图供 AI 分析 ==========
|
|
4232
|
+
if not screenshot_path:
|
|
4233
|
+
screenshot_result = self.take_screenshot(description="需要AI分析", compress=True)
|
|
4234
|
+
|
|
4047
4235
|
result["success"] = False
|
|
4048
|
-
result["fallback"] = "vision"
|
|
4049
4236
|
result["method"] = None
|
|
4050
|
-
result["
|
|
4051
|
-
|
|
4237
|
+
result["message"] = "❌ 控件树和模板匹配都未找到关闭按钮\n" \
|
|
4238
|
+
"📸 已截图,请 AI 分析图片中的 X 按钮位置\n" \
|
|
4239
|
+
"💡 找到后使用 mobile_click_by_percent(x%, y%) 点击"
|
|
4240
|
+
result["screenshot"] = screenshot_result if not screenshot_path else {"screenshot_path": screenshot_path}
|
|
4241
|
+
result["need_ai_analysis"] = True
|
|
4052
4242
|
|
|
4053
4243
|
return result
|
|
4054
4244
|
|