// 工具 Tab-应用管理:APK 上传/安装/删除 + 设备已装应用 // ================== Tab: 应用 ================== let _apksCache=[]; let _installTimer=null; let _devAppsData=[]; function fmtSize(b){ if(!b)return '0 B'; if(b<1024)return b+' B'; if(b<1048576)return (b/1024).toFixed(1)+' KB'; return (b/1048576).toFixed(1)+' MB'; } async function loadApks(){ const r=await apiGet('/api/apks');if(!r||!r.ok)return; const apks=r.apks||[]; _apksCache=apks; document.getElementById('apk-hint').textContent='共 '+apks.length+' 个应用'; setListPager('apks',{ data:()=>apks, sortValue:(a,k)=>{ if(k==='size')return a.size||0; if(k==='upload_time')return a.upload_time||''; if(k==='package_name')return a.package_name||''; if(k==='version_name')return a.version_name||''; return a.display_name||''; }, filter:(a,kw)=>((a.display_name||'').toLowerCase().includes(kw)||(a.package_name||'').toLowerCase().includes(kw)), render:renderApkRow, tbody:'tb-apks',pager:'pager-apks',pageSize:10, empty:'暂无APK,点击"上传APK"上传' }); applyListPager('apks'); initListSearch('apks','search-apks'); await fillAppsDeviceList(); } function renderApkRow(a){ return ''+ ''+esc(a.display_name||'(未命名)')+''+ ''+esc(a.package_name||'-')+''+ ''+(a.version_name?esc(a.version_name):'-')+''+ ''+fmtSize(a.size)+''+ ''+esc(a.upload_time)+''+ ''+ ' '+ ''+ ''+ ''; } // 填充"设备已装应用"的设备下拉(保留当前选择) async function fillAppsDeviceList(){ const sr=await apiGet('/api/status');if(!sr||!sr.ok)return; const sel=document.getElementById('apps-device'); if(!sel)return; const cur=sel.value; const devices=(sr.devices||[]).filter(d=>d.present); sel.innerHTML=''+devices.map(d=> '' ).join(''); if(cur)sel.value=cur; } // 查看指定设备已安装应用 async function loadDeviceApps(){ const sel=document.getElementById('apps-device'); const serial=sel.value; if(!serial){showToast('请先选择设备','error');return;} const hint=document.getElementById('apps-hint'); const tb=document.getElementById('tb-device-apps'); hint.textContent='加载中...'; const r=await apiGet('/api/devices/'+encodeURIComponent(serial)+'/apps'); hint.textContent=''; if(!r||!r.ok){ tb.innerHTML=''+(r&&r.error?esc(r.error):'获取失败')+''; showToast('获取失败','error');return; } const apps=r.apps||[]; if(!apps.length){tb.innerHTML='无应用';return;} const userApps=apps.filter(a=>/^\/data\//.test(a.path||'')).length; hint.textContent='共 '+apps.length+' 个应用(用户安装 '+userApps+')'; _devAppsData=apps; setListPager('devapps',{ data:()=>_devAppsData||[], filter:(a,kw)=>((a.label||a.package||'').toLowerCase().includes(kw)||(a.package||'').toLowerCase().includes(kw)||(a.path||'').toLowerCase().includes(kw)), sortValue:(a,k)=>{ if(k==='version_code')return parseInt(a.version_code)||0; if(k==='version_name')return a.version_name||''; if(k==='label')return (a.label||a.package||''); return a[k]||''; }, render:renderDevAppRow, tbody:'tb-device-apps',pager:'pager-devapps',pageSize:20, empty:'无应用' }); applyListPager('devapps'); initListSearch('devapps','search-devapps'); } function renderDevAppRow(a){ return ''+ ''+esc(a.label||a.package)+''+(a.label?' '+esc(a.package)+'':'')+''+ ''+(a.version_name?esc(a.version_name):'-')+''+ ''+(a.version_code!=null?esc(a.version_code):'-')+''+ ''+esc(a.path||'-')+''+ ''; } async function uploadApk(input){ if(!input.files||!input.files[0])return; const file=input.files[0]; if(!file.name.toLowerCase().endsWith('.apk')){ showToast('请选择 .apk 文件','error');return; } const hint=document.getElementById('apk-hint'); const fd=new FormData(); fd.append('file',file); // 用 XMLHttpRequest 才能拿到上传进度(fetch 不支持上传进度事件) const xhr=new XMLHttpRequest(); xhr.open('POST','/api/apks/upload'); if(_csrfToken)xhr.setRequestHeader('X-CSRF-Token',_csrfToken); xhr.upload.onprogress=function(e){ if(e.lengthComputable){ const pct=Math.round(e.loaded*100/e.total); const loaded=(e.loaded/1048576).toFixed(1); const total=(e.total/1048576).toFixed(1); hint.innerHTML='正在上传 '+esc(file.name)+' '+pct+'% ('+loaded+'/'+total+' MB)'+ '
'; } }; xhr.onload=function(){ if(xhr.status===401){window.location='/login';return;} try{ const data=JSON.parse(xhr.responseText); if(data.ok){ showToast(data.msg||'上传成功','success'); loadApks(); }else{ showToast(data.error||'上传失败','error'); hint.textContent=''; } }catch(e){ showToast('解析响应失败','error'); hint.textContent=''; } }; xhr.onerror=function(){ showToast('上传失败:网络错误','error'); hint.textContent=''; }; xhr.send(fd); input.value=''; } async function deleteApk(id){ if(!confirm('确认删除此APK文件?'))return; const r=await apiDelete('/api/apks/'+id); handleResult(r,loadApks); } async function openInstallModal(apkId){ const apk=_apksCache.find(a=>a.id===apkId); const apkName=apk?(apk.display_name||'APK'):'APK'; const r=await apiGet('/api/apks/install/devices'); const online=(r&&r.ok)?(r.devices||[]):[]; if(!online.length){ showToast('没有可用设备(设备池或本机 adb 均无)','error');return; } const body= '
'+ '安装应用:'+esc(apkName)+''+ ' 运行中设备将自动跳过'+ '
'+ '
'+ ' '+ ' '+ ''+ '
'+ '
'+ online.map(d=>{ const isUsb=d.source==='usb'; const st=''+(isUsb?'本地USB':(d.source==='pool'?'设备池':'adb'))+''; // serial 唯一,显示 IP + 型号便于区分同型号设备 const label=(d.serial||'').replace(/:5555$/,'')+' · '+(d.model||(isUsb?'USB 有线设备':'设备')); return ''; }).join('')+ '
'; document.getElementById('modal-title').textContent='安装应用到设备'; document.getElementById('modal-body').innerHTML=body; document.getElementById('modal-footer').innerHTML= ''+ ''; document.getElementById('modal-overlay').classList.add('show'); } function selectIdleForInstall(){ document.querySelectorAll('#install-dev-list label').forEach(lbl=>{ const cb=lbl.querySelector('input[type=checkbox]'); const isRunning=lbl.querySelector('.label-success'); cb.checked=!isRunning; }); } async function startInstall(apkId){ const checked=document.querySelectorAll('#install-dev-list input[type=checkbox]:checked'); const serials=Array.from(checked).map(c=>c.value); if(!serials.length){ showToast('请至少选择一台设备','error');return; } const r=await apiPost('/api/apks/install',{apk_id:apkId,serials:serials}); if(!r||!r.ok){showToast(r?r.error:'请求失败','error');return;} showToast(r.msg||'安装已启动','success'); showInstallProgress(); } function showInstallProgress(){ document.getElementById('modal-title').textContent='安装进度'; document.getElementById('modal-body').innerHTML= '

加载中...

'; document.getElementById('modal-footer').innerHTML= ''; pollInstallStatus(); if(_installTimer)clearInterval(_installTimer); _installTimer=setInterval(pollInstallStatus,3000); } async function pollInstallStatus(){ const r=await apiGet('/api/apks/install/status'); if(!r||!r.ok)return; const st=r.status; if(!st){ document.getElementById('install-progress').innerHTML='

无安装任务

'; if(_installTimer){clearInterval(_installTimer);_installTimer=null;} return; } const SS={pending:{t:'等待',c:'default'},installing:{t:'安装中',c:'warning'},success:{t:'成功',c:'success'},failed:{t:'失败',c:'danger'},skipped:{t:'跳过',c:'default'}}; const items=st.items||{}; const itemRows=Object.entries(items).map(([serial,info])=>{ const s=SS[info.status]||{t:info.status,c:'default'}; return ''+esc(info.name||serial)+''+ ''+esc(serial)+''+ ''+s.t+''+ ''+esc(info.msg||'')+''; }).join(''); const pct=st.total>0?Math.round((st.success+st.failed+st.skipped)*100/st.total):0; const summary= '
'+ '
'+ '应用:'+esc(st.apk_name)+''+ '成功 '+st.success+''+ '失败 '+st.failed+''+ '跳过 '+st.skipped+''+ '安装中 '+st.installing+''+ '等待 '+st.pending+''+ '
'+ '
'+ '
'; document.getElementById('install-progress').innerHTML= summary+''+itemRows+'
设备Serial状态详情
'; if(st.finished){ if(_installTimer){clearInterval(_installTimer);_installTimer=null;} document.getElementById('install-close-btn').style.display=''; showToast('安装完成:成功'+st.success+' 失败'+st.failed+' 跳过'+st.skipped,'success'); } } function closeInstallModal(){ if(_installTimer){clearInterval(_installTimer);_installTimer=null;} closeModal(); } // ================== 设备端应用商店(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='暂无记录(设备还没从商店装过东西)'; 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 ''+esc(l.created_at||'')+''+esc(l.device_name||l.serial||'-')+''+ ''+esc(_cut(app,30))+''+ ''+esc(act[l.action]||l.action||'')+''+ ''+esc(_cut(l.message||'',60))+''; }).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;}