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.
- mobile_mcp/__init__.py +34 -0
- mobile_mcp/config.py +142 -0
- mobile_mcp/core/basic_tools_lite.py +3266 -0
- {core → mobile_mcp/core}/device_manager.py +2 -2
- mobile_mcp/core/dynamic_config.py +272 -0
- mobile_mcp/core/ios_client_wda.py +569 -0
- mobile_mcp/core/ios_device_manager_wda.py +306 -0
- {core → mobile_mcp/core}/mobile_client.py +279 -39
- 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
- {core → mobile_mcp/core}/utils/smart_wait.py +3 -3
- mobile_mcp/mcp_tools/__init__.py +10 -0
- mobile_mcp/mcp_tools/mcp_server.py +1071 -0
- mobile_mcp_ai-2.5.8.dist-info/METADATA +469 -0
- mobile_mcp_ai-2.5.8.dist-info/RECORD +32 -0
- mobile_mcp_ai-2.5.8.dist-info/entry_points.txt +2 -0
- mobile_mcp_ai-2.5.8.dist-info/licenses/LICENSE +201 -0
- mobile_mcp_ai-2.5.8.dist-info/top_level.txt +1 -0
- core/ai/__init__.py +0 -11
- core/ai/ai_analyzer.py +0 -197
- core/ai/ai_config.py +0 -116
- core/ai/ai_platform_adapter.py +0 -399
- core/ai/smart_test_executor.py +0 -520
- core/ai/test_generator.py +0 -365
- core/ai/test_generator_from_history.py +0 -391
- core/ai/test_generator_standalone.py +0 -293
- core/assertion/__init__.py +0 -9
- core/assertion/smart_assertion.py +0 -341
- core/basic_tools.py +0 -377
- core/h5/__init__.py +0 -10
- core/h5/h5_handler.py +0 -548
- core/ios_client.py +0 -219
- core/ios_device_manager.py +0 -252
- core/locator/__init__.py +0 -10
- core/locator/cursor_ai_auto_analyzer.py +0 -119
- core/locator/cursor_vision_helper.py +0 -414
- core/locator/mobile_smart_locator.py +0 -1640
- core/locator/position_analyzer.py +0 -813
- core/locator/script_updater.py +0 -157
- core/nl_test_runner.py +0 -585
- core/smart_app_launcher.py +0 -334
- core/smart_tools.py +0 -311
- mcp/__init__.py +0 -8
- mcp/mcp_server.py +0 -1919
- mcp/mcp_server_simple.py +0 -476
- mobile_mcp_ai-2.1.2.dist-info/METADATA +0 -567
- mobile_mcp_ai-2.1.2.dist-info/RECORD +0 -45
- mobile_mcp_ai-2.1.2.dist-info/entry_points.txt +0 -2
- mobile_mcp_ai-2.1.2.dist-info/top_level.txt +0 -4
- vision/__init__.py +0 -10
- vision/vision_locator.py +0 -404
- {core → mobile_mcp/core}/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/logger.py +0 -0
- {core → mobile_mcp/core}/utils/operation_history_manager.py +0 -0
- {utils → mobile_mcp/utils}/__init__.py +0 -0
- {utils → mobile_mcp/utils}/logger.py +0 -0
- {utils → mobile_mcp/utils}/xml_formatter.py +0 -0
- {utils → mobile_mcp/utils}/xml_parser.py +0 -0
- {mobile_mcp_ai-2.1.2.dist-info → mobile_mcp_ai-2.5.8.dist-info}/WHEEL +0 -0
core/smart_app_launcher.py
DELETED
|
@@ -1,334 +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
|
-
|
|
11
|
-
class SmartAppLauncher:
|
|
12
|
-
"""
|
|
13
|
-
智能App启动器
|
|
14
|
-
|
|
15
|
-
功能:
|
|
16
|
-
1. 启动App后智能等待主页加载
|
|
17
|
-
2. 自动检测并关闭广告/弹窗
|
|
18
|
-
3. 等待网络加载完成
|
|
19
|
-
4. 智能判断是否进入主页
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def __init__(self, mobile_client):
|
|
23
|
-
"""
|
|
24
|
-
初始化智能启动器
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
mobile_client: MobileClient实例
|
|
28
|
-
"""
|
|
29
|
-
self.client = mobile_client
|
|
30
|
-
|
|
31
|
-
# 常见的广告/弹窗关闭按钮特征
|
|
32
|
-
self.ad_close_keywords = [
|
|
33
|
-
'跳过', '关闭', '×', 'X', 'x', '✕',
|
|
34
|
-
'skip', 'close', '稍后', '取消',
|
|
35
|
-
'我知道了', '不再提示', '下次再说',
|
|
36
|
-
'暂不', '以后再说', '返回'
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
# 常见的弹窗容器特征
|
|
40
|
-
self.popup_keywords = [
|
|
41
|
-
'dialog', 'popup', 'alert', 'modal',
|
|
42
|
-
'弹窗', '对话框', '提示'
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
async def launch_with_smart_wait(
|
|
46
|
-
self,
|
|
47
|
-
package_name: str,
|
|
48
|
-
max_wait: int = 5, # 优化:从10秒减少到5秒
|
|
49
|
-
auto_close_ads: bool = True
|
|
50
|
-
) -> Dict:
|
|
51
|
-
"""
|
|
52
|
-
智能启动App并等待主页加载
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
package_name: App包名
|
|
56
|
-
max_wait: 最大等待时间(秒,默认5秒)
|
|
57
|
-
auto_close_ads: 是否自动关闭广告/弹窗
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
启动结果
|
|
61
|
-
"""
|
|
62
|
-
print(f"\n🚀 智能启动App: {package_name}", file=sys.stderr)
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
# 🎯 启动前:强制恢复竖屏(防止上次横屏残留)
|
|
66
|
-
print(f" 🔄 检查屏幕方向...", file=sys.stderr)
|
|
67
|
-
self.client.force_portrait()
|
|
68
|
-
|
|
69
|
-
# 1. 启动App
|
|
70
|
-
print(f" 📱 正在启动...", file=sys.stderr)
|
|
71
|
-
self.client.u2.app_start(package_name)
|
|
72
|
-
await asyncio.sleep(1) # 等待App进程启动
|
|
73
|
-
|
|
74
|
-
# 🎯 启动后:再次强制竖屏(防止App启动时强制横屏)
|
|
75
|
-
self.client.force_portrait()
|
|
76
|
-
|
|
77
|
-
# 2. 验证App是否启动
|
|
78
|
-
current_package = await self._get_current_package()
|
|
79
|
-
if current_package != package_name:
|
|
80
|
-
return {
|
|
81
|
-
"success": False,
|
|
82
|
-
"reason": f"App启动失败,当前: {current_package},期望: {package_name}"
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
print(f" ✅ App进程已启动", file=sys.stderr)
|
|
86
|
-
|
|
87
|
-
# 3. 智能等待主页加载(检测广告、弹窗、加载状态)
|
|
88
|
-
result = await self._wait_for_home_page(
|
|
89
|
-
package_name,
|
|
90
|
-
max_wait=max_wait,
|
|
91
|
-
auto_close_ads=auto_close_ads
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
if result['loaded']:
|
|
95
|
-
print(f" ✅ 主页加载完成!", file=sys.stderr)
|
|
96
|
-
return {
|
|
97
|
-
"success": True,
|
|
98
|
-
"package": package_name,
|
|
99
|
-
"wait_time": result['wait_time'],
|
|
100
|
-
"ads_closed": result['ads_closed'],
|
|
101
|
-
"popups_closed": result['popups_closed']
|
|
102
|
-
}
|
|
103
|
-
else:
|
|
104
|
-
print(f" ⚠️ 等待超时,但App已启动", file=sys.stderr)
|
|
105
|
-
return {
|
|
106
|
-
"success": True,
|
|
107
|
-
"package": package_name,
|
|
108
|
-
"warning": "主页加载超时,但App已启动",
|
|
109
|
-
"wait_time": result['wait_time'],
|
|
110
|
-
"ads_closed": result['ads_closed'],
|
|
111
|
-
"popups_closed": result['popups_closed']
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
except Exception as e:
|
|
115
|
-
print(f" ❌ 智能启动失败: {e}", file=sys.stderr)
|
|
116
|
-
return {
|
|
117
|
-
"success": False,
|
|
118
|
-
"reason": str(e)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async def _wait_for_home_page(
|
|
122
|
-
self,
|
|
123
|
-
package_name: str,
|
|
124
|
-
max_wait: int = 5, # 优化:从10秒减少到5秒
|
|
125
|
-
auto_close_ads: bool = True
|
|
126
|
-
) -> Dict:
|
|
127
|
-
"""
|
|
128
|
-
等待主页加载完成
|
|
129
|
-
|
|
130
|
-
策略:
|
|
131
|
-
1. 每0.5秒检查一次页面状态
|
|
132
|
-
2. 检测广告/弹窗并自动关闭
|
|
133
|
-
3. 检测页面是否稳定(元素不再变化)
|
|
134
|
-
4. 超时后返回当前状态
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
{
|
|
138
|
-
"loaded": bool, # 是否加载完成
|
|
139
|
-
"wait_time": float, # 等待时间
|
|
140
|
-
"ads_closed": int, # 关闭的广告数
|
|
141
|
-
"popups_closed": int # 关闭的弹窗数
|
|
142
|
-
}
|
|
143
|
-
"""
|
|
144
|
-
import time
|
|
145
|
-
start_time = time.time()
|
|
146
|
-
|
|
147
|
-
ads_closed = 0
|
|
148
|
-
popups_closed = 0
|
|
149
|
-
last_snapshot = None
|
|
150
|
-
stable_count = 0 # 页面稳定计数(连续2次快照相同认为稳定)
|
|
151
|
-
|
|
152
|
-
print(f" ⏳ 等待主页加载(最多{max_wait}秒)...", file=sys.stderr)
|
|
153
|
-
|
|
154
|
-
check_interval = 0.3 # 优化:每0.3秒检查一次(更快响应)
|
|
155
|
-
max_checks = int(max_wait / check_interval)
|
|
156
|
-
|
|
157
|
-
for i in range(max_checks):
|
|
158
|
-
await asyncio.sleep(check_interval)
|
|
159
|
-
elapsed = time.time() - start_time
|
|
160
|
-
|
|
161
|
-
# 检查当前包名(防止跳转到其他App)
|
|
162
|
-
current_package = await self._get_current_package()
|
|
163
|
-
if current_package != package_name:
|
|
164
|
-
print(f" ⚠️ 检测到包名变化: {package_name} -> {current_package}", file=sys.stderr)
|
|
165
|
-
# 可能跳转到其他页面(如授权页),继续等待
|
|
166
|
-
await asyncio.sleep(1)
|
|
167
|
-
continue
|
|
168
|
-
|
|
169
|
-
# 获取页面快照
|
|
170
|
-
try:
|
|
171
|
-
snapshot = self.client.u2.dump_hierarchy()
|
|
172
|
-
|
|
173
|
-
# 1. 检测并关闭广告/弹窗
|
|
174
|
-
if auto_close_ads:
|
|
175
|
-
closed = await self._try_close_ads_and_popups(snapshot)
|
|
176
|
-
if closed:
|
|
177
|
-
ads_closed += closed
|
|
178
|
-
print(f" 🎯 已关闭 {closed} 个广告/弹窗", file=sys.stderr)
|
|
179
|
-
await asyncio.sleep(0.5) # 等待关闭动画
|
|
180
|
-
continue # 重新检查
|
|
181
|
-
|
|
182
|
-
# 2. 检测页面是否稳定
|
|
183
|
-
if last_snapshot and snapshot == last_snapshot:
|
|
184
|
-
stable_count += 1
|
|
185
|
-
if stable_count >= 2:
|
|
186
|
-
# 页面已稳定(连续2次快照相同)
|
|
187
|
-
print(f" ✅ 页面稳定,加载完成(耗时{elapsed:.1f}秒)", file=sys.stderr)
|
|
188
|
-
return {
|
|
189
|
-
"loaded": True,
|
|
190
|
-
"wait_time": elapsed,
|
|
191
|
-
"ads_closed": ads_closed,
|
|
192
|
-
"popups_closed": popups_closed
|
|
193
|
-
}
|
|
194
|
-
else:
|
|
195
|
-
stable_count = 0
|
|
196
|
-
|
|
197
|
-
last_snapshot = snapshot
|
|
198
|
-
|
|
199
|
-
# 优化:每1.5秒打印一次等待进度(从2秒减少)
|
|
200
|
-
if i % 5 == 0 and i > 0: # 5 * 0.3秒 = 1.5秒
|
|
201
|
-
print(f" ⏳ 等待中... ({elapsed:.1f}秒)", file=sys.stderr)
|
|
202
|
-
|
|
203
|
-
except Exception as e:
|
|
204
|
-
print(f" ⚠️ 检查页面状态失败: {e}", file=sys.stderr)
|
|
205
|
-
continue
|
|
206
|
-
|
|
207
|
-
# 超时
|
|
208
|
-
elapsed = time.time() - start_time
|
|
209
|
-
print(f" ⏰ 等待超时({elapsed:.1f}秒),但App已启动", file=sys.stderr)
|
|
210
|
-
return {
|
|
211
|
-
"loaded": False,
|
|
212
|
-
"wait_time": elapsed,
|
|
213
|
-
"ads_closed": ads_closed,
|
|
214
|
-
"popups_closed": popups_closed
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async def _try_close_ads_and_popups(self, snapshot: str) -> int:
|
|
218
|
-
"""
|
|
219
|
-
尝试关闭广告和弹窗(更谨慎的检测逻辑)
|
|
220
|
-
|
|
221
|
-
Args:
|
|
222
|
-
snapshot: 页面XML快照
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
关闭的数量
|
|
226
|
-
"""
|
|
227
|
-
closed_count = 0
|
|
228
|
-
|
|
229
|
-
try:
|
|
230
|
-
# 解析XML查找关闭按钮
|
|
231
|
-
elements = self.client.xml_parser.parse(snapshot)
|
|
232
|
-
|
|
233
|
-
# 🎯 改进:先检测是否有弹窗容器(避免误点击正常UI)
|
|
234
|
-
has_popup = False
|
|
235
|
-
for elem in elements:
|
|
236
|
-
class_name = elem.get('class', '').lower()
|
|
237
|
-
resource_id = elem.get('resource_id', '').lower()
|
|
238
|
-
# 检查是否是弹窗容器
|
|
239
|
-
if any(keyword in class_name or keyword in resource_id
|
|
240
|
-
for keyword in ['dialog', 'popup', 'alert', 'modal']):
|
|
241
|
-
has_popup = True
|
|
242
|
-
break
|
|
243
|
-
|
|
244
|
-
# 如果没有检测到弹窗容器,不执行关闭操作(避免误点击)
|
|
245
|
-
if not has_popup:
|
|
246
|
-
return 0
|
|
247
|
-
|
|
248
|
-
# 查找可能的关闭按钮
|
|
249
|
-
close_buttons = []
|
|
250
|
-
|
|
251
|
-
for elem in elements:
|
|
252
|
-
if not elem.get('clickable', False):
|
|
253
|
-
continue
|
|
254
|
-
|
|
255
|
-
text = elem.get('text', '').lower()
|
|
256
|
-
content_desc = elem.get('content_desc', '').lower()
|
|
257
|
-
resource_id = elem.get('resource_id', '').lower()
|
|
258
|
-
bounds = elem.get('bounds', '')
|
|
259
|
-
|
|
260
|
-
# 检查是否是关闭按钮
|
|
261
|
-
is_close_button = False
|
|
262
|
-
for keyword in self.ad_close_keywords:
|
|
263
|
-
keyword_lower = keyword.lower()
|
|
264
|
-
if (keyword_lower in text or
|
|
265
|
-
keyword_lower in content_desc or
|
|
266
|
-
keyword_lower in resource_id or
|
|
267
|
-
('close' in resource_id and 'btn' in resource_id) or
|
|
268
|
-
('skip' in resource_id)):
|
|
269
|
-
is_close_button = True
|
|
270
|
-
break
|
|
271
|
-
|
|
272
|
-
# 🎯 改进:优先选择右上角的关闭按钮(更可能是真正的关闭按钮)
|
|
273
|
-
if is_close_button and bounds:
|
|
274
|
-
import re
|
|
275
|
-
match = re.search(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
|
|
276
|
-
if match:
|
|
277
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
278
|
-
# 计算元素位置(右上角区域优先级更高)
|
|
279
|
-
elem['_priority'] = 0
|
|
280
|
-
if x1 > 800: # 右侧
|
|
281
|
-
elem['_priority'] += 2
|
|
282
|
-
if y1 < 500: # 上部
|
|
283
|
-
elem['_priority'] += 2
|
|
284
|
-
close_buttons.append(elem)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
# 🎯 改进:按优先级排序(右上角的关闭按钮优先)
|
|
288
|
-
close_buttons.sort(key=lambda x: x.get('_priority', 0), reverse=True)
|
|
289
|
-
|
|
290
|
-
# 尝试点击关闭按钮(最多尝试1个,避免误点击)
|
|
291
|
-
for button in close_buttons[:1]: # 🎯 改进:只点击优先级最高的1个
|
|
292
|
-
try:
|
|
293
|
-
# 优先使用bounds点击(更可靠)
|
|
294
|
-
bounds = button.get('bounds', '')
|
|
295
|
-
if bounds:
|
|
296
|
-
# 解析bounds并点击中心点
|
|
297
|
-
import re
|
|
298
|
-
match = re.search(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
|
|
299
|
-
if match:
|
|
300
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
301
|
-
center_x = (x1 + x2) // 2
|
|
302
|
-
center_y = (y1 + y2) // 2
|
|
303
|
-
|
|
304
|
-
button_desc = button.get('text') or button.get('content_desc') or '未知'
|
|
305
|
-
print(f" 🎯 检测到弹窗,准备点击关闭按钮: {button_desc} (位置: {center_x}, {center_y})", file=sys.stderr)
|
|
306
|
-
|
|
307
|
-
# 🎯 改进:延迟1秒再点击(给用户时间看清楚)
|
|
308
|
-
await asyncio.sleep(1.0)
|
|
309
|
-
|
|
310
|
-
self.client.u2.click(center_x, center_y)
|
|
311
|
-
closed_count += 1
|
|
312
|
-
|
|
313
|
-
print(f" ✅ 已关闭弹窗: {button_desc}", file=sys.stderr)
|
|
314
|
-
|
|
315
|
-
await asyncio.sleep(0.5) # 等待关闭动画
|
|
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 _get_current_package(self) -> Optional[str]:
|
|
328
|
-
"""获取当前包名"""
|
|
329
|
-
try:
|
|
330
|
-
info = self.client.u2.app_current()
|
|
331
|
-
return info.get('package')
|
|
332
|
-
except:
|
|
333
|
-
return None
|
|
334
|
-
|
core/smart_tools.py
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
智能 MCP 工具 - 需要 AI 密钥(可选功能)
|
|
5
|
-
|
|
6
|
-
提供智能定位和分析功能:
|
|
7
|
-
- 自然语言元素定位
|
|
8
|
-
- 智能元素识别
|
|
9
|
-
- 复杂场景分析
|
|
10
|
-
|
|
11
|
-
⚠️ 这些功能需要配置 AI 密钥才能使用
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from typing import Dict, Optional
|
|
15
|
-
import os
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class SmartMobileTools:
|
|
19
|
-
"""智能移动端工具(需要 AI 密钥)"""
|
|
20
|
-
|
|
21
|
-
def __init__(self, mobile_client):
|
|
22
|
-
"""
|
|
23
|
-
初始化智能工具
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
mobile_client: MobileClient 实例
|
|
27
|
-
"""
|
|
28
|
-
self.client = mobile_client
|
|
29
|
-
self.ai_available = self._check_ai_available()
|
|
30
|
-
|
|
31
|
-
if self.ai_available:
|
|
32
|
-
# 延迟导入,避免没有配置 AI 时报错
|
|
33
|
-
from .locator.mobile_smart_locator import MobileSmartLocator
|
|
34
|
-
self.smart_locator = MobileSmartLocator(mobile_client)
|
|
35
|
-
else:
|
|
36
|
-
self.smart_locator = None
|
|
37
|
-
|
|
38
|
-
def _check_ai_available(self) -> bool:
|
|
39
|
-
"""检查 AI 是否可用(是否配置了 AI 密钥)"""
|
|
40
|
-
try:
|
|
41
|
-
from dotenv import load_dotenv
|
|
42
|
-
load_dotenv()
|
|
43
|
-
|
|
44
|
-
ai_provider = os.getenv('AI_PROVIDER', '')
|
|
45
|
-
|
|
46
|
-
# 检查是否配置了任何 AI 提供商
|
|
47
|
-
if ai_provider in ['qwen', 'openai', 'claude', 'ollama']:
|
|
48
|
-
# 检查对应的 API Key
|
|
49
|
-
if ai_provider == 'qwen' and os.getenv('QWEN_API_KEY'):
|
|
50
|
-
return True
|
|
51
|
-
elif ai_provider == 'openai' and os.getenv('OPENAI_API_KEY'):
|
|
52
|
-
return True
|
|
53
|
-
elif ai_provider == 'claude' and os.getenv('ANTHROPIC_API_KEY'):
|
|
54
|
-
return True
|
|
55
|
-
elif ai_provider == 'ollama':
|
|
56
|
-
return True # Ollama 不需要 API Key
|
|
57
|
-
|
|
58
|
-
return False
|
|
59
|
-
except:
|
|
60
|
-
return False
|
|
61
|
-
|
|
62
|
-
def _ensure_ai_available(self):
|
|
63
|
-
"""确保 AI 可用,否则抛出友好的错误提示"""
|
|
64
|
-
if not self.ai_available:
|
|
65
|
-
raise ValueError(
|
|
66
|
-
"❌ 智能定位功能需要配置 AI 密钥!\n\n"
|
|
67
|
-
"请选择以下方案之一:\n\n"
|
|
68
|
-
"方案1:使用基础工具(推荐,不需要 AI)\n"
|
|
69
|
-
" - mobile_list_elements() - 列出所有元素\n"
|
|
70
|
-
" - mobile_click_by_id(resource_id) - 通过 ID 点击\n"
|
|
71
|
-
" - mobile_click_at_coords(x, y) - 通过坐标点击\n\n"
|
|
72
|
-
"方案2:配置 AI 密钥(启用智能功能)\n"
|
|
73
|
-
" 创建 .env 文件:\n"
|
|
74
|
-
" AI_PROVIDER=qwen\n"
|
|
75
|
-
" QWEN_API_KEY=your-api-key\n\n"
|
|
76
|
-
"详见: backend/mobile_mcp/AI_SETUP.md"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
async def smart_click(self, description: str) -> Dict:
|
|
80
|
-
"""
|
|
81
|
-
智能定位并点击元素(需要 AI 密钥)
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
description: 元素的自然语言描述(如 "顶部搜索框"、"登录按钮")
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
{"success": true/false, "message": "...", "method": "..."}
|
|
88
|
-
|
|
89
|
-
示例:
|
|
90
|
-
# 需要先配置 AI 密钥
|
|
91
|
-
result = await tools.smart_click("右上角的设置按钮")
|
|
92
|
-
|
|
93
|
-
⚠️ 如果没有配置 AI 密钥,请使用基础工具:
|
|
94
|
-
elements = mobile_list_elements()
|
|
95
|
-
mobile_click_by_id("com.app:id/settings")
|
|
96
|
-
"""
|
|
97
|
-
self._ensure_ai_available()
|
|
98
|
-
|
|
99
|
-
try:
|
|
100
|
-
# 使用智能定位器
|
|
101
|
-
result = await self.smart_locator.locate(description)
|
|
102
|
-
|
|
103
|
-
if result and result.get('ref'):
|
|
104
|
-
# 执行点击
|
|
105
|
-
ref = result['ref']
|
|
106
|
-
method = result.get('method', 'unknown')
|
|
107
|
-
|
|
108
|
-
# 根据不同的 ref 类型执行点击
|
|
109
|
-
if ref.startswith('[') and ']' in ref:
|
|
110
|
-
# bounds 坐标
|
|
111
|
-
import re
|
|
112
|
-
coords = re.findall(r'\[(\d+),(\d+)\]', ref)
|
|
113
|
-
if coords:
|
|
114
|
-
x1, y1 = int(coords[0][0]), int(coords[0][1])
|
|
115
|
-
x2, y2 = int(coords[1][0]), int(coords[1][1])
|
|
116
|
-
x, y = (x1 + x2) // 2, (y1 + y2) // 2
|
|
117
|
-
self.client.u2.click(x, y)
|
|
118
|
-
return {
|
|
119
|
-
"success": True,
|
|
120
|
-
"message": f"智能定位成功: {description}",
|
|
121
|
-
"method": method,
|
|
122
|
-
"ref": ref
|
|
123
|
-
}
|
|
124
|
-
elif ':id/' in ref:
|
|
125
|
-
# resource-id
|
|
126
|
-
self.client.u2(resourceId=ref).click()
|
|
127
|
-
return {
|
|
128
|
-
"success": True,
|
|
129
|
-
"message": f"智能定位成功: {description}",
|
|
130
|
-
"method": method,
|
|
131
|
-
"ref": ref
|
|
132
|
-
}
|
|
133
|
-
else:
|
|
134
|
-
# text
|
|
135
|
-
self.client.u2(text=ref).click()
|
|
136
|
-
return {
|
|
137
|
-
"success": True,
|
|
138
|
-
"message": f"智能定位成功: {description}",
|
|
139
|
-
"method": method,
|
|
140
|
-
"ref": ref
|
|
141
|
-
}
|
|
142
|
-
else:
|
|
143
|
-
return {
|
|
144
|
-
"success": False,
|
|
145
|
-
"message": f"智能定位失败: {description}",
|
|
146
|
-
"suggestion": "请使用 mobile_list_elements() 查看页面元素,然后使用 mobile_click_by_id()"
|
|
147
|
-
}
|
|
148
|
-
except Exception as e:
|
|
149
|
-
return {
|
|
150
|
-
"success": False,
|
|
151
|
-
"message": f"智能点击失败: {str(e)}",
|
|
152
|
-
"suggestion": "建议使用基础工具: mobile_list_elements() + mobile_click_by_id()"
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async def smart_input(self, description: str, text: str) -> Dict:
|
|
156
|
-
"""
|
|
157
|
-
智能定位输入框并输入文本(需要 AI 密钥)
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
description: 输入框的自然语言描述(如 "用户名输入框")
|
|
161
|
-
text: 要输入的文本
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
{"success": true/false, "message": "..."}
|
|
165
|
-
|
|
166
|
-
示例:
|
|
167
|
-
result = await tools.smart_input("邮箱输入框", "test@example.com")
|
|
168
|
-
|
|
169
|
-
⚠️ 如果没有配置 AI 密钥,请使用基础工具:
|
|
170
|
-
mobile_input_text_by_id("com.app:id/email_input", "test@example.com")
|
|
171
|
-
"""
|
|
172
|
-
self._ensure_ai_available()
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
# 使用智能定位器
|
|
176
|
-
result = await self.smart_locator.locate(description)
|
|
177
|
-
|
|
178
|
-
if result and result.get('ref'):
|
|
179
|
-
ref = result['ref']
|
|
180
|
-
|
|
181
|
-
# 根据不同的 ref 类型执行输入
|
|
182
|
-
if ':id/' in ref:
|
|
183
|
-
# resource-id
|
|
184
|
-
element = self.client.u2(resourceId=ref)
|
|
185
|
-
element.set_text(text)
|
|
186
|
-
return {
|
|
187
|
-
"success": True,
|
|
188
|
-
"message": f"智能输入成功: {description} = {text}",
|
|
189
|
-
"ref": ref
|
|
190
|
-
}
|
|
191
|
-
else:
|
|
192
|
-
return {
|
|
193
|
-
"success": False,
|
|
194
|
-
"message": f"定位成功但无法输入: {ref}",
|
|
195
|
-
"suggestion": "请使用 mobile_find_elements_by_class('android.widget.EditText') 查找输入框"
|
|
196
|
-
}
|
|
197
|
-
else:
|
|
198
|
-
return {
|
|
199
|
-
"success": False,
|
|
200
|
-
"message": f"智能定位失败: {description}",
|
|
201
|
-
"suggestion": "请使用 mobile_list_elements() 查找输入框,然后使用 mobile_input_text_by_id()"
|
|
202
|
-
}
|
|
203
|
-
except Exception as e:
|
|
204
|
-
return {
|
|
205
|
-
"success": False,
|
|
206
|
-
"message": f"智能输入失败: {str(e)}",
|
|
207
|
-
"suggestion": "建议使用基础工具: mobile_input_text_by_id()"
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async def analyze_screenshot_with_ai(self, screenshot_path: str, description: str) -> Dict:
|
|
211
|
-
"""
|
|
212
|
-
使用 AI 分析截图并返回坐标(需要 AI 密钥)
|
|
213
|
-
|
|
214
|
-
Args:
|
|
215
|
-
screenshot_path: 截图文件路径
|
|
216
|
-
description: 要查找的元素描述
|
|
217
|
-
|
|
218
|
-
Returns:
|
|
219
|
-
{
|
|
220
|
-
"success": true/false,
|
|
221
|
-
"x": 坐标X(如果成功),
|
|
222
|
-
"y": 坐标Y(如果成功),
|
|
223
|
-
"confidence": 置信度,
|
|
224
|
-
"message": "..."
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
示例:
|
|
228
|
-
# 先截图
|
|
229
|
-
screenshot = mobile_take_screenshot("登录页面")
|
|
230
|
-
|
|
231
|
-
# 然后用 AI 分析
|
|
232
|
-
result = await tools.analyze_screenshot_with_ai(
|
|
233
|
-
screenshot['screenshot_path'],
|
|
234
|
-
"登录按钮"
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
# 根据返回的坐标点击
|
|
238
|
-
if result['success']:
|
|
239
|
-
mobile_click_at_coords(result['x'], result['y'])
|
|
240
|
-
|
|
241
|
-
⚠️ 需要配置支持视觉识别的 AI(如 GPT-4V、Claude 3、Qwen-VL)
|
|
242
|
-
"""
|
|
243
|
-
self._ensure_ai_available()
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
# 尝试使用视觉识别
|
|
247
|
-
try:
|
|
248
|
-
from ..vision.vision_locator import MobileVisionLocator
|
|
249
|
-
|
|
250
|
-
vision_locator = MobileVisionLocator(self.client)
|
|
251
|
-
result = await vision_locator.locate_element_by_vision(
|
|
252
|
-
description,
|
|
253
|
-
screenshot_path=screenshot_path
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
if result and result.get('found'):
|
|
257
|
-
x, y = result['x'], result['y']
|
|
258
|
-
confidence = result['confidence']
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
"success": True,
|
|
262
|
-
"x": x,
|
|
263
|
-
"y": y,
|
|
264
|
-
"confidence": confidence,
|
|
265
|
-
"message": f"✅ AI 视觉识别成功: ({x}, {y}), 置信度 {confidence}%"
|
|
266
|
-
}
|
|
267
|
-
else:
|
|
268
|
-
reason = result.get('reason', '未知原因') if result else '未知原因'
|
|
269
|
-
return {
|
|
270
|
-
"success": False,
|
|
271
|
-
"message": f"❌ AI 视觉识别未找到元素: {reason}",
|
|
272
|
-
"suggestion": "请检查截图和元素描述是否准确"
|
|
273
|
-
}
|
|
274
|
-
except ImportError:
|
|
275
|
-
return {
|
|
276
|
-
"success": False,
|
|
277
|
-
"message": "❌ 视觉识别模块未安装",
|
|
278
|
-
"suggestion": "安装:pip install dashscope pillow"
|
|
279
|
-
}
|
|
280
|
-
except Exception as e:
|
|
281
|
-
return {
|
|
282
|
-
"success": False,
|
|
283
|
-
"message": f"❌ 视觉识别失败: {str(e)}",
|
|
284
|
-
"suggestion": "请使用基础工具或检查 AI 配置"
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
def get_ai_status(self) -> Dict:
|
|
288
|
-
"""
|
|
289
|
-
获取 AI 功能状态
|
|
290
|
-
|
|
291
|
-
Returns:
|
|
292
|
-
{
|
|
293
|
-
"available": true/false,
|
|
294
|
-
"provider": "qwen/openai/...",
|
|
295
|
-
"message": "..."
|
|
296
|
-
}
|
|
297
|
-
"""
|
|
298
|
-
if self.ai_available:
|
|
299
|
-
provider = os.getenv('AI_PROVIDER', 'unknown')
|
|
300
|
-
return {
|
|
301
|
-
"available": True,
|
|
302
|
-
"provider": provider,
|
|
303
|
-
"message": f"✅ AI 功能已启用 (Provider: {provider})"
|
|
304
|
-
}
|
|
305
|
-
else:
|
|
306
|
-
return {
|
|
307
|
-
"available": False,
|
|
308
|
-
"provider": None,
|
|
309
|
-
"message": "⚠️ AI 功能未配置,当前仅支持基础工具。如需启用智能定位,请配置 AI 密钥。"
|
|
310
|
-
}
|
|
311
|
-
|