Files
agentic-mobile-control/docs/MACOS_IPHONE_SETUP.md
T
q792602257 efeb3eb926
Tests / Test passed: 581
Implement edge-host-self-enrollment
Host Agent:
- One-time local operator account bootstrap (PBKDF2-HMAC-SHA256, atomic
  0600-permission write) gating the daemon's first unattended start via a
  new `setup` CLI subcommand.
- Default control-plane URL now https://amcp.home.jerryyan.top (env var
  override unchanged).
- Enrollment no longer requires a pre-issued token; falls back to
  zero-token self-service enrollment when none is configured.

Cloud control plane:
- CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED (default false) opt-in flag.
- SelfServiceEnrollmentAuthProvider + ChainedEnrollmentAuthProvider:
  configured tokens still take priority; self-service only applies when
  no token matches, preserving edge-host-enrollment's token-bound path.
- Fixed a latent bug in sql_repository.py::enroll_host: the token-conflict
  lookup used `== enrollment_token_digest`, which SQLAlchemy compiles to
  `IS NULL` when the value is None, so every self-service enrollment after
  the first would have falsely collided with an existing NULL-digest host.
  Skipped that lookup entirely when the digest is None.

Docs/deploy: .env.example, compose.yaml, compose.deploy.yaml,
CLOUD_DEPLOYMENT.md, MACOS_IPHONE_SETUP.md updated for the new flag,
URL default, and required `device-host-agent setup` step.

Verification: 494 non-integration tests pass; openspec validate --strict
passes. PostgreSQL-backed contract tests and full manual end-to-end
verification were not run (no Postgres/Docker or reachable cloud-api in
this environment); noted as unchecked in tasks.md 7.2/7.4.
2026-07-13 18:30:49 +08:00

17 KiB

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:

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,至少启动一次,然后执行:

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 窗口复制, 也可运行:
xcrun devicectl list devices

后续示例使用以下环境变量。值必须换成自己的信息:

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.14。Appium 3.1 要求 Node.js ^20.19.0 || ^22.12.0 || >=24.0.0,并要求 npm >=10

已安装 Homebrew 时执行:

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

在仓库根目录执行:

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。运行完整的非实机测试使用:

uv run --all-packages pytest -m "not integration"

如果 Apple Silicon 上安装 paddleocr 或其底层推理依赖失败,可以先安装不带 OCR 的控制链路,用于截图、点击、滑动、输入、启动 App 和读取 UI tree:

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

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 的安装路径:

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 窗口执行:

appium --address 127.0.0.1 --port 4723

保持该进程运行。另开一个 Terminal 检查状态:

curl -s http://127.0.0.1:4723/status

应返回包含 ready 或 Appium build 信息的 JSON。若 4723 端口被占用,可以换端口, 但后续 server_url 必须同步修改。

7. 先做最小真机验证

回到仓库根目录,激活虚拟环境并设置环境变量:

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

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:

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 启动:

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 验证:

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 执行:

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:

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

首次启动前,先在交互式终端创建一次性本地操作账号(仅用于门禁"首次启动"这个 动作本身,后续无人值守重启不会再次要求):

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 注册路径:

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 不监听入站端口。若使用了一次性 enrollment token,成功后可以将其从 边缘环境移除;但必须保留并保护 tasks/host_identity.jsontasks/device_config.sqlite3;前者等同于 Host bearer credential。

零 token 自助注册意味着任何能访问该云端地址的设备都能自行注册成为 Host,没有 审批环节,也没有限流保护;这一取舍依赖网络边界(防火墙/反向代理)而非应用层 限制,仅应在受控网络中开启 CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED

需要回滚到静态模式时,停止 Host Agent,由云端管理员配置匹配的静态 Host credential,然后显式设置 HOST_AGENT_HOST_IDHOST_AGENT_TOKEN。这两个变量 同时存在时优先于 enrollment state。

10. 多设备与端口

同时连接多台 iPhone 时,每台设备至少需要:

  • 唯一的 Runtime device_id
  • 唯一的 iPhone udid
  • 独立的 wda_local_port,例如 8100、8101、8102。

示例 connection info:

{
  "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 端的最终异常。

找不到设备

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 升级后突然失败

npm install -g appium
appium driver list --updates
appium driver update xcuitest
appium driver doctor xcuitest

升级前记录当前 Appium 和 XCUITest Driver 版本。生产或长期运行环境不要在没有回归 验证时自动更新 Xcode、iOS、Appium 和 XCUITest Driver。

端口冲突

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,以及受控环境中的真机验收脚本。