mobile-mcp-ai 2.6.2__tar.gz → 2.6.3__tar.gz

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.
Files changed (57) hide show
  1. {mobile_mcp_ai-2.6.2/mobile_mcp_ai.egg-info → mobile_mcp_ai-2.6.3}/PKG-INFO +1 -1
  2. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/basic_tools_lite.py +367 -122
  3. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mcp_tools/mcp_server.py +85 -0
  4. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3/mobile_mcp_ai.egg-info}/PKG-INFO +1 -1
  5. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mobile_mcp_ai.egg-info/SOURCES.txt +10 -0
  6. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/setup.py +1 -1
  7. mobile_mcp_ai-2.6.3/tests/test_mind_cloud_my_space.py +80 -0
  8. mobile_mcp_ai-2.6.3/tests/test_mind_correct.py +73 -0
  9. mobile_mcp_ai-2.6.3/tests/test_mind_improved.py +83 -0
  10. mobile_mcp_ai-2.6.3/tests/test_mind_optimized.py +77 -0
  11. mobile_mcp_ai-2.6.3/tests/test_open_mind.py +37 -0
  12. mobile_mcp_ai-2.6.3/tests/test_priority_demo.py +81 -0
  13. mobile_mcp_ai-2.6.3/tests/test_simple.py +76 -0
  14. mobile_mcp_ai-2.6.3/tests/test_/344/270/276/346/212/245.py +136 -0
  15. mobile_mcp_ai-2.6.3/tests/test_/345/210/207/346/215/242/350/257/255/350/250/200/345/210/260English.py +158 -0
  16. mobile_mcp_ai-2.6.3/tests/test_/346/265/213/350/257/225.py +114 -0
  17. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/LICENSE +0 -0
  18. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/MANIFEST.in +0 -0
  19. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/README.md +0 -0
  20. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/__init__.py +0 -0
  21. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/config.py +0 -0
  22. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/__init__.py +0 -0
  23. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/device_manager.py +0 -0
  24. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/dynamic_config.py +0 -0
  25. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/ios_client_wda.py +0 -0
  26. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/ios_device_manager_wda.py +0 -0
  27. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/mobile_client.py +0 -0
  28. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/template_matcher.py +0 -0
  29. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
  30. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
  31. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
  32. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
  33. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
  34. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
  35. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/utils/__init__.py +0 -0
  36. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/utils/logger.py +0 -0
  37. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/utils/operation_history_manager.py +0 -0
  38. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/core/utils/smart_wait.py +0 -0
  39. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/docs/iOS_SETUP_GUIDE.md +0 -0
  40. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mcp_tools/__init__.py +0 -0
  41. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mobile_mcp_ai.egg-info/dependency_links.txt +0 -0
  42. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mobile_mcp_ai.egg-info/entry_points.txt +0 -0
  43. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mobile_mcp_ai.egg-info/not-zip-safe +0 -0
  44. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mobile_mcp_ai.egg-info/requires.txt +0 -0
  45. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/mobile_mcp_ai.egg-info/top_level.txt +0 -0
  46. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/requirements.txt +0 -0
  47. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/setup.cfg +0 -0
  48. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/templates/close_buttons/auto_x_0112_151217.png +0 -0
  49. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/templates/close_buttons/auto_x_0112_152037.png +0 -0
  50. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/templates/close_buttons/auto_x_0112_152840.png +0 -0
  51. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/templates/close_buttons/auto_x_0112_153256.png +0 -0
  52. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/templates/close_buttons/auto_x_0112_154847.png +0 -0
  53. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/templates/close_buttons/gray_x_stock_ad.png +0 -0
  54. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/utils/__init__.py +0 -0
  55. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/utils/logger.py +0 -0
  56. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/utils/xml_formatter.py +0 -0
  57. {mobile_mcp_ai-2.6.2 → mobile_mcp_ai-2.6.3}/utils/xml_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mobile-mcp-ai
3
- Version: 2.6.2
3
+ Version: 2.6.3
4
4
  Summary: 移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)
5
5
  Home-page: https://github.com/test111ddff-hash/mobile-mcp-ai
6
6
  Author: douzi
@@ -500,7 +500,7 @@ class BasicMobileToolsLite:
500
500
  # 左侧标注 Y 坐标
501
501
  draw.text((2, y + 2), str(y), fill=text_color, font=font_small)
502
502
 
503
- # 第3步:检测弹窗并标注
503
+ # 第3步:检测弹窗并标注(使用严格的置信度检测,避免误识别)
504
504
  popup_info = None
505
505
  close_positions = []
506
506
 
@@ -510,35 +510,12 @@ class BasicMobileToolsLite:
510
510
  xml_string = self.client.u2.dump_hierarchy(compressed=False)
511
511
  root = ET.fromstring(xml_string)
512
512
 
513
- # 检测弹窗区域
514
- popup_bounds = None
515
- for elem in root.iter():
516
- bounds_str = elem.attrib.get('bounds', '')
517
- class_name = elem.attrib.get('class', '')
518
-
519
- if not bounds_str:
520
- continue
521
-
522
- match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
523
- if not match:
524
- continue
525
-
526
- x1, y1, x2, y2 = map(int, match.groups())
527
- width = x2 - x1
528
- height = y2 - y1
529
- area = width * height
530
- screen_area = screen_width * screen_height
531
-
532
- is_container = any(kw in class_name for kw in ['Layout', 'View', 'Dialog', 'Card'])
533
- area_ratio = area / screen_area if screen_area > 0 else 0
534
- is_not_fullscreen = (width < screen_width * 0.98 or height < screen_height * 0.98)
535
- is_reasonable_size = 0.08 < area_ratio < 0.85
536
-
537
- if is_container and is_not_fullscreen and is_reasonable_size and y1 > 50:
538
- if popup_bounds is None or area > (popup_bounds[2] - popup_bounds[0]) * (popup_bounds[3] - popup_bounds[1]):
539
- popup_bounds = (x1, y1, x2, y2)
513
+ # 使用严格的弹窗检测(置信度 >= 0.6 才认为是弹窗)
514
+ popup_bounds, popup_confidence = self._detect_popup_with_confidence(
515
+ root, screen_width, screen_height
516
+ )
540
517
 
541
- if popup_bounds:
518
+ if popup_bounds and popup_confidence >= 0.6:
542
519
  px1, py1, px2, py2 = popup_bounds
543
520
  popup_width = px2 - px1
544
521
  popup_height = py2 - py1
@@ -769,49 +746,19 @@ class BasicMobileToolsLite:
769
746
  'desc': elem['desc']
770
747
  })
771
748
 
772
- # 第3.5步:检测弹窗区域(用于标注)
749
+ # 第3.5步:检测弹窗区域(使用严格的置信度检测,避免误识别普通页面)
773
750
  popup_bounds = None
751
+ popup_confidence = 0
774
752
 
775
753
  if not self._is_ios():
776
754
  try:
777
- # 检测弹窗区域
778
- for elem in root.iter():
779
- bounds_str = elem.attrib.get('bounds', '')
780
- class_name = elem.attrib.get('class', '')
781
-
782
- if not bounds_str:
783
- continue
784
-
785
- match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
786
- if not match:
787
- continue
788
-
789
- px1, py1, px2, py2 = map(int, match.groups())
790
- p_width = px2 - px1
791
- p_height = py2 - py1
792
- p_area = p_width * p_height
793
- screen_area = screen_width * screen_height
794
-
795
- is_container = any(kw in class_name for kw in ['Layout', 'View', 'Dialog', 'Card', 'Frame'])
796
- area_ratio = p_area / screen_area if screen_area > 0 else 0
797
-
798
- # 弹窗特征判断(更严格,排除主要内容区域):
799
- # 1. 不是全屏(宽度和高度都要小于屏幕的95%)
800
- is_not_fullscreen = (p_width < screen_width * 0.95 and p_height < screen_height * 0.95)
801
- # 2. 面积范围:10% - 70%(排除主要内容区域,通常占80%+)
802
- is_reasonable_size = 0.10 < area_ratio < 0.70
803
- # 3. 不在屏幕左边缘(排除从x=0开始的主要内容容器)
804
- is_not_at_left_edge = px1 > screen_width * 0.05
805
- # 4. 高度不能占据屏幕的大部分(排除主要内容区域)
806
- height_ratio = p_height / screen_height if screen_height > 0 else 0
807
- is_not_main_content = height_ratio < 0.85
808
-
809
- if is_container and is_not_fullscreen and is_reasonable_size and is_not_at_left_edge and is_not_main_content and py1 > 30:
810
- if popup_bounds is None or p_area > (popup_bounds[2] - popup_bounds[0]) * (popup_bounds[3] - popup_bounds[1]):
811
- popup_bounds = (px1, py1, px2, py2)
755
+ # 使用严格的弹窗检测(置信度 >= 0.6 才认为是弹窗)
756
+ popup_bounds, popup_confidence = self._detect_popup_with_confidence(
757
+ root, screen_width, screen_height
758
+ )
812
759
 
813
760
  # 如果检测到弹窗,标注弹窗边界(不再猜测X按钮位置)
814
- if popup_bounds:
761
+ if popup_bounds and popup_confidence >= 0.6:
815
762
  px1, py1, px2, py2 = popup_bounds
816
763
 
817
764
  # 只画弹窗边框(蓝色),不再猜测X按钮位置
@@ -2630,58 +2577,13 @@ class BasicMobileToolsLite:
2630
2577
  root = ET.fromstring(xml_string)
2631
2578
  all_elements = list(root.iter())
2632
2579
 
2633
- # ===== 第一步:检测弹窗区域 =====
2634
- # 弹窗特征:非全屏、面积较大、通常在屏幕中央的容器
2635
- popup_containers = []
2636
- for idx, elem in enumerate(all_elements):
2637
- bounds_str = elem.attrib.get('bounds', '')
2638
- class_name = elem.attrib.get('class', '')
2639
-
2640
- if not bounds_str:
2641
- continue
2642
-
2643
- match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
2644
- if not match:
2645
- continue
2646
-
2647
- x1, y1, x2, y2 = map(int, match.groups())
2648
- width = x2 - x1
2649
- height = y2 - y1
2650
- area = width * height
2651
- screen_area = screen_width * screen_height
2652
-
2653
- # 弹窗容器特征(更严格,排除主要内容区域):
2654
- # 1. 面积在屏幕的 10%-70% 之间(排除主要内容区域,通常占80%+)
2655
- # 2. 宽度和高度都要小于屏幕的95%(不是全屏)
2656
- # 3. 是容器类型(Layout/View/Dialog)
2657
- # 4. 不在屏幕左边缘(排除从x=0开始的主要内容容器)
2658
- # 5. 高度不能占据屏幕的大部分(排除主要内容区域)
2659
- is_container = any(kw in class_name for kw in ['Layout', 'View', 'Dialog', 'Card', 'Container'])
2660
- area_ratio = area / screen_area
2661
- is_not_fullscreen = (width < screen_width * 0.95 and height < screen_height * 0.95)
2662
- is_reasonable_size = 0.10 < area_ratio < 0.70
2663
- is_not_at_left_edge = x1 > screen_width * 0.05
2664
- height_ratio = height / screen_height if screen_height > 0 else 0
2665
- is_not_main_content = height_ratio < 0.85
2666
-
2667
- # 排除状态栏区域(y1 通常很小)
2668
- is_below_statusbar = y1 > 50
2669
-
2670
- if is_container and is_not_fullscreen and is_reasonable_size and is_not_at_left_edge and is_not_main_content and is_below_statusbar:
2671
- popup_containers.append({
2672
- 'bounds': (x1, y1, x2, y2),
2673
- 'bounds_str': bounds_str,
2674
- 'area': area,
2675
- 'area_ratio': area_ratio,
2676
- 'idx': idx, # 元素在 XML 中的顺序(越后越上层)
2677
- 'class': class_name
2678
- })
2580
+ # ===== 第一步:使用严格的置信度检测弹窗区域 =====
2581
+ popup_bounds, popup_confidence = self._detect_popup_with_confidence(
2582
+ root, screen_width, screen_height
2583
+ )
2679
2584
 
2680
- # 选择最可能的弹窗容器(优先选择:XML 顺序靠后 + 面积适中)
2681
- if popup_containers:
2682
- # 按 XML 顺序倒序(后出现的在上层),然后按面积适中程度排序
2683
- popup_containers.sort(key=lambda x: (x['idx'], -abs(x['area_ratio'] - 0.3)), reverse=True)
2684
- popup_bounds = popup_containers[0]['bounds']
2585
+ # 如果置信度不够高,记录但继续尝试查找关闭按钮
2586
+ popup_detected = popup_bounds is not None and popup_confidence >= 0.6
2685
2587
 
2686
2588
  # ===== 第二步:在弹窗范围内查找关闭按钮 =====
2687
2589
  for idx, elem in enumerate(all_elements):
@@ -2813,15 +2715,15 @@ class BasicMobileToolsLite:
2813
2715
  'content_desc': content_desc,
2814
2716
  'x_percent': round(rel_x * 100, 1),
2815
2717
  'y_percent': round(rel_y * 100, 1),
2816
- 'in_popup': popup_bounds is not None
2718
+ 'in_popup': popup_detected
2817
2719
  })
2818
2720
 
2819
2721
  except ET.ParseError:
2820
2722
  pass
2821
2723
 
2822
2724
  if not close_candidates:
2823
- # 如果检测到弹窗区域,先尝试点击常见的关闭按钮位置
2824
- if popup_bounds:
2725
+ # 如果检测到高置信度的弹窗区域,先尝试点击常见的关闭按钮位置
2726
+ if popup_detected and popup_bounds:
2825
2727
  px1, py1, px2, py2 = popup_bounds
2826
2728
  popup_width = px2 - px1
2827
2729
  popup_height = py2 - py1
@@ -2954,8 +2856,9 @@ class BasicMobileToolsLite:
2954
2856
  "percent": (best['x_percent'], best['y_percent'])
2955
2857
  },
2956
2858
  "screenshot": screenshot_result.get("screenshot_path", ""),
2957
- "popup_detected": popup_bounds is not None,
2958
- "popup_bounds": f"[{popup_bounds[0]},{popup_bounds[1]}][{popup_bounds[2]},{popup_bounds[3]}]" if popup_bounds else None,
2859
+ "popup_detected": popup_detected,
2860
+ "popup_confidence": popup_confidence if popup_bounds else 0,
2861
+ "popup_bounds": f"[{popup_bounds[0]},{popup_bounds[1]}][{popup_bounds[2]},{popup_bounds[3]}]" if popup_detected else None,
2959
2862
  "app_check": app_check,
2960
2863
  "return_to_app": return_result,
2961
2864
  "other_candidates": [
@@ -3014,6 +2917,348 @@ class BasicMobileToolsLite:
3014
2917
  return 0.8
3015
2918
  else: # 中间区域
3016
2919
  return 0.5
2920
+
2921
+ def _detect_popup_with_confidence(self, root, screen_width: int, screen_height: int) -> tuple:
2922
+ """严格的弹窗检测 - 使用置信度评分,避免误识别普通页面
2923
+
2924
+ 真正的弹窗特征:
2925
+ 1. class 名称包含 Dialog/Popup/Alert/Modal/BottomSheet(强特征)
2926
+ 2. resource-id 包含 dialog/popup/alert/modal(强特征)
2927
+ 3. 有遮罩层(大面积半透明 View 在弹窗之前)
2928
+ 4. 居中显示且非全屏
2929
+ 5. XML 层级靠后且包含可交互元素
2930
+
2931
+ Returns:
2932
+ (popup_bounds, confidence) 或 (None, 0)
2933
+ confidence >= 0.6 才认为是弹窗
2934
+ """
2935
+ import re
2936
+
2937
+ screen_area = screen_width * screen_height
2938
+
2939
+ # 收集所有元素信息
2940
+ all_elements = []
2941
+ for idx, elem in enumerate(root.iter()):
2942
+ bounds_str = elem.attrib.get('bounds', '')
2943
+ if not bounds_str:
2944
+ continue
2945
+
2946
+ match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
2947
+ if not match:
2948
+ continue
2949
+
2950
+ x1, y1, x2, y2 = map(int, match.groups())
2951
+ width = x2 - x1
2952
+ height = y2 - y1
2953
+ area = width * height
2954
+
2955
+ class_name = elem.attrib.get('class', '')
2956
+ resource_id = elem.attrib.get('resource-id', '')
2957
+ clickable = elem.attrib.get('clickable', 'false') == 'true'
2958
+
2959
+ all_elements.append({
2960
+ 'idx': idx,
2961
+ 'bounds': (x1, y1, x2, y2),
2962
+ 'width': width,
2963
+ 'height': height,
2964
+ 'area': area,
2965
+ 'area_ratio': area / screen_area if screen_area > 0 else 0,
2966
+ 'class': class_name,
2967
+ 'resource_id': resource_id,
2968
+ 'clickable': clickable,
2969
+ 'center_x': (x1 + x2) // 2,
2970
+ 'center_y': (y1 + y2) // 2,
2971
+ })
2972
+
2973
+ if not all_elements:
2974
+ return None, 0
2975
+
2976
+ # 弹窗检测关键词
2977
+ dialog_class_keywords = ['Dialog', 'Popup', 'Alert', 'Modal', 'BottomSheet', 'PopupWindow']
2978
+ dialog_id_keywords = ['dialog', 'popup', 'alert', 'modal', 'bottom_sheet', 'overlay', 'mask']
2979
+
2980
+ popup_candidates = []
2981
+ has_mask_layer = False
2982
+ mask_idx = -1
2983
+
2984
+ # 【新增】检测浮动关闭按钮(小尺寸 clickable ImageView,位于屏幕中央偏上)
2985
+ floating_close_buttons = []
2986
+ for elem in all_elements:
2987
+ x1, y1, x2, y2 = elem['bounds']
2988
+ class_name = elem['class']
2989
+ width = elem['width']
2990
+ height = elem['height']
2991
+
2992
+ # 浮动关闭按钮特征:
2993
+ # 1. 小尺寸(50-200px)
2994
+ # 2. clickable 或 ImageView
2995
+ # 3. 位于屏幕中央区域的上半部分
2996
+ # 4. 接近正方形
2997
+ is_small = 50 < width < 200 and 50 < height < 200
2998
+ is_square_like = 0.5 < (width / height if height > 0 else 0) < 2.0
2999
+ is_clickable_image = elem['clickable'] or 'Image' in class_name
3000
+ is_upper_center = (screen_width * 0.2 < x1 < screen_width * 0.8 and
3001
+ y1 < screen_height * 0.5)
3002
+
3003
+ if is_small and is_square_like and is_clickable_image and is_upper_center:
3004
+ floating_close_buttons.append({
3005
+ 'bounds': elem['bounds'],
3006
+ 'center_x': elem['center_x'],
3007
+ 'center_y': elem['center_y'],
3008
+ 'idx': elem['idx']
3009
+ })
3010
+
3011
+ for elem in all_elements:
3012
+ x1, y1, x2, y2 = elem['bounds']
3013
+ class_name = elem['class']
3014
+ resource_id = elem['resource_id']
3015
+ area_ratio = elem['area_ratio']
3016
+
3017
+ # 检测遮罩层(大面积、几乎全屏、通常是 FrameLayout/View)
3018
+ if area_ratio > 0.85 and elem['width'] >= screen_width * 0.95:
3019
+ # 可能是遮罩层,记录位置
3020
+ if 'FrameLayout' in class_name or 'View' in class_name:
3021
+ has_mask_layer = True
3022
+ mask_idx = elem['idx']
3023
+
3024
+ # 跳过全屏元素
3025
+ if area_ratio > 0.9:
3026
+ continue
3027
+
3028
+ # 跳过太小的元素
3029
+ if area_ratio < 0.05:
3030
+ continue
3031
+
3032
+ # 跳过状态栏区域
3033
+ if y1 < 50:
3034
+ continue
3035
+
3036
+ confidence = 0.0
3037
+
3038
+ # 【强特征】class 名称包含弹窗关键词 (+0.5)
3039
+ if any(kw in class_name for kw in dialog_class_keywords):
3040
+ confidence += 0.5
3041
+
3042
+ # 【强特征】resource-id 包含弹窗关键词 (+0.4)
3043
+ if any(kw in resource_id.lower() for kw in dialog_id_keywords):
3044
+ confidence += 0.4
3045
+
3046
+ # 【中等特征】居中显示 (+0.2)
3047
+ center_x = elem['center_x']
3048
+ center_y = elem['center_y']
3049
+ is_centered_x = abs(center_x - screen_width / 2) < screen_width * 0.15
3050
+ is_centered_y = abs(center_y - screen_height / 2) < screen_height * 0.25
3051
+ if is_centered_x and is_centered_y:
3052
+ confidence += 0.2
3053
+ elif is_centered_x:
3054
+ confidence += 0.1
3055
+
3056
+ # 【中等特征】非全屏但有一定大小 (+0.15)
3057
+ if 0.15 < area_ratio < 0.75:
3058
+ confidence += 0.15
3059
+
3060
+ # 【弱特征】XML 顺序靠后(在视图层级上层)(+0.1)
3061
+ if elem['idx'] > len(all_elements) * 0.5:
3062
+ confidence += 0.1
3063
+
3064
+ # 【弱特征】有遮罩层且在遮罩层之后 (+0.15)
3065
+ if has_mask_layer and elem['idx'] > mask_idx:
3066
+ confidence += 0.15
3067
+
3068
+ # 【新增强特征】有浮动关闭按钮在此容器上方附近 (+0.4)
3069
+ # 这是很多 App 弹窗的典型设计:内容区域 + 上方的 X 按钮
3070
+ for close_btn in floating_close_buttons:
3071
+ btn_x, btn_y = close_btn['center_x'], close_btn['center_y']
3072
+ # 检查关闭按钮是否在容器的上方(扩大范围到 400px)
3073
+ is_above_container = (
3074
+ x1 - 100 < btn_x < x2 + 100 and # 在容器水平范围内
3075
+ y1 - 400 < btn_y < y1 + 100 # 在容器上方 400px 范围内
3076
+ )
3077
+ if is_above_container:
3078
+ confidence += 0.4
3079
+ break # 只加一次分
3080
+
3081
+ # 只有达到阈值才加入候选
3082
+ if confidence >= 0.3:
3083
+ popup_candidates.append({
3084
+ 'bounds': elem['bounds'],
3085
+ 'confidence': confidence,
3086
+ 'class': class_name,
3087
+ 'resource_id': resource_id,
3088
+ 'idx': elem['idx']
3089
+ })
3090
+
3091
+ if not popup_candidates:
3092
+ return None, 0
3093
+
3094
+ # 选择置信度最高的
3095
+ popup_candidates.sort(key=lambda x: (x['confidence'], x['idx']), reverse=True)
3096
+ best = popup_candidates[0]
3097
+
3098
+ # 只有置信度 >= 0.6 才返回弹窗
3099
+ if best['confidence'] >= 0.6:
3100
+ return best['bounds'], best['confidence']
3101
+
3102
+ return None, best['confidence']
3103
+
3104
+ def start_toast_watch(self) -> Dict:
3105
+ """开始监听 Toast(仅 Android)
3106
+
3107
+ ⚠️ 必须在执行操作之前调用!
3108
+
3109
+ 正确流程:
3110
+ 1. 调用 mobile_start_toast_watch() 开始监听
3111
+ 2. 执行操作(如点击提交按钮)
3112
+ 3. 调用 mobile_get_toast() 获取 Toast 内容
3113
+
3114
+ Returns:
3115
+ 监听状态
3116
+ """
3117
+ if self._is_ios():
3118
+ return {
3119
+ "success": False,
3120
+ "message": "❌ iOS 不支持 Toast 检测,Toast 是 Android 特有功能"
3121
+ }
3122
+
3123
+ try:
3124
+ # 清除缓存并开始监听
3125
+ self.client.u2.toast.reset()
3126
+ return {
3127
+ "success": True,
3128
+ "message": "✅ Toast 监听已开启,请立即执行操作,然后调用 mobile_get_toast 获取结果"
3129
+ }
3130
+ except Exception as e:
3131
+ return {
3132
+ "success": False,
3133
+ "message": f"❌ 开启 Toast 监听失败: {e}"
3134
+ }
3135
+
3136
+ def get_toast(self, timeout: float = 5.0, reset_first: bool = False) -> Dict:
3137
+ """获取 Toast 消息(仅 Android)
3138
+
3139
+ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果。
3140
+
3141
+ ⚠️ 推荐用法(两步走):
3142
+ 1. 先调用 mobile_start_toast_watch() 开始监听
3143
+ 2. 执行操作(如点击提交按钮)
3144
+ 3. 调用 mobile_get_toast() 获取 Toast
3145
+
3146
+ 或者设置 reset_first=True,会自动 reset 后等待(适合操作已自动触发的场景)
3147
+
3148
+ Args:
3149
+ timeout: 等待 Toast 出现的超时时间(秒),默认 5 秒
3150
+ reset_first: 是否先 reset(清除旧缓存),默认 False
3151
+
3152
+ Returns:
3153
+ 包含 Toast 消息的字典
3154
+ """
3155
+ if self._is_ios():
3156
+ return {
3157
+ "success": False,
3158
+ "message": "❌ iOS 不支持 Toast 检测,Toast 是 Android 特有功能"
3159
+ }
3160
+
3161
+ try:
3162
+ if reset_first:
3163
+ # 清除旧缓存,适合等待即将出现的 Toast
3164
+ self.client.u2.toast.reset()
3165
+
3166
+ # 等待并获取 Toast 消息
3167
+ toast_message = self.client.u2.toast.get_message(
3168
+ wait_timeout=timeout,
3169
+ default=None
3170
+ )
3171
+
3172
+ if toast_message:
3173
+ return {
3174
+ "success": True,
3175
+ "toast_found": True,
3176
+ "message": toast_message,
3177
+ "tip": "Toast 消息获取成功"
3178
+ }
3179
+ else:
3180
+ return {
3181
+ "success": True,
3182
+ "toast_found": False,
3183
+ "message": None,
3184
+ "tip": f"在 {timeout} 秒内未检测到 Toast。提示:先调用 mobile_start_toast_watch,再执行操作,最后调用此工具"
3185
+ }
3186
+ except Exception as e:
3187
+ return {
3188
+ "success": False,
3189
+ "message": f"❌ 获取 Toast 失败: {e}"
3190
+ }
3191
+
3192
+ def assert_toast(self, expected_text: str, timeout: float = 5.0, contains: bool = True) -> Dict:
3193
+ """断言 Toast 消息(仅 Android)
3194
+
3195
+ 等待 Toast 出现并验证内容是否符合预期。
3196
+
3197
+ ⚠️ 推荐用法:先调用 mobile_start_toast_watch,再执行操作,最后调用此工具
3198
+
3199
+ Args:
3200
+ expected_text: 期望的 Toast 文本
3201
+ timeout: 等待超时时间(秒)
3202
+ contains: True 表示包含匹配,False 表示精确匹配
3203
+
3204
+ Returns:
3205
+ 断言结果
3206
+ """
3207
+ if self._is_ios():
3208
+ return {
3209
+ "success": False,
3210
+ "passed": False,
3211
+ "message": "❌ iOS 不支持 Toast 检测"
3212
+ }
3213
+
3214
+ try:
3215
+ # 获取 Toast(不 reset,假设之前已经调用过 start_toast_watch)
3216
+ toast_message = self.client.u2.toast.get_message(
3217
+ wait_timeout=timeout,
3218
+ default=None
3219
+ )
3220
+
3221
+ if toast_message is None:
3222
+ return {
3223
+ "success": True,
3224
+ "passed": False,
3225
+ "expected": expected_text,
3226
+ "actual": None,
3227
+ "message": f"❌ 断言失败:未检测到 Toast 消息"
3228
+ }
3229
+
3230
+ # 匹配检查
3231
+ if contains:
3232
+ passed = expected_text in toast_message
3233
+ match_type = "包含"
3234
+ else:
3235
+ passed = expected_text == toast_message
3236
+ match_type = "精确"
3237
+
3238
+ if passed:
3239
+ return {
3240
+ "success": True,
3241
+ "passed": True,
3242
+ "expected": expected_text,
3243
+ "actual": toast_message,
3244
+ "match_type": match_type,
3245
+ "message": f"✅ Toast 断言通过:'{toast_message}'"
3246
+ }
3247
+ else:
3248
+ return {
3249
+ "success": True,
3250
+ "passed": False,
3251
+ "expected": expected_text,
3252
+ "actual": toast_message,
3253
+ "match_type": match_type,
3254
+ "message": f"❌ Toast 断言失败:期望 '{expected_text}',实际 '{toast_message}'"
3255
+ }
3256
+ except Exception as e:
3257
+ return {
3258
+ "success": False,
3259
+ "passed": False,
3260
+ "message": f"❌ Toast 断言异常: {e}"
3261
+ }
3017
3262
 
3018
3263
  def assert_text(self, text: str) -> Dict:
3019
3264
  """检查页面是否包含文本(支持精确匹配和包含匹配)"""
@@ -671,6 +671,72 @@ class MobileMCPServer:
671
671
  }
672
672
  ))
673
673
 
674
+ # ==================== Toast 检测工具(仅 Android)====================
675
+ tools.append(Tool(
676
+ name="mobile_start_toast_watch",
677
+ description="""🔔 开始监听 Toast(仅 Android)
678
+
679
+ ⚠️ 【重要】必须在执行操作之前调用!
680
+
681
+ 📋 正确流程(三步走):
682
+ 1️⃣ 调用 mobile_start_toast_watch() 开始监听
683
+ 2️⃣ 执行操作(如点击提交按钮)
684
+ 3️⃣ 调用 mobile_get_toast() 或 mobile_assert_toast() 获取结果
685
+
686
+ ❌ 错误用法:先点击按钮,再调用此工具(Toast 可能已消失)""",
687
+ inputSchema={
688
+ "type": "object",
689
+ "properties": {},
690
+ "required": []
691
+ }
692
+ ))
693
+
694
+ tools.append(Tool(
695
+ name="mobile_get_toast",
696
+ description="""🍞 获取 Toast 消息(仅 Android)
697
+
698
+ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果。
699
+ ⚠️ Toast 不在控件树中,无法通过 mobile_list_elements 获取。
700
+
701
+ 📋 推荐用法(三步走):
702
+ 1️⃣ mobile_start_toast_watch() - 开始监听
703
+ 2️⃣ 执行操作(点击按钮等)
704
+ 3️⃣ mobile_get_toast() - 获取 Toast
705
+
706
+ ⏱️ timeout 设置等待时间,默认 5 秒。""",
707
+ inputSchema={
708
+ "type": "object",
709
+ "properties": {
710
+ "timeout": {"type": "number", "description": "等待 Toast 出现的超时时间(秒),默认 5"},
711
+ "reset_first": {"type": "boolean", "description": "是否先清除旧缓存,默认 False"}
712
+ },
713
+ "required": []
714
+ }
715
+ ))
716
+
717
+ tools.append(Tool(
718
+ name="mobile_assert_toast",
719
+ description="""✅ 断言 Toast 消息(仅 Android)
720
+
721
+ 等待 Toast 出现并验证内容是否符合预期。
722
+
723
+ 📋 推荐用法(三步走):
724
+ 1️⃣ mobile_start_toast_watch() - 开始监听
725
+ 2️⃣ 执行操作(点击按钮等)
726
+ 3️⃣ mobile_assert_toast(expected_text="成功") - 断言
727
+
728
+ 💡 支持包含匹配(默认)和精确匹配。""",
729
+ inputSchema={
730
+ "type": "object",
731
+ "properties": {
732
+ "expected_text": {"type": "string", "description": "期望的 Toast 文本"},
733
+ "timeout": {"type": "number", "description": "等待超时时间(秒),默认 5"},
734
+ "contains": {"type": "boolean", "description": "True=包含匹配(默认),False=精确匹配"}
735
+ },
736
+ "required": ["expected_text"]
737
+ }
738
+ ))
739
+
674
740
  # ==================== pytest 脚本生成 ====================
675
741
  tools.append(Tool(
676
742
  name="mobile_get_operation_history",
@@ -990,6 +1056,25 @@ class MobileMCPServer:
990
1056
  result = self.tools.assert_text(arguments["text"])
991
1057
  return [TextContent(type="text", text=self.format_response(result))]
992
1058
 
1059
+ # Toast 检测(仅 Android)
1060
+ elif name == "mobile_start_toast_watch":
1061
+ result = self.tools.start_toast_watch()
1062
+ return [TextContent(type="text", text=self.format_response(result))]
1063
+
1064
+ elif name == "mobile_get_toast":
1065
+ timeout = arguments.get("timeout", 5.0)
1066
+ reset_first = arguments.get("reset_first", False)
1067
+ result = self.tools.get_toast(timeout=timeout, reset_first=reset_first)
1068
+ return [TextContent(type="text", text=self.format_response(result))]
1069
+
1070
+ elif name == "mobile_assert_toast":
1071
+ result = self.tools.assert_toast(
1072
+ expected_text=arguments["expected_text"],
1073
+ timeout=arguments.get("timeout", 5.0),
1074
+ contains=arguments.get("contains", True)
1075
+ )
1076
+ return [TextContent(type="text", text=self.format_response(result))]
1077
+
993
1078
  # 脚本生成
994
1079
  elif name == "mobile_get_operation_history":
995
1080
  result = self.tools.get_operation_history(arguments.get("limit"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mobile-mcp-ai
3
- Version: 2.6.2
3
+ Version: 2.6.3
4
4
  Summary: 移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)
5
5
  Home-page: https://github.com/test111ddff-hash/mobile-mcp-ai
6
6
  Author: douzi