一、指纹匹配自动认领(可选,默认关)
- 发现设置新增「指纹匹配自动认领」勾选(app_meta: discovery_auto_claim,默认 0)
- 打开后:扫描发现某设备指纹与池中已有记录一致(同一台换了 IP)→ 自动迁移记录到新地址
并同步分组/任务引用,零点击;关闭时维持"识别自动 + 人工点一次确认"
- 默认关的原因:认领会改写分组/任务引用(数据结构变动),交人工确认更稳妥
- 扫描结果与状态行会显示本轮自动认领了几台
二、设备名称在界面上呈现(凡选择/展示设备处都显示名称)
- 新增前端 helper `devText(name, serial)`(base.js):有名称→「名称 · serial」
- 监控页设备表:名称加粗为主、地址作副行(未命名显示橙色提醒);任务概况的覆盖设备
chip 也优先显示名称(tooltip 保留完整地址)
- AI 控制台:目标设备下拉、实时画面设备下拉、目标/运行中提示都带名称(serial→name 映射)
- 任务编辑器「指定设备」下拉、分组编辑的设备勾选列表:带名称
- 后端 `/api/devices` 新增 `items`([{serial,name,model}],`devices` 保持兼容);
`/api/agent/devices` 增加 `name` 字段
- MCP `de_list_devices` 返回 `name`,并在工具说明与 Agent 系统提示里要求"汇报用名称、
调工具用 serial"
文档:API.md(items/name/auto_claim + §6.2.1 名称呈现表)、MCP.md(工具返回)
自测(全通过):自动认领端到端(开开关→扫描→自动迁址 + 名称保留 + 分组/任务引用同步 +
待连接池清理 + 开关默认关且可持久化);名称显示浏览器验证(监控页/覆盖设备 chip/AI 目标与
观看下拉/任务编辑器/分组弹窗/发现设置开关);设备指纹与人工认领回归
182 lines
8.4 KiB
JavaScript
182 lines
8.4 KiB
JavaScript
// 通用基础: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' && 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'};
|
||
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(name==='groups' && typeof loadGroups==='function') loadGroups();
|
||
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');}
|
||
}
|
||
|