mobile-mcp-ai 2.2.6__py3-none-any.whl → 2.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. mobile_mcp/config.py +3 -2
  2. mobile_mcp/core/basic_tools_lite.py +3193 -0
  3. mobile_mcp/core/ios_client_wda.py +569 -0
  4. mobile_mcp/core/ios_device_manager_wda.py +306 -0
  5. mobile_mcp/core/mobile_client.py +246 -20
  6. mobile_mcp/core/template_matcher.py +429 -0
  7. mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
  8. mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
  9. mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
  10. mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
  11. mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
  12. mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
  13. mobile_mcp/mcp_tools/__init__.py +10 -0
  14. mobile_mcp/mcp_tools/mcp_server.py +992 -0
  15. mobile_mcp_ai-2.5.3.dist-info/METADATA +456 -0
  16. mobile_mcp_ai-2.5.3.dist-info/RECORD +32 -0
  17. mobile_mcp_ai-2.5.3.dist-info/entry_points.txt +2 -0
  18. mobile_mcp/core/ai/__init__.py +0 -11
  19. mobile_mcp/core/ai/ai_analyzer.py +0 -197
  20. mobile_mcp/core/ai/ai_config.py +0 -116
  21. mobile_mcp/core/ai/ai_platform_adapter.py +0 -399
  22. mobile_mcp/core/ai/smart_test_executor.py +0 -520
  23. mobile_mcp/core/ai/test_generator.py +0 -365
  24. mobile_mcp/core/ai/test_generator_from_history.py +0 -391
  25. mobile_mcp/core/ai/test_generator_standalone.py +0 -293
  26. mobile_mcp/core/assertion/__init__.py +0 -9
  27. mobile_mcp/core/assertion/smart_assertion.py +0 -341
  28. mobile_mcp/core/basic_tools.py +0 -945
  29. mobile_mcp/core/h5/__init__.py +0 -10
  30. mobile_mcp/core/h5/h5_handler.py +0 -548
  31. mobile_mcp/core/ios_client.py +0 -219
  32. mobile_mcp/core/ios_device_manager.py +0 -252
  33. mobile_mcp/core/locator/__init__.py +0 -10
  34. mobile_mcp/core/locator/cursor_ai_auto_analyzer.py +0 -119
  35. mobile_mcp/core/locator/cursor_vision_helper.py +0 -414
  36. mobile_mcp/core/locator/mobile_smart_locator.py +0 -1747
  37. mobile_mcp/core/locator/position_analyzer.py +0 -813
  38. mobile_mcp/core/locator/script_updater.py +0 -157
  39. mobile_mcp/core/nl_test_runner.py +0 -585
  40. mobile_mcp/core/smart_app_launcher.py +0 -421
  41. mobile_mcp/core/smart_tools.py +0 -311
  42. mobile_mcp/mcp/__init__.py +0 -13
  43. mobile_mcp/mcp/mcp_server.py +0 -1126
  44. mobile_mcp/mcp/mcp_server_simple.py +0 -23
  45. mobile_mcp/vision/__init__.py +0 -10
  46. mobile_mcp/vision/vision_locator.py +0 -405
  47. mobile_mcp_ai-2.2.6.dist-info/METADATA +0 -503
  48. mobile_mcp_ai-2.2.6.dist-info/RECORD +0 -49
  49. mobile_mcp_ai-2.2.6.dist-info/entry_points.txt +0 -2
  50. {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/WHEEL +0 -0
  51. {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/licenses/LICENSE +0 -0
  52. {mobile_mcp_ai-2.2.6.dist-info → mobile_mcp_ai-2.5.3.dist-info}/top_level.txt +0 -0
@@ -1,414 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Cursor AI 视觉识别辅助工具
5
-
6
- 当定位失败时,自动截图并请求Cursor AI分析。
7
- 这个模块提供了与Cursor AI交互的接口。
8
- """
9
- import asyncio
10
- import json
11
- import tempfile
12
- import os
13
- from datetime import datetime
14
- from typing import Dict, Optional, Tuple
15
- from pathlib import Path
16
- import time
17
- import inspect
18
- import traceback
19
-
20
-
21
- class CursorVisionHelper:
22
- """
23
- Cursor AI 视觉识别辅助工具
24
-
25
- 功能:
26
- 1. 截图并保存
27
- 2. 生成提示信息,让Cursor AI分析截图
28
- 3. 解析Cursor AI返回的坐标
29
- """
30
-
31
- def __init__(self, mobile_client):
32
- """
33
- 初始化Cursor视觉识别辅助工具
34
-
35
- Args:
36
- mobile_client: MobileClient实例
37
- """
38
- self.mobile_client = mobile_client
39
- # 🎯 使用项目内的screenshots目录,而不是临时目录
40
- project_root = Path(__file__).parent.parent.parent
41
- self.screenshot_dir = project_root / "screenshots"
42
- self.screenshot_dir.mkdir(exist_ok=True)
43
- self.request_dir = self.screenshot_dir / "requests"
44
- self.request_dir.mkdir(exist_ok=True)
45
- self.result_dir = self.screenshot_dir / "results"
46
- self.result_dir.mkdir(exist_ok=True)
47
-
48
- async def take_screenshot(self, element_desc: str = "", region: Optional[Dict] = None) -> str:
49
- """
50
- 截图并保存(支持区域截图)
51
-
52
- Args:
53
- element_desc: 元素描述(用于文件名)
54
- region: 截图区域 {"x": int, "y": int, "width": int, "height": int},None表示全屏
55
-
56
- Returns:
57
- 截图文件路径
58
- """
59
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
60
- safe_desc = "".join(c for c in element_desc if c.isalnum() or c in (' ', '-', '_')).strip()[:20]
61
- if safe_desc:
62
- filename = f"screenshot_{safe_desc}_{timestamp}.png"
63
- else:
64
- filename = f"screenshot_{timestamp}.png"
65
-
66
- screenshot_path = self.screenshot_dir / filename
67
-
68
- if region:
69
- # 区域截图:先截全屏,再裁剪
70
- try:
71
- from PIL import Image # type: ignore
72
- PIL_AVAILABLE = True
73
- except ImportError:
74
- PIL_AVAILABLE = False
75
-
76
- if PIL_AVAILABLE:
77
- # 先截全屏
78
- temp_path = str(screenshot_path).replace('.png', '_full.png')
79
- self.mobile_client.u2.screenshot(temp_path)
80
-
81
- # 裁剪区域
82
- img = Image.open(temp_path)
83
- x = region.get('x', 0)
84
- y = region.get('y', 0)
85
- width = region.get('width', img.width)
86
- height = region.get('height', img.height)
87
-
88
- # 确保不越界
89
- x = max(0, min(x, img.width))
90
- y = max(0, min(y, img.height))
91
- width = min(width, img.width - x)
92
- height = min(height, img.height - y)
93
-
94
- # 裁剪
95
- cropped = img.crop((x, y, x + width, y + height))
96
- cropped.save(str(screenshot_path))
97
-
98
- # 删除临时文件
99
- import os
100
- if os.path.exists(temp_path):
101
- os.remove(temp_path)
102
-
103
- print(f" 📸 区域截图: ({x}, {y}) - ({x+width}, {y+height}), 尺寸: {width}x{height}")
104
- else:
105
- # PIL不可用时,使用全屏截图
106
- self.mobile_client.u2.screenshot(str(screenshot_path))
107
- print(f" ⚠️ PIL未安装,使用全屏截图")
108
- else:
109
- # 全屏截图
110
- self.mobile_client.u2.screenshot(str(screenshot_path))
111
-
112
- return str(screenshot_path)
113
-
114
- def _smart_region_selection(self, element_desc: str) -> Optional[Dict]:
115
- """
116
- 智能选择截图区域(根据元素描述推断区域)
117
-
118
- Args:
119
- element_desc: 元素描述
120
-
121
- Returns:
122
- 区域信息 或 None(全屏)
123
- """
124
- # 获取屏幕尺寸
125
- screen_info = self.mobile_client.u2.info
126
- screen_width = screen_info.get('displayWidth', 1080)
127
- screen_height = screen_info.get('displayHeight', 2400)
128
-
129
- desc_lower = element_desc.lower()
130
-
131
- # 🎯 角落区域(优先匹配,更精确)
132
- # 右上角区域(右上角图标、搜索图标等)
133
- if any(kw in desc_lower for kw in ['右上角', '上角', '搜索图标', 'search icon']):
134
- return {
135
- 'x': int(screen_width * 0.7), # 右侧30%
136
- 'y': 0,
137
- 'width': int(screen_width * 0.3), # 宽度30%
138
- 'height': int(screen_height * 0.15) # 顶部15%
139
- }
140
-
141
- # 左上角区域
142
- if '左上角' in desc_lower:
143
- return {
144
- 'x': 0,
145
- 'y': 0,
146
- 'width': int(screen_width * 0.3), # 左侧30%
147
- 'height': int(screen_height * 0.15) # 顶部15%
148
- }
149
-
150
- # 右下角区域
151
- if '右下角' in desc_lower:
152
- return {
153
- 'x': int(screen_width * 0.7), # 右侧30%
154
- 'y': int(screen_height * 0.85), # 底部15%
155
- 'width': int(screen_width * 0.3), # 宽度30%
156
- 'height': int(screen_height * 0.15) # 高度15%
157
- }
158
-
159
- # 左下角区域
160
- if '左下角' in desc_lower:
161
- return {
162
- 'x': 0,
163
- 'y': int(screen_height * 0.85), # 底部15%
164
- 'width': int(screen_width * 0.3), # 左侧30%
165
- 'height': int(screen_height * 0.15) # 高度15%
166
- }
167
-
168
- # 底部区域(底部导航栏、底部按钮等)
169
- if any(kw in desc_lower for kw in ['底部', 'bottom', '导航栏', 'tab']):
170
- return {
171
- 'x': 0,
172
- 'y': int(screen_height * 0.8), # 底部20%
173
- 'width': screen_width,
174
- 'height': int(screen_height * 0.2)
175
- }
176
-
177
- # 顶部区域(标题栏、顶部导航、设置图标等)
178
- if any(kw in desc_lower for kw in ['顶部', 'top', '标题', 'header', '设置', 'settings']):
179
- return {
180
- 'x': 0,
181
- 'y': 0,
182
- 'width': screen_width,
183
- 'height': int(screen_height * 0.2) # 顶部20%
184
- }
185
-
186
- # 中间区域(登录按钮、表单等)
187
- if any(kw in desc_lower for kw in ['登录', 'login', '按钮', 'button', '表单', 'form']):
188
- return {
189
- 'x': 0,
190
- 'y': int(screen_height * 0.3),
191
- 'width': screen_width,
192
- 'height': int(screen_height * 0.4) # 中间40%
193
- }
194
-
195
- # 默认全屏
196
- return None
197
-
198
- def generate_analysis_prompt(self, screenshot_path: str, element_desc: str) -> str:
199
- """
200
- 生成分析提示信息
201
-
202
- Args:
203
- screenshot_path: 截图路径
204
- element_desc: 元素描述
205
-
206
- Returns:
207
- 提示信息
208
- """
209
- prompt = f"""
210
- 🎯 需要分析移动端截图并定位元素
211
-
212
- 截图路径: {screenshot_path}
213
- 要查找的元素: {element_desc}
214
-
215
- 请执行以下步骤:
216
- 1. 查看截图文件: {screenshot_path}
217
- 2. 在截图中找到元素: {element_desc}
218
- 3. 返回元素的中心点坐标,格式为JSON:
219
- {{"x": 100, "y": 200, "confidence": 90}}
220
-
221
- 注意:
222
- - x, y 是元素中心点的像素坐标
223
- - confidence 是置信度(0-100)
224
- - 如果找不到元素,返回 {{"found": false}}
225
- """
226
- return prompt
227
-
228
- def parse_coordinate_response(self, response: str) -> Optional[Dict]:
229
- """
230
- 解析坐标响应
231
-
232
- Args:
233
- response: Cursor AI的响应文本
234
-
235
- Returns:
236
- 坐标信息 {"x": int, "y": int, "confidence": int} 或 None
237
- """
238
- try:
239
- # 尝试从响应中提取JSON
240
- import re
241
-
242
- # 查找JSON对象
243
- json_match = re.search(r'\{[^}]+\}', response)
244
- if json_match:
245
- json_str = json_match.group()
246
- coord = json.loads(json_str)
247
-
248
- if coord.get("found") is False:
249
- return None
250
-
251
- if "x" in coord and "y" in coord:
252
- return {
253
- "x": int(coord["x"]),
254
- "y": int(coord["y"]),
255
- "confidence": coord.get("confidence", 80)
256
- }
257
- except Exception as e:
258
- print(f" ⚠️ 解析坐标响应失败: {e}")
259
-
260
- return None
261
-
262
- async def analyze_with_cursor(self, element_desc: str, auto_analyze: bool = False) -> Optional[Dict]:
263
- """
264
- 使用Cursor AI分析截图并返回坐标
265
-
266
- Args:
267
- element_desc: 元素描述
268
- auto_analyze: 是否自动分析(通过MCP工具调用Cursor AI)
269
-
270
- Returns:
271
- 坐标信息 或 None
272
- """
273
- # 智能选择截图区域
274
- region = self._smart_region_selection(element_desc)
275
-
276
- # 截图
277
- screenshot_path = await self.take_screenshot(element_desc, region=region)
278
-
279
- if auto_analyze:
280
- # 🎯 自动分析:通过MCP工具调用Cursor AI
281
- # 这里需要调用MCP工具,让Cursor AI分析截图
282
- # 由于是在测试脚本中调用,需要通过某种机制触发Cursor AI
283
- print(f"\n📸 已截图: {screenshot_path}")
284
- print(f"🎯 自动调用Cursor AI分析截图...")
285
-
286
- # 返回截图路径,等待Cursor AI分析
287
- # 实际的坐标需要通过MCP工具返回
288
- # 🎯 创建分析请求文件,让Cursor AI自动处理
289
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
290
- request_id = f"{timestamp}_{hash(element_desc) % 10000}"
291
- request_file = self.request_dir / f"request_{request_id}.json"
292
- result_file = self.result_dir / f"result_{request_id}.json"
293
-
294
- # 尝试获取测试脚本路径
295
- script_path = None
296
- try:
297
- frame = inspect.currentframe()
298
- while frame:
299
- filename = frame.f_globals.get('__file__', '')
300
- if filename and 'test_' in filename and filename.endswith('.py'):
301
- script_path = filename
302
- break
303
- frame = frame.f_back
304
- except:
305
- pass
306
-
307
- request_data = {
308
- "request_id": request_id,
309
- "screenshot_path": screenshot_path,
310
- "element_desc": element_desc,
311
- "region": region,
312
- "timestamp": timestamp,
313
- "script_path": script_path,
314
- "status": "pending"
315
- }
316
-
317
- # 写入请求文件
318
- with open(request_file, 'w', encoding='utf-8') as f:
319
- json.dump(request_data, f, ensure_ascii=False, indent=2)
320
-
321
- print(f"\n📸 已截图: {screenshot_path}")
322
- print(f"📝 已创建分析请求: {request_file}")
323
- print(f"🎯 等待Cursor AI分析...")
324
- print(f"💡 Cursor AI会自动读取请求文件并分析截图")
325
-
326
- # 等待Cursor AI分析(轮询结果文件)
327
- # 🎯 优化:缩短等待时间,避免阻塞过久
328
- max_wait = 10 # 最多等待10秒(原30秒太长)
329
- wait_interval = 0.5 # 每0.5秒检查一次(提高响应速度)
330
- waited = 0
331
-
332
- while waited < max_wait:
333
- if result_file.exists():
334
- try:
335
- with open(result_file, 'r', encoding='utf-8') as f:
336
- result_data = json.load(f)
337
-
338
- if result_data.get('status') == 'completed':
339
- coord = result_data.get('coordinate')
340
- if coord and 'x' in coord and 'y' in coord:
341
- print(f"✅ Cursor AI分析完成,坐标: ({coord['x']}, {coord['y']})")
342
-
343
- # 🎯 可选:更新测试脚本(重新读取请求文件获取脚本路径)
344
- try:
345
- with open(request_file, 'r', encoding='utf-8') as rf:
346
- request_data = json.load(rf)
347
- script_path = request_data.get('script_path')
348
- self._update_test_script(element_desc, coord, script_path)
349
- except Exception as e:
350
- print(f" ⚠️ 更新脚本失败: {e}")
351
-
352
- # 清理文件
353
- request_file.unlink(missing_ok=True)
354
- result_file.unlink(missing_ok=True)
355
- return {
356
- "screenshot_path": screenshot_path,
357
- "coordinate": coord,
358
- "confidence": coord.get('confidence', 80),
359
- "status": "completed"
360
- }
361
- except Exception as e:
362
- print(f" ⚠️ 读取结果文件失败: {e}")
363
-
364
- await asyncio.sleep(wait_interval)
365
- waited += wait_interval
366
- if waited % 5 == 0:
367
- print(f" ⏳ 等待中... ({waited}/{max_wait}秒)")
368
-
369
- print(f" ⚠️ 超时:Cursor AI未在{max_wait}秒内返回结果")
370
- return {
371
- "screenshot_path": screenshot_path,
372
- "status": "timeout",
373
- "request_file": str(request_file),
374
- "result_file": str(result_file)
375
- }
376
- else:
377
- # 手动分析:生成提示信息
378
- prompt = self.generate_analysis_prompt(screenshot_path, element_desc)
379
-
380
- print(f"\n📸 已截图: {screenshot_path}")
381
- print(f"🎯 请Cursor AI分析截图,查找元素: {element_desc}")
382
- print(f"\n{prompt}\n")
383
-
384
- return {
385
- "screenshot_path": screenshot_path,
386
- "prompt": prompt,
387
- "status": "waiting_for_ai_analysis"
388
- }
389
-
390
- def _update_test_script(self, element_desc: str, coordinate: Dict, script_path: Optional[str] = None):
391
- """
392
- 更新测试脚本,添加坐标信息
393
-
394
- Args:
395
- element_desc: 元素描述
396
- coordinate: 坐标信息 {"x": int, "y": int, "confidence": int}
397
- script_path: 脚本路径(如果为None,尝试自动查找)
398
- """
399
- if not script_path:
400
- return
401
-
402
- try:
403
- from mobile_mcp.core.locator.script_updater import ScriptUpdater
404
- updater = ScriptUpdater(script_path)
405
- success = updater.update_with_coordinate(element_desc, coordinate, method='comment')
406
- if success:
407
- print(f" ✅ 测试脚本已更新: {script_path}")
408
- else:
409
- print(f" ⚠️ 更新测试脚本失败")
410
- except Exception as e:
411
- print(f" ⚠️ 更新测试脚本异常: {e}")
412
- import traceback
413
- traceback.print_exc()
414
-