mobile-mcp-ai 2.2.6__py3-none-any.whl → 2.5.3__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.
Files changed (52) hide show
  1. mobile_mcp/config.py +3 -2
  2. mobile_mcp/core/basic_tools_lite.py +3193 -0
  3. mobile_mcp/core/ios_client_wda.py +569 -0
  4. mobile_mcp/core/ios_device_manager_wda.py +306 -0
  5. mobile_mcp/core/mobile_client.py +246 -20
  6. mobile_mcp/core/template_matcher.py +429 -0
  7. mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
  8. mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
  9. mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
  10. mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
  11. mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
  12. mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
  13. mobile_mcp/mcp_tools/__init__.py +10 -0
  14. mobile_mcp/mcp_tools/mcp_server.py +992 -0
  15. mobile_mcp_ai-2.5.3.dist-info/METADATA +456 -0
  16. mobile_mcp_ai-2.5.3.dist-info/RECORD +32 -0
  17. mobile_mcp_ai-2.5.3.dist-info/entry_points.txt +2 -0
  18. mobile_mcp/core/ai/__init__.py +0 -11
  19. mobile_mcp/core/ai/ai_analyzer.py +0 -197
  20. mobile_mcp/core/ai/ai_config.py +0 -116
  21. mobile_mcp/core/ai/ai_platform_adapter.py +0 -399
  22. mobile_mcp/core/ai/smart_test_executor.py +0 -520
  23. mobile_mcp/core/ai/test_generator.py +0 -365
  24. mobile_mcp/core/ai/test_generator_from_history.py +0 -391
  25. mobile_mcp/core/ai/test_generator_standalone.py +0 -293
  26. mobile_mcp/core/assertion/__init__.py +0 -9
  27. mobile_mcp/core/assertion/smart_assertion.py +0 -341
  28. mobile_mcp/core/basic_tools.py +0 -945
  29. mobile_mcp/core/h5/__init__.py +0 -10
  30. mobile_mcp/core/h5/h5_handler.py +0 -548
  31. mobile_mcp/core/ios_client.py +0 -219
  32. mobile_mcp/core/ios_device_manager.py +0 -252
  33. mobile_mcp/core/locator/__init__.py +0 -10
  34. mobile_mcp/core/locator/cursor_ai_auto_analyzer.py +0 -119
  35. mobile_mcp/core/locator/cursor_vision_helper.py +0 -414
  36. mobile_mcp/core/locator/mobile_smart_locator.py +0 -1747
  37. mobile_mcp/core/locator/position_analyzer.py +0 -813
  38. mobile_mcp/core/locator/script_updater.py +0 -157
  39. mobile_mcp/core/nl_test_runner.py +0 -585
  40. mobile_mcp/core/smart_app_launcher.py +0 -421
  41. mobile_mcp/core/smart_tools.py +0 -311
  42. mobile_mcp/mcp/__init__.py +0 -13
  43. mobile_mcp/mcp/mcp_server.py +0 -1126
  44. mobile_mcp/mcp/mcp_server_simple.py +0 -23
  45. mobile_mcp/vision/__init__.py +0 -10
  46. mobile_mcp/vision/vision_locator.py +0 -405
  47. mobile_mcp_ai-2.2.6.dist-info/METADATA +0 -503
  48. mobile_mcp_ai-2.2.6.dist-info/RECORD +0 -49
  49. mobile_mcp_ai-2.2.6.dist-info/entry_points.txt +0 -2
  50. {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/WHEEL +0 -0
  51. {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/licenses/LICENSE +0 -0
  52. {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/top_level.txt +0 -0
@@ -1,421 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- 智能App启动器 - 处理广告、弹窗、加载等待
5
- """
6
- import asyncio
7
- import sys
8
- from typing import Dict, Optional
9
-
10
- from .dynamic_config import DynamicConfig
11
-
12
-
13
- class SmartAppLauncher:
14
- """
15
- 智能App启动器
16
-
17
- 功能:
18
- 1. 启动App后智能等待主页加载
19
- 2. 自动检测并关闭广告/弹窗
20
- 3. 等待网络加载完成
21
- 4. 智能判断是否进入主页
22
- """
23
-
24
- def __init__(self, mobile_client):
25
- """
26
- 初始化智能启动器
27
-
28
- Args:
29
- mobile_client: MobileClient实例
30
- """
31
- self.client = mobile_client
32
-
33
- # 常见的广告/弹窗关闭按钮特征
34
- self.ad_close_keywords = [
35
- '跳过', '关闭', '×', 'X', 'x', '✕',
36
- 'skip', 'close', '稍后', '取消',
37
- '我知道了', '不再提示', '下次再说',
38
- '暂不', '以后再说', '返回'
39
- ]
40
-
41
- # 常见的弹窗容器特征
42
- self.popup_keywords = [
43
- 'dialog', 'popup', 'alert', 'modal',
44
- '弹窗', '对话框', '提示'
45
- ]
46
-
47
- async def launch_with_smart_wait(
48
- self,
49
- package_name: str,
50
- max_wait: int = 3, # 优化:最多等待3秒(快速启动)
51
- auto_close_ads: bool = True
52
- ) -> Dict:
53
- """
54
- 智能启动App并等待主页加载
55
-
56
- Args:
57
- package_name: App包名
58
- max_wait: 最大等待时间(秒,默认3秒 - 快速模式)
59
- auto_close_ads: 是否自动关闭广告/弹窗
60
-
61
- Returns:
62
- 启动结果
63
- """
64
- print(f"\n🚀 智能启动App: {package_name}", file=sys.stderr)
65
-
66
- try:
67
- # 🎯 启动前:强制恢复竖屏(防止上次横屏残留)
68
- print(f" 🔄 检查屏幕方向...", file=sys.stderr)
69
- self.client.force_portrait()
70
-
71
- # 1. 启动App
72
- print(f" 📱 正在启动...", file=sys.stderr)
73
- self.client.u2.app_start(package_name)
74
- await asyncio.sleep(1) # 等待App进程启动
75
-
76
- # 🎯 启动后:再次强制竖屏(防止App启动时强制横屏)
77
- self.client.force_portrait()
78
-
79
- # 2. 验证App是否启动
80
- current_package = await self._get_current_package()
81
- if current_package != package_name:
82
- return {
83
- "success": False,
84
- "reason": f"App启动失败,当前: {current_package},期望: {package_name}"
85
- }
86
-
87
- print(f" ✅ App进程已启动", file=sys.stderr)
88
-
89
- # 🎯 关键优化:确保启动后至少等待 2 秒,让界面完全渲染
90
- print(f" ⏳ 等待界面渲染...", file=sys.stderr)
91
- await asyncio.sleep(2) # 最小等待 2 秒
92
- print(f" ✅ 已等待 2 秒,界面应已稳定", file=sys.stderr)
93
-
94
- # 3. 快速等待并自动截图验证(新策略)
95
- result = await self._wait_for_home_page_fast(
96
- package_name,
97
- max_wait=max_wait,
98
- auto_close_ads=auto_close_ads
99
- )
100
-
101
- # 快速模式:总是返回成功+截图路径
102
- print(f" ✅ App已启动{result['wait_time'] + 2:.1f}秒,已自动截图", file=sys.stderr)
103
- return {
104
- "success": True,
105
- "package": package_name,
106
- "wait_time": result['wait_time'],
107
- "ads_closed": result['ads_closed'],
108
- "screenshot_path": result.get('screenshot_path'),
109
- "message": "App已启动,请查看截图确认是否进入主页"
110
- }
111
-
112
- except Exception as e:
113
- print(f" ❌ 智能启动失败: {e}", file=sys.stderr)
114
- return {
115
- "success": False,
116
- "reason": str(e)
117
- }
118
-
119
- async def _wait_for_home_page(
120
- self,
121
- package_name: str,
122
- max_wait: int = 5, # 优化:从10秒减少到5秒
123
- auto_close_ads: bool = True
124
- ) -> Dict:
125
- """
126
- 等待主页加载完成
127
-
128
- 策略:
129
- 1. 每0.5秒检查一次页面状态
130
- 2. 检测广告/弹窗并自动关闭
131
- 3. 检测页面是否稳定(元素不再变化)
132
- 4. 超时后返回当前状态
133
-
134
- Returns:
135
- {
136
- "loaded": bool, # 是否加载完成
137
- "wait_time": float, # 等待时间
138
- "ads_closed": int, # 关闭的广告数
139
- "popups_closed": int # 关闭的弹窗数
140
- }
141
- """
142
- import time
143
- start_time = time.time()
144
-
145
- ads_closed = 0
146
- popups_closed = 0
147
- last_snapshot = None
148
- stable_count = 0 # 页面稳定计数(连续2次快照相同认为稳定)
149
-
150
- print(f" ⏳ 等待主页加载(最多{max_wait}秒)...", file=sys.stderr)
151
-
152
- check_interval = 0.3 # 优化:每0.3秒检查一次(更快响应)
153
- max_checks = int(max_wait / check_interval)
154
-
155
- for i in range(max_checks):
156
- await asyncio.sleep(check_interval)
157
- elapsed = time.time() - start_time
158
-
159
- # 检查当前包名(防止跳转到其他App)
160
- current_package = await self._get_current_package()
161
- if current_package != package_name:
162
- print(f" ⚠️ 检测到包名变化: {package_name} -> {current_package}", file=sys.stderr)
163
- # 可能跳转到其他页面(如授权页),继续等待
164
- await asyncio.sleep(1)
165
- continue
166
-
167
- # 获取页面快照
168
- try:
169
- snapshot = self.client.u2.dump_hierarchy()
170
-
171
- # 1. 检测并关闭广告/弹窗
172
- if auto_close_ads:
173
- closed = await self._try_close_ads_and_popups(snapshot)
174
- if closed:
175
- ads_closed += closed
176
- print(f" 🎯 已关闭 {closed} 个广告/弹窗", file=sys.stderr)
177
- await asyncio.sleep(0.3) # 等待关闭动画(从0.5秒优化到0.3秒)
178
- continue # 重新检查
179
-
180
- # 2. 检测页面是否稳定
181
- if last_snapshot and snapshot == last_snapshot:
182
- stable_count += 1
183
- if stable_count >= 2:
184
- # 页面已稳定(连续2次快照相同)
185
- print(f" ✅ 页面稳定,加载完成(耗时{elapsed:.1f}秒)", file=sys.stderr)
186
- return {
187
- "loaded": True,
188
- "wait_time": elapsed,
189
- "ads_closed": ads_closed,
190
- "popups_closed": popups_closed
191
- }
192
- else:
193
- stable_count = 0
194
-
195
- last_snapshot = snapshot
196
-
197
- # 优化:每1.5秒打印一次等待进度(从2秒减少)
198
- if i % 5 == 0 and i > 0: # 5 * 0.3秒 = 1.5秒
199
- print(f" ⏳ 等待中... ({elapsed:.1f}秒)", file=sys.stderr)
200
-
201
- except Exception as e:
202
- print(f" ⚠️ 检查页面状态失败: {e}", file=sys.stderr)
203
- continue
204
-
205
- # 超时
206
- elapsed = time.time() - start_time
207
- print(f" ⏰ 等待超时({elapsed:.1f}秒),但App已启动", file=sys.stderr)
208
- return {
209
- "loaded": False,
210
- "wait_time": elapsed,
211
- "ads_closed": ads_closed,
212
- "popups_closed": popups_closed
213
- }
214
-
215
- async def _try_close_ads_and_popups(self, snapshot: str) -> int:
216
- """
217
- 尝试关闭广告和弹窗(更谨慎的检测逻辑)
218
-
219
- Args:
220
- snapshot: 页面XML快照
221
-
222
- Returns:
223
- 关闭的数量
224
- """
225
- closed_count = 0
226
-
227
- try:
228
- # 解析XML查找关闭按钮
229
- elements = self.client.xml_parser.parse(snapshot)
230
-
231
- # 🎯 改进:先检测是否有弹窗容器(避免误点击正常UI)
232
- has_popup = False
233
- for elem in elements:
234
- class_name = elem.get('class', '').lower()
235
- resource_id = elem.get('resource_id', '').lower()
236
- # 检查是否是弹窗容器
237
- if any(keyword in class_name or keyword in resource_id
238
- for keyword in ['dialog', 'popup', 'alert', 'modal']):
239
- has_popup = True
240
- break
241
-
242
- # 如果没有检测到弹窗容器,不执行关闭操作(避免误点击)
243
- if not has_popup:
244
- return 0
245
-
246
- # 查找可能的关闭按钮
247
- close_buttons = []
248
-
249
- for elem in elements:
250
- if not elem.get('clickable', False):
251
- continue
252
-
253
- text = elem.get('text', '').lower()
254
- content_desc = elem.get('content_desc', '').lower()
255
- resource_id = elem.get('resource_id', '').lower()
256
- bounds = elem.get('bounds', '')
257
-
258
- # 检查是否是关闭按钮
259
- is_close_button = False
260
- for keyword in self.ad_close_keywords:
261
- keyword_lower = keyword.lower()
262
- if (keyword_lower in text or
263
- keyword_lower in content_desc or
264
- keyword_lower in resource_id or
265
- ('close' in resource_id and 'btn' in resource_id) or
266
- ('skip' in resource_id)):
267
- is_close_button = True
268
- break
269
-
270
- # 🎯 改进:优先选择右上角的关闭按钮(更可能是真正的关闭按钮)
271
- if is_close_button and bounds:
272
- import re
273
- match = re.search(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
274
- if match:
275
- x1, y1, x2, y2 = map(int, match.groups())
276
- # 计算元素位置(右上角区域优先级更高)
277
- elem['_priority'] = 0
278
- if x1 > 800: # 右侧
279
- elem['_priority'] += 2
280
- if y1 < 500: # 上部
281
- elem['_priority'] += 2
282
- close_buttons.append(elem)
283
-
284
-
285
- # 🎯 改进:按优先级排序(右上角的关闭按钮优先)
286
- close_buttons.sort(key=lambda x: x.get('_priority', 0), reverse=True)
287
-
288
- # 尝试点击关闭按钮(使用动态配置的最大数量,避免误点击)
289
- max_buttons = DynamicConfig.max_close_buttons
290
- for button in close_buttons[:max_buttons]:
291
- try:
292
- # 优先使用bounds点击(更可靠)
293
- bounds = button.get('bounds', '')
294
- if bounds:
295
- # 解析bounds并点击中心点
296
- import re
297
- match = re.search(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
298
- if match:
299
- x1, y1, x2, y2 = map(int, match.groups())
300
- center_x = (x1 + x2) // 2
301
- center_y = (y1 + y2) // 2
302
-
303
- button_desc = button.get('text') or button.get('content_desc') or '未知'
304
- print(f" 🎯 检测到弹窗,点击关闭按钮: {button_desc} (位置: {center_x}, {center_y})", file=sys.stderr)
305
-
306
- # 使用动态配置的等待时间
307
- await asyncio.sleep(DynamicConfig.wait_before_close_ad)
308
-
309
- self.client.u2.click(center_x, center_y)
310
- closed_count += 1
311
-
312
- print(f" ✅ 已关闭弹窗: {button_desc}", file=sys.stderr)
313
-
314
- # 等待关闭动画(使用动态配置)
315
- await asyncio.sleep(DynamicConfig.wait_after_click)
316
-
317
- except Exception as e:
318
- print(f" ⚠️ 点击关闭按钮失败: {e}", file=sys.stderr)
319
- continue
320
-
321
- return closed_count
322
-
323
- except Exception as e:
324
- print(f" ⚠️ 关闭广告/弹窗失败: {e}", file=sys.stderr)
325
- return 0
326
-
327
- async def _wait_for_home_page_fast(
328
- self,
329
- package_name: str,
330
- max_wait: int = 3, # 快速模式:最多3秒
331
- auto_close_ads: bool = True
332
- ) -> Dict:
333
- """
334
- 快速启动模式:等待短时间后立即截图,让Cursor AI判断是否已进入主页
335
-
336
- 策略:
337
- 1. 等待2秒(让App基本启动)
338
- 2. 期间检测并关闭广告/弹窗
339
- 3. 等待结束后立即截图
340
- 4. 提示用户通过Cursor AI验证截图
341
-
342
- Returns:
343
- {
344
- "loaded": bool, # 是否加载完成
345
- "wait_time": float, # 等待时间
346
- "ads_closed": int, # 关闭的广告数
347
- "screenshot_path": str # 截图路径(供AI验证)
348
- }
349
- """
350
- import time
351
- start_time = time.time()
352
-
353
- ads_closed = 0
354
- screenshot_path = None
355
-
356
- print(f" ⏳ 快速启动模式:等待{max_wait}秒并自动截图...", file=sys.stderr)
357
-
358
- # 快速检测广告/弹窗(最多检查3次,每次1秒)
359
- for i in range(min(max_wait, 3)):
360
- await asyncio.sleep(1)
361
- elapsed = time.time() - start_time
362
-
363
- # 检查当前包名
364
- current_package = await self._get_current_package()
365
- if current_package != package_name:
366
- print(f" ⚠️ 检测到包名变化: {package_name} -> {current_package}", file=sys.stderr)
367
- await asyncio.sleep(0.5)
368
- continue
369
-
370
- # 检测并关闭广告/弹窗
371
- if auto_close_ads:
372
- try:
373
- snapshot = self.client.u2.dump_hierarchy()
374
- closed = await self._try_close_ads_and_popups(snapshot)
375
- if closed:
376
- ads_closed += closed
377
- print(f" 🎯 已关闭 {closed} 个广告/弹窗", file=sys.stderr)
378
- except Exception as e:
379
- print(f" ⚠️ 检查弹窗失败: {e}", file=sys.stderr)
380
-
381
- print(f" ⏳ 已等待{int(elapsed)}秒...", file=sys.stderr)
382
-
383
- # 等待结束,立即截图
384
- elapsed = time.time() - start_time
385
- print(f" 📸 App已启动{elapsed:.1f}秒,正在截图供AI验证...", file=sys.stderr)
386
-
387
- try:
388
- import re
389
- timestamp = time.strftime("%Y%m%d_%H%M%S")
390
- from pathlib import Path
391
- project_root = Path(__file__).parent.parent
392
- screenshot_dir = project_root / "screenshots"
393
- screenshot_dir.mkdir(exist_ok=True)
394
-
395
- # 生成截图文件名
396
- safe_package = re.sub(r'[^\w\s-]', '', package_name).strip()
397
- filename = f"app_launch_{safe_package}_{timestamp}.png"
398
- screenshot_path = screenshot_dir / filename
399
-
400
- # 截图
401
- self.client.u2.screenshot(str(screenshot_path))
402
- print(f" ✅ 截图已保存: {screenshot_path}", file=sys.stderr)
403
- print(f" 💡 提示:请查看截图,确认是否已进入主页", file=sys.stderr)
404
-
405
- except Exception as e:
406
- print(f" ⚠️ 截图失败: {e}", file=sys.stderr)
407
-
408
- return {
409
- "wait_time": elapsed,
410
- "ads_closed": ads_closed,
411
- "screenshot_path": str(screenshot_path) if screenshot_path else None
412
- }
413
-
414
- async def _get_current_package(self) -> Optional[str]:
415
- """获取当前包名"""
416
- try:
417
- info = self.client.u2.app_current()
418
- return info.get('package')
419
- except:
420
- return None
421
-