mobile-mcp-ai 2.5.8__py3-none-any.whl → 2.5.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_mcp/core/basic_tools_lite.py +472 -39
- mobile_mcp/core/mobile_client.py +23 -2
- {mobile_mcp_ai-2.5.8.dist-info → mobile_mcp_ai-2.5.10.dist-info}/METADATA +1 -1
- {mobile_mcp_ai-2.5.8.dist-info → mobile_mcp_ai-2.5.10.dist-info}/RECORD +8 -8
- {mobile_mcp_ai-2.5.8.dist-info → mobile_mcp_ai-2.5.10.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.5.8.dist-info → mobile_mcp_ai-2.5.10.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.5.8.dist-info → mobile_mcp_ai-2.5.10.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.5.8.dist-info → mobile_mcp_ai-2.5.10.dist-info}/top_level.txt +0 -0
|
@@ -31,6 +31,9 @@ class BasicMobileToolsLite:
|
|
|
31
31
|
|
|
32
32
|
# 操作历史(用于生成 pytest 脚本)
|
|
33
33
|
self.operation_history: List[Dict] = []
|
|
34
|
+
|
|
35
|
+
# 目标应用包名(用于监测应用跳转)
|
|
36
|
+
self.target_package: Optional[str] = None
|
|
34
37
|
|
|
35
38
|
def _is_ios(self) -> bool:
|
|
36
39
|
"""判断当前是否为 iOS 平台"""
|
|
@@ -53,6 +56,156 @@ class BasicMobileToolsLite:
|
|
|
53
56
|
}
|
|
54
57
|
self.operation_history.append(record)
|
|
55
58
|
|
|
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
|
+
"""检查是否已跳出目标应用
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
{
|
|
78
|
+
'switched': bool, # 是否跳转
|
|
79
|
+
'current_package': str, # 当前应用包名
|
|
80
|
+
'target_package': str, # 目标应用包名
|
|
81
|
+
'message': str # 提示信息
|
|
82
|
+
}
|
|
83
|
+
"""
|
|
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
|
+
}
|
|
108
|
+
|
|
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
|
+
}
|
|
137
|
+
|
|
138
|
+
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
|
+
}
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return {
|
|
205
|
+
'success': False,
|
|
206
|
+
'message': f'❌ 返回目标应用失败: {e}',
|
|
207
|
+
'method': None
|
|
208
|
+
}
|
|
56
209
|
|
|
57
210
|
|
|
58
211
|
# ==================== 截图 ====================
|
|
@@ -936,25 +1089,41 @@ class BasicMobileToolsLite:
|
|
|
936
1089
|
ref=f"coords_{x}_{y}"
|
|
937
1090
|
)
|
|
938
1091
|
|
|
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
|
+
# 构建返回消息
|
|
939
1101
|
if converted:
|
|
940
1102
|
if conversion_type == "crop_offset":
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
"message": f"✅ 点击成功: ({x}, {y})\n"
|
|
944
|
-
f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
|
|
945
|
-
}
|
|
1103
|
+
msg = f"✅ 点击成功: ({x}, {y})\n" \
|
|
1104
|
+
f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
|
|
946
1105
|
else:
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n"
|
|
951
|
-
f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
|
|
952
|
-
}
|
|
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}"
|
|
953
1109
|
else:
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
+
}
|
|
958
1127
|
except Exception as e:
|
|
959
1128
|
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
960
1129
|
|
|
@@ -1483,7 +1652,28 @@ class BasicMobileToolsLite:
|
|
|
1483
1652
|
elem.set_text(text)
|
|
1484
1653
|
time.sleep(0.3)
|
|
1485
1654
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1486
|
-
|
|
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
|
+
}
|
|
1487
1677
|
return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
|
|
1488
1678
|
else:
|
|
1489
1679
|
elements = self.client.u2(resourceId=resource_id)
|
|
@@ -1497,7 +1687,28 @@ class BasicMobileToolsLite:
|
|
|
1497
1687
|
elements.set_text(text)
|
|
1498
1688
|
time.sleep(0.3)
|
|
1499
1689
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1500
|
-
|
|
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
|
+
}
|
|
1501
1712
|
|
|
1502
1713
|
# 多个相同 ID(<=5个),尝试智能选择
|
|
1503
1714
|
if count <= 5:
|
|
@@ -1510,14 +1721,56 @@ class BasicMobileToolsLite:
|
|
|
1510
1721
|
elem.set_text(text)
|
|
1511
1722
|
time.sleep(0.3)
|
|
1512
1723
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1513
|
-
|
|
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
|
+
}
|
|
1514
1746
|
except:
|
|
1515
1747
|
continue
|
|
1516
1748
|
# 没找到可编辑的,用第一个
|
|
1517
1749
|
elements[0].set_text(text)
|
|
1518
1750
|
time.sleep(0.3)
|
|
1519
1751
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1520
|
-
|
|
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
|
+
}
|
|
1521
1774
|
|
|
1522
1775
|
# ID 不可靠(不存在或太多),改用 EditText 类型定位
|
|
1523
1776
|
edit_texts = self.client.u2(className='android.widget.EditText')
|
|
@@ -1527,7 +1780,28 @@ class BasicMobileToolsLite:
|
|
|
1527
1780
|
edit_texts.set_text(text)
|
|
1528
1781
|
time.sleep(0.3)
|
|
1529
1782
|
self._record_operation('input', element='EditText', ref='EditText', text=text)
|
|
1530
|
-
|
|
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
|
+
}
|
|
1531
1805
|
|
|
1532
1806
|
# 多个 EditText,选择最靠上的
|
|
1533
1807
|
best_elem = None
|
|
@@ -1546,7 +1820,28 @@ class BasicMobileToolsLite:
|
|
|
1546
1820
|
best_elem.set_text(text)
|
|
1547
1821
|
time.sleep(0.3)
|
|
1548
1822
|
self._record_operation('input', element='EditText', ref='EditText', text=text)
|
|
1549
|
-
|
|
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
|
+
}
|
|
1550
1845
|
|
|
1551
1846
|
return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
|
|
1552
1847
|
|
|
@@ -1598,7 +1893,29 @@ class BasicMobileToolsLite:
|
|
|
1598
1893
|
text=text
|
|
1599
1894
|
)
|
|
1600
1895
|
|
|
1601
|
-
|
|
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
|
+
}
|
|
1602
1919
|
except Exception as e:
|
|
1603
1920
|
return {"success": False, "message": f"❌ 输入失败: {e}"}
|
|
1604
1921
|
|
|
@@ -1665,6 +1982,14 @@ class BasicMobileToolsLite:
|
|
|
1665
1982
|
record_info['y_percent'] = y_percent
|
|
1666
1983
|
self._record_operation('swipe', **record_info)
|
|
1667
1984
|
|
|
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
|
+
|
|
1668
1993
|
# 构建返回消息
|
|
1669
1994
|
msg = f"✅ 滑动成功: {direction}"
|
|
1670
1995
|
if direction in ['left', 'right']:
|
|
@@ -1673,7 +1998,21 @@ class BasicMobileToolsLite:
|
|
|
1673
1998
|
elif y is not None:
|
|
1674
1999
|
msg += f" (高度: {y}px)"
|
|
1675
2000
|
|
|
1676
|
-
|
|
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
|
+
}
|
|
1677
2016
|
except Exception as e:
|
|
1678
2017
|
return {"success": False, "message": f"❌ 滑动失败: {e}"}
|
|
1679
2018
|
|
|
@@ -1729,11 +2068,22 @@ class BasicMobileToolsLite:
|
|
|
1729
2068
|
|
|
1730
2069
|
await asyncio.sleep(2)
|
|
1731
2070
|
|
|
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
|
+
|
|
1732
2082
|
self._record_operation('launch_app', package_name=package_name)
|
|
1733
2083
|
|
|
1734
2084
|
return {
|
|
1735
2085
|
"success": True,
|
|
1736
|
-
"message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3
|
|
2086
|
+
"message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3 秒让页面加载\n📱 已设置应用状态监测"
|
|
1737
2087
|
}
|
|
1738
2088
|
except Exception as e:
|
|
1739
2089
|
return {"success": False, "message": f"❌ 启动失败: {e}"}
|
|
@@ -1836,7 +2186,10 @@ class BasicMobileToolsLite:
|
|
|
1836
2186
|
|
|
1837
2187
|
result = []
|
|
1838
2188
|
for elem in elements:
|
|
1839
|
-
|
|
2189
|
+
# 获取文本内容(去除首尾空格)
|
|
2190
|
+
text = elem.get('text', '').strip()
|
|
2191
|
+
# 保留:可点击、可focus或有文本的元素
|
|
2192
|
+
if elem.get('clickable') or elem.get('focusable') or text:
|
|
1840
2193
|
result.append({
|
|
1841
2194
|
'resource_id': elem.get('resource_id', ''),
|
|
1842
2195
|
'text': elem.get('text', ''),
|
|
@@ -2251,13 +2604,33 @@ class BasicMobileToolsLite:
|
|
|
2251
2604
|
self.client.u2.click(try_x, try_y)
|
|
2252
2605
|
time.sleep(0.3)
|
|
2253
2606
|
|
|
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
|
+
|
|
2254
2615
|
# 尝试后截图,让 AI 判断是否成功
|
|
2255
2616
|
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
|
+
|
|
2256
2627
|
return {
|
|
2257
2628
|
"success": True,
|
|
2258
|
-
"message":
|
|
2629
|
+
"message": msg,
|
|
2259
2630
|
"tried_positions": [p[2] for p in try_positions],
|
|
2260
2631
|
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2632
|
+
"app_check": app_check,
|
|
2633
|
+
"return_to_app": return_result,
|
|
2261
2634
|
"tip": "请查看截图确认弹窗是否已关闭。如果还在,可手动分析截图找到关闭按钮位置。"
|
|
2262
2635
|
}
|
|
2263
2636
|
|
|
@@ -2290,6 +2663,14 @@ class BasicMobileToolsLite:
|
|
|
2290
2663
|
self.client.u2.click(best['center_x'], best['center_y'])
|
|
2291
2664
|
time.sleep(0.5)
|
|
2292
2665
|
|
|
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
|
+
|
|
2293
2674
|
# 点击后截图,让 AI 判断是否成功
|
|
2294
2675
|
screenshot_result = self.take_screenshot("关闭弹窗后")
|
|
2295
2676
|
|
|
@@ -2305,11 +2686,21 @@ class BasicMobileToolsLite:
|
|
|
2305
2686
|
ref=f"close_popup_{best['position']}"
|
|
2306
2687
|
)
|
|
2307
2688
|
|
|
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
|
+
|
|
2308
2699
|
# 返回候选按钮列表,让 AI 看截图判断
|
|
2309
2700
|
# 如果弹窗还在,AI 可以选择点击其他候选按钮
|
|
2310
2701
|
return {
|
|
2311
2702
|
"success": True,
|
|
2312
|
-
"message":
|
|
2703
|
+
"message": msg,
|
|
2313
2704
|
"clicked": {
|
|
2314
2705
|
"position": best['position'],
|
|
2315
2706
|
"match_type": best['match_type'],
|
|
@@ -2319,6 +2710,8 @@ class BasicMobileToolsLite:
|
|
|
2319
2710
|
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2320
2711
|
"popup_detected": popup_bounds is not None,
|
|
2321
2712
|
"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,
|
|
2322
2715
|
"other_candidates": [
|
|
2323
2716
|
{
|
|
2324
2717
|
"position": c['position'],
|
|
@@ -2328,7 +2721,7 @@ class BasicMobileToolsLite:
|
|
|
2328
2721
|
}
|
|
2329
2722
|
for c in close_candidates[1:4] # 返回其他3个候选,AI 可以选择
|
|
2330
2723
|
],
|
|
2331
|
-
"tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates
|
|
2724
|
+
"tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates 中的其他位置"
|
|
2332
2725
|
}
|
|
2333
2726
|
|
|
2334
2727
|
except Exception as e:
|
|
@@ -2981,15 +3374,35 @@ class BasicMobileToolsLite:
|
|
|
2981
3374
|
pre_result = self.take_screenshot(description="关闭前", compress=False)
|
|
2982
3375
|
pre_screenshot = pre_result.get("screenshot_path")
|
|
2983
3376
|
|
|
2984
|
-
#
|
|
2985
|
-
self.click_at_coords(cx, cy)
|
|
3377
|
+
# 点击(click_at_coords 内部已包含应用状态检查和自动返回)
|
|
3378
|
+
click_result = self.click_at_coords(cx, cy)
|
|
2986
3379
|
time.sleep(0.5)
|
|
2987
3380
|
|
|
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
|
+
|
|
2988
3389
|
result["success"] = True
|
|
2989
3390
|
result["method"] = "控件树"
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
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
|
|
2993
3406
|
|
|
2994
3407
|
# 自动学习:检查这个 X 是否已在模板库,不在就添加
|
|
2995
3408
|
if auto_learn and pre_screenshot:
|
|
@@ -3019,16 +3432,36 @@ class BasicMobileToolsLite:
|
|
|
3019
3432
|
x_pct = best["percent"]["x"]
|
|
3020
3433
|
y_pct = best["percent"]["y"]
|
|
3021
3434
|
|
|
3022
|
-
#
|
|
3023
|
-
self.click_by_percent(x_pct, y_pct)
|
|
3435
|
+
# 点击(click_by_percent 内部已包含应用状态检查和自动返回)
|
|
3436
|
+
click_result = self.click_by_percent(x_pct, y_pct)
|
|
3024
3437
|
time.sleep(0.5)
|
|
3025
3438
|
|
|
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
|
+
|
|
3026
3447
|
result["success"] = True
|
|
3027
3448
|
result["method"] = "模板匹配"
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
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
|
|
3032
3465
|
return result
|
|
3033
3466
|
|
|
3034
3467
|
except ImportError:
|
mobile_mcp/core/mobile_client.py
CHANGED
|
@@ -189,8 +189,29 @@ class MobileClient:
|
|
|
189
189
|
return xml_string
|
|
190
190
|
|
|
191
191
|
# Android平台
|
|
192
|
-
# 获取XML
|
|
193
|
-
xml_string =
|
|
192
|
+
# 获取XML - 优先使用 ADB 直接 dump(更完整,包含 NAF 元素)
|
|
193
|
+
xml_string = None
|
|
194
|
+
try:
|
|
195
|
+
# 方法1: 使用 ADB 直接 dump(获取最完整的 UI 树,包括 NAF 元素)
|
|
196
|
+
import subprocess
|
|
197
|
+
import tempfile
|
|
198
|
+
import os
|
|
199
|
+
|
|
200
|
+
# 在设备上执行 dump
|
|
201
|
+
self.u2.shell('uiautomator dump /sdcard/ui_dump.xml')
|
|
202
|
+
|
|
203
|
+
# 读取文件内容
|
|
204
|
+
result = self.u2.shell('cat /sdcard/ui_dump.xml')
|
|
205
|
+
if result and isinstance(result, str) and result.strip().startswith('<?xml'):
|
|
206
|
+
xml_string = result.strip()
|
|
207
|
+
# 清理临时文件
|
|
208
|
+
self.u2.shell('rm /sdcard/ui_dump.xml')
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f" ⚠️ ADB dump 失败,使用 uiautomator2: {e}", file=sys.stderr)
|
|
211
|
+
|
|
212
|
+
# 方法2: 回退到 uiautomator2 的 dump_hierarchy
|
|
213
|
+
if not xml_string:
|
|
214
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
194
215
|
|
|
195
216
|
# 确保xml_string是字符串类型
|
|
196
217
|
if not isinstance(xml_string, str):
|
|
@@ -1,12 +1,12 @@
|
|
|
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=
|
|
4
|
+
mobile_mcp/core/basic_tools_lite.py,sha256=YwOSRYRyYo0etA_4XbeDLGIlMNIquWAWOC8ROcPog4E,173551
|
|
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
|
|
8
8
|
mobile_mcp/core/ios_device_manager_wda.py,sha256=A44glqI-24un7qST-E3w6BQD8mV92YVUbxy4rLlTScY,11264
|
|
9
|
-
mobile_mcp/core/mobile_client.py,sha256=
|
|
9
|
+
mobile_mcp/core/mobile_client.py,sha256=AaBntQSW2loAw7xL3j_IABNzrJO_Uukf9-F1z1xl6xE,63672
|
|
10
10
|
mobile_mcp/core/template_matcher.py,sha256=tv8RU6zdeDobqphaP4Y8sicb1esg3gcQlZae1tNyitM,14559
|
|
11
11
|
mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png,sha256=s7tBVaYLBApNSEXjwi5kX8GXwUqgbNyNVEhXYjN9nd4,27373
|
|
12
12
|
mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png,sha256=s7tBVaYLBApNSEXjwi5kX8GXwUqgbNyNVEhXYjN9nd4,27373
|
|
@@ -24,9 +24,9 @@ mobile_mcp/utils/__init__.py,sha256=8EH0i7UGtx1y_j_GEgdN-cZdWn2sRtZSEOLlNF9HRnY,
|
|
|
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.
|
|
28
|
-
mobile_mcp_ai-2.5.
|
|
29
|
-
mobile_mcp_ai-2.5.
|
|
30
|
-
mobile_mcp_ai-2.5.
|
|
31
|
-
mobile_mcp_ai-2.5.
|
|
32
|
-
mobile_mcp_ai-2.5.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|