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