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:
@@ -262,3 +262,77 @@ function closeInstallModal(){
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ================== 设备端应用商店(Agent 直连安装) ==================
|
||||
// 设备上的 Agent 拉 /api/device/agent/bootstrap 拿清单 → 自己下载 → 自己调系统安装器。
|
||||
// 因为安装由设备上的 App 发起,走普通应用安装流程,不会触发 MIUI 的「USB 安装」拦截。
|
||||
let _storeToken='';
|
||||
|
||||
async function loadAgentStore(){
|
||||
try{
|
||||
const cfg=await (await fetch('/api/agent-store/config',{headers:_csrfHeaders()})).json();
|
||||
if(cfg&&cfg.ok){
|
||||
_storeToken=cfg.token||'';
|
||||
const en=document.getElementById('store-enabled');
|
||||
if(en)en.checked=!!cfg.enabled;
|
||||
const u=document.getElementById('store-url');
|
||||
if(u)u.value=cfg.base_url_hint||'';
|
||||
const t=document.getElementById('store-token');
|
||||
if(t)t.value=_storeToken||'(未生成,勾选启用后自动生成)';
|
||||
}
|
||||
const d=await (await fetch('/api/agent-store/logs?limit=30',{headers:_csrfHeaders()})).json();
|
||||
renderStoreLogs((d&&d.logs)||[]);
|
||||
}catch(e){ /* 面板没打开时静默 */ }
|
||||
}
|
||||
|
||||
function renderStoreLogs(logs){
|
||||
const tb=document.getElementById('tb-store-logs');
|
||||
if(!tb)return;
|
||||
if(!logs.length){
|
||||
tb.innerHTML='<tr><td colspan="5" class="empty">暂无记录(设备还没从商店装过东西)</td></tr>';
|
||||
return;
|
||||
}
|
||||
const act={download:'⬇ 已下载',install_ok:'✅ 安装成功',install_fail:'❌ 安装失败'};
|
||||
tb.innerHTML=logs.map(l=>{
|
||||
const app=[l.display_name||l.package_name||l.apk_id,
|
||||
l.version_name?('v'+l.version_name):''].filter(Boolean).join(' ');
|
||||
const cls=l.action==='install_fail'?'style="color:#b42318"':(l.action==='install_ok'?'style="color:#067647"':'');
|
||||
return '<tr><td>'+esc(l.created_at||'')+'</td><td>'+esc(l.device_name||l.serial||'-')+'</td>'+
|
||||
'<td title="'+esc(l.package_name||'')+'('+esc(l.apk_id||'')+')">'+esc(_cut(app,30))+'</td>'+
|
||||
'<td '+cls+'>'+esc(act[l.action]||l.action||'')+'</td>'+
|
||||
'<td>'+esc(_cut(l.message||'',60))+'</td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function toggleAgentStore(on){
|
||||
const r=await apiPost('/api/agent-store/config',{enabled:!!on});
|
||||
if(!r)return;
|
||||
if(r.ok){
|
||||
showToast(on?'设备端商店已启用':'设备端商店已停用','success');
|
||||
loadAgentStore();
|
||||
}else{
|
||||
showToast(r.error||'设置失败','error');
|
||||
document.getElementById('store-enabled').checked=!on;
|
||||
}
|
||||
}
|
||||
|
||||
async function regenStoreToken(){
|
||||
if(!confirm('重置设备令牌?\n\n所有已配置该令牌的设备会立即失效,需要在设备端 Agent 里重新填新令牌。'))return;
|
||||
const r=await apiPost('/api/agent-store/config',{regenerate_token:true});
|
||||
if(r&&r.ok){showToast('令牌已重置','success');loadAgentStore();}
|
||||
else if(r)showToast(r.error||'重置失败','error');
|
||||
}
|
||||
|
||||
function copyStoreToken(){
|
||||
if(!_storeToken){showToast('还没有令牌(先启用设备端商店)','error');return;}
|
||||
const done=()=>showToast('令牌已复制','success');
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(_storeToken).then(done).catch(()=>{
|
||||
window.prompt('手动复制令牌:',_storeToken);
|
||||
});
|
||||
}else{
|
||||
window.prompt('手动复制令牌:',_storeToken);
|
||||
}
|
||||
}
|
||||
|
||||
function _cut(s,n){s=String(s==null?'':s);return s.length>n?(s.slice(0,n-1)+'…'):s;}
|
||||
|
||||
Reference in New Issue
Block a user