// 通用基础:esc/CSRF/权限/API/Toast、Tab 切换与页内子分栏、模态框、行内按钮操作 // ================== 通用工具 ================== const ACTION_LABELS={like:'点赞',comment:'评论',follow:'关注',share:'分享'}; const STATUS_MAP={idle:{t:'未启动',c:'default'},connecting:{t:'连接中',c:'warning'},running:{t:'运行中',c:'success'},done:{t:'完成',c:'primary'},error:{t:'异常',c:'danger'},released:{t:'已释放',c:'default'},failed:{t:'失败',c:'danger'}}; const TARGET_MAP={all:'全部空闲',group:'按分组',serial:'指定设备'}; // 设备显示文案:**名称是设备身份**,地址只是当前连接位置。 // 有名称 → 「名称 · serial」;没名称(历史数据)→ 只显示 serial function devText(name, serial){ const n=String(name||'').trim(); return n ? (n + ' · ' + serial) : String(serial||''); } const APP_NAMES={'com.ss.android.ugc.aweme':'抖音','com.ss.android.ugc.aweme.lite':'抖音极速版','com.smile.gifmaker':'快手','com.kuaishou.nebula':'快手极速版','com.tencent.mm':'微信','com.tencent.mobileqq':'QQ','com.tencent.qqlive':'腾讯视频','com.eg.android.AlipayGphone':'支付宝','com.taobao.taobao':'淘宝','com.tmall.wireless':'天猫','com.jingdong.app.mall':'京东','com.xunmeng.pinduoduo':'拼多多','com.ss.android.article.news':'今日头条','com.zhihu.android':'知乎','com.miui.home':'桌面','com.android.launcher':'桌面','com.android.settings':'设置','com.android.systemui':'系统','com.github.uiautomator':'U2','com.github.uiautomator.test':'U2测试'}; function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));} // ===== CSRF ===== let _csrfToken=''; // 页面加载后调用:获取 CSRF token,非 GET 请求需携带 async function initCsrf(){ try{ const r=await fetch('/api/csrf'); const d=await r.json(); _csrfToken=d.token||''; }catch(e){} } function _csrfHeaders(){ return _csrfToken?{'X-CSRF-Token':_csrfToken}:{}; } // ===== 当前用户与权限 ===== // _me 来自 /api/me:{username, is_admin, perms:[tasks/devices/apks/logs]} // 管理员 perms 恒为全部。无权限的功能入口(tab/按钮)用 data-perm 标记或 _can() 判断隐藏。 let _me={is_admin:false,perms:[],username:''}; async function loadMe(){ try{ const r=await apiGet('/api/me'); if(r&&r.ok)_me=r.user||{is_admin:false,perms:[],username:''}; const un=document.getElementById('me-username'); if(un)un.textContent=_me.username||''; // 隐藏无权限的 tab 和按钮(data-perm="admin" 表示仅管理员) document.querySelectorAll('[data-perm]').forEach(function(el){ const p=el.dataset.perm; const ok=(p==='admin')?_me.is_admin:(_me.perms||[]).includes(p); if(!ok)el.style.display='none'; }); }catch(e){} } function _can(perm){ return _me.is_admin||(_me.perms||[]).includes(perm); } const PERM_LABELS={tasks:'任务管理',devices:'设备控制',apks:'应用管理',logs:'日志查看'}; async function apiGet(url){ const r=await fetch(url); if(r.status===401){window.location='/login';return null;} return r.json(); } async function apiPost(url,body){ const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json',..._csrfHeaders()},body:body?JSON.stringify(body):'{}'}); if(r.status===401){window.location='/login';return null;} return r.json(); } async function apiPut(url,body){ const r=await fetch(url,{method:'PUT',headers:{'Content-Type':'application/json',..._csrfHeaders()},body:body?JSON.stringify(body):'{}'}); if(r.status===401){window.location='/login';return null;} return r.json(); } async function apiDelete(url){ const r=await fetch(url,{method:'DELETE',headers:_csrfHeaders()}); if(r.status===401){window.location='/login';return null;} return r.json(); } function showToast(msg,type){ const t=document.getElementById('toast'); t.className='toast show toast-'+(type||'success'); t.textContent=msg; setTimeout(()=>t.className='toast',3000); } function handleResult(r,callback){ if(!r)return; if(r.ok){showToast(r.msg||'操作成功','success');if(callback)callback();} else{showToast(r.error||'操作失败','error');} } // ================== Tab 切换 ================== let _activeTab='monitor'; let _monitorTimer=null; let _logTimer=null; function showTab(name){ _activeTab=name; document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active')); document.querySelectorAll('.navbar .tab').forEach(t=>t.classList.remove('active')); const panel=document.getElementById('tab-'+name); if(panel)panel.classList.add('active'); const btn=document.querySelector('.navbar .tab[data-tab="'+name+'"]'); if(btn)btn.classList.add('active'); // 停止所有定时器 if(_monitorTimer){clearInterval(_monitorTimer);_monitorTimer=null;} if(_logTimer){clearInterval(_logTimer);_logTimer=null;} if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;} // 按需加载数据 if(name==='monitor'){loadMonitor();_monitorTimer=setInterval(loadMonitor,5000);} if(name==='tasks'){showSubTab('tasks',_activeSubs.tasks);loadTasks();loadCustomActions();} if(name==='tools'){showSubTab('tools',_activeSubs.tools);loadToolsDevices();loadAdbDevices();loadTailscaleDevices();loadApks();} if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);} if(name==='agent'){ showSubTab('agent', _activeSubs.agent||'chat'); // 聊天 / AI 建任务 if(typeof initAgentChat==='function') initAgentChat(); } if(name==='users')loadUsers(); if(name==='system'){showSubTab('system',_activeSubs.system||'backup');} } // ================== 页内子分栏(任务/工具 通用) ================== // 每个带子分栏的 Tab 记住上次选中的子分栏,切走再切回来保持原位 let _activeSubs = {tasks: 'plan', tools: 'clipboard', system: 'backup', agent: 'chat'}; let _discoveryTimer = null; // 设备自动发现 10s 轮询(仅 devpool 子分栏激活时) function showSubTab(tabId, name){ _activeSubs[tabId] = name; const tab = document.getElementById('tab-'+tabId); tab.querySelectorAll('.sub-tab').forEach(b=>b.classList.toggle('active', b.dataset.sub===name)); tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name)); if(tabId==='agent' && name==='taskgen' && typeof initTaskGen==='function') initTaskGen(); if(name==='groups' && typeof loadGroups==='function') loadGroups(); if(name==='apks' && typeof loadAgentStore==='function') loadAgentStore(); if(name==='devpool' && typeof loadDevPool==='function'){ loadDevPool(); if(typeof loadDiscovery==='function'){ loadDiscovery(); if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;} _discoveryTimer=setInterval(loadDiscovery,10000); // 10s 轮询:扫完自动出新结果 } } else if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;} } // ================== 模态框 ================== function showModal(){ document.getElementById('modal-overlay').classList.add('show'); } function closeModal(){ // 先关闭弹窗(最重要的一步,确保一定执行) const ov=document.getElementById('modal-overlay'); if(ov)ov.classList.remove('show'); // 再做清理工作,任何异常都不影响弹窗关闭 try{ stopShotTimer(); _shotSerial=''; const box=document.getElementById('modal-box'); if(box){box.classList.remove('shot-modal');box.classList.remove('wide-modal');} }catch(e){console.warn('closeModal cleanup error:',e);} } // 测试此步骤弹窗关闭(editor.js 的测试按钮用;曾随 STF 清理误删) function closeTestModal(){ const ov=document.getElementById('test-overlay'); if(ov)ov.classList.remove('show'); } // 关闭元素抓取弹窗(不影响任务编辑窗口) function closeElPicker(){ var ov=document.getElementById('el-picker-overlay'); if(ov)ov.classList.remove('show'); // 清理状态 try{ _stepEditor._elData=null; _stepEditor._elSerial=''; }catch(e){} } // ================== 行内按钮操作(停止/执行/启停等,自动 toast + 刷新)================== async function apiAction(url,body){ const r=await apiPost(url,body); if(!r)return; if(r.ok){showToast(r.msg||'操作成功','success');if(_activeTab==='monitor')loadMonitor();if(_activeTab==='tasks')loadTasks();} else{showToast(r.error||'操作失败','error');} } async function scanForeground(){ const r=await apiPost('/api/scan_foreground'); if(!r)return; if(r.ok){showToast('扫描已启动,请等待几秒...','success');loadMonitor();} else{showToast(r.error||'扫描失败','error');} }