Files
agentic-mobile-control/docs/MACOS_IPHONE_SETUP.md
T
q792602257 25ebc10a8a
Tests / Test passed: 789
feat: downgrade Python baseline to 3.13 for PaddleOCR compatibility
paddlepaddle has no Python 3.14 (cp314) wheel on PyPI, so host-agent
deployments on 3.14 can never install it, causing OCR to fail at
runtime with RuntimeError. Pin the workspace to Python 3.13 across
all pyproject.toml files, the Docker base image, and the Jenkins CI
image; regenerate uv.lock against 3.13.

Also fixes a pre-existing Python-2-style `except X, Y:` syntax error
(invalid in all Python 3.x) in runtime/task.py and
packages/cloud-platform/cloud/{sql_repository,internal_api/api}.py,
introduced in 22d37ca9 and unrelated to this change's scope, which
blocked the full test suite from collecting on any interpreter
version.

openspec change: downgrade-python-3-13-paddleocr
2026-07-14 18:05:49 +08:00

637 lines
24 KiB
Markdown

# macOS 迁移与 iPhone 真机控制手册
本文用于把 Device Agent Runtime 从 Windows 或其他开发机迁移到 macOS,并在
macOS 上通过 Appium + WebDriverAgent(WDA)控制真实 iPhone。
## 1. 当前支持范围
- 当前仓库内置 `wda`(iPhone/iPad 的 XCUITest/WDA)与 `uiautomator2`(Android
的 Appium UiAutomator2)两种 Driver,均在 `driver/registry.py` 注册。
- Android 驱动代码已落地但尚未经过真机验证;完整的 Android SDK/adb/Appium 真机
安装手册留待后续变更补充,本文其余章节仍聚焦 iPhone 真机流程。
- iPhone 真机自动化必须在 macOS 上完成,因为 XCUITest、Xcode 和 WDA 签名依赖
Apple 工具链。
- 当前 Web Console 的设备登记接口不会自动连接设备,REST API 也没有公开的
connect/disconnect endpoint。本文提供的 Runtime 启动命令会在启动 API 前显式
注册并连接 iPhone。
## 2. 迁移前准备
建议迁移源代码和必要配置,不要复制原操作系统生成的虚拟环境或缓存。
需要迁移:
- Git 仓库中的源代码。
- 未提交的必要代码和文档变更。
- 自己维护的 API Key、模型配置等敏感环境变量,使用安全渠道单独迁移。
- 如确实需要保留设备登记和任务历史,可单独备份 `tasks/*.sqlite3`;通常建议在
Mac 上重新登记设备,避免沿用旧 UDID、端口和路径。
不要迁移:
- `.venv/`
- `__pycache__/`
- `.pytest_cache/`
- IDE 缓存
- Windows 专用路径或 PowerShell profile
在 Mac 上优先通过 Git 重新 clone:
```bash
git clone <repository-url> agentic-modile-control
cd agentic-modile-control
git status
```
注意:仓库目录仍叫 `agentic-modile-control`,其中 `modile` 是历史拼写;Python
包名已经是 `device-agent-runtime`。不要在迁移时顺手改目录名或数据库路径,除非
准备单独处理相关脚本和配置。
## 3. 准备 Mac、Xcode 和 iPhone
### 3.1 必要条件
- 一台能运行目标 iOS 对应 Xcode 版本的 Mac。
- 完整版 Xcode,不只是 Command Line Tools。
- Apple ID;真机 WDA 签名需要可用的 Apple Development team。免费 Personal
Team 可用于本地验证,但证书和 provisioning profile 有期限限制。
- 数据线连接的 iPhone。第一次配置建议先使用 USB,稳定后再考虑无线调试。
### 3.2 安装并初始化 Xcode
从 App Store 或 Apple Developer 网站安装 Xcode,至少启动一次,然后执行:
```bash
xcode-select --install
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
xcodebuild -version
xcode-select -p
```
如果 Xcode 不在 `/Applications/Xcode.app`,将路径替换成实际位置。
打开 Xcode,进入 `Settings > Accounts`,登录 Apple ID,并确认能看到用于签名的
Team。
### 3.3 配置 iPhone
1. 使用数据线连接 iPhone,解锁手机并选择“信任此电脑”。
2. 在 Xcode 中打开 `Window > Devices and Simulators`,确认设备可见且没有
“Preparing debugger support”之类的未完成状态。
3. 在 iPhone 中进入 `设置 > 隐私与安全性 > 开发者模式`,启用后按提示重启并
再次确认。
4. 保持设备解锁,记录 UDID。可从 Xcode 的 Devices and Simulators 窗口复制,
也可运行:
```bash
xcrun devicectl list devices
```
后续示例使用以下环境变量。值必须换成自己的信息:
```bash
export DEVICE_UDID="<iPhone UDID>"
export APPLE_TEAM_ID="<Apple Developer Team ID>"
export WDA_BUNDLE_ID="com.<your-name>.WebDriverAgentRunner"
```
`WDA_BUNDLE_ID` 必须在自己的 Team 下唯一,不能原样使用示例值。
## 4. 安装本地工具链
### 4.1 Homebrew、Python 和 Node.js
本项目当前在 `pyproject.toml` 中要求 Python `>=3.13,<3.14`。Appium 3.1 要求 Node.js
`^20.19.0 || ^22.12.0 || >=24.0.0`,并要求 npm `>=10`
已安装 Homebrew 时执行:
```bash
brew update
brew install uv node
uv --version
node --version
npm --version
```
确认 Node.js/npm 满足上面的版本范围。Apple Silicon 和 Intel Mac 都应使用本机
架构的 Homebrew,不要在 Rosetta shell 与原生 shell 之间混用 Python/Node。
### 4.2 同步 Python workspace
在仓库根目录执行:
```bash
uv python install 3.13
uv sync --locked --all-packages
uv run --package device-agent-runtime python --version
uv run --package device-cloud-platform python -c "import cloud"
```
uv 会在仓库根目录管理 `.venv`,共享 `uv.lock` 同时锁定本地 Runtime 和 cloud
platform 两个 Python member。运行完整的非实机测试使用:
```bash
uv run --all-packages pytest -m "not integration"
```
如果 Apple Silicon 上安装 `paddleocr` 或其底层推理依赖失败,可以先安装不带 OCR
的控制链路,用于截图、点击、滑动、输入、启动 App 和读取 UI tree:
```bash
uv pip install -e . --no-deps
uv pip install \
"anthropic>=0.69.0" \
"Appium-Python-Client>=5.1.1" \
"fastapi>=0.115.0" \
"httpx>=0.27.0" \
"mcp>=1.27,<2" \
"openai>=1.0.0" \
"uvicorn[standard]>=0.30.0" \
"pytest>=8.3.0"
```
这种降级安装不包含 OCR。`find_text` 和基于 OCR 的 screen description 可能返回空
结果,但 WDA 基础控制不受影响。
项目已固定使用 Python 3.13(见 `pyproject.toml``requires-python`),原因是
`paddlepaddle` 在 PyPI 上尚未发布 Python 3.14 (cp314) 的 wheel,3.14 环境下无法
安装 `paddlepaddle`,会导致 OCR 相关功能在运行时报 `RuntimeError`。注意
`paddlepaddle` 本身并未作为 `paddleocr` 的声明依赖被 `uv sync` 自动安装,需要
在 3.13 环境下手动执行 `uv pip install paddlepaddle` 才能让 OCR 引擎真正可用。
### 4.3 安装 Appium 和 XCUITest Driver
```bash
npm install -g appium
appium --version
appium driver install xcuitest
appium driver list --installed
appium driver doctor xcuitest
```
`doctor` 中与 Xcode、签名、设备通信相关的错误必须先处理。仅有 warning 时,应根据
是否使用对应功能判断;不要忽略 Xcode command line tools、WDA 或 real-device
相关错误。
## 5. 配置 WebDriverAgent 签名
本项目会让 Appium 的 XCUITest Driver 创建和管理 WDA session。真机第一次运行时,
必须给 WDA 提供签名信息。仓库的 `WDADriverConfig` 支持把未知的
`connection_info` 字段作为 Appium extra capabilities 透传,因此可以直接传入:
- `xcodeOrgId`: Apple Team ID。
- `xcodeSigningId`: 推荐使用 `Apple Development`
- `updatedWDABundleId`: 自己 Team 下唯一的 WDA bundle ID。
如果自动签名失败,可查出 XCUITest Driver 的安装路径:
```bash
appium driver list --installed
npm root -g
```
在 Appium 的 XCUITest Driver 目录中找到并用 Xcode 打开
`WebDriverAgent.xcodeproj`,然后:
1. 选择 `WebDriverAgentRunner` target。
2.`Signing & Capabilities` 中启用 Automatically manage signing。
3. 选择自己的 Team。
4. 将 bundle identifier 改成自己可签名的唯一值。
5. 选择已连接的 iPhone,运行 `WebDriverAgentRunner` test target。
6. 如果 iPhone 提示开发者不受信任,在设备的 VPN 与设备管理设置中信任该开发者。
Appium/XCUITest Driver 升级后,WDA 项目位置或内容可能变化;优先使用自动签名
capabilities,只有自动签名无法工作时再手动打开工程。
## 6. 启动 Appium
在单独的 Terminal 窗口执行:
```bash
appium --address 127.0.0.1 --port 4723
```
保持该进程运行。另开一个 Terminal 检查状态:
```bash
curl -s http://127.0.0.1:4723/status
```
应返回包含 `ready` 或 Appium build 信息的 JSON。若 4723 端口被占用,可以换端口,
但后续 `server_url` 必须同步修改。
## 7. 先做最小真机验证
回到仓库根目录,激活虚拟环境并设置环境变量:
```bash
source .venv/bin/activate
export APEX_WDA_SERVER_URL="http://127.0.0.1:4723"
export APEX_WDA_UDID="$DEVICE_UDID"
```
第一次建议使用下面的直接验证,它会显式传递签名 capabilities,并把截图写到
`/tmp/device-agent-runtime.png`
```bash
python - <<'PY'
import os
from pathlib import Path
from driver.wda_driver import WDADriver, WDADriverConfig
driver = WDADriver(
WDADriverConfig(
server_url="http://127.0.0.1:4723",
udid=os.environ["DEVICE_UDID"],
device_name="iPhone",
extra_capabilities={
"xcodeOrgId": os.environ["APPLE_TEAM_ID"],
"xcodeSigningId": "Apple Development",
"updatedWDABundleId": os.environ["WDA_BUNDLE_ID"],
},
)
)
driver.connect()
try:
image = driver.screenshot()
Path("/tmp/device-agent-runtime.png").write_bytes(image)
print(f"screenshot ok: {len(image)} bytes")
finally:
driver.disconnect()
PY
```
验证通过后,可以运行仓库的实机 integration test:
```bash
uv run --package device-agent-runtime pytest -m integration tests/test_wda_integration.py -v
```
该测试只从环境变量读取 server URL、UDID 和 device name,不传签名 capabilities,
因此应在 WDA 已成功签名/安装后运行。
## 8. 启动可控制真机的 Runtime API
当前不能只运行 README 中的普通 `uvicorn ... --factory` 命令,因为它只会加载已登记
设备,不会调用 `DeviceManager.connect()`。使用下面的启动方式,在同一进程中完成
设备注册、WDA 连接和 REST API 启动:
```bash
python - <<'PY'
import os
import uvicorn
from api.rest import create_app
from device.manager import DeviceManager
from driver.registry import build_driver_factory
device_id = "iphone-1"
connection_info = {
"server_url": "http://127.0.0.1:4723",
"device_name": "iPhone",
"udid": os.environ["DEVICE_UDID"],
"xcodeOrgId": os.environ["APPLE_TEAM_ID"],
"xcodeSigningId": "Apple Development",
"updatedWDABundleId": os.environ["WDA_BUNDLE_ID"],
}
manager = DeviceManager()
app = create_app(manager=manager)
manager.register_device(
device_id,
build_driver_factory("wda", connection_info),
name="Local iPhone",
driver_type="wda",
connection_info=connection_info,
)
manager.connect(device_id, max_retries=1)
uvicorn.run(app, host="127.0.0.1", port=8000)
PY
```
保持进程运行,在另一个 Terminal 验证:
```bash
curl -s http://127.0.0.1:8000/devices
curl -s -X POST http://127.0.0.1:8000/devices/iphone-1/tap \
-H 'Content-Type: application/json' \
-d '{"x": 100, "y": 200}'
curl -s -X POST http://127.0.0.1:8000/devices/iphone-1/launch \
-H 'Content-Type: application/json' \
-d '{"app_id": "com.apple.Preferences"}'
```
点击坐标必须按当前设备屏幕坐标选择。先截图或使用 Appium Inspector 确认坐标,避免
误操作。
如需启动 Web Console,保持 Runtime API 运行,再在第三个 Terminal 执行:
```bash
cd console
npm install
npm run dev
```
Console 默认连接 `http://127.0.0.1:8000`。已由上面启动脚本连接的
`iphone-1` 会出现在设备列表中。不要在 Console 中重复登记同一台设备;当前登记
操作只写入配置,不会自动 connect。
如果不想为 Console 单独起一个 `npm run dev` 进程,可以改为一次性构建后交给
Runtime API 同源托管,见 `console/README.md` 的「Same-Origin, Single-Process
Mode」一节:设置 `VITE_API_BASE_URL=` 构建,再用 `RUNTIME_CONSOLE_STATIC_DIR`
指向构建产物启动 Runtime API,浏览器访问 `/ui/` 即可;改前端代码后需要重新
`npm run build`,不支持热更新。
## 9. 启动云端受管 Host Agent
完成 Appium/WDA 真机验证后,可以把这台 Mac 作为只发起出站连接的边缘 Host。
先把设备连接参数写入本地配置;这里的 `device_id` 只是 Mac 内部引用,不需要与
云端协调,云端会在首次启动时下发正式 ID:
```bash
uv run --package device-agent-runtime python - <<'PY'
import os
from storage.device_config import DeviceConfigStore
DeviceConfigStore("tasks/device_config.sqlite3").add(
device_id="local-iphone-1",
name="Edge iPhone",
driver_type="wda",
connection_info={
"server_url": "http://127.0.0.1:4723",
"device_name": "iPhone",
"udid": os.environ["DEVICE_UDID"],
"wda_local_port": 8100,
"xcodeOrgId": os.environ["APPLE_TEAM_ID"],
"xcodeSigningId": "Apple Development",
"updatedWDABundleId": os.environ["WDA_BUNDLE_ID"],
},
)
PY
```
首次启动前,先在交互式终端创建一次性本地操作账号(仅用于门禁"首次启动"这个
动作本身,后续无人值守重启不会再次要求):
```bash
uv run --package device-host-agent device-host-agent setup
```
`HOST_AGENT_CONTROL_PLANE_URL` 默认已固定为 `https://amcp.home.jerryyan.top`
仅在连接其他云端环境(如本地/测试用的 `cloud-api`)时才需要覆盖该变量。未设置
缓存身份不存在时,Host Agent 会直接向云端注册,由云端返回 `host_id`;后续运行
使用本地持久化的随机 Host secret。无需配置静态 Host 或 enrollment token:
```bash
export HOST_AGENT_IDENTITY_PATH="tasks/host_identity.json"
export HOST_AGENT_DISPLAY_NAME="Edge Mac 01"
export AI_PLANNER_ENABLED="true"
export AI_PLANNER_PROVIDER="anthropic"
export ANTHROPIC_API_KEY="<secret>"
uv run --package device-host-agent device-host-agent
```
首次启动顺序为:持久化候选 Host secret、向云端换取 `host_id`、为每个本地设备
换取 `device_id`、保存映射、连接 WDA、发送 heartbeat、开始 long-poll 领取任务。
Host Agent 会在本机回环地址提供 Console。必须保留并保护 `tasks/host_identity.json`
`tasks/device_config.sqlite3`;前者等同于 Host bearer credential。
直接注册意味着任何能访问该云端地址的设备都能自行注册成为 Host,没有审批环节,
也没有限流保护;这一取舍依赖网络边界(防火墙/反向代理)而非应用层限制。
### 重复实例保护
Host Agent 启动时会先尝试获取一个独占的本地文件锁(位于
`tasks/host_agent.lock`,与 `host_identity.json` 同目录),确保同一个 identity
状态目录下同一时刻只有一个 Host Agent 进程在运行。这是为了避免两个进程用同一个
`host_id` 同时心跳和 claim,导致任务派发落到从未注册过该设备的进程上(典型的
`DeviceNotFoundError` 事故场景)。
如果启动时锁已被另一个仍在运行的进程持有,Host Agent 会立即以非零退出码退出,
stderr 打印 `error: another Host Agent instance is already running ...`,**不会**
联系云端、不会触发任何 enrollment。处理方式:
1.`lsof tasks/host_agent.lock`(macOS)/ 任务管理器(Windows)或
`pgrep -fa device-host-agent` 找到仍在运行的旧进程。
2. 确认旧进程应被停止后,再 `kill` 它(或等它的 graceful shutdown 完成)。
3. 重新启动 Host Agent。崩溃/被 `kill -9` 的旧进程退出时 OS 会自动释放文件锁,
不需要手动删除 `host_agent.lock`
### 使用本地 Web Console
Host Agent 启动时会同时启动本地 Web Console,用于在这台 Mac 上直接查看和管理
正在运行的 Host,无需 SSH 进去读原始状态文件。
不要修改 `HOST_AGENT_CONSOLE_BIND_HOST`,保持默认回环地址 `127.0.0.1`;Console
启动后在同一台 Mac 上打开:
```
http://127.0.0.1:8765
```
`8765``HOST_AGENT_CONSOLE_PORT` 的默认值(见
`apps/device-host-agent/host_agent/config.py`)。登录使用与
`uv run --package device-host-agent device-host-agent setup` 创建的同一个本地账号,
没有单独的 Console 账号体系。
登录后可以看到:
- 状态仪表盘:最近一次 heartbeat 结果与时间、enrollment/identity 状态、本地登记
设备及其连接状态、当前 assignment 执行进度。
- 本地设备管理:新增、编辑、删除登记在这台 Host 上的设备,改动会立即在运行中的
`DeviceManager` 上生效,无需重启 Host Agent。
- 修改密码:更新本地操作账号密码,需要先输入当前密码。
- 最近历史:近期 assignment 与 heartbeat 的执行记录。
- **任务进度页面**:`http://127.0.0.1:8765/tasks` 展示本机 Host Agent 上已执行/正在执行
的任务列表(状态、设备、时间戳),点击任务 ID 可查看逐步 timeline 含截图。
完整的 `HOST_AGENT_CONSOLE_*` 环境变量列表(端口、非回环 bind 的显式 opt-in、
session TTL、历史记录条数上限等)参见 `docs/CLOUD_DEPLOYMENT.md`;生产/远程场景下
应优先使用 SSH 端口转发访问该 Console,而不是直接把它暴露到非回环地址。
Host Agent 会把每步执行状态与截图持久化到本地 SQLite/文件系统,路径与 Runtime 自身的
`tasks/tasks.sqlite3` 不冲突:
```bash
# 任务进度持久化路径(默认值,可通过环境变量覆盖)
# HOST_AGENT_TASK_PROGRESS_DB_PATH="host_agent_data/task_progress.sqlite3"
# HOST_AGENT_TASK_ARTIFACT_DIR="host_agent_data/history"
# HOST_AGENT_TASK_RETENTION_MAX_COUNT=50 # 最多保留 50 个任务
# HOST_AGENT_TASK_RETENTION_MAX_AGE_DAYS=7 # 超过 7 天的任务自动清理
```
Retention 策略取"数量上限与天数上限中更严格的"——即先按 `max_count` 取最近 N 个、
再按 `max_age_days` 过滤掉过老的,最终保留两者中较小的集合。任务完成后,这些记录
在 Console 的 `/tasks` 页面可查。
### 可选:由 Host Agent 托管 Appium 和 Runtime API
默认情况下 Host Agent **不会**自动启动 Appium 或本地 Runtime API:必须按
§6 在独立 Terminal 中保持 `appium --address 127.0.0.1 --port 4723` 运行,按
§8 在另一个 Terminal 中启动 Runtime API。忘记其中任意一个,Host Agent 不会报错,
heartbeat 仍会成功,但设备会静默保持 `offline`、所有任务卡在 `queued`
`host-agent-dependency-supervisor` 是一个可选模式,让 Host Agent 自己把这两个
外部进程作为子进程托管,覆盖单机真机工作流。它默认关闭,需要显式 opt-in:
```bash
export HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED="true"
# 任选其一或两者都开。两者默认 false。
export HOST_AGENT_APPIUM_SUPERVISED="true"
export HOST_AGENT_RUNTIME_SUPERVISED="true"
# 可覆盖默认地址/端口(默认值与 §6/§8 手动流程一致):
# export HOST_AGENT_APPIUM_HOST="127.0.0.1"
# export HOST_AGENT_APPIUM_PORT="4723"
# export HOST_AGENT_RUNTIME_HOST="127.0.0.1"
# export HOST_AGENT_RUNTIME_PORT="8000"
# 单次 Host Agent 进程生命周期内允许的最大重启次数,默认 5。
# export HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS="5"
uv run --package device-host-agent device-host-agent
```
启用后的行为(详见 `openspec/changes/host-agent-dependency-supervisor/`):
- **启动顺序**:Host Agent 在第一次 `connect_devices()` 之前,先按上面选中的
依赖项依次做"先探测后启动"。这样一旦开启,Appium 不再需要单独的 Terminal。
- **Adopt-don't-fight**:探测 `(host, port)` 时若已经有进程在监听并通过健康检查
(Appium `GET /status` 返回 200 JSON,Runtime `GET /devices` 返回 200 JSON),
Host Agent 会以 *adopted* 方式记录日志,**不会**再 spawn 一个重复进程,也不会
在退出/崩溃时杀掉或重启它。如果端口被占但健康检查失败,记一条 port-conflict
错误并跳过该依赖,不抢端口、不静默继续。
- **崩溃重启**:只有 Host Agent 自己 spawn 出来的子进程才会被监控。子进程意外
退出时,按指数退避(1s、2s、4s、8s,封顶 30s)重启;当某个依赖在本进程生命
周期内累计达到 `HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS` 次重启后,停止
再次尝试直到 Host Agent 重启。被 adopt 的进程永远不会被 Host Agent 重启或杀死。
- **spawn 失败 ≠ crash**:如果 `appium` 可执行文件不在 `PATH` 上,启动会以
"dependency-supervisor: appium spawn failed — executable not found" 形式记一条
依赖主管特有的错误,与正常 crash 区分开。请确认按 §4.3 安装好 `appium`
XCUITest/UiAutomator2 driver。
- **退出时**:Host Agent 在自身 graceful shutdown 阶段只会 `terminate` 它自己
spawn 的子进程;adopted 进程保留不动。
- **不影响 Docker/Compose**:本模式只针对 macOS 单机真机工作流;
`compose.yaml` / `compose.deploy.yaml` 完全不受影响。
如果偏好保持对 Appium 终端日志的完全控制、或者已经在用其他进程管理工具
(launchd、systemd、tmux 等)托管 Appium,可以继续使用 §6/§8 的手动流程,
不开启本模式即可。
## 10. 多设备与端口
同时连接多台 iPhone 时,每台设备至少需要:
- 唯一的 Runtime `device_id`
- 唯一的 iPhone `udid`
- 独立的 `wda_local_port`,例如 8100、8101、8102。
示例 connection info:
```json
{
"server_url": "http://127.0.0.1:4723",
"device_name": "iPhone",
"udid": "<device-udid>",
"wda_local_port": 8101,
"xcodeOrgId": "<team-id>",
"xcodeSigningId": "Apple Development",
"updatedWDABundleId": "com.example.WebDriverAgentRunner"
}
```
如果使用预先启动的 WDA,可以通过 extra capability 传
`webDriverAgentUrl`。常规单机使用优先让 Appium 管理 WDA,不要一开始就引入
`iproxy` 或手工 WDA 生命周期。
## 11. 常见故障
### Appium 返回 `device offline`
项目会把 Appium session 创建阶段的大多数异常统一转换成 `device offline`。真正原因
通常在 Appium Terminal 日志中。优先查看最早出现的 Xcode、签名、UDID、端口或 WDA
错误,不要只看 Python 端的最终异常。
### 找不到设备
```bash
xcrun devicectl list devices
xcode-select -p
xcodebuild -version
```
确认手机已解锁、信任电脑、启用 Developer Mode,并能在 Xcode Devices and
Simulators 中看到。
### WDA 构建或签名失败
- 确认 `xcodeOrgId` 是 Team ID,不是 Apple ID 邮箱。
- 确认 `updatedWDABundleId` 唯一且属于可签名的 Team。
- 确认使用 `Apple Development` signing identity。
- 打开 WDA Xcode 工程检查 `WebDriverAgentRunner` target 的 Signing &
Capabilities。
- 免费 Personal Team 的 profile 过期后需要重新签名和安装。
### iOS/Xcode 升级后突然失败
```bash
npm install -g appium
appium driver list --updates
appium driver update xcuitest
appium driver doctor xcuitest
```
升级前记录当前 Appium 和 XCUITest Driver 版本。生产或长期运行环境不要在没有回归
验证时自动更新 Xcode、iOS、Appium 和 XCUITest Driver。
### 端口冲突
```bash
lsof -nP -iTCP:4723 -sTCP:LISTEN
lsof -nP -iTCP:8100 -sTCP:LISTEN
```
Appium server 默认使用 4723;WDA 通常使用 8100。多设备必须为每台设备设置不同的
`wda_local_port`
### OCR 不工作但点击/截图正常
这是 Python/PaddleOCR 依赖问题,不是 WDA 问题。先用 screenshot、tap、swipe、
input、launch 和 UI tree 验证基础控制,再单独处理 PaddleOCR/PaddlePaddle 的 macOS
wheel 与 Apple Silicon 兼容性。
## 12. 完成检查表
- [ ] Xcode 能看到已解锁的 iPhone。
- [ ] iPhone 已信任 Mac,并启用 Developer Mode。
- [ ] `xcode-select -p` 指向完整 Xcode。
- [ ] Node.js/npm 满足 Appium 3 要求。
- [ ] `appium driver doctor xcuitest` 没有阻断性错误。
- [ ] WDA 使用自己的 Team ID 和唯一 bundle ID 成功签名。
- [ ] `curl http://127.0.0.1:4723/status` 返回正常。
- [ ] 直接 Python 验证能生成 `/tmp/device-agent-runtime.png`
- [ ] 实机 integration test 通过。
- [ ] Runtime API 返回 `iphone-1`,并能执行 screenshot/tap/launch。
- [ ] 如需 OCR,另行确认 PaddleOCR 在当前 Mac/Python 架构下可运行。
- [ ] 云端受管部署已保存 Host identity,并能在 `/v1/hosts``/v1/devices` 中看到。
## 13. 后续代码改进建议
为了让后续执行不再依赖内联 Python 启动脚本,建议另开变更实现:
- 为 Console/REST 增加显式 connect/disconnect endpoint。
- 增加正式 CLI,例如 `device-runtime serve --device-config ...`
- 统一将遗留的 `APEX_WDA_*` 环境变量改名为 `DEVICE_RUNTIME_WDA_*`,并保留兼容期。
- 将 PaddleOCR 改成 optional dependency,拆分基础控制与 OCR 安装路径。
- 增加 macOS CI 的无真机 smoke test,以及受控环境中的真机验收脚本。