mobile-mcp-ai 2.5.10__py3-none-any.whl → 2.5.11__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.
@@ -31,9 +31,6 @@ class BasicMobileToolsLite:
31
31
 
32
32
  # 操作历史(用于生成 pytest 脚本)
33
33
  self.operation_history: List[Dict] = []
34
-
35
- # 目标应用包名(用于监测应用跳转)
36
- self.target_package: Optional[str] = None
37
34
 
38
35
  def _is_ios(self) -> bool:
39
36
  """判断当前是否为 iOS 平台"""
@@ -56,157 +53,34 @@ class BasicMobileToolsLite:
56
53
  }
57
54
  self.operation_history.append(record)
58
55
 
59
- def _get_current_package(self) -> Optional[str]:
60
- """获取当前前台应用的包名/Bundle ID"""
61
- try:
62
- if self._is_ios():
63
- ios_client = self._get_ios_client()
64
- if ios_client and hasattr(ios_client, 'wda'):
65
- app_info = ios_client.wda.session().app_current()
66
- return app_info.get('bundleId')
67
- else:
68
- info = self.client.u2.app_current()
69
- return info.get('package')
70
- except Exception:
71
- return None
72
-
73
- def _check_app_switched(self) -> Dict:
74
- """检查是否已跳出目标应用
56
+ def _get_full_hierarchy(self) -> str:
57
+ """获取完整的 UI 层级 XML(包含 NAF 元素)
75
58
 
76
- Returns:
77
- {
78
- 'switched': bool, # 是否跳转
79
- 'current_package': str, # 当前应用包名
80
- 'target_package': str, # 目标应用包名
81
- 'message': str # 提示信息
82
- }
59
+ 优先使用 ADB 直接 dump,比 uiautomator2.dump_hierarchy 更完整
83
60
  """
84
- if not self.target_package:
85
- return {
86
- 'switched': False,
87
- 'current_package': None,
88
- 'target_package': None,
89
- 'message': '⚠️ 未设置目标应用,无法监测应用跳转'
90
- }
91
-
92
- current = self._get_current_package()
93
- if not current:
94
- return {
95
- 'switched': False,
96
- 'current_package': None,
97
- 'target_package': self.target_package,
98
- 'message': '⚠️ 无法获取当前应用包名'
99
- }
100
-
101
- if current != self.target_package:
102
- return {
103
- 'switched': True,
104
- 'current_package': current,
105
- 'target_package': self.target_package,
106
- 'message': f'⚠️ 应用已跳转!当前应用: {current},目标应用: {self.target_package}'
107
- }
61
+ import sys
108
62
 
109
- return {
110
- 'switched': False,
111
- 'current_package': current,
112
- 'target_package': self.target_package,
113
- 'message': f'✅ 仍在目标应用: {current}'
114
- }
115
-
116
- def _return_to_target_app(self) -> Dict:
117
- """返回到目标应用
118
-
119
- 策略:
120
- 1. 先按返回键(可能关闭弹窗或返回上一页)
121
- 2. 如果还在其他应用,启动目标应用
122
- 3. 验证是否成功返回
123
-
124
- Returns:
125
- {
126
- 'success': bool,
127
- 'message': str,
128
- 'method': str # 使用的返回方法
129
- }
130
- """
131
- if not self.target_package:
132
- return {
133
- 'success': False,
134
- 'message': '❌ 未设置目标应用,无法返回',
135
- 'method': None
136
- }
63
+ if self._is_ios():
64
+ # iOS 使用 page_source
65
+ ios_client = self._get_ios_client()
66
+ if ios_client and hasattr(ios_client, 'wda'):
67
+ return ios_client.wda.source()
68
+ return ""
137
69
 
70
+ # Android: 优先使用 ADB 直接 dump
138
71
  try:
139
- # 先检查当前应用
140
- current = self._get_current_package()
141
- if not current:
142
- return {
143
- 'success': False,
144
- 'message': '❌ 无法获取当前应用包名',
145
- 'method': None
146
- }
147
-
148
- # 如果已经在目标应用,不需要返回
149
- if current == self.target_package:
150
- return {
151
- 'success': True,
152
- 'message': f'✅ 已在目标应用: {self.target_package}',
153
- 'method': 'already_in_target'
154
- }
155
-
156
- # 策略1: 先按返回键(可能关闭弹窗或返回)
157
- if self._is_ios():
158
- ios_client = self._get_ios_client()
159
- if ios_client and hasattr(ios_client, 'wda'):
160
- # iOS 返回键
161
- ios_client.wda.press('home') # iOS 先按 home
162
- time.sleep(0.5)
163
- # 然后启动目标应用
164
- ios_client.wda.app_activate(self.target_package)
165
- else:
166
- return {
167
- 'success': False,
168
- 'message': '❌ iOS 客户端未初始化',
169
- 'method': None
170
- }
171
- else:
172
- # Android: 先按返回键
173
- self.client.u2.press('back')
174
- time.sleep(0.5)
175
-
176
- # 检查是否已返回
177
- current = self._get_current_package()
178
- if current == self.target_package:
179
- return {
180
- 'success': True,
181
- 'message': f'✅ 已返回目标应用: {self.target_package}(通过返回键)',
182
- 'method': 'back_key'
183
- }
184
-
185
- # 如果还在其他应用,启动目标应用
186
- self.client.u2.app_start(self.target_package)
187
- time.sleep(1)
188
-
189
- # 验证是否成功返回
190
- current = self._get_current_package()
191
- if current == self.target_package:
192
- return {
193
- 'success': True,
194
- 'message': f'✅ 已返回目标应用: {self.target_package}',
195
- 'method': 'app_start'
196
- }
197
- else:
198
- return {
199
- 'success': False,
200
- 'message': f'❌ 返回失败:当前应用仍为 {current},期望 {self.target_package}',
201
- 'method': 'app_start'
202
- }
72
+ # 方法1: ADB dump(获取最完整的 UI 树,包括 NAF 元素)
73
+ self.client.u2.shell('uiautomator dump /sdcard/ui_dump.xml')
74
+ result = self.client.u2.shell('cat /sdcard/ui_dump.xml')
75
+ if result and isinstance(result, str) and result.strip().startswith('<?xml'):
76
+ xml_string = result.strip()
77
+ self.client.u2.shell('rm /sdcard/ui_dump.xml')
78
+ return xml_string
203
79
  except Exception as e:
204
- return {
205
- 'success': False,
206
- 'message': f'❌ 返回目标应用失败: {e}',
207
- 'method': None
208
- }
209
-
80
+ print(f" ⚠️ ADB dump 失败: {e}", file=sys.stderr)
81
+
82
+ # 方法2: 回退到 uiautomator2
83
+ return self.client.u2.dump_hierarchy(compressed=False)
210
84
 
211
85
  # ==================== 截图 ====================
212
86
 
@@ -507,7 +381,7 @@ class BasicMobileToolsLite:
507
381
  if show_popup_hints and not self._is_ios():
508
382
  try:
509
383
  import xml.etree.ElementTree as ET
510
- xml_string = self.client.u2.dump_hierarchy(compressed=False)
384
+ xml_string = self._get_full_hierarchy()
511
385
  root = ET.fromstring(xml_string)
512
386
 
513
387
  # 检测弹窗区域
@@ -684,7 +558,7 @@ class BasicMobileToolsLite:
684
558
  else:
685
559
  try:
686
560
  import xml.etree.ElementTree as ET
687
- xml_string = self.client.u2.dump_hierarchy(compressed=False)
561
+ xml_string = self._get_full_hierarchy()
688
562
  root = ET.fromstring(xml_string)
689
563
 
690
564
  for elem in root.iter():
@@ -1089,41 +963,25 @@ class BasicMobileToolsLite:
1089
963
  ref=f"coords_{x}_{y}"
1090
964
  )
1091
965
 
1092
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1093
- app_check = self._check_app_switched()
1094
- return_result = None
1095
-
1096
- if app_check['switched']:
1097
- # 应用已跳转,尝试返回目标应用
1098
- return_result = self._return_to_target_app()
1099
-
1100
- # 构建返回消息
1101
966
  if converted:
1102
967
  if conversion_type == "crop_offset":
1103
- msg = f"✅ 点击成功: ({x}, {y})\n" \
1104
- f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
968
+ return {
969
+ "success": True,
970
+ "message": f"✅ 点击成功: ({x}, {y})\n"
971
+ f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
972
+ }
1105
973
  else:
1106
- msg = f"✅ 点击成功: ({x}, {y})\n" \
1107
- f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n" \
1108
- f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
974
+ return {
975
+ "success": True,
976
+ "message": f" 点击成功: ({x}, {y})\n"
977
+ f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n"
978
+ f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
979
+ }
1109
980
  else:
1110
- msg = f"✅ 点击成功: ({x}, {y}) [相对位置: {x_percent}%, {y_percent}%]"
1111
-
1112
- # 如果检测到应用跳转,添加警告和返回结果
1113
- if app_check['switched']:
1114
- msg += f"\n{app_check['message']}"
1115
- if return_result:
1116
- if return_result['success']:
1117
- msg += f"\n{return_result['message']}"
1118
- else:
1119
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1120
-
1121
- return {
1122
- "success": True,
1123
- "message": msg,
1124
- "app_check": app_check,
1125
- "return_to_app": return_result
1126
- }
981
+ return {
982
+ "success": True,
983
+ "message": f"✅ 点击成功: ({x}, {y}) [相对位置: {x_percent}%, {y_percent}%]"
984
+ }
1127
985
  except Exception as e:
1128
986
  return {"success": False, "message": f"❌ 点击失败: {e}"}
1129
987
 
@@ -1256,9 +1114,9 @@ class BasicMobileToolsLite:
1256
1114
  return {"success": False, "message": f"❌ 点击失败: {e}"}
1257
1115
 
1258
1116
  def _find_element_in_tree(self, text: str) -> Optional[Dict]:
1259
- """在 XML 树中查找包含指定文本的元素"""
1117
+ """在 XML 树中查找包含指定文本的元素(使用完整 UI 层级)"""
1260
1118
  try:
1261
- xml = self.client.u2.dump_hierarchy(compressed=False)
1119
+ xml = self._get_full_hierarchy()
1262
1120
  import xml.etree.ElementTree as ET
1263
1121
  root = ET.fromstring(xml)
1264
1122
 
@@ -1652,28 +1510,7 @@ class BasicMobileToolsLite:
1652
1510
  elem.set_text(text)
1653
1511
  time.sleep(0.3)
1654
1512
  self._record_operation('input', element=resource_id, ref=resource_id, text=text)
1655
-
1656
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1657
- app_check = self._check_app_switched()
1658
- return_result = None
1659
- if app_check['switched']:
1660
- return_result = self._return_to_target_app()
1661
-
1662
- msg = f"✅ 输入成功: '{text}'"
1663
- if app_check['switched']:
1664
- msg += f"\n{app_check['message']}"
1665
- if return_result:
1666
- if return_result['success']:
1667
- msg += f"\n{return_result['message']}"
1668
- else:
1669
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1670
-
1671
- return {
1672
- "success": True,
1673
- "message": msg,
1674
- "app_check": app_check,
1675
- "return_to_app": return_result
1676
- }
1513
+ return {"success": True, "message": f"✅ 输入成功: '{text}'"}
1677
1514
  return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
1678
1515
  else:
1679
1516
  elements = self.client.u2(resourceId=resource_id)
@@ -1687,28 +1524,7 @@ class BasicMobileToolsLite:
1687
1524
  elements.set_text(text)
1688
1525
  time.sleep(0.3)
1689
1526
  self._record_operation('input', element=resource_id, ref=resource_id, text=text)
1690
-
1691
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1692
- app_check = self._check_app_switched()
1693
- return_result = None
1694
- if app_check['switched']:
1695
- return_result = self._return_to_target_app()
1696
-
1697
- msg = f"✅ 输入成功: '{text}'"
1698
- if app_check['switched']:
1699
- msg += f"\n{app_check['message']}"
1700
- if return_result:
1701
- if return_result['success']:
1702
- msg += f"\n{return_result['message']}"
1703
- else:
1704
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1705
-
1706
- return {
1707
- "success": True,
1708
- "message": msg,
1709
- "app_check": app_check,
1710
- "return_to_app": return_result
1711
- }
1527
+ return {"success": True, "message": f"✅ 输入成功: '{text}'"}
1712
1528
 
1713
1529
  # 多个相同 ID(<=5个),尝试智能选择
1714
1530
  if count <= 5:
@@ -1721,56 +1537,14 @@ class BasicMobileToolsLite:
1721
1537
  elem.set_text(text)
1722
1538
  time.sleep(0.3)
1723
1539
  self._record_operation('input', element=resource_id, ref=resource_id, text=text)
1724
-
1725
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1726
- app_check = self._check_app_switched()
1727
- return_result = None
1728
- if app_check['switched']:
1729
- return_result = self._return_to_target_app()
1730
-
1731
- msg = f"✅ 输入成功: '{text}'"
1732
- if app_check['switched']:
1733
- msg += f"\n{app_check['message']}"
1734
- if return_result:
1735
- if return_result['success']:
1736
- msg += f"\n{return_result['message']}"
1737
- else:
1738
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1739
-
1740
- return {
1741
- "success": True,
1742
- "message": msg,
1743
- "app_check": app_check,
1744
- "return_to_app": return_result
1745
- }
1540
+ return {"success": True, "message": f"✅ 输入成功: '{text}'"}
1746
1541
  except:
1747
1542
  continue
1748
1543
  # 没找到可编辑的,用第一个
1749
1544
  elements[0].set_text(text)
1750
1545
  time.sleep(0.3)
1751
1546
  self._record_operation('input', element=resource_id, ref=resource_id, text=text)
1752
-
1753
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1754
- app_check = self._check_app_switched()
1755
- return_result = None
1756
- if app_check['switched']:
1757
- return_result = self._return_to_target_app()
1758
-
1759
- msg = f"✅ 输入成功: '{text}'"
1760
- if app_check['switched']:
1761
- msg += f"\n{app_check['message']}"
1762
- if return_result:
1763
- if return_result['success']:
1764
- msg += f"\n{return_result['message']}"
1765
- else:
1766
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1767
-
1768
- return {
1769
- "success": True,
1770
- "message": msg,
1771
- "app_check": app_check,
1772
- "return_to_app": return_result
1773
- }
1547
+ return {"success": True, "message": f"✅ 输入成功: '{text}'"}
1774
1548
 
1775
1549
  # ID 不可靠(不存在或太多),改用 EditText 类型定位
1776
1550
  edit_texts = self.client.u2(className='android.widget.EditText')
@@ -1780,28 +1554,7 @@ class BasicMobileToolsLite:
1780
1554
  edit_texts.set_text(text)
1781
1555
  time.sleep(0.3)
1782
1556
  self._record_operation('input', element='EditText', ref='EditText', text=text)
1783
-
1784
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1785
- app_check = self._check_app_switched()
1786
- return_result = None
1787
- if app_check['switched']:
1788
- return_result = self._return_to_target_app()
1789
-
1790
- msg = f"✅ 输入成功: '{text}' (通过 EditText 定位)"
1791
- if app_check['switched']:
1792
- msg += f"\n{app_check['message']}"
1793
- if return_result:
1794
- if return_result['success']:
1795
- msg += f"\n{return_result['message']}"
1796
- else:
1797
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1798
-
1799
- return {
1800
- "success": True,
1801
- "message": msg,
1802
- "app_check": app_check,
1803
- "return_to_app": return_result
1804
- }
1557
+ return {"success": True, "message": f"✅ 输入成功: '{text}' (通过 EditText 定位)"}
1805
1558
 
1806
1559
  # 多个 EditText,选择最靠上的
1807
1560
  best_elem = None
@@ -1820,28 +1573,7 @@ class BasicMobileToolsLite:
1820
1573
  best_elem.set_text(text)
1821
1574
  time.sleep(0.3)
1822
1575
  self._record_operation('input', element='EditText', ref='EditText', text=text)
1823
-
1824
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1825
- app_check = self._check_app_switched()
1826
- return_result = None
1827
- if app_check['switched']:
1828
- return_result = self._return_to_target_app()
1829
-
1830
- msg = f"✅ 输入成功: '{text}' (通过 EditText 定位,选择最顶部的)"
1831
- if app_check['switched']:
1832
- msg += f"\n{app_check['message']}"
1833
- if return_result:
1834
- if return_result['success']:
1835
- msg += f"\n{return_result['message']}"
1836
- else:
1837
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1838
-
1839
- return {
1840
- "success": True,
1841
- "message": msg,
1842
- "app_check": app_check,
1843
- "return_to_app": return_result
1844
- }
1576
+ return {"success": True, "message": f"✅ 输入成功: '{text}' (通过 EditText 定位,选择最顶部的)"}
1845
1577
 
1846
1578
  return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
1847
1579
 
@@ -1893,29 +1625,7 @@ class BasicMobileToolsLite:
1893
1625
  text=text
1894
1626
  )
1895
1627
 
1896
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1897
- app_check = self._check_app_switched()
1898
- return_result = None
1899
-
1900
- if app_check['switched']:
1901
- # 应用已跳转,尝试返回目标应用
1902
- return_result = self._return_to_target_app()
1903
-
1904
- msg = f"✅ 输入成功: ({x}, {y}) [相对位置: {x_percent}%, {y_percent}%] -> '{text}'"
1905
- if app_check['switched']:
1906
- msg += f"\n{app_check['message']}"
1907
- if return_result:
1908
- if return_result['success']:
1909
- msg += f"\n{return_result['message']}"
1910
- else:
1911
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
1912
-
1913
- return {
1914
- "success": True,
1915
- "message": msg,
1916
- "app_check": app_check,
1917
- "return_to_app": return_result
1918
- }
1628
+ return {"success": True, "message": f"✅ 输入成功: ({x}, {y}) [相对位置: {x_percent}%, {y_percent}%] -> '{text}'"}
1919
1629
  except Exception as e:
1920
1630
  return {"success": False, "message": f"❌ 输入失败: {e}"}
1921
1631
 
@@ -1982,14 +1692,6 @@ class BasicMobileToolsLite:
1982
1692
  record_info['y_percent'] = y_percent
1983
1693
  self._record_operation('swipe', **record_info)
1984
1694
 
1985
- # 🎯 关键步骤:检查应用是否跳转,如果跳转则自动返回目标应用
1986
- app_check = self._check_app_switched()
1987
- return_result = None
1988
-
1989
- if app_check['switched']:
1990
- # 应用已跳转,尝试返回目标应用
1991
- return_result = self._return_to_target_app()
1992
-
1993
1695
  # 构建返回消息
1994
1696
  msg = f"✅ 滑动成功: {direction}"
1995
1697
  if direction in ['left', 'right']:
@@ -1998,21 +1700,7 @@ class BasicMobileToolsLite:
1998
1700
  elif y is not None:
1999
1701
  msg += f" (高度: {y}px)"
2000
1702
 
2001
- # 如果检测到应用跳转,添加警告和返回结果
2002
- if app_check['switched']:
2003
- msg += f"\n{app_check['message']}"
2004
- if return_result:
2005
- if return_result['success']:
2006
- msg += f"\n{return_result['message']}"
2007
- else:
2008
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
2009
-
2010
- return {
2011
- "success": True,
2012
- "message": msg,
2013
- "app_check": app_check,
2014
- "return_to_app": return_result
2015
- }
1703
+ return {"success": True, "message": msg}
2016
1704
  except Exception as e:
2017
1705
  return {"success": False, "message": f"❌ 滑动失败: {e}"}
2018
1706
 
@@ -2068,22 +1756,11 @@ class BasicMobileToolsLite:
2068
1756
 
2069
1757
  await asyncio.sleep(2)
2070
1758
 
2071
- # 记录目标应用包名(用于后续监测应用跳转)
2072
- self.target_package = package_name
2073
-
2074
- # 验证是否成功启动到目标应用
2075
- current = self._get_current_package()
2076
- if current and current != package_name:
2077
- return {
2078
- "success": False,
2079
- "message": f"❌ 启动失败:当前应用为 {current},期望 {package_name}"
2080
- }
2081
-
2082
1759
  self._record_operation('launch_app', package_name=package_name)
2083
1760
 
2084
1761
  return {
2085
1762
  "success": True,
2086
- "message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3 秒让页面加载\n📱 已设置应用状态监测"
1763
+ "message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3 秒让页面加载"
2087
1764
  }
2088
1765
  except Exception as e:
2089
1766
  return {"success": False, "message": f"❌ 启动失败: {e}"}
@@ -2181,15 +1858,12 @@ class BasicMobileToolsLite:
2181
1858
  return ios_client.list_elements()
2182
1859
  return [{"error": "iOS 暂不支持元素列表,建议使用截图"}]
2183
1860
  else:
2184
- xml_string = self.client.u2.dump_hierarchy(compressed=False)
1861
+ xml_string = self._get_full_hierarchy()
2185
1862
  elements = self.client.xml_parser.parse(xml_string)
2186
1863
 
2187
1864
  result = []
2188
1865
  for elem in elements:
2189
- # 获取文本内容(去除首尾空格)
2190
- text = elem.get('text', '').strip()
2191
- # 保留:可点击、可focus或有文本的元素
2192
- if elem.get('clickable') or elem.get('focusable') or text:
1866
+ if elem.get('clickable') or elem.get('focusable'):
2193
1867
  result.append({
2194
1868
  'resource_id': elem.get('resource_id', ''),
2195
1869
  'text': elem.get('text', ''),
@@ -2220,8 +1894,8 @@ class BasicMobileToolsLite:
2220
1894
  screen_width = self.client.u2.info.get('displayWidth', 720)
2221
1895
  screen_height = self.client.u2.info.get('displayHeight', 1280)
2222
1896
 
2223
- # 获取元素列表
2224
- xml_string = self.client.u2.dump_hierarchy(compressed=False)
1897
+ # 获取元素列表(使用完整 UI 层级)
1898
+ xml_string = self._get_full_hierarchy()
2225
1899
  import xml.etree.ElementTree as ET
2226
1900
  root = ET.fromstring(xml_string)
2227
1901
 
@@ -2232,6 +1906,7 @@ class BasicMobileToolsLite:
2232
1906
  for elem in root.iter():
2233
1907
  text = elem.attrib.get('text', '')
2234
1908
  content_desc = elem.attrib.get('content-desc', '')
1909
+ resource_id = elem.attrib.get('resource-id', '')
2235
1910
  bounds_str = elem.attrib.get('bounds', '')
2236
1911
  class_name = elem.attrib.get('class', '')
2237
1912
  clickable = elem.attrib.get('clickable', 'false') == 'true'
@@ -2266,6 +1941,13 @@ class BasicMobileToolsLite:
2266
1941
  score = 90
2267
1942
  reason = f"描述='{content_desc}'"
2268
1943
 
1944
+ # 策略2.5:resource-id 包含关闭关键词(如 close_icon, ad_close 等)
1945
+ elif resource_id and any(kw in resource_id.lower() for kw in ['close', 'dismiss', 'skip', 'cancel']):
1946
+ score = 95
1947
+ # 提取简短的 id 名
1948
+ short_id = resource_id.split('/')[-1] if '/' in resource_id else resource_id
1949
+ reason = f"resource-id='{short_id}'"
1950
+
2269
1951
  # 策略3:小尺寸的 clickable 元素(可能是 X 图标)
2270
1952
  elif clickable:
2271
1953
  min_size = max(20, int(screen_width * 0.03))
@@ -2300,7 +1982,9 @@ class BasicMobileToolsLite:
2300
1982
  'center_y': center_y,
2301
1983
  'x_percent': x_percent,
2302
1984
  'y_percent': y_percent,
2303
- 'size': f"{width}x{height}"
1985
+ 'size': f"{width}x{height}",
1986
+ 'resource_id': resource_id,
1987
+ 'text': text
2304
1988
  })
2305
1989
 
2306
1990
  if not candidates:
@@ -2326,7 +2010,16 @@ class BasicMobileToolsLite:
2326
2010
  candidates.sort(key=lambda x: x['score'], reverse=True)
2327
2011
  best = candidates[0]
2328
2012
 
2329
- return {
2013
+ # 生成推荐的点击命令(优先使用 resource-id)
2014
+ if best.get('resource_id'):
2015
+ short_id = best['resource_id'].split('/')[-1] if '/' in best['resource_id'] else best['resource_id']
2016
+ click_cmd = f"mobile_click_by_id('{best['resource_id']}')"
2017
+ elif best.get('text') and best['text'] in ['×', 'X', 'x', '关闭', '取消', '跳过', '知道了']:
2018
+ click_cmd = f"mobile_click_by_text('{best['text']}')"
2019
+ else:
2020
+ click_cmd = f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})"
2021
+
2022
+ result = {
2330
2023
  "success": True,
2331
2024
  "message": f"✅ 找到可能的关闭按钮",
2332
2025
  "best_candidate": {
@@ -2337,7 +2030,7 @@ class BasicMobileToolsLite:
2337
2030
  "size": best['size'],
2338
2031
  "score": best['score']
2339
2032
  },
2340
- "click_command": f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})",
2033
+ "click_command": click_cmd,
2341
2034
  "other_candidates": [
2342
2035
  {"reason": c['reason'], "percent": f"({c['x_percent']}%, {c['y_percent']}%)", "score": c['score']}
2343
2036
  for c in candidates[1:4]
@@ -2345,6 +2038,14 @@ class BasicMobileToolsLite:
2345
2038
  "screen_size": {"width": screen_width, "height": screen_height}
2346
2039
  }
2347
2040
 
2041
+ # 如果有 resource-id,额外提供
2042
+ if best.get('resource_id'):
2043
+ result["best_candidate"]["resource_id"] = best['resource_id']
2044
+ if best.get('text'):
2045
+ result["best_candidate"]["text"] = best['text']
2046
+
2047
+ return result
2048
+
2348
2049
  except Exception as e:
2349
2050
  return {"success": False, "message": f"❌ 查找关闭按钮失败: {e}"}
2350
2051
 
@@ -2374,8 +2075,8 @@ class BasicMobileToolsLite:
2374
2075
  screen_width = self.client.u2.info.get('displayWidth', 720)
2375
2076
  screen_height = self.client.u2.info.get('displayHeight', 1280)
2376
2077
 
2377
- # 获取原始 XML
2378
- xml_string = self.client.u2.dump_hierarchy(compressed=False)
2078
+ # 获取原始 XML(使用完整 UI 层级)
2079
+ xml_string = self._get_full_hierarchy()
2379
2080
 
2380
2081
  # 关闭按钮的文本特征
2381
2082
  close_texts = ['×', 'X', 'x', '关闭', '取消', 'close', 'Close', 'CLOSE', '跳过', '知道了']
@@ -2604,33 +2305,13 @@ class BasicMobileToolsLite:
2604
2305
  self.client.u2.click(try_x, try_y)
2605
2306
  time.sleep(0.3)
2606
2307
 
2607
- # 🎯 关键步骤:检查应用是否跳转,如果跳转说明弹窗去除失败,需要返回目标应用
2608
- app_check = self._check_app_switched()
2609
- return_result = None
2610
-
2611
- if app_check['switched']:
2612
- # 应用已跳转,说明弹窗去除失败,尝试返回目标应用
2613
- return_result = self._return_to_target_app()
2614
-
2615
2308
  # 尝试后截图,让 AI 判断是否成功
2616
2309
  screenshot_result = self.take_screenshot("尝试关闭后")
2617
-
2618
- msg = f"✅ 已尝试点击常见关闭按钮位置"
2619
- if app_check['switched']:
2620
- msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
2621
- if return_result:
2622
- if return_result['success']:
2623
- msg += f"\n{return_result['message']}"
2624
- else:
2625
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
2626
-
2627
2310
  return {
2628
2311
  "success": True,
2629
- "message": msg,
2312
+ "message": f"✅ 已尝试点击常见关闭按钮位置",
2630
2313
  "tried_positions": [p[2] for p in try_positions],
2631
2314
  "screenshot": screenshot_result.get("screenshot_path", ""),
2632
- "app_check": app_check,
2633
- "return_to_app": return_result,
2634
2315
  "tip": "请查看截图确认弹窗是否已关闭。如果还在,可手动分析截图找到关闭按钮位置。"
2635
2316
  }
2636
2317
 
@@ -2663,14 +2344,6 @@ class BasicMobileToolsLite:
2663
2344
  self.client.u2.click(best['center_x'], best['center_y'])
2664
2345
  time.sleep(0.5)
2665
2346
 
2666
- # 🎯 关键步骤:检查应用是否跳转,如果跳转说明弹窗去除失败,需要返回目标应用
2667
- app_check = self._check_app_switched()
2668
- return_result = None
2669
-
2670
- if app_check['switched']:
2671
- # 应用已跳转,说明弹窗去除失败,尝试返回目标应用
2672
- return_result = self._return_to_target_app()
2673
-
2674
2347
  # 点击后截图,让 AI 判断是否成功
2675
2348
  screenshot_result = self.take_screenshot("关闭弹窗后")
2676
2349
 
@@ -2686,21 +2359,11 @@ class BasicMobileToolsLite:
2686
2359
  ref=f"close_popup_{best['position']}"
2687
2360
  )
2688
2361
 
2689
- # 构建返回消息
2690
- msg = f"✅ 已点击关闭按钮 ({best['position']}): ({best['center_x']}, {best['center_y']})"
2691
- if app_check['switched']:
2692
- msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
2693
- if return_result:
2694
- if return_result['success']:
2695
- msg += f"\n{return_result['message']}"
2696
- else:
2697
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
2698
-
2699
2362
  # 返回候选按钮列表,让 AI 看截图判断
2700
2363
  # 如果弹窗还在,AI 可以选择点击其他候选按钮
2701
2364
  return {
2702
2365
  "success": True,
2703
- "message": msg,
2366
+ "message": f"✅ 已点击关闭按钮 ({best['position']}): ({best['center_x']}, {best['center_y']})",
2704
2367
  "clicked": {
2705
2368
  "position": best['position'],
2706
2369
  "match_type": best['match_type'],
@@ -2710,8 +2373,6 @@ class BasicMobileToolsLite:
2710
2373
  "screenshot": screenshot_result.get("screenshot_path", ""),
2711
2374
  "popup_detected": popup_bounds is not None,
2712
2375
  "popup_bounds": f"[{popup_bounds[0]},{popup_bounds[1]}][{popup_bounds[2]},{popup_bounds[3]}]" if popup_bounds else None,
2713
- "app_check": app_check,
2714
- "return_to_app": return_result,
2715
2376
  "other_candidates": [
2716
2377
  {
2717
2378
  "position": c['position'],
@@ -2721,7 +2382,7 @@ class BasicMobileToolsLite:
2721
2382
  }
2722
2383
  for c in close_candidates[1:4] # 返回其他3个候选,AI 可以选择
2723
2384
  ],
2724
- "tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates 中的其他位置"
2385
+ "tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates 中的其他位置;如果误点跳转了,请按返回键"
2725
2386
  }
2726
2387
 
2727
2388
  except Exception as e:
@@ -3286,8 +2947,8 @@ class BasicMobileToolsLite:
3286
2947
  try:
3287
2948
  import xml.etree.ElementTree as ET
3288
2949
 
3289
- # ========== 第1步:控件树查找关闭按钮 ==========
3290
- xml_string = self.client.u2.dump_hierarchy(compressed=False)
2950
+ # ========== 第1步:控件树查找关闭按钮(使用完整 UI 层级)==========
2951
+ xml_string = self._get_full_hierarchy()
3291
2952
  root = ET.fromstring(xml_string)
3292
2953
 
3293
2954
  # 关闭按钮的常见特征
@@ -3325,6 +2986,16 @@ class BasicMobileToolsLite:
3325
2986
  reason = f"文本含'{kw}'"
3326
2987
  break
3327
2988
 
2989
+ # resource-id 匹配(如 close_icon, ad_close 等)
2990
+ if resource_id:
2991
+ res_id_lower = resource_id.lower()
2992
+ for kw in ['close', 'dismiss', 'skip', 'cancel']:
2993
+ if kw in res_id_lower:
2994
+ score += 9
2995
+ short_id = resource_id.split('/')[-1] if '/' in resource_id else resource_id
2996
+ reason = f"resource-id='{short_id}'"
2997
+ break
2998
+
3328
2999
  # content-desc 匹配
3329
3000
  for kw in close_content_desc:
3330
3001
  if kw.lower() in content_desc.lower():
@@ -3374,35 +3045,15 @@ class BasicMobileToolsLite:
3374
3045
  pre_result = self.take_screenshot(description="关闭前", compress=False)
3375
3046
  pre_screenshot = pre_result.get("screenshot_path")
3376
3047
 
3377
- # 点击(click_at_coords 内部已包含应用状态检查和自动返回)
3378
- click_result = self.click_at_coords(cx, cy)
3048
+ # 点击
3049
+ self.click_at_coords(cx, cy)
3379
3050
  time.sleep(0.5)
3380
3051
 
3381
- # 🎯 再次检查应用状态(确保弹窗去除没有导致应用跳转)
3382
- app_check = self._check_app_switched()
3383
- return_result = None
3384
-
3385
- if app_check['switched']:
3386
- # 应用已跳转,说明弹窗去除失败,尝试返回目标应用
3387
- return_result = self._return_to_target_app()
3388
-
3389
3052
  result["success"] = True
3390
3053
  result["method"] = "控件树"
3391
- msg = f"✅ 通过控件树找到关闭按钮并点击\n" \
3392
- f" 位置: ({cx}, {cy})\n" \
3393
- f" 原因: {best['reason']}"
3394
-
3395
- if app_check['switched']:
3396
- msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
3397
- if return_result:
3398
- if return_result['success']:
3399
- msg += f"\n{return_result['message']}"
3400
- else:
3401
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
3402
-
3403
- result["message"] = msg
3404
- result["app_check"] = app_check
3405
- result["return_to_app"] = return_result
3054
+ result["message"] = f"✅ 通过控件树找到关闭按钮并点击\n" \
3055
+ f" 位置: ({cx}, {cy})\n" \
3056
+ f" 原因: {best['reason']}"
3406
3057
 
3407
3058
  # 自动学习:检查这个 X 是否已在模板库,不在就添加
3408
3059
  if auto_learn and pre_screenshot:
@@ -3432,36 +3083,16 @@ class BasicMobileToolsLite:
3432
3083
  x_pct = best["percent"]["x"]
3433
3084
  y_pct = best["percent"]["y"]
3434
3085
 
3435
- # 点击(click_by_percent 内部已包含应用状态检查和自动返回)
3436
- click_result = self.click_by_percent(x_pct, y_pct)
3086
+ # 点击
3087
+ self.click_by_percent(x_pct, y_pct)
3437
3088
  time.sleep(0.5)
3438
3089
 
3439
- # 🎯 再次检查应用状态(确保弹窗去除没有导致应用跳转)
3440
- app_check = self._check_app_switched()
3441
- return_result = None
3442
-
3443
- if app_check['switched']:
3444
- # 应用已跳转,说明弹窗去除失败,尝试返回目标应用
3445
- return_result = self._return_to_target_app()
3446
-
3447
3090
  result["success"] = True
3448
3091
  result["method"] = "模板匹配"
3449
- msg = f"✅ 通过模板匹配找到关闭按钮并点击\n" \
3450
- f" 模板: {best.get('template', 'unknown')}\n" \
3451
- f" 置信度: {best.get('confidence', 'N/A')}%\n" \
3452
- f" 位置: ({x_pct:.1f}%, {y_pct:.1f}%)"
3453
-
3454
- if app_check['switched']:
3455
- msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
3456
- if return_result:
3457
- if return_result['success']:
3458
- msg += f"\n{return_result['message']}"
3459
- else:
3460
- msg += f"\n❌ 自动返回失败: {return_result['message']}"
3461
-
3462
- result["message"] = msg
3463
- result["app_check"] = app_check
3464
- result["return_to_app"] = return_result
3092
+ result["message"] = f"✅ 通过模板匹配找到关闭按钮并点击\n" \
3093
+ f" 模板: {best.get('template', 'unknown')}\n" \
3094
+ f" 置信度: {best.get('confidence', 'N/A')}%\n" \
3095
+ f" 位置: ({x_pct:.1f}%, {y_pct:.1f}%)"
3465
3096
  return result
3466
3097
 
3467
3098
  except ImportError:
@@ -610,22 +610,25 @@ class MobileMCPServer:
610
610
  name="mobile_find_close_button",
611
611
  description="""🔍 智能查找关闭按钮(只找不点,返回位置)
612
612
 
613
- 从元素树中找最可能的关闭按钮,返回坐标和百分比位置。
613
+ ⚡ 【推荐首选】遇到弹窗时优先调用此工具!无需先截图。
614
+
615
+ 从元素树中找最可能的关闭按钮,返回坐标和推荐的点击命令。
614
616
 
615
617
  🎯 识别策略(优先级):
616
- 1. 文本匹配:×、X、关闭、取消、跳过
617
- 2. 描述匹配:content-desc 包含 close/关闭
618
- 3. 小尺寸 clickable 元素(右上角优先)
618
+ 1. 文本匹配:×、X、关闭、取消、跳过 等(得分100)
619
+ 2. resource-id 匹配:包含 close/dismiss/skip(得分95)
620
+ 3. content-desc 匹配:包含 close/关闭(得分90)
621
+ 4. 小尺寸 clickable 元素(右上角优先,得分70+)
619
622
 
620
623
  ✅ 返回内容:
621
624
  - 坐标 (x, y) 和百分比 (x%, y%)
622
- - 推荐的点击命令:mobile_click_by_percent(x%, y%)
623
- - 多个候选位置(供确认)
625
+ - resource-id(如果有)
626
+ - 推荐的点击命令(优先 click_by_id,其次 click_by_text,最后 click_by_percent)
624
627
 
625
628
  💡 使用流程:
626
- 1. 调用此工具找到关闭按钮位置
627
- 2. 确认位置正确后,用 mobile_click_by_percent 点击
628
- 3. 百分比点击兼容不同分辨率手机""",
629
+ 1. 直接调用此工具(无需先截图/列元素)
630
+ 2. 根据返回的 click_command 执行点击
631
+ 3. 如果返回 success=false,才需要截图分析""",
629
632
  inputSchema={"type": "object", "properties": {}, "required": []}
630
633
  ))
631
634
 
@@ -711,24 +714,26 @@ class MobileMCPServer:
711
714
  name="mobile_close_ad",
712
715
  description="""🚫 【推荐】智能关闭广告弹窗
713
716
 
714
- 专门用于关闭广告弹窗,按优先级自动尝试多种方式:
717
+ ⚡ 直接调用即可,无需先截图!会自动按优先级尝试:
715
718
 
716
- 1️⃣ **控件树查找**(最可靠)
717
- - 自动查找"关闭"、"跳过"、"×"等关闭按钮
719
+ 1️⃣ **控件树查找**(最可靠,优先)
720
+ - 自动查找 resource-id 包含 close/dismiss
721
+ - 查找文本"关闭"、"跳过"、"×"等
718
722
  - 找到直接点击,实时可靠
719
723
 
720
724
  2️⃣ **模板匹配**(次优)
721
725
  - 用 OpenCV 匹配已保存的 X 按钮模板
722
- - 需要积累模板库,模板越多成功率越高
726
+ - 模板越多成功率越高
723
727
 
724
728
  3️⃣ **返回截图供 AI 分析**(兜底)
725
- - 如果前两步失败,返回截图
729
+ - 前两步都失败才截图
726
730
  - AI 分析后用 mobile_click_by_percent 点击
727
- - 点击成功后用 mobile_template_add 添加模板(自动学习)
731
+ - 点击成功后用 mobile_template_add 添加模板
728
732
 
729
- 💡 使用流程:
730
- 1. 遇到广告弹窗 → 调用此工具
733
+ 💡 正确流程:
734
+ 1. 遇到广告弹窗 → 直接调用此工具
731
735
  2. 如果成功 → 完成
736
+ 3. 只有失败时才需要截图分析
732
737
  3. 如果失败 → 看截图找 X → 点击 → 添加模板""",
733
738
  inputSchema={
734
739
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mobile-mcp-ai
3
- Version: 2.5.10
3
+ Version: 2.5.11
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
@@ -1,7 +1,7 @@
1
1
  mobile_mcp/__init__.py,sha256=sQJZTL_sxQFzmcS7jOtS2AHCfUySz40vhX96N6u1qy4,816
2
2
  mobile_mcp/config.py,sha256=yaFLAV4bc2wX0GQPtZDo7OYF9E88tXV-av41fQsJwK4,4480
3
3
  mobile_mcp/core/__init__.py,sha256=ndMy-cLAIsQDG5op7gM_AIplycqZSZPWEkec1pEhvEY,170
4
- mobile_mcp/core/basic_tools_lite.py,sha256=YwOSRYRyYo0etA_4XbeDLGIlMNIquWAWOC8ROcPog4E,173551
4
+ mobile_mcp/core/basic_tools_lite.py,sha256=kVmfiaNyO33SFf9ijvK9tO4ltPOI4A8D1ANho6DqNNI,156767
5
5
  mobile_mcp/core/device_manager.py,sha256=xG5DoeNFs45pl-FTEhEWblqVwxtFK-FmVEGlNL6EqRI,8798
6
6
  mobile_mcp/core/dynamic_config.py,sha256=Ja1n1pfb0HspGByqk2_A472mYVniKmGtNEWyjUjmgK8,9811
7
7
  mobile_mcp/core/ios_client_wda.py,sha256=Nq9WxevhTWpVpolM-Ymp-b0nUQV3tXLFszmJHbDC4wA,18770
@@ -19,14 +19,14 @@ mobile_mcp/core/utils/logger.py,sha256=XXQAHUwT1jc70pq_tYFmL6f_nKrFlYm3hcgl-5RYR
19
19
  mobile_mcp/core/utils/operation_history_manager.py,sha256=gi8S8HJAMqvkUrY7_-kVbko3Xt7c4GAUziEujRd-N-Y,4792
20
20
  mobile_mcp/core/utils/smart_wait.py,sha256=N5wKTUYrNWPruBILqrAjpvtso8Z3GRWCfMIR_aZxPLg,8649
21
21
  mobile_mcp/mcp_tools/__init__.py,sha256=xkro8Rwqv_55YlVyhh-3DgRFSsLE3h1r31VIb3bpM6E,143
22
- mobile_mcp/mcp_tools/mcp_server.py,sha256=fr42BjulYQCBreWoLx5lkWEZjT60Df6RCo_kyXMOHfI,49868
22
+ mobile_mcp/mcp_tools/mcp_server.py,sha256=T6H3jAEfxQzGeQgiTg9ROn2GpgonARrrlFWrzVfxmKU,50135
23
23
  mobile_mcp/utils/__init__.py,sha256=8EH0i7UGtx1y_j_GEgdN-cZdWn2sRtZSEOLlNF9HRnY,158
24
24
  mobile_mcp/utils/logger.py,sha256=Sqq2Nr0Y4p03erqcrbYKVPCGiFaNGHMcE_JwCkeOfU4,3626
25
25
  mobile_mcp/utils/xml_formatter.py,sha256=uwTRb3vLbqhT8O-udzWT7s7LsV-DyDUz2DkofD3hXOE,4556
26
26
  mobile_mcp/utils/xml_parser.py,sha256=QhL8CWbdmNDzmBLjtx6mEnjHgMFZzJeHpCL15qfXSpI,3926
27
- mobile_mcp_ai-2.5.10.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
28
- mobile_mcp_ai-2.5.10.dist-info/METADATA,sha256=NsXkw6Ys3YGOXDt7qA82-F9EOuYQDtawMFvzclzDpVk,10496
29
- mobile_mcp_ai-2.5.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- mobile_mcp_ai-2.5.10.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
31
- mobile_mcp_ai-2.5.10.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
32
- mobile_mcp_ai-2.5.10.dist-info/RECORD,,
27
+ mobile_mcp_ai-2.5.11.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
28
+ mobile_mcp_ai-2.5.11.dist-info/METADATA,sha256=0iJCScP_ZkC5ZBZFTJ7ZPJJr4N0h9gwuTanhc81CIkM,10496
29
+ mobile_mcp_ai-2.5.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
+ mobile_mcp_ai-2.5.11.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
31
+ mobile_mcp_ai-2.5.11.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
32
+ mobile_mcp_ai-2.5.11.dist-info/RECORD,,