Files
device_agent/INTERFACE.md
T
butubb b81aa1fd82 feat: 身份页显示本机账号台账(accounts_b64,v1.5)
平台在拉起身份页时顺带推这台设备登记的抖音账号(契约见
平台 doc/DEVICE_AGENT.md §5.2.1 与本仓库 INTERFACE.md §5.2.1):

- 解析 `--es accounts_b64`(标准 base64 + UTF-8 → JSONObject),在设备信息下面
  渲染账号列表:账号名称 / 抖音号 / [卡·发] 标记
- 一屏放不下:**最多显示 4 个** + "…还有 N 个,见平台「账号」页"
  (第一版 26sp 一行塞"名称+号+标记",720x1650 上第 3 个号就被切掉了,实测后收紧)
- 三种情况分清楚:无 extra → 整块不显示(= 这台没登记账号,行为与旧版逐字一致);
  有但解不开 → 显示一行"解析失败"(不静默,否则像"没账号");能解开 → 逐条列出
- 隐私:平台默认不推手机号(身份页是机器旁的公开屏幕)

版本 1.4 → 1.5(versionCode 6)。已在 A03 装机验证(带账号/不带账号两种截图)。
2026-09-24 16:18:25 +08:00

275 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
这是 auto_control 平台仓库 doc/DEVICE_AGENT.md 的副本(两个仓库共享的接口契约)。
以平台仓库那份为准;改协议时两边一起改。
同步时间:2026-09-13
-->
# 设备端 Agent 接口契约(DEVICE_AGENT)
> 适用读者:写**设备端 Agent APK**(另一个仓库)的人,以及改平台侧这组接口的人。
> 相关:[API.md](API.md)(管理端接口)、[DEPLOY.md](DEPLOY.md)(部署)、[ARCHITECTURE.md](ARCHITECTURE.md)。
**这份文档是两个仓库之间的契约。** 平台侧实现见 `web/device_agent_api.py`;
设备端实现见另一个仓库。**改协议必须同时改两边**(见 §6 版本约定)。
---
## 1. 为什么有这组接口
平台通过 **adb** 驱动设备(点击/输入/装应用……),但有三件事 adb 干不好,需要设备上装一个 App:
| 需求 | adb 的困境 | Agent 的做法 |
|------|-----------|-------------|
| 写系统剪贴板 | Android 10+ 禁止后台/shell 写剪贴板;u2 的 `set_clipboard` 会"成功但内容被丢弃" | 启动一个**透明 Activity**(有焦点才能写),写完立即退出 |
| 安装 APK | MIUI 会拦 `adb install`(`USER_RESTRICTED`),要人工点「继续安装」 | **App 自己调系统安装器** → 走普通应用安装流程,**不触发那道 USB 拦截** |
| 显示设备身份 | 现在要把一个 HTML 大字页推给设备浏览器打开,依赖浏览器 | 全屏大字显示:设备名/IP/序列号/平台地址 |
其中「装应用」是**设备主动来取**(拉清单 → 选 → 下载 → 装),所以平台开了一组
**设备专用接口**;「写剪贴板」「显示身份」是**平台推指令**,仍走 adb(§5)。
---
## 2. 接入流程(设备端视角)
```
① 一次性:平台在「工具 → 应用管理 → 📱 设备端应用商店」启用,拿到
接入地址 base_url(如 http://192.168.20.220:18050)和设备令牌 token
—— 这两个值可以通过 adb 一次性推给设备(见 §5.3),不必在每台手机上手输
② 开机/定时:GET {base_url}/api/device/agent/bootstrap
带上 X-Device-Token / X-Device-Fingerprint
→ 拿到「我是谁」(平台分配的设备名)+「有哪些应用可以装」
③ 用户在手机上选一个应用:
GET {base_url}/api/device/agent/apk/{id} → 下载 APK 到应用私有目录
④ 调系统安装器安装(PackageInstaller / ACTION_VIEW + FileProvider)
—— 会弹系统安装确认框,用户在屏幕上点一下即可
⑤ 回报结果:POST {base_url}/api/device/agent/report
(download / install_ok / install_fail + 失败原因)
平台在「设备端应用商店」面板能看到记录
```
> Agent 需要 `INTERNET` 与 `REQUEST_INSTALL_PACKAGES` 权限;还需要用户在系统设置里
> 给"安装未知应用"授权(这是 App 自装的**唯一**前置条件,比 MIUI 的 USB 拦截好过得多)。
---
## 3. 接口规格
所有接口:
- **不需要登录会话**,靠 `X-Device-Token` 鉴权
- 请求/响应均为 UTF-8 JSON(下载接口除外)
- **功能默认关闭**:平台侧未启用时,全部返回 `404 {"ok":false,"error":"not found"}`
(不暴露接口是否存在)
### 3.1 通用请求头
| 头 | 必填 | 说明 |
|----|------|------|
| `X-Device-Token` | ✅ | 平台生成的设备令牌 |
| `X-Device-Fingerprint` | 建议 | `ro.serialno`。**平台靠它认出"你是哪台设备"**(换 IP 也能认回原名) |
| `X-Device-Serial` | 可选 | 当前 adb 地址(`IP:5555`),指纹认不出时的兜底 |
错误码:
| 状态 | 含义 | 设备端该怎么办 |
|------|------|---------------|
| 404 | 平台未启用设备端商店(或接口不存在) | 提示管理员去平台开启;不要重试轰炸 |
| 401 | 令牌不对/缺失 | 提示重新配置令牌(管理员重置后要在设备上更新) |
| 500 | 平台内部错误 | 退避重试 |
### 3.2 `GET /api/device/agent/bootstrap`
拉设备身份 + 可安装应用清单。建议开机后拉一次 + 每次打开"商店"界面时拉一次。
**响应**
```json
{
"ok": true,
"agent_api_version": 1,
"server_time": "2026-09-13 14:38:11",
"platform": { "url": "http://192.168.20.220:18050" },
"device": {
"fingerprint": "woijo7v4sgnrhqb6",
"serial": "192.168.20.100:5555",
"name": "A08",
"known": true
},
"apks": [
{
"id": "56b114aa",
"display_name": "ClipInject",
"package_name": "com.example.clipinject",
"version_name": "1.0",
"version_code": 1,
"size": 11615,
"upload_time": "2026-09-09 13:49:06",
"download_path": "/api/device/agent/apk/56b114aa"
}
]
}
```
- `device.name`:**平台给这台设备起的名字**(如 `A08`)—— 身份显示界面直接用它
- `device.known=false`:这台设备还没登记进平台设备池(指纹没匹配上),`name` 为空;
设备端应显示"未登记",不要假装自己有名字
- `platform.url`:按**设备实际请求到的地址**回填,可直接用于拼下载地址
- `apks[].download_path` 是相对路径,拼 `platform.url` 即完整 URL
### 3.3 `GET /api/device/agent/apk/{apk_id}`
下载 APK 文件本体(`application/vnd.android.package-archive`)。
- 成功:`200` + 文件流
- `404`:id 不存在或平台侧文件已删(**注意与"未启用"的 404 区分:看响应体**,
未启用时是 JSON `{"ok":false,...}`,文件缺失时也是 JSON —— 设备端拿到非 `application/...` 的响应就视为失败)
### 3.4 `POST /api/device/agent/install`(可选:让平台帮你静默装)
**背景**:设备上的 App 自己调系统安装器,MIUI 上要过「继续 → 勾选未经安全检测 →
继续更新」甚至 ICP 备案检查好几步;而**平台用 adb 装是静默的**(实测升级 2.7s、
全新安装 7.9s、336MB 大包都没弹过一次框)。
所以"安装/更新"按钮**建议走这个接口**:用户在手机上选(商店体验),平台用 adb 落地(静默)。
**请求体**:`{"apk_id": "56b114aa"}`
**响应**:
```json
{"ok": true, "msg": "开始安装 X 到 1 台设备", "serial": "192.168.20.100:5555"}
```
- 受理后平台**异步**安装,设备端应轮询本机该包的 versionCode 变化来判断结果
(装上后按 §3.5 上报 `install_ok`)
- 平台装不了(比如 MIUI 拦了 adb 安装、设备不在池里)→ `ok=false` + `error`,
**设备端应退回本机安装**(`ACTION_VIEW` + FileProvider,会弹系统确认框,但至少能装)
- `409` = 这台设备还没登记进平台设备池(平台不认识它,不能替它装)
### 3.5 `POST /api/device/agent/report`
回报下载/安装结果,平台侧留档(面板可见)。
**请求体**
```json
{
"apk_id": "56b114aa",
"action": "download | install_ok | install_fail",
"package_name": "com.example.clipinject",
"version_name": "1.0",
"message": "失败原因(install_fail 时必填,越具体越好)"
}
```
**响应**:`{"ok": true}`;`action` 非法 → `400`。
> **失败原因请写人话**:平台面板直接显示 `message`。
> 例如「MIUI 拦截:用户未确认」「安装包解析失败」「存储空间不足」。
---
## 4. 平台侧配置(管理端)
「工具 → 应用管理 → 📱 设备端应用商店」:
| 操作 | 接口 |
|------|------|
| 读配置(含令牌) | `GET /api/agent-store/config` |
| 启用/停用 | `POST /api/agent-store/config` `{"enabled": true}` |
| 重置令牌 | `POST /api/agent-store/config` `{"regenerate_token": true}` |
| 安装记录 | `GET /api/agent-store/logs?limit=50` |
- 三个接口都需要**登录 + 应用管理权限**
- **停用**不影响已装的 App,只是设备端拿不到清单(返回 404)
- **重置令牌**后旧令牌立即失效,所有设备要在 Agent 里更新
---
## 5. 平台 → 设备的指令(走 adb,不走网络)
这些**没有 HTTP 接口**,由平台用 `adb shell am start` 拉起设备上的 Activity。
### 5.1 剪贴板注入(已在用,务必保持兼容)
```bash
adb -s <serial> shell am start -n <pkg>/.ClipActivity --es text_b64 <base64(UTF-8)>
```
- **必须 base64**:adb shell 直传中文/特殊字符会变形
- 必须用**透明 Activity**(有焦点才能写剪贴板),写完**立即退出**
- 平台判定失败的方式:`am start` 输出里有 `unable to resolve Intent` / `does not exist`
→ 视为"设备未安装 Agent"
### 5.2 设备身份显示(已实现)
```bash
adb -s <serial> shell am start -n <pkg>/.InfoActivity \
--es name "A08" --es ip "192.168.20.100" --es serial "192.168.20.100:5555" \
--es server "http://192.168.20.220:18050" \
--es accounts_b64 "<base64(UTF-8 JSON)>" # 可选,见 §5.2.1
# 关闭:--es close 1
```
要求:全屏大字、再次调用可覆盖内容、`close` 可关闭、不驻留前台。
#### 5.2.1 本机账号台账(`accounts_b64`,可选 extras,已实现 v1.5)
平台在**打开**身份页时顺带推这台设备在「账号」页登记的抖音账号(`close` 时不带)。
**标准 base64(不是 URL_SAFE)+ UTF-8**:
```json
{"v":1,"total":4,"shown":4,"truncated":false,
"note":"设备『A03』登记 4 个号",
"accounts":[
{"name":"AA建材王总","douyin_id":"66057500463","device_name":"A03","device_no":"A03",
"sim_in_device":true,"can_post_video":true}]}
```
| 字段 | 说明 |
|------|------|
| `v` | 块格式版本(当前 **1**) |
| `total` / `shown` / `truncated` | 台账里的总数 / 本次几条 / 是否被截断 |
| `accounts[].name` / `douyin_id` | 账号名称 / **抖音号(纯号)** |
| `accounts[].sim_in_device` / `can_post_video` | 卡在机内 / 可发视频 |
| `accounts[].phone` | **默认不推**(大字页是公开屏幕);平台侧可传 `include_phone:true` 要 |
设备端三种情况要分清(InfoActivity 已按此实现):
| 情况 | 表现 |
|---|---|
| 没有该 extra | 账号块**整块不显示**(= 这台设备在台账里没登记账号,旧平台也走这条) |
| 有但解不开 | 显示一行**解析失败**提示(不要静默,否则看着像"这台没账号") |
| 有且能解开 | 逐条显示;`truncated` 时补"…还有 N 个,见平台「账号」页" |
### 5.3 一次性配置下发(建议设备端实现)
```bash
adb -s <serial> shell am start -n <pkg>/.ConfigActivity \
--es server "http://192.168.20.220:18050" --es token "<设备令牌>"
```
有了它,8 台设备不用一台台手输地址和令牌。
---
## 6. 版本与兼容
- `agent_api_version`(当前 **1**)在每次 `bootstrap` 响应里返回
- **设备端**:启动时对比自己实现的版本,不一致要在日志里明确记下来(不要静默)
- **平台端**:
- 加字段 / 加接口 = 小版本,设备端忽略未知字段即可
- 改已有字段语义 / 删字段 / 改鉴权方式 = **大版本**,两边必须同时改
- 改了协议,**必须同时改这份文档的两个仓库版本**(本文件是平台侧那一份)
---
## 7. 安全须知
- **令牌是凭据**:等于"能读平台的应用清单和 APK 文件"。别写进日志、别外传、
别提交到 git(平台侧存在 `app_meta.agent_device_token`,会随备份一起走)
- 平台侧这组接口**只读**:只能读清单和下载 APK,**不能**触发任务、不能改设备池、
不能读配置或密钥
- 生产环境建议:令牌泄露时立即在面板点「重置令牌」
- 设备端**不需要也不应该**拿 admin 账号密码