mobile-mcp-ai 2.6.4__tar.gz → 2.6.5__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.6.4/mobile_mcp_ai.egg-info → mobile_mcp_ai-2.6.5}/PKG-INFO +43 -3
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/basic_tools_lite.py +113 -5
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mcp_tools/mcp_server.py +54 -59
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5/mobile_mcp_ai.egg-info}/PKG-INFO +43 -3
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mobile_mcp_ai.egg-info/SOURCES.txt +31 -1
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/setup.py +1 -1
- mobile_mcp_ai-2.6.5/tests/test_mind_cloud_my_space.py +80 -0
- mobile_mcp_ai-2.6.5/tests/test_mind_correct.py +73 -0
- mobile_mcp_ai-2.6.5/tests/test_mind_improved.py +83 -0
- mobile_mcp_ai-2.6.5/tests/test_mind_optimized.py +77 -0
- mobile_mcp_ai-2.6.5/tests/test_open_mind.py +37 -0
- mobile_mcp_ai-2.6.5/tests/test_priority_demo.py +81 -0
- mobile_mcp_ai-2.6.5/tests/test_simple.py +76 -0
- mobile_mcp_ai-2.6.5/tests/test_/344/270/276/346/212/245.py +136 -0
- mobile_mcp_ai-2.6.5/tests/test_/345/210/207/346/215/242/350/257/255/350/250/200/345/210/260English.py +158 -0
- mobile_mcp_ai-2.6.5/tests/test_/346/265/213/350/257/225.py +114 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/LICENSE +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/MANIFEST.in +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/README.md +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/__init__.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/config.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/__init__.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/device_manager.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/dynamic_config.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/ios_client_wda.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/ios_device_manager_wda.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/mobile_client.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/template_matcher.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/utils/__init__.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/utils/logger.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/utils/operation_history_manager.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/core/utils/smart_wait.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/docs/iOS_SETUP_GUIDE.md +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mcp_tools/__init__.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mobile_mcp_ai.egg-info/dependency_links.txt +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mobile_mcp_ai.egg-info/entry_points.txt +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mobile_mcp_ai.egg-info/not-zip-safe +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mobile_mcp_ai.egg-info/requires.txt +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/mobile_mcp_ai.egg-info/top_level.txt +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/requirements.txt +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/setup.cfg +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/templates/close_buttons/auto_x_0112_151217.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/templates/close_buttons/auto_x_0112_152037.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/templates/close_buttons/auto_x_0112_152840.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/templates/close_buttons/auto_x_0112_153256.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/templates/close_buttons/auto_x_0112_154847.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/utils/__init__.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/utils/logger.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/utils/xml_formatter.py +0 -0
- {mobile-mcp-ai-2.6.4 → mobile_mcp_ai-2.6.5}/utils/xml_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mobile-mcp-ai
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.5
|
|
4
4
|
Summary: 移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)
|
|
5
5
|
Home-page: https://github.com/test111ddff-hash/mobile-mcp-ai
|
|
6
6
|
Author: douzi
|
|
@@ -21,13 +21,53 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.8
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: uiautomator2>=2.16.0
|
|
26
|
+
Requires-Dist: adbutils>=1.2.0
|
|
27
|
+
Requires-Dist: Pillow>=10.0.0
|
|
28
|
+
Requires-Dist: mcp>=0.9.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
24
30
|
Provides-Extra: ai
|
|
31
|
+
Requires-Dist: dashscope>=1.10.0; extra == "ai"
|
|
32
|
+
Requires-Dist: openai>=1.0.0; extra == "ai"
|
|
33
|
+
Requires-Dist: anthropic>=0.3.0; extra == "ai"
|
|
25
34
|
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
37
|
+
Requires-Dist: allure-pytest>=2.13.0; extra == "test"
|
|
26
38
|
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
41
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
27
43
|
Provides-Extra: ios
|
|
44
|
+
Requires-Dist: tidevice>=0.11.0; extra == "ios"
|
|
45
|
+
Requires-Dist: facebook-wda>=1.4.0; extra == "ios"
|
|
28
46
|
Provides-Extra: h5
|
|
47
|
+
Requires-Dist: Appium-Python-Client>=3.0.0; extra == "h5"
|
|
48
|
+
Requires-Dist: selenium>=4.0.0; extra == "h5"
|
|
29
49
|
Provides-Extra: all
|
|
30
|
-
|
|
50
|
+
Requires-Dist: dashscope>=1.10.0; extra == "all"
|
|
51
|
+
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
52
|
+
Requires-Dist: anthropic>=0.3.0; extra == "all"
|
|
53
|
+
Requires-Dist: Appium-Python-Client>=3.0.0; extra == "all"
|
|
54
|
+
Requires-Dist: selenium>=4.0.0; extra == "all"
|
|
55
|
+
Requires-Dist: pytest>=8.0.0; extra == "all"
|
|
56
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
|
|
57
|
+
Requires-Dist: allure-pytest>=2.13.0; extra == "all"
|
|
58
|
+
Dynamic: author
|
|
59
|
+
Dynamic: author-email
|
|
60
|
+
Dynamic: classifier
|
|
61
|
+
Dynamic: description
|
|
62
|
+
Dynamic: description-content-type
|
|
63
|
+
Dynamic: home-page
|
|
64
|
+
Dynamic: keywords
|
|
65
|
+
Dynamic: license-file
|
|
66
|
+
Dynamic: project-url
|
|
67
|
+
Dynamic: provides-extra
|
|
68
|
+
Dynamic: requires-dist
|
|
69
|
+
Dynamic: requires-python
|
|
70
|
+
Dynamic: summary
|
|
31
71
|
|
|
32
72
|
# 📱 Mobile MCP AI
|
|
33
73
|
|
|
@@ -1194,7 +1194,8 @@ class BasicMobileToolsLite:
|
|
|
1194
1194
|
except Exception as e:
|
|
1195
1195
|
return {"success": False, "message": f"❌ 百分比点击失败: {e}"}
|
|
1196
1196
|
|
|
1197
|
-
def click_by_text(self, text: str, timeout: float = 3.0, position: Optional[str] = None
|
|
1197
|
+
def click_by_text(self, text: str, timeout: float = 3.0, position: Optional[str] = None,
|
|
1198
|
+
verify: Optional[str] = None) -> Dict:
|
|
1198
1199
|
"""通过文本点击 - 先查 XML 树,再精准匹配
|
|
1199
1200
|
|
|
1200
1201
|
Args:
|
|
@@ -1203,6 +1204,7 @@ class BasicMobileToolsLite:
|
|
|
1203
1204
|
position: 位置信息,当有多个相同文案时使用。支持:
|
|
1204
1205
|
- 垂直方向: "top"/"upper"/"上", "bottom"/"lower"/"下", "middle"/"center"/"中"
|
|
1205
1206
|
- 水平方向: "left"/"左", "right"/"右", "center"/"中"
|
|
1207
|
+
verify: 可选,点击后验证的文本。如果指定,会检查该文本是否出现在页面上
|
|
1206
1208
|
"""
|
|
1207
1209
|
try:
|
|
1208
1210
|
if self._is_ios():
|
|
@@ -1215,7 +1217,12 @@ class BasicMobileToolsLite:
|
|
|
1215
1217
|
elem.click()
|
|
1216
1218
|
time.sleep(0.3)
|
|
1217
1219
|
self._record_click('text', text, element_desc=text, locator_attr='text')
|
|
1218
|
-
|
|
1220
|
+
# 验证逻辑
|
|
1221
|
+
if verify:
|
|
1222
|
+
return self._verify_after_click(verify, ios=True)
|
|
1223
|
+
# 返回页面文本摘要,方便确认页面变化
|
|
1224
|
+
page_texts = self._get_page_texts(10)
|
|
1225
|
+
return {"success": True, "page_texts": page_texts}
|
|
1219
1226
|
# 控件树找不到,提示用视觉识别
|
|
1220
1227
|
return {"success": False, "fallback": "vision", "msg": f"未找到'{text}',用截图点击"}
|
|
1221
1228
|
else:
|
|
@@ -1248,7 +1255,12 @@ class BasicMobileToolsLite:
|
|
|
1248
1255
|
time.sleep(0.3)
|
|
1249
1256
|
self._record_click('text', attr_value, x_pct, y_pct,
|
|
1250
1257
|
element_desc=f"{text}({position})", locator_attr=attr_type)
|
|
1251
|
-
|
|
1258
|
+
# 验证逻辑
|
|
1259
|
+
if verify:
|
|
1260
|
+
return self._verify_after_click(verify)
|
|
1261
|
+
# 返回页面文本摘要
|
|
1262
|
+
page_texts = self._get_page_texts(10)
|
|
1263
|
+
return {"success": True, "page_texts": page_texts}
|
|
1252
1264
|
|
|
1253
1265
|
# 没有位置参数时,使用选择器定位
|
|
1254
1266
|
if attr_type == 'text':
|
|
@@ -1267,7 +1279,12 @@ class BasicMobileToolsLite:
|
|
|
1267
1279
|
time.sleep(0.3)
|
|
1268
1280
|
self._record_click('text', attr_value, x_pct, y_pct,
|
|
1269
1281
|
element_desc=text, locator_attr=attr_type)
|
|
1270
|
-
|
|
1282
|
+
# 验证逻辑
|
|
1283
|
+
if verify:
|
|
1284
|
+
return self._verify_after_click(verify)
|
|
1285
|
+
# 返回页面文本摘要
|
|
1286
|
+
page_texts = self._get_page_texts(10)
|
|
1287
|
+
return {"success": True, "page_texts": page_texts}
|
|
1271
1288
|
|
|
1272
1289
|
# 选择器失败,用坐标兜底
|
|
1273
1290
|
if bounds:
|
|
@@ -1277,13 +1294,58 @@ class BasicMobileToolsLite:
|
|
|
1277
1294
|
time.sleep(0.3)
|
|
1278
1295
|
self._record_click('percent', f"{x_pct}%,{y_pct}%", x_pct, y_pct,
|
|
1279
1296
|
element_desc=text)
|
|
1280
|
-
|
|
1297
|
+
# 验证逻辑
|
|
1298
|
+
if verify:
|
|
1299
|
+
return self._verify_after_click(verify)
|
|
1300
|
+
# 返回页面文本摘要
|
|
1301
|
+
page_texts = self._get_page_texts(10)
|
|
1302
|
+
return {"success": True, "page_texts": page_texts}
|
|
1281
1303
|
|
|
1282
1304
|
# 控件树找不到,提示用视觉识别
|
|
1283
1305
|
return {"success": False, "fallback": "vision", "msg": f"未找到'{text}',用截图点击"}
|
|
1284
1306
|
except Exception as e:
|
|
1285
1307
|
return {"success": False, "msg": str(e)}
|
|
1286
1308
|
|
|
1309
|
+
def _verify_after_click(self, verify_text: str, ios: bool = False, timeout: float = 2.0) -> Dict:
|
|
1310
|
+
"""点击后验证期望文本是否出现
|
|
1311
|
+
|
|
1312
|
+
Args:
|
|
1313
|
+
verify_text: 期望出现的文本
|
|
1314
|
+
ios: 是否是 iOS 设备
|
|
1315
|
+
timeout: 验证超时时间
|
|
1316
|
+
|
|
1317
|
+
Returns:
|
|
1318
|
+
{"success": True, "verified": True/False, "hint": "..."}
|
|
1319
|
+
"""
|
|
1320
|
+
time.sleep(0.5) # 等待页面更新
|
|
1321
|
+
|
|
1322
|
+
try:
|
|
1323
|
+
if ios:
|
|
1324
|
+
ios_client = self._get_ios_client()
|
|
1325
|
+
if ios_client and hasattr(ios_client, 'wda'):
|
|
1326
|
+
exists = ios_client.wda(name=verify_text).exists or \
|
|
1327
|
+
ios_client.wda(label=verify_text).exists
|
|
1328
|
+
else:
|
|
1329
|
+
exists = False
|
|
1330
|
+
else:
|
|
1331
|
+
# Android: 检查文本或包含文本
|
|
1332
|
+
exists = self.client.u2(text=verify_text).exists(timeout=timeout) or \
|
|
1333
|
+
self.client.u2(textContains=verify_text).exists(timeout=0.5) or \
|
|
1334
|
+
self.client.u2(description=verify_text).exists(timeout=0.5)
|
|
1335
|
+
|
|
1336
|
+
if exists:
|
|
1337
|
+
return {"success": True, "verified": True}
|
|
1338
|
+
else:
|
|
1339
|
+
# 验证失败,提示可以截图确认
|
|
1340
|
+
return {
|
|
1341
|
+
"success": True, # 点击本身成功
|
|
1342
|
+
"verified": False,
|
|
1343
|
+
"expect": verify_text,
|
|
1344
|
+
"hint": "验证失败,可截图确认"
|
|
1345
|
+
}
|
|
1346
|
+
except Exception as e:
|
|
1347
|
+
return {"success": True, "verified": False, "hint": f"验证异常: {e}"}
|
|
1348
|
+
|
|
1287
1349
|
def _find_element_in_tree(self, text: str, position: Optional[str] = None) -> Optional[Dict]:
|
|
1288
1350
|
"""在 XML 树中查找包含指定文本的元素,优先返回可点击的元素
|
|
1289
1351
|
|
|
@@ -2387,6 +2449,52 @@ class BasicMobileToolsLite:
|
|
|
2387
2449
|
except Exception as e:
|
|
2388
2450
|
return [{"error": f"获取元素失败: {e}"}]
|
|
2389
2451
|
|
|
2452
|
+
def _get_page_texts(self, max_count: int = 15) -> List[str]:
|
|
2453
|
+
"""获取页面关键文本列表(用于点击后快速确认页面变化)
|
|
2454
|
+
|
|
2455
|
+
Args:
|
|
2456
|
+
max_count: 最多返回的文本数量
|
|
2457
|
+
|
|
2458
|
+
Returns:
|
|
2459
|
+
页面上的关键文本列表(去重)
|
|
2460
|
+
"""
|
|
2461
|
+
try:
|
|
2462
|
+
if self._is_ios():
|
|
2463
|
+
ios_client = self._get_ios_client()
|
|
2464
|
+
if ios_client and hasattr(ios_client, 'wda'):
|
|
2465
|
+
# iOS: 获取所有 StaticText 的文本
|
|
2466
|
+
elements = ios_client.wda(type='XCUIElementTypeStaticText').find_elements()
|
|
2467
|
+
texts = set()
|
|
2468
|
+
for elem in elements[:50]: # 限制扫描数量
|
|
2469
|
+
try:
|
|
2470
|
+
name = elem.name or elem.label
|
|
2471
|
+
if name and len(name) > 1 and len(name) < 50:
|
|
2472
|
+
texts.add(name)
|
|
2473
|
+
except:
|
|
2474
|
+
pass
|
|
2475
|
+
return list(texts)[:max_count]
|
|
2476
|
+
return []
|
|
2477
|
+
else:
|
|
2478
|
+
# Android: 快速扫描 XML 获取文本
|
|
2479
|
+
xml_string = self.client.u2.dump_hierarchy(compressed=True)
|
|
2480
|
+
import xml.etree.ElementTree as ET
|
|
2481
|
+
root = ET.fromstring(xml_string)
|
|
2482
|
+
|
|
2483
|
+
texts = set()
|
|
2484
|
+
for elem in root.iter():
|
|
2485
|
+
text = elem.get('text', '').strip()
|
|
2486
|
+
desc = elem.get('content-desc', '').strip()
|
|
2487
|
+
# 只收集有意义的文本(长度2-30,非纯数字)
|
|
2488
|
+
for t in [text, desc]:
|
|
2489
|
+
if t and 2 <= len(t) <= 30 and not t.isdigit():
|
|
2490
|
+
texts.add(t)
|
|
2491
|
+
if len(texts) >= max_count * 2: # 收集足够后停止
|
|
2492
|
+
break
|
|
2493
|
+
|
|
2494
|
+
return list(texts)[:max_count]
|
|
2495
|
+
except Exception:
|
|
2496
|
+
return []
|
|
2497
|
+
|
|
2390
2498
|
def _has_business_id(self, resource_id: str) -> bool:
|
|
2391
2499
|
"""
|
|
2392
2500
|
判断resource_id是否是业务相关的ID
|
|
@@ -213,16 +213,16 @@ class MobileMCPServer:
|
|
|
213
213
|
|
|
214
214
|
# ==================== 元素定位(优先使用)====================
|
|
215
215
|
if compact:
|
|
216
|
-
desc_list_elements = "📋
|
|
216
|
+
desc_list_elements = "📋 【首选】列出页面元素(token低)。返回text/id用于点击,替代截图确认页面状态。"
|
|
217
217
|
else:
|
|
218
|
-
desc_list_elements = ("📋
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"-
|
|
224
|
-
"
|
|
225
|
-
"
|
|
218
|
+
desc_list_elements = ("📋 【⭐首选工具】列出页面所有可交互元素\n\n"
|
|
219
|
+
"🚀 Token 优化:返回文本数据(~500 tokens),远小于截图(~2000 tokens)!\n\n"
|
|
220
|
+
"✅ 推荐使用场景:\n"
|
|
221
|
+
"- 点击前确认元素存在\n"
|
|
222
|
+
"- 点击后确认页面变化(替代截图确认)\n"
|
|
223
|
+
"- 获取 text/id 用于 click_by_text/click_by_id\n\n"
|
|
224
|
+
"❌ 不要用截图确认页面,用此工具!\n"
|
|
225
|
+
"📌 只有需要看视觉布局时才用截图")
|
|
226
226
|
|
|
227
227
|
tools.append(Tool(
|
|
228
228
|
name="mobile_list_elements",
|
|
@@ -232,16 +232,15 @@ class MobileMCPServer:
|
|
|
232
232
|
|
|
233
233
|
# ==================== 截图(视觉兜底)====================
|
|
234
234
|
if compact:
|
|
235
|
-
desc_screenshot = "📸
|
|
235
|
+
desc_screenshot = "📸 截图(token高~2000)。优先用list_elements(~500)确认页面状态。"
|
|
236
236
|
else:
|
|
237
|
-
desc_screenshot = ("📸
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"-
|
|
242
|
-
"-
|
|
243
|
-
"
|
|
244
|
-
"💡 如需点击元素,请用 mobile_screenshot_with_som + mobile_click_by_som")
|
|
237
|
+
desc_screenshot = ("📸 截图查看屏幕内容(⚠️ Token 消耗高 ~2000)\n\n"
|
|
238
|
+
"❌ 【不推荐】确认页面状态请用 list_elements(Token 仅 ~500)!\n\n"
|
|
239
|
+
"✅ 仅在以下场景使用:\n"
|
|
240
|
+
"- 需要看视觉布局/图片内容\n"
|
|
241
|
+
"- 元素无 text/id,只能靠位置点击\n"
|
|
242
|
+
"- 调试问题需要可视化\n\n"
|
|
243
|
+
"💡 compress=false 可获取原图(用于添加模板)")
|
|
245
244
|
|
|
246
245
|
tools.append(Tool(
|
|
247
246
|
name="mobile_take_screenshot",
|
|
@@ -266,20 +265,18 @@ class MobileMCPServer:
|
|
|
266
265
|
))
|
|
267
266
|
|
|
268
267
|
if compact:
|
|
269
|
-
desc_som = "📸 SoM
|
|
268
|
+
desc_som = "📸 SoM截图(token高)。元素有text时优先用list_elements+click_by_text。"
|
|
270
269
|
else:
|
|
271
|
-
desc_som = ("📸🏷️ Set-of-Mark
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"-
|
|
276
|
-
"
|
|
277
|
-
"-
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"3. 调用 mobile_click_by_som(编号) 精准点击\n"
|
|
282
|
-
"4. 🔴【必须】点击后再次截图确认操作是否成功!")
|
|
270
|
+
desc_som = ("📸🏷️ Set-of-Mark 截图(⚠️ Token 消耗高 ~2000)\n\n"
|
|
271
|
+
"【智能标注】给每个可点击元素画框+编号\n\n"
|
|
272
|
+
"❌ 【不推荐常规使用】:\n"
|
|
273
|
+
"- 如果元素有 text,用 list_elements + click_by_text 更省 token\n"
|
|
274
|
+
"- 确认页面状态用 list_elements,不要截图确认!\n\n"
|
|
275
|
+
"✅ 仅在以下场景使用:\n"
|
|
276
|
+
"- 元素无 text/id,只能看图点击\n"
|
|
277
|
+
"- 需要视觉布局信息\n"
|
|
278
|
+
"- 首次探索未知页面\n\n"
|
|
279
|
+
"💡 点击后用 list_elements 确认,不要再截图!")
|
|
283
280
|
|
|
284
281
|
tools.append(Tool(
|
|
285
282
|
name="mobile_screenshot_with_som",
|
|
@@ -314,18 +311,19 @@ class MobileMCPServer:
|
|
|
314
311
|
|
|
315
312
|
# ==================== 点击操作 ====================
|
|
316
313
|
if compact:
|
|
317
|
-
desc_click_text = "👆
|
|
314
|
+
desc_click_text = "👆 文本点击(推荐)。verify可验证点击结果,无需截图确认。position可选top/bottom/left/right。"
|
|
318
315
|
else:
|
|
319
|
-
desc_click_text = ("👆
|
|
316
|
+
desc_click_text = ("👆 通过文本点击元素(⭐ 最推荐)\n\n"
|
|
320
317
|
"✅ 最稳定的定位方式,跨设备兼容\n"
|
|
321
|
-
"✅
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
" -
|
|
328
|
-
"
|
|
318
|
+
"✅ 元素不存在会报错,不会误点击\n\n"
|
|
319
|
+
"🚀 Token 优化流程:\n"
|
|
320
|
+
"1. list_elements 确认元素存在\n"
|
|
321
|
+
"2. click_by_text 点击\n"
|
|
322
|
+
"3. list_elements 确认页面变化(❌不要截图确认!)\n\n"
|
|
323
|
+
"📍 position 参数:多个相同文案时指定位置\n"
|
|
324
|
+
" - top/bottom/left/right/center\n\n"
|
|
325
|
+
"🔍 verify 参数:点击后自动验证文本是否出现\n"
|
|
326
|
+
" - 设置后无需额外调用 list_elements 确认")
|
|
329
327
|
|
|
330
328
|
tools.append(Tool(
|
|
331
329
|
name="mobile_click_by_text",
|
|
@@ -334,21 +332,21 @@ class MobileMCPServer:
|
|
|
334
332
|
"type": "object",
|
|
335
333
|
"properties": {
|
|
336
334
|
"text": {"type": "string", "description": "元素文本"},
|
|
337
|
-
"position": {"type": "string", "description": "位置:top/bottom/left/right"}
|
|
335
|
+
"position": {"type": "string", "description": "位置:top/bottom/left/right"},
|
|
336
|
+
"verify": {"type": "string", "description": "点击后验证的文本(可选)"}
|
|
338
337
|
},
|
|
339
338
|
"required": ["text"]
|
|
340
339
|
}
|
|
341
340
|
))
|
|
342
341
|
|
|
343
342
|
if compact:
|
|
344
|
-
desc_click_id = "👆 通过
|
|
343
|
+
desc_click_id = "👆 通过resource-id点击。index指定第几个(从0开始)。点击后用list_elements确认。"
|
|
345
344
|
else:
|
|
346
345
|
desc_click_id = ("👆 通过 resource-id 点击元素(推荐)\n\n"
|
|
347
|
-
"✅
|
|
348
|
-
"
|
|
349
|
-
"📋
|
|
350
|
-
"💡
|
|
351
|
-
"💡 定位优先级:文本 > ID > 百分比 > 坐标")
|
|
346
|
+
"✅ 稳定的定位方式,元素不存在会报错\n"
|
|
347
|
+
"📋 使用前 list_elements 获取元素 ID\n"
|
|
348
|
+
"📋 点击后 list_elements 确认(❌不要截图确认!)\n"
|
|
349
|
+
"💡 多个相同 ID 时用 index 指定第几个(从 0 开始)")
|
|
352
350
|
|
|
353
351
|
tools.append(Tool(
|
|
354
352
|
name="mobile_click_by_id",
|
|
@@ -364,17 +362,13 @@ class MobileMCPServer:
|
|
|
364
362
|
))
|
|
365
363
|
|
|
366
364
|
if compact:
|
|
367
|
-
desc_click_coords = "👆
|
|
365
|
+
desc_click_coords = "👆 坐标点击(兜底)。优先用click_by_text/id,点击后用list_elements确认。"
|
|
368
366
|
else:
|
|
369
|
-
desc_click_coords = ("👆
|
|
370
|
-
"
|
|
371
|
-
"仅在
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
"- 如果误点击,调用 mobile_press_key(back) 返回\n"
|
|
375
|
-
"- 对于定时弹窗(如广告),建议等待其自动消失\n\n"
|
|
376
|
-
"📐 坐标转换:截图返回的 image_width/height 等参数直接传入即可\n\n"
|
|
377
|
-
"🔴 【必须】点击后必须再次截图确认操作是否成功!")
|
|
367
|
+
desc_click_coords = ("👆 点击指定坐标(⚠️ 兜底方案)\n\n"
|
|
368
|
+
"❌ 优先使用 click_by_text 或 click_by_id!\n"
|
|
369
|
+
"仅在 list_elements 无法获取元素时使用。\n\n"
|
|
370
|
+
"📐 坐标转换:截图返回的参数直接传入即可\n"
|
|
371
|
+
"📋 点击后用 list_elements 确认(❌不要截图确认!)")
|
|
378
372
|
|
|
379
373
|
tools.append(Tool(
|
|
380
374
|
name="mobile_click_at_coords",
|
|
@@ -863,7 +857,8 @@ class MobileMCPServer:
|
|
|
863
857
|
elif name == "mobile_click_by_text":
|
|
864
858
|
result = self.tools.click_by_text(
|
|
865
859
|
arguments["text"],
|
|
866
|
-
position=arguments.get("position")
|
|
860
|
+
position=arguments.get("position"),
|
|
861
|
+
verify=arguments.get("verify")
|
|
867
862
|
)
|
|
868
863
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
869
864
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mobile-mcp-ai
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.5
|
|
4
4
|
Summary: 移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)
|
|
5
5
|
Home-page: https://github.com/test111ddff-hash/mobile-mcp-ai
|
|
6
6
|
Author: douzi
|
|
@@ -21,13 +21,53 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.8
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: uiautomator2>=2.16.0
|
|
26
|
+
Requires-Dist: adbutils>=1.2.0
|
|
27
|
+
Requires-Dist: Pillow>=10.0.0
|
|
28
|
+
Requires-Dist: mcp>=0.9.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
24
30
|
Provides-Extra: ai
|
|
31
|
+
Requires-Dist: dashscope>=1.10.0; extra == "ai"
|
|
32
|
+
Requires-Dist: openai>=1.0.0; extra == "ai"
|
|
33
|
+
Requires-Dist: anthropic>=0.3.0; extra == "ai"
|
|
25
34
|
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
37
|
+
Requires-Dist: allure-pytest>=2.13.0; extra == "test"
|
|
26
38
|
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
41
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
27
43
|
Provides-Extra: ios
|
|
44
|
+
Requires-Dist: tidevice>=0.11.0; extra == "ios"
|
|
45
|
+
Requires-Dist: facebook-wda>=1.4.0; extra == "ios"
|
|
28
46
|
Provides-Extra: h5
|
|
47
|
+
Requires-Dist: Appium-Python-Client>=3.0.0; extra == "h5"
|
|
48
|
+
Requires-Dist: selenium>=4.0.0; extra == "h5"
|
|
29
49
|
Provides-Extra: all
|
|
30
|
-
|
|
50
|
+
Requires-Dist: dashscope>=1.10.0; extra == "all"
|
|
51
|
+
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
52
|
+
Requires-Dist: anthropic>=0.3.0; extra == "all"
|
|
53
|
+
Requires-Dist: Appium-Python-Client>=3.0.0; extra == "all"
|
|
54
|
+
Requires-Dist: selenium>=4.0.0; extra == "all"
|
|
55
|
+
Requires-Dist: pytest>=8.0.0; extra == "all"
|
|
56
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
|
|
57
|
+
Requires-Dist: allure-pytest>=2.13.0; extra == "all"
|
|
58
|
+
Dynamic: author
|
|
59
|
+
Dynamic: author-email
|
|
60
|
+
Dynamic: classifier
|
|
61
|
+
Dynamic: description
|
|
62
|
+
Dynamic: description-content-type
|
|
63
|
+
Dynamic: home-page
|
|
64
|
+
Dynamic: keywords
|
|
65
|
+
Dynamic: license-file
|
|
66
|
+
Dynamic: project-url
|
|
67
|
+
Dynamic: provides-extra
|
|
68
|
+
Dynamic: requires-dist
|
|
69
|
+
Dynamic: requires-python
|
|
70
|
+
Dynamic: summary
|
|
31
71
|
|
|
32
72
|
# 📱 Mobile MCP AI
|
|
33
73
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
MANIFEST.in
|
|
3
3
|
README.md
|
|
4
|
+
__init__.py
|
|
5
|
+
config.py
|
|
4
6
|
requirements.txt
|
|
5
7
|
setup.py
|
|
6
8
|
./__init__.py
|
|
@@ -29,13 +31,27 @@ setup.py
|
|
|
29
31
|
./utils/logger.py
|
|
30
32
|
./utils/xml_formatter.py
|
|
31
33
|
./utils/xml_parser.py
|
|
34
|
+
core/__init__.py
|
|
35
|
+
core/basic_tools_lite.py
|
|
36
|
+
core/device_manager.py
|
|
37
|
+
core/dynamic_config.py
|
|
38
|
+
core/ios_client_wda.py
|
|
39
|
+
core/ios_device_manager_wda.py
|
|
40
|
+
core/mobile_client.py
|
|
41
|
+
core/template_matcher.py
|
|
32
42
|
core/templates/close_buttons/auto_x_0112_151217.png
|
|
33
43
|
core/templates/close_buttons/auto_x_0112_152037.png
|
|
34
44
|
core/templates/close_buttons/auto_x_0112_152840.png
|
|
35
45
|
core/templates/close_buttons/auto_x_0112_153256.png
|
|
36
46
|
core/templates/close_buttons/auto_x_0112_154847.png
|
|
37
47
|
core/templates/close_buttons/gray_x_stock_ad.png
|
|
48
|
+
core/utils/__init__.py
|
|
49
|
+
core/utils/logger.py
|
|
50
|
+
core/utils/operation_history_manager.py
|
|
51
|
+
core/utils/smart_wait.py
|
|
38
52
|
docs/iOS_SETUP_GUIDE.md
|
|
53
|
+
mcp_tools/__init__.py
|
|
54
|
+
mcp_tools/mcp_server.py
|
|
39
55
|
mobile_mcp_ai.egg-info/PKG-INFO
|
|
40
56
|
mobile_mcp_ai.egg-info/SOURCES.txt
|
|
41
57
|
mobile_mcp_ai.egg-info/dependency_links.txt
|
|
@@ -48,4 +64,18 @@ templates/close_buttons/auto_x_0112_152037.png
|
|
|
48
64
|
templates/close_buttons/auto_x_0112_152840.png
|
|
49
65
|
templates/close_buttons/auto_x_0112_153256.png
|
|
50
66
|
templates/close_buttons/auto_x_0112_154847.png
|
|
51
|
-
templates/close_buttons/gray_x_stock_ad.png
|
|
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
|
|
78
|
+
utils/__init__.py
|
|
79
|
+
utils/logger.py
|
|
80
|
+
utils/xml_formatter.py
|
|
81
|
+
utils/xml_parser.py
|
|
@@ -25,7 +25,7 @@ if requirements_file.exists():
|
|
|
25
25
|
|
|
26
26
|
setup(
|
|
27
27
|
name="mobile-mcp-ai",
|
|
28
|
-
version="2.6.
|
|
28
|
+
version="2.6.5", # 代码优化更新
|
|
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()
|