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
mcp/mcp_server.py
DELETED
|
@@ -1,1919 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
Mobile MCP Server - 让 AI 助手通过自然语言控制 Android 手机
|
|
5
|
-
|
|
6
|
-
用法:
|
|
7
|
-
1. 在 Cursor 中配置 MCP Server
|
|
8
|
-
2. AI 可以直接调用 mobile_click("登录按钮") 等工具
|
|
9
|
-
3. 享受 Cursor AI 的智能能力!
|
|
10
|
-
|
|
11
|
-
配置 Cursor:
|
|
12
|
-
在项目根目录创建 .cursor/mcp.json:
|
|
13
|
-
{
|
|
14
|
-
"mcpServers": {
|
|
15
|
-
"mobile-automation": {
|
|
16
|
-
"command": "python",
|
|
17
|
-
"args": ["backend/mobile_mcp/mcp/mcp_server.py"],
|
|
18
|
-
"env": {
|
|
19
|
-
"PYTHONPATH": ".",
|
|
20
|
-
"MOBILE_DEVICE_ID": "auto"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
"""
|
|
26
|
-
import asyncio
|
|
27
|
-
import sys
|
|
28
|
-
import json
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
from typing import Any, Dict, Optional
|
|
31
|
-
|
|
32
|
-
# 添加项目根目录和backend目录到路径
|
|
33
|
-
# mcp_server.py现在在 mcp/ 目录下,所以需要向上2级到mobile_mcp目录
|
|
34
|
-
mobile_mcp_dir = Path(__file__).parent.parent # mobile_mcp目录
|
|
35
|
-
project_root = mobile_mcp_dir.parent.parent # 项目根目录
|
|
36
|
-
backend_dir = project_root / "backend"
|
|
37
|
-
|
|
38
|
-
# 先导入MCP SDK(在添加本地路径之前,避免本地mcp目录冲突)
|
|
39
|
-
# 临时清理sys.path,确保导入的是安装的mcp包而不是本地mcp目录
|
|
40
|
-
_original_sys_path = sys.path.copy()
|
|
41
|
-
_mobile_mcp_dir_str = str(mobile_mcp_dir)
|
|
42
|
-
_project_root_str = str(project_root)
|
|
43
|
-
_backend_dir_str = str(backend_dir)
|
|
44
|
-
|
|
45
|
-
# 只移除精确匹配的项目路径,保留所有其他路径(包括site-packages)
|
|
46
|
-
sys.path = [
|
|
47
|
-
p for p in sys.path
|
|
48
|
-
if p not in [_mobile_mcp_dir_str, _project_root_str, _backend_dir_str, '', '.']
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
from mcp.types import Tool, TextContent
|
|
53
|
-
from mcp.server import Server
|
|
54
|
-
from mcp.server.stdio import stdio_server
|
|
55
|
-
MCP_AVAILABLE = True
|
|
56
|
-
except ImportError as e:
|
|
57
|
-
print(f"⚠️ MCP SDK 未安装或导入失败: {e}", file=sys.stderr)
|
|
58
|
-
print("请运行: pip install mcp", file=sys.stderr)
|
|
59
|
-
MCP_AVAILABLE = False
|
|
60
|
-
sys.exit(1)
|
|
61
|
-
finally:
|
|
62
|
-
# 恢复原始路径
|
|
63
|
-
sys.path = _original_sys_path
|
|
64
|
-
|
|
65
|
-
# 现在添加本地路径(MCP SDK已导入,不会冲突)
|
|
66
|
-
sys.path.insert(0, str(project_root))
|
|
67
|
-
sys.path.insert(0, str(backend_dir))
|
|
68
|
-
|
|
69
|
-
from mobile_mcp.core.mobile_client import MobileClient
|
|
70
|
-
from mobile_mcp.core.locator.mobile_smart_locator import MobileSmartLocator
|
|
71
|
-
from mobile_mcp.config import Config
|
|
72
|
-
from mobile_mcp.core.ai.ai_platform_adapter import get_ai_adapter
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class MobileMCPServer:
|
|
76
|
-
"""Mobile MCP Server - 封装移动端自动化能力为 MCP Tools"""
|
|
77
|
-
|
|
78
|
-
def __init__(self):
|
|
79
|
-
"""初始化 MCP Server"""
|
|
80
|
-
self.client: Optional[MobileClient] = None
|
|
81
|
-
self.locator: Optional[MobileSmartLocator] = None
|
|
82
|
-
self._initialized = False
|
|
83
|
-
|
|
84
|
-
# AI平台适配器(可选)
|
|
85
|
-
self.ai_adapter = None
|
|
86
|
-
if Config.is_ai_enhancement_enabled():
|
|
87
|
-
try:
|
|
88
|
-
self.ai_adapter = get_ai_adapter()
|
|
89
|
-
platform_name = self.ai_adapter.get_platform_name()
|
|
90
|
-
print(f"✅ AI增强功能已启用: {platform_name}", file=sys.stderr)
|
|
91
|
-
except Exception as e:
|
|
92
|
-
print(f"⚠️ AI适配器初始化失败: {e}", file=sys.stderr)
|
|
93
|
-
if not Config.should_fallback_on_ai_failure():
|
|
94
|
-
raise
|
|
95
|
-
|
|
96
|
-
async def initialize(self):
|
|
97
|
-
"""延迟初始化(避免启动时连接设备)"""
|
|
98
|
-
if not self._initialized:
|
|
99
|
-
import os
|
|
100
|
-
from mobile_mcp.config import Config
|
|
101
|
-
|
|
102
|
-
device_id = os.environ.get("MOBILE_DEVICE_ID")
|
|
103
|
-
if device_id == "auto" or device_id is None:
|
|
104
|
-
device_id = None # 自动选择设备
|
|
105
|
-
|
|
106
|
-
# 🎯 根据配置选择平台
|
|
107
|
-
platform = os.environ.get("DEFAULT_PLATFORM", Config.DEFAULT_PLATFORM)
|
|
108
|
-
|
|
109
|
-
if platform == "ios":
|
|
110
|
-
# iOS平台
|
|
111
|
-
if not Config.IOS_SUPPORT_ENABLED:
|
|
112
|
-
raise RuntimeError("iOS支持未启用,请设置 IOS_SUPPORT_ENABLED=true")
|
|
113
|
-
from mobile_mcp.core.ios_client import IOSClient
|
|
114
|
-
self.client = IOSClient(device_id=device_id)
|
|
115
|
-
self.locator = None # iOS暂不支持智能定位器
|
|
116
|
-
print("✅ Mobile MCP Server 已初始化 (iOS)", file=sys.stderr)
|
|
117
|
-
else:
|
|
118
|
-
# Android平台(默认)- 只支持竖屏
|
|
119
|
-
self.client = MobileClient(device_id=device_id, platform="android", lock_orientation=True)
|
|
120
|
-
self.locator = MobileSmartLocator(self.client)
|
|
121
|
-
print("✅ Mobile MCP Server 已初始化 (Android, 竖屏模式)", file=sys.stderr)
|
|
122
|
-
|
|
123
|
-
self._initialized = True
|
|
124
|
-
|
|
125
|
-
def get_tools(self) -> list[Tool]:
|
|
126
|
-
"""定义所有可用的 MCP Tools(根据配置动态生成)"""
|
|
127
|
-
tools = [
|
|
128
|
-
Tool(
|
|
129
|
-
name="mobile_click",
|
|
130
|
-
description="点击手机屏幕上的元素(按钮、链接等)。使用自然语言描述元素,如'登录按钮'、'右上角设置图标'。如果定位失败,可以使用bounds坐标格式 '[x1,y1][x2,y2]' 直接点击。\n\n✨ 支持智能验证:自动检测页面变化,确保点击真的生效",
|
|
131
|
-
inputSchema={
|
|
132
|
-
"type": "object",
|
|
133
|
-
"properties": {
|
|
134
|
-
"element_desc": {
|
|
135
|
-
"type": "string",
|
|
136
|
-
"description": "元素描述(自然语言),如'登录按钮'、'提交'、'右上角返回'。或者bounds坐标格式 '[x1,y1][x2,y2]'"
|
|
137
|
-
},
|
|
138
|
-
"verify": {
|
|
139
|
-
"type": "boolean",
|
|
140
|
-
"description": "是否验证点击效果(默认true)。true=检测页面变化确保点击生效,false=快速模式不验证",
|
|
141
|
-
"default": True
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
"required": ["element_desc"]
|
|
145
|
-
}
|
|
146
|
-
),
|
|
147
|
-
Tool(
|
|
148
|
-
name="mobile_input",
|
|
149
|
-
description="在输入框中输入文本。先定位输入框,然后输入内容。\n\n✨ 支持智能验证:自动检查文本是否真的输入成功",
|
|
150
|
-
inputSchema={
|
|
151
|
-
"type": "object",
|
|
152
|
-
"properties": {
|
|
153
|
-
"element_desc": {
|
|
154
|
-
"type": "string",
|
|
155
|
-
"description": "输入框描述(自然语言),如'用户名输入框'、'搜索框'"
|
|
156
|
-
},
|
|
157
|
-
"text": {
|
|
158
|
-
"type": "string",
|
|
159
|
-
"description": "要输入的文本内容"
|
|
160
|
-
},
|
|
161
|
-
"verify": {
|
|
162
|
-
"type": "boolean",
|
|
163
|
-
"description": "是否验证输入效果(默认true)。true=检查文本是否正确输入,false=快速模式不验证",
|
|
164
|
-
"default": True
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
"required": ["element_desc", "text"]
|
|
168
|
-
}
|
|
169
|
-
),
|
|
170
|
-
Tool(
|
|
171
|
-
name="mobile_swipe",
|
|
172
|
-
description="滑动手机屏幕(上下左右)。\n\n✨ 支持智能验证:自动检测页面内容变化,确认滑动生效",
|
|
173
|
-
inputSchema={
|
|
174
|
-
"type": "object",
|
|
175
|
-
"properties": {
|
|
176
|
-
"direction": {
|
|
177
|
-
"type": "string",
|
|
178
|
-
"enum": ["up", "down", "left", "right"],
|
|
179
|
-
"description": "滑动方向:up(向上)、down(向下)、left(向左)、right(向右)"
|
|
180
|
-
},
|
|
181
|
-
"verify": {
|
|
182
|
-
"type": "boolean",
|
|
183
|
-
"description": "是否验证滑动效果(默认true)。true=检测页面内容变化,false=快速模式不验证",
|
|
184
|
-
"default": True
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
"required": ["direction"]
|
|
188
|
-
}
|
|
189
|
-
),
|
|
190
|
-
Tool(
|
|
191
|
-
name="mobile_press_key",
|
|
192
|
-
description="按键盘按键(支持智能验证)。支持Enter键、搜索键、返回键等。在搜索框输入后,可以使用此工具按搜索键执行搜索。\n\n✨ 新特性:\n- 自动验证按键效果(检测页面变化)\n- 搜索键智能回退(SEARCH无效时自动尝试ENTER)\n- 避免'假成功'问题",
|
|
193
|
-
inputSchema={
|
|
194
|
-
"type": "object",
|
|
195
|
-
"properties": {
|
|
196
|
-
"key": {
|
|
197
|
-
"type": "string",
|
|
198
|
-
"description": "按键名称:'enter'/'回车'(Enter键)、'search'/'搜索'(搜索键)、'back'/'返回'(返回键)、'home'(Home键),或直接使用keycode数字(如66=Enter, 84=Search)"
|
|
199
|
-
},
|
|
200
|
-
"verify": {
|
|
201
|
-
"type": "boolean",
|
|
202
|
-
"description": "是否验证按键效果(默认true)。true=检测页面变化确保按键生效,false=快速模式不验证",
|
|
203
|
-
"default": True
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
"required": ["key"]
|
|
207
|
-
}
|
|
208
|
-
),
|
|
209
|
-
Tool(
|
|
210
|
-
name="mobile_snapshot",
|
|
211
|
-
description="获取当前页面的结构信息(XML树、可点击元素列表等)。用于分析页面结构,帮助定位元素。",
|
|
212
|
-
inputSchema={
|
|
213
|
-
"type": "object",
|
|
214
|
-
"properties": {},
|
|
215
|
-
"required": []
|
|
216
|
-
}
|
|
217
|
-
),
|
|
218
|
-
Tool(
|
|
219
|
-
name="mobile_launch_app",
|
|
220
|
-
description="启动指定的 Android 应用。",
|
|
221
|
-
inputSchema={
|
|
222
|
-
"type": "object",
|
|
223
|
-
"properties": {
|
|
224
|
-
"package_name": {
|
|
225
|
-
"type": "string",
|
|
226
|
-
"description": "应用包名,如 'com.im30.mind'"
|
|
227
|
-
},
|
|
228
|
-
"wait_time": {
|
|
229
|
-
"type": "number",
|
|
230
|
-
"description": "等待应用启动的时间(秒),默认3秒",
|
|
231
|
-
"default": 3
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
"required": ["package_name"]
|
|
235
|
-
}
|
|
236
|
-
),
|
|
237
|
-
Tool(
|
|
238
|
-
name="mobile_assert_text",
|
|
239
|
-
description="断言页面中是否包含指定文本。用于验证操作结果。",
|
|
240
|
-
inputSchema={
|
|
241
|
-
"type": "object",
|
|
242
|
-
"properties": {
|
|
243
|
-
"text": {
|
|
244
|
-
"type": "string",
|
|
245
|
-
"description": "要检查的文本内容"
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
"required": ["text"]
|
|
249
|
-
}
|
|
250
|
-
),
|
|
251
|
-
Tool(
|
|
252
|
-
name="mobile_get_current_package",
|
|
253
|
-
description="获取当前前台应用的包名。用于确认当前在哪个应用。",
|
|
254
|
-
inputSchema={
|
|
255
|
-
"type": "object",
|
|
256
|
-
"properties": {},
|
|
257
|
-
"required": []
|
|
258
|
-
}
|
|
259
|
-
),
|
|
260
|
-
Tool(
|
|
261
|
-
name="mobile_list_devices",
|
|
262
|
-
description="列出所有连接的Android设备。返回设备ID和状态信息。",
|
|
263
|
-
inputSchema={
|
|
264
|
-
"type": "object",
|
|
265
|
-
"properties": {},
|
|
266
|
-
"required": []
|
|
267
|
-
}
|
|
268
|
-
),
|
|
269
|
-
Tool(
|
|
270
|
-
name="mobile_get_screen_size",
|
|
271
|
-
description="获取设备的屏幕尺寸(宽度和高度,单位:像素)。",
|
|
272
|
-
inputSchema={
|
|
273
|
-
"type": "object",
|
|
274
|
-
"properties": {},
|
|
275
|
-
"required": []
|
|
276
|
-
}
|
|
277
|
-
),
|
|
278
|
-
Tool(
|
|
279
|
-
name="mobile_get_orientation",
|
|
280
|
-
description="获取当前屏幕方向(portrait=竖屏,landscape=横屏)。",
|
|
281
|
-
inputSchema={
|
|
282
|
-
"type": "object",
|
|
283
|
-
"properties": {},
|
|
284
|
-
"required": []
|
|
285
|
-
}
|
|
286
|
-
),
|
|
287
|
-
Tool(
|
|
288
|
-
name="mobile_set_orientation",
|
|
289
|
-
description="设置屏幕方向。",
|
|
290
|
-
inputSchema={
|
|
291
|
-
"type": "object",
|
|
292
|
-
"properties": {
|
|
293
|
-
"orientation": {
|
|
294
|
-
"type": "string",
|
|
295
|
-
"enum": ["portrait", "landscape"],
|
|
296
|
-
"description": "屏幕方向:portrait(竖屏) 或 landscape(横屏)"
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
"required": ["orientation"]
|
|
300
|
-
}
|
|
301
|
-
),
|
|
302
|
-
Tool(
|
|
303
|
-
name="mobile_list_apps",
|
|
304
|
-
description="列出设备上已安装的应用。可以按包名过滤。",
|
|
305
|
-
inputSchema={
|
|
306
|
-
"type": "object",
|
|
307
|
-
"properties": {
|
|
308
|
-
"filter": {
|
|
309
|
-
"type": "string",
|
|
310
|
-
"description": "过滤关键词(可选),如包名或应用名"
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
"required": []
|
|
314
|
-
}
|
|
315
|
-
),
|
|
316
|
-
Tool(
|
|
317
|
-
name="mobile_install_app",
|
|
318
|
-
description="安装应用(从APK文件)。",
|
|
319
|
-
inputSchema={
|
|
320
|
-
"type": "object",
|
|
321
|
-
"properties": {
|
|
322
|
-
"apk_path": {
|
|
323
|
-
"type": "string",
|
|
324
|
-
"description": "APK文件路径"
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
"required": ["apk_path"]
|
|
328
|
-
}
|
|
329
|
-
),
|
|
330
|
-
Tool(
|
|
331
|
-
name="mobile_uninstall_app",
|
|
332
|
-
description="卸载应用(通过包名)。",
|
|
333
|
-
inputSchema={
|
|
334
|
-
"type": "object",
|
|
335
|
-
"properties": {
|
|
336
|
-
"package_name": {
|
|
337
|
-
"type": "string",
|
|
338
|
-
"description": "应用包名,如 'com.example.app'"
|
|
339
|
-
}
|
|
340
|
-
},
|
|
341
|
-
"required": ["package_name"]
|
|
342
|
-
}
|
|
343
|
-
),
|
|
344
|
-
Tool(
|
|
345
|
-
name="mobile_terminate_app",
|
|
346
|
-
description="终止应用(通过包名)。",
|
|
347
|
-
inputSchema={
|
|
348
|
-
"type": "object",
|
|
349
|
-
"properties": {
|
|
350
|
-
"package_name": {
|
|
351
|
-
"type": "string",
|
|
352
|
-
"description": "应用包名,如 'com.example.app'"
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
"required": ["package_name"]
|
|
356
|
-
}
|
|
357
|
-
),
|
|
358
|
-
Tool(
|
|
359
|
-
name="mobile_double_click",
|
|
360
|
-
description="双击屏幕上的元素。",
|
|
361
|
-
inputSchema={
|
|
362
|
-
"type": "object",
|
|
363
|
-
"properties": {
|
|
364
|
-
"element_desc": {
|
|
365
|
-
"type": "string",
|
|
366
|
-
"description": "元素描述(自然语言),如'头像'、'图片'"
|
|
367
|
-
},
|
|
368
|
-
"x": {
|
|
369
|
-
"type": "number",
|
|
370
|
-
"description": "X坐标(可选,如果提供则直接点击坐标)"
|
|
371
|
-
},
|
|
372
|
-
"y": {
|
|
373
|
-
"type": "number",
|
|
374
|
-
"description": "Y坐标(可选,如果提供则直接点击坐标)"
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
"required": []
|
|
378
|
-
}
|
|
379
|
-
),
|
|
380
|
-
Tool(
|
|
381
|
-
name="mobile_long_press",
|
|
382
|
-
description="长按屏幕上的元素。",
|
|
383
|
-
inputSchema={
|
|
384
|
-
"type": "object",
|
|
385
|
-
"properties": {
|
|
386
|
-
"element_desc": {
|
|
387
|
-
"type": "string",
|
|
388
|
-
"description": "元素描述(自然语言),如'删除按钮'、'菜单项'"
|
|
389
|
-
},
|
|
390
|
-
"duration": {
|
|
391
|
-
"type": "number",
|
|
392
|
-
"description": "长按持续时间(秒),默认1秒",
|
|
393
|
-
"default": 1.0
|
|
394
|
-
},
|
|
395
|
-
"x": {
|
|
396
|
-
"type": "number",
|
|
397
|
-
"description": "X坐标(可选,如果提供则直接长按坐标)"
|
|
398
|
-
},
|
|
399
|
-
"y": {
|
|
400
|
-
"type": "number",
|
|
401
|
-
"description": "Y坐标(可选,如果提供则直接长按坐标)"
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
"required": []
|
|
405
|
-
}
|
|
406
|
-
),
|
|
407
|
-
Tool(
|
|
408
|
-
name="mobile_open_url",
|
|
409
|
-
description="在设备浏览器中打开URL。",
|
|
410
|
-
inputSchema={
|
|
411
|
-
"type": "object",
|
|
412
|
-
"properties": {
|
|
413
|
-
"url": {
|
|
414
|
-
"type": "string",
|
|
415
|
-
"description": "要打开的URL,如 'https://example.com'"
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
"required": ["url"]
|
|
419
|
-
}
|
|
420
|
-
),
|
|
421
|
-
Tool(
|
|
422
|
-
name="mobile_take_screenshot",
|
|
423
|
-
description="截图并保存,返回截图路径。用于视觉识别分析。",
|
|
424
|
-
inputSchema={
|
|
425
|
-
"type": "object",
|
|
426
|
-
"properties": {
|
|
427
|
-
"save_path": {
|
|
428
|
-
"type": "string",
|
|
429
|
-
"description": "截图保存路径(可选,默认保存到项目screenshots目录)"
|
|
430
|
-
}
|
|
431
|
-
},
|
|
432
|
-
"required": []
|
|
433
|
-
}
|
|
434
|
-
),
|
|
435
|
-
Tool(
|
|
436
|
-
name="mobile_generate_test_script",
|
|
437
|
-
description="基于操作历史生成pytest格式的测试脚本。使用已验证的定位方式(坐标、bounds等),确保生成的脚本100%可执行。生成的脚本支持pytest批量执行和allure报告生成。脚本默认保存到当前工作目录的tests子目录。",
|
|
438
|
-
inputSchema={
|
|
439
|
-
"type": "object",
|
|
440
|
-
"properties": {
|
|
441
|
-
"test_name": {
|
|
442
|
-
"type": "string",
|
|
443
|
-
"description": "测试用例名称,如'建议发帖测试'"
|
|
444
|
-
},
|
|
445
|
-
"package_name": {
|
|
446
|
-
"type": "string",
|
|
447
|
-
"description": "App包名,如'com.im30.way'"
|
|
448
|
-
},
|
|
449
|
-
"filename": {
|
|
450
|
-
"type": "string",
|
|
451
|
-
"description": "生成的脚本文件名(不含.py后缀),如'test_建议发帖'"
|
|
452
|
-
},
|
|
453
|
-
"output_dir": {
|
|
454
|
-
"type": "string",
|
|
455
|
-
"description": "输出目录路径(可选)。默认为当前工作目录的tests子目录"
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
"required": ["test_name", "package_name", "filename"]
|
|
459
|
-
}
|
|
460
|
-
),
|
|
461
|
-
Tool(
|
|
462
|
-
name="mobile_analyze_screenshot",
|
|
463
|
-
description="分析截图并返回元素坐标。使用AI多模态能力分析截图,找到指定元素并返回坐标。支持自动模式(通过request_id)和手动模式(直接提供screenshot_path)。需要AI增强功能支持。",
|
|
464
|
-
inputSchema={
|
|
465
|
-
"type": "object",
|
|
466
|
-
"properties": {
|
|
467
|
-
"screenshot_path": {
|
|
468
|
-
"type": "string",
|
|
469
|
-
"description": "截图文件路径(手动模式)"
|
|
470
|
-
},
|
|
471
|
-
"element_desc": {
|
|
472
|
-
"type": "string",
|
|
473
|
-
"description": "要查找的元素描述(自然语言),如'设置按钮'、'语言选项'、'保存按钮'"
|
|
474
|
-
},
|
|
475
|
-
"request_id": {
|
|
476
|
-
"type": "string",
|
|
477
|
-
"description": "请求ID(自动模式),从请求文件中读取截图路径和元素描述"
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
"required": []
|
|
481
|
-
}
|
|
482
|
-
),
|
|
483
|
-
Tool(
|
|
484
|
-
name="mobile_execute_test_case",
|
|
485
|
-
description="智能执行测试用例。AI会自动规划、执行、验证每一步操作,遇到问题自动分析解决,找不到元素时自动截图分析,自动判断操作是否成功(通过页面元素变化)。需要AI增强功能支持。",
|
|
486
|
-
inputSchema={
|
|
487
|
-
"type": "object",
|
|
488
|
-
"properties": {
|
|
489
|
-
"test_description": {
|
|
490
|
-
"type": "string",
|
|
491
|
-
"description": "自然语言描述的测试用例,如:'打开 com.im30.mind\n点击底部云文档\n点击我的空间'"
|
|
492
|
-
}
|
|
493
|
-
},
|
|
494
|
-
"required": ["test_description"]
|
|
495
|
-
}
|
|
496
|
-
)
|
|
497
|
-
]
|
|
498
|
-
|
|
499
|
-
# 🎯 AI增强工具(可选,根据配置和平台能力动态添加)
|
|
500
|
-
if self.ai_adapter and self.ai_adapter.is_vision_available():
|
|
501
|
-
# 更新视觉识别工具的描述,使用检测到的平台名称
|
|
502
|
-
platform_name = self.ai_adapter.get_platform_name()
|
|
503
|
-
|
|
504
|
-
# 更新 mobile_analyze_screenshot 工具描述
|
|
505
|
-
for tool in tools:
|
|
506
|
-
if tool.name == "mobile_analyze_screenshot":
|
|
507
|
-
tool.description = f"分析截图并返回元素坐标。使用{platform_name}的多模态能力分析截图,找到指定元素并返回坐标。支持自动模式(通过request_id)和手动模式(直接提供screenshot_path)。"
|
|
508
|
-
break
|
|
509
|
-
|
|
510
|
-
# 如果没有AI平台,移除需要AI视觉能力的工具
|
|
511
|
-
# 注意:mobile_generate_test_script 不需要AI,只是基于操作历史生成脚本,所以保留
|
|
512
|
-
if not self.ai_adapter or not self.ai_adapter.is_vision_available():
|
|
513
|
-
tools = [t for t in tools if t.name not in [
|
|
514
|
-
"mobile_analyze_screenshot",
|
|
515
|
-
"mobile_execute_test_case"
|
|
516
|
-
]]
|
|
517
|
-
if Config.is_ai_enhancement_enabled():
|
|
518
|
-
print("⚠️ AI视觉增强工具已禁用(未检测到可用的AI平台)", file=sys.stderr)
|
|
519
|
-
|
|
520
|
-
return tools
|
|
521
|
-
|
|
522
|
-
async def handle_mobile_click(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
523
|
-
"""处理点击操作"""
|
|
524
|
-
await self.initialize()
|
|
525
|
-
element_desc = arguments.get("element_desc")
|
|
526
|
-
verify = arguments.get("verify", True) # 默认启用验证
|
|
527
|
-
|
|
528
|
-
try:
|
|
529
|
-
result = None # 初始化result变量
|
|
530
|
-
# 🎯 检查是否是bounds坐标格式 "[x1,y1][x2,y2]"
|
|
531
|
-
if element_desc.startswith('[') and '][' in element_desc:
|
|
532
|
-
# 直接使用bounds坐标点击
|
|
533
|
-
print(f" 📍 检测到bounds坐标格式,直接使用坐标点击: {element_desc}", file=sys.stderr)
|
|
534
|
-
click_result = await self.client.click(
|
|
535
|
-
element_desc,
|
|
536
|
-
ref=element_desc,
|
|
537
|
-
verify=verify
|
|
538
|
-
)
|
|
539
|
-
result = {'method': 'bounds', 'ref': element_desc} # 设置result用于后续使用
|
|
540
|
-
else:
|
|
541
|
-
# 使用智能定位器定位元素
|
|
542
|
-
result = await self.locator.locate(element_desc)
|
|
543
|
-
if not result:
|
|
544
|
-
# 🎯 定位失败时,自动使用Cursor AI视觉识别
|
|
545
|
-
# 检查是否有待分析的请求文件
|
|
546
|
-
from pathlib import Path
|
|
547
|
-
project_root = Path(__file__).parent.parent
|
|
548
|
-
request_dir = project_root / "screenshots" / "requests"
|
|
549
|
-
if request_dir.exists():
|
|
550
|
-
# 查找最新的请求文件
|
|
551
|
-
request_files = sorted(request_dir.glob("request_*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
552
|
-
if request_files:
|
|
553
|
-
latest_request = request_files[0]
|
|
554
|
-
try:
|
|
555
|
-
import json as json_lib
|
|
556
|
-
with open(latest_request, 'r', encoding='utf-8') as f:
|
|
557
|
-
request_data = json_lib.load(f)
|
|
558
|
-
if request_data.get('element_desc') == element_desc and request_data.get('status') == 'pending':
|
|
559
|
-
# 🎯 自动调用Cursor AI分析
|
|
560
|
-
request_id = request_data.get('request_id')
|
|
561
|
-
print(f" 🎯 检测到待分析的请求文件,自动调用Cursor AI分析: request_id={request_id}", file=sys.stderr)
|
|
562
|
-
# 调用mobile_analyze_screenshot工具
|
|
563
|
-
analyze_result = await self.handle_mobile_analyze_screenshot({
|
|
564
|
-
"request_id": request_id
|
|
565
|
-
})
|
|
566
|
-
# 检查分析结果
|
|
567
|
-
if analyze_result and len(analyze_result) > 0:
|
|
568
|
-
analyze_text = analyze_result[0].text
|
|
569
|
-
analyze_data = json_lib.loads(analyze_text)
|
|
570
|
-
if analyze_data.get('success') and analyze_data.get('coordinate'):
|
|
571
|
-
# ✅ Cursor AI分析成功,重新定位
|
|
572
|
-
coord = analyze_data['coordinate']
|
|
573
|
-
ref = f"vision_coord_{coord['x']}_{coord['y']}"
|
|
574
|
-
click_result = await self.client.click(
|
|
575
|
-
element_desc,
|
|
576
|
-
ref=ref,
|
|
577
|
-
verify=verify
|
|
578
|
-
)
|
|
579
|
-
if click_result.get('success'):
|
|
580
|
-
return [TextContent(
|
|
581
|
-
type="text",
|
|
582
|
-
text=json.dumps({
|
|
583
|
-
"success": True,
|
|
584
|
-
"element": element_desc,
|
|
585
|
-
"method": "cursor_vision_auto",
|
|
586
|
-
"message": f"成功点击: {element_desc}(通过Cursor AI自动分析)"
|
|
587
|
-
}, ensure_ascii=False, indent=2)
|
|
588
|
-
)]
|
|
589
|
-
except Exception as e:
|
|
590
|
-
print(f" ⚠️ 自动分析失败: {e}", file=sys.stderr)
|
|
591
|
-
|
|
592
|
-
# 如果自动分析失败或没有请求文件,返回错误
|
|
593
|
-
return [TextContent(
|
|
594
|
-
type="text",
|
|
595
|
-
text=json.dumps({
|
|
596
|
-
"success": False,
|
|
597
|
-
"error": f"未找到元素: {element_desc}",
|
|
598
|
-
"suggestion": "尝试使用 mobile_snapshot 查看页面结构,或使用 mobile_take_screenshot 截图后使用 mobile_analyze_screenshot 分析,或直接使用bounds坐标格式 '[x1,y1][x2,y2]'"
|
|
599
|
-
}, ensure_ascii=False, indent=2)
|
|
600
|
-
)]
|
|
601
|
-
|
|
602
|
-
# 🎯 记录定位结果(用于调试)
|
|
603
|
-
ref = result.get('ref', '')
|
|
604
|
-
method = result.get('method', 'unknown')
|
|
605
|
-
print(f" 📍 定位结果: {element_desc} -> ref={ref}, method={method}", file=sys.stderr)
|
|
606
|
-
|
|
607
|
-
# 🎯 检查是否是待分析的Cursor AI视觉识别请求
|
|
608
|
-
if method == 'cursor_vision_pending' and result.get('status') == 'pending_analysis':
|
|
609
|
-
request_id = result.get('request_id')
|
|
610
|
-
screenshot_path = result.get('screenshot_path')
|
|
611
|
-
print(f" 🎯 检测到待分析的Cursor AI请求,自动调用分析工具: request_id={request_id}", file=sys.stderr)
|
|
612
|
-
|
|
613
|
-
# 自动调用mobile_analyze_screenshot工具
|
|
614
|
-
analyze_result = await self.handle_mobile_analyze_screenshot({
|
|
615
|
-
"request_id": request_id
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
# 检查分析结果
|
|
619
|
-
if analyze_result and len(analyze_result) > 0:
|
|
620
|
-
analyze_text = analyze_result[0].text
|
|
621
|
-
analyze_data = json.loads(analyze_text)
|
|
622
|
-
if analyze_data.get('success') and analyze_data.get('coordinate'):
|
|
623
|
-
# ✅ Cursor AI分析成功,使用坐标点击
|
|
624
|
-
coord = analyze_data['coordinate']
|
|
625
|
-
ref = f"vision_coord_{coord['x']}_{coord['y']}"
|
|
626
|
-
click_result = await self.client.click(
|
|
627
|
-
element_desc,
|
|
628
|
-
ref=ref,
|
|
629
|
-
verify=verify
|
|
630
|
-
)
|
|
631
|
-
if click_result.get('success'):
|
|
632
|
-
return [TextContent(
|
|
633
|
-
type="text",
|
|
634
|
-
text=json.dumps({
|
|
635
|
-
"success": True,
|
|
636
|
-
"element": element_desc,
|
|
637
|
-
"method": "cursor_vision_auto",
|
|
638
|
-
"message": f"成功点击: {element_desc}(通过Cursor AI自动分析)",
|
|
639
|
-
"coordinate": coord
|
|
640
|
-
}, ensure_ascii=False, indent=2)
|
|
641
|
-
)]
|
|
642
|
-
|
|
643
|
-
# 如果分析失败,返回错误
|
|
644
|
-
return [TextContent(
|
|
645
|
-
type="text",
|
|
646
|
-
text=json.dumps({
|
|
647
|
-
"success": False,
|
|
648
|
-
"error": f"Cursor AI分析失败: {element_desc}",
|
|
649
|
-
"screenshot_path": screenshot_path,
|
|
650
|
-
"request_id": request_id
|
|
651
|
-
}, ensure_ascii=False, indent=2)
|
|
652
|
-
)]
|
|
653
|
-
|
|
654
|
-
# 执行点击
|
|
655
|
-
click_result = await self.client.click(
|
|
656
|
-
element_desc,
|
|
657
|
-
ref=ref,
|
|
658
|
-
verify=verify
|
|
659
|
-
)
|
|
660
|
-
|
|
661
|
-
if click_result.get('success'):
|
|
662
|
-
# 构建详细的成功消息
|
|
663
|
-
response = {
|
|
664
|
-
"success": True,
|
|
665
|
-
"element": element_desc,
|
|
666
|
-
"method": result.get('method', 'unknown') if result else 'bounds',
|
|
667
|
-
"verified": click_result.get('verified', False),
|
|
668
|
-
"message": f"成功点击: {element_desc}"
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
# 添加验证相关信息
|
|
672
|
-
if click_result.get('verified'):
|
|
673
|
-
response['page_changed'] = click_result.get('page_changed', False)
|
|
674
|
-
if click_result.get('warning'):
|
|
675
|
-
response['warning'] = click_result.get('warning')
|
|
676
|
-
|
|
677
|
-
return [TextContent(
|
|
678
|
-
type="text",
|
|
679
|
-
text=json.dumps(response, ensure_ascii=False, indent=2)
|
|
680
|
-
)]
|
|
681
|
-
else:
|
|
682
|
-
return [TextContent(
|
|
683
|
-
type="text",
|
|
684
|
-
text=json.dumps({
|
|
685
|
-
"success": False,
|
|
686
|
-
"error": click_result.get('reason', '点击失败'),
|
|
687
|
-
"element": element_desc
|
|
688
|
-
}, ensure_ascii=False, indent=2)
|
|
689
|
-
)]
|
|
690
|
-
except Exception as e:
|
|
691
|
-
return [TextContent(
|
|
692
|
-
type="text",
|
|
693
|
-
text=json.dumps({
|
|
694
|
-
"success": False,
|
|
695
|
-
"error": f"点击异常: {str(e)}"
|
|
696
|
-
}, ensure_ascii=False, indent=2)
|
|
697
|
-
)]
|
|
698
|
-
|
|
699
|
-
async def handle_mobile_input(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
700
|
-
"""处理输入操作(支持智能验证)"""
|
|
701
|
-
await self.initialize()
|
|
702
|
-
element_desc = arguments.get("element_desc")
|
|
703
|
-
text = arguments.get("text")
|
|
704
|
-
verify = arguments.get("verify", True) # 默认启用验证
|
|
705
|
-
|
|
706
|
-
try:
|
|
707
|
-
# 🎯 检查是否是bounds坐标格式 "[x1,y1][x2,y2]"
|
|
708
|
-
if element_desc.startswith('[') and '][' in element_desc:
|
|
709
|
-
# 直接使用bounds坐标输入
|
|
710
|
-
print(f" 📍 检测到bounds坐标格式,直接使用坐标输入: {element_desc}", file=sys.stderr)
|
|
711
|
-
input_result = await self.client.type_text(element_desc, text, ref=element_desc, verify=verify)
|
|
712
|
-
else:
|
|
713
|
-
# 定位输入框
|
|
714
|
-
result = await self.locator.locate(element_desc)
|
|
715
|
-
if not result:
|
|
716
|
-
return [TextContent(
|
|
717
|
-
type="text",
|
|
718
|
-
text=json.dumps({
|
|
719
|
-
"success": False,
|
|
720
|
-
"error": f"未找到输入框: {element_desc}",
|
|
721
|
-
"suggestion": "尝试使用bounds坐标格式 '[x1,y1][x2,y2]' 直接输入"
|
|
722
|
-
}, ensure_ascii=False, indent=2)
|
|
723
|
-
)]
|
|
724
|
-
|
|
725
|
-
# 执行输入
|
|
726
|
-
input_result = await self.client.type_text(element_desc, text, ref=result['ref'], verify=verify)
|
|
727
|
-
|
|
728
|
-
# 🎯 修复:检查输入结果
|
|
729
|
-
if not input_result.get('success'):
|
|
730
|
-
return [TextContent(
|
|
731
|
-
type="text",
|
|
732
|
-
text=json.dumps({
|
|
733
|
-
"success": False,
|
|
734
|
-
"error": input_result.get('reason', '输入失败'),
|
|
735
|
-
"element": element_desc,
|
|
736
|
-
"text": text
|
|
737
|
-
}, ensure_ascii=False, indent=2)
|
|
738
|
-
)]
|
|
739
|
-
|
|
740
|
-
# 构建详细的成功消息
|
|
741
|
-
response = {
|
|
742
|
-
"success": True,
|
|
743
|
-
"element": element_desc,
|
|
744
|
-
"text": text,
|
|
745
|
-
"verified": input_result.get('verified', False),
|
|
746
|
-
"message": f"成功在 {element_desc} 中输入: {text}"
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
# 添加验证相关信息
|
|
750
|
-
if input_result.get('verified'):
|
|
751
|
-
response['input_verified'] = input_result.get('input_verified', False)
|
|
752
|
-
if input_result.get('actual_text'):
|
|
753
|
-
response['actual_text'] = input_result.get('actual_text')
|
|
754
|
-
if input_result.get('warning'):
|
|
755
|
-
response['warning'] = input_result.get('warning')
|
|
756
|
-
|
|
757
|
-
return [TextContent(
|
|
758
|
-
type="text",
|
|
759
|
-
text=json.dumps(response, ensure_ascii=False, indent=2)
|
|
760
|
-
)]
|
|
761
|
-
except Exception as e:
|
|
762
|
-
return [TextContent(
|
|
763
|
-
type="text",
|
|
764
|
-
text=json.dumps({
|
|
765
|
-
"success": False,
|
|
766
|
-
"error": f"输入异常: {str(e)}"
|
|
767
|
-
}, ensure_ascii=False, indent=2)
|
|
768
|
-
)]
|
|
769
|
-
|
|
770
|
-
async def handle_mobile_swipe(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
771
|
-
"""处理滑动操作(支持智能验证)"""
|
|
772
|
-
await self.initialize()
|
|
773
|
-
direction = arguments.get("direction")
|
|
774
|
-
verify = arguments.get("verify", True) # 默认启用验证
|
|
775
|
-
|
|
776
|
-
try:
|
|
777
|
-
result = await self.client.swipe(direction, verify=verify)
|
|
778
|
-
if result.get('success'):
|
|
779
|
-
# 构建详细的成功消息
|
|
780
|
-
response = {
|
|
781
|
-
"success": True,
|
|
782
|
-
"direction": direction,
|
|
783
|
-
"verified": result.get('verified', False),
|
|
784
|
-
"message": f"成功滑动: {direction}"
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
# 添加验证相关信息
|
|
788
|
-
if result.get('verified'):
|
|
789
|
-
response['page_changed'] = result.get('page_changed', False)
|
|
790
|
-
if result.get('warning'):
|
|
791
|
-
response['warning'] = result.get('warning')
|
|
792
|
-
|
|
793
|
-
return [TextContent(
|
|
794
|
-
type="text",
|
|
795
|
-
text=json.dumps(response, ensure_ascii=False, indent=2)
|
|
796
|
-
)]
|
|
797
|
-
else:
|
|
798
|
-
return [TextContent(
|
|
799
|
-
type="text",
|
|
800
|
-
text=json.dumps({
|
|
801
|
-
"success": False,
|
|
802
|
-
"error": result.get('reason', '滑动失败')
|
|
803
|
-
}, ensure_ascii=False, indent=2)
|
|
804
|
-
)]
|
|
805
|
-
except Exception as e:
|
|
806
|
-
return [TextContent(
|
|
807
|
-
type="text",
|
|
808
|
-
text=json.dumps({
|
|
809
|
-
"success": False,
|
|
810
|
-
"error": f"滑动异常: {str(e)}"
|
|
811
|
-
}, ensure_ascii=False, indent=2)
|
|
812
|
-
)]
|
|
813
|
-
|
|
814
|
-
async def handle_mobile_press_key(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
815
|
-
"""处理按键操作(支持智能验证)"""
|
|
816
|
-
await self.initialize()
|
|
817
|
-
key = arguments.get("key")
|
|
818
|
-
verify = arguments.get("verify", True) # 默认启用验证
|
|
819
|
-
|
|
820
|
-
try:
|
|
821
|
-
result = await self.client.press_key(key, verify=verify)
|
|
822
|
-
if result.get('success'):
|
|
823
|
-
# 构建详细的成功消息
|
|
824
|
-
response = {
|
|
825
|
-
"success": True,
|
|
826
|
-
"key": key,
|
|
827
|
-
"keycode": result.get('keycode'),
|
|
828
|
-
"verified": result.get('verified', False),
|
|
829
|
-
"message": result.get('message', f"成功按键: {key}")
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
# 添加验证相关信息
|
|
833
|
-
if result.get('verified'):
|
|
834
|
-
response['page_changed'] = result.get('page_changed', False)
|
|
835
|
-
if result.get('fallback_used') is not None:
|
|
836
|
-
response['fallback_used'] = result.get('fallback_used')
|
|
837
|
-
if result.get('fallback_used'):
|
|
838
|
-
response['note'] = "SEARCH键无效,已自动使用ENTER键替代"
|
|
839
|
-
|
|
840
|
-
return [TextContent(
|
|
841
|
-
type="text",
|
|
842
|
-
text=json.dumps(response, ensure_ascii=False, indent=2)
|
|
843
|
-
)]
|
|
844
|
-
else:
|
|
845
|
-
# 构建详细的失败消息
|
|
846
|
-
error_response = {
|
|
847
|
-
"success": False,
|
|
848
|
-
"key": key,
|
|
849
|
-
"error": result.get('reason') or result.get('message', '按键失败'),
|
|
850
|
-
"verified": result.get('verified', False)
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
# 添加调试信息
|
|
854
|
-
if result.get('page_changed') is not None:
|
|
855
|
-
error_response['page_changed'] = result.get('page_changed')
|
|
856
|
-
if result.get('message'):
|
|
857
|
-
error_response['detail'] = result.get('message')
|
|
858
|
-
|
|
859
|
-
return [TextContent(
|
|
860
|
-
type="text",
|
|
861
|
-
text=json.dumps(error_response, ensure_ascii=False, indent=2)
|
|
862
|
-
)]
|
|
863
|
-
except Exception as e:
|
|
864
|
-
return [TextContent(
|
|
865
|
-
type="text",
|
|
866
|
-
text=json.dumps({
|
|
867
|
-
"success": False,
|
|
868
|
-
"error": f"按键异常: {str(e)}"
|
|
869
|
-
}, ensure_ascii=False, indent=2)
|
|
870
|
-
)]
|
|
871
|
-
|
|
872
|
-
async def handle_mobile_snapshot(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
873
|
-
"""获取页面快照"""
|
|
874
|
-
await self.initialize()
|
|
875
|
-
|
|
876
|
-
try:
|
|
877
|
-
# client.snapshot() 已经返回格式化后的字符串,不需要再次格式化
|
|
878
|
-
snapshot = await self.client.snapshot()
|
|
879
|
-
|
|
880
|
-
return [TextContent(
|
|
881
|
-
type="text",
|
|
882
|
-
text=json.dumps({
|
|
883
|
-
"success": True,
|
|
884
|
-
"snapshot": snapshot,
|
|
885
|
-
"message": "页面结构已获取"
|
|
886
|
-
}, ensure_ascii=False, indent=2)
|
|
887
|
-
)]
|
|
888
|
-
except Exception as e:
|
|
889
|
-
return [TextContent(
|
|
890
|
-
type="text",
|
|
891
|
-
text=json.dumps({
|
|
892
|
-
"success": False,
|
|
893
|
-
"error": f"获取快照异常: {str(e)}"
|
|
894
|
-
}, ensure_ascii=False, indent=2)
|
|
895
|
-
)]
|
|
896
|
-
|
|
897
|
-
async def handle_mobile_launch_app(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
898
|
-
"""启动应用"""
|
|
899
|
-
await self.initialize()
|
|
900
|
-
package_name = arguments.get("package_name")
|
|
901
|
-
wait_time = arguments.get("wait_time", 3)
|
|
902
|
-
|
|
903
|
-
try:
|
|
904
|
-
await self.client.launch_app(package_name, wait_time=wait_time)
|
|
905
|
-
return [TextContent(
|
|
906
|
-
type="text",
|
|
907
|
-
text=json.dumps({
|
|
908
|
-
"success": True,
|
|
909
|
-
"package": package_name,
|
|
910
|
-
"message": f"成功启动应用: {package_name}"
|
|
911
|
-
}, ensure_ascii=False, indent=2)
|
|
912
|
-
)]
|
|
913
|
-
except Exception as e:
|
|
914
|
-
return [TextContent(
|
|
915
|
-
type="text",
|
|
916
|
-
text=json.dumps({
|
|
917
|
-
"success": False,
|
|
918
|
-
"error": f"启动应用异常: {str(e)}"
|
|
919
|
-
}, ensure_ascii=False, indent=2)
|
|
920
|
-
)]
|
|
921
|
-
|
|
922
|
-
async def handle_mobile_assert_text(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
923
|
-
"""断言文本"""
|
|
924
|
-
await self.initialize()
|
|
925
|
-
text = arguments.get("text")
|
|
926
|
-
|
|
927
|
-
try:
|
|
928
|
-
snapshot = await self.client.snapshot()
|
|
929
|
-
found = text in snapshot
|
|
930
|
-
|
|
931
|
-
return [TextContent(
|
|
932
|
-
type="text",
|
|
933
|
-
text=json.dumps({
|
|
934
|
-
"success": found,
|
|
935
|
-
"text": text,
|
|
936
|
-
"found": found,
|
|
937
|
-
"message": f"文本 '{text}' {'已找到' if found else '未找到'}"
|
|
938
|
-
}, ensure_ascii=False, indent=2)
|
|
939
|
-
)]
|
|
940
|
-
except Exception as e:
|
|
941
|
-
return [TextContent(
|
|
942
|
-
type="text",
|
|
943
|
-
text=json.dumps({
|
|
944
|
-
"success": False,
|
|
945
|
-
"error": f"断言异常: {str(e)}"
|
|
946
|
-
}, ensure_ascii=False, indent=2)
|
|
947
|
-
)]
|
|
948
|
-
|
|
949
|
-
async def handle_mobile_get_current_package(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
950
|
-
"""获取当前应用包名"""
|
|
951
|
-
await self.initialize()
|
|
952
|
-
|
|
953
|
-
try:
|
|
954
|
-
package = self.client.u2.app_current()['package']
|
|
955
|
-
return [TextContent(
|
|
956
|
-
type="text",
|
|
957
|
-
text=json.dumps({
|
|
958
|
-
"success": True,
|
|
959
|
-
"package": package,
|
|
960
|
-
"message": f"当前应用: {package}"
|
|
961
|
-
}, ensure_ascii=False, indent=2)
|
|
962
|
-
)]
|
|
963
|
-
except Exception as e:
|
|
964
|
-
return [TextContent(
|
|
965
|
-
type="text",
|
|
966
|
-
text=json.dumps({
|
|
967
|
-
"success": False,
|
|
968
|
-
"error": f"获取包名异常: {str(e)}"
|
|
969
|
-
}, ensure_ascii=False, indent=2)
|
|
970
|
-
)]
|
|
971
|
-
|
|
972
|
-
async def handle_mobile_take_screenshot(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
973
|
-
"""截图并保存"""
|
|
974
|
-
await self.initialize()
|
|
975
|
-
|
|
976
|
-
try:
|
|
977
|
-
import os
|
|
978
|
-
from datetime import datetime
|
|
979
|
-
|
|
980
|
-
save_path = arguments.get("save_path")
|
|
981
|
-
if not save_path:
|
|
982
|
-
# 默认保存到项目内的screenshots目录
|
|
983
|
-
mobile_mcp_dir = Path(__file__).parent.parent
|
|
984
|
-
screenshot_dir = mobile_mcp_dir / "screenshots"
|
|
985
|
-
screenshot_dir.mkdir(exist_ok=True)
|
|
986
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
987
|
-
save_path = str(screenshot_dir / f"mobile_screenshot_{timestamp}.png")
|
|
988
|
-
|
|
989
|
-
# 截图
|
|
990
|
-
self.client.u2.screenshot(save_path)
|
|
991
|
-
|
|
992
|
-
# 🎯 返回截图路径,Cursor AI可以通过读取文件来查看截图
|
|
993
|
-
# 注意:MCP协议只支持文本返回,但Cursor AI可以读取文件内容
|
|
994
|
-
return [TextContent(
|
|
995
|
-
type="text",
|
|
996
|
-
text=json.dumps({
|
|
997
|
-
"success": True,
|
|
998
|
-
"screenshot_path": save_path,
|
|
999
|
-
"message": f"截图已保存: {save_path}",
|
|
1000
|
-
"note": "Cursor AI可以通过读取此文件路径来查看截图内容"
|
|
1001
|
-
}, ensure_ascii=False, indent=2)
|
|
1002
|
-
)]
|
|
1003
|
-
except Exception as e:
|
|
1004
|
-
return [TextContent(
|
|
1005
|
-
type="text",
|
|
1006
|
-
text=json.dumps({
|
|
1007
|
-
"success": False,
|
|
1008
|
-
"error": f"截图异常: {str(e)}"
|
|
1009
|
-
}, ensure_ascii=False, indent=2)
|
|
1010
|
-
)]
|
|
1011
|
-
|
|
1012
|
-
async def handle_mobile_analyze_screenshot(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1013
|
-
"""
|
|
1014
|
-
分析截图并返回元素坐标(支持自动模式)
|
|
1015
|
-
|
|
1016
|
-
这个工具会:
|
|
1017
|
-
1. 读取请求文件(如果提供request_id)- 自动模式
|
|
1018
|
-
2. 或者直接分析截图(如果提供screenshot_path)- 手动模式
|
|
1019
|
-
3. 使用AI平台的多模态能力分析截图(自动检测平台)
|
|
1020
|
-
4. 返回坐标并写入结果文件(自动模式)
|
|
1021
|
-
"""
|
|
1022
|
-
await self.initialize()
|
|
1023
|
-
|
|
1024
|
-
# 🎯 检查AI平台是否可用
|
|
1025
|
-
if not self.ai_adapter or not self.ai_adapter.is_vision_available():
|
|
1026
|
-
return [TextContent(
|
|
1027
|
-
type="text",
|
|
1028
|
-
text=json.dumps({
|
|
1029
|
-
"success": False,
|
|
1030
|
-
"error": "AI视觉识别功能不可用",
|
|
1031
|
-
"suggestion": "请确保AI增强功能已启用,并且有可用的AI平台(Cursor、Claude、OpenAI等)"
|
|
1032
|
-
}, ensure_ascii=False, indent=2)
|
|
1033
|
-
)]
|
|
1034
|
-
|
|
1035
|
-
screenshot_path = arguments.get("screenshot_path")
|
|
1036
|
-
element_desc = arguments.get("element_desc")
|
|
1037
|
-
request_id = arguments.get("request_id") # 自动模式:从请求文件读取
|
|
1038
|
-
|
|
1039
|
-
try:
|
|
1040
|
-
import os
|
|
1041
|
-
|
|
1042
|
-
# 🎯 自动模式:如果有request_id,从请求文件读取信息
|
|
1043
|
-
if request_id:
|
|
1044
|
-
# 使用项目内的screenshots目录
|
|
1045
|
-
# mcp_server.py在mcp/目录下,所以需要向上1级到mobile_mcp目录
|
|
1046
|
-
mobile_mcp_dir = Path(__file__).parent.parent # mobile_mcp目录
|
|
1047
|
-
request_dir = mobile_mcp_dir / "screenshots" / "requests"
|
|
1048
|
-
request_file = request_dir / f"request_{request_id}.json"
|
|
1049
|
-
result_dir = mobile_mcp_dir / "screenshots" / "results"
|
|
1050
|
-
result_file = result_dir / f"result_{request_id}.json"
|
|
1051
|
-
|
|
1052
|
-
if request_file.exists():
|
|
1053
|
-
with open(request_file, 'r', encoding='utf-8') as f:
|
|
1054
|
-
request_data = json.load(f)
|
|
1055
|
-
screenshot_path = request_data.get('screenshot_path')
|
|
1056
|
-
element_desc = request_data.get('element_desc')
|
|
1057
|
-
script_path = request_data.get('script_path')
|
|
1058
|
-
print(f"📝 读取请求文件: {request_file}", file=sys.stderr)
|
|
1059
|
-
else:
|
|
1060
|
-
return [TextContent(
|
|
1061
|
-
type="text",
|
|
1062
|
-
text=json.dumps({
|
|
1063
|
-
"success": False,
|
|
1064
|
-
"error": f"请求文件不存在: {request_file}"
|
|
1065
|
-
}, ensure_ascii=False, indent=2)
|
|
1066
|
-
)]
|
|
1067
|
-
|
|
1068
|
-
# 检查截图文件是否存在
|
|
1069
|
-
if not screenshot_path or not os.path.exists(screenshot_path):
|
|
1070
|
-
return [TextContent(
|
|
1071
|
-
type="text",
|
|
1072
|
-
text=json.dumps({
|
|
1073
|
-
"success": False,
|
|
1074
|
-
"error": f"截图文件不存在: {screenshot_path}"
|
|
1075
|
-
}, ensure_ascii=False, indent=2)
|
|
1076
|
-
)]
|
|
1077
|
-
|
|
1078
|
-
# 🎯 使用AI平台适配器分析截图
|
|
1079
|
-
platform_name = self.ai_adapter.get_platform_name()
|
|
1080
|
-
|
|
1081
|
-
# 尝试使用适配器分析
|
|
1082
|
-
analyze_result = await self.ai_adapter.analyze_screenshot(
|
|
1083
|
-
screenshot_path=screenshot_path,
|
|
1084
|
-
element_desc=element_desc,
|
|
1085
|
-
request_id=request_id,
|
|
1086
|
-
result_file=str(result_file) if request_id else None,
|
|
1087
|
-
script_path=script_path if request_id else None
|
|
1088
|
-
)
|
|
1089
|
-
|
|
1090
|
-
# 🎯 构建响应数据
|
|
1091
|
-
if analyze_result and "x" in analyze_result:
|
|
1092
|
-
# 直接返回坐标(适配器已分析完成)
|
|
1093
|
-
response_data = {
|
|
1094
|
-
"success": True,
|
|
1095
|
-
"screenshot_path": screenshot_path,
|
|
1096
|
-
"element_desc": element_desc,
|
|
1097
|
-
"coordinate": {
|
|
1098
|
-
"x": analyze_result["x"],
|
|
1099
|
-
"y": analyze_result["y"],
|
|
1100
|
-
"confidence": analyze_result.get("confidence", 90)
|
|
1101
|
-
},
|
|
1102
|
-
"platform": analyze_result.get("platform", "unknown"),
|
|
1103
|
-
"message": f"成功分析截图,找到元素坐标"
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
# 如果是自动模式,写入结果文件
|
|
1107
|
-
if request_id and result_file:
|
|
1108
|
-
result_data = {
|
|
1109
|
-
"request_id": request_id,
|
|
1110
|
-
"status": "completed",
|
|
1111
|
-
"coordinate": response_data["coordinate"]
|
|
1112
|
-
}
|
|
1113
|
-
result_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1114
|
-
with open(result_file, 'w', encoding='utf-8') as f:
|
|
1115
|
-
json.dump(result_data, f, ensure_ascii=False, indent=2)
|
|
1116
|
-
|
|
1117
|
-
return [TextContent(
|
|
1118
|
-
type="text",
|
|
1119
|
-
text=json.dumps(response_data, ensure_ascii=False, indent=2)
|
|
1120
|
-
)]
|
|
1121
|
-
|
|
1122
|
-
# 如果适配器返回指令(需要AI平台进一步处理)
|
|
1123
|
-
if analyze_result and "instruction" in analyze_result:
|
|
1124
|
-
instruction = analyze_result["instruction"]
|
|
1125
|
-
else:
|
|
1126
|
-
# 默认指令
|
|
1127
|
-
instruction = f"""
|
|
1128
|
-
🎯 任务:分析移动端截图,找到元素并返回坐标
|
|
1129
|
-
|
|
1130
|
-
截图路径: {screenshot_path}
|
|
1131
|
-
要查找的元素: {element_desc}
|
|
1132
|
-
截图尺寸: 1080x2400 (竖屏)
|
|
1133
|
-
AI平台: {platform_name}
|
|
1134
|
-
|
|
1135
|
-
请执行以下步骤:
|
|
1136
|
-
1. 读取截图文件: {screenshot_path}
|
|
1137
|
-
2. 使用多模态能力分析截图,找到元素: {element_desc}
|
|
1138
|
-
3. 返回元素的中心点坐标,格式为JSON:
|
|
1139
|
-
{{"x": 100, "y": 200, "confidence": 90}}
|
|
1140
|
-
|
|
1141
|
-
注意:
|
|
1142
|
-
- x, y 是元素中心点的像素坐标(屏幕坐标,左上角为0,0)
|
|
1143
|
-
- confidence 是置信度(0-100)
|
|
1144
|
-
- 如果找不到元素,返回 {{"found": false}}
|
|
1145
|
-
- 截图是竖屏,尺寸为1080x2400
|
|
1146
|
-
""".strip()
|
|
1147
|
-
|
|
1148
|
-
response_data = {
|
|
1149
|
-
"success": True,
|
|
1150
|
-
"screenshot_path": screenshot_path,
|
|
1151
|
-
"element_desc": element_desc,
|
|
1152
|
-
"platform": platform_name,
|
|
1153
|
-
"message": f"请{platform_name}分析截图并返回坐标",
|
|
1154
|
-
"instruction": instruction
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
# 🎯 自动模式:如果有request_id,需要写入结果文件
|
|
1158
|
-
if request_id:
|
|
1159
|
-
response_data["request_id"] = request_id
|
|
1160
|
-
response_data["result_file"] = str(result_file)
|
|
1161
|
-
response_data["script_path"] = script_path
|
|
1162
|
-
response_data["auto_mode"] = True
|
|
1163
|
-
response_data["instruction"] += f"""
|
|
1164
|
-
|
|
1165
|
-
⚠️ 自动模式:请执行以下步骤:
|
|
1166
|
-
1. 读取截图文件: {screenshot_path}
|
|
1167
|
-
2. 使用多模态能力分析截图,找到元素: {element_desc}
|
|
1168
|
-
3. 返回坐标JSON格式: {{"x": 100, "y": 200, "confidence": 90}}
|
|
1169
|
-
4. **重要**:将结果写入文件: {result_file}
|
|
1170
|
-
格式:{{"request_id": "{request_id}", "status": "completed", "coordinate": {{"x": 100, "y": 200, "confidence": 90}}}}
|
|
1171
|
-
"""
|
|
1172
|
-
|
|
1173
|
-
return [TextContent(
|
|
1174
|
-
type="text",
|
|
1175
|
-
text=json.dumps(response_data, ensure_ascii=False, indent=2)
|
|
1176
|
-
)]
|
|
1177
|
-
except Exception as e:
|
|
1178
|
-
return [TextContent(
|
|
1179
|
-
type="text",
|
|
1180
|
-
text=json.dumps({
|
|
1181
|
-
"success": False,
|
|
1182
|
-
"error": f"分析截图异常: {str(e)}"
|
|
1183
|
-
}, ensure_ascii=False, indent=2)
|
|
1184
|
-
)]
|
|
1185
|
-
|
|
1186
|
-
async def handle_mobile_execute_test_case(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1187
|
-
"""智能执行测试用例"""
|
|
1188
|
-
await self.initialize()
|
|
1189
|
-
|
|
1190
|
-
try:
|
|
1191
|
-
test_description = arguments.get("test_description")
|
|
1192
|
-
|
|
1193
|
-
if not test_description:
|
|
1194
|
-
return [TextContent(
|
|
1195
|
-
type="text",
|
|
1196
|
-
text=json.dumps({
|
|
1197
|
-
"success": False,
|
|
1198
|
-
"error": "缺少测试用例描述"
|
|
1199
|
-
}, ensure_ascii=False, indent=2)
|
|
1200
|
-
)]
|
|
1201
|
-
|
|
1202
|
-
# 导入智能执行器
|
|
1203
|
-
from mobile_mcp.core.ai.smart_test_executor import SmartTestExecutor
|
|
1204
|
-
|
|
1205
|
-
executor = SmartTestExecutor(self.client, self.locator)
|
|
1206
|
-
result = await executor.execute_test_case(test_description)
|
|
1207
|
-
|
|
1208
|
-
return [TextContent(
|
|
1209
|
-
type="text",
|
|
1210
|
-
text=json.dumps({
|
|
1211
|
-
"success": True,
|
|
1212
|
-
"total_steps": result['total_steps'],
|
|
1213
|
-
"success_count": result['success_count'],
|
|
1214
|
-
"fail_count": result['fail_count'],
|
|
1215
|
-
"success_rate": f"{result['success_count']/result['total_steps']*100:.1f}%",
|
|
1216
|
-
"results": result['results'],
|
|
1217
|
-
"message": f"测试执行完成:{result['success_count']}/{result['total_steps']} 成功"
|
|
1218
|
-
}, ensure_ascii=False, indent=2)
|
|
1219
|
-
)]
|
|
1220
|
-
except Exception as e:
|
|
1221
|
-
return [TextContent(
|
|
1222
|
-
type="text",
|
|
1223
|
-
text=json.dumps({
|
|
1224
|
-
"success": False,
|
|
1225
|
-
"error": f"执行测试用例异常: {str(e)}"
|
|
1226
|
-
}, ensure_ascii=False, indent=2)
|
|
1227
|
-
)]
|
|
1228
|
-
|
|
1229
|
-
async def handle_mobile_generate_test_script(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1230
|
-
"""生成测试脚本"""
|
|
1231
|
-
await self.initialize()
|
|
1232
|
-
|
|
1233
|
-
try:
|
|
1234
|
-
test_name = arguments.get("test_name")
|
|
1235
|
-
package_name = arguments.get("package_name")
|
|
1236
|
-
filename = arguments.get("filename")
|
|
1237
|
-
|
|
1238
|
-
# 🎯 使用独立测试生成器(不依赖 mobile_mcp 包)
|
|
1239
|
-
from mobile_mcp.core.ai.test_generator_standalone import StandaloneTestGenerator
|
|
1240
|
-
import os
|
|
1241
|
-
|
|
1242
|
-
# 🎯 保存到用户当前工作目录的 tests 子目录
|
|
1243
|
-
# 优先级:参数指定 > 环境变量 > 当前工作目录
|
|
1244
|
-
output_dir = arguments.get("output_dir") # 用户可以指定输出目录
|
|
1245
|
-
|
|
1246
|
-
if not output_dir:
|
|
1247
|
-
# 优先从环境变量获取工作区路径(支持 mcp.json 配置)
|
|
1248
|
-
workspace_path = os.environ.get('CURSOR_WORKSPACE') or os.environ.get('WORKSPACE_PATH')
|
|
1249
|
-
|
|
1250
|
-
if workspace_path and Path(workspace_path).exists():
|
|
1251
|
-
# ✅ 方案1: 使用环境变量(推荐,在 mcp.json 中配置)
|
|
1252
|
-
cwd = Path(workspace_path)
|
|
1253
|
-
print(f" 📂 使用配置的工作区路径: {cwd}", file=sys.stderr)
|
|
1254
|
-
else:
|
|
1255
|
-
# ⚠️ 方案2: 回退到当前工作目录
|
|
1256
|
-
cwd = Path(os.getcwd())
|
|
1257
|
-
|
|
1258
|
-
# 🎯 智能检测:如果当前目录是用户主目录,给出友好提示
|
|
1259
|
-
home_dir = Path.home()
|
|
1260
|
-
if cwd == home_dir or cwd.parent == home_dir:
|
|
1261
|
-
warning_msg = (
|
|
1262
|
-
f"\n⚠️ 警告: 当前工作目录是用户主目录,脚本将生成到 {cwd / 'tests'}\n"
|
|
1263
|
-
f" 建议在 mcp.json 中配置工作区路径,避免文件混乱:\n"
|
|
1264
|
-
f' "cwd": "/path/to/your/project" 或\n'
|
|
1265
|
-
f' "env": {{"WORKSPACE_PATH": "/path/to/your/project"}}\n'
|
|
1266
|
-
)
|
|
1267
|
-
print(warning_msg, file=sys.stderr)
|
|
1268
|
-
else:
|
|
1269
|
-
print(f" 📂 使用当前工作目录: {cwd}", file=sys.stderr)
|
|
1270
|
-
|
|
1271
|
-
output_dir_path = cwd / "tests"
|
|
1272
|
-
else:
|
|
1273
|
-
output_dir_path = Path(output_dir)
|
|
1274
|
-
print(f" 📂 使用指定的输出目录: {output_dir_path}", file=sys.stderr)
|
|
1275
|
-
|
|
1276
|
-
output_dir_path.mkdir(parents=True, exist_ok=True)
|
|
1277
|
-
|
|
1278
|
-
# 确保传入字符串路径
|
|
1279
|
-
output_dir_str = str(output_dir_path.resolve())
|
|
1280
|
-
generator = StandaloneTestGenerator(output_dir=output_dir_str)
|
|
1281
|
-
|
|
1282
|
-
# 从client获取操作历史,只保留成功的操作
|
|
1283
|
-
operation_history = getattr(self.client, 'operation_history', [])
|
|
1284
|
-
successful_operations = [
|
|
1285
|
-
op for op in operation_history
|
|
1286
|
-
if op.get('success', False)
|
|
1287
|
-
]
|
|
1288
|
-
|
|
1289
|
-
if not successful_operations:
|
|
1290
|
-
return [TextContent(
|
|
1291
|
-
type="text",
|
|
1292
|
-
text=json.dumps({
|
|
1293
|
-
"success": False,
|
|
1294
|
-
"error": "没有成功的操作记录,无法生成脚本"
|
|
1295
|
-
}, ensure_ascii=False, indent=2)
|
|
1296
|
-
)]
|
|
1297
|
-
|
|
1298
|
-
# 🎯 获取当前使用的设备ID(重要!多设备时必须指定)
|
|
1299
|
-
device_id = getattr(self.client, 'device_id', None)
|
|
1300
|
-
if not device_id and hasattr(self.client, 'u2'):
|
|
1301
|
-
# 尝试从 u2 对象获取
|
|
1302
|
-
device_id = getattr(self.client.u2, 'serial', None)
|
|
1303
|
-
|
|
1304
|
-
# 生成脚本
|
|
1305
|
-
script = generator.generate_from_history(
|
|
1306
|
-
test_name=test_name,
|
|
1307
|
-
package_name=package_name,
|
|
1308
|
-
operation_history=successful_operations,
|
|
1309
|
-
device_id=device_id # 传递设备ID
|
|
1310
|
-
)
|
|
1311
|
-
|
|
1312
|
-
# 保存脚本
|
|
1313
|
-
script_path = generator.save(filename, script)
|
|
1314
|
-
|
|
1315
|
-
return [TextContent(
|
|
1316
|
-
type="text",
|
|
1317
|
-
text=json.dumps({
|
|
1318
|
-
"success": True,
|
|
1319
|
-
"test_name": test_name,
|
|
1320
|
-
"script_path": str(script_path),
|
|
1321
|
-
"output_dir": str(output_dir_path),
|
|
1322
|
-
"operation_count": len(successful_operations),
|
|
1323
|
-
"format": "pytest",
|
|
1324
|
-
"message": f"✅ 测试脚本已生成到用户项目: {script_path}",
|
|
1325
|
-
"usage": {
|
|
1326
|
-
"run_test": f"pytest {script_path.name} -v",
|
|
1327
|
-
"with_allure": f"pytest {script_path.name} --alluredir=./allure-results",
|
|
1328
|
-
"view_report": "allure serve ./allure-results"
|
|
1329
|
-
}
|
|
1330
|
-
}, ensure_ascii=False, indent=2)
|
|
1331
|
-
)]
|
|
1332
|
-
except Exception as e:
|
|
1333
|
-
return [TextContent(
|
|
1334
|
-
type="text",
|
|
1335
|
-
text=json.dumps({
|
|
1336
|
-
"success": False,
|
|
1337
|
-
"error": f"生成测试脚本异常: {str(e)}"
|
|
1338
|
-
}, ensure_ascii=False, indent=2)
|
|
1339
|
-
)]
|
|
1340
|
-
|
|
1341
|
-
async def handle_tool_call(self, name: str, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1342
|
-
"""路由工具调用"""
|
|
1343
|
-
handlers = {
|
|
1344
|
-
"mobile_click": self.handle_mobile_click,
|
|
1345
|
-
"mobile_input": self.handle_mobile_input,
|
|
1346
|
-
"mobile_swipe": self.handle_mobile_swipe,
|
|
1347
|
-
"mobile_press_key": self.handle_mobile_press_key,
|
|
1348
|
-
"mobile_snapshot": self.handle_mobile_snapshot,
|
|
1349
|
-
"mobile_launch_app": self.handle_mobile_launch_app,
|
|
1350
|
-
"mobile_assert_text": self.handle_mobile_assert_text,
|
|
1351
|
-
"mobile_get_current_package": self.handle_mobile_get_current_package,
|
|
1352
|
-
"mobile_take_screenshot": self.handle_mobile_take_screenshot,
|
|
1353
|
-
"mobile_analyze_screenshot": self.handle_mobile_analyze_screenshot,
|
|
1354
|
-
"mobile_execute_test_case": self.handle_mobile_execute_test_case,
|
|
1355
|
-
"mobile_generate_test_script": self.handle_mobile_generate_test_script,
|
|
1356
|
-
"mobile_list_devices": self.handle_mobile_list_devices,
|
|
1357
|
-
"mobile_get_screen_size": self.handle_mobile_get_screen_size,
|
|
1358
|
-
"mobile_get_orientation": self.handle_mobile_get_orientation,
|
|
1359
|
-
"mobile_set_orientation": self.handle_mobile_set_orientation,
|
|
1360
|
-
"mobile_list_apps": self.handle_mobile_list_apps,
|
|
1361
|
-
"mobile_install_app": self.handle_mobile_install_app,
|
|
1362
|
-
"mobile_uninstall_app": self.handle_mobile_uninstall_app,
|
|
1363
|
-
"mobile_terminate_app": self.handle_mobile_terminate_app,
|
|
1364
|
-
"mobile_double_click": self.handle_mobile_double_click,
|
|
1365
|
-
"mobile_long_press": self.handle_mobile_long_press,
|
|
1366
|
-
"mobile_open_url": self.handle_mobile_open_url,
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
handler = handlers.get(name)
|
|
1370
|
-
if not handler:
|
|
1371
|
-
return [TextContent(
|
|
1372
|
-
type="text",
|
|
1373
|
-
text=json.dumps({
|
|
1374
|
-
"success": False,
|
|
1375
|
-
"error": f"未知工具: {name}"
|
|
1376
|
-
}, ensure_ascii=False, indent=2)
|
|
1377
|
-
)]
|
|
1378
|
-
|
|
1379
|
-
return await handler(arguments)
|
|
1380
|
-
|
|
1381
|
-
# ==================== 新增工具处理函数 ====================
|
|
1382
|
-
|
|
1383
|
-
async def handle_mobile_list_devices(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1384
|
-
"""列出所有连接的设备"""
|
|
1385
|
-
try:
|
|
1386
|
-
from mobile_mcp.core.device_manager import DeviceManager
|
|
1387
|
-
manager = DeviceManager()
|
|
1388
|
-
devices = manager.list_devices()
|
|
1389
|
-
|
|
1390
|
-
return [TextContent(
|
|
1391
|
-
type="text",
|
|
1392
|
-
text=json.dumps({
|
|
1393
|
-
"success": True,
|
|
1394
|
-
"devices": devices,
|
|
1395
|
-
"count": len(devices),
|
|
1396
|
-
"message": f"找到 {len(devices)} 个设备"
|
|
1397
|
-
}, ensure_ascii=False, indent=2)
|
|
1398
|
-
)]
|
|
1399
|
-
except Exception as e:
|
|
1400
|
-
return [TextContent(
|
|
1401
|
-
type="text",
|
|
1402
|
-
text=json.dumps({
|
|
1403
|
-
"success": False,
|
|
1404
|
-
"error": f"获取设备列表失败: {str(e)}"
|
|
1405
|
-
}, ensure_ascii=False, indent=2)
|
|
1406
|
-
)]
|
|
1407
|
-
|
|
1408
|
-
async def handle_mobile_get_screen_size(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1409
|
-
"""获取屏幕尺寸"""
|
|
1410
|
-
await self.initialize()
|
|
1411
|
-
|
|
1412
|
-
try:
|
|
1413
|
-
info = self.client.u2.info
|
|
1414
|
-
width = info.get('displayWidth', 0)
|
|
1415
|
-
height = info.get('displayHeight', 0)
|
|
1416
|
-
|
|
1417
|
-
return [TextContent(
|
|
1418
|
-
type="text",
|
|
1419
|
-
text=json.dumps({
|
|
1420
|
-
"success": True,
|
|
1421
|
-
"width": width,
|
|
1422
|
-
"height": height,
|
|
1423
|
-
"size": f"{width}x{height}",
|
|
1424
|
-
"message": f"屏幕尺寸: {width}x{height}"
|
|
1425
|
-
}, ensure_ascii=False, indent=2)
|
|
1426
|
-
)]
|
|
1427
|
-
except Exception as e:
|
|
1428
|
-
return [TextContent(
|
|
1429
|
-
type="text",
|
|
1430
|
-
text=json.dumps({
|
|
1431
|
-
"success": False,
|
|
1432
|
-
"error": f"获取屏幕尺寸失败: {str(e)}"
|
|
1433
|
-
}, ensure_ascii=False, indent=2)
|
|
1434
|
-
)]
|
|
1435
|
-
|
|
1436
|
-
async def handle_mobile_get_orientation(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1437
|
-
"""获取屏幕方向"""
|
|
1438
|
-
await self.initialize()
|
|
1439
|
-
|
|
1440
|
-
try:
|
|
1441
|
-
info = self.client.u2.info
|
|
1442
|
-
orientation = info.get('displayRotation', 0)
|
|
1443
|
-
|
|
1444
|
-
# 0或2 = 竖屏, 1或3 = 横屏
|
|
1445
|
-
is_portrait = orientation in [0, 2]
|
|
1446
|
-
orientation_name = "portrait" if is_portrait else "landscape"
|
|
1447
|
-
|
|
1448
|
-
return [TextContent(
|
|
1449
|
-
type="text",
|
|
1450
|
-
text=json.dumps({
|
|
1451
|
-
"success": True,
|
|
1452
|
-
"orientation": orientation_name,
|
|
1453
|
-
"rotation": orientation,
|
|
1454
|
-
"message": f"当前方向: {orientation_name}"
|
|
1455
|
-
}, ensure_ascii=False, indent=2)
|
|
1456
|
-
)]
|
|
1457
|
-
except Exception as e:
|
|
1458
|
-
return [TextContent(
|
|
1459
|
-
type="text",
|
|
1460
|
-
text=json.dumps({
|
|
1461
|
-
"success": False,
|
|
1462
|
-
"error": f"获取屏幕方向失败: {str(e)}"
|
|
1463
|
-
}, ensure_ascii=False, indent=2)
|
|
1464
|
-
)]
|
|
1465
|
-
|
|
1466
|
-
async def handle_mobile_set_orientation(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1467
|
-
"""设置屏幕方向"""
|
|
1468
|
-
await self.initialize()
|
|
1469
|
-
|
|
1470
|
-
try:
|
|
1471
|
-
orientation = arguments.get("orientation")
|
|
1472
|
-
if orientation not in ["portrait", "landscape"]:
|
|
1473
|
-
return [TextContent(
|
|
1474
|
-
type="text",
|
|
1475
|
-
text=json.dumps({
|
|
1476
|
-
"success": False,
|
|
1477
|
-
"error": "orientation必须是'portrait'或'landscape'"
|
|
1478
|
-
}, ensure_ascii=False, indent=2)
|
|
1479
|
-
)]
|
|
1480
|
-
|
|
1481
|
-
# 设置方向
|
|
1482
|
-
if orientation == "portrait":
|
|
1483
|
-
self.client.u2.set_orientation("n")
|
|
1484
|
-
else:
|
|
1485
|
-
self.client.u2.set_orientation("l")
|
|
1486
|
-
|
|
1487
|
-
return [TextContent(
|
|
1488
|
-
type="text",
|
|
1489
|
-
text=json.dumps({
|
|
1490
|
-
"success": True,
|
|
1491
|
-
"orientation": orientation,
|
|
1492
|
-
"message": f"屏幕方向已设置为: {orientation}"
|
|
1493
|
-
}, ensure_ascii=False, indent=2)
|
|
1494
|
-
)]
|
|
1495
|
-
except Exception as e:
|
|
1496
|
-
return [TextContent(
|
|
1497
|
-
type="text",
|
|
1498
|
-
text=json.dumps({
|
|
1499
|
-
"success": False,
|
|
1500
|
-
"error": f"设置屏幕方向失败: {str(e)}"
|
|
1501
|
-
}, ensure_ascii=False, indent=2)
|
|
1502
|
-
)]
|
|
1503
|
-
|
|
1504
|
-
async def handle_mobile_list_apps(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1505
|
-
"""列出已安装的应用"""
|
|
1506
|
-
await self.initialize()
|
|
1507
|
-
|
|
1508
|
-
try:
|
|
1509
|
-
filter_keyword = arguments.get("filter", "")
|
|
1510
|
-
|
|
1511
|
-
# 获取所有应用
|
|
1512
|
-
apps = self.client.u2.app_list()
|
|
1513
|
-
|
|
1514
|
-
# 过滤
|
|
1515
|
-
if filter_keyword:
|
|
1516
|
-
filtered_apps = [
|
|
1517
|
-
app for app in apps
|
|
1518
|
-
if filter_keyword.lower() in app.lower()
|
|
1519
|
-
]
|
|
1520
|
-
else:
|
|
1521
|
-
filtered_apps = apps
|
|
1522
|
-
|
|
1523
|
-
return [TextContent(
|
|
1524
|
-
type="text",
|
|
1525
|
-
text=json.dumps({
|
|
1526
|
-
"success": True,
|
|
1527
|
-
"apps": filtered_apps,
|
|
1528
|
-
"count": len(filtered_apps),
|
|
1529
|
-
"total": len(apps),
|
|
1530
|
-
"filter": filter_keyword if filter_keyword else None,
|
|
1531
|
-
"message": f"找到 {len(filtered_apps)} 个应用"
|
|
1532
|
-
}, ensure_ascii=False, indent=2)
|
|
1533
|
-
)]
|
|
1534
|
-
except Exception as e:
|
|
1535
|
-
return [TextContent(
|
|
1536
|
-
type="text",
|
|
1537
|
-
text=json.dumps({
|
|
1538
|
-
"success": False,
|
|
1539
|
-
"error": f"获取应用列表失败: {str(e)}"
|
|
1540
|
-
}, ensure_ascii=False, indent=2)
|
|
1541
|
-
)]
|
|
1542
|
-
|
|
1543
|
-
async def handle_mobile_install_app(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1544
|
-
"""安装应用"""
|
|
1545
|
-
await self.initialize()
|
|
1546
|
-
|
|
1547
|
-
try:
|
|
1548
|
-
apk_path = arguments.get("apk_path")
|
|
1549
|
-
if not apk_path:
|
|
1550
|
-
return [TextContent(
|
|
1551
|
-
type="text",
|
|
1552
|
-
text=json.dumps({
|
|
1553
|
-
"success": False,
|
|
1554
|
-
"error": "缺少apk_path参数"
|
|
1555
|
-
}, ensure_ascii=False, indent=2)
|
|
1556
|
-
)]
|
|
1557
|
-
|
|
1558
|
-
# 检查文件是否存在
|
|
1559
|
-
import os
|
|
1560
|
-
if not os.path.exists(apk_path):
|
|
1561
|
-
return [TextContent(
|
|
1562
|
-
type="text",
|
|
1563
|
-
text=json.dumps({
|
|
1564
|
-
"success": False,
|
|
1565
|
-
"error": f"APK文件不存在: {apk_path}"
|
|
1566
|
-
}, ensure_ascii=False, indent=2)
|
|
1567
|
-
)]
|
|
1568
|
-
|
|
1569
|
-
# 安装应用
|
|
1570
|
-
result = self.client.u2.app_install(apk_path)
|
|
1571
|
-
|
|
1572
|
-
return [TextContent(
|
|
1573
|
-
type="text",
|
|
1574
|
-
text=json.dumps({
|
|
1575
|
-
"success": result,
|
|
1576
|
-
"apk_path": apk_path,
|
|
1577
|
-
"message": "应用安装成功" if result else "应用安装失败"
|
|
1578
|
-
}, ensure_ascii=False, indent=2)
|
|
1579
|
-
)]
|
|
1580
|
-
except Exception as e:
|
|
1581
|
-
return [TextContent(
|
|
1582
|
-
type="text",
|
|
1583
|
-
text=json.dumps({
|
|
1584
|
-
"success": False,
|
|
1585
|
-
"error": f"安装应用失败: {str(e)}"
|
|
1586
|
-
}, ensure_ascii=False, indent=2)
|
|
1587
|
-
)]
|
|
1588
|
-
|
|
1589
|
-
async def handle_mobile_uninstall_app(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1590
|
-
"""卸载应用"""
|
|
1591
|
-
await self.initialize()
|
|
1592
|
-
|
|
1593
|
-
try:
|
|
1594
|
-
package_name = arguments.get("package_name")
|
|
1595
|
-
if not package_name:
|
|
1596
|
-
return [TextContent(
|
|
1597
|
-
type="text",
|
|
1598
|
-
text=json.dumps({
|
|
1599
|
-
"success": False,
|
|
1600
|
-
"error": "缺少package_name参数"
|
|
1601
|
-
}, ensure_ascii=False, indent=2)
|
|
1602
|
-
)]
|
|
1603
|
-
|
|
1604
|
-
# 卸载应用
|
|
1605
|
-
result = self.client.u2.app_uninstall(package_name)
|
|
1606
|
-
|
|
1607
|
-
return [TextContent(
|
|
1608
|
-
type="text",
|
|
1609
|
-
text=json.dumps({
|
|
1610
|
-
"success": result,
|
|
1611
|
-
"package": package_name,
|
|
1612
|
-
"message": "应用卸载成功" if result else "应用卸载失败"
|
|
1613
|
-
}, ensure_ascii=False, indent=2)
|
|
1614
|
-
)]
|
|
1615
|
-
except Exception as e:
|
|
1616
|
-
return [TextContent(
|
|
1617
|
-
type="text",
|
|
1618
|
-
text=json.dumps({
|
|
1619
|
-
"success": False,
|
|
1620
|
-
"error": f"卸载应用失败: {str(e)}"
|
|
1621
|
-
}, ensure_ascii=False, indent=2)
|
|
1622
|
-
)]
|
|
1623
|
-
|
|
1624
|
-
async def handle_mobile_terminate_app(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1625
|
-
"""终止应用"""
|
|
1626
|
-
await self.initialize()
|
|
1627
|
-
|
|
1628
|
-
try:
|
|
1629
|
-
package_name = arguments.get("package_name")
|
|
1630
|
-
if not package_name:
|
|
1631
|
-
return [TextContent(
|
|
1632
|
-
type="text",
|
|
1633
|
-
text=json.dumps({
|
|
1634
|
-
"success": False,
|
|
1635
|
-
"error": "缺少package_name参数"
|
|
1636
|
-
}, ensure_ascii=False, indent=2)
|
|
1637
|
-
)]
|
|
1638
|
-
|
|
1639
|
-
# 终止应用
|
|
1640
|
-
self.client.u2.app_stop(package_name)
|
|
1641
|
-
|
|
1642
|
-
return [TextContent(
|
|
1643
|
-
type="text",
|
|
1644
|
-
text=json.dumps({
|
|
1645
|
-
"success": True,
|
|
1646
|
-
"package": package_name,
|
|
1647
|
-
"message": f"应用 {package_name} 已终止"
|
|
1648
|
-
}, ensure_ascii=False, indent=2)
|
|
1649
|
-
)]
|
|
1650
|
-
except Exception as e:
|
|
1651
|
-
return [TextContent(
|
|
1652
|
-
type="text",
|
|
1653
|
-
text=json.dumps({
|
|
1654
|
-
"success": False,
|
|
1655
|
-
"error": f"终止应用失败: {str(e)}"
|
|
1656
|
-
}, ensure_ascii=False, indent=2)
|
|
1657
|
-
)]
|
|
1658
|
-
|
|
1659
|
-
async def handle_mobile_double_click(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1660
|
-
"""双击元素"""
|
|
1661
|
-
await self.initialize()
|
|
1662
|
-
|
|
1663
|
-
try:
|
|
1664
|
-
element_desc = arguments.get("element_desc")
|
|
1665
|
-
x = arguments.get("x")
|
|
1666
|
-
y = arguments.get("y")
|
|
1667
|
-
|
|
1668
|
-
if x is not None and y is not None:
|
|
1669
|
-
# 直接使用坐标
|
|
1670
|
-
self.client.u2.double_click(x, y)
|
|
1671
|
-
return [TextContent(
|
|
1672
|
-
type="text",
|
|
1673
|
-
text=json.dumps({
|
|
1674
|
-
"success": True,
|
|
1675
|
-
"x": x,
|
|
1676
|
-
"y": y,
|
|
1677
|
-
"method": "coordinate",
|
|
1678
|
-
"message": f"双击坐标: ({x}, {y})"
|
|
1679
|
-
}, ensure_ascii=False, indent=2)
|
|
1680
|
-
)]
|
|
1681
|
-
elif element_desc:
|
|
1682
|
-
# 定位元素后双击
|
|
1683
|
-
result = await self.locator.locate(element_desc)
|
|
1684
|
-
if not result:
|
|
1685
|
-
return [TextContent(
|
|
1686
|
-
type="text",
|
|
1687
|
-
text=json.dumps({
|
|
1688
|
-
"success": False,
|
|
1689
|
-
"error": f"未找到元素: {element_desc}"
|
|
1690
|
-
}, ensure_ascii=False, indent=2)
|
|
1691
|
-
)]
|
|
1692
|
-
|
|
1693
|
-
ref = result.get('ref', '')
|
|
1694
|
-
# 获取元素中心点坐标
|
|
1695
|
-
if ref.startswith('[') and '][' in ref:
|
|
1696
|
-
# 解析bounds坐标
|
|
1697
|
-
import re
|
|
1698
|
-
match = re.search(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', ref)
|
|
1699
|
-
if match:
|
|
1700
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
1701
|
-
x, y = (x1 + x2) // 2, (y1 + y2) // 2
|
|
1702
|
-
self.client.u2.double_click(x, y)
|
|
1703
|
-
else:
|
|
1704
|
-
return [TextContent(
|
|
1705
|
-
type="text",
|
|
1706
|
-
text=json.dumps({
|
|
1707
|
-
"success": False,
|
|
1708
|
-
"error": f"无效的bounds格式: {ref}"
|
|
1709
|
-
}, ensure_ascii=False, indent=2)
|
|
1710
|
-
)]
|
|
1711
|
-
else:
|
|
1712
|
-
# 使用元素双击
|
|
1713
|
-
elem = self.client.u2(resourceId=ref) if (ref.startswith('com.') or ':' in ref) else self.client.u2(text=ref)
|
|
1714
|
-
if elem.exists():
|
|
1715
|
-
elem.double_click()
|
|
1716
|
-
else:
|
|
1717
|
-
return [TextContent(
|
|
1718
|
-
type="text",
|
|
1719
|
-
text=json.dumps({
|
|
1720
|
-
"success": False,
|
|
1721
|
-
"error": f"元素不存在: {element_desc}"
|
|
1722
|
-
}, ensure_ascii=False, indent=2)
|
|
1723
|
-
)]
|
|
1724
|
-
|
|
1725
|
-
return [TextContent(
|
|
1726
|
-
type="text",
|
|
1727
|
-
text=json.dumps({
|
|
1728
|
-
"success": True,
|
|
1729
|
-
"element": element_desc,
|
|
1730
|
-
"method": result.get('method', 'unknown'),
|
|
1731
|
-
"message": f"双击成功: {element_desc}"
|
|
1732
|
-
}, ensure_ascii=False, indent=2)
|
|
1733
|
-
)]
|
|
1734
|
-
else:
|
|
1735
|
-
return [TextContent(
|
|
1736
|
-
type="text",
|
|
1737
|
-
text=json.dumps({
|
|
1738
|
-
"success": False,
|
|
1739
|
-
"error": "需要提供element_desc或x,y坐标"
|
|
1740
|
-
}, ensure_ascii=False, indent=2)
|
|
1741
|
-
)]
|
|
1742
|
-
except Exception as e:
|
|
1743
|
-
return [TextContent(
|
|
1744
|
-
type="text",
|
|
1745
|
-
text=json.dumps({
|
|
1746
|
-
"success": False,
|
|
1747
|
-
"error": f"双击失败: {str(e)}"
|
|
1748
|
-
}, ensure_ascii=False, indent=2)
|
|
1749
|
-
)]
|
|
1750
|
-
|
|
1751
|
-
async def handle_mobile_long_press(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1752
|
-
"""长按元素"""
|
|
1753
|
-
await self.initialize()
|
|
1754
|
-
|
|
1755
|
-
try:
|
|
1756
|
-
element_desc = arguments.get("element_desc")
|
|
1757
|
-
duration = arguments.get("duration", 1.0)
|
|
1758
|
-
x = arguments.get("x")
|
|
1759
|
-
y = arguments.get("y")
|
|
1760
|
-
|
|
1761
|
-
if x is not None and y is not None:
|
|
1762
|
-
# 直接使用坐标
|
|
1763
|
-
self.client.u2.long_click(x, y, duration=duration)
|
|
1764
|
-
return [TextContent(
|
|
1765
|
-
type="text",
|
|
1766
|
-
text=json.dumps({
|
|
1767
|
-
"success": True,
|
|
1768
|
-
"x": x,
|
|
1769
|
-
"y": y,
|
|
1770
|
-
"duration": duration,
|
|
1771
|
-
"method": "coordinate",
|
|
1772
|
-
"message": f"长按坐标: ({x}, {y}), 持续{duration}秒"
|
|
1773
|
-
}, ensure_ascii=False, indent=2)
|
|
1774
|
-
)]
|
|
1775
|
-
elif element_desc:
|
|
1776
|
-
# 定位元素后长按
|
|
1777
|
-
result = await self.locator.locate(element_desc)
|
|
1778
|
-
if not result:
|
|
1779
|
-
return [TextContent(
|
|
1780
|
-
type="text",
|
|
1781
|
-
text=json.dumps({
|
|
1782
|
-
"success": False,
|
|
1783
|
-
"error": f"未找到元素: {element_desc}"
|
|
1784
|
-
}, ensure_ascii=False, indent=2)
|
|
1785
|
-
)]
|
|
1786
|
-
|
|
1787
|
-
ref = result.get('ref', '')
|
|
1788
|
-
# 获取元素中心点坐标
|
|
1789
|
-
if ref.startswith('[') and '][' in ref:
|
|
1790
|
-
# 解析bounds坐标
|
|
1791
|
-
import re
|
|
1792
|
-
match = re.search(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', ref)
|
|
1793
|
-
if match:
|
|
1794
|
-
x1, y1, x2, y2 = map(int, match.groups())
|
|
1795
|
-
x, y = (x1 + x2) // 2, (y1 + y2) // 2
|
|
1796
|
-
self.client.u2.long_click(x, y, duration=duration)
|
|
1797
|
-
else:
|
|
1798
|
-
return [TextContent(
|
|
1799
|
-
type="text",
|
|
1800
|
-
text=json.dumps({
|
|
1801
|
-
"success": False,
|
|
1802
|
-
"error": f"无效的bounds格式: {ref}"
|
|
1803
|
-
}, ensure_ascii=False, indent=2)
|
|
1804
|
-
)]
|
|
1805
|
-
else:
|
|
1806
|
-
# 使用元素长按
|
|
1807
|
-
elem = self.client.u2(resourceId=ref) if (ref.startswith('com.') or ':' in ref) else self.client.u2(text=ref)
|
|
1808
|
-
if elem.exists():
|
|
1809
|
-
elem.long_click(duration=duration)
|
|
1810
|
-
else:
|
|
1811
|
-
return [TextContent(
|
|
1812
|
-
type="text",
|
|
1813
|
-
text=json.dumps({
|
|
1814
|
-
"success": False,
|
|
1815
|
-
"error": f"元素不存在: {element_desc}"
|
|
1816
|
-
}, ensure_ascii=False, indent=2)
|
|
1817
|
-
)]
|
|
1818
|
-
|
|
1819
|
-
return [TextContent(
|
|
1820
|
-
type="text",
|
|
1821
|
-
text=json.dumps({
|
|
1822
|
-
"success": True,
|
|
1823
|
-
"element": element_desc,
|
|
1824
|
-
"duration": duration,
|
|
1825
|
-
"method": result.get('method', 'unknown'),
|
|
1826
|
-
"message": f"长按成功: {element_desc}, 持续{duration}秒"
|
|
1827
|
-
}, ensure_ascii=False, indent=2)
|
|
1828
|
-
)]
|
|
1829
|
-
else:
|
|
1830
|
-
return [TextContent(
|
|
1831
|
-
type="text",
|
|
1832
|
-
text=json.dumps({
|
|
1833
|
-
"success": False,
|
|
1834
|
-
"error": "需要提供element_desc或x,y坐标"
|
|
1835
|
-
}, ensure_ascii=False, indent=2)
|
|
1836
|
-
)]
|
|
1837
|
-
except Exception as e:
|
|
1838
|
-
return [TextContent(
|
|
1839
|
-
type="text",
|
|
1840
|
-
text=json.dumps({
|
|
1841
|
-
"success": False,
|
|
1842
|
-
"error": f"长按失败: {str(e)}"
|
|
1843
|
-
}, ensure_ascii=False, indent=2)
|
|
1844
|
-
)]
|
|
1845
|
-
|
|
1846
|
-
async def handle_mobile_open_url(self, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1847
|
-
"""打开URL"""
|
|
1848
|
-
await self.initialize()
|
|
1849
|
-
|
|
1850
|
-
try:
|
|
1851
|
-
url = arguments.get("url")
|
|
1852
|
-
if not url:
|
|
1853
|
-
return [TextContent(
|
|
1854
|
-
type="text",
|
|
1855
|
-
text=json.dumps({
|
|
1856
|
-
"success": False,
|
|
1857
|
-
"error": "缺少url参数"
|
|
1858
|
-
}, ensure_ascii=False, indent=2)
|
|
1859
|
-
)]
|
|
1860
|
-
|
|
1861
|
-
# 打开URL(使用默认浏览器)
|
|
1862
|
-
self.client.u2.open_url(url)
|
|
1863
|
-
|
|
1864
|
-
return [TextContent(
|
|
1865
|
-
type="text",
|
|
1866
|
-
text=json.dumps({
|
|
1867
|
-
"success": True,
|
|
1868
|
-
"url": url,
|
|
1869
|
-
"message": f"已在浏览器中打开: {url}"
|
|
1870
|
-
}, ensure_ascii=False, indent=2)
|
|
1871
|
-
)]
|
|
1872
|
-
except Exception as e:
|
|
1873
|
-
return [TextContent(
|
|
1874
|
-
type="text",
|
|
1875
|
-
text=json.dumps({
|
|
1876
|
-
"success": False,
|
|
1877
|
-
"error": f"打开URL失败: {str(e)}"
|
|
1878
|
-
}, ensure_ascii=False, indent=2)
|
|
1879
|
-
)]
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
async def main():
|
|
1883
|
-
"""MCP Server 主函数"""
|
|
1884
|
-
server_instance = MobileMCPServer()
|
|
1885
|
-
|
|
1886
|
-
# 创建 MCP Server
|
|
1887
|
-
server = Server("mobile-mcp-ai")
|
|
1888
|
-
|
|
1889
|
-
@server.list_tools()
|
|
1890
|
-
async def list_tools() -> list[Tool]:
|
|
1891
|
-
return server_instance.get_tools()
|
|
1892
|
-
|
|
1893
|
-
@server.call_tool()
|
|
1894
|
-
async def call_tool(name: str, arguments: Dict[str, Any]) -> list[TextContent]:
|
|
1895
|
-
return await server_instance.handle_tool_call(name, arguments)
|
|
1896
|
-
|
|
1897
|
-
# 运行 stdio 服务器
|
|
1898
|
-
async with stdio_server() as (read_stream, write_stream):
|
|
1899
|
-
# 使用 Server 的方法创建正确的 InitializationOptions
|
|
1900
|
-
initialization_options = server.create_initialization_options()
|
|
1901
|
-
await server.run(
|
|
1902
|
-
read_stream,
|
|
1903
|
-
write_stream,
|
|
1904
|
-
initialization_options=initialization_options
|
|
1905
|
-
)
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
if __name__ == "__main__":
|
|
1909
|
-
try:
|
|
1910
|
-
asyncio.run(main())
|
|
1911
|
-
except KeyboardInterrupt:
|
|
1912
|
-
print("⚠️ MCP Server 已停止", file=sys.stderr)
|
|
1913
|
-
except Exception as e:
|
|
1914
|
-
print(f"❌ MCP Server 错误: {e}", file=sys.stderr)
|
|
1915
|
-
import traceback
|
|
1916
|
-
traceback.print_exc()
|
|
1917
|
-
sys.exit(1)
|
|
1918
|
-
|
|
1919
|
-
|