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
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ iOS设备连接管理 - 使用 tidevice + facebook-wda
5
+
6
+ 优势:
7
+ 1. API风格和 uiautomator2 几乎一样
8
+ 2. 不需要启动 Appium Server
9
+ 3. tidevice 简化设备管理
10
+
11
+ 前置条件:
12
+ 1. 安装 tidevice: pip install tidevice
13
+ 2. 安装 facebook-wda: pip install facebook-wda
14
+ 3. 首次需要用 Xcode 编译 WebDriverAgent 到设备上
15
+
16
+ 用法:
17
+ manager = IOSDeviceManagerWDA()
18
+ devices = manager.list_devices()
19
+ client = manager.connect(device_id="xxx")
20
+ client(text="登录").click() # 和 uiautomator2 风格一样!
21
+ """
22
+ import sys
23
+ import subprocess
24
+ from typing import List, Optional, Dict
25
+
26
+
27
+ class IOSDeviceManagerWDA:
28
+ """
29
+ iOS设备管理器 - 使用 tidevice + facebook-wda
30
+
31
+ 用法:
32
+ manager = IOSDeviceManagerWDA()
33
+ devices = manager.list_devices()
34
+ client = manager.connect()
35
+ client(text="登录").click()
36
+ """
37
+
38
+ def __init__(self):
39
+ """初始化iOS设备管理器"""
40
+ self.client = None
41
+ self.current_device_id = None
42
+ self._wda_proxy_process = None
43
+ self._check_dependencies()
44
+
45
+ def _check_dependencies(self):
46
+ """检查依赖是否安装"""
47
+ try:
48
+ import tidevice
49
+ import wda
50
+ except ImportError as e:
51
+ raise ImportError(
52
+ f"缺少iOS自动化依赖: {e}\n"
53
+ f"请运行以下命令安装:\n"
54
+ f" pip install tidevice facebook-wda\n"
55
+ )
56
+
57
+ def list_devices(self) -> List[Dict[str, str]]:
58
+ """
59
+ 列出所有连接的iOS设备
60
+
61
+ Returns:
62
+ 设备列表,每个设备包含 id, name, type 等信息
63
+ """
64
+ devices = []
65
+
66
+ try:
67
+ # 优先使用 tidevice Python API
68
+ try:
69
+ import tidevice
70
+ for d in tidevice.Usbmux().device_list():
71
+ devices.append({
72
+ 'id': d.udid,
73
+ 'name': d.name if hasattr(d, 'name') else 'iOS Device',
74
+ 'type': 'device',
75
+ 'state': 'connected'
76
+ })
77
+ if devices:
78
+ return devices
79
+ except Exception:
80
+ pass
81
+
82
+ # 回退:使用 subprocess 调用 tidevice
83
+ result = subprocess.run(
84
+ [sys.executable, '-m', 'tidevice', 'list', '--json'],
85
+ capture_output=True,
86
+ text=True,
87
+ timeout=10
88
+ )
89
+
90
+ if result.returncode == 0 and result.stdout.strip():
91
+ import json
92
+ try:
93
+ device_list = json.loads(result.stdout)
94
+ for device in device_list:
95
+ devices.append({
96
+ 'id': device.get('udid', ''),
97
+ 'name': device.get('name', 'iOS Device'),
98
+ 'type': 'device',
99
+ 'model': device.get('model', 'Unknown'),
100
+ 'ios_version': device.get('version', 'Unknown'),
101
+ 'state': 'connected'
102
+ })
103
+ except json.JSONDecodeError:
104
+ # 尝试纯文本解析
105
+ for line in result.stdout.strip().split('\n'):
106
+ if line.strip():
107
+ parts = line.split()
108
+ if len(parts) >= 1:
109
+ devices.append({
110
+ 'id': parts[0],
111
+ 'name': ' '.join(parts[1:]) if len(parts) > 1 else 'iOS Device',
112
+ 'type': 'device',
113
+ 'state': 'connected'
114
+ })
115
+
116
+ # 如果 tidevice 没有找到设备,尝试使用 xcrun simctl 列出模拟器
117
+ if not devices:
118
+ sim_result = subprocess.run(
119
+ ['xcrun', 'simctl', 'list', 'devices', 'booted', '--json'],
120
+ capture_output=True,
121
+ text=True,
122
+ timeout=10
123
+ )
124
+
125
+ if sim_result.returncode == 0 and sim_result.stdout.strip():
126
+ import json
127
+ sim_data = json.loads(sim_result.stdout)
128
+ for runtime, sims in sim_data.get('devices', {}).items():
129
+ for sim in sims:
130
+ if sim.get('state') == 'Booted':
131
+ devices.append({
132
+ 'id': sim.get('udid', ''),
133
+ 'name': sim.get('name', 'Simulator'),
134
+ 'type': 'simulator',
135
+ 'runtime': runtime,
136
+ 'state': 'Booted'
137
+ })
138
+
139
+ return devices
140
+
141
+ except FileNotFoundError:
142
+ print("⚠️ tidevice 未安装,请运行: pip install tidevice", file=sys.stderr)
143
+ return []
144
+ except Exception as e:
145
+ print(f"⚠️ 获取设备列表失败: {e}", file=sys.stderr)
146
+ return []
147
+
148
+ def start_wda_proxy(self, device_id: str, port: int = 8100) -> bool:
149
+ """
150
+ 启动 WDA 代理(如果尚未启动)
151
+
152
+ Args:
153
+ device_id: 设备UDID
154
+ port: WDA代理端口,默认8100
155
+
156
+ Returns:
157
+ 是否成功启动
158
+ """
159
+ try:
160
+ import socket
161
+
162
+ # 检查端口是否已被占用(可能WDA已在运行)
163
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
164
+ result = sock.connect_ex(('localhost', port))
165
+ sock.close()
166
+
167
+ if result == 0:
168
+ print(f" ✅ WDA代理已在运行 (端口 {port})", file=sys.stderr)
169
+ return True
170
+
171
+ # 启动 WDA 代理
172
+ print(f" 🚀 启动 WDA 代理...", file=sys.stderr)
173
+
174
+ # 使用 tidevice 启动 WDA(后台运行)
175
+ self._wda_proxy_process = subprocess.Popen(
176
+ [sys.executable, '-m', 'tidevice', '-u', device_id, 'wdaproxy', '-B',
177
+ 'com.facebook.WebDriverAgentRunner.xctrunner', '--port', str(port)],
178
+ stdout=subprocess.DEVNULL,
179
+ stderr=subprocess.DEVNULL
180
+ )
181
+
182
+ # 等待 WDA 启动
183
+ import time
184
+ for i in range(10): # 最多等待10秒
185
+ time.sleep(1)
186
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
187
+ result = sock.connect_ex(('localhost', port))
188
+ sock.close()
189
+
190
+ if result == 0:
191
+ print(f" ✅ WDA代理启动成功 (端口 {port})", file=sys.stderr)
192
+ return True
193
+
194
+ print(f" ⏳ 等待WDA启动... ({i+1}/10)", file=sys.stderr)
195
+
196
+ print(f" ❌ WDA代理启动超时", file=sys.stderr)
197
+ return False
198
+
199
+ except Exception as e:
200
+ print(f" ❌ 启动WDA代理失败: {e}", file=sys.stderr)
201
+ return False
202
+
203
+ def connect(self, device_id: Optional[str] = None, port: int = 8100) -> 'wda.Client':
204
+ """
205
+ 连接iOS设备
206
+
207
+ Args:
208
+ device_id: 设备UDID,None则自动选择第一个设备
209
+ port: WDA代理端口,默认8100
210
+
211
+ Returns:
212
+ wda.Client 对象(API类似 uiautomator2)
213
+ """
214
+ try:
215
+ import wda
216
+
217
+ # 如果没有指定设备ID,自动选择第一个
218
+ if device_id is None:
219
+ devices = self.list_devices()
220
+ if not devices:
221
+ raise RuntimeError(
222
+ "未找到连接的iOS设备\n"
223
+ "请确保:\n"
224
+ "1. iOS设备已通过USB连接\n"
225
+ "2. 设备已信任此电脑\n"
226
+ "3. tidevice已安装: pip install tidevice"
227
+ )
228
+ device_id = devices[0]['id']
229
+ print(f" 📱 自动选择设备: {device_id}", file=sys.stderr)
230
+
231
+ self.current_device_id = device_id
232
+
233
+ # 尝试启动 WDA 代理
234
+ self.start_wda_proxy(device_id, port)
235
+
236
+ # 连接到 WDA
237
+ self.client = wda.Client(f'http://localhost:{port}')
238
+
239
+ # 测试连接
240
+ try:
241
+ status = self.client.status()
242
+ print(f" ✅ iOS设备连接成功: {device_id}", file=sys.stderr)
243
+ print(f" iOS版本: {status.get('os', {}).get('version', 'Unknown')}", file=sys.stderr)
244
+ except Exception as e:
245
+ print(f" ⚠️ 连接可能不稳定: {e}", file=sys.stderr)
246
+
247
+ return self.client
248
+
249
+ except ImportError:
250
+ raise ImportError(
251
+ "facebook-wda 未安装\n"
252
+ "请运行: pip install facebook-wda"
253
+ )
254
+ except Exception as e:
255
+ error_msg = str(e)
256
+ if "Connection refused" in error_msg:
257
+ raise RuntimeError(
258
+ f"无法连接到WDA (端口 {port})\n"
259
+ f"请确保:\n"
260
+ f"1. WebDriverAgent 已安装到设备上(需要用Xcode首次编译)\n"
261
+ f"2. 运行: tidevice -u {device_id} wdaproxy -B com.facebook.WebDriverAgentRunner.xctrunner\n"
262
+ f"3. 或者检查端口 {port} 是否被占用"
263
+ )
264
+ raise RuntimeError(f"连接iOS设备失败: {e}")
265
+
266
+ def check_device_status(self) -> Dict:
267
+ """
268
+ 检查设备连接状态
269
+
270
+ Returns:
271
+ 设备状态信息
272
+ """
273
+ if not self.client:
274
+ return {'connected': False, 'reason': '设备未连接'}
275
+
276
+ try:
277
+ status = self.client.status()
278
+ return {
279
+ 'connected': True,
280
+ 'device_id': self.current_device_id,
281
+ 'ios_version': status.get('os', {}).get('version', 'Unknown'),
282
+ 'wda_version': status.get('build', {}).get('productBundleIdentifier', 'Unknown'),
283
+ }
284
+ except Exception as e:
285
+ return {
286
+ 'connected': False,
287
+ 'reason': str(e)
288
+ }
289
+
290
+ def disconnect(self):
291
+ """断开设备连接"""
292
+ if self._wda_proxy_process:
293
+ try:
294
+ self._wda_proxy_process.terminate()
295
+ self._wda_proxy_process.wait(timeout=5)
296
+ except:
297
+ pass
298
+ self._wda_proxy_process = None
299
+
300
+ self.client = None
301
+ self.current_device_id = None
302
+ print(" 📱 iOS设备已断开连接", file=sys.stderr)
303
+
304
+
305
+
306
+
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- 移动端客户端 - 类似Web端的MCPClient
4
+
5
5
 
6
6
  功能:
7
7
  1. 设备连接管理
@@ -28,15 +28,13 @@ from .dynamic_config import DynamicConfig
28
28
 
29
29
  class MobileClient:
30
30
  """
31
- 移动端客户端 - 类似Web端的MCPClient
32
-
33
31
  用法:
34
32
  client = MobileClient(device_id=None, platform="android")
35
33
  await client.launch_app("com.example.app")
36
34
  await client.click("登录按钮")
37
35
  """
38
36
 
39
- def __init__(self, device_id: Optional[str] = None, platform: str = "android", lock_orientation: bool = True):
37
+ def __init__(self, device_id: Optional[str] = None, platform: str = "android", lock_orientation: bool = True, lazy_connect: bool = False):
40
38
  """
41
39
  初始化移动端客户端
42
40
 
@@ -44,21 +42,33 @@ class MobileClient:
44
42
  device_id: 设备ID,None则自动选择第一个设备
45
43
  platform: 平台类型 ("android" 或 "ios")
46
44
  lock_orientation: 是否锁定屏幕方向为竖屏(默认True,仅Android有效)
45
+ lazy_connect: 是否延迟连接(默认False)。如果为True,则不立即连接设备
47
46
  """
48
47
  self.platform = platform
48
+ self._device_id = device_id
49
+ self._lazy_connect = lazy_connect
49
50
 
50
51
  if platform == "android":
51
52
  self.device_manager = DeviceManager(platform="android")
52
- self.u2 = self.device_manager.connect(device_id)
53
+ if not lazy_connect:
54
+ self.u2 = self.device_manager.connect(device_id)
55
+ else:
56
+ self.u2 = None
53
57
  self.driver = None # iOS使用
54
58
 
55
59
  # 初始化智能等待工具
56
- self.smart_wait = SmartWait(self)
60
+ if not lazy_connect:
61
+ self.smart_wait = SmartWait(self)
62
+ else:
63
+ self.smart_wait = None
57
64
  elif platform == "ios":
58
- from .ios_device_manager import IOSDeviceManager
59
- self.device_manager = IOSDeviceManager()
60
- self.driver = self.device_manager.connect(device_id)
61
- self.u2 = None # Android使用
65
+ # 🍎 iOS 支持:使用 tidevice + facebook-wda
66
+ from .ios_client_wda import IOSClientWDA
67
+ self._ios_client = IOSClientWDA(device_id=device_id, lazy_connect=lazy_connect)
68
+ self.device_manager = self._ios_client.device_manager
69
+ self.wda = self._ios_client.wda if not lazy_connect else None
70
+ self.driver = None
71
+ self.u2 = None
62
72
  else:
63
73
  raise ValueError(f"不支持的平台: {platform}")
64
74
 
@@ -165,6 +175,20 @@ class MobileClient:
165
175
  if current_time - self._cache_timestamp < self._cache_ttl:
166
176
  return self._snapshot_cache
167
177
 
178
+ # iOS平台使用不同的实现
179
+ if self.platform == "ios":
180
+ if not self.driver:
181
+ raise RuntimeError("iOS设备未连接")
182
+ # 获取iOS页面源码
183
+ xml_string = self.driver.page_source
184
+ if not isinstance(xml_string, str):
185
+ xml_string = str(xml_string)
186
+ # iOS的XML格式可能不同,直接返回或简单格式化
187
+ self._snapshot_cache = xml_string
188
+ self._cache_timestamp = time.time()
189
+ return xml_string
190
+
191
+ # Android平台
168
192
  # 获取XML
169
193
  xml_string = self.u2.dump_hierarchy()
170
194
 
@@ -356,10 +380,12 @@ class MobileClient:
356
380
  break
357
381
 
358
382
  if not found:
359
- # 🎯 定位失败,自动使用Cursor AI视觉识别(截图分析)
360
- print(f" ⚠️ 元素'{ref}'未找到,自动使用Cursor AI视觉识别(截图分析)...", file=sys.stderr)
383
+ # 🎯 定位失败,提示用户
384
+ # 注意:CursorVisionHelper 是实验性功能,当前版本建议使用 MCP 方式
385
+ print(f" ⚠️ 元素'{ref}'未找到", file=sys.stderr)
361
386
  try:
362
387
  from .locator.cursor_vision_helper import CursorVisionHelper
388
+ print(f" 🔍 尝试使用Cursor AI视觉识别...", file=sys.stderr)
363
389
  cursor_helper = CursorVisionHelper(self)
364
390
  # 🎯 传递 auto_analyze=True,自动创建请求文件并等待结果
365
391
  cursor_result = await cursor_helper.analyze_with_cursor(element, auto_analyze=True)
@@ -391,12 +417,17 @@ class MobileClient:
391
417
  # 其他情况,抛出异常
392
418
  screenshot_path = cursor_result.get('screenshot_path', 'unknown') if cursor_result else 'unknown'
393
419
  raise ValueError(f"Cursor AI分析失败: {screenshot_path}")
420
+ except ImportError:
421
+ # CursorVisionHelper 模块不存在,跳过视觉识别
422
+ print(f" 💡 提示:建议使用 MCP 方式调用,Cursor AI 会自动进行视觉识别", file=sys.stderr)
394
423
  except ValueError as ve:
395
424
  if "Cursor AI" in str(ve):
396
425
  raise ve
397
426
  print(f" ⚠️ Cursor视觉识别失败: {ve}", file=sys.stderr)
427
+ except Exception as e:
428
+ print(f" ⚠️ 视觉识别异常: {e}", file=sys.stderr)
398
429
 
399
- raise ValueError(f"无法找到元素: {ref}(已等待3秒,并尝试Cursor视觉识别,可能元素不存在)")
430
+ raise ValueError(f"无法找到元素: {ref}(建议使用 MCP 方式,Cursor AI 会自动进行视觉识别)")
400
431
 
401
432
  # 验证点击(可选)
402
433
  page_changed = False
@@ -676,6 +707,31 @@ class MobileClient:
676
707
  - verified: 是否经过验证
677
708
  - page_changed: 页面是否变化(仅 verify=True)
678
709
  """
710
+ # iOS平台使用不同的实现
711
+ if self.platform == "ios":
712
+ if not self.driver:
713
+ return {"success": False, "reason": "iOS设备未连接"}
714
+ try:
715
+ size = self.driver.get_window_size()
716
+ width = size['width']
717
+ height = size['height']
718
+
719
+ if direction == 'up':
720
+ self.driver.swipe(width // 2, int(height * 0.8), width // 2, int(height * 0.2))
721
+ elif direction == 'down':
722
+ self.driver.swipe(width // 2, int(height * 0.2), width // 2, int(height * 0.8))
723
+ elif direction == 'left':
724
+ self.driver.swipe(int(width * 0.8), height // 2, int(width * 0.2), height // 2)
725
+ elif direction == 'right':
726
+ self.driver.swipe(int(width * 0.2), height // 2, int(width * 0.8), height // 2)
727
+ else:
728
+ return {"success": False, "reason": f"不支持的滑动方向: {direction}"}
729
+
730
+ return {"success": True, "direction": direction}
731
+ except Exception as e:
732
+ return {"success": False, "reason": str(e)}
733
+
734
+ # Android平台
679
735
  # 获取屏幕尺寸
680
736
  width, height = self.u2.window_size()
681
737
 
@@ -739,14 +795,36 @@ class MobileClient:
739
795
  启动App(快速模式:最多等待3秒+截图验证)
740
796
 
741
797
  Args:
742
- package_name: App包名(如 "com.example.app"
798
+ package_name: App包名(Android)或Bundle ID(iOS),如 "com.example.app"
743
799
  wait_time: 等待App启动的时间(秒)- 默认3秒
744
- smart_wait: 是否启用智能等待(自动关闭广告、截图验证)
800
+ smart_wait: 是否启用智能等待(自动关闭广告、截图验证)- 仅Android
745
801
 
746
802
  Returns:
747
803
  操作结果(包含screenshot_path字段供AI验证)
748
804
  """
749
805
  try:
806
+ # iOS平台使用不同的实现
807
+ if self.platform == "ios":
808
+ if not self.driver:
809
+ return {"success": False, "reason": "iOS设备未连接"}
810
+ try:
811
+ print(f" 📱 启动iOS App: {package_name}", file=sys.stderr)
812
+ self.driver.activate_app(package_name)
813
+ await asyncio.sleep(wait_time)
814
+
815
+ # 验证是否启动成功
816
+ current = await self.get_current_package()
817
+ if current == package_name:
818
+ print(f" ✅ iOS App启动成功: {package_name}", file=sys.stderr)
819
+ return {"success": True, "package": package_name}
820
+ else:
821
+ print(f" ⚠️ iOS App可能未启动成功,当前App: {current},期望: {package_name}", file=sys.stderr)
822
+ return {"success": True, "package": package_name, "warning": f"当前App: {current}"}
823
+ except Exception as e:
824
+ print(f" ❌ iOS App启动异常: {e}", file=sys.stderr)
825
+ return {"success": False, "reason": str(e)}
826
+
827
+ # Android平台
750
828
  # 🎯 优先使用智能启动(推荐)
751
829
  if smart_wait:
752
830
  from .smart_app_launcher import SmartAppLauncher
@@ -809,13 +887,27 @@ class MobileClient:
809
887
  停止App
810
888
 
811
889
  Args:
812
- package_name: App包名
890
+ package_name: App包名(Android)或Bundle ID(iOS)
813
891
 
814
892
  Returns:
815
893
  操作结果
816
894
  """
817
895
  try:
818
896
  print(f" 📱 停止App: {package_name}", file=sys.stderr)
897
+
898
+ # iOS平台使用不同的实现
899
+ if self.platform == "ios":
900
+ if not self.driver:
901
+ return {"success": False, "reason": "iOS设备未连接"}
902
+ try:
903
+ self.driver.terminate_app(package_name)
904
+ print(f" ✅ iOS App已停止: {package_name}", file=sys.stderr)
905
+ return {"success": True}
906
+ except Exception as e:
907
+ print(f" ❌ iOS App停止失败: {e}", file=sys.stderr)
908
+ return {"success": False, "reason": str(e)}
909
+
910
+ # Android平台
819
911
  self.u2.app_stop(package_name)
820
912
  print(f" ✅ App已停止: {package_name}", file=sys.stderr)
821
913
  return {"success": True}
@@ -825,14 +917,19 @@ class MobileClient:
825
917
 
826
918
  async def get_current_package(self) -> Optional[str]:
827
919
  """
828
- 获取当前App包名
920
+ 获取当前App包名(Android)或Bundle ID(iOS)
829
921
 
830
922
  Returns:
831
- 包名或None
923
+ 包名/Bundle ID或None
832
924
  """
833
925
  try:
834
- info = self.u2.app_current()
835
- return info.get('package')
926
+ if self.platform == "ios":
927
+ if not self.driver:
928
+ return None
929
+ return self.driver.current_package
930
+ else:
931
+ info = self.u2.app_current()
932
+ return info.get('package')
836
933
  except:
837
934
  return None
838
935
 
@@ -860,6 +957,34 @@ class MobileClient:
860
957
  - page_changed: 页面是否变化(仅 verify=True 时)
861
958
  - fallback_used: 是否使用了备选方案(仅搜索键)
862
959
  """
960
+ # iOS平台使用不同的实现
961
+ if self.platform == "ios":
962
+ if not self.driver:
963
+ return {"success": False, "reason": "iOS设备未连接"}
964
+ try:
965
+ # iOS按键映射(使用XCUITest的按键)
966
+ ios_key_map = {
967
+ 'enter': 'return',
968
+ '回车': 'return',
969
+ 'back': 'back',
970
+ '返回': 'back',
971
+ 'home': 'home',
972
+ }
973
+
974
+ key_lower = key.lower()
975
+ if key_lower in ios_key_map:
976
+ ios_key = ios_key_map[key_lower]
977
+ # iOS使用execute_script发送按键
978
+ self.driver.execute_script("mobile: pressButton", {"name": ios_key})
979
+ print(f" ✅ iOS按键成功: {key} ({ios_key})", file=sys.stderr)
980
+ return {"success": True, "key": key, "verified": False}
981
+ else:
982
+ return {"success": False, "reason": f"iOS不支持的按键: {key}"}
983
+ except Exception as e:
984
+ print(f" ❌ iOS按键失败: {e}", file=sys.stderr)
985
+ return {"success": False, "reason": str(e)}
986
+
987
+ # Android平台
863
988
  key_map = {
864
989
  'enter': 66, # KEYCODE_ENTER
865
990
  '回车': 66,
@@ -1094,4 +1219,105 @@ class MobileClient:
1094
1219
  x1, y1, x2, y2 = map(int, match.groups())
1095
1220
  return ((x1 + x2) // 2, (y1 + y2) // 2)
1096
1221
  return (0, 0)
1222
+
1223
+ async def _ios_click(self, element: str, ref: Optional[str] = None):
1224
+ """
1225
+ iOS平台的点击实现
1226
+
1227
+ Args:
1228
+ element: 元素描述
1229
+ ref: 元素定位器
1230
+
1231
+ Returns:
1232
+ 操作结果
1233
+ """
1234
+ try:
1235
+ from selenium.webdriver.common.by import By
1236
+
1237
+ # 如果提供了ref,直接使用
1238
+ if ref:
1239
+ if ref.startswith('//') or ref.startswith('/'):
1240
+ # XPath
1241
+ elem = self.driver.find_element(By.XPATH, ref)
1242
+ elif ref.startswith('id='):
1243
+ # accessibility_id
1244
+ elem = self.driver.find_element(By.ID, ref.replace('id=', ''))
1245
+ else:
1246
+ # 默认作为accessibility_id
1247
+ elem = self.driver.find_element(By.ID, ref)
1248
+ else:
1249
+ # 尝试多种定位方式
1250
+ selectors = [
1251
+ (By.XPATH, f"//*[@name='{element}']"),
1252
+ (By.XPATH, f"//*[@label='{element}']"),
1253
+ (By.XPATH, f"//*[contains(@name, '{element}')]"),
1254
+ ]
1255
+
1256
+ elem = None
1257
+ for by, selector in selectors:
1258
+ try:
1259
+ elem = self.driver.find_element(by, selector)
1260
+ break
1261
+ except:
1262
+ continue
1263
+
1264
+ if not elem:
1265
+ raise ValueError(f"未找到元素: {element}")
1266
+
1267
+ elem.click()
1268
+
1269
+ # 记录操作
1270
+ self.operation_history.append({
1271
+ 'action': 'click',
1272
+ 'element': element,
1273
+ 'ref': ref or 'auto',
1274
+ 'success': True
1275
+ })
1276
+
1277
+ return {"success": True, "ref": ref or element}
1278
+
1279
+ except Exception as e:
1280
+ return {"success": False, "reason": str(e)}
1281
+
1282
+ async def _ios_type_text(self, element: str, text: str, ref: Optional[str] = None):
1283
+ """
1284
+ iOS平台的输入文本实现
1285
+
1286
+ Args:
1287
+ element: 元素描述
1288
+ text: 要输入的文本
1289
+ ref: 元素定位器
1290
+
1291
+ Returns:
1292
+ 操作结果
1293
+ """
1294
+ try:
1295
+ from selenium.webdriver.common.by import By
1296
+
1297
+ # 定位输入框
1298
+ if ref:
1299
+ if ref.startswith('//'):
1300
+ elem = self.driver.find_element(By.XPATH, ref)
1301
+ else:
1302
+ elem = self.driver.find_element(By.ID, ref)
1303
+ else:
1304
+ # 查找第一个输入框
1305
+ elem = self.driver.find_element(By.XPATH, "//XCUIElementTypeTextField | //XCUIElementTypeSecureTextField")
1306
+
1307
+ elem.clear()
1308
+ elem.send_keys(text)
1309
+
1310
+ # 记录操作
1311
+ self.operation_history.append({
1312
+ 'action': 'type',
1313
+ 'element': element,
1314
+ 'text': text,
1315
+ 'ref': ref or 'auto',
1316
+ 'success': True
1317
+ })
1318
+
1319
+ return {"success": True, "ref": ref or element}
1320
+
1321
+ except Exception as e:
1322
+ return {"success": False, "reason": str(e)}
1097
1323