平台在拉起身份页时顺带推这台设备登记的抖音账号(契约见 平台 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 装机验证(带账号/不带账号两种截图)。
275 lines
11 KiB
Markdown
275 lines
11 KiB
Markdown
<!--
|
||
这是 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 账号密码
|