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.
- mobile_mcp/config.py +3 -2
- mobile_mcp/core/basic_tools_lite.py +3193 -0
- mobile_mcp/core/ios_client_wda.py +569 -0
- mobile_mcp/core/ios_device_manager_wda.py +306 -0
- mobile_mcp/core/mobile_client.py +246 -20
- mobile_mcp/core/template_matcher.py +429 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- mobile_mcp/mcp_tools/__init__.py +10 -0
- mobile_mcp/mcp_tools/mcp_server.py +992 -0
- mobile_mcp_ai-2.5.3.dist-info/METADATA +456 -0
- mobile_mcp_ai-2.5.3.dist-info/RECORD +32 -0
- mobile_mcp_ai-2.5.3.dist-info/entry_points.txt +2 -0
- mobile_mcp/core/ai/__init__.py +0 -11
- mobile_mcp/core/ai/ai_analyzer.py +0 -197
- mobile_mcp/core/ai/ai_config.py +0 -116
- mobile_mcp/core/ai/ai_platform_adapter.py +0 -399
- mobile_mcp/core/ai/smart_test_executor.py +0 -520
- mobile_mcp/core/ai/test_generator.py +0 -365
- mobile_mcp/core/ai/test_generator_from_history.py +0 -391
- mobile_mcp/core/ai/test_generator_standalone.py +0 -293
- mobile_mcp/core/assertion/__init__.py +0 -9
- mobile_mcp/core/assertion/smart_assertion.py +0 -341
- mobile_mcp/core/basic_tools.py +0 -945
- mobile_mcp/core/h5/__init__.py +0 -10
- mobile_mcp/core/h5/h5_handler.py +0 -548
- mobile_mcp/core/ios_client.py +0 -219
- mobile_mcp/core/ios_device_manager.py +0 -252
- mobile_mcp/core/locator/__init__.py +0 -10
- mobile_mcp/core/locator/cursor_ai_auto_analyzer.py +0 -119
- mobile_mcp/core/locator/cursor_vision_helper.py +0 -414
- mobile_mcp/core/locator/mobile_smart_locator.py +0 -1747
- mobile_mcp/core/locator/position_analyzer.py +0 -813
- mobile_mcp/core/locator/script_updater.py +0 -157
- mobile_mcp/core/nl_test_runner.py +0 -585
- mobile_mcp/core/smart_app_launcher.py +0 -421
- mobile_mcp/core/smart_tools.py +0 -311
- mobile_mcp/mcp/__init__.py +0 -13
- mobile_mcp/mcp/mcp_server.py +0 -1126
- mobile_mcp/mcp/mcp_server_simple.py +0 -23
- mobile_mcp/vision/__init__.py +0 -10
- mobile_mcp/vision/vision_locator.py +0 -405
- mobile_mcp_ai-2.2.6.dist-info/METADATA +0 -503
- mobile_mcp_ai-2.2.6.dist-info/RECORD +0 -49
- mobile_mcp_ai-2.2.6.dist-info/entry_points.txt +0 -2
- {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
|
mobile_mcp/core/mobile_client.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
if not lazy_connect:
|
|
61
|
+
self.smart_wait = SmartWait(self)
|
|
62
|
+
else:
|
|
63
|
+
self.smart_wait = None
|
|
57
64
|
elif platform == "ios":
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
self.
|
|
61
|
-
self.
|
|
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
|
-
# 🎯
|
|
360
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
923
|
+
包名/Bundle ID或None
|
|
832
924
|
"""
|
|
833
925
|
try:
|
|
834
|
-
|
|
835
|
-
|
|
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
|
|