mobile-mcp-ai 2.5.5__tar.gz → 2.5.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mobile_mcp_ai-2.5.5/mobile_mcp_ai.egg-info → mobile_mcp_ai-2.5.9}/PKG-INFO +1 -1
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/__init__.py +1 -1
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/basic_tools_lite.py +93 -28
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/device_manager.py +2 -2
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/mobile_client.py +31 -10
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/utils/smart_wait.py +3 -3
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mcp_tools/mcp_server.py +8 -3
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9/mobile_mcp_ai.egg-info}/PKG-INFO +1 -1
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mobile_mcp_ai.egg-info/SOURCES.txt +10 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/setup.py +1 -1
- mobile_mcp_ai-2.5.9/tests/test_mind_cloud_my_space.py +80 -0
- mobile_mcp_ai-2.5.9/tests/test_mind_correct.py +73 -0
- mobile_mcp_ai-2.5.9/tests/test_mind_improved.py +83 -0
- mobile_mcp_ai-2.5.9/tests/test_mind_optimized.py +77 -0
- mobile_mcp_ai-2.5.9/tests/test_open_mind.py +37 -0
- mobile_mcp_ai-2.5.9/tests/test_priority_demo.py +81 -0
- mobile_mcp_ai-2.5.9/tests/test_simple.py +76 -0
- mobile_mcp_ai-2.5.9/tests/test_/344/270/276/346/212/245.py +136 -0
- mobile_mcp_ai-2.5.9/tests/test_/345/210/207/346/215/242/350/257/255/350/250/200/345/210/260English.py +158 -0
- mobile_mcp_ai-2.5.9/tests/test_/346/265/213/350/257/225.py +114 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/LICENSE +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/MANIFEST.in +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/README.md +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/config.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/__init__.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/dynamic_config.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/ios_client_wda.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/ios_device_manager_wda.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/template_matcher.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/utils/logger.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/core/utils/operation_history_manager.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/docs/iOS_SETUP_GUIDE.md +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mcp_tools/__init__.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mobile_mcp_ai.egg-info/dependency_links.txt +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mobile_mcp_ai.egg-info/entry_points.txt +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mobile_mcp_ai.egg-info/not-zip-safe +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mobile_mcp_ai.egg-info/requires.txt +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/mobile_mcp_ai.egg-info/top_level.txt +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/requirements.txt +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/setup.cfg +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/utils/__init__.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/utils/logger.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/utils/xml_formatter.py +0 -0
- {mobile_mcp_ai-2.5.5 → mobile_mcp_ai-2.5.9}/utils/xml_parser.py +0 -0
|
@@ -53,7 +53,34 @@ class BasicMobileToolsLite:
|
|
|
53
53
|
}
|
|
54
54
|
self.operation_history.append(record)
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
def _get_full_hierarchy(self) -> str:
|
|
57
|
+
"""获取完整的 UI 层级 XML(包含 NAF 元素)
|
|
58
|
+
|
|
59
|
+
优先使用 ADB 直接 dump,比 uiautomator2.dump_hierarchy 更完整
|
|
60
|
+
"""
|
|
61
|
+
import sys
|
|
62
|
+
|
|
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 ""
|
|
69
|
+
|
|
70
|
+
# Android: 优先使用 ADB 直接 dump
|
|
71
|
+
try:
|
|
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
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f" ⚠️ ADB dump 失败: {e}", file=sys.stderr)
|
|
81
|
+
|
|
82
|
+
# 方法2: 回退到 uiautomator2
|
|
83
|
+
return self.client.u2.dump_hierarchy(compressed=False)
|
|
57
84
|
|
|
58
85
|
# ==================== 截图 ====================
|
|
59
86
|
|
|
@@ -354,7 +381,7 @@ class BasicMobileToolsLite:
|
|
|
354
381
|
if show_popup_hints and not self._is_ios():
|
|
355
382
|
try:
|
|
356
383
|
import xml.etree.ElementTree as ET
|
|
357
|
-
xml_string = self.
|
|
384
|
+
xml_string = self._get_full_hierarchy()
|
|
358
385
|
root = ET.fromstring(xml_string)
|
|
359
386
|
|
|
360
387
|
# 检测弹窗区域
|
|
@@ -531,7 +558,7 @@ class BasicMobileToolsLite:
|
|
|
531
558
|
else:
|
|
532
559
|
try:
|
|
533
560
|
import xml.etree.ElementTree as ET
|
|
534
|
-
xml_string = self.
|
|
561
|
+
xml_string = self._get_full_hierarchy()
|
|
535
562
|
root = ET.fromstring(xml_string)
|
|
536
563
|
|
|
537
564
|
for elem in root.iter():
|
|
@@ -1087,9 +1114,9 @@ class BasicMobileToolsLite:
|
|
|
1087
1114
|
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
1088
1115
|
|
|
1089
1116
|
def _find_element_in_tree(self, text: str) -> Optional[Dict]:
|
|
1090
|
-
"""在 XML
|
|
1117
|
+
"""在 XML 树中查找包含指定文本的元素(使用完整 UI 层级)"""
|
|
1091
1118
|
try:
|
|
1092
|
-
xml = self.
|
|
1119
|
+
xml = self._get_full_hierarchy()
|
|
1093
1120
|
import xml.etree.ElementTree as ET
|
|
1094
1121
|
root = ET.fromstring(xml)
|
|
1095
1122
|
|
|
@@ -1126,9 +1153,16 @@ class BasicMobileToolsLite:
|
|
|
1126
1153
|
except Exception:
|
|
1127
1154
|
return None
|
|
1128
1155
|
|
|
1129
|
-
def click_by_id(self, resource_id: str) -> Dict:
|
|
1130
|
-
"""通过 resource-id
|
|
1156
|
+
def click_by_id(self, resource_id: str, index: int = 0) -> Dict:
|
|
1157
|
+
"""通过 resource-id 点击(支持点击第 N 个元素)
|
|
1158
|
+
|
|
1159
|
+
Args:
|
|
1160
|
+
resource_id: 元素的 resource-id
|
|
1161
|
+
index: 第几个元素(从 0 开始),默认 0 表示第一个
|
|
1162
|
+
"""
|
|
1131
1163
|
try:
|
|
1164
|
+
index_desc = f"[{index}]" if index > 0 else ""
|
|
1165
|
+
|
|
1132
1166
|
if self._is_ios():
|
|
1133
1167
|
ios_client = self._get_ios_client()
|
|
1134
1168
|
if ios_client and hasattr(ios_client, 'wda'):
|
|
@@ -1136,18 +1170,28 @@ class BasicMobileToolsLite:
|
|
|
1136
1170
|
if not elem.exists:
|
|
1137
1171
|
elem = ios_client.wda(name=resource_id)
|
|
1138
1172
|
if elem.exists:
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1173
|
+
# 获取所有匹配的元素
|
|
1174
|
+
elements = elem.find_elements()
|
|
1175
|
+
if index < len(elements):
|
|
1176
|
+
elements[index].click()
|
|
1177
|
+
time.sleep(0.3)
|
|
1178
|
+
self._record_operation('click', element=f"{resource_id}{index_desc}", ref=resource_id, index=index)
|
|
1179
|
+
return {"success": True, "message": f"✅ 点击成功: {resource_id}{index_desc}"}
|
|
1180
|
+
else:
|
|
1181
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {len(elements)} 个元素,但请求索引 {index}"}
|
|
1143
1182
|
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1144
1183
|
else:
|
|
1145
1184
|
elem = self.client.u2(resourceId=resource_id)
|
|
1146
1185
|
if elem.exists(timeout=0.5):
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1186
|
+
# 获取匹配元素数量
|
|
1187
|
+
count = elem.count
|
|
1188
|
+
if index < count:
|
|
1189
|
+
elem[index].click()
|
|
1190
|
+
time.sleep(0.3)
|
|
1191
|
+
self._record_operation('click', element=f"{resource_id}{index_desc}", ref=resource_id, index=index)
|
|
1192
|
+
return {"success": True, "message": f"✅ 点击成功: {resource_id}{index_desc}" + (f" (共 {count} 个)" if count > 1 else "")}
|
|
1193
|
+
else:
|
|
1194
|
+
return {"success": False, "message": f"❌ 索引超出范围: 找到 {count} 个元素,但请求索引 {index}"}
|
|
1151
1195
|
return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
|
|
1152
1196
|
except Exception as e:
|
|
1153
1197
|
return {"success": False, "message": f"❌ 点击失败: {e}"}
|
|
@@ -1814,7 +1858,7 @@ class BasicMobileToolsLite:
|
|
|
1814
1858
|
return ios_client.list_elements()
|
|
1815
1859
|
return [{"error": "iOS 暂不支持元素列表,建议使用截图"}]
|
|
1816
1860
|
else:
|
|
1817
|
-
xml_string = self.
|
|
1861
|
+
xml_string = self._get_full_hierarchy()
|
|
1818
1862
|
elements = self.client.xml_parser.parse(xml_string)
|
|
1819
1863
|
|
|
1820
1864
|
result = []
|
|
@@ -1850,8 +1894,8 @@ class BasicMobileToolsLite:
|
|
|
1850
1894
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
1851
1895
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
1852
1896
|
|
|
1853
|
-
#
|
|
1854
|
-
xml_string = self.
|
|
1897
|
+
# 获取元素列表(使用完整 UI 层级)
|
|
1898
|
+
xml_string = self._get_full_hierarchy()
|
|
1855
1899
|
import xml.etree.ElementTree as ET
|
|
1856
1900
|
root = ET.fromstring(xml_string)
|
|
1857
1901
|
|
|
@@ -2004,8 +2048,8 @@ class BasicMobileToolsLite:
|
|
|
2004
2048
|
screen_width = self.client.u2.info.get('displayWidth', 720)
|
|
2005
2049
|
screen_height = self.client.u2.info.get('displayHeight', 1280)
|
|
2006
2050
|
|
|
2007
|
-
# 获取原始 XML
|
|
2008
|
-
xml_string = self.
|
|
2051
|
+
# 获取原始 XML(使用完整 UI 层级)
|
|
2052
|
+
xml_string = self._get_full_hierarchy()
|
|
2009
2053
|
|
|
2010
2054
|
# 关闭按钮的文本特征
|
|
2011
2055
|
close_texts = ['×', 'X', 'x', '关闭', '取消', 'close', 'Close', 'CLOSE', '跳过', '知道了']
|
|
@@ -2360,22 +2404,43 @@ class BasicMobileToolsLite:
|
|
|
2360
2404
|
return 0.5
|
|
2361
2405
|
|
|
2362
2406
|
def assert_text(self, text: str) -> Dict:
|
|
2363
|
-
"""
|
|
2407
|
+
"""检查页面是否包含文本(支持精确匹配和包含匹配)"""
|
|
2364
2408
|
try:
|
|
2409
|
+
exists = False
|
|
2410
|
+
match_type = ""
|
|
2411
|
+
|
|
2365
2412
|
if self._is_ios():
|
|
2366
2413
|
ios_client = self._get_ios_client()
|
|
2367
2414
|
if ios_client and hasattr(ios_client, 'wda'):
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2415
|
+
# 先尝试精确匹配
|
|
2416
|
+
if ios_client.wda(name=text).exists or ios_client.wda(label=text).exists:
|
|
2417
|
+
exists = True
|
|
2418
|
+
match_type = "精确匹配"
|
|
2419
|
+
# 再尝试包含匹配
|
|
2420
|
+
elif ios_client.wda(nameContains=text).exists or ios_client.wda(labelContains=text).exists:
|
|
2421
|
+
exists = True
|
|
2422
|
+
match_type = "包含匹配"
|
|
2423
|
+
else:
|
|
2424
|
+
# Android: 先尝试精确匹配
|
|
2425
|
+
if self.client.u2(text=text).exists():
|
|
2426
|
+
exists = True
|
|
2427
|
+
match_type = "精确匹配"
|
|
2428
|
+
# 再尝试包含匹配
|
|
2429
|
+
elif self.client.u2(textContains=text).exists():
|
|
2430
|
+
exists = True
|
|
2431
|
+
match_type = "包含匹配"
|
|
2432
|
+
|
|
2433
|
+
if exists:
|
|
2434
|
+
message = f"✅ 文本'{text}' 存在({match_type})"
|
|
2371
2435
|
else:
|
|
2372
|
-
|
|
2436
|
+
message = f"❌ 文本'{text}' 不存在"
|
|
2373
2437
|
|
|
2374
2438
|
return {
|
|
2375
2439
|
"success": True,
|
|
2376
2440
|
"found": exists,
|
|
2377
2441
|
"text": text,
|
|
2378
|
-
"
|
|
2442
|
+
"match_type": match_type if exists else None,
|
|
2443
|
+
"message": message
|
|
2379
2444
|
}
|
|
2380
2445
|
except Exception as e:
|
|
2381
2446
|
return {"success": False, "message": f"❌ 断言失败: {e}"}
|
|
@@ -2855,8 +2920,8 @@ class BasicMobileToolsLite:
|
|
|
2855
2920
|
try:
|
|
2856
2921
|
import xml.etree.ElementTree as ET
|
|
2857
2922
|
|
|
2858
|
-
# ========== 第1
|
|
2859
|
-
xml_string = self.
|
|
2923
|
+
# ========== 第1步:控件树查找关闭按钮(使用完整 UI 层级)==========
|
|
2924
|
+
xml_string = self._get_full_hierarchy()
|
|
2860
2925
|
root = ET.fromstring(xml_string)
|
|
2861
2926
|
|
|
2862
2927
|
# 关闭按钮的常见特征
|
|
@@ -208,7 +208,7 @@ class DeviceManager:
|
|
|
208
208
|
|
|
209
209
|
try:
|
|
210
210
|
# 尝试获取页面结构,如果失败可能是无障碍服务未启用
|
|
211
|
-
xml = self.u2.dump_hierarchy()
|
|
211
|
+
xml = self.u2.dump_hierarchy(compressed=False)
|
|
212
212
|
if xml and len(xml) > 100: # 有内容说明无障碍服务正常
|
|
213
213
|
print(f" ✅ 无障碍服务: 已启用", file=sys.stderr)
|
|
214
214
|
return
|
|
@@ -235,7 +235,7 @@ class DeviceManager:
|
|
|
235
235
|
|
|
236
236
|
try:
|
|
237
237
|
# 尝试获取页面结构
|
|
238
|
-
xml = self.u2.dump_hierarchy()
|
|
238
|
+
xml = self.u2.dump_hierarchy(compressed=False)
|
|
239
239
|
if xml and len(xml) > 100:
|
|
240
240
|
return {
|
|
241
241
|
'enabled': True,
|
|
@@ -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):
|
|
@@ -321,7 +342,7 @@ class MobileClient:
|
|
|
321
342
|
# 🎯 改进:尝试模糊匹配(忽略空格、括号)
|
|
322
343
|
ref_normalized = ref.replace(' ', '').replace('(', '').replace(')', '').replace('(', '').replace(')', '')
|
|
323
344
|
# 获取所有元素,手动匹配
|
|
324
|
-
xml_string = self.u2.dump_hierarchy()
|
|
345
|
+
xml_string = self.u2.dump_hierarchy(compressed=False)
|
|
325
346
|
elements = self.xml_parser.parse(xml_string)
|
|
326
347
|
for elem in elements:
|
|
327
348
|
elem_desc = elem.get('content_desc', '')
|
|
@@ -434,7 +455,7 @@ class MobileClient:
|
|
|
434
455
|
if verify:
|
|
435
456
|
# 获取点击前页面状态
|
|
436
457
|
try:
|
|
437
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
458
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
438
459
|
initial_length = len(initial_xml)
|
|
439
460
|
|
|
440
461
|
# 等待页面变化
|
|
@@ -757,7 +778,7 @@ class MobileClient:
|
|
|
757
778
|
initial_length = 0
|
|
758
779
|
if verify:
|
|
759
780
|
try:
|
|
760
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
781
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
761
782
|
initial_length = len(initial_xml)
|
|
762
783
|
except Exception as e:
|
|
763
784
|
print(f" ⚠️ 获取初始页面状态失败: {e}", file=sys.stderr)
|
|
@@ -1008,7 +1029,7 @@ class MobileClient:
|
|
|
1008
1029
|
try:
|
|
1009
1030
|
if verify:
|
|
1010
1031
|
# 获取操作前页面状态
|
|
1011
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1032
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1012
1033
|
initial_length = len(initial_xml)
|
|
1013
1034
|
|
|
1014
1035
|
self.u2.press(key.lower())
|
|
@@ -1037,7 +1058,7 @@ class MobileClient:
|
|
|
1037
1058
|
# 标准按键处理
|
|
1038
1059
|
if verify:
|
|
1039
1060
|
# 获取操作前页面状态
|
|
1040
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1061
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1041
1062
|
initial_length = len(initial_xml)
|
|
1042
1063
|
|
|
1043
1064
|
# 使用keycode按键 - uiautomator2使用shell命令
|
|
@@ -1099,7 +1120,7 @@ class MobileClient:
|
|
|
1099
1120
|
print(f" 🔍 智能搜索键:先尝试SEARCH键...", file=sys.stderr)
|
|
1100
1121
|
|
|
1101
1122
|
# 获取初始页面状态
|
|
1102
|
-
initial_xml = self.u2.dump_hierarchy()
|
|
1123
|
+
initial_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1103
1124
|
initial_length = len(initial_xml)
|
|
1104
1125
|
|
|
1105
1126
|
# 方案1: 尝试 SEARCH 键 (keycode=84)
|
|
@@ -1126,7 +1147,7 @@ class MobileClient:
|
|
|
1126
1147
|
|
|
1127
1148
|
# 方案2: 尝试 ENTER 键 (keycode=66)
|
|
1128
1149
|
# 重新获取当前页面状态(因为可能有轻微变化)
|
|
1129
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1150
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1130
1151
|
current_length = len(current_xml)
|
|
1131
1152
|
|
|
1132
1153
|
self.u2.shell('input keyevent 66')
|
|
@@ -1184,7 +1205,7 @@ class MobileClient:
|
|
|
1184
1205
|
await asyncio.sleep(0.1) # 每100ms检查一次
|
|
1185
1206
|
|
|
1186
1207
|
try:
|
|
1187
|
-
current_xml = self.u2.dump_hierarchy()
|
|
1208
|
+
current_xml = self.u2.dump_hierarchy(compressed=False)
|
|
1188
1209
|
current_length = len(current_xml)
|
|
1189
1210
|
|
|
1190
1211
|
# 计算变化百分比
|
|
@@ -61,7 +61,7 @@ class SmartWait:
|
|
|
61
61
|
while time.time() - start_time < timeout:
|
|
62
62
|
try:
|
|
63
63
|
# 获取当前页面快照(只获取元素数量,不解析详细内容)
|
|
64
|
-
xml = self.client.u2.dump_hierarchy()
|
|
64
|
+
xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
65
65
|
current_snapshot = len(xml) # 使用XML长度作为简单的页面状态标识
|
|
66
66
|
|
|
67
67
|
if last_snapshot is not None:
|
|
@@ -137,14 +137,14 @@ class SmartWait:
|
|
|
137
137
|
|
|
138
138
|
try:
|
|
139
139
|
# 获取初始页面状态
|
|
140
|
-
initial_xml = self.client.u2.dump_hierarchy()
|
|
140
|
+
initial_xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
141
141
|
initial_length = len(initial_xml)
|
|
142
142
|
|
|
143
143
|
while time.time() - start_time < timeout:
|
|
144
144
|
await asyncio.sleep(self.poll_interval)
|
|
145
145
|
|
|
146
146
|
try:
|
|
147
|
-
current_xml = self.client.u2.dump_hierarchy()
|
|
147
|
+
current_xml = self.client.u2.dump_hierarchy(compressed=False)
|
|
148
148
|
current_length = len(current_xml)
|
|
149
149
|
|
|
150
150
|
# 页面变化超过5%认为有变化
|
|
@@ -331,11 +331,13 @@ class MobileMCPServer:
|
|
|
331
331
|
description="👆 通过 resource-id 点击元素(最推荐)\n\n"
|
|
332
332
|
"✅ 最稳定的定位方式\n"
|
|
333
333
|
"✅ 实时检测元素是否存在,元素不存在会报错\n"
|
|
334
|
-
"📋 使用前先调用 mobile_list_elements 获取元素 ID"
|
|
334
|
+
"📋 使用前先调用 mobile_list_elements 获取元素 ID\n"
|
|
335
|
+
"💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)",
|
|
335
336
|
inputSchema={
|
|
336
337
|
"type": "object",
|
|
337
338
|
"properties": {
|
|
338
|
-
"resource_id": {"type": "string", "description": "元素的 resource-id"}
|
|
339
|
+
"resource_id": {"type": "string", "description": "元素的 resource-id"},
|
|
340
|
+
"index": {"type": "integer", "description": "第几个元素(从 0 开始),默认 0 表示第一个", "default": 0}
|
|
339
341
|
},
|
|
340
342
|
"required": ["resource_id"]
|
|
341
343
|
}
|
|
@@ -860,7 +862,10 @@ class MobileMCPServer:
|
|
|
860
862
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
861
863
|
|
|
862
864
|
elif name == "mobile_click_by_id":
|
|
863
|
-
result = self.tools.click_by_id(
|
|
865
|
+
result = self.tools.click_by_id(
|
|
866
|
+
arguments["resource_id"],
|
|
867
|
+
arguments.get("index", 0)
|
|
868
|
+
)
|
|
864
869
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
865
870
|
|
|
866
871
|
elif name == "mobile_click_by_percent":
|
|
@@ -65,6 +65,16 @@ templates/close_buttons/auto_x_0112_152840.png
|
|
|
65
65
|
templates/close_buttons/auto_x_0112_153256.png
|
|
66
66
|
templates/close_buttons/auto_x_0112_154847.png
|
|
67
67
|
templates/close_buttons/gray_x_stock_ad.png
|
|
68
|
+
tests/test_mind_cloud_my_space.py
|
|
69
|
+
tests/test_mind_correct.py
|
|
70
|
+
tests/test_mind_improved.py
|
|
71
|
+
tests/test_mind_optimized.py
|
|
72
|
+
tests/test_open_mind.py
|
|
73
|
+
tests/test_priority_demo.py
|
|
74
|
+
tests/test_simple.py
|
|
75
|
+
tests/test_举报.py
|
|
76
|
+
tests/test_切换语言到English.py
|
|
77
|
+
tests/test_测试.py
|
|
68
78
|
utils/__init__.py
|
|
69
79
|
utils/logger.py
|
|
70
80
|
utils/xml_formatter.py
|
|
@@ -25,7 +25,7 @@ if requirements_file.exists():
|
|
|
25
25
|
|
|
26
26
|
setup(
|
|
27
27
|
name="mobile-mcp-ai",
|
|
28
|
-
version="2.5.
|
|
28
|
+
version="2.5.9", # 使用 ADB dump 获取完整 UI 层级(包含 NAF 元素)
|
|
29
29
|
author="douzi",
|
|
30
30
|
author_email="1492994674@qq.com",
|
|
31
31
|
description="移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
测试用例: Mind云文档我的空间
|
|
5
|
+
生成时间: 2025-12-17 11:00:00
|
|
6
|
+
"""
|
|
7
|
+
import time
|
|
8
|
+
import uiautomator2 as u2
|
|
9
|
+
|
|
10
|
+
PACKAGE_NAME = "com.im30.mind"
|
|
11
|
+
|
|
12
|
+
# 广告关闭按钮关键词(可自定义)
|
|
13
|
+
AD_CLOSE_KEYWORDS = ['关闭', '跳过', 'Skip', 'Close', '×', 'X', '我知道了', '稍后再说']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def smart_wait(d, timeout=10):
|
|
17
|
+
"""智能等待页面稳定"""
|
|
18
|
+
d.implicitly_wait(timeout)
|
|
19
|
+
time.sleep(0.5) # 额外等待动画
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def close_ad_if_exists(d):
|
|
23
|
+
"""尝试关闭广告弹窗"""
|
|
24
|
+
for keyword in AD_CLOSE_KEYWORDS:
|
|
25
|
+
elem = d(textContains=keyword)
|
|
26
|
+
if elem.exists(timeout=0.5):
|
|
27
|
+
try:
|
|
28
|
+
elem.click()
|
|
29
|
+
print(f' 📢 关闭广告: {keyword}')
|
|
30
|
+
time.sleep(0.5)
|
|
31
|
+
return True
|
|
32
|
+
except:
|
|
33
|
+
pass
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def safe_click(d, selector, timeout=5):
|
|
38
|
+
"""安全点击(带等待和重试)"""
|
|
39
|
+
try:
|
|
40
|
+
if selector.exists(timeout=timeout):
|
|
41
|
+
selector.click()
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f' ⚠️ 点击失败: {e}')
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_main():
|
|
50
|
+
# 连接设备
|
|
51
|
+
d = u2.connect()
|
|
52
|
+
d.implicitly_wait(10) # 设置全局等待
|
|
53
|
+
|
|
54
|
+
# 启动应用
|
|
55
|
+
d.app_start(PACKAGE_NAME)
|
|
56
|
+
smart_wait(d)
|
|
57
|
+
|
|
58
|
+
# 尝试关闭启动广告
|
|
59
|
+
close_ad_if_exists(d)
|
|
60
|
+
|
|
61
|
+
# 步骤1: 点击文本 'Mind'
|
|
62
|
+
safe_click(d, d(text='Mind'))
|
|
63
|
+
smart_wait(d)
|
|
64
|
+
close_ad_if_exists(d) # 检查广告
|
|
65
|
+
|
|
66
|
+
# 步骤2: 点击坐标 (756, 2277)
|
|
67
|
+
d.click(756, 2277)
|
|
68
|
+
smart_wait(d)
|
|
69
|
+
close_ad_if_exists(d) # 检查广告
|
|
70
|
+
|
|
71
|
+
# 步骤3: 点击坐标 (815, 285)
|
|
72
|
+
d.click(815, 285)
|
|
73
|
+
smart_wait(d)
|
|
74
|
+
close_ad_if_exists(d) # 检查广告
|
|
75
|
+
|
|
76
|
+
print('✅ 测试完成')
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == '__main__':
|
|
80
|
+
test_main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
测试用例: Mind云文档我的空间_正确版
|
|
5
|
+
生成时间: 2025-12-17 11:08:10
|
|
6
|
+
"""
|
|
7
|
+
import time
|
|
8
|
+
import uiautomator2 as u2
|
|
9
|
+
|
|
10
|
+
PACKAGE_NAME = "com.im30.mind"
|
|
11
|
+
|
|
12
|
+
# 广告关闭按钮关键词(可自定义)
|
|
13
|
+
AD_CLOSE_KEYWORDS = ['关闭', '跳过', 'Skip', 'Close', '×', 'X', '我知道了', '稍后再说']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def smart_wait(d, seconds=1):
|
|
17
|
+
"""等待页面稳定"""
|
|
18
|
+
time.sleep(seconds)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def close_ad_if_exists(d, quick=False):
|
|
22
|
+
"""尝试关闭广告弹窗(quick=True 时只检查常见的)"""
|
|
23
|
+
keywords = AD_CLOSE_KEYWORDS[:3] if quick else AD_CLOSE_KEYWORDS
|
|
24
|
+
for keyword in keywords:
|
|
25
|
+
elem = d(textContains=keyword)
|
|
26
|
+
if elem.exists(timeout=0.3): # 缩短超时
|
|
27
|
+
try:
|
|
28
|
+
elem.click()
|
|
29
|
+
print(f' 📢 关闭广告: {keyword}')
|
|
30
|
+
time.sleep(0.3)
|
|
31
|
+
return True
|
|
32
|
+
except:
|
|
33
|
+
pass
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def safe_click(d, selector, timeout=3):
|
|
38
|
+
"""安全点击(带等待)"""
|
|
39
|
+
try:
|
|
40
|
+
if selector.exists(timeout=timeout):
|
|
41
|
+
selector.click()
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f' ⚠️ 点击失败: {e}')
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_main():
|
|
50
|
+
# 连接设备
|
|
51
|
+
d = u2.connect()
|
|
52
|
+
d.implicitly_wait(10) # 设置全局等待
|
|
53
|
+
|
|
54
|
+
# 启动应用
|
|
55
|
+
d.app_start(PACKAGE_NAME)
|
|
56
|
+
smart_wait(d)
|
|
57
|
+
|
|
58
|
+
# 尝试关闭启动广告
|
|
59
|
+
close_ad_if_exists(d)
|
|
60
|
+
|
|
61
|
+
# 步骤1: 点击坐标 (756, 2277)
|
|
62
|
+
d.click(756, 2277)
|
|
63
|
+
time.sleep(0.5) # 等待响应
|
|
64
|
+
|
|
65
|
+
# 步骤2: 点击坐标 (815, 285)
|
|
66
|
+
d.click(815, 285)
|
|
67
|
+
time.sleep(0.5) # 等待响应
|
|
68
|
+
|
|
69
|
+
print('✅ 测试完成')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == '__main__':
|
|
73
|
+
test_main()
|