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.
@@ -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, "msg": "iOS未初始化"}
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
- "image_width": image_width,
468
- "image_height": image_height,
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
- "image_width": img.width,
489
- "image_height": img.height
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, "msg": "iOS未初始化"}
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["popup"] = popup_info["bounds"]
650
- # 只返回前3个最可能的关闭按钮位置
651
- if close_positions:
652
- result["close_hints"] = [(p['x'], p['y']) for p in close_positions[:3]]
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, "msg": "iOS未初始化"}
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
- # 返回结果(Token 优化:不返回 elements 列表,已存储在 self._som_elements)
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
- "hint": "查看截图上的编号,用 click_by_som(编号) 点击"
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, "msg": "iOS未初始化"}
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, "msg": "iOS未初始化"}
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, "msg": "iOS未初始化"}
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, "msg": "无法获取屏幕尺寸"}
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}({position})", locator_attr=attr_type)
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, "msg": str(e)}
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
- self._record_click('id', resource_id, element_desc=resource_id)
1438
- return {"success": True}
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, "msg": f"索引{index}超出范围(共{len(elements)}个)"}
1441
- return {"success": False, "fallback": "vision", "msg": f"未找到ID'{resource_id}'"}
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
- self._record_click('id', resource_id, element_desc=resource_id)
1452
- return {"success": True}
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, "msg": f"索引{index}超出范围(共{count}个)"}
1455
- return {"success": False, "fallback": "vision", "msg": f"未找到ID'{resource_id}'"}
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, "msg": str(e)}
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, "msg": "iOS未初始化"}
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 {"success": True}
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 {"success": True}
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 {"success": True}
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, "msg": "iOS未初始化"}
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, "msg": "无法获取屏幕尺寸"}
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 {"success": True
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, "msg": f"未找到'{text}'"}
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, "msg": f"未找到'{text}'"}
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, "msg": f"未找到'{resource_id}'"}
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, "msg": f"未找到'{resource_id}'"}
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
- self._record_input(text, 'percent', f"{x_percent}%,{y_percent}%", x_percent, y_percent)
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) -> Dict:
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, "msg": "iOS未初始化"}
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
- swipe_map = {
2049
- 'up': (center_x, int(height * 0.8), center_x, int(height * 0.2)),
2050
- 'down': (center_x, int(height * 0.2), center_x, int(height * 0.8)),
2051
- 'left': (int(width * 0.8), swipe_y, int(width * 0.2), swipe_y),
2052
- 'right': (int(width * 0.2), swipe_y, int(width * 0.8), swipe_y),
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
- msg += f" (高度: {y_percent}% = {swipe_y}px)"
2204
+ msg_parts.append(f"高度: {y_percent}% = {swipe_y}px")
2081
2205
  elif y is not None:
2082
- msg += f" (高度: {y}px)"
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, "msg": f"iOS不支持{key}"}
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, "msg": f"不支持按键{key}"}
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 {"success": True}
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, "msg": str(e)}
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
- # 使用启发式判断可点击性(替代不准确的 clickable 属性)
2334
- likely_click = self._is_likely_clickable(class_name, resource_id, text, content_desc, clickable, bounds)
2335
- item = build_compact_element(resource_id, text, content_desc, bounds, likely_click, class_name)
2336
- if item:
2337
- result.append(item)
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
- likely_click = self._is_likely_clickable(class_name, resource_id, text, content_desc, clickable, bounds)
2351
- item = build_compact_element(resource_id, text, content_desc, bounds, likely_click, class_name)
2352
- if item:
2353
- result.append(item)
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
- likely_click = self._is_likely_clickable(class_name, resource_id, text, content_desc, clickable, bounds)
2371
- item = build_compact_element(resource_id, text, content_desc, bounds, likely_click, class_name)
2372
- if item:
2373
- result.append(item)
2374
-
2375
- # Token 优化:可选限制返回元素数量(默认不限制,确保准确度)
2376
- if TOKEN_OPTIMIZATION and MAX_ELEMENTS > 0 and len(result) > MAX_ELEMENTS:
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, "msg": "iOS暂不支持"}
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
- "popup": True,
2620
- "close": {"x": best['x_percent'], "y": best['y_percent']},
2621
- "cmd": f"click_by_percent({best['x_percent']},{best['y_percent']})"
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, "msg": str(e)}
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, "msg": "iOS暂不支持"}
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
- return {"success": False, "fallback": "vision", "popup": True}
2820
- return {"success": True, "popup": False}
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._record_click('percent', f"{best['x_percent']}%,{best['y_percent']}%",
2840
- best['x_percent'], best['y_percent'],
2841
- element_desc=f"关闭按钮({best['position']})")
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
- # Token 优化:精简返回值
2844
- result = {"success": True, "clicked": True}
2983
+ # 构建返回消息
2984
+ msg = f"✅ 已点击关闭按钮 ({best['position']}): ({best['center_x']}, {best['center_y']})"
2845
2985
  if app_check['switched']:
2846
- result["switched"] = True
2986
+ msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
2847
2987
  if return_result:
2848
- result["returned"] = return_result['success']
2988
+ if return_result['success']:
2989
+ msg += f"\n{return_result['message']}"
2990
+ else:
2991
+ msg += f"\n❌ 自动返回失败: {return_result['message']}"
2849
2992
 
2850
- return result
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, "msg": str(e)}
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
- # ========== 第0步:先检测是否有弹窗 ==========
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
- result["tip"] = "💡 建议调用 mobile_screenshot_with_som 确认弹窗是否已关闭"
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步:模板匹配(自动执行,不需要 AI 介入)==========
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
- msg += f"\n{return_result['message']}"
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步:控件树和模板匹配都失败,提示 AI 使用视觉识别 ==========
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["popup_detected"] = True
3943
- result["message"] = "⚠️ 控件树和模板匹配都未找到关闭按钮,请调用 mobile_screenshot_with_som 截图后用 click_by_som 点击"
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