mobile-mcp-ai 2.6.5__py3-none-any.whl → 2.6.7__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/config.py +0 -32
- mobile_mcp/core/basic_tools_lite.py +575 -385
- mobile_mcp/mcp_tools/mcp_server.py +345 -241
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/METADATA +1 -1
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/RECORD +9 -9
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/WHEEL +0 -0
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.6.5.dist-info → mobile_mcp_ai-2.6.7.dist-info}/top_level.txt +0 -0
|
@@ -102,19 +102,12 @@ class MobileMCPServer:
|
|
|
102
102
|
self.tools = None
|
|
103
103
|
self._initialized = False
|
|
104
104
|
self._last_error = None # 保存最后一次连接失败的错误
|
|
105
|
-
|
|
106
|
-
# Token 优化配置
|
|
107
|
-
try:
|
|
108
|
-
from mobile_mcp.config import Config
|
|
109
|
-
self._compact_desc = Config.COMPACT_TOOL_DESCRIPTION
|
|
110
|
-
except ImportError:
|
|
111
|
-
self._compact_desc = True # 默认开启精简模式
|
|
112
105
|
|
|
113
106
|
@staticmethod
|
|
114
107
|
def format_response(result) -> str:
|
|
115
|
-
"""
|
|
108
|
+
"""统一格式化返回值"""
|
|
116
109
|
if isinstance(result, (dict, list)):
|
|
117
|
-
return json.dumps(result, ensure_ascii=False,
|
|
110
|
+
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
118
111
|
return str(result)
|
|
119
112
|
|
|
120
113
|
async def initialize(self):
|
|
@@ -205,54 +198,42 @@ class MobileMCPServer:
|
|
|
205
198
|
return "android"
|
|
206
199
|
|
|
207
200
|
def get_tools(self):
|
|
208
|
-
"""注册 MCP
|
|
201
|
+
"""注册 MCP 工具(20 个)"""
|
|
209
202
|
tools = []
|
|
210
203
|
|
|
211
|
-
# 根据配置选择精简或完整描述
|
|
212
|
-
compact = getattr(self, '_compact_desc', True)
|
|
213
|
-
|
|
214
204
|
# ==================== 元素定位(优先使用)====================
|
|
215
|
-
if compact:
|
|
216
|
-
desc_list_elements = "📋 【首选】列出页面元素(token低)。返回text/id用于点击,替代截图确认页面状态。"
|
|
217
|
-
else:
|
|
218
|
-
desc_list_elements = ("📋 【⭐首选工具】列出页面所有可交互元素\n\n"
|
|
219
|
-
"🚀 Token 优化:返回文本数据(~500 tokens),远小于截图(~2000 tokens)!\n\n"
|
|
220
|
-
"✅ 推荐使用场景:\n"
|
|
221
|
-
"- 点击前确认元素存在\n"
|
|
222
|
-
"- 点击后确认页面变化(替代截图确认)\n"
|
|
223
|
-
"- 获取 text/id 用于 click_by_text/click_by_id\n\n"
|
|
224
|
-
"❌ 不要用截图确认页面,用此工具!\n"
|
|
225
|
-
"📌 只有需要看视觉布局时才用截图")
|
|
226
|
-
|
|
227
205
|
tools.append(Tool(
|
|
228
206
|
name="mobile_list_elements",
|
|
229
|
-
description=
|
|
207
|
+
description="📋 列出页面所有可交互元素\n\n"
|
|
208
|
+
"⚠️ 【重要】点击元素前必须先调用此工具!\n"
|
|
209
|
+
"如果元素在控件树中存在,使用 click_by_text 或 click_by_id 定位。\n"
|
|
210
|
+
"只有当此工具返回空或找不到目标元素时,才使用截图+坐标方式。\n\n"
|
|
211
|
+
"📌 控件树定位优势:\n"
|
|
212
|
+
"- 实时检测元素是否存在\n"
|
|
213
|
+
"- 元素消失时会报错,不会误点击\n"
|
|
214
|
+
"- 跨设备兼容性好",
|
|
230
215
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
231
216
|
))
|
|
232
217
|
|
|
233
218
|
# ==================== 截图(视觉兜底)====================
|
|
234
|
-
if compact:
|
|
235
|
-
desc_screenshot = "📸 截图(token高~2000)。优先用list_elements(~500)确认页面状态。"
|
|
236
|
-
else:
|
|
237
|
-
desc_screenshot = ("📸 截图查看屏幕内容(⚠️ Token 消耗高 ~2000)\n\n"
|
|
238
|
-
"❌ 【不推荐】确认页面状态请用 list_elements(Token 仅 ~500)!\n\n"
|
|
239
|
-
"✅ 仅在以下场景使用:\n"
|
|
240
|
-
"- 需要看视觉布局/图片内容\n"
|
|
241
|
-
"- 元素无 text/id,只能靠位置点击\n"
|
|
242
|
-
"- 调试问题需要可视化\n\n"
|
|
243
|
-
"💡 compress=false 可获取原图(用于添加模板)")
|
|
244
|
-
|
|
245
219
|
tools.append(Tool(
|
|
246
220
|
name="mobile_take_screenshot",
|
|
247
|
-
description=
|
|
221
|
+
description="📸 截图查看屏幕内容\n\n"
|
|
222
|
+
"⚠️ 【推荐使用 mobile_screenshot_with_som 代替!】\n"
|
|
223
|
+
"SoM 截图会给元素标号,AI 可以直接说'点击几号',更精准!\n\n"
|
|
224
|
+
"🎯 本工具仅用于:\n"
|
|
225
|
+
"- 快速确认页面状态(不需要点击时)\n"
|
|
226
|
+
"- 操作后确认结果\n"
|
|
227
|
+
"- compress=false 时可获取原始分辨率截图(用于添加模板)\n\n"
|
|
228
|
+
"💡 如需点击元素,请用 mobile_screenshot_with_som + mobile_click_by_som",
|
|
248
229
|
inputSchema={
|
|
249
230
|
"type": "object",
|
|
250
231
|
"properties": {
|
|
251
|
-
"description": {"type": "string", "description": "
|
|
252
|
-
"compress": {"type": "boolean", "description": "
|
|
253
|
-
"crop_x": {"type": "integer", "description": "
|
|
254
|
-
"crop_y": {"type": "integer", "description": "
|
|
255
|
-
"crop_size": {"type": "integer", "description": "
|
|
232
|
+
"description": {"type": "string", "description": "截图描述(可选)"},
|
|
233
|
+
"compress": {"type": "boolean", "description": "是否压缩,默认 true。设为 false 可获取原始分辨率(用于模板添加)", "default": True},
|
|
234
|
+
"crop_x": {"type": "integer", "description": "局部裁剪中心 X 坐标(屏幕坐标,0 表示不裁剪)"},
|
|
235
|
+
"crop_y": {"type": "integer", "description": "局部裁剪中心 Y 坐标(屏幕坐标,0 表示不裁剪)"},
|
|
236
|
+
"crop_size": {"type": "integer", "description": "裁剪区域大小(推荐 200-400,0 表示不裁剪)"}
|
|
256
237
|
},
|
|
257
238
|
"required": []
|
|
258
239
|
}
|
|
@@ -264,33 +245,36 @@ class MobileMCPServer:
|
|
|
264
245
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
265
246
|
))
|
|
266
247
|
|
|
267
|
-
if compact:
|
|
268
|
-
desc_som = "📸 SoM截图(token高)。元素有text时优先用list_elements+click_by_text。"
|
|
269
|
-
else:
|
|
270
|
-
desc_som = ("📸🏷️ Set-of-Mark 截图(⚠️ Token 消耗高 ~2000)\n\n"
|
|
271
|
-
"【智能标注】给每个可点击元素画框+编号\n\n"
|
|
272
|
-
"❌ 【不推荐常规使用】:\n"
|
|
273
|
-
"- 如果元素有 text,用 list_elements + click_by_text 更省 token\n"
|
|
274
|
-
"- 确认页面状态用 list_elements,不要截图确认!\n\n"
|
|
275
|
-
"✅ 仅在以下场景使用:\n"
|
|
276
|
-
"- 元素无 text/id,只能看图点击\n"
|
|
277
|
-
"- 需要视觉布局信息\n"
|
|
278
|
-
"- 首次探索未知页面\n\n"
|
|
279
|
-
"💡 点击后用 list_elements 确认,不要再截图!")
|
|
280
|
-
|
|
281
248
|
tools.append(Tool(
|
|
282
249
|
name="mobile_screenshot_with_som",
|
|
283
|
-
description=
|
|
250
|
+
description="📸🏷️ Set-of-Mark 截图(⭐⭐ 强烈推荐!默认截图方式)\n\n"
|
|
251
|
+
"【智能标注】给每个可点击元素画框+编号,检测弹窗时额外标注可能的X按钮位置(黄色)。\n"
|
|
252
|
+
"AI 看图直接说'点击 3 号',调用 mobile_click_by_som(3) 即可!\n\n"
|
|
253
|
+
"🎯 优势:\n"
|
|
254
|
+
"- 元素有编号,精准点击不会误触\n"
|
|
255
|
+
"- 自动检测弹窗,标注可能的关闭按钮位置\n"
|
|
256
|
+
"- 适用于所有页面和所有操作\n\n"
|
|
257
|
+
"⚡ 推荐流程:\n"
|
|
258
|
+
"1. 找不到目标元素时,优先调用此工具\n"
|
|
259
|
+
"2. 看标注图,找到目标元素编号\n"
|
|
260
|
+
"3. 调用 mobile_click_by_som(编号) 精准点击\n"
|
|
261
|
+
"4. 🔴【必须】点击后再次截图确认操作是否成功!",
|
|
284
262
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
285
263
|
))
|
|
286
264
|
|
|
287
265
|
tools.append(Tool(
|
|
288
266
|
name="mobile_click_by_som",
|
|
289
|
-
description="🎯 根据SoM
|
|
267
|
+
description="🎯 根据 SoM 编号点击元素\n\n"
|
|
268
|
+
"配合 mobile_screenshot_with_som 使用。\n"
|
|
269
|
+
"看图后直接说'点击 3 号',调用此函数即可。\n\n"
|
|
270
|
+
"⚠️ 【重要】点击后建议再次截图确认操作是否成功!",
|
|
290
271
|
inputSchema={
|
|
291
272
|
"type": "object",
|
|
292
273
|
"properties": {
|
|
293
|
-
"index": {
|
|
274
|
+
"index": {
|
|
275
|
+
"type": "integer",
|
|
276
|
+
"description": "元素编号(从 1 开始,对应截图中的标注数字)"
|
|
277
|
+
}
|
|
294
278
|
},
|
|
295
279
|
"required": ["index"]
|
|
296
280
|
}
|
|
@@ -298,92 +282,97 @@ class MobileMCPServer:
|
|
|
298
282
|
|
|
299
283
|
tools.append(Tool(
|
|
300
284
|
name="mobile_screenshot_with_grid",
|
|
301
|
-
description="
|
|
285
|
+
description="📸📏 带网格坐标的截图(精确定位神器!)\n\n"
|
|
286
|
+
"在截图上绘制网格线和坐标刻度,帮助快速定位元素位置。\n"
|
|
287
|
+
"如果检测到弹窗,会用绿色圆圈标注可能的关闭按钮位置。\n\n"
|
|
288
|
+
"🎯 适用场景:\n"
|
|
289
|
+
"- 需要精确知道某个元素的坐标\n"
|
|
290
|
+
"- 关闭广告弹窗时定位 X 按钮\n"
|
|
291
|
+
"- 元素不在控件树中时的视觉定位\n\n"
|
|
292
|
+
"💡 返回信息:\n"
|
|
293
|
+
"- 带网格标注的截图\n"
|
|
294
|
+
"- 弹窗边界坐标(如果检测到)\n"
|
|
295
|
+
"- 可能的关闭按钮位置列表(带优先级)\n\n"
|
|
296
|
+
"🔴 【必须】点击后必须再次截图确认操作是否成功!",
|
|
302
297
|
inputSchema={
|
|
303
298
|
"type": "object",
|
|
304
299
|
"properties": {
|
|
305
|
-
"grid_size": {
|
|
306
|
-
|
|
300
|
+
"grid_size": {
|
|
301
|
+
"type": "integer",
|
|
302
|
+
"description": "网格间距(像素),默认 100。值越小网格越密,建议 50-200"
|
|
303
|
+
},
|
|
304
|
+
"show_popup_hints": {
|
|
305
|
+
"type": "boolean",
|
|
306
|
+
"description": "是否显示弹窗关闭按钮提示位置,默认 true"
|
|
307
|
+
}
|
|
307
308
|
},
|
|
308
309
|
"required": []
|
|
309
310
|
}
|
|
310
311
|
))
|
|
311
312
|
|
|
312
313
|
# ==================== 点击操作 ====================
|
|
313
|
-
if compact:
|
|
314
|
-
desc_click_text = "👆 文本点击(推荐)。verify可验证点击结果,无需截图确认。position可选top/bottom/left/right。"
|
|
315
|
-
else:
|
|
316
|
-
desc_click_text = ("👆 通过文本点击元素(⭐ 最推荐)\n\n"
|
|
317
|
-
"✅ 最稳定的定位方式,跨设备兼容\n"
|
|
318
|
-
"✅ 元素不存在会报错,不会误点击\n\n"
|
|
319
|
-
"🚀 Token 优化流程:\n"
|
|
320
|
-
"1. list_elements 确认元素存在\n"
|
|
321
|
-
"2. click_by_text 点击\n"
|
|
322
|
-
"3. list_elements 确认页面变化(❌不要截图确认!)\n\n"
|
|
323
|
-
"📍 position 参数:多个相同文案时指定位置\n"
|
|
324
|
-
" - top/bottom/left/right/center\n\n"
|
|
325
|
-
"🔍 verify 参数:点击后自动验证文本是否出现\n"
|
|
326
|
-
" - 设置后无需额外调用 list_elements 确认")
|
|
327
|
-
|
|
328
314
|
tools.append(Tool(
|
|
329
315
|
name="mobile_click_by_text",
|
|
330
|
-
description=
|
|
316
|
+
description="👆 通过文本点击元素(最推荐)\n\n"
|
|
317
|
+
"✅ 最稳定的定位方式,跨设备兼容\n"
|
|
318
|
+
"✅ 实时检测元素是否存在,元素不存在会报错\n"
|
|
319
|
+
"✅ 不会误点击到其他位置\n"
|
|
320
|
+
"📋 使用前先调用 mobile_list_elements 确认元素文本\n"
|
|
321
|
+
"💡 定位优先级:文本 > ID > 百分比 > 坐标\n\n"
|
|
322
|
+
"📍 当页面有多个相同文案时,可使用 position 参数指定位置:\n"
|
|
323
|
+
" - 垂直方向: \"top\"/\"upper\"/\"上\", \"bottom\"/\"lower\"/\"下\", \"middle\"/\"center\"/\"中\"\n"
|
|
324
|
+
" - 水平方向: \"left\"/\"左\", \"right\"/\"右\", \"center\"/\"中\"\n"
|
|
325
|
+
" 例如:点击\"底部\"的\"微剧\"tab,使用 position=\"bottom\"",
|
|
331
326
|
inputSchema={
|
|
332
327
|
"type": "object",
|
|
333
328
|
"properties": {
|
|
334
|
-
"text": {"type": "string", "description": "
|
|
335
|
-
"position": {"type": "string", "description": "
|
|
336
|
-
"verify": {"type": "string", "description": "点击后验证的文本(可选)"}
|
|
329
|
+
"text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
|
|
330
|
+
"position": {"type": "string", "description": "位置信息(可选)。当有多个相同文案时使用,支持:top/bottom/left/right/middle 或 上/下/左/右/中"}
|
|
337
331
|
},
|
|
338
332
|
"required": ["text"]
|
|
339
333
|
}
|
|
340
334
|
))
|
|
341
335
|
|
|
342
|
-
if compact:
|
|
343
|
-
desc_click_id = "👆 通过resource-id点击。index指定第几个(从0开始)。点击后用list_elements确认。"
|
|
344
|
-
else:
|
|
345
|
-
desc_click_id = ("👆 通过 resource-id 点击元素(推荐)\n\n"
|
|
346
|
-
"✅ 稳定的定位方式,元素不存在会报错\n"
|
|
347
|
-
"📋 使用前 list_elements 获取元素 ID\n"
|
|
348
|
-
"📋 点击后 list_elements 确认(❌不要截图确认!)\n"
|
|
349
|
-
"💡 多个相同 ID 时用 index 指定第几个(从 0 开始)")
|
|
350
|
-
|
|
351
336
|
tools.append(Tool(
|
|
352
337
|
name="mobile_click_by_id",
|
|
353
|
-
description=
|
|
338
|
+
description="👆 通过 resource-id 点击元素(推荐)\n\n"
|
|
339
|
+
"✅ 稳定的定位方式\n"
|
|
340
|
+
"✅ 实时检测元素是否存在,元素不存在会报错\n"
|
|
341
|
+
"📋 使用前先调用 mobile_list_elements 获取元素 ID\n"
|
|
342
|
+
"💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)\n"
|
|
343
|
+
"💡 定位优先级:文本 > ID > 百分比 > 坐标",
|
|
354
344
|
inputSchema={
|
|
355
345
|
"type": "object",
|
|
356
346
|
"properties": {
|
|
357
|
-
"resource_id": {"type": "string", "description": "resource-id"},
|
|
358
|
-
"index": {"type": "integer", "description": "
|
|
347
|
+
"resource_id": {"type": "string", "description": "元素的 resource-id"},
|
|
348
|
+
"index": {"type": "integer", "description": "第几个元素(从 0 开始),默认 0 表示第一个", "default": 0}
|
|
359
349
|
},
|
|
360
350
|
"required": ["resource_id"]
|
|
361
351
|
}
|
|
362
352
|
))
|
|
363
353
|
|
|
364
|
-
if compact:
|
|
365
|
-
desc_click_coords = "👆 坐标点击(兜底)。优先用click_by_text/id,点击后用list_elements确认。"
|
|
366
|
-
else:
|
|
367
|
-
desc_click_coords = ("👆 点击指定坐标(⚠️ 兜底方案)\n\n"
|
|
368
|
-
"❌ 优先使用 click_by_text 或 click_by_id!\n"
|
|
369
|
-
"仅在 list_elements 无法获取元素时使用。\n\n"
|
|
370
|
-
"📐 坐标转换:截图返回的参数直接传入即可\n"
|
|
371
|
-
"📋 点击后用 list_elements 确认(❌不要截图确认!)")
|
|
372
|
-
|
|
373
354
|
tools.append(Tool(
|
|
374
355
|
name="mobile_click_at_coords",
|
|
375
|
-
description=
|
|
356
|
+
description="👆 点击指定坐标(兜底方案)\n\n"
|
|
357
|
+
"⚠️ 【重要】优先使用 mobile_click_by_text 或 mobile_click_by_id!\n"
|
|
358
|
+
"仅在 mobile_list_elements 无法获取元素时使用此工具。\n\n"
|
|
359
|
+
"⚠️ 【时序限制】截图分析期间页面可能变化:\n"
|
|
360
|
+
"- 坐标是基于截图时刻的,点击时页面可能已不同\n"
|
|
361
|
+
"- 如果误点击,调用 mobile_press_key(back) 返回\n"
|
|
362
|
+
"- 对于定时弹窗(如广告),建议等待其自动消失\n\n"
|
|
363
|
+
"📐 坐标转换:截图返回的 image_width/height 等参数直接传入即可\n\n"
|
|
364
|
+
"🔴 【必须】点击后必须再次截图确认操作是否成功!",
|
|
376
365
|
inputSchema={
|
|
377
366
|
"type": "object",
|
|
378
367
|
"properties": {
|
|
379
|
-
"x": {"type": "number", "description": "X
|
|
380
|
-
"y": {"type": "number", "description": "Y
|
|
381
|
-
"image_width": {"type": "number", "description": "
|
|
382
|
-
"image_height": {"type": "number", "description": "
|
|
383
|
-
"original_img_width": {"type": "number", "description": "
|
|
384
|
-
"original_img_height": {"type": "number", "description": "
|
|
385
|
-
"crop_offset_x": {"type": "number", "description": "
|
|
386
|
-
"crop_offset_y": {"type": "number", "description": "
|
|
368
|
+
"x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
|
|
369
|
+
"y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
|
|
370
|
+
"image_width": {"type": "number", "description": "压缩后图片宽度(截图返回的 image_width)"},
|
|
371
|
+
"image_height": {"type": "number", "description": "压缩后图片高度(截图返回的 image_height)"},
|
|
372
|
+
"original_img_width": {"type": "number", "description": "原图宽度(截图返回的 original_img_width)"},
|
|
373
|
+
"original_img_height": {"type": "number", "description": "原图高度(截图返回的 original_img_height)"},
|
|
374
|
+
"crop_offset_x": {"type": "number", "description": "局部截图 X 偏移(裁剪截图时传入)"},
|
|
375
|
+
"crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移(裁剪截图时传入)"}
|
|
387
376
|
},
|
|
388
377
|
"required": ["x", "y"]
|
|
389
378
|
}
|
|
@@ -391,7 +380,15 @@ class MobileMCPServer:
|
|
|
391
380
|
|
|
392
381
|
tools.append(Tool(
|
|
393
382
|
name="mobile_click_by_percent",
|
|
394
|
-
description="👆
|
|
383
|
+
description="👆 通过百分比位置点击(跨设备兼容!)。\n\n"
|
|
384
|
+
"🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
|
|
385
|
+
"📐 示例:\n"
|
|
386
|
+
" - (50, 50) = 屏幕正中央\n"
|
|
387
|
+
" - (10, 5) = 左上角附近\n"
|
|
388
|
+
" - (85, 90) = 右下角附近\n\n"
|
|
389
|
+
"✅ 优势:同样的百分比在不同分辨率设备上都能点到相同相对位置\n"
|
|
390
|
+
"💡 录制一次,多设备回放\n\n"
|
|
391
|
+
"🔴 【必须】点击后必须再次截图确认操作是否成功!",
|
|
395
392
|
inputSchema={
|
|
396
393
|
"type": "object",
|
|
397
394
|
"properties": {
|
|
@@ -405,12 +402,15 @@ class MobileMCPServer:
|
|
|
405
402
|
# ==================== 长按操作 ====================
|
|
406
403
|
tools.append(Tool(
|
|
407
404
|
name="mobile_long_press_by_id",
|
|
408
|
-
description="👆 通过resource-id
|
|
405
|
+
description="👆 通过 resource-id 长按(⭐⭐ 最稳定!)\n\n"
|
|
406
|
+
"✅ 最稳定的长按定位方式,跨设备完美兼容\n"
|
|
407
|
+
"📋 使用前请先调用 mobile_list_elements 获取元素 ID\n"
|
|
408
|
+
"💡 生成的脚本使用 d(resourceId='...').long_click() 定位,最稳定",
|
|
409
409
|
inputSchema={
|
|
410
410
|
"type": "object",
|
|
411
411
|
"properties": {
|
|
412
|
-
"resource_id": {"type": "string", "description": "resource-id"},
|
|
413
|
-
"duration": {"type": "number", "description": "
|
|
412
|
+
"resource_id": {"type": "string", "description": "元素的 resource-id"},
|
|
413
|
+
"duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
|
|
414
414
|
},
|
|
415
415
|
"required": ["resource_id"]
|
|
416
416
|
}
|
|
@@ -418,12 +418,15 @@ class MobileMCPServer:
|
|
|
418
418
|
|
|
419
419
|
tools.append(Tool(
|
|
420
420
|
name="mobile_long_press_by_text",
|
|
421
|
-
description="👆
|
|
421
|
+
description="👆 通过文本长按(⭐ 推荐!)\n\n"
|
|
422
|
+
"✅ 优势:跨设备兼容,不受屏幕分辨率影响\n"
|
|
423
|
+
"📋 使用前请先调用 mobile_list_elements 确认元素有文本\n"
|
|
424
|
+
"💡 生成的脚本使用 d(text='...').long_click() 定位,稳定可靠",
|
|
422
425
|
inputSchema={
|
|
423
426
|
"type": "object",
|
|
424
427
|
"properties": {
|
|
425
|
-
"text": {"type": "string", "description": "
|
|
426
|
-
"duration": {"type": "number", "description": "
|
|
428
|
+
"text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
|
|
429
|
+
"duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
|
|
427
430
|
},
|
|
428
431
|
"required": ["text"]
|
|
429
432
|
}
|
|
@@ -431,13 +434,20 @@ class MobileMCPServer:
|
|
|
431
434
|
|
|
432
435
|
tools.append(Tool(
|
|
433
436
|
name="mobile_long_press_by_percent",
|
|
434
|
-
description="👆
|
|
437
|
+
description="👆 通过百分比位置长按(跨设备兼容!)\n\n"
|
|
438
|
+
"🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
|
|
439
|
+
"📐 示例:\n"
|
|
440
|
+
" - (50, 50) = 屏幕正中央\n"
|
|
441
|
+
" - (10, 5) = 左上角附近\n"
|
|
442
|
+
" - (85, 90) = 右下角附近\n\n"
|
|
443
|
+
"✅ 优势:同样的百分比在不同分辨率设备上都能长按到相同相对位置\n"
|
|
444
|
+
"💡 录制一次,多设备回放",
|
|
435
445
|
inputSchema={
|
|
436
446
|
"type": "object",
|
|
437
447
|
"properties": {
|
|
438
|
-
"x_percent": {"type": "number", "description": "X
|
|
439
|
-
"y_percent": {"type": "number", "description": "Y
|
|
440
|
-
"duration": {"type": "number", "description": "
|
|
448
|
+
"x_percent": {"type": "number", "description": "X 轴百分比 (0-100)"},
|
|
449
|
+
"y_percent": {"type": "number", "description": "Y 轴百分比 (0-100)"},
|
|
450
|
+
"duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
|
|
441
451
|
},
|
|
442
452
|
"required": ["x_percent", "y_percent"]
|
|
443
453
|
}
|
|
@@ -445,19 +455,28 @@ class MobileMCPServer:
|
|
|
445
455
|
|
|
446
456
|
tools.append(Tool(
|
|
447
457
|
name="mobile_long_press_at_coords",
|
|
448
|
-
description="👆
|
|
458
|
+
description="👆 长按指定坐标(⚠️ 兜底方案,优先用文本/ID定位!)\n\n"
|
|
459
|
+
"🎯 仅在以下场景使用:\n"
|
|
460
|
+
"- 游戏(Unity/Cocos)无法获取元素\n"
|
|
461
|
+
"- mobile_list_elements 返回空\n"
|
|
462
|
+
"- 元素没有 id 和 text\n\n"
|
|
463
|
+
"⚠️ 【坐标转换】截图返回的参数直接传入:\n"
|
|
464
|
+
" - image_width/image_height: 压缩后尺寸(AI 看到的)\n"
|
|
465
|
+
" - original_img_width/original_img_height: 原图尺寸(用于转换)\n"
|
|
466
|
+
" - crop_offset_x/crop_offset_y: 局部截图偏移\n\n"
|
|
467
|
+
"✅ 自动记录百分比坐标,生成脚本时转换为跨分辨率兼容的百分比定位",
|
|
449
468
|
inputSchema={
|
|
450
469
|
"type": "object",
|
|
451
470
|
"properties": {
|
|
452
|
-
"x": {"type": "number", "description": "X
|
|
453
|
-
"y": {"type": "number", "description": "Y
|
|
454
|
-
"duration": {"type": "number", "description": "
|
|
455
|
-
"image_width": {"type": "number", "description": "
|
|
456
|
-
"image_height": {"type": "number", "description": "
|
|
457
|
-
"original_img_width": {"type": "number", "description": "
|
|
458
|
-
"original_img_height": {"type": "number", "description": "
|
|
459
|
-
"crop_offset_x": {"type": "number", "description": "
|
|
460
|
-
"crop_offset_y": {"type": "number", "description": "
|
|
471
|
+
"x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
|
|
472
|
+
"y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
|
|
473
|
+
"duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"},
|
|
474
|
+
"image_width": {"type": "number", "description": "压缩后图片宽度"},
|
|
475
|
+
"image_height": {"type": "number", "description": "压缩后图片高度"},
|
|
476
|
+
"original_img_width": {"type": "number", "description": "原图宽度"},
|
|
477
|
+
"original_img_height": {"type": "number", "description": "原图高度"},
|
|
478
|
+
"crop_offset_x": {"type": "number", "description": "局部截图 X 偏移"},
|
|
479
|
+
"crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移"}
|
|
461
480
|
},
|
|
462
481
|
"required": ["x", "y"]
|
|
463
482
|
}
|
|
@@ -466,12 +485,12 @@ class MobileMCPServer:
|
|
|
466
485
|
# ==================== 输入操作 ====================
|
|
467
486
|
tools.append(Tool(
|
|
468
487
|
name="mobile_input_text_by_id",
|
|
469
|
-
description="⌨️
|
|
488
|
+
description="⌨️ 在输入框输入文本。需要先用 mobile_list_elements 获取输入框 ID。",
|
|
470
489
|
inputSchema={
|
|
471
490
|
"type": "object",
|
|
472
491
|
"properties": {
|
|
473
|
-
"resource_id": {"type": "string", "description": "resource-id"},
|
|
474
|
-
"text": {"type": "string", "description": "
|
|
492
|
+
"resource_id": {"type": "string", "description": "输入框的 resource-id"},
|
|
493
|
+
"text": {"type": "string", "description": "要输入的文本"}
|
|
475
494
|
},
|
|
476
495
|
"required": ["resource_id", "text"]
|
|
477
496
|
}
|
|
@@ -479,13 +498,13 @@ class MobileMCPServer:
|
|
|
479
498
|
|
|
480
499
|
tools.append(Tool(
|
|
481
500
|
name="mobile_input_at_coords",
|
|
482
|
-
description="⌨️
|
|
501
|
+
description="⌨️ 点击坐标后输入文本。适合游戏等无法获取元素 ID 的场景。",
|
|
483
502
|
inputSchema={
|
|
484
503
|
"type": "object",
|
|
485
504
|
"properties": {
|
|
486
|
-
"x": {"type": "number", "description": "X坐标"},
|
|
487
|
-
"y": {"type": "number", "description": "Y坐标"},
|
|
488
|
-
"text": {"type": "string", "description": "
|
|
505
|
+
"x": {"type": "number", "description": "输入框 X 坐标"},
|
|
506
|
+
"y": {"type": "number", "description": "输入框 Y 坐标"},
|
|
507
|
+
"text": {"type": "string", "description": "要输入的文本"}
|
|
489
508
|
},
|
|
490
509
|
"required": ["x", "y", "text"]
|
|
491
510
|
}
|
|
@@ -494,13 +513,48 @@ class MobileMCPServer:
|
|
|
494
513
|
# ==================== 导航操作 ====================
|
|
495
514
|
tools.append(Tool(
|
|
496
515
|
name="mobile_swipe",
|
|
497
|
-
description="👆
|
|
516
|
+
description="👆 滑动屏幕。方向:up/down/left/right\n\n"
|
|
517
|
+
"🎯 适用场景:\n"
|
|
518
|
+
"- 滑动页面(列表、页面切换)\n"
|
|
519
|
+
"- 拖动进度条/滑块(SeekBar、ProgressBar)\n"
|
|
520
|
+
"- 滑动选择器(Picker、Slider)\n\n"
|
|
521
|
+
"💡 左右滑动时,可指定高度坐标或百分比:\n"
|
|
522
|
+
"- y: 指定高度坐标(像素)\n"
|
|
523
|
+
"- y_percent: 指定高度百分比 (0-100)\n"
|
|
524
|
+
"- 两者都未指定时,使用屏幕中心高度\n"
|
|
525
|
+
"- 📌 拖动进度条时,使用进度条的 Y 位置(百分比或像素)\n\n"
|
|
526
|
+
"💡 横向滑动(left/right)时,可指定滑动距离:\n"
|
|
527
|
+
"- distance: 滑动距离(像素)\n"
|
|
528
|
+
"- distance_percent: 滑动距离百分比 (0-100)\n"
|
|
529
|
+
"- 两者都未指定时,使用默认距离(屏幕宽度的 60%)\n"
|
|
530
|
+
"- 📌 拖动进度条时,distance_percent 控制拖动幅度\n\n"
|
|
531
|
+
"💡 拖动进度条示例:\n"
|
|
532
|
+
"- 倒退:direction='left', y_percent=91(进度条位置), distance_percent=30\n"
|
|
533
|
+
"- 前进:direction='right', y_percent=91, distance_percent=30",
|
|
498
534
|
inputSchema={
|
|
499
535
|
"type": "object",
|
|
500
536
|
"properties": {
|
|
501
|
-
"direction": {
|
|
502
|
-
|
|
503
|
-
|
|
537
|
+
"direction": {
|
|
538
|
+
"type": "string",
|
|
539
|
+
"enum": ["up", "down", "left", "right"],
|
|
540
|
+
"description": "滑动方向"
|
|
541
|
+
},
|
|
542
|
+
"y": {
|
|
543
|
+
"type": "integer",
|
|
544
|
+
"description": "左右滑动时指定的高度坐标(像素,0-屏幕高度)"
|
|
545
|
+
},
|
|
546
|
+
"y_percent": {
|
|
547
|
+
"type": "number",
|
|
548
|
+
"description": "左右滑动时指定的高度百分比 (0-100)"
|
|
549
|
+
},
|
|
550
|
+
"distance": {
|
|
551
|
+
"type": "integer",
|
|
552
|
+
"description": "横向滑动时指定的滑动距离(像素),仅用于 left/right"
|
|
553
|
+
},
|
|
554
|
+
"distance_percent": {
|
|
555
|
+
"type": "number",
|
|
556
|
+
"description": "横向滑动时指定的滑动距离百分比 (0-100),仅用于 left/right"
|
|
557
|
+
}
|
|
504
558
|
},
|
|
505
559
|
"required": ["direction"]
|
|
506
560
|
}
|
|
@@ -508,11 +562,11 @@ class MobileMCPServer:
|
|
|
508
562
|
|
|
509
563
|
tools.append(Tool(
|
|
510
564
|
name="mobile_press_key",
|
|
511
|
-
description="⌨️
|
|
565
|
+
description="⌨️ 按键操作。支持:home, back, enter, search",
|
|
512
566
|
inputSchema={
|
|
513
567
|
"type": "object",
|
|
514
568
|
"properties": {
|
|
515
|
-
"key": {"type": "string", "description": "
|
|
569
|
+
"key": {"type": "string", "description": "按键名称:home, back, enter, search"}
|
|
516
570
|
},
|
|
517
571
|
"required": ["key"]
|
|
518
572
|
}
|
|
@@ -520,11 +574,11 @@ class MobileMCPServer:
|
|
|
520
574
|
|
|
521
575
|
tools.append(Tool(
|
|
522
576
|
name="mobile_wait",
|
|
523
|
-
description="⏰
|
|
577
|
+
description="⏰ 等待指定时间。用于等待页面加载、动画完成等。",
|
|
524
578
|
inputSchema={
|
|
525
579
|
"type": "object",
|
|
526
580
|
"properties": {
|
|
527
|
-
"seconds": {"type": "number", "description": "
|
|
581
|
+
"seconds": {"type": "number", "description": "等待时间(秒)"}
|
|
528
582
|
},
|
|
529
583
|
"required": ["seconds"]
|
|
530
584
|
}
|
|
@@ -533,11 +587,11 @@ class MobileMCPServer:
|
|
|
533
587
|
# ==================== 应用管理 ====================
|
|
534
588
|
tools.append(Tool(
|
|
535
589
|
name="mobile_launch_app",
|
|
536
|
-
description="🚀
|
|
590
|
+
description="🚀 启动应用。启动后建议等待 2-3 秒让页面加载。",
|
|
537
591
|
inputSchema={
|
|
538
592
|
"type": "object",
|
|
539
593
|
"properties": {
|
|
540
|
-
"package_name": {"type": "string", "description": "
|
|
594
|
+
"package_name": {"type": "string", "description": "应用包名"}
|
|
541
595
|
},
|
|
542
596
|
"required": ["package_name"]
|
|
543
597
|
}
|
|
@@ -549,7 +603,7 @@ class MobileMCPServer:
|
|
|
549
603
|
inputSchema={
|
|
550
604
|
"type": "object",
|
|
551
605
|
"properties": {
|
|
552
|
-
"package_name": {"type": "string", "description": "
|
|
606
|
+
"package_name": {"type": "string", "description": "应用包名"}
|
|
553
607
|
},
|
|
554
608
|
"required": ["package_name"]
|
|
555
609
|
}
|
|
@@ -557,11 +611,11 @@ class MobileMCPServer:
|
|
|
557
611
|
|
|
558
612
|
tools.append(Tool(
|
|
559
613
|
name="mobile_list_apps",
|
|
560
|
-
description="📦
|
|
614
|
+
description="📦 列出已安装的应用。可按关键词过滤。",
|
|
561
615
|
inputSchema={
|
|
562
616
|
"type": "object",
|
|
563
617
|
"properties": {
|
|
564
|
-
"filter": {"type": "string", "description": "
|
|
618
|
+
"filter": {"type": "string", "description": "过滤关键词(可选)"}
|
|
565
619
|
},
|
|
566
620
|
"required": []
|
|
567
621
|
}
|
|
@@ -570,21 +624,20 @@ class MobileMCPServer:
|
|
|
570
624
|
# ==================== 设备管理 ====================
|
|
571
625
|
tools.append(Tool(
|
|
572
626
|
name="mobile_list_devices",
|
|
573
|
-
description="📱
|
|
627
|
+
description="📱 列出已连接的设备。",
|
|
574
628
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
575
629
|
))
|
|
576
630
|
|
|
577
631
|
tools.append(Tool(
|
|
578
632
|
name="mobile_check_connection",
|
|
579
|
-
description="🔌
|
|
633
|
+
description="🔌 检查设备连接状态。",
|
|
580
634
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
581
635
|
))
|
|
582
636
|
|
|
583
637
|
# ==================== 辅助工具 ====================
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
desc_find_close = """🔍 智能查找关闭按钮(只找不点,返回位置)
|
|
638
|
+
tools.append(Tool(
|
|
639
|
+
name="mobile_find_close_button",
|
|
640
|
+
description="""🔍 智能查找关闭按钮(只找不点,返回位置)
|
|
588
641
|
|
|
589
642
|
⚡ 【推荐首选】遇到弹窗时优先调用此工具!无需先截图。
|
|
590
643
|
|
|
@@ -604,68 +657,79 @@ class MobileMCPServer:
|
|
|
604
657
|
💡 使用流程:
|
|
605
658
|
1. 直接调用此工具(无需先截图/列元素)
|
|
606
659
|
2. 根据返回的 click_command 执行点击
|
|
607
|
-
3. 如果返回 success=false,才需要截图分析"""
|
|
608
|
-
|
|
609
|
-
tools.append(Tool(
|
|
610
|
-
name="mobile_find_close_button",
|
|
611
|
-
description=desc_find_close,
|
|
660
|
+
3. 如果返回 success=false,才需要截图分析""",
|
|
612
661
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
613
662
|
))
|
|
614
663
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
desc_close_popup = """🚫 智能检测并关闭弹窗
|
|
664
|
+
tools.append(Tool(
|
|
665
|
+
name="mobile_close_popup",
|
|
666
|
+
description="""🚫 智能关闭弹窗
|
|
619
667
|
|
|
620
|
-
|
|
621
|
-
- 如果没有弹窗 → 直接返回"无弹窗",不执行任何操作
|
|
622
|
-
- 如果有弹窗 → 自动查找并点击关闭按钮
|
|
668
|
+
通过控件树识别并点击关闭按钮(×、关闭、跳过等)。
|
|
623
669
|
|
|
624
|
-
✅
|
|
625
|
-
|
|
626
|
-
- 页面跳转后检测并关闭弹窗
|
|
627
|
-
- 无需先截图确认弹窗是否存在
|
|
670
|
+
✅ 控件树有元素时:直接点击,实时可靠
|
|
671
|
+
❌ 控件树无元素时:截图供 AI 分析
|
|
628
672
|
|
|
629
|
-
|
|
630
|
-
-
|
|
631
|
-
-
|
|
632
|
-
-
|
|
673
|
+
⚠️ 【时序限制】如果需要截图分析:
|
|
674
|
+
- 分析期间弹窗可能自动消失
|
|
675
|
+
- 对于定时弹窗(如广告),建议等待其自动消失
|
|
676
|
+
- 点击前可再次截图确认弹窗是否还在
|
|
633
677
|
|
|
634
|
-
🔴
|
|
635
|
-
|
|
636
|
-
tools.append(Tool(
|
|
637
|
-
name="mobile_close_popup",
|
|
638
|
-
description=desc_close_popup,
|
|
678
|
+
🔴 【必须】点击关闭后,必须再次截图确认弹窗是否真的关闭了!
|
|
679
|
+
如果弹窗仍在,需要尝试其他方法或位置。""",
|
|
639
680
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
640
681
|
))
|
|
641
682
|
|
|
642
683
|
tools.append(Tool(
|
|
643
684
|
name="mobile_assert_text",
|
|
644
|
-
description="✅
|
|
685
|
+
description="✅ 检查页面是否包含指定文本。用于验证操作结果。",
|
|
645
686
|
inputSchema={
|
|
646
687
|
"type": "object",
|
|
647
688
|
"properties": {
|
|
648
|
-
"text": {"type": "string", "description": "
|
|
689
|
+
"text": {"type": "string", "description": "要检查的文本"}
|
|
649
690
|
},
|
|
650
691
|
"required": ["text"]
|
|
651
692
|
}
|
|
652
693
|
))
|
|
653
694
|
|
|
654
|
-
# ==================== Toast
|
|
695
|
+
# ==================== Toast 检测工具(仅 Android)====================
|
|
655
696
|
tools.append(Tool(
|
|
656
697
|
name="mobile_start_toast_watch",
|
|
657
|
-
description="🔔 开始监听Toast
|
|
658
|
-
|
|
698
|
+
description="""🔔 开始监听 Toast(仅 Android)
|
|
699
|
+
|
|
700
|
+
⚠️ 【重要】必须在执行操作之前调用!
|
|
701
|
+
|
|
702
|
+
📋 正确流程(三步走):
|
|
703
|
+
1️⃣ 调用 mobile_start_toast_watch() 开始监听
|
|
704
|
+
2️⃣ 执行操作(如点击提交按钮)
|
|
705
|
+
3️⃣ 调用 mobile_get_toast() 或 mobile_assert_toast() 获取结果
|
|
706
|
+
|
|
707
|
+
❌ 错误用法:先点击按钮,再调用此工具(Toast 可能已消失)""",
|
|
708
|
+
inputSchema={
|
|
709
|
+
"type": "object",
|
|
710
|
+
"properties": {},
|
|
711
|
+
"required": []
|
|
712
|
+
}
|
|
659
713
|
))
|
|
660
714
|
|
|
661
715
|
tools.append(Tool(
|
|
662
716
|
name="mobile_get_toast",
|
|
663
|
-
description="🍞 获取Toast
|
|
717
|
+
description="""🍞 获取 Toast 消息(仅 Android)
|
|
718
|
+
|
|
719
|
+
Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果。
|
|
720
|
+
⚠️ Toast 不在控件树中,无法通过 mobile_list_elements 获取。
|
|
721
|
+
|
|
722
|
+
📋 推荐用法(三步走):
|
|
723
|
+
1️⃣ mobile_start_toast_watch() - 开始监听
|
|
724
|
+
2️⃣ 执行操作(点击按钮等)
|
|
725
|
+
3️⃣ mobile_get_toast() - 获取 Toast
|
|
726
|
+
|
|
727
|
+
⏱️ timeout 设置等待时间,默认 5 秒。""",
|
|
664
728
|
inputSchema={
|
|
665
729
|
"type": "object",
|
|
666
730
|
"properties": {
|
|
667
|
-
"timeout": {"type": "number", "description": "
|
|
668
|
-
"reset_first": {"type": "boolean", "description": "
|
|
731
|
+
"timeout": {"type": "number", "description": "等待 Toast 出现的超时时间(秒),默认 5"},
|
|
732
|
+
"reset_first": {"type": "boolean", "description": "是否先清除旧缓存,默认 False"}
|
|
669
733
|
},
|
|
670
734
|
"required": []
|
|
671
735
|
}
|
|
@@ -673,13 +737,22 @@ class MobileMCPServer:
|
|
|
673
737
|
|
|
674
738
|
tools.append(Tool(
|
|
675
739
|
name="mobile_assert_toast",
|
|
676
|
-
description="✅ 断言Toast
|
|
740
|
+
description="""✅ 断言 Toast 消息(仅 Android)
|
|
741
|
+
|
|
742
|
+
等待 Toast 出现并验证内容是否符合预期。
|
|
743
|
+
|
|
744
|
+
📋 推荐用法(三步走):
|
|
745
|
+
1️⃣ mobile_start_toast_watch() - 开始监听
|
|
746
|
+
2️⃣ 执行操作(点击按钮等)
|
|
747
|
+
3️⃣ mobile_assert_toast(expected_text="成功") - 断言
|
|
748
|
+
|
|
749
|
+
💡 支持包含匹配(默认)和精确匹配。""",
|
|
677
750
|
inputSchema={
|
|
678
751
|
"type": "object",
|
|
679
752
|
"properties": {
|
|
680
|
-
"expected_text": {"type": "string", "description": "
|
|
681
|
-
"timeout": {"type": "number", "description": "
|
|
682
|
-
"contains": {"type": "boolean", "description": "
|
|
753
|
+
"expected_text": {"type": "string", "description": "期望的 Toast 文本"},
|
|
754
|
+
"timeout": {"type": "number", "description": "等待超时时间(秒),默认 5"},
|
|
755
|
+
"contains": {"type": "boolean", "description": "True=包含匹配(默认),False=精确匹配"}
|
|
683
756
|
},
|
|
684
757
|
"required": ["expected_text"]
|
|
685
758
|
}
|
|
@@ -688,11 +761,11 @@ class MobileMCPServer:
|
|
|
688
761
|
# ==================== pytest 脚本生成 ====================
|
|
689
762
|
tools.append(Tool(
|
|
690
763
|
name="mobile_get_operation_history",
|
|
691
|
-
description="📜
|
|
764
|
+
description="📜 获取操作历史记录。",
|
|
692
765
|
inputSchema={
|
|
693
766
|
"type": "object",
|
|
694
767
|
"properties": {
|
|
695
|
-
"limit": {"type": "number", "description": "
|
|
768
|
+
"limit": {"type": "number", "description": "返回最近的N条记录"}
|
|
696
769
|
},
|
|
697
770
|
"required": []
|
|
698
771
|
}
|
|
@@ -700,53 +773,63 @@ class MobileMCPServer:
|
|
|
700
773
|
|
|
701
774
|
tools.append(Tool(
|
|
702
775
|
name="mobile_clear_operation_history",
|
|
703
|
-
description="🗑️
|
|
776
|
+
description="🗑️ 清空操作历史记录。\n\n"
|
|
777
|
+
"⚠️ 开始新的测试录制前必须调用!\n"
|
|
778
|
+
"📋 录制流程:清空历史 → 执行操作(优先用文本/ID定位)→ 生成脚本",
|
|
704
779
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
705
780
|
))
|
|
706
781
|
|
|
707
782
|
tools.append(Tool(
|
|
708
783
|
name="mobile_generate_test_script",
|
|
709
|
-
description="📝 生成pytest
|
|
784
|
+
description="📝 生成 pytest 测试脚本。基于操作历史自动生成。\n\n"
|
|
785
|
+
"⚠️ 【重要】录制操作时请优先使用稳定定位:\n"
|
|
786
|
+
"1️⃣ 先调用 mobile_list_elements 获取元素列表\n"
|
|
787
|
+
"2️⃣ 优先用 mobile_click_by_text(最稳定,跨设备兼容)\n"
|
|
788
|
+
"3️⃣ 其次用 mobile_click_by_id(稳定)\n"
|
|
789
|
+
"4️⃣ 最后才用坐标点击(会自动转百分比,跨分辨率兼容)\n\n"
|
|
790
|
+
"使用流程:\n"
|
|
791
|
+
"1. 清空历史 mobile_clear_operation_history\n"
|
|
792
|
+
"2. 执行操作(优先用文本/ID定位)\n"
|
|
793
|
+
"3. 调用此工具生成脚本\n"
|
|
794
|
+
"4. 脚本保存到 tests/ 目录\n\n"
|
|
795
|
+
"💡 定位优先级:文本 > ID > 百分比 > 坐标",
|
|
710
796
|
inputSchema={
|
|
711
797
|
"type": "object",
|
|
712
798
|
"properties": {
|
|
713
|
-
"test_name": {"type": "string", "description": "
|
|
714
|
-
"package_name": {"type": "string", "description": "包名"},
|
|
715
|
-
"filename": {"type": "string", "description": "
|
|
799
|
+
"test_name": {"type": "string", "description": "测试用例名称"},
|
|
800
|
+
"package_name": {"type": "string", "description": "App 包名"},
|
|
801
|
+
"filename": {"type": "string", "description": "脚本文件名(不含 .py)"}
|
|
716
802
|
},
|
|
717
803
|
"required": ["test_name", "package_name", "filename"]
|
|
718
804
|
}
|
|
719
805
|
))
|
|
720
806
|
|
|
721
807
|
# ==================== 广告弹窗关闭工具 ====================
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
desc_close_ad = """🚫 【推荐】智能检测并关闭广告弹窗
|
|
808
|
+
tools.append(Tool(
|
|
809
|
+
name="mobile_close_ad",
|
|
810
|
+
description="""🚫 【推荐】智能关闭广告弹窗
|
|
726
811
|
|
|
727
|
-
⚡
|
|
728
|
-
- 如果没有弹窗 → 直接返回"无弹窗",不执行任何操作
|
|
729
|
-
- 如果有弹窗 → 自动按优先级尝试关闭
|
|
812
|
+
⚡ 直接调用即可,无需先截图!会自动按优先级尝试:
|
|
730
813
|
|
|
731
|
-
|
|
732
|
-
|
|
814
|
+
1️⃣ **控件树查找**(最可靠,优先)
|
|
815
|
+
- 自动查找 resource-id 包含 close/dismiss
|
|
733
816
|
- 查找文本"关闭"、"跳过"、"×"等
|
|
734
|
-
-
|
|
735
|
-
|
|
736
|
-
2️⃣ **截图 AI 分析**(次优)
|
|
737
|
-
- 返回 SoM 标注截图供 AI 视觉分析
|
|
738
|
-
- AI 找到 X 按钮后用 click_by_som(编号) 点击
|
|
817
|
+
- 找到直接点击,实时可靠
|
|
739
818
|
|
|
740
|
-
|
|
819
|
+
2️⃣ **模板匹配**(次优)
|
|
741
820
|
- 用 OpenCV 匹配已保存的 X 按钮模板
|
|
821
|
+
- 模板越多成功率越高
|
|
742
822
|
|
|
743
|
-
|
|
744
|
-
-
|
|
745
|
-
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
823
|
+
3️⃣ **返回截图供 AI 分析**(兜底)
|
|
824
|
+
- 前两步都失败才截图
|
|
825
|
+
- AI 分析后用 mobile_click_by_percent 点击
|
|
826
|
+
- 点击成功后用 mobile_template_add 添加模板
|
|
827
|
+
|
|
828
|
+
💡 正确流程:
|
|
829
|
+
1. 遇到广告弹窗 → 直接调用此工具
|
|
830
|
+
2. 如果成功 → 完成
|
|
831
|
+
3. 只有失败时才需要截图分析
|
|
832
|
+
3. 如果失败 → 看截图找 X → 点击 → 添加模板""",
|
|
750
833
|
inputSchema={
|
|
751
834
|
"type": "object",
|
|
752
835
|
"properties": {},
|
|
@@ -756,12 +839,19 @@ class MobileMCPServer:
|
|
|
756
839
|
|
|
757
840
|
tools.append(Tool(
|
|
758
841
|
name="mobile_template_close",
|
|
759
|
-
description="🎯
|
|
842
|
+
description="""🎯 模板匹配关闭弹窗(仅模板匹配)
|
|
843
|
+
|
|
844
|
+
只用 OpenCV 模板匹配,不走控件树。
|
|
845
|
+
一般建议用 mobile_close_ad 代替(会自动先查控件树)。
|
|
846
|
+
|
|
847
|
+
⚙️ 参数:
|
|
848
|
+
- click: 是否点击,默认 true
|
|
849
|
+
- threshold: 匹配阈值 0-1,默认 0.75""",
|
|
760
850
|
inputSchema={
|
|
761
851
|
"type": "object",
|
|
762
852
|
"properties": {
|
|
763
|
-
"click": {"type": "boolean", "description": "
|
|
764
|
-
"threshold": {"type": "number", "description": "
|
|
853
|
+
"click": {"type": "boolean", "description": "是否点击,默认 true"},
|
|
854
|
+
"threshold": {"type": "number", "description": "匹配阈值 0-1,默认 0.75"}
|
|
765
855
|
},
|
|
766
856
|
"required": []
|
|
767
857
|
}
|
|
@@ -769,19 +859,32 @@ class MobileMCPServer:
|
|
|
769
859
|
|
|
770
860
|
tools.append(Tool(
|
|
771
861
|
name="mobile_template_add",
|
|
772
|
-
description="➕ 添加X
|
|
862
|
+
description="""➕ 添加 X 号模板
|
|
863
|
+
|
|
864
|
+
遇到新样式 X 号时,截图并添加到模板库。
|
|
865
|
+
|
|
866
|
+
⚙️ 两种方式(二选一):
|
|
867
|
+
1. 百分比定位(推荐):提供 x_percent, y_percent, size
|
|
868
|
+
2. 像素定位:提供 screenshot_path, x, y, width, height
|
|
869
|
+
|
|
870
|
+
📋 流程:
|
|
871
|
+
1. mobile_screenshot_with_grid 查看 X 号位置
|
|
872
|
+
2. 调用此工具添加模板
|
|
873
|
+
3. 下次同样 X 号就能自动匹配
|
|
874
|
+
|
|
875
|
+
💡 百分比示例:X 在右上角 → x_percent=85, y_percent=12, size=80""",
|
|
773
876
|
inputSchema={
|
|
774
877
|
"type": "object",
|
|
775
878
|
"properties": {
|
|
776
|
-
"template_name": {"type": "string", "description": "
|
|
777
|
-
"x_percent": {"type": "number", "description": "X
|
|
778
|
-
"y_percent": {"type": "number", "description": "
|
|
779
|
-
"size": {"type": "integer", "description": "
|
|
780
|
-
"screenshot_path": {"type": "string", "description": "
|
|
781
|
-
"x": {"type": "integer", "description": "
|
|
782
|
-
"y": {"type": "integer", "description": "
|
|
783
|
-
"width": {"type": "integer", "description": "
|
|
784
|
-
"height": {"type": "integer", "description": "
|
|
879
|
+
"template_name": {"type": "string", "description": "模板名称"},
|
|
880
|
+
"x_percent": {"type": "number", "description": "X号中心水平百分比 (0-100)"},
|
|
881
|
+
"y_percent": {"type": "number", "description": "X号中心垂直百分比 (0-100)"},
|
|
882
|
+
"size": {"type": "integer", "description": "裁剪正方形边长(像素)"},
|
|
883
|
+
"screenshot_path": {"type": "string", "description": "截图路径(像素定位时用)"},
|
|
884
|
+
"x": {"type": "integer", "description": "左上角 X 坐标"},
|
|
885
|
+
"y": {"type": "integer", "description": "左上角 Y 坐标"},
|
|
886
|
+
"width": {"type": "integer", "description": "裁剪宽度"},
|
|
887
|
+
"height": {"type": "integer", "description": "裁剪高度"}
|
|
785
888
|
},
|
|
786
889
|
"required": ["template_name"]
|
|
787
890
|
}
|
|
@@ -857,8 +960,7 @@ class MobileMCPServer:
|
|
|
857
960
|
elif name == "mobile_click_by_text":
|
|
858
961
|
result = self.tools.click_by_text(
|
|
859
962
|
arguments["text"],
|
|
860
|
-
position=arguments.get("position")
|
|
861
|
-
verify=arguments.get("verify")
|
|
963
|
+
position=arguments.get("position")
|
|
862
964
|
)
|
|
863
965
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
864
966
|
|
|
@@ -924,7 +1026,9 @@ class MobileMCPServer:
|
|
|
924
1026
|
result = await self.tools.swipe(
|
|
925
1027
|
arguments["direction"],
|
|
926
1028
|
y=arguments.get("y"),
|
|
927
|
-
y_percent=arguments.get("y_percent")
|
|
1029
|
+
y_percent=arguments.get("y_percent"),
|
|
1030
|
+
distance=arguments.get("distance"),
|
|
1031
|
+
distance_percent=arguments.get("distance_percent")
|
|
928
1032
|
)
|
|
929
1033
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
930
1034
|
|