阶段3: 摘除 STF——删除 stf_client/stf_device_mgmt 与全部引用(STF 容器重启/设备管理/强制释放占用/agent 按钮/占用列),调度与设备管理完全基于本地设备池+adb;错误类迁入 device_worker,create_worker 去 stf 参数
This commit is contained in:
@@ -152,7 +152,7 @@ async function openInstallModal(apkId){
|
||||
const r=await apiGet('/api/apks/install/devices');
|
||||
const online=(r&&r.ok)?(r.devices||[]):[];
|
||||
if(!online.length){
|
||||
showToast('没有可用设备(STF 池或本机 adb 均无)','error');return;
|
||||
showToast('没有可用设备(设备池或本机 adb 均无)','error');return;
|
||||
}
|
||||
const body=
|
||||
'<div style="margin-bottom:12px">'+
|
||||
|
||||
@@ -1055,7 +1055,7 @@ async function saveTask(jobId){
|
||||
catch(e){showToast('参数 JSON 格式错误','error');return;}
|
||||
}
|
||||
|
||||
// 离线自动跳过(serial/group 模式):任务触发时跳过 STF 池中不在线的设备
|
||||
// 离线自动跳过(serial/group 模式):任务触发时跳过设备池中不在线的设备
|
||||
const skipEl=document.getElementById('f-skip_offline');
|
||||
params.skip_offline = skipEl?skipEl.checked:true;
|
||||
// 抢占设备:触发时停止目标设备上的其他任务,接管执行
|
||||
|
||||
@@ -9,8 +9,6 @@ function _sortValue(dev,key){
|
||||
if(key==='progress'){
|
||||
const p=dev.progress||{};
|
||||
v=(p.done||0)+(((p.total||0)>0?(p.done||0)/(p.total):0)*10000);
|
||||
}else if(key==='stf_occupied'){
|
||||
v=dev.stf_occupied?1:0;
|
||||
}else if(key==='worker_status'){
|
||||
// 按状态优先级排序:running>connecting>idle>done>error>released
|
||||
const order={running:0,connecting:1,idle:2,done:3,released:4,error:5,failed:6};
|
||||
@@ -231,14 +229,12 @@ async function loadMonitor(){
|
||||
const running=devices.filter(x=>x.worker_status==='running').length;
|
||||
const idle=devices.filter(x=>x.worker_status==='idle').length;
|
||||
const error=devices.filter(x=>['error','failed'].includes(x.worker_status)).length;
|
||||
const occupied=devices.filter(x=>x.stf_occupied).length;
|
||||
const total=devices.length;
|
||||
const online=devices.filter(x=>x.present).length;
|
||||
const cards=[
|
||||
{n:total,l:'设备总数',c:'#3b82f6'},
|
||||
{n:online,l:'在线',c:'#10b981'},
|
||||
{n:running,l:'运行中',c:'#10b981'},
|
||||
{n:occupied,l:'STF占用',c:'#f59e0b'},
|
||||
{n:idle,l:'空闲',c:'#94a3b8'},
|
||||
{n:error,l:'异常',c:'#ef4444'},
|
||||
];
|
||||
@@ -318,7 +314,6 @@ function renderDevices(){
|
||||
initListSearch('devices','search-devices');
|
||||
}
|
||||
function renderDeviceRow(dev){
|
||||
const stf=dev.stf_occupied?'<span class="label label-success">占用</span>':'<span class="label label-default">空闲</span>';
|
||||
const offline=dev.present?'':' <span class="label label-danger">离线</span>';
|
||||
const st=STATUS_MAP[dev.worker_status]||{t:dev.worker_status,c:'default'};
|
||||
const wb='<span class="label label-'+st.c+'">'+st.t+'</span>';
|
||||
@@ -355,7 +350,7 @@ function renderDeviceRow(dev){
|
||||
const checked=_selectedSerials.has(dev.serial)?'checked':'';
|
||||
const rowCls=_selectedSerials.has(dev.serial)?'row-checked':'';
|
||||
const cb='<input type="checkbox" '+checked+' onclick="toggleDevice(\''+esc(dev.serial)+'\',this.checked)">';
|
||||
return '<tr class="'+rowCls+'"><td class="col-cb">'+cb+'</td><td>'+devLabel+'</td><td>'+esc(dev.model)+'</td><td>'+stf+offline+'</td><td>'+wb+'</td><td>'+fg+'</td><td>'+prog+'</td><td>'+tname+retry+endInfo+'</td><td class="text-muted">'+esc(dev.current_action||'-')+warnInfo+'</td><td class="err-text" title="'+esc(dev.last_error||'')+'">'+esc(dev.last_error||'-')+'</td><td>'+btns+'</td></tr>';
|
||||
return '<tr class="'+rowCls+'"><td class="col-cb">'+cb+'</td><td>'+devLabel+'</td><td>'+esc(dev.model)+'</td><td>'+offline+'</td><td>'+wb+'</td><td>'+fg+'</td><td>'+prog+'</td><td>'+tname+retry+endInfo+'</td><td class="text-muted">'+esc(dev.current_action||'-')+warnInfo+'</td><td class="err-text" title="'+esc(dev.last_error||'')+'">'+esc(dev.last_error||'-')+'</td><td>'+btns+'</td></tr>';
|
||||
}
|
||||
|
||||
async function loadJobsForMonitor(){
|
||||
|
||||
+8
-152
@@ -1,6 +1,6 @@
|
||||
// 工具 Tab:剪贴板注入/应用版本/STF设备管理 + 维护(重启STF) + adb终端 + Tailscale管理
|
||||
// 工具 Tab:剪贴板注入/应用版本/设备池管理 + adb终端 + Tailscale管理 + 远程看屏
|
||||
// ================== 工具:剪贴板注入 ==================
|
||||
let _clipDevices = []; // [{serial, state}],来源 /api/adb/devices(本地 adb + STF 池)
|
||||
let _clipDevices = []; // [{serial, state}],来源 /api/adb/devices(本地 adb + 设备池)
|
||||
let _clipSelected = new Set();
|
||||
|
||||
// 本地 adb 设备列表共享缓存(剪贴板/终端两个面板同源,10 秒内复用避免重复请求;force 强制刷新)
|
||||
@@ -24,13 +24,13 @@ async function loadToolsDevices(force){
|
||||
_clipDevices = list;
|
||||
const wrap = document.getElementById('clip-devices');
|
||||
if(!_clipDevices.length){
|
||||
wrap.innerHTML='<div class="empty" style="padding:20px">无可用设备(本地 adb 或 STF 池均无)</div>';
|
||||
wrap.innerHTML='<div class="empty" style="padding:20px">无可用设备(本地 adb 或设备池均无)</div>';
|
||||
status.textContent='';
|
||||
return;
|
||||
}
|
||||
wrap.innerHTML = _clipDevices.map(d=>{
|
||||
const st = d.state==='stf' ? 'STF池' : (d.state==='device' ? '已连接' : (d.state==='stf_not_ready' ? 'STF未就绪' : d.state));
|
||||
const stCls = (d.state==='device'||d.state==='stf') ? 'label-success' : 'label-warning';
|
||||
const st = d.state==='device' ? '已连接' : (d.state==='pool' ? '设备池' : d.state);
|
||||
const stCls = d.state==='device' ? 'label-success' : 'label-warning';
|
||||
return '<label data-serial="'+esc(d.serial)+'">'
|
||||
+'<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">'+esc(d.serial)+'</span>'
|
||||
@@ -87,119 +87,6 @@ function renderAppVerRow(x){
|
||||
'<td><span class="label label-default">未安装</span></td><td>-</td><td>-</td></tr>';
|
||||
}
|
||||
|
||||
// ================== 工具:STF 设备管理 ==================
|
||||
let _stfDevRows = []; // [{ip, inCfg, state}]
|
||||
|
||||
async function loadStfDevMgmt(){
|
||||
const status = document.getElementById('stfdev-status');
|
||||
const r = await apiGet('/api/stf/devmgmt/status');
|
||||
if(!r || !r.ok){status.textContent='加载失败';showToast('加载失败: '+(r&&r.error||''),'error');return;}
|
||||
const configured = r.configured || []; // 脚本 DEVICES 里的 IP(裸 IP)
|
||||
const connected = {}; // 裸 IP -> state(220 adb devices 的 serial 带 :5555,归一化)
|
||||
(r.connected || []).forEach(d=>{
|
||||
const s = d.serial;
|
||||
connected[s.endsWith(':5555') ? s.slice(0,-5) : s] = d.state;
|
||||
});
|
||||
_stfDevRows = Array.from(new Set(configured.concat(Object.keys(connected)))).map(ip=>({
|
||||
ip, inCfg: configured.includes(ip), state: connected[ip]||''
|
||||
}));
|
||||
setListPager('stfdev',{
|
||||
data:()=>_stfDevRows,
|
||||
filter:(x,kw)=>x.ip.toLowerCase().includes(kw),
|
||||
sortValue:(x,k)=>{
|
||||
if(k==='cfg')return x.inCfg?1:0;
|
||||
if(k==='state'){ const o={device:0,offline:1,unauthorized:2,'':9}; return (o[x.state]!==undefined)?o[x.state]:9; }
|
||||
return x.ip||'';
|
||||
},
|
||||
render:renderStfDevRow,
|
||||
tbody:'tb-stfdev',pager:'pager-stfdev',pageSize:10,
|
||||
empty:'<tr><td colspan="4" class="empty">无设备(脚本未配置、220 adb 也无连接)</td></tr>'
|
||||
});
|
||||
applyListPager('stfdev');
|
||||
initListSearch('stfdev','search-stfdev');
|
||||
status.textContent = '脚本配置 '+configured.length+' 台,已连接 '+Object.keys(connected).length+' 台';
|
||||
}
|
||||
function renderStfDevRow(x){
|
||||
const stHtml = x.state==='device'
|
||||
? '<span class="label label-success">已连接</span>'
|
||||
: (x.state ? '<span class="label label-warning">'+esc(x.state)+'</span>' : '<span class="label label-default">未连接</span>');
|
||||
return '<tr><td style="font-family:monospace">'+esc(x.ip)+'</td>'+
|
||||
'<td>'+(x.inCfg?'<span class="label label-info">脚本中</span>':'<span class="label label-default">未配置</span>')+'</td>'+
|
||||
'<td>'+stHtml+'</td>'+
|
||||
'<td style="white-space:nowrap">'+
|
||||
'<button class="btn btn-xs btn-danger" onclick="removeStfDev(\''+esc(x.ip)+'\')" title="断开+删脚本">移除</button> '+
|
||||
'<button class="btn btn-xs btn-danger" onclick="deleteStfDev(\''+esc(x.ip)+'\')" title="断开+删脚本+清 STF 池记录(设备从池中彻底消失)">彻底删除</button>'+
|
||||
'</td></tr>';
|
||||
}
|
||||
|
||||
// 一键重连:手动运行 220 的 connect_devices.sh,补连所有配置设备
|
||||
async function reconnectStfDevices(){
|
||||
if(!confirm('立即运行 220 上的 connect_devices.sh?\n\n会补连脚本里所有未连接的设备(在线设备跳过)。'))return;
|
||||
const r=await apiPost('/api/stf/devmgmt/reconnect',{});
|
||||
if(!r)return;
|
||||
const box=document.getElementById('stfdev-reconnect-out');
|
||||
if(r.ok){
|
||||
box.style.display='block';
|
||||
box.textContent=r.output||'执行完成';
|
||||
showToast('重连完成','success');
|
||||
loadStfDevMgmt();
|
||||
}else{
|
||||
showToast('重连失败: '+(r.error||''),'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 彻底删除:脚本移除 + 断开 + 清 STF 池记录(设备从池中彻底消失)
|
||||
async function deleteStfDev(ip){
|
||||
if(!confirm('彻底删除 '+ip+'?\n\n1. 从 connect_devices.sh 移除\n2. 断开 220 的 adb 连接\n3. 调用 STF API 清除设备记录(设备从设备池彻底消失,需重新添加才会回来)'))return;
|
||||
const r=await apiPost('/api/stf/devmgmt/delete',{ip:ip});
|
||||
if(!r)return;
|
||||
if(r.ok){
|
||||
showToast((r.msgs||[]).join(';'),'success');
|
||||
loadStfDevMgmt();
|
||||
}else showToast('删除失败: '+(r.error||''),'error');
|
||||
}
|
||||
|
||||
async function addStfDev(){
|
||||
const ip = document.getElementById('stfdev-ip').value.trim();
|
||||
if(!ip){showToast('请输入设备 IP','error');return;}
|
||||
const r = await apiPost('/api/stf/devmgmt/add', {ip:ip});
|
||||
if(!r)return;
|
||||
if(r.ok){
|
||||
showToast((r.msgs||[]).join(';'),'success');
|
||||
document.getElementById('stfdev-ip').value='';
|
||||
loadStfDevMgmt();
|
||||
}else showToast('添加失败: '+(r.error||''),'error');
|
||||
}
|
||||
|
||||
async function removeStfDev(ip){
|
||||
if(!confirm('确定移除 '+ip+'?\n\n将从 connect_devices.sh 删除并断开 220 的 adb 连接,\n设备会从 OpenSTF 设备池下线(任务不再分配到它)。'))return;
|
||||
const r = await apiPost('/api/stf/devmgmt/remove', {ip:ip});
|
||||
if(!r)return;
|
||||
if(r.ok){
|
||||
showToast((r.msgs||[]).join(';'),'success');
|
||||
loadStfDevMgmt();
|
||||
}else showToast('移除失败: '+(r.error||''),'error');
|
||||
}
|
||||
|
||||
async function injectClipboard(){
|
||||
const text = document.getElementById('clip-text').value.trim();
|
||||
if(!text){showToast('请输入要注入的文字','error');return;}
|
||||
if(!_clipSelected.size){showToast('请先勾选目标设备','error');return;}
|
||||
const btn = document.querySelector('#tools-sub-clipboard .btn-primary');
|
||||
btn.disabled = true; const old = btn.textContent; btn.textContent = '注入中...';
|
||||
try{
|
||||
const r = await apiPost('/api/tools/clipboard/set', {serials:[..._clipSelected], text});
|
||||
if(!r||!r.ok){showToast('注入失败: '+(r&&r.error||''),'error');return;}
|
||||
const rows = Object.entries(r.results).map(([s,v])=>
|
||||
'<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-wrap').style.display = 'block';
|
||||
showToast('注入完成:成功 '+r.ok_count+' 台,失败 '+r.fail_count+' 台', r.fail_count?'error':'success');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = old;
|
||||
}
|
||||
}
|
||||
|
||||
// ================== Tab: 维护(仅管理员) ==================
|
||||
// 服务器类命令(不需要 -s 目标设备)
|
||||
const _NO_DEVICE_CMDS=['devices','version','help','start-server','connect','kill-server','disconnect'];
|
||||
@@ -222,7 +109,7 @@ async function loadAdbDevices(force){
|
||||
const list=await _fetchAdbDevices(force);
|
||||
if(list===null)return;
|
||||
const cur=sel.value;
|
||||
const stateTxt={device:'在线',offline:'离线',stf:'STF池(未连接)',stf_not_ready:'STF未就绪'};
|
||||
const stateTxt={device:'在线',offline:'离线',pool:'设备池(未连接)'};
|
||||
const opts=['<option value="">(不指定设备)</option>'].concat(
|
||||
list.map(d=>
|
||||
'<option value="'+esc(d.serial)+'">'+esc(d.serial)+' ('+(stateTxt[d.state]||d.state)+')</option>'
|
||||
@@ -251,21 +138,6 @@ async function runAdbCmd(){
|
||||
cmd=_attachDevice(cmd);
|
||||
await _runAdbFull('adb '+cmd);
|
||||
}
|
||||
// 修复类命令:目标为 IP:port 时先 adb connect(已连接自动跳过),
|
||||
// 否则设备不在本机 adb server 里会报 "device not found"(STF 未就绪的设备尤其如此)。
|
||||
async function _runAdbRepair(full){
|
||||
const serial=_selectedSerial();
|
||||
if(serial && serial.indexOf(':')>=0){
|
||||
await _runAdbFull('adb connect '+serial);
|
||||
}
|
||||
await _runAdbFull(full);
|
||||
}
|
||||
function uninstallStfAgent(){
|
||||
const serial=_selectedSerial();
|
||||
if(!serial){showToast('请先选择目标设备','error');return;}
|
||||
if(!confirm('确定卸载 '+serial+' 上的 STF agent(jp.co.cyberagent.stf)?\n\n卸载后 STF 会在设备重连时自动重新推送安装。'))return;
|
||||
_runAdbRepair('adb -s '+serial+' uninstall jp.co.cyberagent.stf');
|
||||
}
|
||||
function reconnectAdbDevice(){
|
||||
const serial=_selectedSerial();
|
||||
if(!serial){showToast('请先选择目标设备','error');return;}
|
||||
@@ -276,22 +148,6 @@ function checkStfAgent(){
|
||||
if(!serial){showToast('请先选择目标设备','error');return;}
|
||||
_runAdbRepair('adb -s '+serial+' shell pm list packages | grep cyberagent');
|
||||
}
|
||||
async function restartStf(){
|
||||
if(!confirm('确定重启 STF 容器?\n\n影响:STF 约 30 秒不可用,期间所有设备占用状态重置,正在执行的任务会受影响。\n\n确认继续吗?'))return;
|
||||
const st=document.getElementById('stf-restart-status');
|
||||
st.textContent='正在重启 STF 容器(SSH 执行中,最长 60s)...';
|
||||
const r=await apiPost('/api/stf/restart',{});
|
||||
if(!r)return;
|
||||
if(r.ok){st.textContent='✅ '+r.msg+(r.detail?' ['+r.detail+']':'');showToast(r.msg,'success');}
|
||||
else{st.textContent='❌ '+r.error;showToast(r.error,'error');}
|
||||
}
|
||||
|
||||
// 测试此步骤弹窗关闭
|
||||
function closeTestModal(){
|
||||
const ov=document.getElementById('test-overlay');
|
||||
if(ov)ov.classList.remove('show');
|
||||
}
|
||||
|
||||
// ================== Tab: 维护 - Tailscale 管理 ==================
|
||||
// 与其他表格共用 setListPager/applyListPager 统一逻辑(搜索 + 分页)
|
||||
let _tsDevices=[];
|
||||
@@ -358,7 +214,7 @@ function renameTailscaleDevice(id, current){
|
||||
});
|
||||
}
|
||||
function setTsIp(id, current){
|
||||
const ip=prompt('设置新的 Tailscale IPv4 地址(如 100.100.10.16):\n\n⚠ 改 IP 会断开设备当前 tailscale 连接,且平台设备池(STF serial)随之变化,改完记得同步 STF 设备池/分组/任务目标。', (current||'').split(':')[0]||'');
|
||||
const ip=prompt('设置新的 Tailscale IPv4 地址(如 100.100.10.16):\n\n⚠ 改 IP 会断开设备当前 tailscale 连接,且平台设备池(serial)随之变化,改完记得同步设备池/分组/任务目标。', (current||'').split(':')[0]||'');
|
||||
if(ip===null)return;
|
||||
const v=ip.trim();
|
||||
if(!v){showToast('IP 不能为空','error');return;}
|
||||
@@ -435,7 +291,7 @@ async function loadScreenDevices(force){
|
||||
const list=await _fetchAdbDevices(force);
|
||||
if(list===null)return;
|
||||
const cur=sel.value;
|
||||
const stateTxt={device:'在线',offline:'离线',pool:'设备池(未连接)',stf_not_ready:'STF未就绪'};
|
||||
const stateTxt={device:'在线',offline:'离线',pool:'设备池(未连接)'};
|
||||
sel.innerHTML=list.map(d=>
|
||||
'<option value="'+esc(d.serial)+'">'+esc(d.serial)+' ('+(stateTxt[d.state]||d.state)+')</option>'
|
||||
).join('');
|
||||
|
||||
Reference in New Issue
Block a user