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,945 +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
- - 高级交互(双击、长按)
14
- """
15
-
16
- from typing import Dict, List, Optional
17
- from pathlib import Path
18
- import time
19
- import json
20
-
21
- from .dynamic_config import DynamicConfig
22
- from .utils.operation_history_manager import OperationHistoryManager
23
-
24
-
25
- class BasicMobileTools:
26
- """基础移动端工具(不依赖 AI)"""
27
-
28
- def __init__(self, mobile_client):
29
- """
30
- 初始化基础工具
31
-
32
- Args:
33
- mobile_client: MobileClient 实例
34
- """
35
- self.client = mobile_client
36
-
37
- # 截图目录
38
- project_root = Path(__file__).parent.parent # core -> mobile_mcp (项目根目录)
39
- self.screenshot_dir = project_root / "screenshots"
40
- self.screenshot_dir.mkdir(parents=True, exist_ok=True)
41
-
42
- def list_elements(self) -> List[Dict]:
43
- """
44
- 列出页面所有可交互元素
45
-
46
- Returns:
47
- 元素列表,每个元素包含:
48
- - resource_id: 资源ID
49
- - text: 文本内容
50
- - content_desc: 描述
51
- - class_name: 类名
52
- - bounds: 坐标 [x1,y1][x2,y2]
53
- - clickable: 是否可点击
54
- - enabled: 是否启用
55
-
56
- 示例:
57
- elements = tools.list_elements()
58
- # [
59
- # {
60
- # "resource_id": "com.app:id/search",
61
- # "text": "搜索",
62
- # "bounds": "[100,200][300,400]",
63
- # "clickable": true
64
- # },
65
- # ...
66
- # ]
67
- """
68
- xml_string = self.client.u2.dump_hierarchy()
69
- elements = self.client.xml_parser.parse(xml_string)
70
-
71
- # 过滤掉不可交互的元素,简化返回
72
- interactive_elements = []
73
- for elem in elements:
74
- if elem.get('clickable') or elem.get('long_clickable') or elem.get('focusable'):
75
- interactive_elements.append({
76
- 'resource_id': elem.get('resource_id', ''),
77
- 'text': elem.get('text', ''),
78
- 'content_desc': elem.get('content_desc', ''),
79
- 'class_name': elem.get('class_name', ''),
80
- 'bounds': elem.get('bounds', ''),
81
- 'clickable': elem.get('clickable', False),
82
- 'enabled': elem.get('enabled', True)
83
- })
84
-
85
- return interactive_elements
86
-
87
- def click_by_id(self, resource_id: str) -> Dict:
88
- """
89
- 通过 resource-id 点击元素
90
-
91
- Args:
92
- resource_id: 元素的 resource-id(如 "com.app:id/search")
93
-
94
- Returns:
95
- {"success": true/false, "message": "..."}
96
-
97
- 示例:
98
- tools.click_by_id("com.duitang.main:id/search_btn")
99
- """
100
- try:
101
- element = self.client.u2(resourceId=resource_id)
102
-
103
- # 先检查元素是否存在(timeout=0.5秒快速检查)
104
- if not element.exists(timeout=0.5):
105
- return {"success": False, "message": f"❌ 元素不存在: {resource_id}"}
106
-
107
- # 元素存在,执行点击
108
- element.click()
109
-
110
- # 点击后等待页面响应(使用动态配置)
111
- time.sleep(DynamicConfig.wait_after_click)
112
-
113
- return {
114
- "success": True,
115
- "message": f"✅ 点击成功!已点击元素: {resource_id}\n⚠️ 无需重复点击,操作已完成。"
116
- }
117
- except Exception as e:
118
- return {"success": False, "message": f"❌ 点击失败: {str(e)}"}
119
-
120
- def click_by_text(self, text: str) -> Dict:
121
- """
122
- 通过文本内容点击元素
123
-
124
- Args:
125
- text: 元素的文本内容(精确匹配)
126
-
127
- Returns:
128
- {"success": true/false, "message": "..."}
129
-
130
- 示例:
131
- tools.click_by_text("登录")
132
- """
133
- try:
134
- element = self.client.u2(text=text)
135
-
136
- # 先检查元素是否存在(timeout=0.5秒快速检查)
137
- if not element.exists(timeout=0.5):
138
- return {"success": False, "message": f"❌ 文本不存在: {text}"}
139
-
140
- # 元素存在,执行点击
141
- element.click()
142
-
143
- # 点击后等待页面响应(使用动态配置)
144
- time.sleep(DynamicConfig.wait_after_click)
145
-
146
- return {
147
- "success": True,
148
- "message": f"✅ 点击成功!已点击按钮: '{text}'\n⚠️ 无需重复点击或使用其他方式点击,操作已完成。"
149
- }
150
- except Exception as e:
151
- return {"success": False, "message": f"❌ 点击失败: {str(e)}"}
152
-
153
- def click_at_coords(self, x: int, y: int) -> Dict:
154
- """
155
- 点击指定坐标
156
-
157
- Args:
158
- x: X 坐标
159
- y: Y 坐标
160
-
161
- Returns:
162
- {"success": true/false, "message": "..."}
163
-
164
- 示例:
165
- tools.click_at_coords(500, 300)
166
- """
167
- try:
168
- self.client.u2.click(x, y)
169
- # 点击后等待页面响应(使用动态配置)
170
- time.sleep(DynamicConfig.wait_after_click)
171
- return {
172
- "success": True,
173
- "message": f"✅ 点击成功!坐标: ({x}, {y})\n⚠️ 无需重复点击,操作已完成。"
174
- }
175
- except Exception as e:
176
- return {"success": False, "message": f"❌ 点击失败: {str(e)}"}
177
-
178
- def input_text_by_id(self, resource_id: str, text: str) -> Dict:
179
- """
180
- 通过 resource-id 在输入框输入文本
181
-
182
- Args:
183
- resource_id: 输入框的 resource-id
184
- text: 要输入的文本
185
-
186
- Returns:
187
- {"success": true/false, "message": "..."}
188
-
189
- 示例:
190
- tools.input_text_by_id("com.app:id/username", "test@example.com")
191
- """
192
- try:
193
- element = self.client.u2(resourceId=resource_id)
194
- if element.exists:
195
- element.set_text(text)
196
- # 输入后等待UI更新(使用动态配置)
197
- time.sleep(DynamicConfig.wait_after_input)
198
- return {
199
- "success": True,
200
- "message": f"✅ 输入成功!已输入文本: '{text}'\n⚠️ 无需重复输入,操作已完成。"
201
- }
202
- else:
203
- return {"success": False, "message": f"❌ 输入框不存在: {resource_id}"}
204
- except Exception as e:
205
- return {"success": False, "message": f"❌ 输入失败: {str(e)}"}
206
-
207
- def get_element_info(self, resource_id: str) -> Optional[Dict]:
208
- """
209
- 获取指定元素的详细信息
210
-
211
- Args:
212
- resource_id: 元素的 resource-id
213
-
214
- Returns:
215
- 元素信息字典,如果不存在返回 None
216
-
217
- 示例:
218
- info = tools.get_element_info("com.app:id/search")
219
- # {
220
- # "text": "搜索",
221
- # "bounds": "[100,200][300,400]",
222
- # "enabled": true
223
- # }
224
- """
225
- try:
226
- element = self.client.u2(resourceId=resource_id)
227
- if element.exists:
228
- info = element.info
229
- return {
230
- 'text': info.get('text', ''),
231
- 'content_desc': info.get('contentDescription', ''),
232
- 'class_name': info.get('className', ''),
233
- 'bounds': info.get('bounds', {}),
234
- 'clickable': info.get('clickable', False),
235
- 'enabled': info.get('enabled', True),
236
- 'focused': info.get('focused', False),
237
- 'selected': info.get('selected', False)
238
- }
239
- else:
240
- return None
241
- except Exception as e:
242
- return None
243
-
244
- def find_elements_by_class(self, class_name: str) -> List[Dict]:
245
- """
246
- 查找指定类名的所有元素
247
-
248
- Args:
249
- class_name: 类名(如 "android.widget.EditText")
250
-
251
- Returns:
252
- 元素列表
253
-
254
- 示例:
255
- # 查找所有输入框
256
- edit_texts = tools.find_elements_by_class("android.widget.EditText")
257
- """
258
- xml_string = self.client.u2.dump_hierarchy()
259
- elements = self.client.xml_parser.parse(xml_string)
260
-
261
- matched = []
262
- for elem in elements:
263
- if elem.get('class_name') == class_name:
264
- matched.append({
265
- 'resource_id': elem.get('resource_id', ''),
266
- 'text': elem.get('text', ''),
267
- 'content_desc': elem.get('content_desc', ''),
268
- 'bounds': elem.get('bounds', ''),
269
- 'clickable': elem.get('clickable', False),
270
- })
271
-
272
- return matched
273
-
274
- def wait_for_element(self, resource_id: str, timeout: int = 10) -> Dict:
275
- """
276
- 等待元素出现
277
-
278
- Args:
279
- resource_id: 元素的 resource-id
280
- timeout: 超时时间(秒)
281
-
282
- Returns:
283
- {"success": true/false, "message": "...", "exists": true/false}
284
-
285
- 示例:
286
- result = tools.wait_for_element("com.app:id/login_btn", timeout=5)
287
- """
288
- try:
289
- exists = self.client.u2(resourceId=resource_id).wait(timeout=timeout)
290
- if exists:
291
- return {
292
- "success": True,
293
- "exists": True,
294
- "message": f"元素已出现: {resource_id}"
295
- }
296
- else:
297
- return {
298
- "success": False,
299
- "exists": False,
300
- "message": f"等待超时: {resource_id}"
301
- }
302
- except Exception as e:
303
- return {
304
- "success": False,
305
- "exists": False,
306
- "message": f"等待失败: {str(e)}"
307
- }
308
-
309
- def take_screenshot(self, description: str = "") -> Dict:
310
- """
311
- 截取屏幕截图(不需要 AI)
312
-
313
- Args:
314
- description: 截图描述(可选),用于生成文件名
315
-
316
- Returns:
317
- {
318
- "success": true/false,
319
- "screenshot_path": "截图保存路径",
320
- "message": "..."
321
- }
322
-
323
- 示例:
324
- result = tools.take_screenshot("登录页面")
325
- # {"success": true, "screenshot_path": "/path/to/screenshot_登录页面_xxx.png"}
326
-
327
- 用途:
328
- - 用于 Cursor AI 视觉识别
329
- - 调试页面状态
330
- - 记录测试过程
331
- """
332
- try:
333
- import re
334
- timestamp = time.strftime("%Y%m%d_%H%M%S")
335
-
336
- # 清理描述中的特殊字符
337
- if description:
338
- safe_desc = re.sub(r'[^\w\s-]', '', description).strip()
339
- safe_desc = re.sub(r'[\s]+', '_', safe_desc)
340
- filename = f"screenshot_{safe_desc}_{timestamp}.png"
341
- else:
342
- filename = f"screenshot_{timestamp}.png"
343
-
344
- screenshot_path = self.screenshot_dir / filename
345
-
346
- # 截图
347
- self.client.u2.screenshot(str(screenshot_path))
348
-
349
- return {
350
- "success": True,
351
- "screenshot_path": str(screenshot_path),
352
- "message": f"截图已保存: {screenshot_path}"
353
- }
354
- except Exception as e:
355
- return {
356
- "success": False,
357
- "screenshot_path": "",
358
- "message": f"截图失败: {str(e)}"
359
- }
360
-
361
- def take_screenshot_region(self, x1: int, y1: int, x2: int, y2: int, description: str = "") -> Dict:
362
- """
363
- 截取屏幕指定区域(不需要 AI)
364
-
365
- Args:
366
- x1, y1: 左上角坐标
367
- x2, y2: 右下角坐标
368
- description: 截图描述(可选)
369
-
370
- Returns:
371
- {"success": true/false, "screenshot_path": "...", "message": "..."}
372
-
373
- 示例:
374
- result = tools.take_screenshot_region(100, 200, 500, 800, "搜索框区域")
375
- """
376
- try:
377
- from PIL import Image
378
- import re
379
-
380
- timestamp = time.strftime("%Y%m%d_%H%M%S")
381
-
382
- # 清理描述
383
- if description:
384
- safe_desc = re.sub(r'[^\w\s-]', '', description).strip()
385
- safe_desc = re.sub(r'[\s]+', '_', safe_desc)
386
- filename = f"screenshot_region_{safe_desc}_{timestamp}.png"
387
- else:
388
- filename = f"screenshot_region_{timestamp}.png"
389
-
390
- # 先截全屏
391
- temp_path = self.screenshot_dir / f"temp_{timestamp}.png"
392
- self.client.u2.screenshot(str(temp_path))
393
-
394
- # 裁剪指定区域
395
- img = Image.open(str(temp_path))
396
- cropped = img.crop((x1, y1, x2, y2))
397
-
398
- screenshot_path = self.screenshot_dir / filename
399
- cropped.save(str(screenshot_path))
400
-
401
- # 删除临时文件
402
- temp_path.unlink()
403
-
404
- return {
405
- "success": True,
406
- "screenshot_path": str(screenshot_path),
407
- "message": f"区域截图已保存: {screenshot_path}"
408
- }
409
- except Exception as e:
410
- return {
411
- "success": False,
412
- "screenshot_path": "",
413
- "message": f"区域截图失败: {str(e)}"
414
- }
415
-
416
- # ==================== 设备管理工具 ====================
417
-
418
- def list_devices(self) -> Dict:
419
- """
420
- 列出所有已连接的 Android 设备
421
-
422
- Returns:
423
- {"success": true/false, "devices": [...], "count": N}
424
- """
425
- try:
426
- from .device_manager import DeviceManager
427
- manager = DeviceManager()
428
- devices = manager.list_devices()
429
-
430
- return {
431
- "success": True,
432
- "devices": devices,
433
- "count": len(devices),
434
- "message": f"找到 {len(devices)} 个设备"
435
- }
436
- except Exception as e:
437
- return {"success": False, "error": f"获取设备列表失败: {str(e)}"}
438
-
439
- def get_screen_size(self) -> Dict:
440
- """
441
- 获取设备屏幕尺寸
442
-
443
- Returns:
444
- {"success": true/false, "width": N, "height": N, "size": "WxH"}
445
- """
446
- try:
447
- info = self.client.u2.info
448
- width = info.get('displayWidth', 0)
449
- height = info.get('displayHeight', 0)
450
-
451
- return {
452
- "success": True,
453
- "width": width,
454
- "height": height,
455
- "size": f"{width}x{height}",
456
- "message": f"屏幕尺寸: {width}x{height}"
457
- }
458
- except Exception as e:
459
- return {"success": False, "error": f"获取屏幕尺寸失败: {str(e)}"}
460
-
461
- def get_orientation(self) -> Dict:
462
- """
463
- 获取当前屏幕方向
464
-
465
- Returns:
466
- {"success": true/false, "orientation": "portrait/landscape", "rotation": N}
467
- """
468
- try:
469
- info = self.client.u2.info
470
- rotation = info.get('displayRotation', 0)
471
-
472
- # 0或2 = 竖屏, 1或3 = 横屏
473
- is_portrait = rotation in [0, 2]
474
- orientation = "portrait" if is_portrait else "landscape"
475
-
476
- return {
477
- "success": True,
478
- "orientation": orientation,
479
- "rotation": rotation,
480
- "message": f"当前方向: {orientation}"
481
- }
482
- except Exception as e:
483
- return {"success": False, "error": f"获取屏幕方向失败: {str(e)}"}
484
-
485
- def set_orientation(self, orientation: str) -> Dict:
486
- """
487
- 设置屏幕方向
488
-
489
- Args:
490
- orientation: "portrait"(竖屏)或 "landscape"(横屏)
491
-
492
- Returns:
493
- {"success": true/false, "orientation": "...", "message": "..."}
494
- """
495
- try:
496
- if orientation not in ["portrait", "landscape"]:
497
- return {"success": False, "error": "orientation必须是'portrait'或'landscape'"}
498
-
499
- # 设置方向
500
- if orientation == "portrait":
501
- self.client.u2.set_orientation("n")
502
- else:
503
- self.client.u2.set_orientation("l")
504
-
505
- return {
506
- "success": True,
507
- "orientation": orientation,
508
- "message": f"屏幕方向已设置为: {orientation}"
509
- }
510
- except Exception as e:
511
- return {"success": False, "error": f"设置屏幕方向失败: {str(e)}"}
512
-
513
- # ==================== 应用管理工具 ====================
514
-
515
- def list_apps(self, filter_keyword: str = "") -> Dict:
516
- """
517
- 列出设备上已安装的应用
518
-
519
- Args:
520
- filter_keyword: 过滤关键词(可选)
521
-
522
- Returns:
523
- {"success": true/false, "apps": [...], "count": N}
524
- """
525
- try:
526
- apps = self.client.u2.app_list()
527
-
528
- # 过滤
529
- if filter_keyword:
530
- filtered_apps = [
531
- app for app in apps
532
- if filter_keyword.lower() in app.lower()
533
- ]
534
- else:
535
- filtered_apps = apps
536
-
537
- return {
538
- "success": True,
539
- "apps": filtered_apps,
540
- "count": len(filtered_apps),
541
- "total": len(apps),
542
- "message": f"找到 {len(filtered_apps)}/{len(apps)} 个应用"
543
- }
544
- except Exception as e:
545
- return {"success": False, "error": f"获取应用列表失败: {str(e)}"}
546
-
547
- def install_app(self, apk_path: str) -> Dict:
548
- """
549
- 安装 APK 文件
550
-
551
- Args:
552
- apk_path: APK 文件路径
553
-
554
- Returns:
555
- {"success": true/false, "message": "..."}
556
- """
557
- try:
558
- import os
559
-
560
- # 检查文件是否存在
561
- if not os.path.exists(apk_path):
562
- return {"success": False, "error": f"APK文件不存在: {apk_path}"}
563
-
564
- # 安装应用
565
- self.client.u2.app_install(apk_path)
566
-
567
- return {
568
- "success": True,
569
- "apk_path": apk_path,
570
- "message": f"应用安装成功: {apk_path}"
571
- }
572
- except Exception as e:
573
- return {"success": False, "error": f"安装应用失败: {str(e)}"}
574
-
575
- def uninstall_app(self, package_name: str) -> Dict:
576
- """
577
- 卸载应用
578
-
579
- Args:
580
- package_name: 应用包名
581
-
582
- Returns:
583
- {"success": true/false, "message": "..."}
584
- """
585
- try:
586
- self.client.u2.app_uninstall(package_name)
587
-
588
- return {
589
- "success": True,
590
- "package_name": package_name,
591
- "message": f"应用卸载成功: {package_name}"
592
- }
593
- except Exception as e:
594
- return {"success": False, "error": f"卸载应用失败: {str(e)}"}
595
-
596
- def terminate_app(self, package_name: str) -> Dict:
597
- """
598
- 终止应用(强制停止)
599
-
600
- Args:
601
- package_name: 应用包名
602
-
603
- Returns:
604
- {"success": true/false, "message": "..."}
605
- """
606
- try:
607
- self.client.u2.app_stop(package_name)
608
-
609
- return {
610
- "success": True,
611
- "package_name": package_name,
612
- "message": f"应用已终止: {package_name}"
613
- }
614
- except Exception as e:
615
- return {"success": False, "error": f"终止应用失败: {str(e)}"}
616
-
617
- def get_current_package(self) -> Dict:
618
- """
619
- 获取当前前台应用的包名
620
-
621
- Returns:
622
- {"success": true/false, "package": "...", "activity": "..."}
623
- """
624
- try:
625
- current = self.client.u2.app_current()
626
-
627
- return {
628
- "success": True,
629
- "package": current.get('package', ''),
630
- "activity": current.get('activity', ''),
631
- "message": f"当前应用: {current.get('package', '')}"
632
- }
633
- except Exception as e:
634
- return {"success": False, "error": f"获取当前包名失败: {str(e)}"}
635
-
636
- # ==================== 高级交互工具 ====================
637
-
638
- def double_click_at_coords(self, x: int, y: int) -> Dict:
639
- """
640
- 双击指定坐标
641
-
642
- Args:
643
- x: X 坐标
644
- y: Y 坐标
645
-
646
- Returns:
647
- {"success": true/false, "message": "..."}
648
- """
649
- try:
650
- self.client.u2.double_click(x, y)
651
- return {
652
- "success": True,
653
- "x": x,
654
- "y": y,
655
- "message": f"双击坐标: ({x}, {y})"
656
- }
657
- except Exception as e:
658
- return {"success": False, "error": f"双击失败: {str(e)}"}
659
-
660
- def long_press_at_coords(self, x: int, y: int, duration: float = 1.0) -> Dict:
661
- """
662
- 长按指定坐标
663
-
664
- Args:
665
- x: X 坐标
666
- y: Y 坐标
667
- duration: 长按时长(秒),默认 1.0
668
-
669
- Returns:
670
- {"success": true/false, "message": "..."}
671
- """
672
- try:
673
- self.client.u2.long_click(x, y, duration=duration)
674
- return {
675
- "success": True,
676
- "x": x,
677
- "y": y,
678
- "duration": duration,
679
- "message": f"长按坐标: ({x}, {y}), 持续{duration}秒"
680
- }
681
- except Exception as e:
682
- return {"success": False, "error": f"长按失败: {str(e)}"}
683
-
684
- def open_url(self, url: str) -> Dict:
685
- """
686
- 在设备浏览器中打开 URL
687
-
688
- Args:
689
- url: 要打开的 URL
690
-
691
- Returns:
692
- {"success": true/false, "message": "..."}
693
- """
694
- try:
695
- self.client.u2.open_url(url)
696
- return {
697
- "success": True,
698
- "url": url,
699
- "message": f"已打开URL: {url}"
700
- }
701
- except Exception as e:
702
- return {"success": False, "error": f"打开URL失败: {str(e)}"}
703
-
704
- def assert_text(self, text: str) -> Dict:
705
- """
706
- 断言页面中是否包含指定文本
707
-
708
- Args:
709
- text: 要检查的文本
710
-
711
- Returns:
712
- {"success": true/false, "found": true/false, "message": "..."}
713
- """
714
- try:
715
- exists = self.client.u2(text=text).exists()
716
-
717
- return {
718
- "success": True,
719
- "found": exists,
720
- "text": text,
721
- "message": f"文本'{text}' {'存在' if exists else '不存在'}"
722
- }
723
- except Exception as e:
724
- return {"success": False, "error": f"断言失败: {str(e)}"}
725
-
726
- # ==================== 等待工具 ====================
727
-
728
- def wait(self, seconds: Optional[float] = None, wait_for_text: Optional[str] = None,
729
- wait_for_id: Optional[str] = None, timeout: float = 10) -> Dict:
730
- """
731
- 通用等待工具 - 让 AI 根据场景灵活控制等待
732
-
733
- Args:
734
- seconds: 固定等待时间(秒),如等待广告加载
735
- wait_for_text: 等待指定文本出现,如 "首页"、"搜索结果"
736
- wait_for_id: 等待指定元素ID出现,如 "com.app:id/home"
737
- timeout: 等待元素的超时时间(秒),默认10秒
738
-
739
- Returns:
740
- {"success": true/false, "message": "...", "waited": N}
741
-
742
- 使用场景:
743
- 1. 打开App等广告:wait(seconds=5)
744
- 2. 等待搜索结果:wait(wait_for_text="搜索结果")
745
- 3. 等待页面加载:wait(wait_for_id="com.app:id/main")
746
-
747
- 示例:
748
- # 等待3秒广告
749
- tools.wait(seconds=3)
750
-
751
- # 等待"首页"文本出现
752
- tools.wait(wait_for_text="首页", timeout=5)
753
-
754
- # 等待主页元素出现
755
- tools.wait(wait_for_id="com.app:id/home_layout")
756
- """
757
- import time
758
-
759
- try:
760
- start_time = time.time()
761
-
762
- # 场景1: 固定等待时间
763
- if seconds:
764
- time.sleep(seconds)
765
- return {
766
- "success": True,
767
- "waited": seconds,
768
- "message": f"已等待 {seconds} 秒"
769
- }
770
-
771
- # 场景2: 等待文本出现
772
- elif wait_for_text:
773
- exists = self.client.u2(text=wait_for_text).wait(timeout=timeout)
774
- waited_time = time.time() - start_time
775
-
776
- if exists:
777
- return {
778
- "success": True,
779
- "waited": round(waited_time, 2),
780
- "message": f"文本'{wait_for_text}'已出现,等待了 {waited_time:.1f} 秒"
781
- }
782
- else:
783
- return {
784
- "success": False,
785
- "waited": timeout,
786
- "message": f"等待超时:文本'{wait_for_text}'未出现({timeout}秒)"
787
- }
788
-
789
- # 场景3: 等待元素ID出现
790
- elif wait_for_id:
791
- exists = self.client.u2(resourceId=wait_for_id).wait(timeout=timeout)
792
- waited_time = time.time() - start_time
793
-
794
- if exists:
795
- return {
796
- "success": True,
797
- "waited": round(waited_time, 2),
798
- "message": f"元素'{wait_for_id}'已出现,等待了 {waited_time:.1f} 秒"
799
- }
800
- else:
801
- return {
802
- "success": False,
803
- "waited": timeout,
804
- "message": f"等待超时:元素'{wait_for_id}'未出现({timeout}秒)"
805
- }
806
-
807
- else:
808
- return {
809
- "success": False,
810
- "error": "请指定等待条件:seconds, wait_for_text 或 wait_for_id"
811
- }
812
-
813
- except Exception as e:
814
- return {
815
- "success": False,
816
- "error": f"等待失败: {str(e)}"
817
- }
818
-
819
- def check_connection(self) -> Dict:
820
- """
821
- 检查设备连接状态
822
-
823
- Returns:
824
- 连接状态信息
825
- """
826
- try:
827
- # 尝试获取设备信息
828
- device_info = self.client.u2.device_info
829
- screen_size = self.client.u2.window_size()
830
-
831
- return {
832
- "success": True,
833
- "connected": True,
834
- "device_info": {
835
- "serial": device_info.get("serial", "unknown"),
836
- "brand": device_info.get("brand", "unknown"),
837
- "model": device_info.get("model", "unknown"),
838
- "version": device_info.get("version", "unknown"),
839
- "screen_size": f"{screen_size[0]}x{screen_size[1]}"
840
- },
841
- "message": "✅ 设备已连接"
842
- }
843
- except Exception as e:
844
- return {
845
- "success": False,
846
- "connected": False,
847
- "error": str(e),
848
- "message": f"❌ 设备未连接: {str(e)}"
849
- }
850
-
851
- def reconnect_device(self) -> Dict:
852
- """
853
- 重新连接设备
854
-
855
- Returns:
856
- 重连结果
857
- """
858
- try:
859
- # 通过 device_manager 重新连接,保持原有配置
860
- if hasattr(self.client, 'device_manager') and self.client.device_manager:
861
- self.client.u2 = self.client.device_manager.connect()
862
- else:
863
- # 降级方案:直接重连默认设备
864
- import uiautomator2 as u2
865
- self.client.u2 = u2.connect()
866
-
867
- # 验证连接
868
- device_info = self.client.u2.device_info
869
-
870
- return {
871
- "success": True,
872
- "device_info": {
873
- "serial": device_info.get("serial", "unknown"),
874
- "model": device_info.get("model", "unknown")
875
- },
876
- "message": f"✅ 重连成功: {device_info.get('brand', '')} {device_info.get('model', 'unknown')}"
877
- }
878
- except Exception as e:
879
- return {
880
- "success": False,
881
- "error": str(e),
882
- "message": f"❌ 重连失败: {str(e)}",
883
- "suggestion": "请检查设备USB连接或执行 'adb devices'"
884
- }
885
-
886
- def get_operation_history(self, limit: Optional[int] = None) -> Dict:
887
- """
888
- 获取操作历史记录
889
-
890
- Args:
891
- limit: 返回最近的N条记录,None表示全部
892
-
893
- Returns:
894
- 历史记录信息
895
- """
896
- try:
897
- history_manager = OperationHistoryManager()
898
- operations = history_manager.load(limit=limit) # load已经处理了limit
899
- statistics = history_manager.get_statistics()
900
-
901
- return {
902
- "success": True,
903
- "count": len(operations),
904
- "total": statistics.get("total", 0),
905
- "operations": operations, # 不需要再次切片
906
- "statistics": statistics,
907
- "message": f"✅ 获取到 {len(operations)} 条操作记录"
908
- }
909
- except Exception as e:
910
- return {
911
- "success": False,
912
- "error": str(e),
913
- "message": f"❌ 获取历史记录失败: {str(e)}"
914
- }
915
-
916
- def clear_operation_history(self) -> Dict:
917
- """
918
- 清空操作历史记录
919
-
920
- Returns:
921
- 清空结果
922
- """
923
- try:
924
- history_manager = OperationHistoryManager()
925
-
926
- # 获取清空前的统计
927
- old_stats = history_manager.get_statistics()
928
- old_count = old_stats.get("total", 0)
929
-
930
- # 清空历史
931
- history_manager.clear()
932
-
933
- return {
934
- "success": True,
935
- "cleared_count": old_count,
936
- "message": f"✅ 已清空 {old_count} 条操作记录"
937
- }
938
- except Exception as e:
939
- return {
940
- "success": False,
941
- "error": str(e),
942
- "message": f"❌ 清空历史记录失败: {str(e)}"
943
- }
944
-
945
-