# 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 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="" export APPLE_TEAM_ID="" export WDA_BUNDLE_ID="com..WebDriverAgentRunner" ``` `WDA_BUNDLE_ID` 必须在自己的 Team 下唯一,不能原样使用示例值。 ## 4. 安装本地工具链 ### 4.1 Homebrew、Python 和 Node.js 本项目当前在 `pyproject.toml` 中要求 Python `>=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.14 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 基础控制不受影响。不要为了兼容 PaddleOCR 擅自降到 Python 3.13, 因为当前项目元数据明确要求 Python 3.14;如需降级,应先作为独立兼容性变更修改 和验证 `pyproject.toml`。 ### 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。 ## 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_ENROLLMENT_TOKEN` 时,Host Agent 会直接向云端发起零 token 自助注册 (需要云端 `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED=true`);若仍持有云端管理员 签发的一次性 enrollment token,也可以继续设置 `HOST_AGENT_ENROLLMENT_TOKEN` 走原有的 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="" uv run --package device-host-agent device-host-agent ``` 首次启动顺序为:持久化候选 Host secret、向云端换取 `host_id`、为每个本地设备 换取 `device_id`、保存映射、连接 WDA、发送 heartbeat、开始 long-poll 领取任务。 Host Agent 不监听入站端口。若使用了一次性 enrollment token,成功后可以将其从 边缘环境移除;但必须保留并保护 `tasks/host_identity.json` 和 `tasks/device_config.sqlite3`;前者等同于 Host bearer credential。 零 token 自助注册意味着任何能访问该云端地址的设备都能自行注册成为 Host,没有 审批环节,也没有限流保护;这一取舍依赖网络边界(防火墙/反向代理)而非应用层 限制,仅应在受控网络中开启 `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED`。 需要回滚到静态模式时,停止 Host Agent,由云端管理员配置匹配的静态 Host credential,然后显式设置 `HOST_AGENT_HOST_ID` 与 `HOST_AGENT_TOKEN`。这两个变量 同时存在时优先于 enrollment state。 ## 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": "", "wda_local_port": 8101, "xcodeOrgId": "", "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,以及受控环境中的真机验收脚本。