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,429 @@
1
+ """
2
+ OpenCV 模板匹配器 - 用于精确识别广告弹窗X号
3
+ 核心优势:
4
+ 1. 收集常见X号样式建立模板库
5
+ 2. 多尺度匹配解决分辨率差异
6
+ 3. 返回精确坐标,点击准确率高
7
+ """
8
+
9
+ import os
10
+ import cv2
11
+ import numpy as np
12
+ from typing import Dict, List, Tuple, Optional
13
+ from pathlib import Path
14
+
15
+
16
+ class TemplateMatcher:
17
+ """OpenCV 模板匹配器"""
18
+
19
+ def __init__(self, template_dir: Optional[str] = None):
20
+ """
21
+ 初始化模板匹配器
22
+
23
+ Args:
24
+ template_dir: 模板目录路径,默认为 templates/close_buttons/
25
+ """
26
+ if template_dir is None:
27
+ # 默认模板目录:优先使用包内目录,其次使用项目根目录
28
+ core_dir = Path(__file__).parent
29
+ # 1. 包内目录 (pip 安装后)
30
+ pkg_template_dir = core_dir / "templates" / "close_buttons"
31
+ # 2. 项目根目录 (开发时)
32
+ root_template_dir = core_dir.parent / "templates" / "close_buttons"
33
+
34
+ if pkg_template_dir.exists():
35
+ self.template_dir = pkg_template_dir
36
+ elif root_template_dir.exists():
37
+ self.template_dir = root_template_dir
38
+ else:
39
+ # 默认创建包内目录
40
+ self.template_dir = pkg_template_dir
41
+ else:
42
+ self.template_dir = Path(template_dir)
43
+
44
+ # 确保目录存在
45
+ self.template_dir.mkdir(parents=True, exist_ok=True)
46
+
47
+ # 多尺度匹配的缩放范围
48
+ self.scales = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5, 1.8, 2.0]
49
+
50
+ # 匹配阈值(越高越严格)
51
+ self.match_threshold = 0.75
52
+
53
+ # 缓存加载的模板
54
+ self._template_cache: Dict[str, np.ndarray] = {}
55
+
56
+ def load_templates(self) -> List[Tuple[str, np.ndarray]]:
57
+ """
58
+ 加载所有模板图片
59
+
60
+ Returns:
61
+ List of (template_name, template_image) tuples
62
+ """
63
+ templates = []
64
+
65
+ if not self.template_dir.exists():
66
+ return templates
67
+
68
+ # 支持的图片格式
69
+ extensions = ['.png', '.jpg', '.jpeg', '.bmp']
70
+
71
+ for file in self.template_dir.iterdir():
72
+ if file.suffix.lower() in extensions:
73
+ template_name = file.stem
74
+
75
+ # 使用缓存
76
+ if template_name in self._template_cache:
77
+ templates.append((template_name, self._template_cache[template_name]))
78
+ continue
79
+
80
+ # 读取模板(支持透明通道)
81
+ template = cv2.imread(str(file), cv2.IMREAD_UNCHANGED)
82
+ if template is not None:
83
+ self._template_cache[template_name] = template
84
+ templates.append((template_name, template))
85
+
86
+ return templates
87
+
88
+ def match_single_template(
89
+ self,
90
+ screenshot: np.ndarray,
91
+ template: np.ndarray,
92
+ threshold: Optional[float] = None
93
+ ) -> List[Dict]:
94
+ """
95
+ 单模板多尺度匹配
96
+
97
+ Args:
98
+ screenshot: 截图 (BGR格式)
99
+ template: 模板图片
100
+ threshold: 匹配阈值
101
+
102
+ Returns:
103
+ 匹配结果列表
104
+ """
105
+ if threshold is None:
106
+ threshold = self.match_threshold
107
+
108
+ results = []
109
+
110
+ # 转灰度图
111
+ if len(screenshot.shape) == 3:
112
+ gray_screen = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
113
+ else:
114
+ gray_screen = screenshot
115
+
116
+ # 处理模板(可能有透明通道)
117
+ # 注意:不使用 mask,因为 TM_CCOEFF_NORMED + mask 可能返回 INF
118
+ if len(template.shape) == 3:
119
+ if template.shape[2] == 4: # BGRA
120
+ template_gray = cv2.cvtColor(template[:, :, :3], cv2.COLOR_BGR2GRAY)
121
+ else: # BGR
122
+ template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
123
+ else:
124
+ template_gray = template
125
+
126
+ template_h, template_w = template_gray.shape[:2]
127
+
128
+ # 多尺度匹配
129
+ for scale in self.scales:
130
+ # 缩放模板
131
+ new_w = int(template_w * scale)
132
+ new_h = int(template_h * scale)
133
+
134
+ # 跳过太小或太大的模板
135
+ if new_w < 10 or new_h < 10:
136
+ continue
137
+ if new_w > gray_screen.shape[1] or new_h > gray_screen.shape[0]:
138
+ continue
139
+
140
+ resized_template = cv2.resize(template_gray, (new_w, new_h))
141
+
142
+ # 模板匹配
143
+ try:
144
+ result = cv2.matchTemplate(
145
+ gray_screen, resized_template,
146
+ cv2.TM_CCOEFF_NORMED
147
+ )
148
+ except cv2.error:
149
+ continue
150
+
151
+ # 跳过包含 INF/NAN 的结果
152
+ if np.isinf(result).any() or np.isnan(result).any():
153
+ continue
154
+
155
+ # 找所有超过阈值的匹配点
156
+ locations = np.where(result >= threshold)
157
+
158
+ for pt in zip(*locations[::-1]): # (x, y)
159
+ confidence = float(result[pt[1], pt[0]])
160
+ center_x = int(pt[0] + new_w // 2)
161
+ center_y = int(pt[1] + new_h // 2)
162
+
163
+ results.append({
164
+ 'x': center_x,
165
+ 'y': center_y,
166
+ 'width': int(new_w),
167
+ 'height': int(new_h),
168
+ 'scale': float(scale),
169
+ 'confidence': confidence,
170
+ 'top_left': (int(pt[0]), int(pt[1])),
171
+ 'bottom_right': (int(pt[0] + new_w), int(pt[1] + new_h))
172
+ })
173
+
174
+ # 非极大值抑制(去除重叠的检测框)
175
+ results = self._non_max_suppression(results)
176
+
177
+ return results
178
+
179
+ def _non_max_suppression(self, results: List[Dict], overlap_thresh: float = 0.3) -> List[Dict]:
180
+ """
181
+ 非极大值抑制,去除重叠的检测框
182
+ """
183
+ if len(results) == 0:
184
+ return []
185
+
186
+ # 按置信度排序
187
+ results = sorted(results, key=lambda x: x['confidence'], reverse=True)
188
+
189
+ kept = []
190
+ for result in results:
191
+ is_duplicate = False
192
+ for kept_result in kept:
193
+ # 计算中心点距离
194
+ dx = abs(result['x'] - kept_result['x'])
195
+ dy = abs(result['y'] - kept_result['y'])
196
+
197
+ # 如果中心点距离小于框的一半大小,认为是重复
198
+ avg_size = (result['width'] + result['height'] +
199
+ kept_result['width'] + kept_result['height']) / 4
200
+
201
+ if dx < avg_size * overlap_thresh and dy < avg_size * overlap_thresh:
202
+ is_duplicate = True
203
+ break
204
+
205
+ if not is_duplicate:
206
+ kept.append(result)
207
+
208
+ return kept
209
+
210
+ def find_close_buttons(
211
+ self,
212
+ screenshot_path: str,
213
+ threshold: Optional[float] = None
214
+ ) -> Dict:
215
+ """
216
+ 在截图中查找所有关闭按钮
217
+
218
+ Args:
219
+ screenshot_path: 截图路径
220
+ threshold: 匹配阈值 (0-1)
221
+
222
+ Returns:
223
+ 匹配结果
224
+ """
225
+ # 读取截图
226
+ screenshot = cv2.imread(screenshot_path)
227
+ if screenshot is None:
228
+ return {
229
+ "success": False,
230
+ "error": f"无法读取截图: {screenshot_path}"
231
+ }
232
+
233
+ img_height, img_width = screenshot.shape[:2]
234
+
235
+ # 加载模板
236
+ templates = self.load_templates()
237
+ if not templates:
238
+ return {
239
+ "success": False,
240
+ "error": "没有找到模板图片,请在 templates/close_buttons/ 目录添加X号模板",
241
+ "template_dir": str(self.template_dir),
242
+ "tip": "添加常见X号截图到模板目录,命名如 x_circle.png, x_white.png 等"
243
+ }
244
+
245
+ all_matches = []
246
+
247
+ for template_name, template in templates:
248
+ matches = self.match_single_template(screenshot, template, threshold)
249
+ for match in matches:
250
+ match['template'] = template_name
251
+ all_matches.append(match)
252
+
253
+ # 按置信度排序
254
+ all_matches = sorted(all_matches, key=lambda x: x['confidence'], reverse=True)
255
+
256
+ # 再次 NMS 去除不同模板的重复检测
257
+ all_matches = self._non_max_suppression(all_matches)
258
+
259
+ if not all_matches:
260
+ return {
261
+ "success": False,
262
+ "message": "未找到匹配的关闭按钮",
263
+ "templates_used": [t[0] for t in templates],
264
+ "threshold": threshold or self.match_threshold,
265
+ "tip": "可能需要添加新的X号模板,或降低匹配阈值"
266
+ }
267
+
268
+ # 计算百分比坐标
269
+ for match in all_matches:
270
+ match['x_percent'] = round(match['x'] / img_width * 100, 1)
271
+ match['y_percent'] = round(match['y'] / img_height * 100, 1)
272
+
273
+ best = all_matches[0]
274
+
275
+ return {
276
+ "success": True,
277
+ "message": f"✅ 找到 {len(all_matches)} 个关闭按钮",
278
+ "best_match": {
279
+ "template": best['template'],
280
+ "center": {"x": int(best['x']), "y": int(best['y'])},
281
+ "percent": {"x": float(best['x_percent']), "y": float(best['y_percent'])},
282
+ "size": f"{best['width']}x{best['height']}",
283
+ "confidence": float(round(best['confidence'] * 100, 1))
284
+ },
285
+ "click_command": f"mobile_click_by_percent({best['x_percent']}, {best['y_percent']})",
286
+ "all_matches": [
287
+ {
288
+ "template": m['template'],
289
+ "percent": f"({m['x_percent']}%, {m['y_percent']}%)",
290
+ "confidence": f"{m['confidence']*100:.1f}%"
291
+ }
292
+ for m in all_matches[:5] # 最多返回5个
293
+ ],
294
+ "image_size": {"width": img_width, "height": img_height}
295
+ }
296
+
297
+ def add_template(self, image_path: str, template_name: str) -> Dict:
298
+ """
299
+ 添加新模板到模板库
300
+
301
+ Args:
302
+ image_path: 图片路径(可以是截图的一部分)
303
+ template_name: 模板名称
304
+
305
+ Returns:
306
+ 结果
307
+ """
308
+ # 读取图片
309
+ img = cv2.imread(image_path)
310
+ if img is None:
311
+ return {"success": False, "error": f"无法读取图片: {image_path}"}
312
+
313
+ # 保存到模板目录
314
+ output_path = self.template_dir / f"{template_name}.png"
315
+ cv2.imwrite(str(output_path), img)
316
+
317
+ # 清除缓存
318
+ self._template_cache.clear()
319
+
320
+ return {
321
+ "success": True,
322
+ "message": f"✅ 模板已保存: {output_path}",
323
+ "template_name": template_name
324
+ }
325
+
326
+ def crop_and_add_template(
327
+ self,
328
+ screenshot_path: str,
329
+ x: int, y: int,
330
+ width: int, height: int,
331
+ template_name: str
332
+ ) -> Dict:
333
+ """
334
+ 从截图中裁剪区域并添加为模板
335
+
336
+ Args:
337
+ screenshot_path: 截图路径
338
+ x, y: 左上角坐标
339
+ width, height: 裁剪尺寸
340
+ template_name: 模板名称
341
+
342
+ Returns:
343
+ 结果
344
+ """
345
+ img = cv2.imread(screenshot_path)
346
+ if img is None:
347
+ return {"success": False, "error": f"无法读取截图: {screenshot_path}"}
348
+
349
+ # 裁剪
350
+ cropped = img[y:y+height, x:x+width]
351
+
352
+ if cropped.size == 0:
353
+ return {"success": False, "error": "裁剪区域无效"}
354
+
355
+ # 保存
356
+ output_path = self.template_dir / f"{template_name}.png"
357
+ cv2.imwrite(str(output_path), cropped)
358
+
359
+ # 清除缓存
360
+ self._template_cache.clear()
361
+
362
+ return {
363
+ "success": True,
364
+ "message": f"✅ 模板已保存: {output_path}",
365
+ "template_name": template_name,
366
+ "size": f"{width}x{height}"
367
+ }
368
+
369
+ def list_templates(self) -> Dict:
370
+ """列出所有模板"""
371
+ templates = self.load_templates()
372
+
373
+ if not templates:
374
+ return {
375
+ "success": True,
376
+ "templates": [],
377
+ "message": "模板库为空",
378
+ "template_dir": str(self.template_dir)
379
+ }
380
+
381
+ template_info = []
382
+ for name, img in templates:
383
+ h, w = img.shape[:2]
384
+ template_info.append({
385
+ "name": name,
386
+ "size": f"{w}x{h}",
387
+ "path": str(self.template_dir / f"{name}.png")
388
+ })
389
+
390
+ return {
391
+ "success": True,
392
+ "templates": template_info,
393
+ "count": len(template_info),
394
+ "template_dir": str(self.template_dir)
395
+ }
396
+
397
+ def delete_template(self, template_name: str) -> Dict:
398
+ """删除模板"""
399
+ # 查找模板文件
400
+ for ext in ['.png', '.jpg', '.jpeg', '.bmp']:
401
+ path = self.template_dir / f"{template_name}{ext}"
402
+ if path.exists():
403
+ path.unlink()
404
+ self._template_cache.pop(template_name, None)
405
+ return {
406
+ "success": True,
407
+ "message": f"✅ 已删除模板: {template_name}"
408
+ }
409
+
410
+ return {
411
+ "success": False,
412
+ "error": f"模板不存在: {template_name}"
413
+ }
414
+
415
+
416
+ # 便捷函数
417
+ def match_close_button(screenshot_path: str, threshold: float = 0.75) -> Dict:
418
+ """
419
+ 快速匹配关闭按钮
420
+
421
+ 用法:
422
+ from core.template_matcher import match_close_button
423
+ result = match_close_button("screenshot.png")
424
+ if result["success"]:
425
+ print(result["click_command"])
426
+ """
427
+ matcher = TemplateMatcher()
428
+ return matcher.find_close_buttons(screenshot_path, threshold)
429
+
@@ -61,7 +61,7 @@ class SmartWait:
61
61
  while time.time() - start_time < timeout:
62
62
  try:
63
63
  # 获取当前页面快照(只获取元素数量,不解析详细内容)
64
- xml = self.client.u2.dump_hierarchy()
64
+ xml = self.client.u2.dump_hierarchy(compressed=False)
65
65
  current_snapshot = len(xml) # 使用XML长度作为简单的页面状态标识
66
66
 
67
67
  if last_snapshot is not None:
@@ -137,14 +137,14 @@ class SmartWait:
137
137
 
138
138
  try:
139
139
  # 获取初始页面状态
140
- initial_xml = self.client.u2.dump_hierarchy()
140
+ initial_xml = self.client.u2.dump_hierarchy(compressed=False)
141
141
  initial_length = len(initial_xml)
142
142
 
143
143
  while time.time() - start_time < timeout:
144
144
  await asyncio.sleep(self.poll_interval)
145
145
 
146
146
  try:
147
- current_xml = self.client.u2.dump_hierarchy()
147
+ current_xml = self.client.u2.dump_hierarchy(compressed=False)
148
148
  current_length = len(current_xml)
149
149
 
150
150
  # 页面变化超过5%认为有变化
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Mobile MCP Tools - MCP Server 模块
5
+ """
6
+
7
+ from .mcp_server import main
8
+
9
+ __all__ = ['main']
10
+