chore: 初始化仓库(排除 adb 二进制文件以解决推送超时)

This commit is contained in:
2026-08-07 13:55:48 +08:00
commit bce48fc137
35 changed files with 6301 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>设备监控大屏</title>
<style>
*{box-sizing:border-box} body{font-family:-apple-system,"Segoe UI",Roboto,sans-serif;margin:0;background:#f0f2f5;color:#2c3e50}
.header{background:#fff;padding:14px 24px;box-shadow:0 1px 4px rgba(0,0,0,.08);display:flex;align-items:center;gap:16px}
.header h1{font-size:18px;margin:0} .header .ts{color:#95a5a6;font-size:12px;margin-left:auto}
.header a{color:#1890ff;text-decoration:none;font-size:13px}
.header a:hover{text-decoration:underline}
.content{padding:20px 24px;max-width:1400px;margin:0 auto}
.bar{display:flex;gap:8px;margin-bottom:16px;align-items:center;flex-wrap:wrap}
button{padding:7px 16px;border:1px solid #d9d9d9;border-radius:4px;cursor:pointer;font-size:13px;background:#fff;color:#595959}
button:hover{border-color:#40a9ff;color:#40a9ff} button.primary{background:#1890ff;color:#fff;border-color:#1890ff}
button.primary:hover{background:#40a9ff} button.danger{color:#ff4d4f;border-color:#ff4d4f}
button.danger:hover{background:#ff4d4f;color:#fff} button.sm{padding:3px 10px;font-size:12px}
table{width:100%;border-collapse:collapse;background:#fff;border-radius:6px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.08)}
th,td{padding:9px 10px;text-align:left;border-bottom:1px solid #f0f0f0;font-size:13px}
th{background:#fafafa;font-weight:600;color:#595959} tr:hover td{background:#fafafa}
.badge{padding:2px 8px;border-radius:10px;font-size:11px;color:#fff}
.b-running{background:#52c41a}.b-connecting{background:#faad14}.b-error{background:#ff4d4f}
.b-done{background:#1890ff}.b-idle{background:#bfbfbf;color:#595959}.b-released{background:#8c8c8c}.b-failed{background:#ff4d4f}
.ok{color:#52c41a}.no{color:#ff4d4f}.muted{color:#8c8c8c;font-size:12px}
.err{color:#ff4d4f;font-size:12px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.progress{width:70px;height:5px;background:#f0f0f0;border-radius:3px;overflow:hidden;display:inline-block;vertical-align:middle}
.progress>div{height:100%;background:#1890ff}
.tag{display:inline-block;background:#e6f7ff;color:#1890ff;padding:1px 8px;border-radius:3px;font-size:11px;margin:1px}
.kv{display:inline-block;background:#f5f5f5;border-radius:3px;padding:0 6px;margin:1px;font-size:11px;color:#595959}
.stats{display:flex;gap:16px;margin-bottom:16px}
.stat-card{background:#fff;padding:14px 20px;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,.08);min-width:120px}
.stat-card .num{font-size:24px;font-weight:600;color:#1890ff}
.stat-card .lbl{font-size:12px;color:#8c8c8c;margin-top:2px}
</style></head><body>
<div class="header">
<h1>设备监控大屏</h1>
<span class="muted" id="devCount"></span>
<span class="ts" id="srvTime"></span>
<a href="/admin" target="_blank">管理后台 →</a>
</div>
<div class="content">
<div class="stats" id="stats"></div>
<div class="bar">
<button class="primary" onclick="doPost('/api/jobs/_quick/run')" title="快速启动:对全部空闲设备跑默认抖音养号">快速启动全部</button>
<button class="danger" onclick="doPost('/api/stop_all')">停止全部</button>
<button onclick="doPost('/api/release')">强制释放占用</button>
<button onclick="loadStatus()">刷新</button>
<span class="muted">每 5 秒自动刷新 · 分组/任务管理请到 <a href="/admin" target="_blank">管理后台</a></span>
</div>
<table><thead><tr>
<th>设备</th><th>型号</th><th>STF</th><th>任务状态</th><th>抖音</th><th>进度</th>
<th>任务名称</th><th>当前动作</th><th>操作计数</th><th>最近错误</th><th>操作</th>
</tr></thead><tbody id="tb-devices"></tbody></table>
</div>
<script>
const ACTION_LABELS={like:'点赞',comment:'评论',follow:'关注',share:'分享'};
function doPost(url,body){return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):'{}'}).then(r=>r.json()).then(d=>{alert(JSON.stringify(d,null,1));loadStatus();}).catch(e=>alert(e));}
function fmtActionCounts(c){
if(!c||!Object.keys(c).length)return '<span class="muted">-</span>';
const parts=Object.entries(c).filter(([_,v])=>v>0).map(([k,v])=>'<span class="kv">'+(ACTION_LABELS[k]||k)+v+'</span>');
return parts.length?parts.join(''):'<span class="muted">-</span>';
}
async function loadStatus(){
try{
const r=await fetch('/api/status');const d=await r.json();if(!d.ok)return;
document.getElementById('devCount').textContent='设备 '+d.devices.length;
document.getElementById('srvTime').textContent=new Date(d.server_time*1000).toLocaleTimeString();
// 统计卡片
const running=d.devices.filter(x=>x.worker_status==='running').length;
const idle=d.devices.filter(x=>x.worker_status==='idle').length;
const error=d.devices.filter(x=>['error','failed'].includes(x.worker_status)).length;
const occupied=d.devices.filter(x=>x.stf_occupied).length;
document.getElementById('stats').innerHTML=
'<div class="stat-card"><div class="num" style="color:#52c41a">'+running+'</div><div class="lbl">运行中</div></div>'+
'<div class="stat-card"><div class="num" style="color:#1890ff">'+occupied+'</div><div class="lbl">STF占用</div></div>'+
'<div class="stat-card"><div class="num" style="color:#bfbfbf">'+idle+'</div><div class="lbl">空闲</div></div>'+
'<div class="stat-card"><div class="num" style="color:#ff4d4f">'+error+'</div><div class="lbl">异常</div></div>';
// 设备表格
document.getElementById('tb-devices').innerHTML=d.devices.map(dev=>{
const stf=dev.stf_occupied?'<span class="badge b-running">占用</span>':'<span class="badge b-idle">空闲</span>';
if(!dev.present)stf+=' <span class="no">离线</span>';
const ws=dev.worker_status;
const wb='<span class="badge b-'+ws+'">'+({idle:'未启动',connecting:'连接中',running:'运行中',done:'完成',error:'异常',released:'已释放',failed:'失败'}[ws]||ws)+'</span>';
const dy=dev.douyin_running?'<span class="ok">●</span>':'<span class="no">○</span>';
const pct=Math.round(dev.videos_watched/80*100);
const prog='<span class="progress"><div style="width:'+pct+'%"></div></span> '+dev.videos_watched;
const tname=dev.task_job?'<span class="tag">'+dev.task_job+'</span>':'<span class="muted">-</span>';
const retry=dev.attempt>0?' <span class="muted">'+dev.attempt+'次</span>':'';
const btn=dev.worker_status==='running'||dev.worker_status==='connecting'?'<button class="danger sm" onclick="doPost(\'/api/stop_device\',{serial:\''+dev.serial+'\'})">停止</button>':'';
return '<tr><td>'+dev.serial+'</td><td>'+dev.model+'</td><td>'+stf+'</td><td>'+wb+'</td><td>'+dy+'</td><td>'+prog+'</td><td>'+tname+retry+'</td><td class="muted">'+(dev.current_action||'-')+'</td><td>'+fmtActionCounts(dev.action_counts)+'</td><td class="err" title="'+(dev.last_error||'')+'">'+(dev.last_error||'-')+'</td><td>'+btn+'</td></tr>';
}).join('');
}catch(e){console.error(e);}
}
loadStatus();
setInterval(loadStatus,5000);
</script>
</body></html>