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
core/basic_tools.py DELETED
@@ -1,377 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- 基础 MCP 工具 - 不需要 AI 密钥
5
-
6
- 提供基础的移动端自动化工具:
7
- - 元素列表获取
8
- - 精确点击(resource-id/坐标)
9
- - 输入、滑动、按键等
10
- - 截图功能
11
- """
12
-
13
- from typing import Dict, List, Optional
14
- from pathlib import Path
15
- import time
16
-
17
-
18
- class BasicMobileTools:
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
-
30
- # 截图目录
31
- project_root = Path(__file__).parent.parent.parent.parent
32
- self.screenshot_dir = project_root / "backend" / "mobile_mcp" / "screenshots"
33
- self.screenshot_dir.mkdir(exist_ok=True)
34
-
35
- def list_elements(self) -> List[Dict]:
36
- """
37
- 列出页面所有可交互元素
38
-
39
- Returns:
40
- 元素列表,每个元素包含:
41
- - resource_id: 资源ID
42
- - text: 文本内容
43
- - content_desc: 描述
44
- - class_name: 类名
45
- - bounds: 坐标 [x1,y1][x2,y2]
46
- - clickable: 是否可点击
47
- - enabled: 是否启用
48
-
49
- 示例:
50
- elements = tools.list_elements()
51
- # [
52
- # {
53
- # "resource_id": "com.app:id/search",
54
- # "text": "搜索",
55
- # "bounds": "[100,200][300,400]",
56
- # "clickable": true
57
- # },
58
- # ...
59
- # ]
60
- """
61
- xml_string = self.client.u2.dump_hierarchy()
62
- elements = self.client.xml_parser.parse(xml_string)
63
-
64
- # 过滤掉不可交互的元素,简化返回
65
- interactive_elements = []
66
- for elem in elements:
67
- if elem.get('clickable') or elem.get('long_clickable') or elem.get('focusable'):
68
- interactive_elements.append({
69
- 'resource_id': elem.get('resource_id', ''),
70
- 'text': elem.get('text', ''),
71
- 'content_desc': elem.get('content_desc', ''),
72
- 'class_name': elem.get('class_name', ''),
73
- 'bounds': elem.get('bounds', ''),
74
- 'clickable': elem.get('clickable', False),
75
- 'enabled': elem.get('enabled', True)
76
- })
77
-
78
- return interactive_elements
79
-
80
- def click_by_id(self, resource_id: str) -> Dict:
81
- """
82
- 通过 resource-id 点击元素
83
-
84
- Args:
85
- resource_id: 元素的 resource-id(如 "com.app:id/search")
86
-
87
- Returns:
88
- {"success": true/false, "message": "..."}
89
-
90
- 示例:
91
- tools.click_by_id("com.duitang.main:id/search_btn")
92
- """
93
- try:
94
- result = self.client.u2(resourceId=resource_id).click()
95
- if result:
96
- return {"success": True, "message": f"成功点击: {resource_id}"}
97
- else:
98
- return {"success": False, "message": f"元素不存在: {resource_id}"}
99
- except Exception as e:
100
- return {"success": False, "message": f"点击失败: {str(e)}"}
101
-
102
- def click_by_text(self, text: str) -> Dict:
103
- """
104
- 通过文本内容点击元素
105
-
106
- Args:
107
- text: 元素的文本内容(精确匹配)
108
-
109
- Returns:
110
- {"success": true/false, "message": "..."}
111
-
112
- 示例:
113
- tools.click_by_text("登录")
114
- """
115
- try:
116
- result = self.client.u2(text=text).click()
117
- if result:
118
- return {"success": True, "message": f"成功点击: {text}"}
119
- else:
120
- return {"success": False, "message": f"文本不存在: {text}"}
121
- except Exception as e:
122
- return {"success": False, "message": f"点击失败: {str(e)}"}
123
-
124
- def click_at_coords(self, x: int, y: int) -> Dict:
125
- """
126
- 点击指定坐标
127
-
128
- Args:
129
- x: X 坐标
130
- y: Y 坐标
131
-
132
- Returns:
133
- {"success": true/false, "message": "..."}
134
-
135
- 示例:
136
- tools.click_at_coords(500, 300)
137
- """
138
- try:
139
- self.client.u2.click(x, y)
140
- return {"success": True, "message": f"成功点击坐标: ({x}, {y})"}
141
- except Exception as e:
142
- return {"success": False, "message": f"点击失败: {str(e)}"}
143
-
144
- def input_text_by_id(self, resource_id: str, text: str) -> Dict:
145
- """
146
- 通过 resource-id 在输入框输入文本
147
-
148
- Args:
149
- resource_id: 输入框的 resource-id
150
- text: 要输入的文本
151
-
152
- Returns:
153
- {"success": true/false, "message": "..."}
154
-
155
- 示例:
156
- tools.input_text_by_id("com.app:id/username", "test@example.com")
157
- """
158
- try:
159
- element = self.client.u2(resourceId=resource_id)
160
- if element.exists:
161
- element.set_text(text)
162
- return {"success": True, "message": f"成功输入: {text}"}
163
- else:
164
- return {"success": False, "message": f"输入框不存在: {resource_id}"}
165
- except Exception as e:
166
- return {"success": False, "message": f"输入失败: {str(e)}"}
167
-
168
- def get_element_info(self, resource_id: str) -> Optional[Dict]:
169
- """
170
- 获取指定元素的详细信息
171
-
172
- Args:
173
- resource_id: 元素的 resource-id
174
-
175
- Returns:
176
- 元素信息字典,如果不存在返回 None
177
-
178
- 示例:
179
- info = tools.get_element_info("com.app:id/search")
180
- # {
181
- # "text": "搜索",
182
- # "bounds": "[100,200][300,400]",
183
- # "enabled": true
184
- # }
185
- """
186
- try:
187
- element = self.client.u2(resourceId=resource_id)
188
- if element.exists:
189
- info = element.info
190
- return {
191
- 'text': info.get('text', ''),
192
- 'content_desc': info.get('contentDescription', ''),
193
- 'class_name': info.get('className', ''),
194
- 'bounds': info.get('bounds', {}),
195
- 'clickable': info.get('clickable', False),
196
- 'enabled': info.get('enabled', True),
197
- 'focused': info.get('focused', False),
198
- 'selected': info.get('selected', False)
199
- }
200
- else:
201
- return None
202
- except Exception as e:
203
- return None
204
-
205
- def find_elements_by_class(self, class_name: str) -> List[Dict]:
206
- """
207
- 查找指定类名的所有元素
208
-
209
- Args:
210
- class_name: 类名(如 "android.widget.EditText")
211
-
212
- Returns:
213
- 元素列表
214
-
215
- 示例:
216
- # 查找所有输入框
217
- edit_texts = tools.find_elements_by_class("android.widget.EditText")
218
- """
219
- xml_string = self.client.u2.dump_hierarchy()
220
- elements = self.client.xml_parser.parse(xml_string)
221
-
222
- matched = []
223
- for elem in elements:
224
- if elem.get('class_name') == class_name:
225
- matched.append({
226
- 'resource_id': elem.get('resource_id', ''),
227
- 'text': elem.get('text', ''),
228
- 'content_desc': elem.get('content_desc', ''),
229
- 'bounds': elem.get('bounds', ''),
230
- 'clickable': elem.get('clickable', False),
231
- })
232
-
233
- return matched
234
-
235
- def wait_for_element(self, resource_id: str, timeout: int = 10) -> Dict:
236
- """
237
- 等待元素出现
238
-
239
- Args:
240
- resource_id: 元素的 resource-id
241
- timeout: 超时时间(秒)
242
-
243
- Returns:
244
- {"success": true/false, "message": "...", "exists": true/false}
245
-
246
- 示例:
247
- result = tools.wait_for_element("com.app:id/login_btn", timeout=5)
248
- """
249
- try:
250
- exists = self.client.u2(resourceId=resource_id).wait(timeout=timeout)
251
- if exists:
252
- return {
253
- "success": True,
254
- "exists": True,
255
- "message": f"元素已出现: {resource_id}"
256
- }
257
- else:
258
- return {
259
- "success": False,
260
- "exists": False,
261
- "message": f"等待超时: {resource_id}"
262
- }
263
- except Exception as e:
264
- return {
265
- "success": False,
266
- "exists": False,
267
- "message": f"等待失败: {str(e)}"
268
- }
269
-
270
- def take_screenshot(self, description: str = "") -> Dict:
271
- """
272
- 截取屏幕截图(不需要 AI)
273
-
274
- Args:
275
- description: 截图描述(可选),用于生成文件名
276
-
277
- Returns:
278
- {
279
- "success": true/false,
280
- "screenshot_path": "截图保存路径",
281
- "message": "..."
282
- }
283
-
284
- 示例:
285
- result = tools.take_screenshot("登录页面")
286
- # {"success": true, "screenshot_path": "/path/to/screenshot_登录页面_xxx.png"}
287
-
288
- 用途:
289
- - 用于 Cursor AI 视觉识别
290
- - 调试页面状态
291
- - 记录测试过程
292
- """
293
- try:
294
- import re
295
- timestamp = time.strftime("%Y%m%d_%H%M%S")
296
-
297
- # 清理描述中的特殊字符
298
- if description:
299
- safe_desc = re.sub(r'[^\w\s-]', '', description).strip()
300
- safe_desc = re.sub(r'[\s]+', '_', safe_desc)
301
- filename = f"screenshot_{safe_desc}_{timestamp}.png"
302
- else:
303
- filename = f"screenshot_{timestamp}.png"
304
-
305
- screenshot_path = self.screenshot_dir / filename
306
-
307
- # 截图
308
- self.client.u2.screenshot(str(screenshot_path))
309
-
310
- return {
311
- "success": True,
312
- "screenshot_path": str(screenshot_path),
313
- "message": f"截图已保存: {screenshot_path}"
314
- }
315
- except Exception as e:
316
- return {
317
- "success": False,
318
- "screenshot_path": "",
319
- "message": f"截图失败: {str(e)}"
320
- }
321
-
322
- def take_screenshot_region(self, x1: int, y1: int, x2: int, y2: int, description: str = "") -> Dict:
323
- """
324
- 截取屏幕指定区域(不需要 AI)
325
-
326
- Args:
327
- x1, y1: 左上角坐标
328
- x2, y2: 右下角坐标
329
- description: 截图描述(可选)
330
-
331
- Returns:
332
- {"success": true/false, "screenshot_path": "...", "message": "..."}
333
-
334
- 示例:
335
- result = tools.take_screenshot_region(100, 200, 500, 800, "搜索框区域")
336
- """
337
- try:
338
- from PIL import Image
339
- import re
340
-
341
- timestamp = time.strftime("%Y%m%d_%H%M%S")
342
-
343
- # 清理描述
344
- if description:
345
- safe_desc = re.sub(r'[^\w\s-]', '', description).strip()
346
- safe_desc = re.sub(r'[\s]+', '_', safe_desc)
347
- filename = f"screenshot_region_{safe_desc}_{timestamp}.png"
348
- else:
349
- filename = f"screenshot_region_{timestamp}.png"
350
-
351
- # 先截全屏
352
- temp_path = self.screenshot_dir / f"temp_{timestamp}.png"
353
- self.client.u2.screenshot(str(temp_path))
354
-
355
- # 裁剪指定区域
356
- img = Image.open(str(temp_path))
357
- cropped = img.crop((x1, y1, x2, y2))
358
-
359
- screenshot_path = self.screenshot_dir / filename
360
- cropped.save(str(screenshot_path))
361
-
362
- # 删除临时文件
363
- temp_path.unlink()
364
-
365
- return {
366
- "success": True,
367
- "screenshot_path": str(screenshot_path),
368
- "message": f"区域截图已保存: {screenshot_path}"
369
- }
370
- except Exception as e:
371
- return {
372
- "success": False,
373
- "screenshot_path": "",
374
- "message": f"区域截图失败: {str(e)}"
375
- }
376
-
377
-
core/h5/__init__.py DELETED
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- H5处理模块 - 智能检测和处理H5/WebView内容
5
- """
6
-
7
- from .h5_handler import H5Handler
8
-
9
- __all__ = ['H5Handler']
10
-