mobile-mcp-ai 2.6.10__py3-none-any.whl → 2.7.0__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.
@@ -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, indent=2)
117
+ return json.dumps(result, ensure_ascii=False, separators=(',', ':'))
111
118
  return str(result)
112
119
 
113
120
  async def initialize(self):
@@ -197,43 +204,164 @@ class MobileMCPServer:
197
204
 
198
205
  return "android"
199
206
 
207
+ def _open_new_chat(self, message: str = "继续执行飞书用例", delay: float = 5) -> dict:
208
+ """
209
+ 打开新Chat窗口并发送消息
210
+
211
+ 原理:用后台线程延迟执行键盘操作,避免打断当前AI响应
212
+ 跨平台统一用 pyautogui + pyperclip,配合系统API激活窗口
213
+ """
214
+ import threading
215
+ import platform
216
+
217
+ def delayed_action():
218
+ import time
219
+ time.sleep(delay) # 等待当前响应结束
220
+
221
+ try:
222
+ system = platform.system()
223
+
224
+ if system == "Darwin": # ========== macOS: 纯 AppleScript(无需额外安装)==========
225
+ import subprocess
226
+
227
+ # AppleScript: 激活Cursor → Cmd+L聚焦Chat → Cmd+N新建 → 粘贴 → 回车
228
+ script = f'''
229
+ -- 设置剪贴板(支持中文)
230
+ set the clipboard to "{message}"
231
+
232
+ -- 激活 Cursor
233
+ tell application "Cursor" to activate
234
+ delay 0.5
235
+
236
+ tell application "System Events"
237
+ tell process "Cursor"
238
+ -- Cmd+L 聚焦 Chat
239
+ keystroke "l" using command down
240
+ delay 0.5
241
+ -- Cmd+N 新建对话
242
+ keystroke "n" using command down
243
+ delay 1
244
+ -- Cmd+V 粘贴
245
+ keystroke "v" using command down
246
+ delay 0.3
247
+ -- Enter 发送
248
+ key code 36
249
+ end tell
250
+ end tell
251
+ '''
252
+ result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True)
253
+ if result.returncode == 0:
254
+ print(f"[open_new_chat] macOS成功: {message}", file=sys.stderr)
255
+ else:
256
+ print(f"[open_new_chat] AppleScript错误: {result.stderr}", file=sys.stderr)
257
+
258
+ elif system == "Windows": # ========== Windows: 需要 pip install pyautogui pyperclip ==========
259
+ import pyautogui
260
+ import pyperclip
261
+
262
+ # 尝试激活 Cursor 窗口
263
+ try:
264
+ import pygetwindow as gw
265
+ windows = gw.getWindowsWithTitle('Cursor')
266
+ if windows:
267
+ windows[0].activate()
268
+ time.sleep(0.5)
269
+ except ImportError:
270
+ pass
271
+
272
+ pyautogui.PAUSE = 0.3
273
+
274
+ # Ctrl+L 聚焦 Chat → Ctrl+N 新建
275
+ pyautogui.hotkey('ctrl', 'l')
276
+ time.sleep(0.5)
277
+ pyautogui.hotkey('ctrl', 'n')
278
+ time.sleep(1)
279
+
280
+ # 粘贴消息
281
+ pyperclip.copy(message)
282
+ pyautogui.hotkey('ctrl', 'v')
283
+ time.sleep(0.3)
284
+ pyautogui.press('enter')
285
+
286
+ print(f"[open_new_chat] Windows成功: {message}", file=sys.stderr)
287
+
288
+ else: # Linux: 写信号文件
289
+ from pathlib import Path
290
+ signal_file = Path(__file__).parent.parent / ".new_chat_signal"
291
+ signal_file.write_text(message)
292
+ print(f"[open_new_chat] Linux: 已写入信号文件", file=sys.stderr)
293
+
294
+ except ImportError as e:
295
+ # Windows 缺少依赖时写信号文件
296
+ from pathlib import Path
297
+ signal_file = Path(__file__).parent.parent / ".new_chat_signal"
298
+ signal_file.write_text(message)
299
+ print(f"[open_new_chat] Windows缺少依赖,请运行: pip install pyautogui pyperclip", file=sys.stderr)
300
+
301
+ except Exception as e:
302
+ # 写错误日志
303
+ import sys
304
+ print(f"[open_new_chat] 错误: {e}", file=sys.stderr)
305
+
306
+ # 后台线程执行,不阻塞MCP响应
307
+ thread = threading.Thread(target=delayed_action, daemon=True)
308
+ thread.start()
309
+
310
+ return {
311
+ "success": True,
312
+ "message": f"⏰ {delay}秒后将打开新Chat并发送: {message}",
313
+ "tip": "请不要手动操作,等待自动执行"
314
+ }
315
+
200
316
  def get_tools(self):
201
- """注册 MCP 工具(20 个)"""
317
+ """注册 MCP 工具"""
202
318
  tools = []
203
319
 
320
+ # 根据配置选择精简或完整描述
321
+ compact = getattr(self, '_compact_desc', True)
322
+
204
323
  # ==================== 元素定位(优先使用)====================
324
+ if compact:
325
+ desc_list_elements = "📋 【首选】列出页面元素(token低)。返回text/id用于点击,替代截图确认页面状态。"
326
+ else:
327
+ desc_list_elements = ("📋 【⭐首选工具】列出页面所有可交互元素\n\n"
328
+ "🚀 Token 优化:返回文本数据(~500 tokens),远小于截图(~2000 tokens)!\n\n"
329
+ "✅ 推荐使用场景:\n"
330
+ "- 点击前确认元素存在\n"
331
+ "- 点击后确认页面变化(替代截图确认)\n"
332
+ "- 获取 text/id 用于 click_by_text/click_by_id\n\n"
333
+ "❌ 不要用截图确认页面,用此工具!\n"
334
+ "📌 只有需要看视觉布局时才用截图")
335
+
205
336
  tools.append(Tool(
206
337
  name="mobile_list_elements",
207
- description="📋 列出页面所有可交互元素\n\n"
208
- "⚠️ 【重要】点击元素前必须先调用此工具!\n"
209
- "如果元素在控件树中存在,使用 click_by_text 或 click_by_id 定位。\n"
210
- "只有当此工具返回空或找不到目标元素时,才使用截图+坐标方式。\n\n"
211
- "📌 控件树定位优势:\n"
212
- "- 实时检测元素是否存在\n"
213
- "- 元素消失时会报错,不会误点击\n"
214
- "- 跨设备兼容性好",
338
+ description=desc_list_elements,
215
339
  inputSchema={"type": "object", "properties": {}, "required": []}
216
340
  ))
217
341
 
218
342
  # ==================== 截图(视觉兜底)====================
343
+ if compact:
344
+ desc_screenshot = "📸 截图(token高~2000)。优先用list_elements(~500)确认页面状态。"
345
+ else:
346
+ desc_screenshot = ("📸 截图查看屏幕内容(⚠️ Token 消耗高 ~2000)\n\n"
347
+ "❌ 【不推荐】确认页面状态请用 list_elements(Token 仅 ~500)!\n\n"
348
+ "✅ 仅在以下场景使用:\n"
349
+ "- 需要看视觉布局/图片内容\n"
350
+ "- 元素无 text/id,只能靠位置点击\n"
351
+ "- 调试问题需要可视化\n\n"
352
+ "💡 compress=false 可获取原图(用于添加模板)")
353
+
219
354
  tools.append(Tool(
220
355
  name="mobile_take_screenshot",
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",
356
+ description=desc_screenshot,
229
357
  inputSchema={
230
358
  "type": "object",
231
359
  "properties": {
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 表示不裁剪)"}
360
+ "description": {"type": "string", "description": "截图描述"},
361
+ "compress": {"type": "boolean", "description": "是否压缩", "default": True},
362
+ "crop_x": {"type": "integer", "description": "裁剪中心 X"},
363
+ "crop_y": {"type": "integer", "description": "裁剪中心 Y"},
364
+ "crop_size": {"type": "integer", "description": "裁剪大小"}
237
365
  },
238
366
  "required": []
239
367
  }
@@ -245,48 +373,33 @@ class MobileMCPServer:
245
373
  inputSchema={"type": "object", "properties": {}, "required": []}
246
374
  ))
247
375
 
376
+ if compact:
377
+ desc_som = "📸 SoM截图(token高)。元素有text时优先用list_elements+click_by_text。"
378
+ else:
379
+ desc_som = ("📸🏷️ Set-of-Mark 截图(⚠️ Token 消耗高 ~2000)\n\n"
380
+ "【智能标注】给每个可点击元素画框+编号\n\n"
381
+ "❌ 【不推荐常规使用】:\n"
382
+ "- 如果元素有 text,用 list_elements + click_by_text 更省 token\n"
383
+ "- 确认页面状态用 list_elements,不要截图确认!\n\n"
384
+ "✅ 仅在以下场景使用:\n"
385
+ "- 元素无 text/id,只能看图点击\n"
386
+ "- 需要视觉布局信息\n"
387
+ "- 首次探索未知页面\n\n"
388
+ "💡 点击后用 list_elements 确认,不要再截图!")
389
+
248
390
  tools.append(Tool(
249
391
  name="mobile_screenshot_with_som",
250
- description="📸🏷️ Set-of-Mark 截图(⭐⭐ 强烈推荐!默认截图方式)\n\n"
251
- "【智能标注】给每个可点击元素画框+编号。\n"
252
- "AI 看图直接说'点击 3 号',调用 mobile_click_by_som(3) 即可!\n\n"
253
- "🎯 优势:\n"
254
- "- 元素有编号,精准点击不会误触\n"
255
- "- 适用于所有页面和所有操作\n\n"
256
- "⚡ 推荐流程:\n"
257
- "1. 找不到目标元素时,优先调用此工具\n"
258
- "2. 看标注图,找到目标元素编号\n"
259
- "3. 调用 mobile_click_by_som(编号) 精准点击\n"
260
- "4. 🔴【必须】点击后再次截图确认操作是否成功!\n\n"
261
- "💡 弹窗检测:\n"
262
- "- check_popup=True: 明确弹窗场景时使用(如调用 mobile_close_popup 前)\n"
263
- "- check_popup=False: 普通截图,不检测弹窗(默认)",
264
- inputSchema={
265
- "type": "object",
266
- "properties": {
267
- "check_popup": {
268
- "type": "boolean",
269
- "description": "是否检测弹窗,默认 False。仅在明确弹窗场景时设置为 True",
270
- "default": False
271
- }
272
- },
273
- "required": []
274
- }
392
+ description=desc_som,
393
+ inputSchema={"type": "object", "properties": {}, "required": []}
275
394
  ))
276
395
 
277
396
  tools.append(Tool(
278
397
  name="mobile_click_by_som",
279
- description="🎯 根据 SoM 编号点击元素\n\n"
280
- "配合 mobile_screenshot_with_som 使用。\n"
281
- "看图后直接说'点击 3 号',调用此函数即可。\n\n"
282
- "⚠️ 【重要】点击后建议再次截图确认操作是否成功!",
398
+ description="🎯 根据SoM编号点击。配合screenshot_with_som使用。",
283
399
  inputSchema={
284
400
  "type": "object",
285
401
  "properties": {
286
- "index": {
287
- "type": "integer",
288
- "description": "元素编号(从 1 开始,对应截图中的标注数字)"
289
- }
402
+ "index": {"type": "integer", "description": "元素编号(从1开始)"}
290
403
  },
291
404
  "required": ["index"]
292
405
  }
@@ -294,97 +407,92 @@ class MobileMCPServer:
294
407
 
295
408
  tools.append(Tool(
296
409
  name="mobile_screenshot_with_grid",
297
- description="📸📏 带网格坐标的截图(精确定位神器!)\n\n"
298
- "在截图上绘制网格线和坐标刻度,帮助快速定位元素位置。\n"
299
- "如果检测到弹窗,会用绿色圆圈标注可能的关闭按钮位置。\n\n"
300
- "🎯 适用场景:\n"
301
- "- 需要精确知道某个元素的坐标\n"
302
- "- 关闭广告弹窗时定位 X 按钮\n"
303
- "- 元素不在控件树中时的视觉定位\n\n"
304
- "💡 返回信息:\n"
305
- "- 带网格标注的截图\n"
306
- "- 弹窗边界坐标(如果检测到)\n"
307
- "- 可能的关闭按钮位置列表(带优先级)\n\n"
308
- "🔴 【必须】点击后必须再次截图确认操作是否成功!",
410
+ description="📸 带网格坐标截图。用于精确定位元素坐标。",
309
411
  inputSchema={
310
412
  "type": "object",
311
413
  "properties": {
312
- "grid_size": {
313
- "type": "integer",
314
- "description": "网格间距(像素),默认 100。值越小网格越密,建议 50-200"
315
- },
316
- "show_popup_hints": {
317
- "type": "boolean",
318
- "description": "是否显示弹窗关闭按钮提示位置,默认 true"
319
- }
414
+ "grid_size": {"type": "integer", "description": "网格间距(px),默认100"},
415
+ "show_popup_hints": {"type": "boolean", "description": "显示弹窗提示"}
320
416
  },
321
417
  "required": []
322
418
  }
323
419
  ))
324
420
 
325
421
  # ==================== 点击操作 ====================
422
+ if compact:
423
+ desc_click_text = "👆 文本点击(推荐)。verify可验证点击结果,无需截图确认。position可选top/bottom/left/right。"
424
+ else:
425
+ desc_click_text = ("👆 通过文本点击元素(⭐ 最推荐)\n\n"
426
+ "✅ 最稳定的定位方式,跨设备兼容\n"
427
+ "✅ 元素不存在会报错,不会误点击\n\n"
428
+ "🚀 Token 优化流程:\n"
429
+ "1. list_elements 确认元素存在\n"
430
+ "2. click_by_text 点击\n"
431
+ "3. list_elements 确认页面变化(❌不要截图确认!)\n\n"
432
+ "📍 position 参数:多个相同文案时指定位置\n"
433
+ " - top/bottom/left/right/center\n\n"
434
+ "🔍 verify 参数:点击后自动验证文本是否出现\n"
435
+ " - 设置后无需额外调用 list_elements 确认")
436
+
326
437
  tools.append(Tool(
327
438
  name="mobile_click_by_text",
328
- description="👆 通过文本点击元素(最推荐)\n\n"
329
- "✅ 最稳定的定位方式,跨设备兼容\n"
330
- "✅ 实时检测元素是否存在,元素不存在会报错\n"
331
- "✅ 不会误点击到其他位置\n"
332
- "📋 使用前先调用 mobile_list_elements 确认元素文本\n"
333
- "💡 定位优先级:文本 > ID > 百分比 > 坐标\n\n"
334
- "📍 当页面有多个相同文案时,可使用 position 参数指定位置:\n"
335
- " - 垂直方向: \"top\"/\"upper\"/\"上\", \"bottom\"/\"lower\"/\"下\", \"middle\"/\"center\"/\"中\"\n"
336
- " - 水平方向: \"left\"/\"左\", \"right\"/\"右\", \"center\"/\"中\"\n"
337
- " 例如:点击\"底部\"的\"微剧\"tab,使用 position=\"bottom\"",
439
+ description=desc_click_text,
338
440
  inputSchema={
339
441
  "type": "object",
340
442
  "properties": {
341
- "text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
342
- "position": {"type": "string", "description": "位置信息(可选)。当有多个相同文案时使用,支持:top/bottom/left/right/middle 或 上/下/左/右/中"}
443
+ "text": {"type": "string", "description": "元素文本"},
444
+ "position": {"type": "string", "description": "位置:top/bottom/left/right"},
445
+ "verify": {"type": "string", "description": "点击后验证的文本(可选)"}
343
446
  },
344
447
  "required": ["text"]
345
448
  }
346
449
  ))
347
450
 
451
+ if compact:
452
+ desc_click_id = "👆 通过resource-id点击。index指定第几个(从0开始)。点击后用list_elements确认。"
453
+ else:
454
+ desc_click_id = ("👆 通过 resource-id 点击元素(推荐)\n\n"
455
+ "✅ 稳定的定位方式,元素不存在会报错\n"
456
+ "📋 使用前 list_elements 获取元素 ID\n"
457
+ "📋 点击后 list_elements 确认(❌不要截图确认!)\n"
458
+ "💡 多个相同 ID 时用 index 指定第几个(从 0 开始)")
459
+
348
460
  tools.append(Tool(
349
461
  name="mobile_click_by_id",
350
- description="👆 通过 resource-id 点击元素(推荐)\n\n"
351
- "✅ 稳定的定位方式\n"
352
- "✅ 实时检测元素是否存在,元素不存在会报错\n"
353
- "📋 使用前先调用 mobile_list_elements 获取元素 ID\n"
354
- "💡 当有多个相同 ID 的元素时,用 index 指定第几个(从 0 开始)\n"
355
- "💡 定位优先级:文本 > ID > 百分比 > 坐标",
462
+ description=desc_click_id,
356
463
  inputSchema={
357
464
  "type": "object",
358
465
  "properties": {
359
- "resource_id": {"type": "string", "description": "元素的 resource-id"},
360
- "index": {"type": "integer", "description": "第几个元素(从 0 开始),默认 0 表示第一个", "default": 0}
466
+ "resource_id": {"type": "string", "description": "resource-id"},
467
+ "index": {"type": "integer", "description": "第几个(从0开始)", "default": 0}
361
468
  },
362
469
  "required": ["resource_id"]
363
470
  }
364
471
  ))
365
472
 
473
+ if compact:
474
+ desc_click_coords = "👆 坐标点击(兜底)。优先用click_by_text/id,点击后用list_elements确认。"
475
+ else:
476
+ desc_click_coords = ("👆 点击指定坐标(⚠️ 兜底方案)\n\n"
477
+ "❌ 优先使用 click_by_text 或 click_by_id!\n"
478
+ "仅在 list_elements 无法获取元素时使用。\n\n"
479
+ "📐 坐标转换:截图返回的参数直接传入即可\n"
480
+ "📋 点击后用 list_elements 确认(❌不要截图确认!)")
481
+
366
482
  tools.append(Tool(
367
483
  name="mobile_click_at_coords",
368
- description="👆 点击指定坐标(兜底方案)\n\n"
369
- "⚠️ 【重要】优先使用 mobile_click_by_text 或 mobile_click_by_id!\n"
370
- "仅在 mobile_list_elements 无法获取元素时使用此工具。\n\n"
371
- "⚠️ 【时序限制】截图分析期间页面可能变化:\n"
372
- "- 坐标是基于截图时刻的,点击时页面可能已不同\n"
373
- "- 如果误点击,调用 mobile_press_key(back) 返回\n"
374
- "- 对于定时弹窗(如广告),建议等待其自动消失\n\n"
375
- "📐 坐标转换:截图返回的 image_width/height 等参数直接传入即可\n\n"
376
- "🔴 【必须】点击后必须再次截图确认操作是否成功!",
484
+ description=desc_click_coords,
377
485
  inputSchema={
378
486
  "type": "object",
379
487
  "properties": {
380
- "x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
381
- "y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
382
- "image_width": {"type": "number", "description": "压缩后图片宽度(截图返回的 image_width)"},
383
- "image_height": {"type": "number", "description": "压缩后图片高度(截图返回的 image_height)"},
384
- "original_img_width": {"type": "number", "description": "原图宽度(截图返回的 original_img_width)"},
385
- "original_img_height": {"type": "number", "description": "原图高度(截图返回的 original_img_height)"},
386
- "crop_offset_x": {"type": "number", "description": "局部截图 X 偏移(裁剪截图时传入)"},
387
- "crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移(裁剪截图时传入)"}
488
+ "x": {"type": "number", "description": "X 坐标"},
489
+ "y": {"type": "number", "description": "Y 坐标"},
490
+ "image_width": {"type": "number", "description": "图片宽度"},
491
+ "image_height": {"type": "number", "description": "图片高度"},
492
+ "original_img_width": {"type": "number", "description": "原图宽"},
493
+ "original_img_height": {"type": "number", "description": "原图高"},
494
+ "crop_offset_x": {"type": "number", "description": "裁剪X偏移"},
495
+ "crop_offset_y": {"type": "number", "description": "裁剪Y偏移"}
388
496
  },
389
497
  "required": ["x", "y"]
390
498
  }
@@ -392,15 +500,7 @@ class MobileMCPServer:
392
500
 
393
501
  tools.append(Tool(
394
502
  name="mobile_click_by_percent",
395
- description="👆 通过百分比位置点击(跨设备兼容!)。\n\n"
396
- "🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
397
- "📐 示例:\n"
398
- " - (50, 50) = 屏幕正中央\n"
399
- " - (10, 5) = 左上角附近\n"
400
- " - (85, 90) = 右下角附近\n\n"
401
- "✅ 优势:同样的百分比在不同分辨率设备上都能点到相同相对位置\n"
402
- "💡 录制一次,多设备回放\n\n"
403
- "🔴 【必须】点击后必须再次截图确认操作是否成功!",
503
+ description="👆 百分比点击。(50,50)=屏幕中心。跨设备兼容。",
404
504
  inputSchema={
405
505
  "type": "object",
406
506
  "properties": {
@@ -414,15 +514,12 @@ class MobileMCPServer:
414
514
  # ==================== 长按操作 ====================
415
515
  tools.append(Tool(
416
516
  name="mobile_long_press_by_id",
417
- description="👆 通过 resource-id 长按(⭐⭐ 最稳定!)\n\n"
418
- "✅ 最稳定的长按定位方式,跨设备完美兼容\n"
419
- "📋 使用前请先调用 mobile_list_elements 获取元素 ID\n"
420
- "💡 生成的脚本使用 d(resourceId='...').long_click() 定位,最稳定",
517
+ description="👆 通过resource-id长按。",
421
518
  inputSchema={
422
519
  "type": "object",
423
520
  "properties": {
424
- "resource_id": {"type": "string", "description": "元素的 resource-id"},
425
- "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
521
+ "resource_id": {"type": "string", "description": "resource-id"},
522
+ "duration": {"type": "number", "description": "长按秒数,默认1.0"}
426
523
  },
427
524
  "required": ["resource_id"]
428
525
  }
@@ -430,15 +527,12 @@ class MobileMCPServer:
430
527
 
431
528
  tools.append(Tool(
432
529
  name="mobile_long_press_by_text",
433
- description="👆 通过文本长按(⭐ 推荐!)\n\n"
434
- "✅ 优势:跨设备兼容,不受屏幕分辨率影响\n"
435
- "📋 使用前请先调用 mobile_list_elements 确认元素有文本\n"
436
- "💡 生成的脚本使用 d(text='...').long_click() 定位,稳定可靠",
530
+ description="👆 通过文本长按。",
437
531
  inputSchema={
438
532
  "type": "object",
439
533
  "properties": {
440
- "text": {"type": "string", "description": "元素的文本内容(精确匹配)"},
441
- "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
534
+ "text": {"type": "string", "description": "文本内容"},
535
+ "duration": {"type": "number", "description": "长按秒数,默认1.0"}
442
536
  },
443
537
  "required": ["text"]
444
538
  }
@@ -446,20 +540,13 @@ class MobileMCPServer:
446
540
 
447
541
  tools.append(Tool(
448
542
  name="mobile_long_press_by_percent",
449
- description="👆 通过百分比位置长按(跨设备兼容!)\n\n"
450
- "🎯 原理:屏幕左上角是 (0%, 0%),右下角是 (100%, 100%)\n"
451
- "📐 示例:\n"
452
- " - (50, 50) = 屏幕正中央\n"
453
- " - (10, 5) = 左上角附近\n"
454
- " - (85, 90) = 右下角附近\n\n"
455
- "✅ 优势:同样的百分比在不同分辨率设备上都能长按到相同相对位置\n"
456
- "💡 录制一次,多设备回放",
543
+ description="👆 百分比长按。(50,50)=屏幕中心。",
457
544
  inputSchema={
458
545
  "type": "object",
459
546
  "properties": {
460
- "x_percent": {"type": "number", "description": "X 轴百分比 (0-100)"},
461
- "y_percent": {"type": "number", "description": "Y 轴百分比 (0-100)"},
462
- "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"}
547
+ "x_percent": {"type": "number", "description": "X百分比(0-100)"},
548
+ "y_percent": {"type": "number", "description": "Y百分比(0-100)"},
549
+ "duration": {"type": "number", "description": "长按秒数,默认1.0"}
463
550
  },
464
551
  "required": ["x_percent", "y_percent"]
465
552
  }
@@ -467,28 +554,19 @@ class MobileMCPServer:
467
554
 
468
555
  tools.append(Tool(
469
556
  name="mobile_long_press_at_coords",
470
- description="👆 长按指定坐标(⚠️ 兜底方案,优先用文本/ID定位!)\n\n"
471
- "🎯 仅在以下场景使用:\n"
472
- "- 游戏(Unity/Cocos)无法获取元素\n"
473
- "- mobile_list_elements 返回空\n"
474
- "- 元素没有 id 和 text\n\n"
475
- "⚠️ 【坐标转换】截图返回的参数直接传入:\n"
476
- " - image_width/image_height: 压缩后尺寸(AI 看到的)\n"
477
- " - original_img_width/original_img_height: 原图尺寸(用于转换)\n"
478
- " - crop_offset_x/crop_offset_y: 局部截图偏移\n\n"
479
- "✅ 自动记录百分比坐标,生成脚本时转换为跨分辨率兼容的百分比定位",
557
+ description="👆 坐标长按(兜底)。优先用text/id。",
480
558
  inputSchema={
481
559
  "type": "object",
482
560
  "properties": {
483
- "x": {"type": "number", "description": "X 坐标(来自 AI 分析截图)"},
484
- "y": {"type": "number", "description": "Y 坐标(来自 AI 分析截图)"},
485
- "duration": {"type": "number", "description": "长按持续时间(秒),默认 1.0"},
486
- "image_width": {"type": "number", "description": "压缩后图片宽度"},
487
- "image_height": {"type": "number", "description": "压缩后图片高度"},
488
- "original_img_width": {"type": "number", "description": "原图宽度"},
489
- "original_img_height": {"type": "number", "description": "原图高度"},
490
- "crop_offset_x": {"type": "number", "description": "局部截图 X 偏移"},
491
- "crop_offset_y": {"type": "number", "description": "局部截图 Y 偏移"}
561
+ "x": {"type": "number", "description": "X坐标"},
562
+ "y": {"type": "number", "description": "Y坐标"},
563
+ "duration": {"type": "number", "description": "长按秒数"},
564
+ "image_width": {"type": "number", "description": "图片宽"},
565
+ "image_height": {"type": "number", "description": "图片高"},
566
+ "original_img_width": {"type": "number", "description": "原图宽"},
567
+ "original_img_height": {"type": "number", "description": "原图高"},
568
+ "crop_offset_x": {"type": "number", "description": "裁剪X偏移"},
569
+ "crop_offset_y": {"type": "number", "description": "裁剪Y偏移"}
492
570
  },
493
571
  "required": ["x", "y"]
494
572
  }
@@ -497,12 +575,12 @@ class MobileMCPServer:
497
575
  # ==================== 输入操作 ====================
498
576
  tools.append(Tool(
499
577
  name="mobile_input_text_by_id",
500
- description="⌨️ 在输入框输入文本。需要先用 mobile_list_elements 获取输入框 ID",
578
+ description="⌨️ 通过ID输入文本。",
501
579
  inputSchema={
502
580
  "type": "object",
503
581
  "properties": {
504
- "resource_id": {"type": "string", "description": "输入框的 resource-id"},
505
- "text": {"type": "string", "description": "要输入的文本"}
582
+ "resource_id": {"type": "string", "description": "resource-id"},
583
+ "text": {"type": "string", "description": "输入文本"}
506
584
  },
507
585
  "required": ["resource_id", "text"]
508
586
  }
@@ -510,13 +588,13 @@ class MobileMCPServer:
510
588
 
511
589
  tools.append(Tool(
512
590
  name="mobile_input_at_coords",
513
- description="⌨️ 点击坐标后输入文本。适合游戏等无法获取元素 ID 的场景。",
591
+ description="⌨️ 坐标输入文本。",
514
592
  inputSchema={
515
593
  "type": "object",
516
594
  "properties": {
517
- "x": {"type": "number", "description": "输入框 X 坐标"},
518
- "y": {"type": "number", "description": "输入框 Y 坐标"},
519
- "text": {"type": "string", "description": "要输入的文本"}
595
+ "x": {"type": "number", "description": "X坐标"},
596
+ "y": {"type": "number", "description": "Y坐标"},
597
+ "text": {"type": "string", "description": "输入文本"}
520
598
  },
521
599
  "required": ["x", "y", "text"]
522
600
  }
@@ -525,105 +603,25 @@ class MobileMCPServer:
525
603
  # ==================== 导航操作 ====================
526
604
  tools.append(Tool(
527
605
  name="mobile_swipe",
528
- description="👆 滑动屏幕。方向:up/down/left/right\n\n"
529
- "🎯 适用场景:\n"
530
- "- 滑动页面(列表、页面切换)\n"
531
- "- 拖动进度条/滑块(SeekBar、ProgressBar)\n"
532
- "- 滑动选择器(Picker、Slider)\n\n"
533
- "💡 左右滑动时,可指定高度坐标或百分比:\n"
534
- "- y: 指定高度坐标(像素)\n"
535
- "- y_percent: 指定高度百分比 (0-100)\n"
536
- "- 两者都未指定时,使用屏幕中心高度\n"
537
- "- 📌 拖动进度条时,使用进度条的 Y 位置(百分比或像素)\n\n"
538
- "💡 横向滑动(left/right)时,可指定滑动距离:\n"
539
- "- distance: 滑动距离(像素)\n"
540
- "- distance_percent: 滑动距离百分比 (0-100)\n"
541
- "- 两者都未指定时,使用默认距离(屏幕宽度的 60%)\n"
542
- "- 📌 拖动进度条时,distance_percent 控制拖动幅度\n\n"
543
- "💡 拖动进度条示例:\n"
544
- "- 倒退:direction='left', y_percent=91(进度条位置), distance_percent=30\n"
545
- "- 前进:direction='right', y_percent=91, distance_percent=30\n\n"
546
- "⚠️ **推荐使用 mobile_drag_progress_bar 拖动进度条**(自动检测进度条位置,无需手动指定)",
606
+ description="👆 滑动。方向:up/down/left/right",
547
607
  inputSchema={
548
608
  "type": "object",
549
609
  "properties": {
550
- "direction": {
551
- "type": "string",
552
- "enum": ["up", "down", "left", "right"],
553
- "description": "滑动方向"
554
- },
555
- "y": {
556
- "type": "integer",
557
- "description": "左右滑动时指定的高度坐标(像素,0-屏幕高度)"
558
- },
559
- "y_percent": {
560
- "type": "number",
561
- "description": "左右滑动时指定的高度百分比 (0-100)"
562
- },
563
- "distance": {
564
- "type": "integer",
565
- "description": "横向滑动时指定的滑动距离(像素),仅用于 left/right"
566
- },
567
- "distance_percent": {
568
- "type": "number",
569
- "description": "横向滑动时指定的滑动距离百分比 (0-100),仅用于 left/right"
570
- }
610
+ "direction": {"type": "string", "enum": ["up", "down", "left", "right"], "description": "方向"},
611
+ "y": {"type": "integer", "description": "左右滑动高度(px)"},
612
+ "y_percent": {"type": "number", "description": "左右滑动高度(%)"}
571
613
  },
572
614
  "required": ["direction"]
573
615
  }
574
616
  ))
575
617
 
576
- tools.append(Tool(
577
- name="mobile_drag_progress_bar",
578
- description="🎯 智能拖动进度条(⭐⭐ 推荐用于拖动视频/音频进度条)\n\n"
579
- "✅ **自动检测进度条是否可见**:\n"
580
- "- 如果进度条已显示,直接拖动(无需先点击播放区域)\n"
581
- "- 如果进度条未显示,自动点击播放区域显示控制栏,再拖动\n\n"
582
- "🎯 优势:\n"
583
- "- 自动检测进度条位置,无需手动指定 y_percent\n"
584
- "- 智能判断是否需要显示控制栏\n"
585
- "- 使用 swipe 拖动,更稳定可靠\n\n"
586
- "💡 参数说明:\n"
587
- "- direction: 'left'(倒退)或 'right'(前进),默认 'right'\n"
588
- "- distance_percent: 拖动距离百分比 (0-100),默认 30%\n"
589
- "- y_percent: 进度条位置(可选,未指定则自动检测)\n"
590
- "- y: 进度条位置坐标(可选,未指定则自动检测)\n\n"
591
- "📋 使用示例:\n"
592
- "- 前进30%:mobile_drag_progress_bar(direction='right', distance_percent=30)\n"
593
- "- 倒退30%:mobile_drag_progress_bar(direction='left', distance_percent=30)\n"
594
- "- 前进到指定位置:先点击进度条位置,或使用 mobile_swipe",
595
- inputSchema={
596
- "type": "object",
597
- "properties": {
598
- "direction": {
599
- "type": "string",
600
- "enum": ["left", "right"],
601
- "description": "拖动方向:'left'(倒退)或 'right'(前进),默认 'right'"
602
- },
603
- "distance_percent": {
604
- "type": "number",
605
- "description": "拖动距离百分比 (0-100),默认 30%"
606
- },
607
- "y_percent": {
608
- "type": "number",
609
- "description": "进度条的垂直位置百分比 (0-100),可选,未指定则自动检测"
610
- },
611
- "y": {
612
- "type": "integer",
613
- "description": "进度条的垂直位置坐标(像素),可选,未指定则自动检测"
614
- }
615
- },
616
- "required": []
617
- }
618
- ))
619
-
620
618
  tools.append(Tool(
621
619
  name="mobile_press_key",
622
- description="⌨️ 按键操作。支持:home, back, enter, search",
620
+ description="⌨️ 按键:home/back/enter/search",
623
621
  inputSchema={
624
622
  "type": "object",
625
623
  "properties": {
626
- "key": {"type": "string", "description": "按键名称:home, back, enter, search"}
624
+ "key": {"type": "string", "description": "按键名"}
627
625
  },
628
626
  "required": ["key"]
629
627
  }
@@ -631,11 +629,11 @@ class MobileMCPServer:
631
629
 
632
630
  tools.append(Tool(
633
631
  name="mobile_wait",
634
- description="⏰ 等待指定时间。用于等待页面加载、动画完成等。",
632
+ description="⏰ 等待指定秒数。",
635
633
  inputSchema={
636
634
  "type": "object",
637
635
  "properties": {
638
- "seconds": {"type": "number", "description": "等待时间(秒)"}
636
+ "seconds": {"type": "number", "description": "等待秒数"}
639
637
  },
640
638
  "required": ["seconds"]
641
639
  }
@@ -644,16 +642,11 @@ class MobileMCPServer:
644
642
  # ==================== 应用管理 ====================
645
643
  tools.append(Tool(
646
644
  name="mobile_launch_app",
647
- description="""🚀 启动应用
648
-
649
- 🎯 弹窗检测场景:启动应用后会自动检测弹窗(启动应用场景)
650
-
651
- 启动后建议等待 2-3 秒让页面加载。
652
- 如果检测到弹窗,返回结果中会包含 popup_detected=true,建议调用 mobile_close_popup() 关闭弹窗。""",
645
+ description="🚀 启动应用。",
653
646
  inputSchema={
654
647
  "type": "object",
655
648
  "properties": {
656
- "package_name": {"type": "string", "description": "应用包名"}
649
+ "package_name": {"type": "string", "description": "包名"}
657
650
  },
658
651
  "required": ["package_name"]
659
652
  }
@@ -665,7 +658,7 @@ class MobileMCPServer:
665
658
  inputSchema={
666
659
  "type": "object",
667
660
  "properties": {
668
- "package_name": {"type": "string", "description": "应用包名"}
661
+ "package_name": {"type": "string", "description": "包名"}
669
662
  },
670
663
  "required": ["package_name"]
671
664
  }
@@ -673,11 +666,11 @@ class MobileMCPServer:
673
666
 
674
667
  tools.append(Tool(
675
668
  name="mobile_list_apps",
676
- description="📦 列出已安装的应用。可按关键词过滤。",
669
+ description="📦 列出应用。",
677
670
  inputSchema={
678
671
  "type": "object",
679
672
  "properties": {
680
- "filter": {"type": "string", "description": "过滤关键词(可选)"}
673
+ "filter": {"type": "string", "description": "过滤词"}
681
674
  },
682
675
  "required": []
683
676
  }
@@ -686,33 +679,31 @@ class MobileMCPServer:
686
679
  # ==================== 设备管理 ====================
687
680
  tools.append(Tool(
688
681
  name="mobile_list_devices",
689
- description="📱 列出已连接的设备。",
682
+ description="📱 列出设备。",
690
683
  inputSchema={"type": "object", "properties": {}, "required": []}
691
684
  ))
692
685
 
693
686
  tools.append(Tool(
694
687
  name="mobile_check_connection",
695
- description="🔌 检查设备连接状态。",
688
+ description="🔌 检查连接。",
696
689
  inputSchema={"type": "object", "properties": {}, "required": []}
697
690
  ))
698
691
 
699
692
  # ==================== 辅助工具 ====================
700
- tools.append(Tool(
701
- name="mobile_find_close_button",
702
- description="""🔍 智能查找关闭按钮(只找不点,返回位置)
693
+ if compact:
694
+ desc_find_close = "🔍 查找关闭按钮(只找不点)。返回坐标和推荐的点击命令。"
695
+ else:
696
+ desc_find_close = """🔍 智能查找关闭按钮(只找不点,返回位置)
703
697
 
704
698
  ⚡ 【推荐首选】遇到弹窗时优先调用此工具!无需先截图。
705
699
 
706
700
  从元素树中找最可能的关闭按钮,返回坐标和推荐的点击命令。
707
701
 
708
702
  🎯 识别策略(优先级):
709
- 1. 确认性按钮文本:同意、确认、允许、好的、OK等(得分120
710
- 2. 确认性 resource-id:包含 confirm/accept/agree/allow(得分115
711
- 3. 关闭/取消按钮文本:×、X、关闭、取消、跳过等(得分100
712
- 4. 关闭类 resource-id:包含 close/dismiss/cancel(得分95)
713
- 5. 小尺寸 clickable 元素(右上角优先,得分70+)
714
-
715
- ⭐ 优先点击确认性按钮(同意、确认等),而不是取消/拒绝按钮
703
+ 1. 文本匹配:×、X、关闭、取消、跳过 等(得分100
704
+ 2. resource-id 匹配:包含 close/dismiss/skip(得分95
705
+ 3. content-desc 匹配:包含 close/关闭(得分90
706
+ 4. 小尺寸 clickable 元素(右上角优先,得分70+)
716
707
 
717
708
  ✅ 返回内容:
718
709
  - 坐标 (x, y) 和百分比 (x%, y%)
@@ -722,100 +713,68 @@ class MobileMCPServer:
722
713
  💡 使用流程:
723
714
  1. 直接调用此工具(无需先截图/列元素)
724
715
  2. 根据返回的 click_command 执行点击
725
- 3. 如果返回 success=false,才需要截图分析""",
716
+ 3. 如果返回 success=false,才需要截图分析"""
717
+
718
+ tools.append(Tool(
719
+ name="mobile_find_close_button",
720
+ description=desc_find_close,
726
721
  inputSchema={"type": "object", "properties": {}, "required": []}
727
722
  ))
728
723
 
729
- tools.append(Tool(
730
- name="mobile_close_popup",
731
- description="""🚫 智能关闭弹窗(优化版)
724
+ if compact:
725
+ desc_close_popup = "🚫 智能检测并关闭弹窗。自动查找×/关闭/跳过按钮。"
726
+ else:
727
+ desc_close_popup = """🚫 智能检测并关闭弹窗
732
728
 
733
- 🎯 弹窗检测场景:
734
- - 启动应用后:mobile_launch_app 会自动检测弹窗
735
- - 异常情况:操作失败时会检测弹窗
736
- - 明确弹窗场景:调用此工具时(明确弹窗场景)
729
+ 【自动检测】会先检测是否存在弹窗:
730
+ - 如果没有弹窗 → 直接返回"无弹窗",不执行任何操作
731
+ - 如果有弹窗 → 自动查找并点击关闭按钮
737
732
 
738
- 按钮点击优先级(确认按钮优先):
739
- 1️⃣ **确认性按钮**(最高优先级):同意、确认、允许、好的、知道了、OK等
740
- 2️⃣ **关闭/取消按钮**(次优先级):关闭、取消、跳过、×、X等
741
- 3️⃣ **小尺寸可点击元素**(最低优先级):右上角X图标等
733
+ 适用场景:
734
+ - 启动应用后检测并关闭可能出现的弹窗
735
+ - 页面跳转后检测并关闭弹窗
736
+ - 无需先截图确认弹窗是否存在
742
737
 
743
- 优化后的降级策略:
744
- 1️⃣ **控件树优先**(最快、最可靠):list_elements() → 查找确认/关闭按钮 → 点击
745
- 2️⃣ **截图AI分析**(中等速度、高准确率):如果控件树失败 → 截图 → 返回候选建议 → AI分析 → 点击
746
- 3️⃣ **模板匹配**(最慢、最精确):如果AI分析失败 → 模板匹配 → 点击
738
+ 🎯 检测策略:
739
+ - 查找控件树中的关闭按钮(×、关闭、跳过等)
740
+ - 检测弹窗区域(Dialog/Popup/Alert 等)
741
+ - 查找小尺寸的可点击元素(优先角落位置)
747
742
 
748
- 优先点击「同意」「确认」等确认性按钮,而不是「取消」「不同意」
749
- ✅ 每个阶段都会使用页面指纹对比验证弹窗是否真的关闭
750
- ✅ 复用元素列表和XML,减少重复调用开销
751
- ✅ 支持 Android 和 iOS 双平台
752
- ✅ 支持自动学习模板(可选)""",
753
- inputSchema={
754
- "type": "object",
755
- "properties": {
756
- "auto_learn": {
757
- "type": "boolean",
758
- "description": "是否自动学习新模板(模板匹配成功时),默认 false"
759
- },
760
- "confidence_threshold": {
761
- "type": "number",
762
- "description": "弹窗检测置信度阈值(0-1),默认 0.6。值越高越严格,减少误检但可能漏检"
763
- }
764
- },
765
- "required": []
766
- }
743
+ 🔴 【必须】如果返回已点击,需再次截图确认弹窗是否真的关闭了!"""
744
+
745
+ tools.append(Tool(
746
+ name="mobile_close_popup",
747
+ description=desc_close_popup,
748
+ inputSchema={"type": "object", "properties": {}, "required": []}
767
749
  ))
768
750
 
769
751
  tools.append(Tool(
770
752
  name="mobile_assert_text",
771
- description="✅ 检查页面是否包含指定文本。用于验证操作结果。",
753
+ description="✅ 检查页面是否包含文本。",
772
754
  inputSchema={
773
755
  "type": "object",
774
756
  "properties": {
775
- "text": {"type": "string", "description": "要检查的文本"}
757
+ "text": {"type": "string", "description": "文本"}
776
758
  },
777
759
  "required": ["text"]
778
760
  }
779
761
  ))
780
762
 
781
- # ==================== Toast 检测工具(仅 Android)====================
763
+ # ==================== Toast 检测(仅 Android)====================
782
764
  tools.append(Tool(
783
765
  name="mobile_start_toast_watch",
784
- description="""🔔 开始监听 Toast(仅 Android)
785
-
786
- ⚠️ 【重要】必须在执行操作之前调用!
787
-
788
- 📋 正确流程(三步走):
789
- 1️⃣ 调用 mobile_start_toast_watch() 开始监听
790
- 2️⃣ 执行操作(如点击提交按钮)
791
- 3️⃣ 调用 mobile_get_toast() 或 mobile_assert_toast() 获取结果
792
-
793
- ❌ 错误用法:先点击按钮,再调用此工具(Toast 可能已消失)""",
794
- inputSchema={
795
- "type": "object",
796
- "properties": {},
797
- "required": []
798
- }
766
+ description="🔔 开始监听Toast。必须在操作前调用。",
767
+ inputSchema={"type": "object", "properties": {}, "required": []}
799
768
  ))
800
769
 
801
770
  tools.append(Tool(
802
771
  name="mobile_get_toast",
803
- description="""🍞 获取 Toast 消息(仅 Android)
804
-
805
- Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果。
806
- ⚠️ Toast 不在控件树中,无法通过 mobile_list_elements 获取。
807
-
808
- 📋 推荐用法(三步走):
809
- 1️⃣ mobile_start_toast_watch() - 开始监听
810
- 2️⃣ 执行操作(点击按钮等)
811
- 3️⃣ mobile_get_toast() - 获取 Toast
812
-
813
- ⏱️ timeout 设置等待时间,默认 5 秒。""",
772
+ description="🍞 获取Toast消息。配合start_toast_watch使用。",
814
773
  inputSchema={
815
774
  "type": "object",
816
775
  "properties": {
817
- "timeout": {"type": "number", "description": "等待 Toast 出现的超时时间(秒),默认 5"},
818
- "reset_first": {"type": "boolean", "description": "是否先清除旧缓存,默认 False"}
776
+ "timeout": {"type": "number", "description": "超时秒数,默认5"},
777
+ "reset_first": {"type": "boolean", "description": "清除旧缓存"}
819
778
  },
820
779
  "required": []
821
780
  }
@@ -823,22 +782,13 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
823
782
 
824
783
  tools.append(Tool(
825
784
  name="mobile_assert_toast",
826
- description="""✅ 断言 Toast 消息(仅 Android)
827
-
828
- 等待 Toast 出现并验证内容是否符合预期。
829
-
830
- 📋 推荐用法(三步走):
831
- 1️⃣ mobile_start_toast_watch() - 开始监听
832
- 2️⃣ 执行操作(点击按钮等)
833
- 3️⃣ mobile_assert_toast(expected_text="成功") - 断言
834
-
835
- 💡 支持包含匹配(默认)和精确匹配。""",
785
+ description="✅ 断言Toast内容。",
836
786
  inputSchema={
837
787
  "type": "object",
838
788
  "properties": {
839
- "expected_text": {"type": "string", "description": "期望的 Toast 文本"},
840
- "timeout": {"type": "number", "description": "等待超时时间(秒),默认 5"},
841
- "contains": {"type": "boolean", "description": "True=包含匹配(默认),False=精确匹配"}
789
+ "expected_text": {"type": "string", "description": "期望文本"},
790
+ "timeout": {"type": "number", "description": "超时秒数"},
791
+ "contains": {"type": "boolean", "description": "包含匹配(默认true)"}
842
792
  },
843
793
  "required": ["expected_text"]
844
794
  }
@@ -847,11 +797,11 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
847
797
  # ==================== pytest 脚本生成 ====================
848
798
  tools.append(Tool(
849
799
  name="mobile_get_operation_history",
850
- description="📜 获取操作历史记录。",
800
+ description="📜 获取操作历史。",
851
801
  inputSchema={
852
802
  "type": "object",
853
803
  "properties": {
854
- "limit": {"type": "number", "description": "返回最近的N条记录"}
804
+ "limit": {"type": "number", "description": "条数"}
855
805
  },
856
806
  "required": []
857
807
  }
@@ -859,50 +809,53 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
859
809
 
860
810
  tools.append(Tool(
861
811
  name="mobile_clear_operation_history",
862
- description="🗑️ 清空操作历史记录。\n\n"
863
- "⚠️ 开始新的测试录制前必须调用!\n"
864
- "📋 录制流程:清空历史 → 执行操作(优先用文本/ID定位)→ 生成脚本",
812
+ description="🗑️ 清空操作历史。录制前调用。",
865
813
  inputSchema={"type": "object", "properties": {}, "required": []}
866
814
  ))
867
815
 
868
816
  tools.append(Tool(
869
817
  name="mobile_generate_test_script",
870
- description="📝 生成 pytest 测试脚本。基于操作历史自动生成。\n\n"
871
- "⚠️ 【重要】录制操作时请优先使用稳定定位:\n"
872
- "1️⃣ 先调用 mobile_list_elements 获取元素列表\n"
873
- "2️⃣ 优先用 mobile_click_by_text(最稳定,跨设备兼容)\n"
874
- "3️⃣ 其次用 mobile_click_by_id(稳定)\n"
875
- "4️⃣ 最后才用坐标点击(会自动转百分比,跨分辨率兼容)\n\n"
876
- "使用流程:\n"
877
- "1. 清空历史 mobile_clear_operation_history\n"
878
- "2. 执行操作(优先用文本/ID定位)\n"
879
- "3. 调用此工具生成脚本\n"
880
- "4. 脚本保存到 tests/ 目录\n\n"
881
- "💡 定位优先级:文本 > ID > 百分比 > 坐标",
818
+ description="📝 生成pytest脚本。基于操作历史生成。",
882
819
  inputSchema={
883
820
  "type": "object",
884
821
  "properties": {
885
- "test_name": {"type": "string", "description": "测试用例名称"},
886
- "package_name": {"type": "string", "description": "App 包名"},
887
- "filename": {"type": "string", "description": "脚本文件名(不含 .py"}
822
+ "test_name": {"type": "string", "description": "用例名"},
823
+ "package_name": {"type": "string", "description": "包名"},
824
+ "filename": {"type": "string", "description": "文件名(不含.py)"}
888
825
  },
889
826
  "required": ["test_name", "package_name", "filename"]
890
827
  }
891
828
  ))
892
829
 
893
830
  # ==================== 广告弹窗关闭工具 ====================
894
- tools.append(Tool(
895
- name="mobile_close_ad",
896
- description="""🚫 【推荐】智能关闭广告弹窗(已合并到 mobile_close_popup)
831
+ if compact:
832
+ desc_close_ad = "🚫 智能关闭广告弹窗。优先级:控件树→截图AI→模板匹配。"
833
+ else:
834
+ desc_close_ad = """🚫 【推荐】智能检测并关闭广告弹窗
835
+
836
+ ⚡ 【自动检测】会先检测是否存在弹窗:
837
+ - 如果没有弹窗 → 直接返回"无弹窗",不执行任何操作
838
+ - 如果有弹窗 → 自动按优先级尝试关闭
897
839
 
898
- 此工具现在调用 mobile_close_popup,功能完全相同。
840
+ 🎯 关闭策略(优先级从高到低):
841
+ 1️⃣ **控件树查找**(最可靠)
842
+ - 查找文本"关闭"、"跳过"、"×"等
843
+ - 查找 resource-id 包含 close/dismiss
844
+
845
+ 2️⃣ **截图 AI 分析**(次优)
846
+ - 返回 SoM 标注截图供 AI 视觉分析
847
+ - AI 找到 X 按钮后用 click_by_som(编号) 点击
899
848
 
900
- 内部使用统一的降级策略:
901
- 1️⃣ **控件树优先**(最快、最可靠)
902
- 2️⃣ **截图AI分析**(中等速度、高准确率)
903
- 3️⃣ **模板匹配**(最精确的兜底方案,支持自动学习)
849
+ 3️⃣ **模板匹配**(兜底)
850
+ - 用 OpenCV 匹配已保存的 X 按钮模板
904
851
 
905
- 💡 推荐使用 mobile_close_popup 代替(功能相同)""",
852
+ 适用场景:
853
+ - 启动应用后检测并关闭可能出现的广告
854
+ - 无需先截图确认弹窗是否存在"""
855
+
856
+ tools.append(Tool(
857
+ name="mobile_close_ad",
858
+ description=desc_close_ad,
906
859
  inputSchema={
907
860
  "type": "object",
908
861
  "properties": {},
@@ -912,19 +865,12 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
912
865
 
913
866
  tools.append(Tool(
914
867
  name="mobile_template_close",
915
- description="""🎯 模板匹配关闭弹窗(仅模板匹配)
916
-
917
- 只用 OpenCV 模板匹配,不走控件树。
918
- 一般建议用 mobile_close_ad 代替(会自动先查控件树)。
919
-
920
- ⚙️ 参数:
921
- - click: 是否点击,默认 true
922
- - threshold: 匹配阈值 0-1,默认 0.75""",
868
+ description="🎯 模板匹配关闭弹窗。",
923
869
  inputSchema={
924
870
  "type": "object",
925
871
  "properties": {
926
- "click": {"type": "boolean", "description": "是否点击,默认 true"},
927
- "threshold": {"type": "number", "description": "匹配阈值 0-1,默认 0.75"}
872
+ "click": {"type": "boolean", "description": "是否点击"},
873
+ "threshold": {"type": "number", "description": "阈值0-1"}
928
874
  },
929
875
  "required": []
930
876
  }
@@ -932,37 +878,38 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
932
878
 
933
879
  tools.append(Tool(
934
880
  name="mobile_template_add",
935
- description="""➕ 添加 X 号模板
936
-
937
- 遇到新样式 X 号时,截图并添加到模板库。
938
-
939
- ⚙️ 两种方式(二选一):
940
- 1. 百分比定位(推荐):提供 x_percent, y_percent, size
941
- 2. 像素定位:提供 screenshot_path, x, y, width, height
942
-
943
- 📋 流程:
944
- 1. mobile_screenshot_with_grid 查看 X 号位置
945
- 2. 调用此工具添加模板
946
- 3. 下次同样 X 号就能自动匹配
947
-
948
- 💡 百分比示例:X 在右上角 → x_percent=85, y_percent=12, size=80""",
881
+ description="➕ 添加X号模板。",
949
882
  inputSchema={
950
883
  "type": "object",
951
884
  "properties": {
952
- "template_name": {"type": "string", "description": "模板名称"},
953
- "x_percent": {"type": "number", "description": "X号中心水平百分比 (0-100)"},
954
- "y_percent": {"type": "number", "description": "X号中心垂直百分比 (0-100)"},
955
- "size": {"type": "integer", "description": "裁剪正方形边长(像素)"},
956
- "screenshot_path": {"type": "string", "description": "截图路径(像素定位时用)"},
957
- "x": {"type": "integer", "description": "左上角 X 坐标"},
958
- "y": {"type": "integer", "description": "左上角 Y 坐标"},
959
- "width": {"type": "integer", "description": "裁剪宽度"},
960
- "height": {"type": "integer", "description": "裁剪高度"}
885
+ "template_name": {"type": "string", "description": "模板名"},
886
+ "x_percent": {"type": "number", "description": "X百分比"},
887
+ "y_percent": {"type": "number", "description": "Y百分比"},
888
+ "size": {"type": "integer", "description": "裁剪大小(px)"},
889
+ "screenshot_path": {"type": "string", "description": "截图路径"},
890
+ "x": {"type": "integer", "description": "左上X"},
891
+ "y": {"type": "integer", "description": "左上Y"},
892
+ "width": {"type": "integer", "description": ""},
893
+ "height": {"type": "integer", "description": ""}
961
894
  },
962
895
  "required": ["template_name"]
963
896
  }
964
897
  ))
965
898
 
899
+ # ==================== Cursor自动化 ====================
900
+ tools.append(Tool(
901
+ name="mobile_open_new_chat",
902
+ description="🔄 打开新Chat继续执行。执行完10个用例后调用,会延迟5秒后按Cmd+T打开新会话并输入继续命令。",
903
+ inputSchema={
904
+ "type": "object",
905
+ "properties": {
906
+ "message": {"type": "string", "description": "新会话中要发送的消息", "default": "继续执行飞书用例"},
907
+ "delay": {"type": "number", "description": "延迟秒数(等待当前响应结束)", "default": 5}
908
+ },
909
+ "required": []
910
+ }
911
+ ))
912
+
966
913
  return tools
967
914
 
968
915
  async def handle_tool_call(self, name: str, arguments: dict):
@@ -1009,37 +956,7 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
1009
956
  return [TextContent(type="text", text=self.format_response(result))]
1010
957
 
1011
958
  elif name == "mobile_screenshot_with_som":
1012
- check_popup = arguments.get("check_popup", False)
1013
- result = self.tools.take_screenshot_with_som(check_popup=check_popup)
1014
-
1015
- # 如果结果包含截图路径,尝试返回图片内容供 AI 分析
1016
- screenshot_path = result.get("screenshot_path")
1017
- if screenshot_path:
1018
- try:
1019
- from pathlib import Path
1020
- import base64
1021
-
1022
- img_path = Path(screenshot_path)
1023
- if img_path.exists():
1024
- # 读取图片并转换为 base64
1025
- with open(img_path, 'rb') as f:
1026
- image_data = f.read()
1027
- image_base64 = base64.b64encode(image_data).decode('utf-8')
1028
-
1029
- # 检查是否有 AI 分析请求
1030
- ai_analysis = result.get("ai_analysis")
1031
- if ai_analysis and ai_analysis.get("needs_ai_analysis"):
1032
- # 返回文本结果和图片内容
1033
- # 注意:MCP 协议可能不支持 ImageContent,这里先返回文本
1034
- # Cursor AI 会自动看到截图文件并分析
1035
- return [
1036
- TextContent(type="text", text=self.format_response(result)),
1037
- # 如果 MCP 支持 ImageContent,可以添加:
1038
- # ImageContent(type="image", data=image_base64, mimeType="image/jpeg")
1039
- ]
1040
- except Exception:
1041
- pass # 图片处理失败,只返回文本
1042
-
959
+ result = self.tools.take_screenshot_with_som()
1043
960
  return [TextContent(type="text", text=self.format_response(result))]
1044
961
 
1045
962
  elif name == "mobile_click_by_som":
@@ -1063,7 +980,8 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
1063
980
  elif name == "mobile_click_by_text":
1064
981
  result = self.tools.click_by_text(
1065
982
  arguments["text"],
1066
- position=arguments.get("position")
983
+ position=arguments.get("position"),
984
+ verify=arguments.get("verify")
1067
985
  )
1068
986
  return [TextContent(type="text", text=self.format_response(result))]
1069
987
 
@@ -1129,9 +1047,7 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
1129
1047
  result = await self.tools.swipe(
1130
1048
  arguments["direction"],
1131
1049
  y=arguments.get("y"),
1132
- y_percent=arguments.get("y_percent"),
1133
- distance=arguments.get("distance"),
1134
- distance_percent=arguments.get("distance_percent")
1050
+ y_percent=arguments.get("y_percent")
1135
1051
  )
1136
1052
  return [TextContent(type="text", text=self.format_response(result))]
1137
1053
 
@@ -1175,12 +1091,7 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
1175
1091
  return [TextContent(type="text", text=self.format_response(result))]
1176
1092
 
1177
1093
  elif name == "mobile_close_popup":
1178
- auto_learn = arguments.get("auto_learn", False)
1179
- confidence_threshold = arguments.get("confidence_threshold", None)
1180
- result = self.tools.close_popup(
1181
- auto_learn=auto_learn,
1182
- confidence_threshold=confidence_threshold
1183
- )
1094
+ result = self.tools.close_popup()
1184
1095
  return [TextContent(type="text", text=self.format_response(result))]
1185
1096
 
1186
1097
  elif name == "mobile_assert_text":
@@ -1223,9 +1134,9 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
1223
1134
  )
1224
1135
  return [TextContent(type="text", text=self.format_response(result))]
1225
1136
 
1226
- # 智能关闭广告弹窗(已合并到 mobile_close_popup)
1137
+ # 智能关闭广告弹窗
1227
1138
  elif name == "mobile_close_ad":
1228
- result = self.tools.close_popup(auto_learn=False)
1139
+ result = self.tools.close_ad_popup(auto_learn=True)
1229
1140
  return [TextContent(type="text", text=self.format_response(result))]
1230
1141
 
1231
1142
  # 模板匹配(精简版)
@@ -1263,6 +1174,13 @@ Toast 是 Android 系统级的短暂提示消息,常用于显示操作结果
1263
1174
  result = {"success": False, "error": "请提供 x_percent/y_percent 或 screenshot_path/x/y/width/height"}
1264
1175
  return [TextContent(type="text", text=self.format_response(result))]
1265
1176
 
1177
+ # Cursor自动化:打开新Chat
1178
+ elif name == "mobile_open_new_chat":
1179
+ message = arguments.get("message", "继续执行飞书用例")
1180
+ delay = arguments.get("delay", 5)
1181
+ result = self._open_new_chat(message, delay)
1182
+ return [TextContent(type="text", text=self.format_response(result))]
1183
+
1266
1184
  else:
1267
1185
  return [TextContent(type="text", text=f"❌ 未知工具: {name}")]
1268
1186