mobile-mcp-ai 2.1.2__py3-none-any.whl → 2.5.8__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 (65) hide show
  1. mobile_mcp/__init__.py +34 -0
  2. mobile_mcp/config.py +142 -0
  3. mobile_mcp/core/basic_tools_lite.py +3266 -0
  4. {core → mobile_mcp/core}/device_manager.py +2 -2
  5. mobile_mcp/core/dynamic_config.py +272 -0
  6. mobile_mcp/core/ios_client_wda.py +569 -0
  7. mobile_mcp/core/ios_device_manager_wda.py +306 -0
  8. {core → mobile_mcp/core}/mobile_client.py +279 -39
  9. mobile_mcp/core/template_matcher.py +429 -0
  10. mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
  11. mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
  12. mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
  13. mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
  14. mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
  15. mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
  16. {core → mobile_mcp/core}/utils/smart_wait.py +3 -3
  17. mobile_mcp/mcp_tools/__init__.py +10 -0
  18. mobile_mcp/mcp_tools/mcp_server.py +1071 -0
  19. mobile_mcp_ai-2.5.8.dist-info/METADATA +469 -0
  20. mobile_mcp_ai-2.5.8.dist-info/RECORD +32 -0
  21. mobile_mcp_ai-2.5.8.dist-info/entry_points.txt +2 -0
  22. mobile_mcp_ai-2.5.8.dist-info/licenses/LICENSE +201 -0
  23. mobile_mcp_ai-2.5.8.dist-info/top_level.txt +1 -0
  24. core/ai/__init__.py +0 -11
  25. core/ai/ai_analyzer.py +0 -197
  26. core/ai/ai_config.py +0 -116
  27. core/ai/ai_platform_adapter.py +0 -399
  28. core/ai/smart_test_executor.py +0 -520
  29. core/ai/test_generator.py +0 -365
  30. core/ai/test_generator_from_history.py +0 -391
  31. core/ai/test_generator_standalone.py +0 -293
  32. core/assertion/__init__.py +0 -9
  33. core/assertion/smart_assertion.py +0 -341
  34. core/basic_tools.py +0 -377
  35. core/h5/__init__.py +0 -10
  36. core/h5/h5_handler.py +0 -548
  37. core/ios_client.py +0 -219
  38. core/ios_device_manager.py +0 -252
  39. core/locator/__init__.py +0 -10
  40. core/locator/cursor_ai_auto_analyzer.py +0 -119
  41. core/locator/cursor_vision_helper.py +0 -414
  42. core/locator/mobile_smart_locator.py +0 -1640
  43. core/locator/position_analyzer.py +0 -813
  44. core/locator/script_updater.py +0 -157
  45. core/nl_test_runner.py +0 -585
  46. core/smart_app_launcher.py +0 -334
  47. core/smart_tools.py +0 -311
  48. mcp/__init__.py +0 -8
  49. mcp/mcp_server.py +0 -1919
  50. mcp/mcp_server_simple.py +0 -476
  51. mobile_mcp_ai-2.1.2.dist-info/METADATA +0 -567
  52. mobile_mcp_ai-2.1.2.dist-info/RECORD +0 -45
  53. mobile_mcp_ai-2.1.2.dist-info/entry_points.txt +0 -2
  54. mobile_mcp_ai-2.1.2.dist-info/top_level.txt +0 -4
  55. vision/__init__.py +0 -10
  56. vision/vision_locator.py +0 -404
  57. {core → mobile_mcp/core}/__init__.py +0 -0
  58. {core → mobile_mcp/core}/utils/__init__.py +0 -0
  59. {core → mobile_mcp/core}/utils/logger.py +0 -0
  60. {core → mobile_mcp/core}/utils/operation_history_manager.py +0 -0
  61. {utils → mobile_mcp/utils}/__init__.py +0 -0
  62. {utils → mobile_mcp/utils}/logger.py +0 -0
  63. {utils → mobile_mcp/utils}/xml_formatter.py +0 -0
  64. {utils → mobile_mcp/utils}/xml_parser.py +0 -0
  65. {mobile_mcp_ai-2.1.2.dist-info → mobile_mcp_ai-2.5.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ iOS客户端 - 使用 facebook-wda(API风格和 uiautomator2 一样)
5
+
6
+ 优势:
7
+ 1. API和Android端(uiautomator2)几乎完全一致
8
+ 2. 不需要Appium Server
9
+ 3. 代码可以跨平台复用
10
+
11
+ 用法:
12
+ client = IOSClientWDA(device_id=None)
13
+ await client.launch_app("com.example.app")
14
+ await client.click("登录") # 和Android端一样的调用方式!
15
+ """
16
+ import asyncio
17
+ import sys
18
+ import time
19
+ from typing import Dict, Optional, List
20
+
21
+ from .ios_device_manager_wda import IOSDeviceManagerWDA
22
+
23
+
24
+ class IOSClientWDA:
25
+ """
26
+ iOS客户端 - 使用 facebook-wda
27
+
28
+ API风格和MobileClient(Android)保持一致
29
+
30
+ 用法:
31
+ client = IOSClientWDA(device_id=None)
32
+ await client.launch_app("com.apple.Preferences")
33
+ await client.click("通用")
34
+ """
35
+
36
+ def __init__(self, device_id: Optional[str] = None, lazy_connect: bool = False):
37
+ """
38
+ 初始化iOS客户端
39
+
40
+ Args:
41
+ device_id: 设备UDID,None则自动选择第一个设备
42
+ lazy_connect: 是否延迟连接(默认False)
43
+ """
44
+ self.device_manager = IOSDeviceManagerWDA()
45
+ self._device_id = device_id
46
+ self._lazy_connect = lazy_connect
47
+
48
+ if not lazy_connect:
49
+ self.wda = self.device_manager.connect(device_id)
50
+ else:
51
+ self.wda = None
52
+
53
+ # 操作历史(用于录制)
54
+ self.operation_history: List[Dict] = []
55
+
56
+ # 缓存
57
+ self._snapshot_cache = None
58
+ self._cache_timestamp = 0
59
+ self._cache_ttl = 1 # 缓存1秒
60
+
61
+ def _ensure_connected(self):
62
+ """确保设备已连接"""
63
+ if self.wda is None:
64
+ self.wda = self.device_manager.connect(self._device_id)
65
+
66
+ async def snapshot(self, use_cache: bool = True) -> str:
67
+ """
68
+ 获取页面XML结构
69
+
70
+ Args:
71
+ use_cache: 是否使用缓存
72
+
73
+ Returns:
74
+ 页面结构字符串
75
+ """
76
+ self._ensure_connected()
77
+
78
+ # 检查缓存
79
+ if use_cache and self._snapshot_cache:
80
+ current_time = time.time()
81
+ if current_time - self._cache_timestamp < self._cache_ttl:
82
+ return self._snapshot_cache
83
+
84
+ try:
85
+ # 获取页面源码
86
+ source = self.wda.source()
87
+
88
+ # 更新缓存
89
+ self._snapshot_cache = source
90
+ self._cache_timestamp = time.time()
91
+
92
+ return source
93
+ except Exception as e:
94
+ raise RuntimeError(f"获取页面结构失败: {e}")
95
+
96
+ async def click(self, element: str, ref: Optional[str] = None, verify: bool = True):
97
+ """
98
+ 点击元素(API和Android端一致)
99
+
100
+ Args:
101
+ element: 元素描述(自然语言)
102
+ ref: 元素定位器,支持多种格式:
103
+ - text: 如 "登录"
104
+ - accessibility_id: 如 "login_button"
105
+ - xpath: 如 "//XCUIElementTypeButton[@name='登录']"
106
+ - bounds: 如 "[100,200][300,400]" 或坐标 (x, y)
107
+ verify: 是否验证点击成功
108
+
109
+ Returns:
110
+ 操作结果
111
+ """
112
+ self._ensure_connected()
113
+
114
+ # 记录操作
115
+ operation_record = {
116
+ 'action': 'click',
117
+ 'element': element,
118
+ 'ref': ref,
119
+ 'success': False,
120
+ }
121
+ self.operation_history.append(operation_record)
122
+
123
+ try:
124
+ if ref:
125
+ # 根据ref类型执行点击
126
+ if ref.startswith('//'):
127
+ # XPath
128
+ elem = self.wda(xpath=ref)
129
+ elif ref.startswith('[') and '][' in ref:
130
+ # bounds坐标 "[x1,y1][x2,y2]"
131
+ x, y = self._parse_bounds_coords(ref)
132
+ self.wda.click(x, y)
133
+ print(f" ✅ 坐标点击成功: ({x}, {y})", file=sys.stderr)
134
+ operation_record['success'] = True
135
+ return {"success": True, "ref": ref}
136
+ elif ',' in ref and ref.replace(',', '').replace(' ', '').isdigit():
137
+ # 直接坐标 "x,y"
138
+ parts = ref.split(',')
139
+ x, y = int(parts[0].strip()), int(parts[1].strip())
140
+ self.wda.click(x, y)
141
+ print(f" ✅ 坐标点击成功: ({x}, {y})", file=sys.stderr)
142
+ operation_record['success'] = True
143
+ return {"success": True, "ref": ref}
144
+ else:
145
+ # 默认尝试多种定位方式
146
+ elem = self._find_element(ref)
147
+ else:
148
+ # 使用元素描述进行定位
149
+ elem = self._find_element(element)
150
+
151
+ # 点击元素
152
+ if elem and elem.exists:
153
+ elem.click()
154
+ print(f" ✅ 点击成功: {ref or element}", file=sys.stderr)
155
+ operation_record['success'] = True
156
+
157
+ # 等待页面稳定
158
+ await asyncio.sleep(0.3)
159
+
160
+ return {"success": True, "ref": ref or element}
161
+ else:
162
+ raise ValueError(f"未找到元素: {ref or element}")
163
+
164
+ except Exception as e:
165
+ operation_record['error'] = str(e)
166
+ print(f" ❌ 点击失败: {e}", file=sys.stderr)
167
+ return {"success": False, "reason": str(e)}
168
+
169
+ def _find_element(self, locator: str):
170
+ """
171
+ 尝试多种方式定位元素
172
+
173
+ Args:
174
+ locator: 定位器字符串
175
+
176
+ Returns:
177
+ 元素对象或None
178
+ """
179
+ # 尝试顺序:name > label > text > accessibility_id
180
+ strategies = [
181
+ lambda: self.wda(name=locator),
182
+ lambda: self.wda(label=locator),
183
+ lambda: self.wda(text=locator),
184
+ lambda: self.wda(nameContains=locator),
185
+ lambda: self.wda(labelContains=locator),
186
+ ]
187
+
188
+ for strategy in strategies:
189
+ try:
190
+ elem = strategy()
191
+ if elem.exists:
192
+ return elem
193
+ except:
194
+ continue
195
+
196
+ return None
197
+
198
+ async def type_text(self, element: str, text: str, ref: Optional[str] = None, verify: bool = True):
199
+ """
200
+ 输入文本(API和Android端一致)
201
+
202
+ Args:
203
+ element: 元素描述
204
+ text: 要输入的文本
205
+ ref: 元素定位器
206
+ verify: 是否验证输入成功
207
+
208
+ Returns:
209
+ 操作结果
210
+ """
211
+ self._ensure_connected()
212
+
213
+ # 记录操作
214
+ operation_record = {
215
+ 'action': 'type',
216
+ 'element': element,
217
+ 'text': text,
218
+ 'ref': ref,
219
+ 'success': False,
220
+ }
221
+ self.operation_history.append(operation_record)
222
+
223
+ try:
224
+ if ref:
225
+ if ref.startswith('//'):
226
+ elem = self.wda(xpath=ref)
227
+ else:
228
+ elem = self._find_element(ref)
229
+ else:
230
+ # 查找第一个输入框
231
+ elem = self.wda(className='XCUIElementTypeTextField')
232
+ if not elem.exists:
233
+ elem = self.wda(className='XCUIElementTypeSecureTextField')
234
+ if not elem.exists:
235
+ elem = self._find_element(element)
236
+
237
+ if elem and elem.exists:
238
+ elem.clear_text()
239
+ elem.set_text(text)
240
+ print(f" ✅ 输入成功: {text}", file=sys.stderr)
241
+ operation_record['success'] = True
242
+ return {"success": True, "ref": ref or element}
243
+ else:
244
+ raise ValueError(f"未找到输入框: {ref or element}")
245
+
246
+ except Exception as e:
247
+ operation_record['error'] = str(e)
248
+ print(f" ❌ 输入失败: {e}", file=sys.stderr)
249
+ return {"success": False, "reason": str(e)}
250
+
251
+ async def swipe(self, direction: str, distance: int = 500, verify: bool = True):
252
+ """
253
+ 滑动操作
254
+
255
+ Args:
256
+ direction: 滑动方向 ('up', 'down', 'left', 'right')
257
+ distance: 滑动距离(像素)
258
+ verify: 是否验证滑动成功
259
+
260
+ Returns:
261
+ 操作结果
262
+ """
263
+ self._ensure_connected()
264
+
265
+ try:
266
+ # 获取屏幕尺寸
267
+ window = self.wda.window_size()
268
+ width = window.width
269
+ height = window.height
270
+
271
+ # 计算滑动坐标
272
+ center_x = width // 2
273
+ center_y = height // 2
274
+
275
+ direction_map = {
276
+ 'up': (center_x, int(height * 0.8), center_x, int(height * 0.2)),
277
+ 'down': (center_x, int(height * 0.2), center_x, int(height * 0.8)),
278
+ 'left': (int(width * 0.8), center_y, int(width * 0.2), center_y),
279
+ 'right': (int(width * 0.2), center_y, int(width * 0.8), center_y),
280
+ }
281
+
282
+ if direction not in direction_map:
283
+ return {"success": False, "reason": f"不支持的滑动方向: {direction}"}
284
+
285
+ x1, y1, x2, y2 = direction_map[direction]
286
+
287
+ print(f" 📍 滑动方向: {direction}, 坐标: ({x1}, {y1}) -> ({x2}, {y2})", file=sys.stderr)
288
+ self.wda.swipe(x1, y1, x2, y2, duration=0.5)
289
+
290
+ print(f" ✅ 滑动成功: {direction}", file=sys.stderr)
291
+ return {"success": True, "direction": direction}
292
+
293
+ except Exception as e:
294
+ print(f" ❌ 滑动失败: {e}", file=sys.stderr)
295
+ return {"success": False, "reason": str(e)}
296
+
297
+ async def launch_app(self, bundle_id: str, wait_time: int = 3):
298
+ """
299
+ 启动应用
300
+
301
+ Args:
302
+ bundle_id: 应用Bundle ID,如 'com.apple.Preferences'
303
+ wait_time: 等待时间(秒)
304
+
305
+ Returns:
306
+ 操作结果
307
+ """
308
+ self._ensure_connected()
309
+
310
+ try:
311
+ print(f" 📱 启动App: {bundle_id}", file=sys.stderr)
312
+
313
+ # 使用 wda 启动应用
314
+ self.wda.session().app_activate(bundle_id)
315
+
316
+ # 等待应用启动
317
+ await asyncio.sleep(wait_time)
318
+
319
+ # 验证是否启动成功
320
+ current = await self.get_current_package()
321
+ if current == bundle_id:
322
+ print(f" ✅ App启动成功: {bundle_id}", file=sys.stderr)
323
+ return {"success": True, "package": bundle_id}
324
+ else:
325
+ print(f" ⚠️ App可能未启动成功,当前App: {current}", file=sys.stderr)
326
+ return {"success": True, "package": bundle_id, "warning": f"当前App: {current}"}
327
+
328
+ except Exception as e:
329
+ print(f" ❌ App启动失败: {e}", file=sys.stderr)
330
+ return {"success": False, "reason": str(e)}
331
+
332
+ async def stop_app(self, bundle_id: str):
333
+ """
334
+ 停止应用
335
+
336
+ Args:
337
+ bundle_id: 应用Bundle ID
338
+
339
+ Returns:
340
+ 操作结果
341
+ """
342
+ self._ensure_connected()
343
+
344
+ try:
345
+ print(f" 📱 停止App: {bundle_id}", file=sys.stderr)
346
+ self.wda.session().app_terminate(bundle_id)
347
+ print(f" ✅ App已停止: {bundle_id}", file=sys.stderr)
348
+ return {"success": True}
349
+ except Exception as e:
350
+ print(f" ❌ App停止失败: {e}", file=sys.stderr)
351
+ return {"success": False, "reason": str(e)}
352
+
353
+ async def get_current_package(self) -> Optional[str]:
354
+ """获取当前前台应用的Bundle ID"""
355
+ self._ensure_connected()
356
+
357
+ try:
358
+ app_info = self.wda.session().app_current()
359
+ return app_info.get('bundleId')
360
+ except:
361
+ return None
362
+
363
+ async def press_key(self, key: str, verify: bool = True):
364
+ """
365
+ 按键盘按键
366
+
367
+ Args:
368
+ key: 按键名称,支持:
369
+ - "enter" / "回车" - Enter键
370
+ - "back" / "返回" - 返回(在iOS上是导航返回)
371
+ - "home" - Home键
372
+ verify: 是否验证按键效果
373
+
374
+ Returns:
375
+ 操作结果
376
+ """
377
+ self._ensure_connected()
378
+
379
+ try:
380
+ key_lower = key.lower()
381
+
382
+ if key_lower in ['enter', '回车', 'return']:
383
+ # 发送回车键
384
+ self.wda(className='XCUIElementTypeKeyboard').buttons['return'].click()
385
+ print(f" ✅ 按键成功: Enter", file=sys.stderr)
386
+ elif key_lower in ['back', '返回']:
387
+ # iOS没有真正的返回键,尝试点击导航栏的返回按钮
388
+ back_buttons = [
389
+ self.wda(name='返回'),
390
+ self.wda(name='Back'),
391
+ self.wda(label='返回'),
392
+ self.wda(label='Back'),
393
+ ]
394
+ clicked = False
395
+ for btn in back_buttons:
396
+ if btn.exists:
397
+ btn.click()
398
+ clicked = True
399
+ break
400
+
401
+ if clicked:
402
+ print(f" ✅ 返回按钮点击成功", file=sys.stderr)
403
+ else:
404
+ # 如果没有返回按钮,尝试从左边缘滑动
405
+ window = self.wda.window_size()
406
+ self.wda.swipe(0, window.height // 2, window.width // 2, window.height // 2)
407
+ print(f" ✅ 边缘滑动返回成功", file=sys.stderr)
408
+ elif key_lower == 'home':
409
+ # 按Home键
410
+ self.wda.home()
411
+ print(f" ✅ 按键成功: Home", file=sys.stderr)
412
+ else:
413
+ return {"success": False, "reason": f"不支持的按键: {key}"}
414
+
415
+ return {"success": True, "key": key, "verified": False}
416
+
417
+ except Exception as e:
418
+ print(f" ❌ 按键失败: {e}", file=sys.stderr)
419
+ return {"success": False, "reason": str(e)}
420
+
421
+ async def take_screenshot(self, filename: Optional[str] = None) -> str:
422
+ """
423
+ 截图
424
+
425
+ Args:
426
+ filename: 保存的文件名(可选)
427
+
428
+ Returns:
429
+ 截图文件路径
430
+ """
431
+ self._ensure_connected()
432
+
433
+ import os
434
+ from datetime import datetime
435
+
436
+ if not filename:
437
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
438
+ filename = f"ios_screenshot_{timestamp}.png"
439
+
440
+ # 确保截图目录存在
441
+ screenshots_dir = os.path.join(os.getcwd(), 'screenshots')
442
+ os.makedirs(screenshots_dir, exist_ok=True)
443
+
444
+ filepath = os.path.join(screenshots_dir, filename)
445
+
446
+ try:
447
+ # 使用 wda 截图
448
+ self.wda.screenshot(filepath)
449
+ print(f" 📸 截图已保存: {filepath}", file=sys.stderr)
450
+ return filepath
451
+ except Exception as e:
452
+ print(f" ❌ 截图失败: {e}", file=sys.stderr)
453
+ raise
454
+
455
+ def _parse_bounds_coords(self, bounds_str: str) -> tuple:
456
+ """
457
+ 解析bounds字符串,返回中心点坐标
458
+
459
+ Args:
460
+ bounds_str: 格式如 "[100,200][300,400]"
461
+
462
+ Returns:
463
+ (x, y) 中心点坐标
464
+ """
465
+ import re
466
+ match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds_str)
467
+ if match:
468
+ x1, y1, x2, y2 = map(int, match.groups())
469
+ return ((x1 + x2) // 2, (y1 + y2) // 2)
470
+ return (0, 0)
471
+
472
+ def get_screen_size(self) -> tuple:
473
+ """获取屏幕尺寸"""
474
+ self._ensure_connected()
475
+
476
+ window = self.wda.window_size()
477
+ return (window.width, window.height)
478
+
479
+ def list_elements(self) -> List[Dict]:
480
+ """
481
+ 列出所有可交互元素(类似Android的mobile_list_elements)
482
+
483
+ Returns:
484
+ 元素列表
485
+ """
486
+ self._ensure_connected()
487
+
488
+ elements = []
489
+
490
+ try:
491
+ # 获取页面源码并解析
492
+ source = self.wda.source(format='json')
493
+
494
+ def extract_elements(node, depth=0):
495
+ """递归提取元素"""
496
+ if not isinstance(node, dict):
497
+ return
498
+
499
+ elem_type = node.get('type', '')
500
+ name = node.get('name', '')
501
+ label = node.get('label', '')
502
+ value = node.get('value', '')
503
+ rect = node.get('rect', {})
504
+ enabled = node.get('enabled', True)
505
+
506
+ # 只收集可交互的元素
507
+ interactable_types = [
508
+ 'XCUIElementTypeButton',
509
+ 'XCUIElementTypeTextField',
510
+ 'XCUIElementTypeSecureTextField',
511
+ 'XCUIElementTypeTextView',
512
+ 'XCUIElementTypeSwitch',
513
+ 'XCUIElementTypeSlider',
514
+ 'XCUIElementTypeLink',
515
+ 'XCUIElementTypeCell',
516
+ 'XCUIElementTypeStaticText',
517
+ ]
518
+
519
+ if elem_type in interactable_types and enabled:
520
+ elements.append({
521
+ 'type': elem_type,
522
+ 'name': name,
523
+ 'label': label,
524
+ 'value': value,
525
+ 'bounds': f"[{rect.get('x', 0)},{rect.get('y', 0)}][{rect.get('x', 0) + rect.get('width', 0)},{rect.get('y', 0) + rect.get('height', 0)}]",
526
+ 'enabled': enabled,
527
+ })
528
+
529
+ # 递归处理子元素
530
+ for child in node.get('children', []):
531
+ extract_elements(child, depth + 1)
532
+
533
+ extract_elements(source)
534
+
535
+ except Exception as e:
536
+ print(f" ⚠️ 获取元素列表失败: {e}", file=sys.stderr)
537
+
538
+ return elements
539
+
540
+
541
+
542
+
543
+
544
+
545
+
546
+
547
+
548
+
549
+
550
+
551
+
552
+
553
+
554
+
555
+
556
+
557
+
558
+
559
+
560
+
561
+
562
+
563
+
564
+
565
+
566
+
567
+
568
+
569
+