ops.html
6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{% extends "base.html" %}
{% block title %}后台监管 - WeChat Viewer{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.ops{max-width:900px;margin:0 auto;padding:16px;}
.ops h1{font-size:20px;color:#2c3e50;margin-bottom:10px;}
.ops h2{font-size:15px;color:#2c3e50;margin:20px 0 8px;}
.banner{border-radius:10px;padding:14px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:6px;}
.banner.run{background:#e8f6ee;border:1px solid #27ae60;}
.banner.pause{background:#fdecea;border:1px solid #e74c3c;}
.banner b{font-size:16px;}.banner small{color:#666;display:block;margin-top:2px;}
.banner .grow{flex:1;min-width:180px;}
.btn{border:none;border-radius:8px;padding:9px 18px;font-size:14px;color:#fff;cursor:pointer;}
.btn.red{background:#e74c3c}.btn.green{background:#27ae60}.btn.gray{background:#7f8c8d}.btn.sm{padding:4px 10px;font-size:12px;border-radius:6px}.btn.blue{background:#3498db}
.card{background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:6px;box-shadow:0 1px 2px rgba(0,0,0,.06);display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.card .nm{font-weight:600;font-size:14px;}.card .meta{font-size:11px;color:#888;word-break:break-all;flex:1;min-width:160px}
.dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.dot.up{background:#27ae60}.dot.down{background:#bbb}
.tag{font-size:10px;padding:2px 7px;border-radius:9px;background:#fdebd0;color:#b9770e;font-weight:600}
table{width:100%;border-collapse:collapse;font-size:12px;background:#fff;border-radius:8px;overflow:hidden}
td,th{padding:6px 8px;border-bottom:1px solid #f0f0f0;text-align:left;vertical-align:top;word-break:break-all}
td.BLOCKED{color:#e74c3c;font-weight:600}
pre{background:#1e1e1e;color:#ddd;padding:10px;border-radius:8px;font-size:11px;overflow:auto;max-height:260px;white-space:pre-wrap}
.empty{color:#999;font-size:13px;padding:8px}
</style>
<div class="ops">
<h1>🛠 后台监管</h1>
<div id="banner" class="banner run"></div>
<div id="screen" style="font-size:12px;color:#888;margin-bottom:4px"></div>
<h2>🖐 UI 操作记录 <small style="font-weight:normal;color:#888">谁在动屏幕(最近 60 条,红色 = 被总闸拦下)</small></h2>
<div id="ui"></div>
<h2>▶ 运行中的任务</h2><div id="tasks"></div>
<h2>⚙ 常驻服务</h2><div id="svcs"></div>
<h2>⏱ Crontab</h2><pre id="cron"></pre>
<h2>📜 日志</h2>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px" id="logbtns"></div>
<pre id="log">点上面的按钮看日志</pre>
</div>
<script>
const H={'Content-Type':'application/json','X-Requested-With':'ops'};
const post=(u,b)=>fetch(u,{method:'POST',headers:H,body:JSON.stringify(b||{})}).then(r=>r.json());
const esc=s=>String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
let busy=false;
async function pause(on){ if(on||confirm('恢复 UI 操作?排队的回复会开始发送。')){await post('/ops/pause',{on});load();} }
async function kill(pid,force){ if(confirm((force?'强杀 ':'终止 ')+pid+' ?')){await post('/ops/kill',{pid,force});load();} }
async function svc(key,action){ if(confirm(action+' '+key+' ?')){await post('/ops/service',{key,action});setTimeout(load,1500);} }
async function log(name){ const r=await fetch('/ops/log?name='+name).then(r=>r.json()); document.getElementById('log').textContent=r.text||'(空)'; }
async function load(){
if(busy)return; busy=true;
try{
const d=await fetch('/ops/state').then(r=>r.json());
const b=document.getElementById('banner');
b.className='banner '+(d.paused?'pause':'run');
b.innerHTML=d.paused
?`<div class="grow"><b>⏸ UI 操作已暂停</b><small>自 ${esc(d.paused_at)};所有 input / am start 被拒绝,回复照常生成但留在队列,不会发出</small></div><button class="btn green" onclick="pause(false)">恢复 UI 操作</button>`
:`<div class="grow"><b>▶ UI 操作正常</b><small>后台任务可以操作手机屏幕</small></div><button class="btn red" onclick="pause(true)">⏸ 暂停所有 UI 操作</button>`;
document.getElementById('screen').textContent=d.screen.replace(/\s+/g,' ');
document.getElementById('ui').innerHTML=d.ui_actions.length
?'<table><tr><th>时间</th><th>结果</th><th>来源</th><th>动作</th></tr>'+d.ui_actions.map(a=>`<tr><td>${esc(a.ts)}</td><td class="${a.result}">${a.result}</td><td>${esc(a.who)}</td><td>${esc(a.what)}</td></tr>`).join('')+'</table>'
:'<div class="empty">暂无记录(新加的审计,下一次有 UI 动作开始记)</div>';
document.getElementById('tasks').innerHTML=d.tasks.length?d.tasks.map(t=>`<div class="card"><span class="dot up"></span><span class="nm">${esc(t.name)}</span>${t.ui?'<span class="tag">会动UI</span>':''}<span class="meta">pid ${t.pid} · ${esc(t.etime)} · ${esc(t.args)}</span><button class="btn sm gray" onclick="kill(${t.pid},false)">终止</button><button class="btn sm red" onclick="kill(${t.pid},true)">强杀</button></div>`).join(''):'<div class="empty">当前没有运行中的任务</div>';
document.getElementById('svcs').innerHTML=d.services.map(s=>`<div class="card"><span class="dot ${s.up?'up':'down'}"></span><span class="nm">${esc(s.name)}</span><span class="meta">${s.up?'pid '+s.pids.join(','):'未运行'}</span>${s.controllable?`<button class="btn sm green" onclick="svc('${s.key}','start')">启动</button><button class="btn sm blue" onclick="svc('${s.key}','restart')">重启</button><button class="btn sm gray" onclick="svc('${s.key}','stop')">停止</button>`:''}</div>`).join('');
document.getElementById('cron').textContent=d.cron.join('\n');
}catch(e){ document.getElementById('screen').textContent='加载失败: '+e; }
busy=false;
}
document.getElementById('logbtns').innerHTML=[['ui','UI 操作审计'],['reply-daemon','回复 daemon'],['reply-sender','reply-sender'],['orig-save','原图保存'],['digest','日报发送'],['dsh-web','dsh web']].map(([k,n])=>`<button class="btn sm gray" onclick="log('${k}')">${n}</button>`).join('');
load(); setInterval(load,4000);
</script>
{% endblock %}