## 为什么要这一层

镜像窗口对 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」)。

## 前置条件

- Mac:Xcode(命令行套件),并在 Xcode → Settings → Accounts 登录 Apple ID(免费 Personal Team 即可)。
- iPhone:开启开发者模式(设置 → 隐私与安全性 → 开发者模式 → 开 → 重启)。
- 中继工具:`brew install socat`(Wi-Fi)或 `brew install libimobiledevice`(USB 的 iproxy)。

## 手动跑(理解原理用)

    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/screenshot` | L3 → 无窗口时回退 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

更正一个曾经的误判: 一度以为「设备 iOS 大版本必须 ≤ Xcode 的 DDI」是硬性卡点(iOS 27 设备 + Xcode 26.5)。真机验证推翻了它 —— Xcode 26.5(DDI build 17F42 = iOS 26.5)成功把 WDA 装上了 iOS 27.0 的设备并尝试启动。所以「版本差一截」往往能用,真正卡住的是解锁/连接(本坑)和证书信任(见「手机上要信任开发者证书」一节)。只有当 DDI 比设备老很多、确实拒绝挂载时才需要升 Xcode。

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

编译/签名/装机都成功后,启动 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 -showdestinations` 或 `devicectl 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(镜像实时视频)是二选一的模式,不是叠加层:

- 镜像模式(给人看):实时视频 + L3 像素输入。先 `setup-wda.sh stop`,再在镜像窗口点 Try Again。
- Agent 模式(给 agent 用):元素树、中文直通、手机端原生截图;此时网页端自动切到 静帧兜底(每 2.5s 一帧 WDA 抓屏,人仍能看到手机在干嘛),镜像窗口显示 Interrupted 属正常现象。

切换方式(推荐用 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)的核心优势,均已实测:

- `GET /source` — 元素树直接读到中文 UI(设置项、日期标签)。
- 按元素 label 定位 + 点击 — 无坐标漂移、不碰 Mac 光标(用它驱动设置把自动锁定设成「永不」,人没动电脑)。
- `POST /wda/keys` 送字符串 — 中文一次到位:在备忘录里干净打出整段中文,而同机 L3 像素路径打出的是 `我爱上…AI阿根廷鸥鸟啊` 这种被拼音 IME 吃 keycode 的乱码。WDA 直接把 Unicode 送进文本框,绕开键盘/IME。

这一层不取代镜像。 镜像 + WebRTC 仍是人类远程桌面、视觉兜底、以及 WDA 覆盖不到时的保险。 agent 的默认路径优先 L1 动词 → L2 元素 → L3 像素兜底。
定位: L2 需要用户装 Xcode 自己编 WDA,与「一行 curl 安装」冲突 —— 所以 L3(像素,零 setup)是默认,L2(WDA)是高级/power-user 路径。