mobile-mcp-ai 2.6.12__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.
- mobile_mcp/core/basic_tools_lite.py +39 -8
- mobile_mcp/mcp_tools/mcp_server.py +134 -98
- {mobile_mcp_ai-2.6.12.dist-info → mobile_mcp_ai-2.7.0.dist-info}/METADATA +47 -19
- {mobile_mcp_ai-2.6.12.dist-info → mobile_mcp_ai-2.7.0.dist-info}/RECORD +8 -9
- {mobile_mcp_ai-2.6.12.dist-info → mobile_mcp_ai-2.7.0.dist-info}/WHEEL +1 -1
- mobile_mcp/core/tool_selection_helper.py +0 -168
- {mobile_mcp_ai-2.6.12.dist-info → mobile_mcp_ai-2.7.0.dist-info}/entry_points.txt +0 -0
- {mobile_mcp_ai-2.6.12.dist-info → mobile_mcp_ai-2.7.0.dist-info}/licenses/LICENSE +0 -0
- {mobile_mcp_ai-2.6.12.dist-info → mobile_mcp_ai-2.7.0.dist-info}/top_level.txt +0 -0
|
@@ -2197,6 +2197,13 @@ class BasicMobileToolsLite:
|
|
|
2197
2197
|
def wait(self, seconds: float) -> Dict:
|
|
2198
2198
|
"""等待指定时间"""
|
|
2199
2199
|
time.sleep(seconds)
|
|
2200
|
+
# 记录等待操作
|
|
2201
|
+
record = {
|
|
2202
|
+
'action': 'wait',
|
|
2203
|
+
'timestamp': datetime.now().isoformat(),
|
|
2204
|
+
'seconds': seconds,
|
|
2205
|
+
}
|
|
2206
|
+
self.operation_history.append(record)
|
|
2200
2207
|
return {"success": True}
|
|
2201
2208
|
|
|
2202
2209
|
# ==================== 应用管理 ====================
|
|
@@ -2353,6 +2360,15 @@ class BasicMobileToolsLite:
|
|
|
2353
2360
|
'_shadow', 'shadow_', '_divider', 'divider_', '_line', 'line_'
|
|
2354
2361
|
}
|
|
2355
2362
|
|
|
2363
|
+
# 状态栏相关关键词(这些元素对测试没有意义,直接过滤)
|
|
2364
|
+
STATUS_BAR_KEYWORDS = {
|
|
2365
|
+
'status_bar', 'statusbar', 'notification_icon', 'notificationicons',
|
|
2366
|
+
'system_icons', 'statusicons', 'battery', 'wifi_', 'wifi_combo',
|
|
2367
|
+
'wifi_group', 'wifi_signal', 'wifi_in', 'wifi_out', 'signal_',
|
|
2368
|
+
'clock', 'cutout', 'networkspeed', 'speed_container',
|
|
2369
|
+
'carrier', 'operator', 'sim_', 'mobile_signal'
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2356
2372
|
# Token 优化:构建精简元素(只返回非空字段)
|
|
2357
2373
|
def build_compact_element(resource_id, text, content_desc, bounds, likely_click, class_name):
|
|
2358
2374
|
"""只返回有值的字段,节省 token"""
|
|
@@ -2390,6 +2406,12 @@ class BasicMobileToolsLite:
|
|
|
2390
2406
|
if bounds == '[0,0][0,0]':
|
|
2391
2407
|
continue
|
|
2392
2408
|
|
|
2409
|
+
# 1.5 过滤状态栏元素(对测试没有意义)
|
|
2410
|
+
if resource_id:
|
|
2411
|
+
resource_id_lower = resource_id.lower()
|
|
2412
|
+
if any(keyword in resource_id_lower for keyword in STATUS_BAR_KEYWORDS):
|
|
2413
|
+
continue
|
|
2414
|
+
|
|
2393
2415
|
# 2. 检查是否是功能控件(直接保留)
|
|
2394
2416
|
if class_name in FUNCTIONAL_WIDGETS:
|
|
2395
2417
|
# 使用启发式判断可点击性(替代不准确的 clickable 属性)
|
|
@@ -3379,6 +3401,15 @@ class BasicMobileToolsLite:
|
|
|
3379
3401
|
|
|
3380
3402
|
# 生成脚本
|
|
3381
3403
|
safe_name = re.sub(r'[^\w\s-]', '', test_name).strip().replace(' ', '_')
|
|
3404
|
+
# 确保 safe_name 不为空,否则使用默认名称
|
|
3405
|
+
if not safe_name:
|
|
3406
|
+
safe_name = 'generated_case'
|
|
3407
|
+
|
|
3408
|
+
# 提前处理文件名,确保文档字符串中的文件名正确
|
|
3409
|
+
if not filename.endswith('.py'):
|
|
3410
|
+
filename = f"{filename}.py"
|
|
3411
|
+
if not filename.startswith('test_'):
|
|
3412
|
+
filename = f"test_{filename}"
|
|
3382
3413
|
|
|
3383
3414
|
script_lines = [
|
|
3384
3415
|
"#!/usr/bin/env python3",
|
|
@@ -3393,8 +3424,8 @@ class BasicMobileToolsLite:
|
|
|
3393
3424
|
"3. 百分比定位 - 跨分辨率兼容(坐标自动转换)",
|
|
3394
3425
|
"",
|
|
3395
3426
|
"运行方式:",
|
|
3396
|
-
" pytest {filename} -v # 使用 pytest 运行",
|
|
3397
|
-
" python {filename} # 直接运行",
|
|
3427
|
+
f" pytest {filename} -v # 使用 pytest 运行",
|
|
3428
|
+
f" python {filename} # 直接运行",
|
|
3398
3429
|
f'"""',
|
|
3399
3430
|
"import time",
|
|
3400
3431
|
"import pytest",
|
|
@@ -3655,6 +3686,12 @@ class BasicMobileToolsLite:
|
|
|
3655
3686
|
script_lines.append(f" d.press('{key}')")
|
|
3656
3687
|
script_lines.append(" time.sleep(0.5)")
|
|
3657
3688
|
script_lines.append(" ")
|
|
3689
|
+
|
|
3690
|
+
elif action == 'wait':
|
|
3691
|
+
seconds = op.get('seconds', 1)
|
|
3692
|
+
script_lines.append(f" # 步骤{step_num}: 等待 {seconds} 秒")
|
|
3693
|
+
script_lines.append(f" time.sleep({seconds})")
|
|
3694
|
+
script_lines.append(" ")
|
|
3658
3695
|
|
|
3659
3696
|
script_lines.extend([
|
|
3660
3697
|
" print('✅ 测试完成')",
|
|
@@ -3678,12 +3715,6 @@ class BasicMobileToolsLite:
|
|
|
3678
3715
|
output_dir = Path("tests")
|
|
3679
3716
|
output_dir.mkdir(exist_ok=True)
|
|
3680
3717
|
|
|
3681
|
-
# 确保文件名符合 pytest 规范(以 test_ 开头)
|
|
3682
|
-
if not filename.endswith('.py'):
|
|
3683
|
-
filename = f"{filename}.py"
|
|
3684
|
-
if not filename.startswith('test_'):
|
|
3685
|
-
filename = f"test_{filename}"
|
|
3686
|
-
|
|
3687
3718
|
file_path = output_dir / filename
|
|
3688
3719
|
file_path.write_text(script, encoding='utf-8')
|
|
3689
3720
|
|
|
@@ -204,6 +204,115 @@ class MobileMCPServer:
|
|
|
204
204
|
|
|
205
205
|
return "android"
|
|
206
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
|
+
|
|
207
316
|
def get_tools(self):
|
|
208
317
|
"""注册 MCP 工具"""
|
|
209
318
|
tools = []
|
|
@@ -226,36 +335,7 @@ class MobileMCPServer:
|
|
|
226
335
|
|
|
227
336
|
tools.append(Tool(
|
|
228
337
|
name="mobile_list_elements",
|
|
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("退出登录") # 复用元素列表""",
|
|
338
|
+
description=desc_list_elements,
|
|
259
339
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
260
340
|
))
|
|
261
341
|
|
|
@@ -309,49 +389,8 @@ mobile_click_by_text("退出登录") # 复用元素列表""",
|
|
|
309
389
|
|
|
310
390
|
tools.append(Tool(
|
|
311
391
|
name="mobile_screenshot_with_som",
|
|
312
|
-
description=
|
|
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
|
-
}
|
|
392
|
+
description=desc_som,
|
|
393
|
+
inputSchema={"type": "object", "properties": {}, "required": []}
|
|
355
394
|
))
|
|
356
395
|
|
|
357
396
|
tools.append(Tool(
|
|
@@ -397,31 +436,7 @@ if not find_target(elements):
|
|
|
397
436
|
|
|
398
437
|
tools.append(Tool(
|
|
399
438
|
name="mobile_click_by_text",
|
|
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("设置") # 从元素列表中找到后直接点击""",
|
|
439
|
+
description=desc_click_text,
|
|
425
440
|
inputSchema={
|
|
426
441
|
"type": "object",
|
|
427
442
|
"properties": {
|
|
@@ -881,6 +896,20 @@ mobile_click_by_text("设置") # 从元素列表中找到后直接点击"
|
|
|
881
896
|
}
|
|
882
897
|
))
|
|
883
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
|
+
|
|
884
913
|
return tools
|
|
885
914
|
|
|
886
915
|
async def handle_tool_call(self, name: str, arguments: dict):
|
|
@@ -1145,6 +1174,13 @@ mobile_click_by_text("设置") # 从元素列表中找到后直接点击"
|
|
|
1145
1174
|
result = {"success": False, "error": "请提供 x_percent/y_percent 或 screenshot_path/x/y/width/height"}
|
|
1146
1175
|
return [TextContent(type="text", text=self.format_response(result))]
|
|
1147
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
|
+
|
|
1148
1184
|
else:
|
|
1149
1185
|
return [TextContent(type="text", text=f"❌ 未知工具: {name}")]
|
|
1150
1186
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mobile-mcp-ai
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: 移动端自动化 MCP Server - 支持 Android/iOS,AI 功能可选(基础工具不需要 AI)
|
|
5
5
|
Home-page: https://github.com/test111ddff-hash/mobile-mcp-ai
|
|
6
6
|
Author: douzi
|
|
@@ -46,6 +46,10 @@ Requires-Dist: facebook-wda>=1.4.0; extra == "ios"
|
|
|
46
46
|
Provides-Extra: h5
|
|
47
47
|
Requires-Dist: Appium-Python-Client>=3.0.0; extra == "h5"
|
|
48
48
|
Requires-Dist: selenium>=4.0.0; extra == "h5"
|
|
49
|
+
Provides-Extra: windows
|
|
50
|
+
Requires-Dist: pyautogui>=0.9.0; extra == "windows"
|
|
51
|
+
Requires-Dist: pyperclip>=1.8.0; extra == "windows"
|
|
52
|
+
Requires-Dist: pygetwindow>=0.0.9; extra == "windows"
|
|
49
53
|
Provides-Extra: all
|
|
50
54
|
Requires-Dist: dashscope>=1.10.0; extra == "all"
|
|
51
55
|
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
@@ -55,6 +59,9 @@ Requires-Dist: selenium>=4.0.0; extra == "all"
|
|
|
55
59
|
Requires-Dist: pytest>=8.0.0; extra == "all"
|
|
56
60
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
|
|
57
61
|
Requires-Dist: allure-pytest>=2.13.0; extra == "all"
|
|
62
|
+
Requires-Dist: pyautogui>=0.9.0; extra == "all"
|
|
63
|
+
Requires-Dist: pyperclip>=1.8.0; extra == "all"
|
|
64
|
+
Requires-Dist: pygetwindow>=0.0.9; extra == "all"
|
|
58
65
|
Dynamic: author
|
|
59
66
|
Dynamic: author-email
|
|
60
67
|
Dynamic: classifier
|
|
@@ -336,6 +343,34 @@ tidevice list
|
|
|
336
343
|
|
|
337
344
|
保存后**重启 Cursor**。
|
|
338
345
|
|
|
346
|
+
### 批量执行用例(飞书集成)
|
|
347
|
+
|
|
348
|
+
如果你需要从飞书多维表格批量执行用例,`mobile_open_new_chat` 功能会自动打开新会话继续执行。
|
|
349
|
+
|
|
350
|
+
**macOS 用户:** 需要开启辅助功能权限
|
|
351
|
+
|
|
352
|
+
| 步骤 | 操作 |
|
|
353
|
+
|:---:|------|
|
|
354
|
+
| 1 | 打开「系统设置」 |
|
|
355
|
+
| 2 | 点击「隐私与安全性」 |
|
|
356
|
+
| 3 | 点击「辅助功能」 |
|
|
357
|
+
| 4 | 点击 + 号,添加 **Cursor.app** |
|
|
358
|
+
| 5 | 确保开关已打开 ✅ |
|
|
359
|
+
|
|
360
|
+
> ⚠️ 没有此权限,无法自动打开新会话继续执行
|
|
361
|
+
|
|
362
|
+
**Windows 用户:** 需要安装额外依赖
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
pip install mobile-mcp-ai[windows]
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
或手动安装:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
pip install pyautogui pyperclip pygetwindow
|
|
372
|
+
```
|
|
373
|
+
|
|
339
374
|
---
|
|
340
375
|
|
|
341
376
|
## 🚀 使用示例
|
|
@@ -404,24 +439,17 @@ tidevice list
|
|
|
404
439
|
|
|
405
440
|
## 🛠️ 工具列表
|
|
406
441
|
|
|
407
|
-
| 类别 | 工具 | 说明 |
|
|
408
|
-
|
|
409
|
-
| 📋 | `mobile_list_elements` | 列出页面元素 |
|
|
410
|
-
|
|
|
411
|
-
|
|
|
412
|
-
| 📸 | `
|
|
413
|
-
|
|
|
414
|
-
|
|
|
415
|
-
| 👆 | `
|
|
416
|
-
| 👆 | `
|
|
417
|
-
| 👆 | `
|
|
418
|
-
| 📐 | `mobile_get_screen_size` | 屏幕尺寸 | 辅助工具 |
|
|
419
|
-
|
|
420
|
-
> 💡 **工具选择策略**:优先使用控件树定位(`list_elements` + `click_by_text/id`),截图仅作为兜底方案。
|
|
421
|
-
>
|
|
422
|
-
> 📖 详细指南:
|
|
423
|
-
> - [工具选择策略指南](docs/TOOL_SELECTION_STRATEGY.md) - 详细的工具选择策略和决策树
|
|
424
|
-
> - [用例执行最佳实践](docs/EXECUTION_BEST_PRACTICES.md) - 用例执行流程和优化建议
|
|
442
|
+
| 类别 | 工具 | 说明 |
|
|
443
|
+
|:---:|------|------|
|
|
444
|
+
| 📋 | `mobile_list_elements` | 列出页面元素 |
|
|
445
|
+
| 📸 | `mobile_take_screenshot` | 截图 |
|
|
446
|
+
| 📸 | `mobile_screenshot_with_som` | Set-of-Mark 截图(智能标注) |
|
|
447
|
+
| 📸 | `mobile_screenshot_with_grid` | 带网格坐标的截图 |
|
|
448
|
+
| 📐 | `mobile_get_screen_size` | 屏幕尺寸 |
|
|
449
|
+
| 👆 | `mobile_click_by_text` | 文本点击 |
|
|
450
|
+
| 👆 | `mobile_click_by_id` | ID 点击 |
|
|
451
|
+
| 👆 | `mobile_click_at_coords` | 坐标点击 |
|
|
452
|
+
| 👆 | `mobile_click_by_percent` | 百分比点击 |
|
|
425
453
|
| 👆 | `mobile_click_by_som` | SoM 编号点击 |
|
|
426
454
|
| 👆 | `mobile_long_press_by_id` | ID 长按 |
|
|
427
455
|
| 👆 | `mobile_long_press_by_text` | 文本长按 |
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
mobile_mcp/__init__.py,sha256=sQJZTL_sxQFzmcS7jOtS2AHCfUySz40vhX96N6u1qy4,816
|
|
2
2
|
mobile_mcp/config.py,sha256=-xSl9vahp3EFAA97P1ahcnQC-HHAFvccGHpnFAXeKHU,5841
|
|
3
3
|
mobile_mcp/core/__init__.py,sha256=ndMy-cLAIsQDG5op7gM_AIplycqZSZPWEkec1pEhvEY,170
|
|
4
|
-
mobile_mcp/core/basic_tools_lite.py,sha256=
|
|
4
|
+
mobile_mcp/core/basic_tools_lite.py,sha256=8l0zxzFBB-41c_uVcS-5Z4slzawNGa7yu1GglYAkhME,193754
|
|
5
5
|
mobile_mcp/core/device_manager.py,sha256=xG5DoeNFs45pl-FTEhEWblqVwxtFK-FmVEGlNL6EqRI,8798
|
|
6
6
|
mobile_mcp/core/dynamic_config.py,sha256=Ja1n1pfb0HspGByqk2_A472mYVniKmGtNEWyjUjmgK8,9811
|
|
7
7
|
mobile_mcp/core/ios_client_wda.py,sha256=Nq9WxevhTWpVpolM-Ymp-b0nUQV3tXLFszmJHbDC4wA,18770
|
|
8
8
|
mobile_mcp/core/ios_device_manager_wda.py,sha256=A44glqI-24un7qST-E3w6BQD8mV92YVUbxy4rLlTScY,11264
|
|
9
9
|
mobile_mcp/core/mobile_client.py,sha256=AaBntQSW2loAw7xL3j_IABNzrJO_Uukf9-F1z1xl6xE,63672
|
|
10
10
|
mobile_mcp/core/template_matcher.py,sha256=tv8RU6zdeDobqphaP4Y8sicb1esg3gcQlZae1tNyitM,14559
|
|
11
|
-
mobile_mcp/core/tool_selection_helper.py,sha256=k5_E4oK_fGg4n6qembvudt6Fw-QXq7_SLJDt4FO3cHc,5764
|
|
12
11
|
mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png,sha256=s7tBVaYLBApNSEXjwi5kX8GXwUqgbNyNVEhXYjN9nd4,27373
|
|
13
12
|
mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png,sha256=s7tBVaYLBApNSEXjwi5kX8GXwUqgbNyNVEhXYjN9nd4,27373
|
|
14
13
|
mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png,sha256=s7tBVaYLBApNSEXjwi5kX8GXwUqgbNyNVEhXYjN9nd4,27373
|
|
@@ -20,14 +19,14 @@ mobile_mcp/core/utils/logger.py,sha256=XXQAHUwT1jc70pq_tYFmL6f_nKrFlYm3hcgl-5RYR
|
|
|
20
19
|
mobile_mcp/core/utils/operation_history_manager.py,sha256=gi8S8HJAMqvkUrY7_-kVbko3Xt7c4GAUziEujRd-N-Y,4792
|
|
21
20
|
mobile_mcp/core/utils/smart_wait.py,sha256=N5wKTUYrNWPruBILqrAjpvtso8Z3GRWCfMIR_aZxPLg,8649
|
|
22
21
|
mobile_mcp/mcp_tools/__init__.py,sha256=xkro8Rwqv_55YlVyhh-3DgRFSsLE3h1r31VIb3bpM6E,143
|
|
23
|
-
mobile_mcp/mcp_tools/mcp_server.py,sha256=
|
|
22
|
+
mobile_mcp/mcp_tools/mcp_server.py,sha256=nY_4jrdn4siJtm2HvettTv5bTN0rq9iJvwqBMlI7sMc,53175
|
|
24
23
|
mobile_mcp/utils/__init__.py,sha256=8EH0i7UGtx1y_j_GEgdN-cZdWn2sRtZSEOLlNF9HRnY,158
|
|
25
24
|
mobile_mcp/utils/logger.py,sha256=Sqq2Nr0Y4p03erqcrbYKVPCGiFaNGHMcE_JwCkeOfU4,3626
|
|
26
25
|
mobile_mcp/utils/xml_formatter.py,sha256=uwTRb3vLbqhT8O-udzWT7s7LsV-DyDUz2DkofD3hXOE,4556
|
|
27
26
|
mobile_mcp/utils/xml_parser.py,sha256=QhL8CWbdmNDzmBLjtx6mEnjHgMFZzJeHpCL15qfXSpI,3926
|
|
28
|
-
mobile_mcp_ai-2.
|
|
29
|
-
mobile_mcp_ai-2.
|
|
30
|
-
mobile_mcp_ai-2.
|
|
31
|
-
mobile_mcp_ai-2.
|
|
32
|
-
mobile_mcp_ai-2.
|
|
33
|
-
mobile_mcp_ai-2.
|
|
27
|
+
mobile_mcp_ai-2.7.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
28
|
+
mobile_mcp_ai-2.7.0.dist-info/METADATA,sha256=-yl4iprOIClkYPv7YmyRLrDF9QBlRvhkro86tuqYr6U,11505
|
|
29
|
+
mobile_mcp_ai-2.7.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
30
|
+
mobile_mcp_ai-2.7.0.dist-info/entry_points.txt,sha256=KB_FglozgPHBprSM1vFbIzGyheFuHFmGanscRdMJ_8A,68
|
|
31
|
+
mobile_mcp_ai-2.7.0.dist-info/top_level.txt,sha256=lLm6YpbTv855Lbh8BIA0rPxhybIrvYUzMEk9OErHT94,11
|
|
32
|
+
mobile_mcp_ai-2.7.0.dist-info/RECORD,,
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
工具选择辅助函数 - 帮助 AI 选择正确的工具
|
|
5
|
-
|
|
6
|
-
这个模块提供工具选择建议,帮助 AI 在执行用例时遵循最佳实践。
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from typing import Dict, List, Optional, Tuple
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ToolSelectionHelper:
|
|
13
|
-
"""工具选择辅助类"""
|
|
14
|
-
|
|
15
|
-
@staticmethod
|
|
16
|
-
def should_use_list_elements(scenario: str) -> Tuple[bool, str]:
|
|
17
|
-
"""判断是否应该使用 mobile_list_elements()
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
scenario: 使用场景描述
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
(should_use, reason) 元组
|
|
24
|
-
"""
|
|
25
|
-
scenarios_should_use = [
|
|
26
|
-
"查找元素", "定位元素", "点击元素", "查找按钮", "查找文本",
|
|
27
|
-
"检查元素", "验证元素", "确认元素", "获取元素",
|
|
28
|
-
"进入页面", "新页面", "页面加载",
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
scenario_lower = scenario.lower()
|
|
32
|
-
for keyword in scenarios_should_use:
|
|
33
|
-
if keyword in scenario_lower:
|
|
34
|
-
return True, f"场景'{scenario}'应该优先使用 mobile_list_elements() 获取元素列表"
|
|
35
|
-
|
|
36
|
-
return False, ""
|
|
37
|
-
|
|
38
|
-
@staticmethod
|
|
39
|
-
def should_use_screenshot(scenario: str) -> Tuple[bool, str]:
|
|
40
|
-
"""判断是否应该使用截图
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
scenario: 使用场景描述
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
(should_use, reason) 元组
|
|
47
|
-
"""
|
|
48
|
-
scenarios_should_use = [
|
|
49
|
-
"视觉确认", "查看页面", "页面截图", "展示页面",
|
|
50
|
-
"控件树找不到", "元素不存在", "游戏", "Unity",
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
scenario_lower = scenario.lower()
|
|
54
|
-
for keyword in scenarios_should_use:
|
|
55
|
-
if keyword in scenario_lower:
|
|
56
|
-
return True, f"场景'{scenario}'可以使用截图作为兜底方案"
|
|
57
|
-
|
|
58
|
-
return False, ""
|
|
59
|
-
|
|
60
|
-
@staticmethod
|
|
61
|
-
def get_tool_selection_advice(action: str, elements: Optional[List[Dict]] = None) -> str:
|
|
62
|
-
"""获取工具选择建议
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
action: 要执行的操作(如"点击设置按钮")
|
|
66
|
-
elements: 已有的元素列表(如果有)
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
工具选择建议文本
|
|
70
|
-
"""
|
|
71
|
-
advice = []
|
|
72
|
-
|
|
73
|
-
# 如果还没有元素列表,建议先获取
|
|
74
|
-
if not elements:
|
|
75
|
-
advice.append("💡 建议:先调用 mobile_list_elements() 获取元素列表")
|
|
76
|
-
advice.append(" 然后使用 mobile_click_by_text() 或 mobile_click_by_id() 点击")
|
|
77
|
-
advice.append(" 仅在控件树找不到元素时,才使用 mobile_screenshot_with_som()")
|
|
78
|
-
else:
|
|
79
|
-
advice.append("✅ 已有元素列表,可以直接使用 mobile_click_by_text() 或 mobile_click_by_id()")
|
|
80
|
-
advice.append(" 无需再次调用 mobile_list_elements() 或截图")
|
|
81
|
-
|
|
82
|
-
return "\n".join(advice)
|
|
83
|
-
|
|
84
|
-
@staticmethod
|
|
85
|
-
def find_element_in_list(elements: List[Dict],
|
|
86
|
-
text: Optional[str] = None,
|
|
87
|
-
resource_id: Optional[str] = None,
|
|
88
|
-
content_desc: Optional[str] = None) -> Optional[Dict]:
|
|
89
|
-
"""在元素列表中查找元素
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
elements: 元素列表
|
|
93
|
-
text: 要查找的文本
|
|
94
|
-
resource_id: 要查找的 resource_id
|
|
95
|
-
content_desc: 要查找的 content_desc
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
找到的元素,如果未找到返回 None
|
|
99
|
-
"""
|
|
100
|
-
for elem in elements:
|
|
101
|
-
if text and elem.get("text") == text:
|
|
102
|
-
return elem
|
|
103
|
-
if resource_id and elem.get("resource_id") == resource_id:
|
|
104
|
-
return elem
|
|
105
|
-
if content_desc and elem.get("content_desc") == content_desc:
|
|
106
|
-
return elem
|
|
107
|
-
|
|
108
|
-
return None
|
|
109
|
-
|
|
110
|
-
@staticmethod
|
|
111
|
-
def suggest_click_method(elements: List[Dict], target_text: str) -> Dict:
|
|
112
|
-
"""建议点击方法
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
elements: 元素列表
|
|
116
|
-
target_text: 目标文本
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
建议信息字典
|
|
120
|
-
"""
|
|
121
|
-
# 查找元素
|
|
122
|
-
target_elem = ToolSelectionHelper.find_element_in_list(elements, text=target_text)
|
|
123
|
-
|
|
124
|
-
if target_elem:
|
|
125
|
-
# 元素存在,建议使用文本点击
|
|
126
|
-
return {
|
|
127
|
-
"found": True,
|
|
128
|
-
"method": "mobile_click_by_text",
|
|
129
|
-
"params": {"text": target_text},
|
|
130
|
-
"reason": f"元素'{target_text}'在控件树中找到,建议使用 mobile_click_by_text('{target_text}')"
|
|
131
|
-
}
|
|
132
|
-
else:
|
|
133
|
-
# 元素不存在,建议使用截图
|
|
134
|
-
return {
|
|
135
|
-
"found": False,
|
|
136
|
-
"method": "mobile_screenshot_with_som",
|
|
137
|
-
"params": {},
|
|
138
|
-
"reason": f"元素'{target_text}'在控件树中未找到,建议使用 mobile_screenshot_with_som() 作为兜底方案"
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# 导出辅助函数
|
|
143
|
-
def should_use_list_elements_first(action: str) -> bool:
|
|
144
|
-
"""判断是否应该优先使用 list_elements
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
action: 要执行的操作描述
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
是否应该优先使用 list_elements
|
|
151
|
-
"""
|
|
152
|
-
helper = ToolSelectionHelper()
|
|
153
|
-
should_use, _ = helper.should_use_list_elements(action)
|
|
154
|
-
return should_use
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def get_click_suggestion(elements: List[Dict], target_text: str) -> Dict:
|
|
158
|
-
"""获取点击建议
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
elements: 元素列表
|
|
162
|
-
target_text: 目标文本
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
点击建议字典
|
|
166
|
-
"""
|
|
167
|
-
helper = ToolSelectionHelper()
|
|
168
|
-
return helper.suggest_click_method(elements, target_text)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|