feat: 设备端应用商店(平台侧)—— Agent 直连拉清单/下载/安装,绕开 MIUI 的 USB 安装拦截

背景:`adb install` 在 MIUI 上被「USB 安装」拦下(USER_RESTRICTED),必须人工点确认;
而**由设备上的 App 自己调系统安装器**走的是普通应用安装流程,不触发那道拦截。

平台侧实现(设备端 Agent APK 在另一个仓库,契约见 doc/DEVICE_AGENT.md):

- web/device_agent_api.py(新):
  * 设备侧 `/api/device/agent/{bootstrap,apk/<id>,report}` —— 无登录会话,靠
    `X-Device-Token`(常量时间比对)鉴权;`X-Device-Fingerprint` 让平台认出是哪台设备
    (换 IP 也认得出,并把平台分配的名称回给它);**默认关闭,关闭时统一 404**,
    不暴露接口存在性;只读清单与文件,不返回任何配置/密钥
  * 管理侧 `/api/agent-store/{config,logs}` —— 登录 + 应用管理权限;启用时自动生成令牌,
    可一键重置(旧令牌立即失效)
- core/models.py:新增 `device_install_log`(下载/安装记录,自动裁剪保留 500 条)
- 应用管理页新增「📱 设备端应用商店」面板:开关 / 接入地址 / 令牌(复制·重置)/ 安装记录;
  面板里写明它与「平台批量推送」的分工
- doc/DEVICE_AGENT.md(新):**两个仓库共享的接口契约** —— 接入流程、三个设备接口的
  完整规格与错误码、adb 指令协议(剪贴板/身份显示/配置下发)、版本兼容约定、安全须知;
  API.md、DATA_MODEL.md、doc/README.md 索引同步

验证:
- 后端 9 组用例:默认关闭 404、无/错令牌 401、指纹认出设备名、清单、下载、上报与失败
  原因落库、关闭后重新 404、重置令牌后旧令牌立即失效、未登录管理端被拒 —— 全通过
- 真实浏览器:面板渲染、点开关 → 自动生成令牌 → 设备端带令牌拉到清单且 device.name=A08、
  无 JS 报错;测试后已把开关还原为关闭
- 全量回归 105 项通过(42 GET + 62 写探测 + 7 关键业务)
This commit is contained in:
2026-09-13 14:40:38 +08:00
parent 66632bb7d6
commit dad1af8b0b
11 changed files with 668 additions and 3 deletions
+28
View File
@@ -788,6 +788,34 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
<tbody id="tb-apks"></tbody>
</table>
<div class="list-pager" id="pager-apks"></div>
<!-- 设备端应用商店:设备上的 Agent 自己拉清单/下载/安装(绕开 MIUI 的 USB 安装拦截) -->
<div class="section-title" style="margin-top:16px">📱 设备端应用商店(Agent 直连安装)</div>
<div class="help">
设备上装的 <b>Agent</b> 自己来拉这份应用清单 → 在手机上选一个 → 自己下载 → 自己调系统安装器安装。
因为安装由设备上的 App 发起,走的是<b>普通应用安装流程</b>,
<b>不会触发 MIUI 针对 adb 安装的「USB 安装」拦截</b>(那正是 <code>adb install</code> 报 USER_RESTRICTED 的原因)。
适合"人就在机器旁边"的场景(本来就要点一次安装确认)。平台批量推送仍用上面的「安装」按钮。
<br><span class="text-muted">接口契约见 <code>doc/DEVICE_AGENT.md</code>;令牌属于凭据,别外传。</span>
</div>
<div class="toolbar" style="gap:10px;flex-wrap:wrap">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0">
<input type="checkbox" id="store-enabled" onchange="toggleAgentStore(this.checked)"> 启用设备端商店
</label>
<input class="form-control" id="store-url" readonly style="width:290px;font-family:monospace" title="Agent 里要填的平台地址">
<input class="form-control" id="store-token" readonly style="width:300px;font-family:monospace" title="Agent 里要填的设备令牌">
<button class="btn" onclick="copyStoreToken()">复制令牌</button>
<button class="btn" onclick="regenStoreToken()">重置令牌</button>
<button class="btn" onclick="loadAgentStore()">刷新记录</button>
<span class="text-muted" id="store-status"></span>
</div>
<table class="table">
<thead><tr>
<th style="width:150px">时间</th><th style="width:90px">设备</th>
<th style="width:200px">应用</th><th style="width:110px">动作</th><th>说明</th>
</tr></thead>
<tbody id="tb-store-logs"><tr><td colspan="5" class="empty">暂无记录(设备还没从商店装过东西)</td></tr></tbody>
</table>
</div>
<!-- 子分栏 5:应用版本管理 -->