Reapply "feat: 设备端应用商店(平台侧)"

This reverts commit bf5071b14d.
This commit is contained in:
2026-09-13 17:37:32 +08:00
parent bf5071b14d
commit 524ae20c49
11 changed files with 668 additions and 3 deletions
+55 -1
View File
@@ -219,6 +219,26 @@
| POST | `/api/system/backup/preview` | 上传备份校验预览(multipart,字段 `file`) |
| POST | `/api/system/backup/apply` | 应用恢复(重启生效) |
### 2.11 device_agent(`web/device_agent_api.py`)
**设备端专用**(无登录会话,靠 `X-Device-Token` 鉴权;平台未启用时统一 404):
| 方法 | 路径 | 功能 |
|------|------|------|
| GET | `/api/device/agent/bootstrap` | 设备拉自己的身份 + 可安装应用清单 |
| GET | `/api/device/agent/apk/<apk_id>` | 下载 APK 文件 |
| POST | `/api/device/agent/report` | 上报下载/安装结果 |
**管理端**(登录 + 应用管理权限):
| 方法 | 路径 | 功能 |
|------|------|------|
| GET | `/api/agent-store/config` | 读配置(含设备令牌) |
| POST | `/api/agent-store/config` | 启用/停用、重置令牌 |
| GET | `/api/agent-store/logs` | 设备端安装记录 |
> 完整协议、鉴权与版本约定见 [DEVICE_AGENT.md](DEVICE_AGENT.md)。
---
## 3. 认证与页面
@@ -656,7 +676,41 @@ multipart 上传 `.zip` 或 `.db`(字段 `file`)→ 校验并暂存:
---
## 14. 非 JSON 响应汇总
## 14. 设备端应用商店
**设备侧**三个接口(`bootstrap` / `apk` / `report`)的完整协议、鉴权、错误码与版本约定
见 [DEVICE_AGENT.md](DEVICE_AGENT.md)——那份文档同时是设备端 APK 仓库的对接契约。
**管理侧**(登录 + 应用管理权限):
### GET /api/agent-store/config
```json
{"ok": true, "enabled": false, "token": "…", "token_created_at": "2026-09-13 14:38:11",
"api_version": 1, "base_url_hint": "http://192.168.20.220:18050"}
```
`base_url_hint` 由请求的 Host 回填,直接可作为设备端要填的平台地址。
### POST /api/agent-store/config
`{"enabled": true}` 启用(并自动生成令牌);`{"regenerate_token": true}` 重置令牌
(旧令牌立即失效)。返回 `{"ok": true, "enabled": …, "token": …}`。
### GET /api/agent-store/logs?limit=50
```json
{"ok": true, "logs": [{"id": 1, "device_name": "A08", "serial": "192.168.20.100:5555",
"fingerprint": "…", "apk_id": "56b114aa", "package_name": "com.example.clipinject",
"version_name": "1.0", "action": "install_fail", "message": "MIUI 拦截未确认",
"created_at": "2026-09-13 14:40:00"}]}
```
`action` ∈ `download` / `install_ok` / `install_fail`;记录表最多保留 500 条(自动裁旧)。
---
## 15. 非 JSON 响应汇总
| 方法 | 路径 | 响应类型 | 说明 |
|------|------|---------|------|
+4
View File
@@ -43,6 +43,7 @@
| 10 | `agent_experience` | 经验库(任务级配方) |
| 11 | `experience_audit` | 经验巡检结论 |
| 12 | `agent_action` | 动作库(命名动作) |
| 13 | `device_install_log` | 设备端应用商店的下载/安装记录(设备上报,见 [DEVICE_AGENT.md](DEVICE_AGENT.md)) |
> 2026-09-13 之前,`app_meta` 与 4 张 `agent_*` 表是各模块里的裸 `CREATE TABLE`
> (不进模型层)。迁 MySQL 时那批 SQL 的 `AUTOINCREMENT`/`TEXT DEFAULT ''`/`TEXT PRIMARY KEY`
@@ -232,6 +233,9 @@ UTF-8 等价于字节序)。
| `discovery_interval` | 扫描周期秒(10-3600) | 同上 |
| `discovery_port` | adb 探测端口(1-65535) | 同上 |
| `discovery_auto_claim` | 指纹匹配时自动认领(`"1"`/`"0"`,默认关) | 同上 |
| `agent_store_enabled` | 设备端应用商店开关(`"1"`/`"0"`,默认关) | 应用管理页 |
| `agent_device_token` | **设备端令牌**(Agent 调设备接口用;属凭据) | 同上(启用时自动生成,可重置) |
| `agent_device_token_at` | 令牌生成/重置时间 | 同上 |
| `deployment_env` | **库环境标签**(`dev`/`prod`),启动时与 `.env` 比对 | `core/db_config.py`(首次连接)/ 迁移脚本 |
| `deployment_id` | 库唯一标识(uuid),用于识别"这份备份来自哪个库" | 同上 |
| `deployment_claimed_at` | 标签写入时间 | 同上 |
+217
View File
@@ -0,0 +1,217 @@
# 设备端 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/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 close 1
```
要求:全屏大字、再次调用可覆盖内容、`close` 可关闭、不驻留前台。
### 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 账号密码
+2
View File
@@ -18,6 +18,7 @@
| [MCP_DESIGN.md](MCP_DESIGN.md) | **MCP 设计文档**:边界划分、错误码、白名单/审计设计、演进方向 | 平台开发者 |
| [AI_CONSOLE.md](AI_CONSOLE.md) | **AI 控制台**:会话/SSE、经验库、动作库、巡检、Markdown 渲染、推理链、token 统计 | 使用者、平台开发者 |
| [AI_TASK_GEN.md](AI_TASK_GEN.md) | **AI 建任务**:设计稿与里程碑(P0 未实现,属规划) | 平台开发者 |
| [DEVICE_AGENT.md](DEVICE_AGENT.md) | **设备端 Agent 接口契约**:应用商店的设备专用接口(清单/下载/上报)、adb 指令协议、版本约定 —— 与设备端 APK 仓库共享的契约 | 设备端开发者、平台开发者 |
| [DEPLOY.md](DEPLOY.md) | **部署与运维**:环境准备、生产容器、数据备份导出/导入、故障排查 | 运维、部署者 |
| [DEVELOPMENT.md](DEVELOPMENT.md) | **开发手册**:git 流程、技术红线、本地开发与调试、常见开发任务、文档同步约定 | 所有开发者 |
| [STF_REMOVAL.md](STF_REMOVAL.md) | **历史记录**:摘除 OpenSTF 的迁移过程(阶段 0-3) | 追溯背景时参考 |
@@ -56,6 +57,7 @@
| 任务类型 / 步骤 schema | [TASK_DEV.md](TASK_DEV.md) |
| 常驻线程 / 进程装配 | [ARCHITECTURE.md](ARCHITECTURE.md) §线程与并发 |
| MCP 工具 | [MCP.md](MCP.md) + [MCP_DESIGN.md](MCP_DESIGN.md) |
| 设备端 Agent(APK) | [DEVICE_AGENT.md](DEVICE_AGENT.md) |
| 对外接入约定 | [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md) |
| 暂缓项 / 已知问题 | [backlog/TODO.md](backlog/TODO.md)(完成时移出并同步相关文档) |