Files
agentic-mobile-control/docs/MACOS_IPHONE_SETUP.md
T
q792602257andClaude Opus 4.6 e00c50e703 feat(api): server-rendered Jinja2 Runtime console at /ui/
Replaces the separate Vue/Vite `console/` SPA with a same-origin,
server-rendered console built on a module-level Jinja2 Environment
with select_autoescape(["html","xml"]).

- Add api/console_web.py with /ui/ routes (dashboard, tasks, task
  detail/timeline, config) and a _status_fragment polled every 10s.
- Refactor api/console.py into a typed ConsoleService shared by the
  JSON and HTML routers so validation/persistence cannot drift.
- Remove RUNTIME_CONSOLE_STATIC_DIR, SpaStaticFiles, and the wildcard
  CORS middleware from api/rest.py; GET / now redirects to /ui/.
- Delete the top-level console/ project; add jinja2 and python-multipart
  as direct dependencies and ship templates/CSS/JS via package-data.
- Add 31 tests (XSS probes, PRG flows, fragment refresh, no-static-dir
  and no-CORS regressions, wheel-packaging smoke test).

/console/* JSON endpoints remain unchanged. The console keeps the
trusted-network-only boundary; auth/CSRF is intentionally deferred.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-15 08:03:13 +08:00

639 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 确认坐标,避免
误操作。
Runtime API 自带同源 Web Console,无需额外的前端进程、Node 工具链或
`RUNTIME_CONSOLE_STATIC_DIR`。保持 Runtime API 运行,浏览器访问
`http://127.0.0.1:8000/`(会自动 307 跳转到 `/ui/`)即可:
- `/ui/`:设备状态面板,约每 10 秒自动刷新一次;
- `/ui/tasks`:任务列表与筛选;
- `/ui/tasks/{task_id}`:任务详情与逐步 timeline(含截图);
- `/ui/config`:登记/移除设备、调整 `max_steps`
已由上面启动脚本连接的 `iphone-1` 会出现在设备列表中。不要在 Console 中
重复登记同一台设备;当前登记操作只写入配置,不会自动 connect。Console 与
`/console/*` JSON API 共用同一份 Runtime 状态,两者行为一致。Runtime Console
仅假设受信任本地网络访问,不提供鉴权 / CSRF;如需暴露到非受信网络请另行评估。
## 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:
Host Agent 的 Planner transport 默认是 `cloud`。启动前在 Cloud Console 创建并激活
LLM Provider profile;Provider API key 只由 Cloud API 加密保存,边缘 Host 不需要也
不应配置厂商 API key。
```bash
export HOST_AGENT_IDENTITY_PATH="tasks/host_identity.json"
export HOST_AGENT_DISPLAY_NAME="Edge Mac 01"
export AI_PLANNER_ENABLED="true"
uv run --package device-host-agent device-host-agent
```
只有需要绕过 Cloud API 时,才显式设置
`AI_PLANNER_TRANSPORT=direct`,并在该 Host 上配置
`AI_PLANNER_PROVIDER``AI_PLANNER_MODEL` 与对应的厂商 API key。
首次启动顺序为:持久化候选 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,以及受控环境中的真机验收脚本。