refactor: adb远程终端与Tailscale管理从维护页移至工具Tab(维护仅保留STF服务),设备列表共享缓存+复用esc
This commit is contained in:
+27
-18
@@ -289,16 +289,15 @@ function showTab(name){
|
|||||||
if(name==='monitor'){loadMonitor();_monitorTimer=setInterval(loadMonitor,5000);}
|
if(name==='monitor'){loadMonitor();_monitorTimer=setInterval(loadMonitor,5000);}
|
||||||
if(name==='tasks'){showSubTab('tasks',_activeSubs.tasks);loadTasks();loadCustomActions();}
|
if(name==='tasks'){showSubTab('tasks',_activeSubs.tasks);loadTasks();loadCustomActions();}
|
||||||
if(name==='apps'){showSubTab('apps',_activeSubs.apps);loadApks();}
|
if(name==='apps'){showSubTab('apps',_activeSubs.apps);loadApks();}
|
||||||
if(name==='maintenance'){showSubTab('maintenance',_activeSubs.maintenance);loadAdbDevices();loadTailscaleDevices();}
|
if(name==='tools'){showSubTab('tools',_activeSubs.tools);loadToolsDevices();loadAdbDevices();loadTailscaleDevices();}
|
||||||
if(name==='tools'){showSubTab('tools',_activeSubs.tools);loadToolsDevices();}
|
|
||||||
if(name==='groups')loadGroups();
|
if(name==='groups')loadGroups();
|
||||||
if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);}
|
if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);}
|
||||||
if(name==='users')loadUsers();
|
if(name==='users')loadUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== 页内子分栏(任务/应用/维护/工具 通用) ==================
|
// ================== 页内子分栏(任务/应用/工具 通用) ==================
|
||||||
// 每个带子分栏的 Tab 记住上次选中的子分栏,切走再切回来保持原位
|
// 每个带子分栏的 Tab 记住上次选中的子分栏,切走再切回来保持原位
|
||||||
let _activeSubs = {tasks: 'plan', apps: 'apks', maintenance: 'stf', tools: 'clipboard'};
|
let _activeSubs = {tasks: 'plan', apps: 'apks', tools: 'clipboard'};
|
||||||
|
|
||||||
function showSubTab(tabId, name){
|
function showSubTab(tabId, name){
|
||||||
_activeSubs[tabId] = name;
|
_activeSubs[tabId] = name;
|
||||||
@@ -311,15 +310,25 @@ function showSubTab(tabId, name){
|
|||||||
let _clipDevices = []; // [{serial, state}],来源 /api/adb/devices(本地 adb + STF 池)
|
let _clipDevices = []; // [{serial, state}],来源 /api/adb/devices(本地 adb + STF 池)
|
||||||
let _clipSelected = new Set();
|
let _clipSelected = new Set();
|
||||||
|
|
||||||
function _escHtml(s){
|
// 本地 adb 设备列表共享缓存(剪贴板/终端两个面板同源,10 秒内复用避免重复请求;force 强制刷新)
|
||||||
return String(s==null?'':s).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
let _adbDevicesCache = null; // {ts, data}
|
||||||
|
|
||||||
|
async function _fetchAdbDevices(force){
|
||||||
|
const now = Date.now();
|
||||||
|
if(!force && _adbDevicesCache && now - _adbDevicesCache.ts < 10000){
|
||||||
|
return _adbDevicesCache.data;
|
||||||
|
}
|
||||||
|
const r = await apiGet('/api/adb/devices');
|
||||||
|
if(!r || !r.ok) return null;
|
||||||
|
_adbDevicesCache = {ts: now, data: r.devices || []};
|
||||||
|
return _adbDevicesCache.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadToolsDevices(){
|
async function loadToolsDevices(force){
|
||||||
const r = await apiGet('/api/adb/devices');
|
const list = await _fetchAdbDevices(force);
|
||||||
const status = document.getElementById('clip-status');
|
const status = document.getElementById('clip-status');
|
||||||
if(!r||!r.ok){status.textContent='加载失败';showToast('加载设备失败: '+(r&&r.error||''),'error');return;}
|
if(list === null){status.textContent='加载失败';showToast('加载设备失败','error');return;}
|
||||||
_clipDevices = r.devices||[];
|
_clipDevices = list;
|
||||||
const wrap = document.getElementById('clip-devices');
|
const wrap = document.getElementById('clip-devices');
|
||||||
if(!_clipDevices.length){
|
if(!_clipDevices.length){
|
||||||
wrap.innerHTML='<div class="empty" style="padding:20px">无可用设备(本地 adb 或 STF 池均无)</div>';
|
wrap.innerHTML='<div class="empty" style="padding:20px">无可用设备(本地 adb 或 STF 池均无)</div>';
|
||||||
@@ -329,9 +338,9 @@ async function loadToolsDevices(){
|
|||||||
wrap.innerHTML = _clipDevices.map(d=>{
|
wrap.innerHTML = _clipDevices.map(d=>{
|
||||||
const st = d.state==='stf' ? 'STF池' : (d.state==='device' ? '已连接' : d.state);
|
const st = d.state==='stf' ? 'STF池' : (d.state==='device' ? '已连接' : d.state);
|
||||||
const stCls = (d.state==='device'||d.state==='stf') ? 'label-success' : 'label-warning';
|
const stCls = (d.state==='device'||d.state==='stf') ? 'label-success' : 'label-warning';
|
||||||
return '<label data-serial="'+_escHtml(d.serial)+'">'
|
return '<label data-serial="'+esc(d.serial)+'">'
|
||||||
+'<input type="checkbox" value="'+_escHtml(d.serial)+'"'+( _clipSelected.has(d.serial)?' checked':'')+' onchange="toggleClipDevice(this)">'
|
+'<input type="checkbox" value="'+esc(d.serial)+'"'+( _clipSelected.has(d.serial)?' checked':'')+' onchange="toggleClipDevice(this)">'
|
||||||
+'<span style="font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis">'+_escHtml(d.serial)+'</span>'
|
+'<span style="font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis">'+esc(d.serial)+'</span>'
|
||||||
+'<span class="label '+stCls+'">'+st+'</span></label>';
|
+'<span class="label '+stCls+'">'+st+'</span></label>';
|
||||||
}).join('');
|
}).join('');
|
||||||
status.textContent = '共 '+_clipDevices.length+' 台设备';
|
status.textContent = '共 '+_clipDevices.length+' 台设备';
|
||||||
@@ -352,7 +361,7 @@ async function injectClipboard(){
|
|||||||
const r = await apiPost('/api/tools/clipboard/set', {serials:[..._clipSelected], text});
|
const r = await apiPost('/api/tools/clipboard/set', {serials:[..._clipSelected], text});
|
||||||
if(!r||!r.ok){showToast('注入失败: '+(r&&r.error||''),'error');return;}
|
if(!r||!r.ok){showToast('注入失败: '+(r&&r.error||''),'error');return;}
|
||||||
const rows = Object.entries(r.results).map(([s,v])=>
|
const rows = Object.entries(r.results).map(([s,v])=>
|
||||||
'<tr><td style="font-family:monospace">'+_escHtml(s)+'</td><td>'+(v.ok?'<span class="label label-success">成功</span>':'<span class="label label-danger">失败</span> '+_escHtml(v.msg))+'</td></tr>').join('');
|
'<tr><td style="font-family:monospace">'+esc(s)+'</td><td>'+(v.ok?'<span class="label label-success">成功</span>':'<span class="label label-danger">失败</span> '+esc(v.msg))+'</td></tr>').join('');
|
||||||
document.getElementById('clip-result').innerHTML = rows;
|
document.getElementById('clip-result').innerHTML = rows;
|
||||||
document.getElementById('clip-result-wrap').style.display = 'block';
|
document.getElementById('clip-result-wrap').style.display = 'block';
|
||||||
showToast('注入完成:成功 '+r.ok_count+' 台,失败 '+r.fail_count+' 台', r.fail_count?'error':'success');
|
showToast('注入完成:成功 '+r.ok_count+' 台,失败 '+r.fail_count+' 台', r.fail_count?'error':'success');
|
||||||
@@ -2266,15 +2275,15 @@ function _attachDevice(cmd){
|
|||||||
if(_NO_DEVICE_CMDS.indexOf(first)>=0)return cmd;
|
if(_NO_DEVICE_CMDS.indexOf(first)>=0)return cmd;
|
||||||
return '-s '+serial+' '+cmd;
|
return '-s '+serial+' '+cmd;
|
||||||
}
|
}
|
||||||
async function loadAdbDevices(){
|
async function loadAdbDevices(force){
|
||||||
const sel=document.getElementById('adb-device');
|
const sel=document.getElementById('adb-device');
|
||||||
if(!sel)return;
|
if(!sel)return;
|
||||||
const r=await apiGet('/api/adb/devices');
|
const list=await _fetchAdbDevices(force);
|
||||||
if(!r||!r.ok)return;
|
if(list===null)return;
|
||||||
const cur=sel.value;
|
const cur=sel.value;
|
||||||
const stateTxt={device:'在线',offline:'离线',stf:'STF池(未连接)'};
|
const stateTxt={device:'在线',offline:'离线',stf:'STF池(未连接)'};
|
||||||
const opts=['<option value="">(不指定设备)</option>'].concat(
|
const opts=['<option value="">(不指定设备)</option>'].concat(
|
||||||
(r.devices||[]).map(d=>
|
list.map(d=>
|
||||||
'<option value="'+esc(d.serial)+'">'+esc(d.serial)+' ('+(stateTxt[d.state]||d.state)+')</option>'
|
'<option value="'+esc(d.serial)+'">'+esc(d.serial)+' ('+(stateTxt[d.state]||d.state)+')</option>'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -501,75 +501,15 @@ select.form-control{cursor:pointer}
|
|||||||
<!-- ========== Tab 7: 维护(仅管理员) ========== -->
|
<!-- ========== Tab 7: 维护(仅管理员) ========== -->
|
||||||
<div id="tab-maintenance" class="tab-panel">
|
<div id="tab-maintenance" class="tab-panel">
|
||||||
<div class="page-title">维护</div>
|
<div class="page-title">维护</div>
|
||||||
<div class="page-sub">管理员专用:STF 服务、adb 远程终端与 Tailscale 设备管理</div>
|
<div class="page-sub">管理员专用:STF 服务管理(adb 远程终端与 Tailscale 管理已移至"工具")</div>
|
||||||
|
<div class="toolbar">
|
||||||
<div class="sub-tabs">
|
<button class="btn btn-danger" onclick="restartStf()">一键重启 STF 容器</button>
|
||||||
<button class="sub-tab active" data-sub="stf" onclick="showSubTab('maintenance','stf')">STF 服务</button>
|
<span id="stf-restart-status" class="text-muted"></span>
|
||||||
<button class="sub-tab" data-sub="adb" onclick="showSubTab('maintenance','adb')">adb 远程终端</button>
|
|
||||||
<button class="sub-tab" data-sub="ts" onclick="showSubTab('maintenance','ts')">Tailscale 管理</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="help" style="margin-top:8px">
|
||||||
<!-- 子分栏 1:STF 服务 -->
|
通过 SSH 到部署机执行 <code>docker restart</code>(目标与容器名可用环境变量
|
||||||
<div class="sub-panel active" id="maintenance-sub-stf">
|
<code>STF_SSH_TARGET</code> / <code>STF_DOCKER_CONTAINER</code> 配置)。
|
||||||
<div class="toolbar">
|
影响:STF 约 30 秒不可用,期间设备占用状态重置;<b>有任务在运行时会被拒绝执行</b>。
|
||||||
<button class="btn btn-danger" onclick="restartStf()">一键重启 STF 容器</button>
|
|
||||||
<span id="stf-restart-status" class="text-muted"></span>
|
|
||||||
</div>
|
|
||||||
<div class="help" style="margin-top:8px">
|
|
||||||
通过 SSH 到部署机执行 <code>docker restart</code>(目标与容器名可用环境变量
|
|
||||||
<code>STF_SSH_TARGET</code> / <code>STF_DOCKER_CONTAINER</code> 配置)。
|
|
||||||
影响:STF 约 30 秒不可用,期间设备占用状态重置;<b>有任务在运行时会被拒绝执行</b>。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 子分栏 2:adb 远程终端 -->
|
|
||||||
<div class="sub-panel" id="maintenance-sub-adb">
|
|
||||||
<div class="help">选中目标设备后,命令里的 <code>-s <设备></code> 自动附加,直接写
|
|
||||||
<code>shell ls /sdcard</code> 即可。离线设备也可选,用"重连设备"恢复。
|
|
||||||
<b>禁止 kill-server / disconnect</b>(会中断 STF provider 的设备监控)。</div>
|
|
||||||
<div style="display:flex;gap:8px;margin:8px 0;align-items:center">
|
|
||||||
<label style="font-size:12px;color:var(--text-light)">目标设备</label>
|
|
||||||
<select id="adb-device" class="form-control" style="flex:1;max-width:340px;font-family:monospace"></select>
|
|
||||||
<button class="btn btn-xs" onclick="loadAdbDevices()" title="刷新设备列表">刷新</button>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:8px;margin:8px 0">
|
|
||||||
<input type="text" id="adb-cmd-input" class="form-control" style="flex:1;font-family:monospace"
|
|
||||||
placeholder="shell ls /sdcard(选中设备后 -s 自动附加)"
|
|
||||||
onkeydown="if(event.key==='Enter')runAdbCmd()">
|
|
||||||
<button class="btn btn-primary" onclick="runAdbCmd()">执行</button>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap">
|
|
||||||
<button class="btn btn-xs" onclick="fillAdbCmd('adb devices')">adb devices</button>
|
|
||||||
<button class="btn btn-xs" onclick="fillAdbCmd('shell dumpsys window | grep mCurrentFocus')">前台App</button>
|
|
||||||
<button class="btn btn-xs" onclick="fillAdbCmd('shell ls /sdcard')">列文件</button>
|
|
||||||
<button class="btn btn-xs" onclick="fillAdbCmd('shell screencap -p /sdcard/shot.png')">截屏</button>
|
|
||||||
<button class="btn btn-xs" onclick="fillAdbCmd('shell pm list packages -3')">已装应用</button>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap">
|
|
||||||
<button class="btn btn-xs btn-danger" onclick="uninstallStfAgent()" title="卸载 jp.co.cyberagent.stf,STF 会在设备重连时重新推送">卸载 STF agent(卡死时用)</button>
|
|
||||||
<button class="btn btn-xs" onclick="reconnectAdbDevice()" title="重新 adb connect 该设备">重连设备</button>
|
|
||||||
<button class="btn btn-xs" onclick="checkStfAgent()" title="查看 jp.co.cyberagent.stf 是否已安装">检查 agent</button>
|
|
||||||
</div>
|
|
||||||
<pre id="adb-output" class="adb-console">(执行命令后输出显示在这里)</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 子分栏 3:Tailscale 管理 -->
|
|
||||||
<div class="sub-panel" id="maintenance-sub-ts">
|
|
||||||
<div class="help" id="ts-hint"></div>
|
|
||||||
<div class="toolbar" style="margin:8px 0">
|
|
||||||
<button class="btn btn-primary" onclick="loadTailscaleDevices()">刷新设备</button>
|
|
||||||
<button class="btn" onclick="openTailscaleAuthKey()">生成接入 Auth Key</button>
|
|
||||||
<input class="form-control list-search" id="search-tailscale" placeholder="搜索主机名/名称/IP...">
|
|
||||||
<span id="ts-status" class="text-muted" style="margin-left:10px"></span>
|
|
||||||
</div>
|
|
||||||
<table class="table">
|
|
||||||
<thead><tr>
|
|
||||||
<th>主机名</th><th>显示名</th><th>IP</th><th>系统</th><th>在线</th><th>授权</th><th>密钥过期</th><th>操作</th>
|
|
||||||
</tr></thead>
|
|
||||||
<tbody id="tb-tailscale"><tr><td colspan="8" class="empty">点击"刷新设备"加载</td></tr></tbody>
|
|
||||||
</table>
|
|
||||||
<div class="list-pager" id="pager-tailscale"></div>
|
|
||||||
<div class="help">"设置IP"可修改设备 IPv4(会断开当前连接,平台 serial 随之变化,需同步 STF 设备池);"密钥不过期"= 关闭设备密钥验证,设备不会被定期踢下线;auth key 生成后只显示一次,请立即复制。</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -580,12 +520,14 @@ select.form-control{cursor:pointer}
|
|||||||
|
|
||||||
<div class="sub-tabs">
|
<div class="sub-tabs">
|
||||||
<button class="sub-tab active" data-sub="clipboard" onclick="showSubTab('tools','clipboard')">剪贴板注入</button>
|
<button class="sub-tab active" data-sub="clipboard" onclick="showSubTab('tools','clipboard')">剪贴板注入</button>
|
||||||
|
<button class="sub-tab" data-sub="adb" onclick="showSubTab('tools','adb')">adb 远程终端</button>
|
||||||
|
<button class="sub-tab" data-sub="ts" onclick="showSubTab('tools','ts')">Tailscale 管理</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 子分栏 1:剪贴板注入 -->
|
<!-- 子分栏 1:剪贴板注入 -->
|
||||||
<div class="sub-panel active" id="tools-sub-clipboard">
|
<div class="sub-panel active" id="tools-sub-clipboard">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="btn" onclick="loadToolsDevices()">刷新设备</button>
|
<button class="btn" onclick="loadToolsDevices(true)">刷新设备</button>
|
||||||
<span id="clip-status" class="text-muted"></span>
|
<span id="clip-status" class="text-muted"></span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:20px;flex-wrap:wrap">
|
<div style="display:flex;gap:20px;flex-wrap:wrap">
|
||||||
@@ -618,6 +560,56 @@ select.form-control{cursor:pointer}
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 子分栏 2:adb 远程终端 -->
|
||||||
|
<div class="sub-panel" id="tools-sub-adb">
|
||||||
|
<div class="help">选中目标设备后,命令里的 <code>-s <设备></code> 自动附加,直接写
|
||||||
|
<code>shell ls /sdcard</code> 即可。离线设备也可选,用"重连设备"恢复。
|
||||||
|
<b>禁止 kill-server / disconnect</b>(会中断 STF provider 的设备监控)。</div>
|
||||||
|
<div style="display:flex;gap:8px;margin:8px 0;align-items:center">
|
||||||
|
<label style="font-size:12px;color:var(--text-light)">目标设备</label>
|
||||||
|
<select id="adb-device" class="form-control" style="flex:1;max-width:340px;font-family:monospace"></select>
|
||||||
|
<button class="btn btn-xs" onclick="loadAdbDevices(true)" title="刷新设备列表">刷新</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;margin:8px 0">
|
||||||
|
<input type="text" id="adb-cmd-input" class="form-control" style="flex:1;font-family:monospace"
|
||||||
|
placeholder="shell ls /sdcard(选中设备后 -s 自动附加)"
|
||||||
|
onkeydown="if(event.key==='Enter')runAdbCmd()">
|
||||||
|
<button class="btn btn-primary" onclick="runAdbCmd()">执行</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-xs" onclick="fillAdbCmd('adb devices')">adb devices</button>
|
||||||
|
<button class="btn btn-xs" onclick="fillAdbCmd('shell dumpsys window | grep mCurrentFocus')">前台App</button>
|
||||||
|
<button class="btn btn-xs" onclick="fillAdbCmd('shell ls /sdcard')">列文件</button>
|
||||||
|
<button class="btn btn-xs" onclick="fillAdbCmd('shell screencap -p /sdcard/shot.png')">截屏</button>
|
||||||
|
<button class="btn btn-xs" onclick="fillAdbCmd('shell pm list packages -3')">已装应用</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-xs btn-danger" onclick="uninstallStfAgent()" title="卸载 jp.co.cyberagent.stf,STF 会在设备重连时重新推送">卸载 STF agent(卡死时用)</button>
|
||||||
|
<button class="btn btn-xs" onclick="reconnectAdbDevice()" title="重新 adb connect 该设备">重连设备</button>
|
||||||
|
<button class="btn btn-xs" onclick="checkStfAgent()" title="查看 jp.co.cyberagent.stf 是否已安装">检查 agent</button>
|
||||||
|
</div>
|
||||||
|
<pre id="adb-output" class="adb-console">(执行命令后输出显示在这里)</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 子分栏 3:Tailscale 管理 -->
|
||||||
|
<div class="sub-panel" id="tools-sub-ts">
|
||||||
|
<div class="help" id="ts-hint"></div>
|
||||||
|
<div class="toolbar" style="margin:8px 0">
|
||||||
|
<button class="btn btn-primary" onclick="loadTailscaleDevices()">刷新设备</button>
|
||||||
|
<button class="btn" onclick="openTailscaleAuthKey()">生成接入 Auth Key</button>
|
||||||
|
<input class="form-control list-search" id="search-tailscale" placeholder="搜索主机名/名称/IP...">
|
||||||
|
<span id="ts-status" class="text-muted" style="margin-left:10px"></span>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<thead><tr>
|
||||||
|
<th>主机名</th><th>显示名</th><th>IP</th><th>系统</th><th>在线</th><th>授权</th><th>密钥过期</th><th>操作</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="tb-tailscale"><tr><td colspan="8" class="empty">点击"刷新设备"加载</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="list-pager" id="pager-tailscale"></div>
|
||||||
|
<div class="help">"设置IP"可修改设备 IPv4(会断开当前连接,平台 serial 随之变化,需同步 STF 设备池);"密钥不过期"= 关闭设备密钥验证,设备不会被定期踢下线;auth key 生成后只显示一次,请立即复制。</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- /.content -->
|
</div><!-- /.content -->
|
||||||
|
|||||||
Reference in New Issue
Block a user