关于 Mobdev
Mobdev 是一款 Mac 应用,让用户和 AI Agent 在同一窗口操作 iPhone、iOS 模拟器和 Android 设备。它提供设备画面、触控输入、应用管理及 Agent 接口。
主要功能
- 01镜像设备画面并发送触控与键盘输入
- 02安装、启动和管理应用
- 03查看控制台输出与崩溃报告
- 04录制和重放操作流程
- 05检查界面元素并生成点击步骤
适用场景
- 01让编码 Agent 操作移动设备
- 02在设备上安装并测试应用
- 03查看应用日志和崩溃信息
- 04录制并重放测试流程
MCP 相关
Mobdev 附带 MCP 服务,向客户端提供连接设备、查看画面及执行输入等能力。可通过 stdio 或 Streamable HTTP 连接。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
适用于 Mac 的 Mobdev
Mobdev 应用:将你的 iPhone、iOS 模拟器和 Android 设备集中在一个窗口中,供你和 AI Agent 使用。免费、开源、约 2 MB,无需账户。
Mobdev 通过 USB 线读取 iPhone 屏幕,并通过蓝牙模拟键盘和指针进行点击和输入。手机上无需安装任何内容:无需开发者模式,也无需越狱。(只有可选的 iPhone UI 树功能会在手机上运行一个小型 UI 测试。)已启动的模拟器和 Android 设备也使用相同的工具。Agent 可使用 MCP 服务和小型 HTTP API。 可选的中继服务可让其他位置的 Agent 访问这些设备。
USB cable: screen frames (like QuickTime)
┌─────────┐ ◀──────────────────────────────────────── ┌────────┐
│ Mac │ │ iPhone │
│ Mobdev │ ────────────────────────────────────────▶ │ │
└─────────┘ Bluetooth LE: keyboard + pointer (HID) └────────┘
▲ ▲ AssistiveTouch turns
│ └── MCP / HTTP on 127.0.0.1 the pointer into taps
│
└── optional outgoing connection to a relay (see ../relay)
要求
- 配备蓝牙 LE、运行 macOS 26 或更高版本的 Mac(界面使用 Liquid Glass)。在 macOS 27 和 Swift 6.4 上开发。
- 一部 iPhone 和一根 USB 数据线。仅充电线只能供电,无法传输画面。
- 构建要求:Xcode 26 或更高版本(Swift 6.2+、macOS 26 SDK)。
构建和运行
cd macos
MOBDEV_VARIANT=release scripts/build-app.sh # builds build/Mobdev.app (ad hoc signature)
open build/Mobdev.app # allow Bluetooth and Camera when macOS asks
macOS 会将 iPhone 屏幕视为摄像头,因此会显示摄像头权限提示。如需使用证书签名,请设置 CODESIGN_IDENTITY;UNIVERSAL=1 会同时为 Apple 芯片和 Intel 构建。使用临时签名时,macOS 可能会在每次重新构建后再次请求权限。
如果未设置 MOBDEV_VARIANT=release,脚本会构建用于开发的 Mobdev Dev(build/Mobdev Dev.app)。它是一个独立应用,可与已安装的 Mobdev 同时运行而不对其造成影响:包 ID 为 dev.mobdev.mac.dev,在 ~/Library/Application Support/dev.mobdev.mac.dev 中存储独立的设置和密钥,端口为 4687,URL scheme 为 mobdev-dev://(因此仪表板链接仍会在已安装的应用中打开),MCP 名称为 mobdev-dev,使用琥珀色图标且不会自动更新。两个应用共用 Mac 的蓝牙,因此测试触控前请先退出其中一个。
设置 iPhone(仅需一次)
首次启动时,设置助手会引导你完成以下步骤。在说明相应功能的页面上,它会请求摄像头和蓝牙权限;Mac 检测到每个步骤已完成后,就会将其标记为完成。可通过 Mobdev › 设置 iPhone… 再次打开。如没有 iPhone,请在欢迎页面选择 仅使用模拟器和 Android:在设置设备之前,不会请求摄像头或蓝牙权限。
- 插入 iPhone,解锁并轻点 信任。如果 Mac 询问是否允许配件接入,请点击 允许。 屏幕会显示在 Mobdev 中。
- 在 iPhone 上打开 设置 > 蓝牙,在 其他设备下轻点这台 Mac 进行配对。iOS 会显示 Mac 的名称(系统设置 › 通用 › 共享),而不是“Mobdev”。使用同一 Apple Account 的 iPhone 可能找不到 Mac,因为它已经通过接力功能识别了这台 Mac,并信任缓存的蓝牙服务。在 iPhone 完成配对前,Mobdev 会在 8 秒后重建蓝牙服务,之后按间隔加倍的方式重建,最长间隔为 2 分钟,以促使 iOS 再次查找。设置中的 再次在 iPhone 上显示可立即执行此操作。
- 打开 设置 > 辅助功能 > 触控 > 辅助触控。随后 iOS 会显示指针,并将点击转换为轻点。在同一页面关闭 吸附到项目,并保持开启 执行触控手势。开启“吸附到项目”后,iOS 会将指针移到最近的项目上,每次滑动都会变成对该项目的轻点。在首次滑动前,以及镜像中的拖动操作后,Mobdev 都会在不点击的情况下检查此设置,并在设备面板和
status中显示检查结果 指针;如果指针会吸附,swipe就会拒绝执行。 - 在 Mobdev 中选择与 iPhone 上“设置 > 通用 > 键盘 > 硬件键盘”相匹配的键盘布局(美国或德国)。
- Agent 工作期间,将自动锁定设为“永不”。手机必须保持解锁。
iOS 在连接蓝牙键盘时会隐藏屏幕键盘,无论是否连接了线缆。闲置 5 分钟后,Mobdev 会断开蓝牙连接,让 iPhone 再次显示自己的键盘;之后 Mobdev 的下一次点击或按键会在几秒内重新连接(
MOBDEV_BLUETOOTH_REST_SECONDS可更改这 5 分钟)。iPhone 或 iPad 插入后,如果尚未配对,Mobdev 会在 10 分钟内保持连接,以便仍可进行配对;之后点击 Show on iPhone Again 即可重新连接。将鼠标移到镜像画面上会立即连接,并保持蓝牙唤醒,因此第一次点击不会延迟。在 iPhone 重新连接所有输入设备之前,Mobdev 会拒绝输入,而不是在没有先移动指针的情况下发送点击。
应用中的镜像画面是实时的:点击即可轻点,拖动即可滑动,在触控板上用两根手指滑动(也可横向滑动),用滚轮滚动(横向滚轮或 Shift 键可横向滚动),并在镜像画面获得焦点时输入。 ⌘V 可将 Mac 剪贴板内容输入手机。右侧的检查器会显示尚缺少的内容,Activity 会列出每项 Agent 操作,而 Settings(⌘,)中可设置键盘布局和 API 令牌。
- Apps(在检查器中)会列出为开发而安装的应用,或列出所有应用。将
.app、.ipa或.apk拖到其中即可安装;也可启动、停止和移除应用,实时查看应用控制台、打开崩溃报告,以及尝试深层链接。它使用与 Agent 相同的工具,因此在 iPhone 上需要启用开发者模式并安装 Xcode,而模拟器和 Android 设备则无需这些条件。 - Diagnose…(位于设置步骤下方和设备信息中)会检查线缆、画面、iPhone 的 USB 屏幕接口、macOS 的屏幕捕获助手、蓝牙、辅助触控和开发者模式,并针对每项提供修复方法,例如重启卡住的屏幕捕获助手。
- Projects(位于侧边栏顶部)会在项目仓库的文件夹中保存应用的测试、流程、屏幕截图、地图、录制内容和运行记录;请参阅 Projects。
- Record 和 Run Flow…(位于活动记录标题栏中)可录制和重放流程;请参阅下文。
- Inspect(位于工具栏中)会在屏幕上叠加检查器:指针下方的元素会以轮廓标出,并显示其角色、标签和标识符;点击该元素会复制轻点该元素的步骤(例如
{"tap_element":{"id":"login"}}),而不是轻点设备。适用于模拟器、Android 设备,以及已启用 UI 树的 iPhone。 - Settings › Agents 会列出 Agent 可能无法打开的应用;请参阅 Security and privacy。
连接 Agent
应用会显示可直接复制且路径正确的配置。stdio 变体无需令牌:Mobdev mcp 会在本地读取令牌,并在需要时于后台启动应用。它通过应用数据目录中的 Unix 套接字连接应用,而非使用 TCP 端口,因此 Mac 上的其他用户无法冒充应用并获取令牌。
Claude Code:
claude mcp add --scope user mobdev -- /Applications/Mobdev.app/Contents/MacOS/Mobdev mcp
Codex(~/.codex/config.toml):
[mcp_servers.mobdev]
command = "/Applications/Mobdev.app/Contents/MacOS/Mobdev"
args = ["mcp"]
Claude Desktop、Cursor 和其他客户端:
{ "mcpServers": { "mobdev": { "command": "/Applications/Mobdev.app/Contents/MacOS/Mobdev", "args": ["mcp"] } } }
支持 Streamable HTTP 的客户端也可使用 http://127.0.0.1:4686/mcp,并提供 Authorization: Bearer <token>(在应用中复制)。该服务支持 MCP 2026-07-28,以及基于较早 initialize 的 2024-11-05 至 2025-11-25 版本。
工具
坐标以 screenshot 返回图像中的像素为单位(长边为 1280 px,原点位于左上角)。除非 screenshot 为 false,操作都会通过 MCP 返回一张新的屏幕截图。连接多台设备时,传入 device(来自 list_devices 的 ID 或名称)以选择设备。若未指定,Mobdev 会使用唯一的设备;若模拟器或 Android 设备与 iPhone 同时运行,则使用已连接的 iPhone。
| 工具 | 参数 | |
|---|---|---|
list_devices | iPhone、已启动的模拟器和 Android 设备:ID、名称、型号、系统版本、就绪状态 | |
status | 屏幕和输入就绪状态、截图尺寸 | |
screenshot | 屏幕的 JPEG 图像 | |
tap | x、y、double | x 和 y 也可以是屏幕比例,例如 "50%";所有带坐标的工具均如此 |
long_press | x、y、seconds | |
swipe | from_x、from_y、to_x、to_y、duration | |
scroll | direction(up/down/left/right)、amount、x、y | 鼠标滚轮;right 可显示更右侧的内容 |
type_text | text、submit | 在当前聚焦的输入框中输入,每次调用最多 1000 个字符 |
press_key | key、modifiers、count | 例如,space + cmd 可打开 Spotlight;volume_up 和 volume_down 用于按下音量按钮(iPhone、Android) |
home | ||
open_app | name | 在 iPhone 上通过 Spotlight 打开;在模拟器和 Android 上按名称或 bundle ID 打开 |
read_screen | 屏幕上所有可见文字及其位置(设备端 OCR) | |
find_text | text | |
tap_text | text、index、double、hold | 点击可见标签;double 表示点击两次,hold 表示按住指定秒数 |
wait_for_text | text、timeout、gone | |
observe | image、contains | 将屏幕内容标记为编号:优先使用 UI 树元素,否则使用识别出的文字;image 会在截图上绘制这些标记 |
tap_mark | mark | 点击上次 observe 返回的标记 |
scroll_until_visible | text、id、direction、max_scrolls | 滚动直到目标显示;到达列表末尾时停止 |
wait_for_idle | timeout、stable | 等待屏幕停止变化,不考虑状态栏 |
ui_tree | contains、all | 无障碍树中的元素:角色、标签、标识符、值、位置。适用于模拟器、Android,以及已启用 UI 树的 iPhone |
tap_element | id、text、index、timeout、double、hold | 按标识符或标签点击元素,最多等待 5 秒 |
wait_for_element | id、text、timeout、gone | 类似 wait_for_text,但数据来自树 |
assert_screenshot | name 或 path、threshold、mask、compare_status_bar、update | 将屏幕与基准图像比较,并在不匹配时提供差异图;如果没有基准图像,则记录当前屏幕(参见 检查) |
accessibility_audit | image、fail_on、ignore | 从 UI 树中检查缺失标签、过小的点击目标、文本对比度过低、重复或含糊的标签 |
assert_with_ai | question、expect | 向 Mac 上的 Apple Intelligence 提出有关屏幕的是非问题 |
run_flow | path、steps、variables、video | 重放工作流(JSON 或 Maestro YAML),并在第一个失败步骤处停止;video 会保存录制文件(.mp4) |
run_tests | project、tests、variables、video、language | 在此设备上运行项目测试:返回每项结果、第一个失败步骤的屏幕、results.json 和 junit.xml |
list_projects | 列出 Mobdev 已知的项目、当前激活的项目,以及调用所使用的项目 | |
create_project | path、name、bundle_id、builds | 创建项目(例如 ~/code/my-app/mobdev)并将其设为当前项目;如果未指定 bundle_id,则使用仓库中唯一的应用,或列出可选应用 |
open_project | project | 将项目设为当前项目:可以是其文件夹、含有 mobdev/ 的仓库,或项目名称 |
list_tests | project | 列出项目的测试及最新结果 |
save_test | project、name、steps、description、platforms、file | 将测试写入项目;如有需要则创建项目 |
save_flow | project、name、steps、file | 将工作流写入项目的 flows/ |
test_result | project、run | 返回最新一次运行的结果,包括每个失败步骤、截图和视频 |
save_screenshot | path | 将屏幕以全分辨率 PNG 格式保存到项目的 screenshots/,例如 de-DE/iphone-6.9/01-home |
crawl_app | bundle_id、max_actions、max_screens、max_depth、seconds、avoid、output | 无需模型即可探索应用、绘制屏幕地图,并记录每次崩溃及触发崩溃的步骤 |
navigate_to | bundle_id、screen、map | 前往 crawl_app 已知地图中的屏幕 |
list_apps | all | 列出为开发安装的应用,或所有应用 |
install_app | path | 从此 Mac 上的路径安装构建文件:iPhone 使用 .app/.ipa,模拟器使用 .app,Android 使用 .apk |
uninstall_app | bundle_id | 仅限为开发安装的应用 |
launch_app | bundle_id、arguments、environment、restart | 捕获应用输出的内容 |
stop_app | bundle_id | |
open_url | url | 打开深层链接、通用链接或网页;在存在 UI 树时确认 iOS 的“在……中打开?”提示 |
logs | bundle_id、after、lines、contains | 查看已启动应用的输出及其结束方式 |
crash_reports | app、name、limit | 列出报告;name 用于读取单个报告 |
start_network_capture | bundle_id | 记录应用发出的请求(参见 网络) |
network_log | bundle_id、contains、after、limit、query、headers | 查看已记录的请求:方法、URL、状态、双向字节数、持续时间和发现位置 |
stop_network_capture | bundle_id | 结束抓取;在 Android 上会从设备中移除 Mobdev 代理 |
mock_response | url、status、body、content_type、clear | Android:为通过代理发送的普通 HTTP 请求返回固定响应 |
set_location | latitude、longitude、route、speed、clear | 设置模拟位置,或设置设备沿其移动的路线 |
set_permission | bundle_id、permission、state | 无需提示即可授予、撤销或重置位置、照片、通讯录、相机等权限 |
send_push | bundle_id、title、body、badge、data、payload | 向模拟器上的应用发送推送通知 |
set_appearance | dark、text_size、increase_contrast、reduce_motion | 深色模式、动态字体和无障碍开关 |
set_language | language、bundle_id | 例如 de-DE:设置模拟器语言,或某个 Android 应用的语言 |
set_status_bar | preset、time、battery_level、battery_state、wifi_bars、cellular_bars、network | screenshot 会设置为 9:41 和满格信号;clear 会重置 |
biometrics | action(match/fail/enroll/unenroll) | 响应 Face ID、Touch ID 或指纹提示 |
reset_app | bundle_id、keychain | 删除应用数据,效果如同刚安装 |
clipboard | text | 读取剪贴板,或将其内容设为 text |
set_orientation | orientation | portrait、landscape_left、landscape_right、portrait_upside_down |
start_recording | path | 将屏幕录制为 .mp4,直到调用 stop_recording;默认保存到项目的 output/recordings |
stop_recording | 结束录制并告知文件位置 | |
recent_steps | count、clear | 返回最近成功执行的操作,格式为可供 save_test 使用的工作流步骤 |
run_shortcut | name | 运行 Apple“快捷指令”应用中的快捷指令 |
performance | bundle_id、seconds、max_cpu、max_memory_mb、max_janky_percent | 查看运行中应用的 CPU、内存和(Android)帧表现;超出预算会导致调用失败。适用于模拟器和 Android |
measure_launch | bundle_id、runs、method、max_ms、stable | 多次运行测量冷启动时间:最小值、中位数、最大值 |
dev_menu | port | 打开 React Native 或 Expo 应用的开发者菜单 |
reload_app | port | 通过 Metro 重新加载 React Native 或 Expo 应用;否则通过其开发者菜单重新加载 |
文本识别在 Mac 上使用 Apple 的 Vision 框架;除非你的 Agent 将屏幕内容发送给其模型,否则内容不会离开设备。每次识别都会在一个全新的进程(Mobdev __text)中运行;如果 Neural Engine 失效,则会切换到 CPU。如果仍然失败,文本工具会在模拟器、Android 和搭载 Mobdev Runner 的 iPhone 上从 UI 树读取信息,并说明这一点。如果没有 UI 树,工具会说明识别失败,并建议使用 screenshot。 |
从终端运行
Mobdev call <tool> 可在 shell 中运行一个工具,适用于脚本以及偏好使用终端而非 MCP 的 Agent;Mobdev <tool> key=value … 是它的简写,而 Mobdev tools 会列出所有工具。能解析为 JSON 的值会按 JSON 处理,否则按文本处理:
M=/Applications/Mobdev.app/Contents/MacOS/Mobdev
$M observe
$M tap_mark mark=4
$M type_text text="hello world" submit=true
$M screenshot --image screen.png
$M call set_location '{"latitude": 52.52, "longitude": 13.405}' --device "iPhone 17"
命令会通过正在运行的应用执行,因此也能访问 iPhone,并会显示在 Activity 中;如果应用未运行,或使用了 --local,则会像 Mobdev flow 一样,在已启动的模拟器和 Android 设备上独立运行。--json 会输出完整结果,--image 会保存返回的屏幕截图,而 --project 用于指定工具保存内容的项目(默认值:当前文件夹所属的项目,参见项目)。工具执行成功时退出码为 0,报告错误时为 1,无法运行时为 2。start_recording、tap_mark、网络捕获以及其他需要依赖先前调用的操作,都需要应用运行。
观察、标记和等待
observe 是 Agent 查看屏幕最省资源的方式:它会列出可读取或点击内容的编号列表;有 UI 树时从 UI 树读取,否则使用文本识别,例如 [4] Button "Sign in" id=login (295, 640)。对于 Android 列表中这类没有标签但可点击的行,工具会读取行内文本。image: true 会添加一张标有编号的屏幕截图,帮助容易误判坐标的模型。tap_mark 会按编号点击标记;屏幕变化后请再次调用 observe。录制工作流期间,点击标记会保存为 tap_element(或 tap_text),使工作流不依赖编号。
scroll_until_visible 会持续滚动,直到显示某个元素(id)或文本,并在列表不再移动时停止。wait_for_idle 会等待屏幕在 stable 秒内保持不变(不计状态栏),以代替在动画或加载后固定暂停一段时间。
设备状态
测试和 Agent 直接设置情境,而不是逐步点击「设置」:
| iOS Simulator | Android | iPhone | |
|---|---|---|---|
set_location | simctl location,也支持路线 | 模拟器(geo fix);Mobdev 会沿路线移动 | devicectl,需要开发者模式 |
set_permission | simctl privacy:日历、联系人、位置、照片、麦克风、运动、提醒事项、Siri(不包括相机或通知) | pm grant/revoke,也支持相机和通知,或任何 android.permission 名称 | 不支持 |
send_push | 使用 APNs 负载的 simctl push | 从 Shell 发布通知,不会投递给应用 | 不支持;推送需通过你的 APNs 密钥发送 |
set_appearance | simctl ui;减少动态效果需要 Xcode 27 | 夜间模式、字体缩放、对比度、动画 | devicectl,需要开发者模式 |
set_language | 整个模拟器;重新启动应用 | 单个应用(Android 13 及更高版本) | 不支持 |
set_status_bar | simctl status_bar | System UI 演示模式 | devicectl,需要开发者模式 |
biometrics | Face ID 和 Touch ID,无论是否已录入 | 模拟器的指纹传感器(在「设置」中录入一次) | 不支持 |
reset_app | 删除应用数据,可选择是否删除钥匙串 | pm clear | 不支持;改为重新安装 |
clipboard | simctl pbcopy/pbpaste | 不支持 | devicectl,需要开发者模式 |
set_orientation | Xcode 27 | 支持任意方向 | devicectl,需要开发者模式 |
iPhone 列使用 Xcode 27 的 devicectl,目前已在模拟器上测试,尚未在 iPhone 上测试。若要让 iOS 应用以其他语言启动一次,可使用带有 arguments ["-AppleLanguages", "(de)", "-AppleLocale", "de_DE"] 的 launch_app,在模拟器和 iPhone 上都有效。通过 set_language 设置的语言会在应用再次启动时生效;系统提醒则会在模拟器重启后切换语言。模拟器的截图在设备旋转时仍保持正向:应用会在截图中横向显示,点击也使用同一张图片,因此仍会落在 Agent 看到的元素上。
录制和最近步骤
start_recording 会将屏幕录制为 .mp4 文件(保存到项目的 output/recordings 中;如果 recordings/ 位于 Mobdev 文件夹中且未指定项目,则保存在该文件夹中),直到调用 stop_recording;在模拟器和 iPhone 上的录制帧率约为每秒 10 帧;30 分钟后会自动停止。recent_steps 会按流程步骤返回设备上由任何 Agent 或应用执行的最新成功操作:浏览操作和失败的调用会被省略,输入操作会合并。Agent 找到通过应用的路径后,会使用 save_test 保存这些步骤。
run_shortcut 会按名称运行 Apple「快捷指令」应用中的快捷指令:在开发者模式下通过模拟器和 iPhone 上的 shortcuts:// 链接运行,否则通过 Spotlight 运行。快捷指令可以执行工具无法从外部触发的操作,例如开启专注模式或更改亮度。
开发者工具
从 list_apps 到 crash_reports 的工具为你构建的应用形成了完整闭环:Agent 使用
xcodebuild 构建应用,安装构建产物,启动应用,使用上述工具操控应用,并读取其输出和崩溃报告。
这些工具使用 Xcode 的 devicectl,因此需要 Mac 上安装 Xcode,并在 iPhone 上开启开发者模式(设置 > 隐私与安全性 > 开发者模式)。其他所有功能都不需要这两项。
xcodebuild -scheme MyApp -destination 'generic/platform=iOS' -derivedDataPath build build
# install_app {"path": "/path/to/build/Build/Products/Debug-iphoneos/MyApp.app"}
# launch_app {"bundle_id": "com.example.MyApp"}
# logs {"bundle_id": "com.example.MyApp"} → print, NSLog and os_log lines
launch_app会设置OS_ACTIVITY_DT_MODE,让Logger和os_log消息像在 Xcode 中一样输出到控制台。devicectl会在启动片刻后连接控制台,因此启动后最初几毫秒内打印的行可能会缺失。logs会返回一个cursor;将它作为after传入,即可只获取更新的行。应用退出或崩溃时,logs会说明情况,crash_reports则会列出报告。crash_reports搭配name使用时,会将报告复制到~/Library/Application Support/dev.mobdev.mac/crash-reports,并返回异常、原因和崩溃线程。如果报告没有符号信息,你自己的代码帧会附带供atos使用的地址。install_app会在运行 Mobdev 的 Mac 上读取path,即使 Agent 是通过中继连接也一样。在其他位置构建的应用(例如云端 Agent 容器中的构建产物)会先上传,再通过upload安装(参见从任意位置安装构建产物)。uninstall_app不会卸载 App Store 应用和系统应用。
性能
performance 会对正在运行的应用采样 seconds 秒(默认 5 秒,最多 60 秒),并报告 CPU 平均值和峰值(以单个核心的百分比表示)、开始时、结束时和峰值时的内存,以及 Android 上的帧数据。measure_launch 会停止应用、重新启动应用,并计时 runs 次(默认 3 次)。
预算可将这两项变成测试和工作流的检查:当应用超过 max_cpu(平均值)、max_memory_mb(峰值)、max_janky_percent 或 max_ms(中位数)时,调用会失败并返回相关数值。
- iOS Simulator:应用是 Mac 上的进程。Mobdev 会在模拟器内通过
launchctl list查找其 pid,并使用proc_pid_rusage读取 CPU 时间和物理内存占用量(即 Xcode 仪表显示的内存)。启动时间是 iOS 自带的ApplicationFirstFramePresentationsignpost,通过simctl spawn <udid> log stream获取:从 SpringBoard 收到启动请求到应用显示第一帧的时间,也是 Xcode Organizer 和 MetricKit 报告的数值。模拟器没有帧耗时数据;如需此类数据,请使用 Android 或 Instruments。模拟器与 Mac 共享 CPU,因此这些数值反映的是应用与自身对比的表现,而非 iPhone 的运行速度。 - Android:一条 adb shell 命令每秒左右读取一次
/proc/<pid>/stat和VmRSS,在读取前重置dumpsys gfxinfo,读取后获取其数据(帧、卡顿占比、第 50 至第 99 百分位的帧耗时),并加上dumpsys meminfo中的 PSS。启动时间是am force-stop后am start -W返回的TotalTime。 - iPhone:CPU 和内存数据需要使用 Instruments,因此
performance会说明如何通过xcrun xctrace record --template 'Activity Monitor'录制。measure_launch则会测量屏幕启动耗时。 method: "screen"(任意设备):从第一个发生变化的画面(启动动画)开始,到屏幕保持静止stable秒(默认 2 秒)之前的最后一次变化为止。这包括应用显示第一帧后加载的内容,但如果启动画面或闪屏显示时间超过stable,计时会提前结束。- 通过中继发起的调用限时 90 秒,因此请确保
seconds和启动计时的runs次数都不会超时。
React Native、Expo 和 Flutter
reload_app连接到这台 Mac 上 Metro 的消息套接字(ws://localhost:<port>/message,默认端口为 8081,也适用于 Expo CLI),并向所有连接到它的应用发送reload,效果与在 Metro 终端中按r相同;Expo CLI 仅接受来自同一台 Mac 的指令。如果没有应用连接,或 Metro 没有响应,Mobdev 会打开开发者菜单并点按其中的 Reload。dev_menu会摇动模拟器(发送 Darwin 通知com.apple.UIKit.SimulatorShake,模拟器中的 UIKit 会将其转换为对最前台应用的摇动操作),在 Android 上按下 Menu 键(input keyevent 82);在无法从 Mac 摇动的 iPhone 上,则通过 Metro 发送devMenu。- 使用
open_url和exp://127.0.0.1:8081打开 Expo Go 项目;使用<scheme>://expo-development-client/?url=…打开开发版本。testID是tap_element使用的id。mobdev-react-native技能涵盖工作循环、日志和 Flutter。
网络
start_network_capture、network_log 和 stop_network_capture 可显示应用发出的请求,
用于调试、检查分析数据和 API 调用,以及进行隐私审计。设备上不会安装任何内容,
也不会解密 HTTPS,因此各个平台只能显示其能够获取的信息:
| iOS Simulator、iPhone(Developer Mode) | Android | |
|---|---|---|
| 原理 | 通过 CFNETWORK_DIAGNOSTICS=1 和 ACTIVITY_LOG_STDERR=1 重新启动应用,并读取 CFNetwork 输出到其控制台的日志 | 将设备指向 Mac 上 Mobdev 的代理(settings put global http_proxy):模拟器通过 10.0.2.2 访问代理,手机则通过 adb reverse 访问 |
| 请求范围 | 应用自身发出的 URLSession 请求 | 所有遵循 Android 代理设置的应用,无法区分具体应用 |
| 显示内容 | 方法、协议和主机、状态、收发字节数、耗时、HTTP 版本,以及 host not found 等错误 | 普通 HTTP:方法、完整 URL、状态、字节数、耗时、标头。HTTPS:主机、端口、隧道的收发字节数和耗时 |
| 不显示的内容 | 路径和查询参数(iOS 会将它们记录为 <redacted>)、网页视图(WebKit 会在另一个进程中加载内容)、URLSession 之外的套接字 | HTTPS 内部传输的内容;忽略代理设置的应用,例如 Flutter 应用 |
- 每条记录都会注明其来源(
source:cfnetwork或proxy)。请求完成后才会列出;运行中的请求和打开的 HTTPS 隧道会单独列为仍在运行。除非query为 true,否则查询参数会显示为?…。headers: true会添加普通 HTTP 的标头,其中Authorization、Proxy-Authorization、Cookie和Set-Cookie会替换为<redacted>。 - iOS:
ACTIVITY_LOG_STDERR会将应用的整个系统日志复制到其控制台。网络堆栈的日志行会显示在network_log而非logs中;应用以这种方式运行时,其他系统日志行会显示在logs中。stop_network_capture会停止记录;诊断功能会一直保留,直到launch_app再次启动应用。已在 iOS 27 模拟器上测试;iPhone 的控制台也会通过devicectl以相同方式传输,但尚未测试。 - Android:代理仅监听 127.0.0.1,并通过独立连接转发每个普通 HTTP 请求。当执行
stop_network_capture、设备断开、Mobdev 停止查找设备,以及 Mobdev 或命令行运行退出时,Mobdev 会移除代理设置;发生崩溃后,下一个检测到该设备的 Mobdev 会移除该设置。它不会替换其他工具设置的代理。应用会保留捕获开始前打开的连接,因此请传入bundle_id,以重启要监视的应用。捕获期间,模拟器上的应用仍可通过10.0.2.2访问 Mac 上的开发服务器:代理会代应用连接到 Mac 的127.0.0.1,并记录应用使用的地址。 mock_response(Android)会为 URL 包含url的普通 HTTP 请求返回固定的状态码和响应正文,例如,可针对http://10.0.2.2:3000上的开发服务器测试错误状态。- 捕获在运行中的应用内进行;工作流(
run_flow、Mobdev flow)也可以启动和停止捕获。
模拟器和 Android
已启动的 iOS 模拟器以及 Android 模拟器和手机会与 iPhone 一起显示在应用和 list_devices 中,并可使用相同的工具。无需在这些设备上进行任何设置:不需要连接线、蓝牙或开发者模式。如果只想使用 iPhone,可在“设置”›“通用”中关闭它们。
- iOS 模拟器(需要 Xcode):Mobdev 从模拟器的帧缓冲区读取屏幕,并通过 Xcode 的 SimulatorKit 发送触摸、按键和主屏幕按钮操作,方式与 Simulator.app 和 idb 相同,因此无需辅助应用,也无需保持窗口打开。按键遵循模拟器自身的键盘布局。应用通过
devicectl操作,stop_app通过simctl操作;崩溃报告来自~/Library/Logs/DiagnosticReports。install_app接受为模拟器构建的.app:xcodebuild -scheme MyApp -destination 'generic/platform=iOS Simulator' build。 - Android(需要 Android SDK 中的 adb,例如 Android Studio 提供的版本):只要 adb 服务正在运行,启用了 USB 调试的模拟器和手机就会显示;Mobdev 不会启动该服务。屏幕和输入通过 scrcpy 的服务端处理。首次使用时(显示画面或点击,大约半秒),Mobdev 会将其推送到
/data/local/tmp,并通过 adb 启动:生成长边为 1280 像素的 H.264 流,使用 VideoToolbox 解码,因此窗口每秒显示 20 帧,截屏无需额外开销;触摸会保留真实时序(长按会持续按住,滑动会移动),并支持双指缩放;type_text会将可打印的 ASCII 字符作为按键事件输入,其他文本(变音符号、表情符号、CJK 字符)则通过剪贴板和“粘贴”输入,因此输入后剪贴板中仍会保留该文本;clipboard可读取和设置设备剪贴板。服务端以 shell 用户身份运行,在 Mobdev 退出或设备断开时结束,并删除自身副本;如果服务端意外退出或设备重启,则会重新启动。无法运行时,Mobdev 会回退到 adb,并在日志中说明原因:使用原始screencap帧(每秒一到三帧)、通过input处理触摸和按键、仅支持 ASCII 文本且不支持剪贴板。MOBDEV_ANDROID_SCRCPY=0会让 Mobdev 仅使用 adb。press_key的escape对应返回键。open_app会匹配可启动的包名(“Settings”会打开com.android.settings)。launch_app会跟踪应用的 logcat,crash_reports会读取崩溃缓冲区,install_app接受.apk;arguments和environment会被忽略。
UI 树
在模拟器和 Android 设备上,ui_tree 会根据无障碍树列出屏幕上的元素:角色、标签、标识符、值以及在截图像素坐标中的位置。tap_element 和 wait_for_element 可通过标识符(accessibilityIdentifier,或 Android 上的资源 ID,可使用完整 ID 或末尾部分)或标签查找元素。这比 OCR 更稳定,也能找到只显示图标的按钮。模拟器通过 macOS 的无障碍转换读取,方式与 idb 相同;Android 则通过 uiautomator dump 读取,耗时约两秒。iPhone 需要开启 UI 树(见下文);在开启之前,这些工具会说明如何操作,也可以改用 read_screen 和 tap_text。
iPhone 上的 UI 树
iOS 只会向在手机上运行的 UI 测试显示应用的辅助功能树。在 iPhone 上开启开发者模式,并在 Mac 上安装 Xcode 后,在 Mobdev 中打开 iPhone 的信息,在 UI Tree 下选择一个团队,然后点击 Turn On。Mobdev 会构建 Mobdev Runner,这是一个小型 XCUITest(Runner/),使用你的团队签名并让它在 iPhone 上持续运行,方式与 WebDriverAgent 类似。随后,ui_tree、tap_element 和 wait_for_element 就能像在模拟器上一样工作,Record 也会记录被点击元素的名称。
- 团队:Mobdev 会列出你钥匙串中 Apple Development 证书所属的团队(即证书的组织单位,而不是证书名称中括号内的 ID),并选择最新的团队。Xcode 必须登录该团队的 Apple Account(在 Xcode 的 Settings 中),才能创建配置描述文件并注册 iPhone。运行器的 Bundle ID 为
dev.mobdev.runner.<team>。 - 首次启动:构建需要一两分钟,之后启动只需几秒。使用免费 Apple Account 时,iOS 会提示你信任开发者一次:前往 Settings › General › VPN & Device Management。启动失败时,UI Tree 下方会显示 xcodebuild 给出的原因和常见解决办法,例如开启 Settings › Developer › Enable UI Automation;点击 Try Again 会重新构建。
- 运行期间:Mobdev 会让
xcodebuild test-without-building持续运行,在它结束时重新启动;iPhone 拔出或 Mobdev 退出时会停止运行,iPhone 重新连接后则会再次启动。运行器会以 MobdevRunner-Runner 的名称显示在 iPhone 上。构建文件、日志和项目副本位于~/Library/Application Support/dev.mobdev.mac/runner/<udid>。 - 运行器仅在 iPhone 的回环接口上通过端口 47270 提供 HTTP 服务。Mobdev 通过 USB 线缆使用 usbmuxd 与其通信,每个请求都会携带为本次启动生成的令牌,因此手机上的其他应用无法使用它。点击仍通过 Bluetooth 执行;键盘布局中没有对应按键的文本(例如 emoji)则由运行器输入。
项目
项目是应用仓库中的一个文件夹,通常是 mobdev/,其中包含 Mobdev 为应用保存的所有内容:
mobdev/
mobdev.json the app, its builds and what every test starts with (see Tests)
tests/ tests
flows/ saved flows
baselines/ assert_screenshot's reference pictures
screenshots/ save_screenshot's pictures, such as App Store screenshots
maps/ crawl_app's map of each app
output/ test runs, recordings, crawls, failed checks and flow videos
提交除 output/ 以外的所有内容。它自带 .gitignore,因此仓库原有的 .gitignore 保持不变;该文件夹仅在 Mobdev 首次向其中写入内容时出现。只有 mobdev.json 才会将文件夹标记为项目;其他文件夹会在有内容保存到其中时出现。
- 新建项目…(⇧⌘N,或侧边栏下方的 +)会询问仓库位置,并在其中写入
mobdev/mobdev.json。它会填入仓库声明的 Bundle ID:Expo 的app.json或app.config、Xcode 项目(或 XcodeGen 的project.yml或Info.plist)、Gradle 的applicationId或 Capacitor 配置;优先选择 iOS 和 Android 共用的 ID。字段旁的菜单会列出找到的所有 ID,不包括测试目标和依赖项。打开项目…(⌘O)会添加现有项目,也可以在选择仓库时添加;如果文件夹中没有项目,还会提供创建选项。项目显示在侧边栏顶部;当前项目会显示其 Tests、Flows、Screenshots、App Map、Recordings 和 Runs。 - 新项目的概览会说明接下来要做什么。开始使用会引导你完成四个步骤,并自动勾选已完成的步骤:连接 Agent(Claude Code 插件,或 Codex、Cursor 等使用的 MCP 配置,以及
npx skills add niklas-schmidt-dev/mobdev)、让它编写首批测试、运行测试,然后在 CI 中运行。下方的询问你的 Agent提供可复制的提示词,用于编写测试、绘制应用地图、检查每项更改、复现 Bug、生成商店截图、审核引导流程和设置 CI;提示词会自动填入项目文件夹、应用和设备信息,并注明执行相应任务的 skill。 - 重命名…(位于项目概览的 ⋯ 菜单或项目的上下文菜单中)会更改
mobdev.json中的项目名称,测试结果和 CI 中显示的名称也会随之更改;文件的其余部分保持原样。 - 你选择的项目就是当前项目。未指定路径时,保存内容会放到该项目中:录制内容、
save_screenshot、save_flow、save_test、带有应用地图的爬取结果,以及命名的assert_screenshot基线。省略project参数时,测试工具会使用当前项目;Record 中的**保存…**会打开项目的flows/文件夹,运行 Flow会列出其中的 flows。 - Agent 的工作文件夹优先。
Mobdev mcp和Mobdev call会使用其运行目录对应的项目(该目录或其上层目录中有mobdev/,且不超出仓库根目录;MOBDEV_PROJECT可指定其他项目),应用也会将其用于该 Agent 的调用。因此,在两个仓库或同一仓库的两个 worktree 中工作的 Agent,绝不会把内容保存到对方的项目中。通过 relay 连接的 Agent 会使用当前项目。 - 测试运行结果和项目中的 flow 文件都会留在各自的项目中,即使期间切换了当前项目。
- 每个项目都会显示其应用图标,查找方式与 T3 Code 查找项目 favicon 的方式相同:Expo
app.json中的图标、iOSAppIcon(其中最大的图像)、最高密度的 Androidic_launcher;否则查找 Web 应用存放 favicon 或 logo 的位置,也会查找 monorepo 中的apps/…。点击项目概览中的图标,或在其上下文菜单中选择图标,即可从找到的图像中选择其他图标、选择任意图像文件(PNG、JPEG、SVG、ICO、WebP、HEIC…),或不设置图标。此选择会保存在这台 Mac 上的应用设置中。 - Agent 可通过
list_projects查找项目,通过open_project切换项目,并通过create_project创建项目。 - 如果没有项目,所有内容仍会像以前一样保存到 Mobdev 文件夹。
每种语言的商店截图都来自保存截图的测试,并通过 --language 运行:
{"name": "Store screenshots", "platforms": ["ios"], "steps": [
{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}},
{"set_status_bar": {"preset": "screenshot"}},
{"wait_for_element": {"id": "home.title"}},
{"save_screenshot": {"path": "${LANGUAGE}/iphone-6.9/01-home"}}
]}
Mobdev test mobdev/ --simulator "iPhone 17 Pro Max" --language en-US --language de-DE --test "Store screenshots"
Flows 和 CI
Flow 是以 JSON 保存的工具调用列表,每行一个步骤;不带参数的工具可以直接写工具名。Maestro 的 YAML flows 也可以运行(见下文)。
{
"name": "Sign in",
"steps": [
{"install_app": {"path": "build/Build/Products/Debug-iphonesimulator/MyApp.app"}},
{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}},
{"tap_element": {"id": "email"}},
{"type_text": {"text": "[email protected]", "submit": true}},
{"wait_for_element": {"text": "Welcome"}},
"home"
]
}
控制步骤
控制步骤通过查看屏幕来决定运行哪些内容,因此流程无需猜测等待时长。它们的键不是工具名称:
{
"name": "Check out",
"steps": [
{"launch_app": {"bundle_id": "com.example.MyApp"}},
{"if": {"visible": {"text": "Allow"}, "then": [{"tap_element": {"text": "Allow"}}]}},
{"repeat": {"until_visible": {"id": "checkout"}, "max": 10, "steps": [{"scroll": {"direction": "down"}}]}},
{"extract": {"id": "total", "into": "TOTAL"}},
{"retry": {"times": 2, "steps": [
{"tap_element": {"id": "checkout"}},
{"wait_for_element": {"text": "Pay ${TOTAL}", "timeout": 5}}
]}},
{"run": "pay.json"},
{"tap_element": {"text": "Rate this app", "timeout": 2}, "optional": true}
]
}
| 步骤 | |
|---|---|
{"if": {"visible": {"text": "…"}, "then": […], "else": […]}} | 如果元素或文本此刻出现在屏幕上,则运行 then,否则运行 else(可选)。not_visible 表示相反;{"platform": "ios"} 或 "android" 根据设备决定。可见表示此刻出现在屏幕上:有 UI 树时从 UI 树判断,否则通过文本识别判断(仅限文本)。不会等待任何内容。 |
{"repeat": {"times": 3, "steps": […]}} | 将这些步骤运行 3 次,最多 100 次。 |
{"repeat": {"while_visible": {…}, "max": 20, "steps": […]}} | 在元素出现在屏幕上时运行这些步骤,每轮开始前检查;until_visible 则会一直运行,直到元素可见。max(默认值为 20,最多 100)会结束循环,但不会使流程失败,行为与 Maestro 一致:如果结束时机很重要,请在后面接一个等待步骤。 |
{"retry": {"times": 2, "steps": […]}} | 如果某一步失败,则从第一步重新运行这些步骤,最多额外重试 2 次(默认 1 次,最多 10 次)。失败的尝试会显示在结果中,并标记为 –,但不会导致流程失败。 |
{"run": "sign-in.json"} | 运行另一个流程文件,支持 JSON 或 Maestro YAML;路径相对于引用该文件的文件,在测试项目中也可以相对于项目文件夹。子流程最多嵌套 5 层,流程运行自身会报错。run_flow 的内联 steps 必须使用绝对路径。 |
{"set": {"EMAIL": "[email protected]"}} | 设置供后续步骤使用的变量。 |
{"extract": {"id": "total", "into": "TOTAL"}} | 将 UI 树中的一个元素读入变量:读取其值;若没有值,则读取其标签。"from": "label" 或 "value" 用于选择读取内容。像 tap_element 一样等待元素出现(timeout,默认 5 s);index 用于从多个元素中选择一个。 |
- 步骤字符串(包括条件)中的
${NAME}是变量,来源可以是set和extract、Mobdev flow的--var NAME=value、run_flow的variables,以及测试中的mobdev.json。如果某个名称没有设置,流程会在第一步开始前、在使用该变量的步骤处失败。 - 在任何步骤的键旁添加
"optional": true,即可允许该步骤失败:结果会将其标记为 –,并继续执行流程。 - 结果会为嵌套步骤编号:3.2 表示步骤 3 内的第二步(
else接在then后继续计数);由 repeat 或 retry 再次运行的步骤会注明轮次或尝试次数。流程失败时会指出最内层的失败步骤,例如 "failed at step 3.2 of 7";run_flow的数据中包含failed_step(3)和failed_at("3.2")。Mobdev flow会在每个步骤完成时打印该步骤;控制步骤内的步骤会缩进显示,并在控制步骤本身之前打印。 - 一个流程最多包含 500 个步骤,包括控制步骤和子流程中的步骤;一次运行最多执行 2000 步,包括每一轮和每次重试。取消后会立即停止,包括循环内部。
- 录制会保存纯工具调用;请手动添加控制步骤,或让 Agent 编写。
录制、回放和 CI
-
Record 会在设备的活动记录中收集 Agent 或你对设备进行的每次调用,以及你在应用窗口中的点击、拖动和按键操作。在模拟器和 Android 上,点击具有唯一标识符或标签的元素会保存为
tap_element,因此布局变化后流程仍然有效;其他点击则保存为同一位置的tap。Save… 会将文件写入当前项目的flows/目录,除非你选择其他文件夹;Agent 会使用save_flow将流程保存在该目录中。 -
Run Flow…、
run_flow工具(path指向 Mac 上的 .json 或 Maestro .yaml 文件,或内联的steps)和Mobdev flow都会回放流程,并在第一个失败步骤处停止并指出该步骤。tap_element最多等待元素 5 s,因此流程很少需要显式等待。 -
每次运行都可以保留视频。Run Flow… 会录制视频,运行结果中会提供 Show Video;应用会在项目的
output/flow-videos目录中保留最新的 20 个视频;如果没有项目,则保存在~/Library/Application Support/dev.mobdev.mac/flow-videos。当run_flow的video指定了 Mac 上的 .mp4 文件时,也会写入视频。在模拟器、iPhone 和 Android 上,视频约为每秒 10 帧(Android 上没有 scrcpy 时,帧率取决于screencap的能力,为 1 到 3 帧)。视频会以流程结束时停留的屏幕画面作为结尾,并保持片刻,让失败一目了然。 -
Mobdev flow无需应用即可运行,适用于脚本和 CI,可在已启动的模拟器和 Android 设备上运行:/Applications/Mobdev.app/Contents/MacOS/Mobdev flow sign-in.json --device <udid> --artifacts out它会为每个步骤输出一行;所有步骤均通过时退出代码为 0,有步骤失败时为 1,流程无法启动时为 2。
--artifacts会保留运行视频(run.mp4)、活动日志、复制的崩溃报告、summary.md,以及失败后生成的failure.png;--no-video会跳过视频,--var NAME=value会设置变量(可重复使用)。由于模拟器在速度较慢的 CI 运行器上启动需要一些时间,它最多会等待两分钟,直到设备出现;--wait <seconds>可更改等待时间。Ctrl-C 会停止流程,并仍然完成视频录制。iPhone 需要应用正在运行:通过 MCP 或 HTTP API 调用run_flow。
examples/flows 中有一个流程和一个可供试用的小应用;.github/workflows/flows.yml 会在 GitHub 免费的 macos-26 运行器上运行这些流程。适用于你的应用的 GitHub Actions 作业:
- name: Boot a simulator
run: |
UDID=$(xcrun simctl create ci "iPhone 17")
xcrun simctl bootstatus "$UDID" -b
echo "UDID=$UDID" >> "$GITHUB_ENV"
- name: Build
run: xcodebuild -scheme MyApp -destination "id=$UDID" -derivedDataPath build build
- name: Run flows
run: |
curl -fsSL -o Mobdev.dmg https://github.com/niklas-schmidt-dev/mobdev/releases/latest/download/Mobdev.dmg
hdiutil attach -nobrowse -mountpoint /tmp/mobdev Mobdev.dmg
/tmp/mobdev/Mobdev.app/Contents/MacOS/Mobdev flow flows/sign-in.json --device "$UDID" --artifacts flow-artifacts
- uses: actions/upload-artifact@v4
if: failure()
with: { name: flow-artifacts, path: flow-artifacts }
流程失败时,上传的构件中会包含 run.mp4,即以失败屏幕作为结尾的运行视频,以及 failure.png 和活动日志。使用 if: always() 也可保留通过运行的视频。
在 CI 中,优先使用 tap_element 和 wait_for_element,而不是 OCR 工具:Vision 文本识别在 GitHub 的虚拟 Mac 上不会返回结果,Mobdev 会在 90 秒后放弃等待。
Maestro 流程
Mobdev flow、run_flow(path)、Run Flow… 和测试项目(tests/*.yaml)也支持 Maestro 流程。Mobdev 加载文件时会将 YAML 转换为上述步骤;无法执行的命令会导致加载失败,并指出命令名称和行号,因此流程不会只成功执行一部分。转换过程中发生的变化(省略了屏幕截图、将模式识别为文本)会以备注形式打印。Mobdev convert 会以另一种格式打印流程:
Mobdev convert login.yaml > login.json # Maestro to Mobdev
Mobdev convert login.json > login.yaml # Mobdev to Maestro
| Maestro | Mobdev |
|---|---|
配置 appId、name、env、onFlowStart | 命令所在的应用(未命名)、流程名称、一个 set 步骤、开头的步骤 |
launchApp(appId、clearState、clearKeychain、stopApp、arguments、permissions) | reset_app(keychain)、set_permission、launch_app(restart;arguments 格式为 -key value) |
stopApp、killApp、clearState | stop_app、stop_app、reset_app |
tapOn、doubleTapOn、longPressOn,使用文本、id、index 或 point | tap_element(double、hold: 1);指定点使用 tap 或 long_press;tapOn 的 repeat 对应一个重复步骤 |
inputText、eraseText、pasteText | type_text;使用 press_key 按退格键,并设置 count(默认值为 50,最多 100);type_text ${COPIED_TEXT} |
pressKey | press_key(Enter、Backspace、Tab、Volume Up 和 Down、Remote Dpad 键);Home 对应下文的 home,Back 对应下文的 back |
back | {"if": {"platform": "android", "then": [{"press_key": {"key": "escape"}}], "else": [{"tap_element": {"id": "BackButton"}}]}}:Android 的 Back 键;iOS 没有系统级 Back 键,escape 在 iOS 上也不会返回,因此在 iOS 上会点击导航栏的返回按钮,其标识符为 BackButton |
assertVisible、assertNotVisible | wait_for_element,设置 timeout 为 7(以及 gone) |
extendedWaitUntil(visible、notVisible、timeout) | wait_for_element,超时时间以秒为单位,最多 60 |
scrollUntilVisible(element、direction、timeout) | scroll_until_visible,超时时间每 2 秒滚动一次 |
scroll、swipe(direction,或 start 和 end、duration) | 向下滚动使用 scroll,在屏幕的相应部分之间滑动使用 swipe |
openLink、waitForAnimationToEnd、setLocation、travel、setOrientation、setPermissions | open_url、wait_for_idle、set_location(travel 转为路线,km/h 转为 m/s)、set_orientation、set_permission |
runFlow(文件或 commands;when 使用 visible、notVisible、platform;env) | run,或将命令就地执行;when 转为 if,env 转为 set |
repeat(times、while)、retry(maxRetries、commands 或 file) | repeat(times,或使用 times 作为 max 的 while_visible/until_visible)、retry |
copyTextFrom、${maestro.copiedText} | extract 到 COPIED_TEXT、${COPIED_TEXT} |
optional、label | "optional": true;省略标签 |
hideKeyboard、takeScreenshot | 省略并附注:Mobdev 在 iOS 上使用硬件键盘输入,并保留运行过程的视频 |
限制:
- Mobdev 不运行 JavaScript:
evalScript、runScript、assertTrue、when: true和变量名以外的${…}表达式都会失败;AI 命令、inputRandom…、飞行模式、addMedia、startRecording、单独使用的clearKeychain以及onFlowComplete也会失败,后者会在失败后运行。 - 选择器可以是文本、
id、index和point。相对选择器(below、childOf等)和状态(enabled、checked等)会失败。Maestro 将文本作为正则表达式与整个标签匹配;Mobdev 则查找文本本身,并优先精确匹配,因此锚点、开头或结尾的.*以及转义字符都会被移除;保留下来的模式会附注说明。 - 百分比坐标会原样保留。像素坐标会按 Mobdev 截图中的像素读取(长边为 1280),与 Maestro 的截图不同,并会附注说明。
- YAML 可以使用块和流式映射及列表、带引号和纯文本、注释、
---以及|或>块。锚点、别名、标签和跨多行的纯文本会失败,并标明行号。 将 Mobdev flow 转换为 Maestro 时,会写入每个具有 Maestro 命令的步骤;其他步骤(observe、不在 flow 或if开头的set、带else的可见性if、提取到 COPIED_TEXT 以外名称的extract、带修饰键的按键等)会一并转换失败,并显示对应编号。7 秒等待会转换为assertVisible,launch_app前的reset_app会转换为带clearState的launchApp,而运行 .json flow 时,会显示转换生成的 .yaml 文件名。
测试
项目是仓库中的一个文件夹,包含某个应用的测试:mobdev.json 指定应用,而 tests/ 中的每个文件都是一个测试,可以是带有名称、描述和运行平台的流程,也可以是 Maestro 流程(.yaml,其名称取自配置中的 name)。run 步骤会在测试旁边或相对于项目文件夹的位置查找子流程,因此共享流程可以放在例如 flows/ 这样的目录中。
{
"name": "My App",
"app": {
"bundle_id": "com.example.MyApp",
"builds": {"simulator": "build/Build/Products/Debug-iphonesimulator/MyApp.app", "android": "app/build/outputs/apk/debug/app-debug.apk"}
},
"before_each": [{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}}],
"variables": {"EMAIL": "[email protected]"},
"secrets": ["PASSWORD"]
}
{
"name": "Sign in",
"description": "A member signs in and sees the welcome screen.",
"steps": [
{"tap_element": {"id": "email"}},
{"type_text": {"text": "${EMAIL}", "submit": true}},
{"tap_element": {"id": "password"}},
{"type_text": {"text": "${PASSWORD}", "submit": true}},
{"wait_for_element": {"text": "Welcome"}}
]
}
mobdev.json 中的每个键都是可选的。运行时会先为设备的平台(iphone、simulator 或 android)安装一次构建版本,然后运行 before_each 和测试步骤。每个步骤都成功,测试才算通过,因此请以能证明结果的 wait_for_element 或 wait_for_text 结束测试;如果启动的应用在测试期间崩溃,测试会以该崩溃为失败结果。${NAME} 会从文件、环境或本次运行中获取变量;密钥的值只能来自环境或本次运行,且绝不会出现在结果中。"platforms": ["android"] 的测试会在 iOS 上跳过,反之亦然。
-
侧边栏中的项目 Tests 会在设备上运行测试,并显示每项结果及其步骤、失败时的屏幕截图和运行视频。项目的
output/runs中会保留最新的 20 次运行。 -
Agent 也可以使用相同功能:
list_tests会显示项目,save_test会写入测试(如果project中指定的文件夹尚无项目,也会创建项目),run_tests会运行测试并返回每项结果及首次失败时的屏幕,test_result会再次返回最新结果。未指定project时,它们会使用 Agent 的项目或当前活动项目。因此,Agent 会使用上述工具驱动应用,直到某条路径运行成功;然后将起关键作用的调用保存为测试,运行测试,并根据失败步骤进行修复。 -
Mobdev test无需打开应用即可运行项目,适用于脚本和 CI,可在已启动的模拟器和 Android 设备上运行:/Applications/Mobdev.app/Contents/MacOS/Mobdev test mobdev/ --device <udid> --artifacts test-artifacts每项测试会输出一行;所有测试都通过时退出码为 0,有测试失败时为 1,测试无法启动时为 2。
--artifacts会为每项测试在其独立文件夹中生成results.json、junit.xml、视频,以及失败后的屏幕截图(和失败检查的文件,例如屏幕截图差异),还会包含活动日志和已复制的崩溃报告。失败的测试还会记录应用在测试期间输出的内容以及应用的结束状态。--test <name>运行一项测试(可重复使用),--var NAME=value设置变量,--no-video和--wait的用法与Mobdev flow相同。iPhone 需要应用正在运行:请通过 MCP 或 HTTP API 调用run_tests。 -
多个设备和语言。 重复使用
--device可同时在多个已启动的设备上运行;添加--simulator "iPhone 17"(或"iPhone 17,<runtime id>")可创建并启动该类型的模拟器供本次运行使用,之后再将其删除。--emulator Pixel_9_Pro会以只读模式启动该 Android 虚拟设备,供本次运行使用,之后将其关闭;可重复使用此参数,也可对同一 AVD 重复使用,以同时启动多个实例。只读意味着运行所做的任何更改都不会保存到 AVD;每个运行中的 AVD 实例都必须为只读,因此请先关闭之前以非只读模式启动的实例。--language de-DE会以该语言运行测试:模拟器会使用模拟器的语言;Android 会使用应用的语言(Android 13 及更高版本,且需要app.bundle_id)。重复指定时,每台设备会依次运行每种语言。运行会停止正在运行的 iOS 应用,使其以新语言启动;并将语言作为${LANGUAGE}传递给步骤,之后恢复设备原来的语言。每台设备和每种语言都会有独立文件夹,顶部的summary.md会汇总所有内容。run_tests也接受language。Mobdev test mobdev/ --simulator "iPhone 17" --simulator "iPhone SE (3rd generation)" \ --emulator Pixel_9_Pro --language en-US --language de-DE --artifacts test-artifacts
examples/tests 是示例应用的项目;.github/workflows/flows.yml 会在 GitHub 的 macos-26 运行器上与流程一起运行该项目。你应用的作业可参考上面的示例,将 Mobdev flow 替换为 Mobdev test,并在构件中包含 junit.xml。
检查
三个工具会检查屏幕,并在检查未通过时使步骤失败,因此测试可以检查屏幕的显示效果,而不仅是屏幕上的内容:
{
"name": "Home looks right",
"steps": [
{"wait_for_element": {"id": "home.title"}},
{"assert_screenshot": {"name": "home", "mask": ["home.clock"]}},
{"accessibility_audit": {"fail_on": "error"}},
{"assert_with_ai": {"question": "Is any text cut off or drawn over other text?", "expect": "no"}}
]
}
assert_screenshot 将屏幕与基准图片进行比较(视觉回归)。
name对应baselines/<ios|android>/<width>x<height>/<name>.png:测试运行期间(run_tests、Tests 窗口、Mobdev test)在项目中;对于流程文件(使用path的run_flow、Mobdev flow),在其所属项目中,或在项目外时与文件位于同一目录;其他情况下,在 Agent 的项目或当前项目中。对于不带应用的Mobdev assert_screenshot,如果当前文件夹属于某个项目,则基准图片位于该项目中,否则位于文件夹本身;如果 Mobdev 所在文件夹不属于任何项目,则位于~/Library/Application Support/dev.mobdev.mac/baselines。 尺寸是设备屏幕的像素尺寸,因此每种机型和方向都有自己的基准图片。path则指定 PNG 文件;同等尺寸的全分辨率屏幕截图也可以使用。- 如果没有基准图片,工具会等待屏幕稳定,记录当前屏幕,然后通过并说明这一点。请将
baselines文件夹与测试一并提交。 CI 运行时如果没有基准图片,会记录当前屏幕并通过,因此请在可信的运行中记录基准图片,将其提交,并在预期变更后设置update: true来替换基准图片。 - 比较会在屏幕截图尺寸下进行(长边为 1280 px)。如果某个像素的颜色差异超过约 10%(按 pixelmatch 的测量方式,以 YIQ 计算),且该差异不是基准图片偏移最多一个像素造成的(抗锯齿、缩放),并且至少有三个相邻像素也发生变化(压缩噪点、单像素线条),该像素就会被视为变化。当变化像素超过比较像素的
threshold时,步骤将失败:默认值为 0.0005,即 0.05%,约等于 20×20 px 的方块;设置为 0 则不允许任何变化。文字发生变化或按钮移动几个点都会导致失败;重新绘制相同的屏幕则不会。失败前,工具会等待屏幕稳定,然后再比较一次。 - 除非
compare_status_bar为 true,否则会排除状态栏(顶部 7%)。mask可以排除更多区域:使用屏幕截图像素表示的[x, y, width, height],或元素的 id 或标签(没有 UI 树时识别到的文本),例如时钟、头像或地图。 - 失败时会生成
<name>-diff.png(变化像素为红色、容许的差异为黄色、屏蔽区域为蓝色,并在每个变化区域周围框出方框)和<name>-actual.png:运行期间,文件会写入测试所在的文件夹,results.json会将它们列为该测试的files;运行之外时,文件会写入项目的output/checks,以避免将它们提交,或在没有项目时写入基准图片旁边。工具会返回差异图和变化区域;之后通过时会再次删除这些文件。
accessibility_audit 会根据 UI 树检查屏幕(模拟器、Android、搭载 Mobdev Runner 的 iPhone):
| 规则 | 错误 | 警告 |
|---|---|---|
missing_label | 可供点击但没有标签且内部没有文本的内容,例如仅含图标的按钮 | 没有标签或占位符的字段 |
small_target | 小于 24×24 pt 或 dp,低于 WCAG 2.2 的最低要求 | 小于 44×44 pt(iOS)或 48×48 dp(Android) |
low_contrast | 文本对比度低于 3:1 | 文本对比度低于 4.5:1,即 WCAG AA 对普通文本的要求 |
duplicate_label | 标签相同的不同目标 | |
unclear_label | 文件名,例如 ic_close.png | 标识符,例如 close_button、btnBack 或 chevron.right,或仅为“button” |
- 对比度根据屏幕截图中每段文本和按钮内部的颜色测量:最常见的颜色是背景色,与其差异最大且仍覆盖一小部分区域的颜色是文本颜色(或按钮的图标颜色)。禁用的元素和照片等繁杂背景不纳入计算。
- 尺寸采用屏幕比例:Android 上使用
wm density;在 iOS 上,宽度至少为 1000 px 的 iPhone 按每点 3 个像素计算,iPad 和较旧的 iPhone 按每点 2 个像素计算。 - 问题按从上到下的顺序列出,每个元素的问题会放在一起。
image: true会返回带有元素编号框的截图;有错误的元素框为红色。fail_on: "error"(或"warning")会在出现相应问题时使调用失败,这正是测试所需的行为;ignore会跳过规则。 - Apple 和 Google 的标准控件不一定总能达到 4.5:1 的对比度或 44 pt 的尺寸(iOS 的蓝色文字在白色背景上的对比度约为 3.5:1),因此请将警告视为建议,并在出现错误时使调用失败。
assert_with_ai 会向 Mac 上的 Apple Intelligence 提出一个关于屏幕的是非问题;如果答案不是 expect(默认为 yes),或模型无法确定,就会失败;结果中会附带原因。在 macOS 27 上,模型会查看屏幕截图;在不获取图像的 macOS 26 上,它会读取屏幕元素并说明这一点。任何内容都不会离开 Mac。此功能需要启用了 Apple Intelligence 且已下载其模型的 Mac,否则会说明缺少什么。一次调用需要几秒钟。本地设备上的模型较小:每次只询问一个可见对象;对于可通过元素树判断的内容,优先使用 wait_for_element。
录制的流程会保留这些检查;accessibility_audit 只有在设置了 fail_on 时才有用,因为未设置时,该调用只会进行检查。
HTTP API
所有内容都监听 127.0.0.1:4686(可通过 MOBDEV_PORT 覆盖),并且需要
Authorization: Bearer <token>。令牌位于 ~/Library/Application Support/dev.mobdev.mac/token。
TOKEN=$(cat ~/Library/Application\ Support/dev.mobdev.mac/token)
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4686/v1/status
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4686/v1/screenshot -o screen.jpg # ?format=png, ?full=1
curl -H "Authorization: Bearer $TOKEN" -X POST http://127.0.0.1:4686/v1/tools/tap_text -d '{"text":"Settings"}'
curl -H "Authorization: Bearer $TOKEN" -X POST http://127.0.0.1:4686/v1/tools/type_text -d '{"text":"hello","submit":true}'
POST /v1/tools/<name> 接收工具的参数,并返回 {"ok", "text", "data"};传入
"screenshot": true 可将截图一并包含在响应中。GET /v1/tools 会列出工具及其架构。
/v1/uploads 用于接收供 install_app 使用的构建版本(参见从任意位置安装构建版本)。
远程访问(可选)
启用 Remote Access,即可让其他计算机上的 Agent 连接手机。Mac 会与中继服务保持一个向外发起的 WebSocket 连接;Mac 上不会开放任何端口。
- 托管中继: 在 mobdev.sh/dashboard 创建访问令牌,
然后点击 Open in Mobdev。确认后,
mobdev://connect链接会填入中继 URL 和令牌。你也可以在 Remote Access 下粘贴令牌。 - 自有中继: 运行
../relay,然后输入其 URL。
随后,应用会显示远程 MCP URL 和客户端密钥:
claude mcp add --transport http mobdev-remote https://relay.mobdev.sh/h/<mac-name>/mcp \
--header "Authorization: Bearer mdc_…"
中继只转发请求,不会存储任何内容。任何持有客户端密钥的人都能控制手机; New Client Key 会撤销旧密钥;在仪表板中撤销访问令牌则会断开 Mac 的连接。
实时画面
在“远程访问”中启用 允许实时画面 后,中继服务可以将设备屏幕串流到浏览器:在控制面板中,设备旁边的 实时画面 会显示屏幕;点击可轻触,拖动可滑动,输入文字可打字,就像应用的镜像画面一样。点击那里的 共享…,即可为单台设备或整台 Mac 创建链接,任何人都可以打开链接,无需账号;链接可设置为仅查看或同时允许控制,有效期为一小时、一天或一周;也可以在那里撤销链接。使用客户端密钥的 Agent 可通过 /v1/live 打开相同的串流(协议)。
实时画面默认关闭,且需要在每台 Mac 上单独启用:屏幕上可能显示消息、验证码和其他私人数据,因此是否允许他人观看由 Mac 决定。有人观看时,设备页面会显示 正在观看实时画面;“远程访问”会列出观看者身份(控制面板中的你、名称旁标注的共享链接、使用客户端密钥的 Agent),每位观看者旁都有 停止 按钮。每位观看者加入或离开时,都会记录在设备的活动日志中;通过浏览器进行的轻触和打字也会记录为“浏览器”。关闭该设置会立即结束所有实时画面。画面以 JPEG 格式传输,长边为 800 px,每秒最多 10 帧;仅当屏幕发生变化时才发送(否则每 2 秒发送一次);中继服务只在内存中转发画面。
从任何位置安装构建产物
云端 Agent(网页端 Claude Code、Codex cloud、Cursor 后台 Agent)或 CI 可在自己的容器中构建应用,通过中继服务将构建产物上传到 Mac,再通过 install_app {"upload": "<id>"} 将其安装到 Mac 上的设备。中继服务像转发其他请求一样转发数据块,不保留任何内容;构建产物会存放在 Mobdev 文件夹的 uploads/ 中,只有你可以读取,并会在最后一次使用 24 小时后删除。
scripts/mobdev-upload.sh 只需要 shell、curl 和 sha256sum 或 shasum,因此无需 Mobdev 即可在 Linux 容器中运行:
curl -fsSLO https://raw.githubusercontent.com/niklas-schmidt-dev/mobdev/main/scripts/mobdev-upload.sh
export MOBDEV_URL=https://relay.mobdev.sh/h/<mac-name> MOBDEV_KEY=mdc_…
id=$(sh mobdev-upload.sh app/build/outputs/apk/debug/app-debug.apk)
curl -sS -H "Authorization: Bearer $MOBDEV_KEY" -H 'Content-Type: application/json' \
-d "{\"upload\": \"$id\", \"device\": \"emulator-5554\"}" "$MOBDEV_URL/v1/tools/install_app"
通过 MCP 连接的 Agent 可使用 {"upload": "<id>"} 调用 install_app,而不是使用 curl。在 Mac 上,Mobdev upload <file> --url https://relay.mobdev.sh/h/<mac-name>(密钥通过 MOBDEV_KEY 或 --key 指定)可完成相同操作;不带 --url 时,会将构建产物发送到这台 Mac 上的 Mobdev 应用。两种方式都会先将 .app 文件夹压缩成 zip,再以最多 8 MiB 的数据块发送;如果数据块发送失败,会从 Mac 上已有副本的末尾位置重新发送,并输出上传 ID(--json:已完成的上传)。
iPhone 使用 .ipa 或面向设备构建的 .app;模拟器使用面向模拟器构建的 .app;Android 使用 .apk。.app 会作为 zip 传输,且应用位于压缩包顶层;ditto -c -k --keepParent MyApp.app MyApp.zip 可生成这种压缩包。install_app 对上传文件的检查方式与对 path 的检查方式相同。以下协议适用于中继服务以及使用 API 令牌的本地 API:
| 请求 | |
|---|---|
POST /v1/uploads {"name", "size", "sha256"} | 201 {"id", "chunk_size", "received", …}。name 以 .ipa、.apk 或 .zip 结尾;sha256 为可选项 |
PUT /v1/uploads/<id>?offset=<n>,并附带字节数据 | 大小不得超过 chunk_size(8 MiB)。offset 必须与 Mac 上已有数据相符,否则返回 409 和 received |
GET /v1/uploads/<id> | 返回 received 和 state(receiving、finishing、finished),用于恢复上传 |
POST /v1/uploads/<id>/finish | 检查大小和 SHA-256,并解压 zip;返回 {"path", "kind", "sha256"}。可安全重复调用 |
DELETE /v1/uploads/<id> | 删除上传文件 |
如果 zip 包含 ../ 或绝对路径、指向其自身符号链接路径的路径、指向应用外部的符号链接、除顶层一个 .app 以外的任何内容,或解压后超过 4 GB,则会在解压前或解压后立即拒绝该压缩包;setuid 位会被移除。Mobdev 最多保留 20 个上传文件,总大小不超过 4 GB;需要腾出空间时,会删除最长时间未使用的文件,但不会删除过去 10 分钟内使用过的文件。每次上传都会记录在 Mobdev 日志中(Console,子系统 dev.mobdev.mac);安装操作会记录在设备的活动日志中。
安全与隐私
- API 仅绑定到 127.0.0.1,需要 bearer 令牌,并会拒绝浏览器请求(
Origin) 和外部Host标头。 - 令牌和中继密钥存储在
~/Library/Application Support/dev.mobdev.mac中的文件里,只有你能读取(mode 0600)。MOBDEV_HOME可更改该目录。 - 不收集遥测数据,也无需账户。每项 Agent 操作都会显示在应用的 Activity 下。
- 浏览器中的实时视图默认关闭;只有在 Remote Access 下启用 Allow live view 后才会开启。有人观看时,应用会显示提示,你也可以停止实时视图(参见实时视图)。
- Agent 会使用你的账户在你的真实手机上操作。对于任何发送消息、付款或删除内容的操作,请确保有人监督。
- 已屏蔽的应用(Settings › Agents,或在
MOBDEV_BLOCKED_APPS中以逗号分隔的名称和 bundle ID,适用于Mobdev flow、test和call):open_app和launch_app会拒绝打开这些应用,流程和测试中也一样。名称还会匹配包含该名称的 bundle ID。这能避免误操作,但不是沙盒:Agent 仍可点击应用图标。
限制
- 一台 Mac 上的多部 iPhone 各自使用独立的 Bluetooth 连接:先按名称匹配,再按机型匹配;如果仍无法区分(两部相同机型的手机都自称为“iPhone”),则移动各连接的指针并观察哪个屏幕上显示指针,这需要 AssistiveTouch。目前尚未尝试同时连接两部 iPhone。
- 目前已在硬件上验证:通过 USB 从 iPhone 截取屏幕、Bluetooth 配对、API、OCR、MCP、中继、轻点、滑动、使用 AssistiveTouch 滚动,以及使用德语键盘布局输入(字母、变音符、ß 和符号),运行于 iOS 27。
- 输入支持美国和德国硬件键盘布局,包括常见的重音符号。Emoji 和其他没有对应按键的字符需要开启 UI 树:Mobdev Runner 会输入这些字符。
- 已测试的情形为竖屏。坐标会根据当前屏幕截图尺寸确定。
- 手机必须保持解锁状态。Mobdev 无法输入密码。
- 模拟器输入使用 Xcode 的私有 SimulatorKit;已在 Xcode 27 和 26.6 上测试。较新的 Xcode 可能会更改此功能,idb 和 AXe 也曾遇到这种情况。模拟器屏幕仅显示主显示屏。
- 在 Xcode 27 之前,
devicectl不识别模拟器,因此模拟器应用会通过simctl打开。随后 iOS 会在open_url打开应用自身的 URL 方案前询问“是否在……中打开?”;如果存在 UI 树,open_url会自行点击“打开”按钮,因此流程或测试无需为此添加步骤。 - Android 通过粘贴输入非可打印 ASCII 以外的文本,这会替换剪贴板,并且需要使用支持粘贴的输入框(Android 7 及更高版本)。不使用 scrcpy 时,只能输入 ASCII。adb 无法识别应用名称,因此
open_app会匹配包名。 - scrcpy 的服务器固定为 3.3.4,Mobdev 使用其协议;已在 Android 16 模拟器上测试。对于制造商禁止通过 USB 调试注入输入的手机(小米的“USB 调试(安全设置)”),使用 scrcpy 和
input一样需要开启该开关。服务器无法执行的触摸操作(例如屏幕旋转期间的操作)会被丢弃,不会报错。 ui_tree通过 macOS 的私有辅助功能转换读取模拟器;已在 macOS 27 上使用 Xcode 27 测试,也在 GitHub 的 macOS 26 运行器上使用 Xcode 26.6 测试,示例流程会在每次变更时运行。在这些运行器上,模拟器显示内容可能比输入的文本晚数秒,期间也无法读取其应用;tap_element和wait_for_element会在各自超时后最多再等待 15 秒。- 录制不会捕获滚动轮;录制时请拖动以滚动。
- 在 Android 上,双击(
double)会通过 adb 调用两次input tap,这可能比应用的双击超时时间更长。 - Mobdev Runner 通过 XCTest 的私有 API 查找前台应用,方式与 WebDriverAgent 相同;已在 Xcode 27 的模拟器上测试。其 frame 假定为竖屏。它由你的 Xcode 构建,因此新版 Xcode 可能会导致其无法运行,直到 Mobdev 完成适配。
开发
swift test # unit, HTTP, MCP, OCR and relay end-to-end tests (needs Go for the last)
scripts/build-app.sh # build/Mobdev Dev.app, the separate development app
视觉文本识别在 GitHub 的虚拟化 macOS 运行器上永远不会返回结果,因此设置了 CI 时会跳过 OCR 测试,只有在真实 Mac 上才会运行。
开发者工具使用脚本模拟的 devicectl 响应进行测试。选择加入的测试会使用真实设备或模拟器运行这些工具(Xcode 27 的 devicectl 可驱动两者):
# Installs the app (it should print after launch), launches it and uninstalls it.
MOBDEV_TEST_DEVICE=<udid> MOBDEV_TEST_APP=/path/to/App.app swift test --filter DeveloperIntegration
# Launches an installed developer app, reads its logs, stops it, reads crash reports. Installs nothing.
MOBDEV_TEST_DEVICE=<udid> MOBDEV_TEST_BUNDLE_ID=<bundle id> swift test --filter DeveloperIntegration
# Captures the requests of an installed app that makes some within seconds of its launch.
MOBDEV_TEST_DEVICE=<udid> MOBDEV_TEST_NETWORK_APP=<bundle id> swift test --filter DeveloperIntegration
网络捕获使用代理进行测试,测试进程中的客户端和服务端运行在 127.0.0.1 上,使用会保留 http_proxy 的伪造 adb,并记录 iOS 27 模拟器上的 CFNetwork 控制台输出。MOBDEV_TEST_INTERNET=1 swift test --filter HTTPProxyTests 还会通过代理使用 HTTP 和 HTTPS 将 curl 请求发送到 example.com。
模拟器和 Android 使用伪造设备及由真实 adb 输出提供数据的解析器进行测试。选择加入的 EmulatorIntegration 测试会通过所有工具驱动已启动的模拟器和正在运行的模拟器。模拟器测试使用 ../examples/flows/fixture 中的夹具应用:它会将每次点击输出为 fixture: tap <x> <y> fraction <fx> <fy>,回显输入的文本,并在使用参数 crash 启动时崩溃。使用其 build.sh 构建:
../examples/flows/fixture/build.sh
MOBDEV_TEST_SIMULATOR=<udid> MOBDEV_TEST_SIMULATOR_APP=../examples/flows/fixture/MobdevFixture.app swift test --filter EmulatorIntegration
MOBDEV_TEST_ANDROID=emulator-5554 MOBDEV_TEST_ANDROID_APK=/path/to/any.apk swift test --filter EmulatorIntegration
Android 测试使用 am crash 让 Settings 应用崩溃,并安装、移除和重新安装 .apk;使用 -read-only 运行模拟器即可撤销这些更改。androidThroughScrcpy 会在滑动操作移动 Settings 时测量串流(期望每秒新增图片超过 10 张),在 Settings 的搜索框中输入“Grüße 👋”并从 UI 树中读回内容,设置并读取剪贴板,终止服务端并等待下一个服务端启动,还会检查设备上是否没有遗留内容。scrcpy 的服务端不在 git 中:scripts/build-app.sh 会将固定版本的发行包下载到应用的 Resources 中(并对照其 SHA-256 校验,旁边附有 scrcpy 的许可证),从 swift build 运行的构建会在首次使用时将其下载到 .build/scrcpy-server-v<version>。其协议会逐字节对照 scrcpy 自带的测试进行测试,解码器使用这台 Mac 编码的图片进行测试,会话则使用伪造服务端和伪造 adb 进行测试。
Mobdev Runner 使用伪造程序提供的 usbmuxd 和 runner 响应进行测试。选择加入的 RunnerIntegration 测试会为模拟器构建它(无需签名;runner 在 Mac 的 127.0.0.1 上监听),通过 /tree、/tap 和 /type,再通过 ui_tree、tap_element 和 wait_for_element 操作 Settings,并检查停止操作是否会结束它。两项只读检查有助于在 iPhone 上进行验证:一项向 usbmuxd 请求设备列表,另一项通过 usbmuxd 读取手动启动的 runner 的树,启动命令为 TEST_RUNNER_MOBDEV_RUNNER_TOKEN=<token> xcodebuild test-without-building …:
UDID=$(xcrun simctl create "Mobdev Runner Test" "iPhone 17")
MOBDEV_TEST_RUNNER_SIMULATOR=$UDID swift test --filter RunnerIntegration
xcrun simctl delete "$UDID"
MOBDEV_TEST_USBMUXD=<iPhone udid> swift test --filter usbmuxdListsTheConnectedIPhone
MOBDEV_TEST_RUNNER_DEVICE=<iPhone udid> MOBDEV_TEST_RUNNER_TOKEN=<token> swift test --filter runnerOnAnIPhoneAnswersOverUSB
实时视图使用伪造手机进行测试,既单独通过 LiveStreamsTests 测试,也通过 Go 中继使用 RelayEndToEndTests 测试。若要在浏览器中查看,选择加入的 LiveHarness 测试会让这台 Mac 的一端与中继运行一段时间,模拟应用启用 Allow live view 时的行为:使用模拟器时会串流该模拟器,并可安装和打开夹具应用;没有模拟器时则使用伪造手机。它会将观看者的操作以及夹具最后的点击写入 MOBDEV_LIVE_LOG:
MOBDEV_LIVE_RELAY=http://127.0.0.1:8787 MOBDEV_LIVE_SECRET=mdh_… MOBDEV_LIVE_TOKEN=mda_… \
MOBDEV_LIVE_SIMULATOR=<udid> MOBDEV_LIVE_APP=../examples/flows/fixture/MobdevFixture.app \
MOBDEV_LIVE_SECONDS=300 MOBDEV_LIVE_LOG=/tmp/live.log swift test --filter LiveHarness
MobdevCore 包含所有可测试的内容:HID 报告和手势(HID/)、屏幕捕获和文本识别(Capture/)、工具(Phone/)、用于开发者工具的 devicectl(Developer/)、模拟器和 Android(Emulators/)、构建 Mobdev Runner 并通过 usbmuxd 访问它(Runner/;其 Xcode 项目位于 Sources/ 旁边的 Runner/)、HTTP、MCP 和 stdio 桥接(Server/)以及中继客户端(Relay/)。Mobdev 目标是 SwiftUI 应用:包含 Liquid Glass 控件的 NavigationSplitView、用于设置的检查器、用于活动记录的 Table,以及一个 Settings 场景。测试使用可渲染真实文本的虚拟手机,因此无需硬件即可测试 OCR、tap_text 和坐标。scripts/make-icon.swift 会在 macOS 26 图标网格上打包来自 ../assets/branding/ 的 Imagegen 图片;--dev 用于选择琥珀色开发版本。请参阅 ../assets/branding/README.md 以重新生成所有品牌素材导出文件。
致谢
这个创意源自 TapKit。iphone-use(MIT)和 sryo/clak 记录了蓝牙 LE HID 方案(长格式 1812 UUID、加密的报告属性、Report Reference 描述符、用于处理过期缓存的 Service Changed,以及用于 AssistiveTouch 的绝对指针)。这是独立实现;与 TapKit 或 MobAI 无关联。Android 的屏幕和输入通过 Genymobile 和 Romain Vimont 的 scrcpy 服务端实现(Apache-2.0);应用附带了未经修改的该服务端及其许可证。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。