WebDriverAgent 元素树控制 — 接入指南与踩坑

iphone-use 的 L2 元素层:通过 iOS 自己的辅助功能树按「元素」操作 App, 事件由手机自己合成 —— 不抢 Mac 光标、不靠视觉猜坐标、中文输入直通。这是对镜像+CGEvent(L3 像素层)的升级, 不是替代。本页记录在真机上验证出的全部坑。

为什么要这一层

镜像窗口对 macOS 来说只是一块视频,iOS 的按钮不在 Mac 的辅助功能树里,所以现状(L3)只能看图猜坐标 + 用 Mac 的真实光标代点:慢、随布局/语言/分辨率漂移、且和正在用电脑的人抢同一颗光标。WebDriverAgent(WDA,Appium 的底座)作为一个测试 runner 跑在手机上,能直接拿到 iOS 的元素树并由手机自己合成事件。

通道本质短板
L1 动词Shortcuts / App Intents调 App 暴露的类型化动作,最快最确定只覆盖 App 愿意暴露的
L2 元素WebDriverAgent读 iOS 元素树,按元素点击,手机自己合成事件需 Xcode 签名装 runner
L3 像素镜像 + CGEvent看图猜坐标,Mac 光标代点 —— 万能兜底慢、脆、抢光标

一键安装(推荐)

# 前置:Xcode 已装且登录了 Apple ID(Settings → Accounts);
#       手机已配对、开发者模式已开;brew install socat(或 libimobiledevice)
./scripts/setup-wda.sh           # 编译装机 → 守护运行 → localhost 中继 → 配置 daemon
./scripts/setup-wda.sh status    # 看 WDA + 中继状态
./scripts/setup-wda.sh stop      # 停掉

脚本把本页所有坑都编码进去了:等 DDI 挂载会提示你解锁、撞到证书信任会给出指路、自动选 iproxy(USB)或 socat(Wi-Fi)做 localhost 中继、最后自动把 PHONE_REMOTE_WDA_URL 写进 daemon 的 LaunchAgent 并重启。完成后 daemon 的 agent API 自动启用 L2 路由(见下方「统一 API」)。

前置条件

手动跑(理解原理用)

git clone --depth 1 https://github.com/appium/WebDriverAgent.git && cd WebDriverAgent
xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner \
  -destination "platform=iOS,id=<设备UDID>" \
  -allowProvisioningUpdates \
  DEVELOPMENT_TEAM=<你的TeamID> \
  PRODUCT_BUNDLE_IDENTIFIER=com.leeguoo.iphone-use.wda \
  test
# 成功标志:日志出现 ServerURLHere -> http://<phone-ip>:8100/
# 注意 xcodebuild 不能退出 —— 它退了 WDA 就死(setup-wda.sh 帮你守护)。

统一 API:daemon 的自动路由

给 daemon 设 PHONE_REMOTE_WDA_URL=http://127.0.0.1:8100(setup 脚本自动配)后,同一套 agent API 自动选最优路径,调用方零改动:

调用路由说明
POST /agent/input {"type":"text","text":"你好"}L2 → 失败回退 L3中文直通(Unicode 进文本框,不被 IME 吃)
POST /agent/input {"type":"tap","label":"新备忘录"}仅 L2按元素点:无坐标、无漂移、不碰 Mac 光标(L3 无此能力)
POST /agent/input {"type":"tap","x":0.5,"y":0.5}L2 → 失败回退 L3归一化坐标,手机端合成,无需镜像窗口在最前
GET /agent/elements仅 L2扁平元素树 {kind,label,rect,depth} —— 文本,比截图便宜一个量级
GET /agent/screenshotL3 → 无窗口时回退 L2镜像断开(人拿着手机)时仍能看到屏幕
scroll / key / shortcut仅 L3仍走镜像 + CGEvent

GET /agent/status 返回 {"ok","phone_target","wda"} —— wda:true 表示 L2 实时可用。

踩坑清单(真机验证)

① 让 DDI 挂载:手机必须解锁 + 保持连接 真·头号坑

开发服务起不来时报 "Developer Disk Image is not mounted… Ensure that the device is unlocked",devicectl 显示 ddiServicesAvailable: false。**最常见的真实原因是手机锁屏 / 刚连上还没握手完**,不是版本问题。修复:**解锁手机、保持亮屏不锁、保持连接(USB 最稳)**,等 ddiServicesAvailable 翻成 true 再 build。可以轮询等:

until xcrun devicectl device info details --device <UUID> \
  | grep -q "ddiServicesAvailable: true"; do sleep 4; done

② 手机上要信任开发者证书 必撞

编译/签名/装机都成功后,启动 runner 报 "The application could not be launched because the Developer App Certificate is not trusted."(或更隐晦的 "Lost pending connection to the test runner before launch")。这不是构建失败,是 iOS 拒绝启动未受信任的开发者 app。

修复:手机上 设置 → 通用 → VPN 与设备管理 → 「开发者 App」下选你的 Apple Development 证书 → 信任注意找的是「Apple Development: <Apple ID>」证书,不是 app 名(app 显示为 WebDriverAgentRunner)。 换 bundle id 重装后要重新信任一次。

③ 两套设备 ID,别搞混

xcrun devicectl list devices 显示的是 CoreDevice UUID(如 96ECE27A-…);xcodebuild -destination "id=…" 要的是老式 UDID(如 00008150-000A…)。用错直接报 "Unable to find a device matching the provided destination specifier"。查正确 UDID:xcodebuild -showdestinationsdevicectl device info details 里的 udid 字段。

④ 未登录开发团队 → 签名失败

新机器 security find-identity -p codesigning 返回 0,xcodebuild 报 "Signing for WebDriverAgentRunner requires a development team"。修复:Xcode → Settings → Accounts 登录 Apple ID;证书首次编译用 -allowProvisioningUpdates 自动生成。

⑤ 不能用 PRODUCT_NAME 改显示名(会搞坏构建)

想把手机上的 app 名从 WebDriverAgentRunner 换成自己的品牌,千万别在命令行传 PRODUCT_NAME=… —— 它是全局覆盖,会把 WDA 的内部框架 WebDriverAgentLib 也改名,导致它自身的 #import <WebDriverAgentLib/FBElement.h> 全部找不到、编译失败。安全的只有 PRODUCT_BUNDLE_IDENTIFIER(全局覆盖无害)。干净改显示名要改单个 target 的设置(放进 setup 脚本),不能粗暴全局覆盖。

⑥ 免费账号签名 7 天过期

免费 Personal Team 签出来的 WDA 7 天失效,要每周重装;付费开发者账号(¥688/年)签名一年有效。长期部署建议付费账号。

⑦ macOS「本地网络」隐私会静默拦截 daemon 直连手机 必须走 localhost 中继

真机实测:终端里 curl http://<phone-ip>:8100/status 通,但 daemon(后台 LaunchAgent)发同样的请求失败、不弹任何授权框 —— macOS 15+/26 的「本地网络」隐私对后台进程访问私网地址是静默拒绝

解法:让 daemon 访问 127.0.0.1(localhost 豁免本地网络权限),由一个终端上下文的中继进程把流量转给手机:USB 在时用 iproxy 8100 8100 -u <UDID>(最稳,手机换 Wi-Fi 也不断);否则 socat TCP-LISTEN:8100,fork,reuseaddr,bind=127.0.0.1 TCP:<phone-ip>:8100。setup-wda.sh 自动选择。顺带的好处:daemon 配置永远是 http://127.0.0.1:8100,手机 IP 变了只动中继。

注:iproxy 走 usbmuxd,只在 USB(或 usbmuxd 级 Wi-Fi 配对)下可用 —— CoreDevice 的 Wi-Fi 隧道(devicectl 那套)usbmuxd 看不见,那种情况用 socat。

⑧ 跨子网也能直连;手机频繁锁屏挡操作

WDA 监听手机的 0.0.0.0:8100,日志打印 ServerURLHere->http://<phone-ip>:8100<-ServerURLHere。实测手机和 Mac 不在同一 /24 子网也能直连(只要可路由);否则用 iproxy 8100 8100 走 USB 转发。
另:很多操作(启动 app、输入)要求手机解锁,默认「自动锁定 30 秒」会反复打断 —— 把它设成永不。讽刺的是 agent 自己就能用 WDA 设:按标签点 设置 → 显示与亮度 → 自动锁定 → 永不(本项目已这么验证过)。但首次解锁仍需真人(Face ID/密码),WDA 不能解锁。

⑨ WDA 与 iPhone 镜像互斥:runner 一启动,镜像必断 架构级

跑在手机上的 XCUITest runner(WDA)会独占设备的远程会话:它一启动,iPhone 镜像立刻断成 「Connection Interrupted」;它活着,镜像点 Try Again 永远连不回去 —— 哪怕手机已锁屏。 反向同样成立,先连镜像再启 runner,镜像当场被踢掉。真机 A/B 实测(iPhone 17 / iOS 27,2026-06-12): 带着 WDA 点 Try Again → 失败;杀掉 runner 再点 → 秒连;镜像连着时重启 runner → 镜像当场断。

所以 L2(元素树)和 L3(镜像实时视频)是二选一的模式,不是叠加层:

切换方式(推荐用 API,一条龙全自动):POST /agent/mode {"mode":"mirror"}(WDA 锁屏 → 停 runner → 激活镜像 → 自动点 Try Again,约 10 秒)/ {"mode":"agent"}(后台拉起 runner;若手机锁着需人解锁一次,launcher 会等)。命令行等价:scripts/setup-wda.sh / scripts/setup-wda.sh stop。 排障速记:镜像怎么也连不上时,第一反应查 WDA 是否在跑(curl http://127.0.0.1:8100/status)。

已验证(iPhone 17 / iOS 27 真机,2026-06-11)

整条 L2 链路在真机跑通,以下能力是相对镜像+CGEvent(L3)的核心优势,均已实测: