mobile-mcp-ai 2.5.11__py3-none-any.whl → 2.6.0__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 +604 -118
- mobile_mcp/mcp_tools/mcp_server.py +17 -22
- {mobile_mcp_ai-2.5.11.dist-info → mobile_mcp_ai-2.6.0.dist-info}/METADATA +1 -1
- {mobile_mcp_ai-2.5.11.dist-info → mobile_mcp_ai-2.6.0.dist-info}/RECORD +8 -8
- {mobile_mcp_ai-2.5.11.dist-info → mobile_mcp_ai-2.6.0.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.5.11.dist-info → mobile_mcp_ai-2.6.0.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.5.11.dist-info → mobile_mcp_ai-2.6.0.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.5.11.dist-info → mobile_mcp_ai-2.6.0.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,34 +56,157 @@ class BasicMobileToolsLite:
|
|
|
53
56
|
}
|
|
54
57
|
self.operation_history.append(record)
|
|
55
58
|
|
|
56
|
-
def
|
|
57
|
-
"""
|
|
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
|
+
"""检查是否已跳出目标应用
|
|
58
75
|
|
|
59
|
-
|
|
76
|
+
Returns:
|
|
77
|
+
{
|
|
78
|
+
'switched': bool, # 是否跳转
|
|
79
|
+
'current_package': str, # 当前应用包名
|
|
80
|
+
'target_package': str, # 目标应用包名
|
|
81
|
+
'message': str # 提示信息
|
|
82
|
+
}
|
|
60
83
|
"""
|
|
61
|
-
|
|
84
|
+
if not self.target_package:
|
|
85
|
+
return {
|
|
86
|
+
'switched': False,
|
|
87
|
+
'current_package': None,
|
|
88
|
+
'target_package': None,
|
|
89
|
+
'message': '⚠️ 未设置目标应用,无法监测应用跳转'
|
|
90
|
+
}
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
137
|
|
|
70
|
-
# Android: 优先使用 ADB 直接 dump
|
|
71
138
|
try:
|
|
72
|
-
#
|
|
73
|
-
self.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
79
203
|
except Exception as e:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
204
|
+
return {
|
|
205
|
+
'success': False,
|
|
206
|
+
'message': f'❌ 返回目标应用失败: {e}',
|
|
207
|
+
'method': None
|
|
208
|
+
}
|
|
209
|
+
|
|
84
210
|
|
|
85
211
|
# ==================== 截图 ====================
|
|
86
212
|
|
|
@@ -381,7 +507,7 @@ class BasicMobileToolsLite:
|
|
|
381
507
|
if show_popup_hints and not self._is_ios():
|
|
382
508
|
try:
|
|
383
509
|
import xml.etree.ElementTree as ET
|
|
384
|
-
xml_string = self.
|
|
510
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
385
511
|
root = ET.fromstring(xml_string)
|
|
386
512
|
|
|
387
513
|
# 检测弹窗区域
|
|
@@ -558,7 +684,7 @@ class BasicMobileToolsLite:
|
|
|
558
684
|
else:
|
|
559
685
|
try:
|
|
560
686
|
import xml.etree.ElementTree as ET
|
|
561
|
-
xml_string = self.
|
|
687
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
562
688
|
root = ET.fromstring(xml_string)
|
|
563
689
|
|
|
564
690
|
for elem in root.iter():
|
|
@@ -963,25 +1089,41 @@ class BasicMobileToolsLite:
|
|
|
963
1089
|
ref=f"coords_{x}_{y}"
|
|
964
1090
|
)
|
|
965
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
|
+
# 构建返回消息
|
|
966
1101
|
if converted:
|
|
967
1102
|
if conversion_type == "crop_offset":
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
"message": f"✅ 点击成功: ({x}, {y})\n"
|
|
971
|
-
f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
|
|
972
|
-
}
|
|
1103
|
+
msg = f"✅ 点击成功: ({x}, {y})\n" \
|
|
1104
|
+
f" 🔍 局部截图坐标转换: ({original_x},{original_y}) + 偏移({crop_offset_x},{crop_offset_y}) → ({x},{y})"
|
|
973
1105
|
else:
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
f" 📐 坐标已转换: ({original_x},{original_y}) → ({x},{y})\n"
|
|
978
|
-
f" 🖼️ 图片尺寸: {image_width}x{image_height} → 屏幕: {screen_width}x{screen_height}"
|
|
979
|
-
}
|
|
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}"
|
|
980
1109
|
else:
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
+
}
|
|
985
1127
|
except Exception as e:
|
|
986
1128
|
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
987
1129
|
|
|
@@ -1114,9 +1256,9 @@ class BasicMobileToolsLite:
|
|
|
1114
1256
|
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
1115
1257
|
|
|
1116
1258
|
def _find_element_in_tree(self, text: str) -> Optional[Dict]:
|
|
1117
|
-
"""在 XML
|
|
1259
|
+
"""在 XML 树中查找包含指定文本的元素"""
|
|
1118
1260
|
try:
|
|
1119
|
-
xml = self.
|
|
1261
|
+
xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
1120
1262
|
import xml.etree.ElementTree as ET
|
|
1121
1263
|
root = ET.fromstring(xml)
|
|
1122
1264
|
|
|
@@ -1510,7 +1652,28 @@ class BasicMobileToolsLite:
|
|
|
1510
1652
|
elem.set_text(text)
|
|
1511
1653
|
time.sleep(0.3)
|
|
1512
1654
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1513
|
-
|
|
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
|
+
}
|
|
1514
1677
|
return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
|
|
1515
1678
|
else:
|
|
1516
1679
|
elements = self.client.u2(resourceId=resource_id)
|
|
@@ -1524,7 +1687,28 @@ class BasicMobileToolsLite:
|
|
|
1524
1687
|
elements.set_text(text)
|
|
1525
1688
|
time.sleep(0.3)
|
|
1526
1689
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1527
|
-
|
|
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
|
+
}
|
|
1528
1712
|
|
|
1529
1713
|
# 多个相同 ID(<=5个),尝试智能选择
|
|
1530
1714
|
if count <= 5:
|
|
@@ -1537,14 +1721,56 @@ class BasicMobileToolsLite:
|
|
|
1537
1721
|
elem.set_text(text)
|
|
1538
1722
|
time.sleep(0.3)
|
|
1539
1723
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1540
|
-
|
|
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
|
+
}
|
|
1541
1746
|
except:
|
|
1542
1747
|
continue
|
|
1543
1748
|
# 没找到可编辑的,用第一个
|
|
1544
1749
|
elements[0].set_text(text)
|
|
1545
1750
|
time.sleep(0.3)
|
|
1546
1751
|
self._record_operation('input', element=resource_id, ref=resource_id, text=text)
|
|
1547
|
-
|
|
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
|
+
}
|
|
1548
1774
|
|
|
1549
1775
|
# ID 不可靠(不存在或太多),改用 EditText 类型定位
|
|
1550
1776
|
edit_texts = self.client.u2(className='android.widget.EditText')
|
|
@@ -1554,7 +1780,28 @@ class BasicMobileToolsLite:
|
|
|
1554
1780
|
edit_texts.set_text(text)
|
|
1555
1781
|
time.sleep(0.3)
|
|
1556
1782
|
self._record_operation('input', element='EditText', ref='EditText', text=text)
|
|
1557
|
-
|
|
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
|
+
}
|
|
1558
1805
|
|
|
1559
1806
|
# 多个 EditText,选择最靠上的
|
|
1560
1807
|
best_elem = None
|
|
@@ -1573,7 +1820,28 @@ class BasicMobileToolsLite:
|
|
|
1573
1820
|
best_elem.set_text(text)
|
|
1574
1821
|
time.sleep(0.3)
|
|
1575
1822
|
self._record_operation('input', element='EditText', ref='EditText', text=text)
|
|
1576
|
-
|
|
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
|
+
}
|
|
1577
1845
|
|
|
1578
1846
|
return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
|
|
1579
1847
|
|
|
@@ -1625,7 +1893,29 @@ class BasicMobileToolsLite:
|
|
|
1625
1893
|
text=text
|
|
1626
1894
|
)
|
|
1627
1895
|
|
|
1628
|
-
|
|
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
|
+
}
|
|
1629
1919
|
except Exception as e:
|
|
1630
1920
|
return {"success": False, "message": f"❌ 输入失败: {e}"}
|
|
1631
1921
|
|
|
@@ -1692,6 +1982,14 @@ class BasicMobileToolsLite:
|
|
|
1692
1982
|
record_info['y_percent'] = y_percent
|
|
1693
1983
|
self._record_operation('swipe', **record_info)
|
|
1694
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
|
+
|
|
1695
1993
|
# 构建返回消息
|
|
1696
1994
|
msg = f"✅ 滑动成功: {direction}"
|
|
1697
1995
|
if direction in ['left', 'right']:
|
|
@@ -1700,7 +1998,21 @@ class BasicMobileToolsLite:
|
|
|
1700
1998
|
elif y is not None:
|
|
1701
1999
|
msg += f" (高度: {y}px)"
|
|
1702
2000
|
|
|
1703
|
-
|
|
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
|
+
}
|
|
1704
2016
|
except Exception as e:
|
|
1705
2017
|
return {"success": False, "message": f"❌ 滑动失败: {e}"}
|
|
1706
2018
|
|
|
@@ -1756,11 +2068,22 @@ class BasicMobileToolsLite:
|
|
|
1756
2068
|
|
|
1757
2069
|
await asyncio.sleep(2)
|
|
1758
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
|
+
|
|
1759
2082
|
self._record_operation('launch_app', package_name=package_name)
|
|
1760
2083
|
|
|
1761
2084
|
return {
|
|
1762
2085
|
"success": True,
|
|
1763
|
-
"message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3
|
|
2086
|
+
"message": f"✅ 已启动: {package_name}\n💡 建议等待 2-3 秒让页面加载\n📱 已设置应用状态监测"
|
|
1764
2087
|
}
|
|
1765
2088
|
except Exception as e:
|
|
1766
2089
|
return {"success": False, "message": f"❌ 启动失败: {e}"}
|
|
@@ -1850,7 +2173,7 @@ class BasicMobileToolsLite:
|
|
|
1850
2173
|
# ==================== 辅助工具 ====================
|
|
1851
2174
|
|
|
1852
2175
|
def list_elements(self) -> List[Dict]:
|
|
1853
|
-
"""
|
|
2176
|
+
"""列出页面元素(已优化:过滤排版容器,保留功能控件)"""
|
|
1854
2177
|
try:
|
|
1855
2178
|
if self._is_ios():
|
|
1856
2179
|
ios_client = self._get_ios_client()
|
|
@@ -1858,23 +2181,143 @@ class BasicMobileToolsLite:
|
|
|
1858
2181
|
return ios_client.list_elements()
|
|
1859
2182
|
return [{"error": "iOS 暂不支持元素列表,建议使用截图"}]
|
|
1860
2183
|
else:
|
|
1861
|
-
xml_string = self.
|
|
2184
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
1862
2185
|
elements = self.client.xml_parser.parse(xml_string)
|
|
1863
2186
|
|
|
2187
|
+
# 功能控件类型(需要保留)
|
|
2188
|
+
FUNCTIONAL_WIDGETS = {
|
|
2189
|
+
'TextView', 'Text', 'Label', # 文本类
|
|
2190
|
+
'ImageView', 'Image', 'ImageButton', # 图片类
|
|
2191
|
+
'Button', 'CheckBox', 'RadioButton', 'Switch', # 交互类
|
|
2192
|
+
'SeekBar', 'ProgressBar', 'RatingBar', # 滑动/进度类
|
|
2193
|
+
'EditText', 'TextInput', # 输入类
|
|
2194
|
+
'VideoView', 'WebView', # 特殊功能类
|
|
2195
|
+
'RecyclerView', 'ListView', 'GridView', # 列表类
|
|
2196
|
+
'ScrollView', 'NestedScrollView', # 滚动容器(有实际功能)
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
# 容器控件类型(需要过滤,除非有业务ID)
|
|
2200
|
+
CONTAINER_WIDGETS = {
|
|
2201
|
+
'FrameLayout', 'LinearLayout', 'RelativeLayout',
|
|
2202
|
+
'ViewGroup', 'ConstraintLayout', 'CoordinatorLayout',
|
|
2203
|
+
'CardView', 'View', # 基础View也可能只是容器
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
# 装饰类控件关键词(resource_id中包含这些关键词的通常可以过滤)
|
|
2207
|
+
# 支持匹配如 qylt_item_short_video_shadow_one 这样的命名
|
|
2208
|
+
DECORATIVE_KEYWORDS = {
|
|
2209
|
+
'shadow', 'divider', 'separator', 'line', 'border',
|
|
2210
|
+
'background', 'bg_', '_bg', 'decorative', 'decoration',
|
|
2211
|
+
'_shadow', 'shadow_', '_divider', 'divider_', '_line', 'line_'
|
|
2212
|
+
}
|
|
2213
|
+
|
|
1864
2214
|
result = []
|
|
1865
2215
|
for elem in elements:
|
|
1866
|
-
|
|
2216
|
+
# 获取元素属性
|
|
2217
|
+
class_name = elem.get('class_name', '')
|
|
2218
|
+
resource_id = elem.get('resource_id', '').strip()
|
|
2219
|
+
text = elem.get('text', '').strip()
|
|
2220
|
+
content_desc = elem.get('content_desc', '').strip()
|
|
2221
|
+
bounds = elem.get('bounds', '')
|
|
2222
|
+
clickable = elem.get('clickable', False)
|
|
2223
|
+
focusable = elem.get('focusable', False)
|
|
2224
|
+
scrollable = elem.get('scrollable', False)
|
|
2225
|
+
enabled = elem.get('enabled', True)
|
|
2226
|
+
|
|
2227
|
+
# 1. 过滤 bounds="[0,0][0,0]" 的视觉隐藏元素
|
|
2228
|
+
if bounds == '[0,0][0,0]':
|
|
2229
|
+
continue
|
|
2230
|
+
|
|
2231
|
+
# 2. 检查是否是功能控件(直接保留)
|
|
2232
|
+
if class_name in FUNCTIONAL_WIDGETS:
|
|
1867
2233
|
result.append({
|
|
1868
|
-
'resource_id':
|
|
1869
|
-
'text':
|
|
1870
|
-
'content_desc':
|
|
1871
|
-
'bounds':
|
|
1872
|
-
'clickable':
|
|
2234
|
+
'resource_id': resource_id,
|
|
2235
|
+
'text': text,
|
|
2236
|
+
'content_desc': content_desc,
|
|
2237
|
+
'bounds': bounds,
|
|
2238
|
+
'clickable': clickable,
|
|
2239
|
+
'class': class_name
|
|
1873
2240
|
})
|
|
2241
|
+
continue
|
|
2242
|
+
|
|
2243
|
+
# 3. 检查是否是容器控件
|
|
2244
|
+
if class_name in CONTAINER_WIDGETS:
|
|
2245
|
+
# 容器控件需要检查是否有业务相关的ID
|
|
2246
|
+
has_business_id = self._has_business_id(resource_id)
|
|
2247
|
+
if not has_business_id:
|
|
2248
|
+
# 无业务ID的容器控件,检查是否有其他有意义属性
|
|
2249
|
+
if not (clickable or focusable or scrollable or text or content_desc):
|
|
2250
|
+
# 所有属性都是默认值,过滤掉
|
|
2251
|
+
continue
|
|
2252
|
+
# 有业务ID或其他有意义属性,保留
|
|
2253
|
+
result.append({
|
|
2254
|
+
'resource_id': resource_id,
|
|
2255
|
+
'text': text,
|
|
2256
|
+
'content_desc': content_desc,
|
|
2257
|
+
'bounds': bounds,
|
|
2258
|
+
'clickable': clickable,
|
|
2259
|
+
'class': class_name
|
|
2260
|
+
})
|
|
2261
|
+
continue
|
|
2262
|
+
|
|
2263
|
+
# 4. 检查是否是装饰类控件
|
|
2264
|
+
if resource_id:
|
|
2265
|
+
resource_id_lower = resource_id.lower()
|
|
2266
|
+
if any(keyword in resource_id_lower for keyword in DECORATIVE_KEYWORDS):
|
|
2267
|
+
# 是装饰类控件,且没有交互属性,过滤掉
|
|
2268
|
+
if not (clickable or focusable or text or content_desc):
|
|
2269
|
+
continue
|
|
2270
|
+
|
|
2271
|
+
# 5. 检查是否所有属性均为默认值
|
|
2272
|
+
if not (text or content_desc or resource_id or clickable or focusable or scrollable):
|
|
2273
|
+
# 所有属性都是默认值,过滤掉
|
|
2274
|
+
continue
|
|
2275
|
+
|
|
2276
|
+
# 6. 其他情况:有意义的元素保留
|
|
2277
|
+
result.append({
|
|
2278
|
+
'resource_id': resource_id,
|
|
2279
|
+
'text': text,
|
|
2280
|
+
'content_desc': content_desc,
|
|
2281
|
+
'bounds': bounds,
|
|
2282
|
+
'clickable': clickable,
|
|
2283
|
+
'class': class_name
|
|
2284
|
+
})
|
|
2285
|
+
|
|
1874
2286
|
return result
|
|
1875
2287
|
except Exception as e:
|
|
1876
2288
|
return [{"error": f"获取元素失败: {e}"}]
|
|
1877
2289
|
|
|
2290
|
+
def _has_business_id(self, resource_id: str) -> bool:
|
|
2291
|
+
"""
|
|
2292
|
+
判断resource_id是否是业务相关的ID
|
|
2293
|
+
|
|
2294
|
+
业务相关的ID通常包含:
|
|
2295
|
+
- 有意义的命名(不是自动生成的)
|
|
2296
|
+
- 不包含常见的自动生成模式
|
|
2297
|
+
"""
|
|
2298
|
+
if not resource_id:
|
|
2299
|
+
return False
|
|
2300
|
+
|
|
2301
|
+
# 自动生成的ID模式(通常可以忽略)
|
|
2302
|
+
auto_generated_patterns = [
|
|
2303
|
+
r'^android:id/', # 系统ID
|
|
2304
|
+
r':id/\d+', # 数字ID
|
|
2305
|
+
r':id/view_\d+', # view_数字
|
|
2306
|
+
r':id/item_\d+', # item_数字
|
|
2307
|
+
]
|
|
2308
|
+
|
|
2309
|
+
for pattern in auto_generated_patterns:
|
|
2310
|
+
if re.search(pattern, resource_id):
|
|
2311
|
+
return False
|
|
2312
|
+
|
|
2313
|
+
# 如果resource_id有实际内容且不是自动生成的,认为是业务ID
|
|
2314
|
+
# 排除一些常见的系统ID
|
|
2315
|
+
system_ids = ['android:id/content', 'android:id/statusBarBackground']
|
|
2316
|
+
if resource_id in system_ids:
|
|
2317
|
+
return False
|
|
2318
|
+
|
|
2319
|
+
return True
|
|
2320
|
+
|
|
1878
2321
|
def find_close_button(self) -> Dict:
|
|
1879
2322
|
"""智能查找关闭按钮(不点击,只返回位置)
|
|
1880
2323
|
|
|
@@ -1894,8 +2337,8 @@ class BasicMobileToolsLite:
|
|
|
1894
2337
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
1895
2338
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
1896
2339
|
|
|
1897
|
-
#
|
|
1898
|
-
xml_string = self.
|
|
2340
|
+
# 获取元素列表
|
|
2341
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
1899
2342
|
import xml.etree.ElementTree as ET
|
|
1900
2343
|
root = ET.fromstring(xml_string)
|
|
1901
2344
|
|
|
@@ -1906,7 +2349,6 @@ class BasicMobileToolsLite:
|
|
|
1906
2349
|
for elem in root.iter():
|
|
1907
2350
|
text = elem.attrib.get('text', '')
|
|
1908
2351
|
content_desc = elem.attrib.get('content-desc', '')
|
|
1909
|
-
resource_id = elem.attrib.get('resource-id', '')
|
|
1910
2352
|
bounds_str = elem.attrib.get('bounds', '')
|
|
1911
2353
|
class_name = elem.attrib.get('class', '')
|
|
1912
2354
|
clickable = elem.attrib.get('clickable', 'false') == 'true'
|
|
@@ -1941,13 +2383,6 @@ class BasicMobileToolsLite:
|
|
|
1941
2383
|
score = 90
|
|
1942
2384
|
reason = f"描述='{content_desc}'"
|
|
1943
2385
|
|
|
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
|
-
|
|
1951
2386
|
# 策略3:小尺寸的 clickable 元素(可能是 X 图标)
|
|
1952
2387
|
elif clickable:
|
|
1953
2388
|
min_size = max(20, int(screen_width * 0.03))
|
|
@@ -1982,9 +2417,7 @@ class BasicMobileToolsLite:
|
|
|
1982
2417
|
'center_y': center_y,
|
|
1983
2418
|
'x_percent': x_percent,
|
|
1984
2419
|
'y_percent': y_percent,
|
|
1985
|
-
'size': f"{width}x{height}"
|
|
1986
|
-
'resource_id': resource_id,
|
|
1987
|
-
'text': text
|
|
2420
|
+
'size': f"{width}x{height}"
|
|
1988
2421
|
})
|
|
1989
2422
|
|
|
1990
2423
|
if not candidates:
|
|
@@ -2010,16 +2443,7 @@ class BasicMobileToolsLite:
|
|
|
2010
2443
|
candidates.sort(key=lambda x: x['score'], reverse=True)
|
|
2011
2444
|
best = candidates[0]
|
|
2012
2445
|
|
|
2013
|
-
|
|
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 = {
|
|
2446
|
+
return {
|
|
2023
2447
|
"success": True,
|
|
2024
2448
|
"message": f"✅ 找到可能的关闭按钮",
|
|
2025
2449
|
"best_candidate": {
|
|
@@ -2030,7 +2454,7 @@ class BasicMobileToolsLite:
|
|
|
2030
2454
|
"size": best['size'],
|
|
2031
2455
|
"score": best['score']
|
|
2032
2456
|
},
|
|
2033
|
-
"click_command":
|
|
2457
|
+
"click_command": f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})",
|
|
2034
2458
|
"other_candidates": [
|
|
2035
2459
|
{"reason": c['reason'], "percent": f"({c['x_percent']}%, {c['y_percent']}%)", "score": c['score']}
|
|
2036
2460
|
for c in candidates[1:4]
|
|
@@ -2038,14 +2462,6 @@ class BasicMobileToolsLite:
|
|
|
2038
2462
|
"screen_size": {"width": screen_width, "height": screen_height}
|
|
2039
2463
|
}
|
|
2040
2464
|
|
|
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
|
-
|
|
2049
2465
|
except Exception as e:
|
|
2050
2466
|
return {"success": False, "message": f"❌ 查找关闭按钮失败: {e}"}
|
|
2051
2467
|
|
|
@@ -2075,8 +2491,8 @@ class BasicMobileToolsLite:
|
|
|
2075
2491
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
2076
2492
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
2077
2493
|
|
|
2078
|
-
# 获取原始 XML
|
|
2079
|
-
xml_string = self.
|
|
2494
|
+
# 获取原始 XML
|
|
2495
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
2080
2496
|
|
|
2081
2497
|
# 关闭按钮的文本特征
|
|
2082
2498
|
close_texts = ['×', 'X', 'x', '关闭', '取消', 'close', 'Close', 'CLOSE', '跳过', '知道了']
|
|
@@ -2305,13 +2721,33 @@ class BasicMobileToolsLite:
|
|
|
2305
2721
|
self.client.u2.click(try_x, try_y)
|
|
2306
2722
|
time.sleep(0.3)
|
|
2307
2723
|
|
|
2724
|
+
# 🎯 关键步骤:检查应用是否跳转,如果跳转说明弹窗去除失败,需要返回目标应用
|
|
2725
|
+
app_check = self._check_app_switched()
|
|
2726
|
+
return_result = None
|
|
2727
|
+
|
|
2728
|
+
if app_check['switched']:
|
|
2729
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
2730
|
+
return_result = self._return_to_target_app()
|
|
2731
|
+
|
|
2308
2732
|
# 尝试后截图,让 AI 判断是否成功
|
|
2309
2733
|
screenshot_result = self.take_screenshot("尝试关闭后")
|
|
2734
|
+
|
|
2735
|
+
msg = f"✅ 已尝试点击常见关闭按钮位置"
|
|
2736
|
+
if app_check['switched']:
|
|
2737
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
2738
|
+
if return_result:
|
|
2739
|
+
if return_result['success']:
|
|
2740
|
+
msg += f"\n{return_result['message']}"
|
|
2741
|
+
else:
|
|
2742
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
2743
|
+
|
|
2310
2744
|
return {
|
|
2311
2745
|
"success": True,
|
|
2312
|
-
"message":
|
|
2746
|
+
"message": msg,
|
|
2313
2747
|
"tried_positions": [p[2] for p in try_positions],
|
|
2314
2748
|
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2749
|
+
"app_check": app_check,
|
|
2750
|
+
"return_to_app": return_result,
|
|
2315
2751
|
"tip": "请查看截图确认弹窗是否已关闭。如果还在,可手动分析截图找到关闭按钮位置。"
|
|
2316
2752
|
}
|
|
2317
2753
|
|
|
@@ -2344,6 +2780,14 @@ class BasicMobileToolsLite:
|
|
|
2344
2780
|
self.client.u2.click(best['center_x'], best['center_y'])
|
|
2345
2781
|
time.sleep(0.5)
|
|
2346
2782
|
|
|
2783
|
+
# 🎯 关键步骤:检查应用是否跳转,如果跳转说明弹窗去除失败,需要返回目标应用
|
|
2784
|
+
app_check = self._check_app_switched()
|
|
2785
|
+
return_result = None
|
|
2786
|
+
|
|
2787
|
+
if app_check['switched']:
|
|
2788
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
2789
|
+
return_result = self._return_to_target_app()
|
|
2790
|
+
|
|
2347
2791
|
# 点击后截图,让 AI 判断是否成功
|
|
2348
2792
|
screenshot_result = self.take_screenshot("关闭弹窗后")
|
|
2349
2793
|
|
|
@@ -2359,11 +2803,21 @@ class BasicMobileToolsLite:
|
|
|
2359
2803
|
ref=f"close_popup_{best['position']}"
|
|
2360
2804
|
)
|
|
2361
2805
|
|
|
2806
|
+
# 构建返回消息
|
|
2807
|
+
msg = f"✅ 已点击关闭按钮 ({best['position']}): ({best['center_x']}, {best['center_y']})"
|
|
2808
|
+
if app_check['switched']:
|
|
2809
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
2810
|
+
if return_result:
|
|
2811
|
+
if return_result['success']:
|
|
2812
|
+
msg += f"\n{return_result['message']}"
|
|
2813
|
+
else:
|
|
2814
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
2815
|
+
|
|
2362
2816
|
# 返回候选按钮列表,让 AI 看截图判断
|
|
2363
2817
|
# 如果弹窗还在,AI 可以选择点击其他候选按钮
|
|
2364
2818
|
return {
|
|
2365
2819
|
"success": True,
|
|
2366
|
-
"message":
|
|
2820
|
+
"message": msg,
|
|
2367
2821
|
"clicked": {
|
|
2368
2822
|
"position": best['position'],
|
|
2369
2823
|
"match_type": best['match_type'],
|
|
@@ -2373,6 +2827,8 @@ class BasicMobileToolsLite:
|
|
|
2373
2827
|
"screenshot": screenshot_result.get("screenshot_path", ""),
|
|
2374
2828
|
"popup_detected": popup_bounds is not None,
|
|
2375
2829
|
"popup_bounds": f"[{popup_bounds[0]},{popup_bounds[1]}][{popup_bounds[2]},{popup_bounds[3]}]" if popup_bounds else None,
|
|
2830
|
+
"app_check": app_check,
|
|
2831
|
+
"return_to_app": return_result,
|
|
2376
2832
|
"other_candidates": [
|
|
2377
2833
|
{
|
|
2378
2834
|
"position": c['position'],
|
|
@@ -2382,7 +2838,7 @@ class BasicMobileToolsLite:
|
|
|
2382
2838
|
}
|
|
2383
2839
|
for c in close_candidates[1:4] # 返回其他3个候选,AI 可以选择
|
|
2384
2840
|
],
|
|
2385
|
-
"tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates
|
|
2841
|
+
"tip": "请查看截图判断弹窗是否已关闭。如果弹窗还在,可以尝试点击 other_candidates 中的其他位置"
|
|
2386
2842
|
}
|
|
2387
2843
|
|
|
2388
2844
|
except Exception as e:
|
|
@@ -2947,8 +3403,8 @@ class BasicMobileToolsLite:
|
|
|
2947
3403
|
try:
|
|
2948
3404
|
import xml.etree.ElementTree as ET
|
|
2949
3405
|
|
|
2950
|
-
# ========== 第1
|
|
2951
|
-
xml_string = self.
|
|
3406
|
+
# ========== 第1步:控件树查找关闭按钮 ==========
|
|
3407
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=False)
|
|
2952
3408
|
root = ET.fromstring(xml_string)
|
|
2953
3409
|
|
|
2954
3410
|
# 关闭按钮的常见特征
|
|
@@ -2986,16 +3442,6 @@ class BasicMobileToolsLite:
|
|
|
2986
3442
|
reason = f"文本含'{kw}'"
|
|
2987
3443
|
break
|
|
2988
3444
|
|
|
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
|
-
|
|
2999
3445
|
# content-desc 匹配
|
|
3000
3446
|
for kw in close_content_desc:
|
|
3001
3447
|
if kw.lower() in content_desc.lower():
|
|
@@ -3045,15 +3491,35 @@ class BasicMobileToolsLite:
|
|
|
3045
3491
|
pre_result = self.take_screenshot(description="关闭前", compress=False)
|
|
3046
3492
|
pre_screenshot = pre_result.get("screenshot_path")
|
|
3047
3493
|
|
|
3048
|
-
#
|
|
3049
|
-
self.click_at_coords(cx, cy)
|
|
3494
|
+
# 点击(click_at_coords 内部已包含应用状态检查和自动返回)
|
|
3495
|
+
click_result = self.click_at_coords(cx, cy)
|
|
3050
3496
|
time.sleep(0.5)
|
|
3051
3497
|
|
|
3498
|
+
# 🎯 再次检查应用状态(确保弹窗去除没有导致应用跳转)
|
|
3499
|
+
app_check = self._check_app_switched()
|
|
3500
|
+
return_result = None
|
|
3501
|
+
|
|
3502
|
+
if app_check['switched']:
|
|
3503
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
3504
|
+
return_result = self._return_to_target_app()
|
|
3505
|
+
|
|
3052
3506
|
result["success"] = True
|
|
3053
3507
|
result["method"] = "控件树"
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3508
|
+
msg = f"✅ 通过控件树找到关闭按钮并点击\n" \
|
|
3509
|
+
f" 位置: ({cx}, {cy})\n" \
|
|
3510
|
+
f" 原因: {best['reason']}"
|
|
3511
|
+
|
|
3512
|
+
if app_check['switched']:
|
|
3513
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
3514
|
+
if return_result:
|
|
3515
|
+
if return_result['success']:
|
|
3516
|
+
msg += f"\n{return_result['message']}"
|
|
3517
|
+
else:
|
|
3518
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
3519
|
+
|
|
3520
|
+
result["message"] = msg
|
|
3521
|
+
result["app_check"] = app_check
|
|
3522
|
+
result["return_to_app"] = return_result
|
|
3057
3523
|
|
|
3058
3524
|
# 自动学习:检查这个 X 是否已在模板库,不在就添加
|
|
3059
3525
|
if auto_learn and pre_screenshot:
|
|
@@ -3083,16 +3549,36 @@ class BasicMobileToolsLite:
|
|
|
3083
3549
|
x_pct = best["percent"]["x"]
|
|
3084
3550
|
y_pct = best["percent"]["y"]
|
|
3085
3551
|
|
|
3086
|
-
#
|
|
3087
|
-
self.click_by_percent(x_pct, y_pct)
|
|
3552
|
+
# 点击(click_by_percent 内部已包含应用状态检查和自动返回)
|
|
3553
|
+
click_result = self.click_by_percent(x_pct, y_pct)
|
|
3088
3554
|
time.sleep(0.5)
|
|
3089
3555
|
|
|
3556
|
+
# 🎯 再次检查应用状态(确保弹窗去除没有导致应用跳转)
|
|
3557
|
+
app_check = self._check_app_switched()
|
|
3558
|
+
return_result = None
|
|
3559
|
+
|
|
3560
|
+
if app_check['switched']:
|
|
3561
|
+
# 应用已跳转,说明弹窗去除失败,尝试返回目标应用
|
|
3562
|
+
return_result = self._return_to_target_app()
|
|
3563
|
+
|
|
3090
3564
|
result["success"] = True
|
|
3091
3565
|
result["method"] = "模板匹配"
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3566
|
+
msg = f"✅ 通过模板匹配找到关闭按钮并点击\n" \
|
|
3567
|
+
f" 模板: {best.get('template', 'unknown')}\n" \
|
|
3568
|
+
f" 置信度: {best.get('confidence', 'N/A')}%\n" \
|
|
3569
|
+
f" 位置: ({x_pct:.1f}%, {y_pct:.1f}%)"
|
|
3570
|
+
|
|
3571
|
+
if app_check['switched']:
|
|
3572
|
+
msg += f"\n⚠️ 应用已跳转,说明弹窗去除失败"
|
|
3573
|
+
if return_result:
|
|
3574
|
+
if return_result['success']:
|
|
3575
|
+
msg += f"\n{return_result['message']}"
|
|
3576
|
+
else:
|
|
3577
|
+
msg += f"\n❌ 自动返回失败: {return_result['message']}"
|
|
3578
|
+
|
|
3579
|
+
result["message"] = msg
|
|
3580
|
+
result["app_check"] = app_check
|
|
3581
|
+
result["return_to_app"] = return_result
|
|
3096
3582
|
return result
|
|
3097
3583
|
|
|
3098
3584
|
except ImportError:
|
|
@@ -610,25 +610,22 @@ class MobileMCPServer:
|
|
|
610
610
|
name="mobile_find_close_button",
|
|
611
611
|
description="""🔍 智能查找关闭按钮(只找不点,返回位置)
|
|
612
612
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
从元素树中找最可能的关闭按钮,返回坐标和推荐的点击命令。
|
|
613
|
+
从元素树中找最可能的关闭按钮,返回坐标和百分比位置。
|
|
616
614
|
|
|
617
615
|
🎯 识别策略(优先级):
|
|
618
|
-
1. 文本匹配:×、X、关闭、取消、跳过
|
|
619
|
-
2.
|
|
620
|
-
3.
|
|
621
|
-
4. 小尺寸 clickable 元素(右上角优先,得分70+)
|
|
616
|
+
1. 文本匹配:×、X、关闭、取消、跳过 等
|
|
617
|
+
2. 描述匹配:content-desc 包含 close/关闭
|
|
618
|
+
3. 小尺寸 clickable 元素(右上角优先)
|
|
622
619
|
|
|
623
620
|
✅ 返回内容:
|
|
624
621
|
- 坐标 (x, y) 和百分比 (x%, y%)
|
|
625
|
-
-
|
|
626
|
-
-
|
|
622
|
+
- 推荐的点击命令:mobile_click_by_percent(x%, y%)
|
|
623
|
+
- 多个候选位置(供确认)
|
|
627
624
|
|
|
628
625
|
💡 使用流程:
|
|
629
|
-
1.
|
|
630
|
-
2.
|
|
631
|
-
3.
|
|
626
|
+
1. 调用此工具找到关闭按钮位置
|
|
627
|
+
2. 确认位置正确后,用 mobile_click_by_percent 点击
|
|
628
|
+
3. 百分比点击兼容不同分辨率手机""",
|
|
632
629
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
633
630
|
))
|
|
634
631
|
|
|
@@ -714,26 +711,24 @@ class MobileMCPServer:
|
|
|
714
711
|
name="mobile_close_ad",
|
|
715
712
|
description="""🚫 【推荐】智能关闭广告弹窗
|
|
716
713
|
|
|
717
|
-
|
|
714
|
+
专门用于关闭广告弹窗,按优先级自动尝试多种方式:
|
|
718
715
|
|
|
719
|
-
1️⃣
|
|
720
|
-
- 自动查找
|
|
721
|
-
- 查找文本"关闭"、"跳过"、"×"等
|
|
716
|
+
1️⃣ **控件树查找**(最可靠)
|
|
717
|
+
- 自动查找"关闭"、"跳过"、"×"等关闭按钮
|
|
722
718
|
- 找到直接点击,实时可靠
|
|
723
719
|
|
|
724
720
|
2️⃣ **模板匹配**(次优)
|
|
725
721
|
- 用 OpenCV 匹配已保存的 X 按钮模板
|
|
726
|
-
-
|
|
722
|
+
- 需要积累模板库,模板越多成功率越高
|
|
727
723
|
|
|
728
724
|
3️⃣ **返回截图供 AI 分析**(兜底)
|
|
729
|
-
-
|
|
725
|
+
- 如果前两步失败,返回截图
|
|
730
726
|
- AI 分析后用 mobile_click_by_percent 点击
|
|
731
|
-
- 点击成功后用 mobile_template_add
|
|
727
|
+
- 点击成功后用 mobile_template_add 添加模板(自动学习)
|
|
732
728
|
|
|
733
|
-
💡
|
|
734
|
-
1. 遇到广告弹窗 →
|
|
729
|
+
💡 使用流程:
|
|
730
|
+
1. 遇到广告弹窗 → 调用此工具
|
|
735
731
|
2. 如果成功 → 完成
|
|
736
|
-
3. 只有失败时才需要截图分析
|
|
737
732
|
3. 如果失败 → 看截图找 X → 点击 → 添加模板""",
|
|
738
733
|
inputSchema={
|
|
739
734
|
"type": "object",
|
|
@@ -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=
|
|
4
|
+
mobile_mcp/core/basic_tools_lite.py,sha256=7QbmeNduFm-KDuKMGMmlnC3HqzNAlKFHcNpquicakAs,179209
|
|
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=
|
|
22
|
+
mobile_mcp/mcp_tools/mcp_server.py,sha256=fr42BjulYQCBreWoLx5lkWEZjT60Df6RCo_kyXMOHfI,49868
|
|
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.
|
|
28
|
-
mobile_mcp_ai-2.
|
|
29
|
-
mobile_mcp_ai-2.
|
|
30
|
-
mobile_mcp_ai-2.
|
|
31
|
-
mobile_mcp_ai-2.
|
|
32
|
-
mobile_mcp_ai-2.
|
|
27
|
+
mobile_mcp_ai-2.6.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
28
|
+
mobile_mcp_ai-2.6.0.dist-info/METADATA,sha256=QeGdsOiTatw7dLmpFxeWxvIXt2bG3zuIt-HXlm-9XDw,10495
|
|
29
|
+
mobile_mcp_ai-2.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
+
mobile_mcp_ai-2.6.0.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
|
|
31
|
+
mobile_mcp_ai-2.6.0.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
|
|
32
|
+
mobile_mcp_ai-2.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|