feat(host-agent): make execution history authoritative
Tests / Test failed: 2, passed: 830

This commit is contained in:
2026-07-15 11:46:27 +08:00
parent ccde30e378
commit 77d4813bb2
46 changed files with 890 additions and 3132 deletions
+20 -17
View File
@@ -37,14 +37,14 @@ uv run --package device-host-agent device-host-agent
```
At startup, the Host Agent loads device registrations from
`tasks/device_config.sqlite3`, the same `DeviceConfigStore` used by the local
Runtime console API. Register or update devices before starting the Host Agent,
then restart it to reload changes. In Compose,
`tasks/device_config.sqlite3`. Register or update devices before starting the
Host Agent, then restart it to reload changes. In Compose,
`HOST_AGENT_TASKS_PATH` selects the host directory mounted at `/app/tasks`; it
defaults to `./tasks`.
The Host Agent only initiates outbound HTTP requests. It does not expose an
inbound port.
The Host Agent only initiates outbound requests to the Cloud Control Plane. It
does expose an authenticated local console on loopback by default; this is not
a Cloud-facing inbound API.
## Direct Edge Enrollment
@@ -96,9 +96,9 @@ image, repository, log, or general backup.
The Host Agent always serves a small local-only web console on the
edge machine: heartbeat/enrollment status, registered local devices, current
assignment progress, local device add/edit/remove, a local account password
change, and recent assignment/heartbeat history. It authenticates with the
same local account created by `device-host-agent setup` above — there is no
separate console credential.
change, assignment/heartbeat history, and complete execution evidence. It
authenticates with the same local account created by `device-host-agent setup`
above; there is no separate console credential.
```text
HOST_AGENT_CONSOLE_BIND_HOST=127.0.0.1
@@ -123,10 +123,10 @@ HOST_AGENT_CONSOLE_HISTORY_LIMIT=200
### Task progress storage and retention
The Host Agent persists step-by-step task execution state (metadata +
timeline screenshots) to local SQLite/files on the edge machine. These
paths are independent from the Runtime's own `tasks/tasks.sqlite3` and
do not collide when both processes run on the same host.
The Host Agent persists step-by-step task execution state (metadata, Timeline
artifacts, screenshots, OCR, and UI-tree results) to local SQLite/files on the
edge machine. Runtime is an in-process library, so there is no second Runtime
database or service to inspect.
```text
HOST_AGENT_TASK_PROGRESS_DB_PATH=host_agent_data/task_progress.sqlite3
@@ -147,12 +147,15 @@ HOST_AGENT_TASK_RETENTION_MAX_AGE_DAYS=7
**Viewing live and historical task progress:**
- **Host Agent console**: Open `http://127.0.0.1:8765/tasks` for the task
list (status, device, timestamps). Click a task ID to see the detail page
with full step-by-step timeline and inlined screenshots.
- **Host Agent console**: Open `http://127.0.0.1:8765/tasks` for the
authoritative list of tasks actually executing on that Host (Cloud task ID,
attempt, status, device, timestamps). Click an execution ID for the full
step-by-step timeline with before/after screenshots, operation details, OCR,
and UI-tree results.
- **Cloud console**: The Cloud Console task detail page shows the latest
coarse-grained progress badge (step index, status, summary) that the Host
Agent piggybacks on each lease renewal.
Agent piggybacks on each lease renewal. For Cloud-proxy hosts it also shows
retained LLM prompt/decision history, but it does not store screenshots.
Treat `HOST_AGENT_CONSOLE_ALLOW_NON_LOOPBACK` as an explicit,
operator-accepted risk: the console has no built-in TLS and no rate
@@ -380,7 +383,7 @@ back to the upstream.
## Runtime AI Planner
The Host Agent reuses the local Runtime planner. Unlike the shared Runtime
The Host Agent reuses the shared Runtime planner. Unlike the shared Runtime
library (whose own default is the deterministic stub planner), the **Host
Agent defaults `AI_PLANNER_ENABLED` to on** -- it is the actual device-control
path, so goal assignments use a model unless an operator explicitly opts out.
+5 -5
View File
@@ -48,11 +48,11 @@ owned by `packages/cloud-platform` and may depend on the Runtime through an
explicit workspace source; the Runtime distribution must never depend on or
package `cloud`.
All Python members share the committed root `uv.lock`. The Runtime operator
console is server-rendered by the `api` layer through Jinja2 templates and
static assets packaged with `device-agent-runtime`; there is no separate
frontend project or Node build step for the Runtime console. The unrelated
`cloud-console/` Vue/Vite application keeps its own independent npm lifecycle.
All Python members share the committed root `uv.lock`. Runtime is an
in-process execution library; its operator evidence view is the authenticated,
server-rendered Host Agent console rather than a separately packaged Runtime
service. The unrelated `cloud-console/` Vue/Vite application keeps its own
independent npm lifecycle.
## Change Discipline
+27 -118
View File
@@ -273,77 +273,15 @@ uv run --package device-agent-runtime pytest -m integration tests/test_wda_integ
该测试只从环境变量读取 server URL、UDID 和 device name,不传签名 capabilities,
因此应在 WDA 已成功签名/安装后运行。
## 8. 启动可控制真机的 Runtime API
## 8. 独立 Runtime API 已撤销
当前不能只运行 README 中的普通 `uvicorn ... --factory` 命令,因为它只会加载已登记
设备,不会调用 `DeviceManager.connect()`。使用下面的启动方式,在同一进程中完成
设备注册、WDA 连接和 REST API 启动:
Runtime 现在是由 Host Agent 在进程内调用的执行库,不再提供 `api.rest`
端口 `8000``/ui/``/console/*`。不要再启动单独的 Runtime 服务,
也不要使用无鉴权的 REST 调用控制设备。
```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;如需暴露到非受信网络请另行评估。
真机连接和任务执行都由下一节的受管 Host Agent 完成。保持 §6 的 Appium 服务
可用,然后启动 Host Agent;本机执行记录、截图、OCR 和 UI 树均从其已鉴权的
Console 查看。
## 9. 启动云端受管 Host Agent
@@ -459,15 +397,16 @@ http://127.0.0.1:8765
`DeviceManager` 上生效,无需重启 Host Agent。
- 修改密码:更新本地操作账号密码,需要先输入当前密码。
- 最近历史:近期 assignment 与 heartbeat 的执行记录。
- **任务进度页面**:`http://127.0.0.1:8765/tasks` 展示本机 Host Agent 上已执行/正在执行
的任务列表(状态、设备、时间戳),点击任务 ID 可查看逐步 timeline 含截图。
- **任务进度页面**:`http://127.0.0.1:8765/tasks` 是本机实际执行任务的权威查看入口。
它展示已执行/正在执行的任务、Cloud task ID 与 attempt;点击执行 ID 可查看每步
timeline 的操作、前后截图、OCR 和 UI 树结果。
完整的 `HOST_AGENT_CONSOLE_*` 环境变量列表(端口、非回环 bind 的显式 opt-in、
session TTL、历史记录条数上限等)参见 `docs/CLOUD_DEPLOYMENT.md`;生产/远程场景下
应优先使用 SSH 端口转发访问该 Console,而不是直接把它暴露到非回环地址。
Host Agent 会把每步执行状态与截图持久化到本地 SQLite/文件系统,路径与 Runtime 自身的
`tasks/tasks.sqlite3` 不冲突
Host Agent 会把每步执行状态、前后截图、OCR 和 UI 树结果持久化到本地
SQLite/文件系统。该 Host Console 是这些实际执行证据的唯一 Web 查看入口
```bash
# 任务进度持久化路径(默认值,可通过环境变量覆盖)
@@ -477,64 +416,34 @@ Host Agent 会把每步执行状态与截图持久化到本地 SQLite/文件系
# HOST_AGENT_TASK_RETENTION_MAX_AGE_DAYS=7 # 超过 7 天的任务自动清理
```
Retention 策略取"数量上限与天数上限中更严格的"——即先按 `max_count` 取最近 N 个、
Retention 策略取"数量上限与天数上限中更严格的" - 即先按 `max_count` 取最近 N 个、
再按 `max_age_days` 过滤掉过老的,最终保留两者中较小的集合。任务完成后,这些记录
在 Console 的 `/tasks` 页面可查。
### 可选:由 Host Agent 托管 Appium 和 Runtime API
### 可选:由 Host Agent 托管 Appium
默认情况下 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:
默认情况下 Host Agent 不会自动启动 Appium;可继续按 §6 在独立 Terminal 中保持
`appium --address 127.0.0.1 --port 4723` 运行。也可以显式让 Host Agent 托管
Appium,避免忘记启动导致设备保持 `offline`
```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` 完全不受影响。
Host Agent 会先探测 Appium;健康实例会被 adopt 而不会重复启动或停止。仅由 Host
Agent 自己启动的 Appium 子进程会在异常退出后按有界指数退避重启,并在 Host Agent
正常关闭时 terminate。已移除的 `HOST_AGENT_RUNTIME_*` 变量会使启动明确失败;
不要再配置或启动独立 Runtime 服务
如果偏好保持对 Appium 终端日志的完全控制、或者已经在用其他进程管理工具
(launchd、systemd、tmux 等)托管 Appium,可以继续使用 §6/§8 的手动流程,
不开启本模式即可。
(launchd、systemd、tmux 等)托管 Appium,可以继续使用 §6 的手动流程,不开启
本模式即可。
## 10. 多设备与端口
@@ -629,16 +538,16 @@ wheel 与 Apple Silicon 兼容性。
- [ ] `curl http://127.0.0.1:4723/status` 返回正常。
- [ ] 直接 Python 验证能生成 `/tmp/device-agent-runtime.png`
- [ ] 实机 integration test 通过。
- [ ] Runtime API 返回 `iphone-1`,并能执行 screenshot/tap/launch
- [ ] Host Agent Console 显示已连接设备,并能在 `/tasks` 查看一次完成任务的完整证据
- [ ] 如需 OCR,另行确认 PaddleOCR 在当前 Mac/Python 架构下可运行。
- [ ] 云端受管部署已保存 Host identity,并能在 `/v1/hosts``/v1/devices` 中看到。
## 13. 后续代码改进建议
为了让后续执行不再依赖内联 Python 启动脚本,建议另开变更实现:
为了让后续设备管理和验收更易操作,建议另开变更实现:
- 为 Console/REST 增加显式 connect/disconnect endpoint
- 增加正式 CLI,例如 `device-runtime serve --device-config ...`
- Host Agent Console 增加显式 connect/disconnect 状态诊断
- 增加正式 CLI,用于校验 Host 的设备配置和 Appium 连通性
- 统一将遗留的 `APEX_WDA_*` 环境变量改名为 `DEVICE_RUNTIME_WDA_*`,并保留兼容期。
- 将 PaddleOCR 改成 optional dependency,拆分基础控制与 OCR 安装路径。
- 增加 macOS CI 的无真机 smoke test,以及受控环境中的真机验收脚本。