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

11 KiB
Raw Blame History

设备端 Agent 接口契约(DEVICE_AGENT)

适用读者:写设备端 Agent APK(另一个仓库)的人,以及改平台侧这组接口的人。 相关:API.md(管理端接口)、DEPLOY.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

拉设备身份 + 可安装应用清单。建议开机后拉一次 + 每次打开"商店"界面时拉一次。

响应

{
  "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"}

响应:

{"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

回报下载/安装结果,平台侧留档(面板可见)。

请求体

{
  "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 剪贴板注入(已在用,务必保持兼容)

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 设备身份显示(已实现)

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:

{"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 一次性配置下发(建议设备端实现)

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 账号密码