730 lines
66 KiB
HTML
730 lines
66 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>AgentsMeeting Dashboard</title>
|
||
<style>
|
||
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--dim:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--yellow:#d29922}
|
||
*{margin:0;padding:0;box-sizing:border-box}body{background:var(--bg);color:var(--text);font:20px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:28px}
|
||
h1{font-size:32px;font-weight:600;color:var(--accent);margin-bottom:4px}.subtitle{color:var(--dim);font-size:18px;margin-bottom:20px}
|
||
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
||
.tab-btn{padding:12px 24px;cursor:pointer;font-size:19px;color:var(--dim);border:none;background:none;border-bottom:2px solid transparent}
|
||
.tab-btn:hover{color:var(--text)}.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||
.tab-pane{display:none}.tab-pane.active{display:block}
|
||
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}
|
||
.stat-c{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px}
|
||
.stat-c .n{font-size:42px;font-weight:700}.stat-c .l{font-size:18px;color:var(--dim)}
|
||
.stat-c.g .n{color:var(--green)}.stat-c.r .n{color:var(--red)}
|
||
.agents{display:flex;flex-direction:column;gap:12px}
|
||
.ac{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden}
|
||
.ac.online{border-left:3px solid var(--green)}.ac.degraded{border-left:3px solid var(--yellow)}
|
||
.ac.offline{border-left:3px solid var(--red);opacity:.7}
|
||
.ah{display:flex;align-items:center;padding:14px 16px;gap:12px;cursor:pointer}
|
||
.ah:hover{background:rgba(255,255,255,.02)}
|
||
.sd{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||
.sd.g{background:var(--green);box-shadow:0 0 6px var(--green)}
|
||
.sd.y{background:var(--yellow)}.sd.r{background:var(--red)}
|
||
.ai{flex:1;min-width:0}.an{font-size:19px;font-weight:600}
|
||
.am{font-size:18px;color:var(--dim);margin-top:2px}
|
||
.b{display:inline-block;padding:2px 10px;border-radius:10px;font-size:18px;font-weight:600;margin-left:8px}
|
||
.b.w{background:#0078d420;color:#60cdff}.b.l{background:#fcc62420;color:#fcc624}.b.m{background:#99999920;color:#ccc}
|
||
.as{display:flex;gap:16px;align-items:center;padding-right:8px}
|
||
.as .v{font-size:19px;font-weight:600}.as .l{font-size:18px;color:var(--dim)}
|
||
.aa{display:flex;gap:4px}
|
||
.btn{padding:6px 14px;border:1px solid var(--border);border-radius:5px;background:#21262d;color:var(--text);cursor:pointer;font-size:19px}
|
||
.btn:hover{background:#30363d}
|
||
.btn.s{border-color:var(--green);color:var(--green)}
|
||
.btn.x{border-color:var(--red);color:var(--red)}
|
||
.btn.r{border-color:var(--yellow);color:var(--yellow)}
|
||
.btn.l{border-color:var(--accent);color:var(--accent)}
|
||
.btn:disabled{opacity:.4;cursor:not-allowed}
|
||
.svcs{display:flex;gap:8px;padding:0 16px 10px;flex-wrap:wrap}
|
||
.st{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:12px;font-size:19px;background:#0d1117;border:1px solid var(--border)}
|
||
.st .d{width:6px;height:6px;border-radius:50%}.st .d.running{background:var(--green)}.st .d.stopped{background:var(--dim)}
|
||
.st .p{color:var(--accent);font-family:monospace;font-size:18px;margin-left:4px}
|
||
.i{display:flex;align-items:center;gap:6px;margin:4px 0}
|
||
.sn{flex:1;min-width:0}
|
||
.lp{display:none;border-top:1px solid var(--border);padding:12px 16px;background:#0a0e14}
|
||
.lp.open{display:block}.lh{font-size:18px;color:var(--dim);margin-bottom:8px;display:flex;justify-content:space-between}
|
||
.lc{font:11px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);max-height:300px;overflow-y:auto;background:#06080c;border:1px solid var(--border);border-radius:6px;padding:10px;white-space:pre-wrap}
|
||
.help-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;min-width:20px;max-width:20px;min-height:20px;max-height:20px;border-radius:50%;border:1px solid var(--border);background:var(--card);color:var(--dim);cursor:pointer;font-size:14px;line-height:1;margin-left:8px;flex-shrink:0;overflow:hidden}
|
||
.i .help-btn{margin-left:0}
|
||
.help-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||
.help-btn.ai{color:var(--yellow);border-color:var(--yellow);font-size:12px}
|
||
.help-btn.ai:hover{background:var(--yellow);color:#000}
|
||
.spec-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);z-index:1000;display:none;align-items:center;justify-content:center}
|
||
.spec-modal.show{display:flex}
|
||
.spec-modal .modal-box{background:var(--card);border:1px solid var(--border);border-radius:12px;max-width:700px;max-height:80vh;overflow-y:auto;padding:28px;margin:20px}
|
||
.spec-modal .modal-box h3{font-size:22px;color:var(--accent);margin-bottom:12px}
|
||
.spec-modal .modal-box .close{float:right;cursor:pointer;color:var(--dim);font-size:22px}
|
||
.spec-modal .modal-box .close:hover{color:var(--text)}
|
||
.spec-modal .modal-box h4{font-size:19px;color:var(--accent);margin:16px 0 6px}
|
||
.spec-modal .modal-box p{font-size:18px;color:var(--text);margin:4px 0}
|
||
.spec-modal .modal-box ul{margin:4px 0 4px 20px}
|
||
.spec-modal .modal-box li{font-size:18px;color:var(--text);margin:2px 0}
|
||
.spec-modal .modal-box pre{background:var(--bg);padding:12px;border-radius:6px;font-size:15px;overflow-x:auto;margin:8px 0}
|
||
.toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:6px;font-size:19px;z-index:999;opacity:0;pointer-events:none}
|
||
.toast.show{opacity:1}.toast.ok{background:#238636;color:#fff}.toast.err{background:#da3633;color:#fff}
|
||
.ps{margin-top:24px}.ps h2{font-size:18px;color:var(--dim);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center}
|
||
.psv{display:flex;gap:8px;flex-wrap:wrap}
|
||
.psv .i{background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 12px;display:flex;align-items:center;gap:8px;font-size:18px}
|
||
h2{display:flex;align-items:center}
|
||
.psv .i .d{width:6px;height:6px;border-radius:50%}.psv .i .d.ok{background:var(--green)}.psv .i .d.stopped{background:var(--dim)}
|
||
.section{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:12px}
|
||
.section h2{font-size:19px;font-weight:600;color:var(--accent);display:flex;align-items:center}
|
||
.mono{font-family:"Cascadia Code",Consolas,monospace;font-size:18px}
|
||
.ar{font-size:16px;color:var(--yellow);margin-top:4px}
|
||
.tag{display:inline-block;padding:1px 8px;border-radius:8px;font-size:19px;font-weight:600}
|
||
.tag.g{background:#3fb95020;color:var(--green)}.tag.r{background:#f8514920;color:var(--red)}
|
||
.tag.y{background:#d2992220;color:var(--yellow)}.tag.d{background:#8b949e20;color:var(--dim)}
|
||
table{width:100%;border-collapse:collapse;font-size:18px}th,td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)}
|
||
th{color:var(--dim);font-weight:600;font-size:19px}
|
||
.log-line{font:16px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);padding:2px 0}
|
||
.sr{display:flex;align-items:center;gap:10px;padding:5px 0}
|
||
.sub-bar{display:flex;gap:2px;margin-bottom:10px;flex-wrap:wrap}
|
||
.sub-btn{padding:4px 12px;font-size:18px;background:0 0;border:1px solid transparent;border-radius:12px;color:var(--dim);cursor:pointer}
|
||
.sub-btn:hover{color:var(--text);border-color:var(--border)}.sub-btn.active{color:var(--accent);border-color:var(--accent);background:var(--bg)}
|
||
@media(max-width:768px){.g2{grid-template-columns:1fr}}
|
||
.section{font-size:18px;line-height:1.8}.section p,.section li{font-size:18px;line-height:1.8}.section code{font-size:18px}.section pre{font-size:18px;line-height:1.5}
|
||
</style></head><body>
|
||
<h1>AgentsMeeting</h1><div class="subtitle" id="subtitle">Dashboard</div>
|
||
<div class="tabs" id="tabs"></div><div id="panes"></div><div id="toast" class="toast"></div>
|
||
<div id="spec-modal" class="spec-modal"><div class="modal-box" id="spec-modal-box"></div></div>
|
||
<script>
|
||
var T=[{"id":"agents","label":"Agents"},{"id":"kanban","label":"Kanban"},{"id":"infra","label":"Infrastructure"},{"id":"principles","label":"开发原则","children":[{"id":"spec","label":"G 规范"},{"id":"tests","label":"K 测试"},{"id":"health","label":"F 健康"},{"id":"prd","label":"H 需求"}]}];
|
||
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'spec';
|
||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||
function qs(id){return document.getElementById(id)}
|
||
function toast(m,t){var e=qs('toast');e.textContent=m;e.className='toast show '+(t||'ok');setTimeout(function(){e.className='toast'},2500)}
|
||
var _specCache={};
|
||
function showModuleHelp(module,type){
|
||
var modal=qs('spec-modal');var box=qs('spec-modal-box');
|
||
if(_specCache[module]){
|
||
_renderSpecModal(_specCache[module],type);modal.classList.add('show');return;
|
||
}
|
||
fetch('/api/module-spec/'+module).then(function(r){return r.json()}).then(function(spec){
|
||
_specCache[module]=spec;_renderSpecModal(spec,type);modal.classList.add('show');
|
||
}).catch(function(){
|
||
box.innerHTML='<span class="close" onclick="qs(\'spec-modal\').classList.remove(\'show\')">×</span><h3>Spec not found</h3><p>Module: '+esc(module)+'</p>';modal.classList.add('show');
|
||
});
|
||
}
|
||
function _renderSpecModal(spec,type){
|
||
var box=qs('spec-modal-box');var h='';
|
||
h+='<span class="close" onclick="qs(\'spec-modal\').classList.remove(\'show\')">×</span>';
|
||
if(type==='human'){
|
||
var hh=spec.human_help||{};
|
||
h+='<h3>'+esc(hh.title||spec.module||'Help')+'</h3>';
|
||
if(hh.description){h+='<h4>说明</h4><ul>';hh.description.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>';}
|
||
if(hh.usage){h+='<h4>使用方法</h4><ul>';hh.usage.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>';}
|
||
if(hh.troubleshooting){h+='<h4>常见问题</h4><ul>';hh.troubleshooting.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>';}
|
||
if(hh.related){h+='<h4>关联模块</h4><p>'+esc(hh.related)+'</p>';}
|
||
}else{
|
||
var ai=spec.ai_spec||{};
|
||
h+='<h3>§ AI Spec — '+esc(spec.module||'')+'</h3>';
|
||
h+='<p style="color:var(--dim)">'+esc(spec.purpose||'')+'</p>';
|
||
if(ai.apis){h+='<h4>API 端点</h4><ul>';ai.apis.forEach(function(a){h+='<li><code>'+esc(a.method)+' '+esc(a.path)+'</code> — '+esc(a.returns||'')+(a.proxied_to?' <span style=color:var(--dim)>→ '+esc(a.proxied_to)+'</span>':'')+'</li>'});h+='</ul>';}
|
||
if(ai.dependencies){h+='<h4>依赖</h4><ul>';ai.dependencies.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>';}
|
||
if(ai.constraints){h+='<h4>关键约束</h4><ul>';ai.constraints.forEach(function(d){h+='<li style="color:var(--yellow)">'+esc(d)+'</li>'});h+='</ul>';}
|
||
if(ai.must_not){h+='<h4>禁止行为</h4><ul>';ai.must_not.forEach(function(d){h+='<li style="color:var(--red)">'+esc(d)+'</li>'});h+='</ul>';}
|
||
if(ai.tests){h+='<h4>测试用例</h4><ul>';ai.tests.forEach(function(t){h+='<li><code>'+esc(t.id)+'</code> '+esc(t.name)+'</li>'});h+='</ul>';}
|
||
if(ai.related_files){h+='<h4>相关文件</h4><ul>';ai.related_files.forEach(function(f){h+='<li><code>'+esc(f)+'</code></li>'});h+='</ul>';}
|
||
if(ai.related_modules&&ai.related_modules.length){h+='<h4>关联模块</h4><ul>';ai.related_modules.forEach(function(m){h+='<li><span class="help-btn" style="cursor:pointer;margin-right:6px" onclick="showModuleHelp(\''+esc(m.module)+'\',\'ai\')">§</span><code>'+esc(m.module)+'</code> — '+esc(m.relation)+'</li>'});h+='</ul>';}
|
||
}
|
||
box.innerHTML=h;
|
||
}
|
||
function init(){var b='',p='';for(var i=0;i<T.length;i++){var t=T[i],a=t.id===at||(t.children&&t.children.some(function(c){return c.id===at}));
|
||
b+='<button class="tab-btn'+(a?' active':'')+'" onclick="sw(\''+t.id+'\')">'+t.label+'</button>';
|
||
if(t.children){p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div class="sub-bar">';
|
||
for(var j=0;j<t.children.length;j++){var c=t.children[j];p+='<button class="sub-btn'+(c.id===as?' active':'')+'" onclick="sws(\''+c.id+'\')">'+c.label+'</button>';}
|
||
p+='</div>';
|
||
for(var j=0;j<t.children.length;j++){var c=t.children[j];p+='<div id="sp-'+c.id+'"'+(c.id===as?'':' style="display:none"')+'><div class="section"><div id="ct-'+c.id+'"><span class="dim">Loading...</span></div></div></div>';}
|
||
p+='</div>';}else{p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div id="ct-'+t.id+'"><span class="dim">Loading...</span></div></div>';}}
|
||
qs('tabs').innerHTML=b;qs('panes').innerHTML=p;}
|
||
function sw(id){if(id==='principles'){at='principles';as=localStorage.getItem('s')||'spec'}else{at=id;localStorage.setItem('t',id)}
|
||
document.querySelectorAll('.tab-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T.find(function(t){return t.id===at||(t.children&&t.children.some(function(c){return c.id===at}))}).label)});
|
||
document.querySelectorAll('.tab-pane').forEach(function(p){p.classList.toggle('active',p.id==='pn-'+id)});
|
||
if(id==='principles'){document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[3].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+as?'':'none'});(FETCHES[as]||function(){})()}}
|
||
function sws(id){as=id;localStorage.setItem('s',id);document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[3].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+id?'':'none'});(FETCHES[id]||function(){})()}
|
||
function fill(id,h){var e=qs('ct-'+id);if(e)e.innerHTML=h}
|
||
var L=new Set();
|
||
async function fa(){try{var r=await fetch('/api/agents'),d=await r.json(),on=d.filter(function(a){return a.status==='online'}).length,er=d.reduce(function(s,a){return s+(a.errors||0)},0);
|
||
var h='<h2>Agents 总览<span class="help-btn" onclick="showModuleHelp(\'agents\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'agents\',\'ai\')" title="AI Spec">§</span></h2><div class="stats"><div class="stat-c g"><div class="n">'+on+'/'+d.length+'</div><div class="l">Agents Online</div></div><div class="stat-c '+(er>0?'y':'g')+'"><div class="n">'+(er||0)+'</div><div class="l">Errors</div></div><div class="stat-c g"><div class="n">'+d.length+'</div><div class="l">Total</div></div></div><div class="agents">';
|
||
for(var i=0;i<d.length;i++){var a=d[i];var st=a.status==='online'?'online':a.status==='degraded'?'degraded':'offline';var pb={windows:'w',linux:'l',mac:'m'}[a.platform]||'';
|
||
var rs=a.status_reason&&a.status_reason!=='OK'?'<div class="ar">'+esc(a.status_reason)+'</div>':'';
|
||
h+='<div class="ac '+st+'"><div class="ah" onclick="tgL(\''+a.id+'\')"><span class="sd '+(a.status==='online'?'g':a.status==='degraded'?'y':'r')+'"></span><div class="ai"><div class="an">'+esc(a.display_name||a.name)+(pb?' <span class="b '+pb+'">'+esc(a.platform)+'</span>':'')+'</div><div class="am">'+esc(a.jid||'')+(a.host?' · '+esc(a.host):'')+rs+'</div></div><div class="as"><div class="v">'+(a.message_count_5min||0)+'</div><div class="l">msg/5m</div></div><div class="aa"><button class="btn s" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'start\')" '+(a.status!=='offline'||!a.restartable?'disabled':'')+'>Start</button><button class="btn x" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'stop\')" '+(a.status==='offline'||!a.restartable?'disabled':'')+'>Stop</button><button class="btn r" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'restart\')" '+(!a.restartable?'disabled':'')+'>Restart</button><button class="btn l" onclick="event.stopPropagation();tgL(\''+a.id+'\')">Logs</button></div></div><div class="svcs">';
|
||
if(a.services){for(var k=0;k<a.services.length;k++){var s=a.services[k];h+='<span class="st"><span class="d '+(s.status||'stopped')+'"></span>'+esc(s.type)+(s.port?':<span class="p">'+s.port+'</span>':'')+'</span>';}}
|
||
h+='</div><div class="lp'+(L.has(a.id)?' open':'')+'" id="lp-'+a.id+'"><div class="lh"><span>Logs</span><button class="btn l" onclick="event.stopPropagation();fL(\''+a.id+'\')">Refresh</button></div><div class="lc" id="lc-'+a.id+'">Click Logs to load</div></div></div>';}
|
||
h+='</div>';qs('ct-agents').innerHTML=h;}catch(e){}}
|
||
async function doAct(id,act){try{var r=await fetch('/api/agents/'+id+'/'+act,{method:'POST'}),d=await r.json();toast(act+(d.ok?' OK':' FAILED'),d.ok?'ok':'err');setTimeout(fa,1500)}catch(e){toast('Error: '+e.message,'err')}}
|
||
function tgL(id){if(L.has(id))L.delete(id);else L.add(id);var lp=qs('lp-'+id);if(lp)lp.className='lp'+(L.has(id)?' open':'');if(L.has(id))fL(id)}
|
||
async function fL(id){var el=qs('lc-'+id);if(!el||!L.has(id))return;try{var r=await fetch('/api/agents/'+id+'/logs?lines=50'),d=await r.json();el.innerHTML=(d.lines||[]).map(function(l){return'<div>'+esc(l)+'</div>'}).join('')||'<span class="dim">no log data</span>';el.scrollTop=el.scrollHeight}catch(e){el.innerHTML='<span class="red">Failed</span>'}}
|
||
async function fHealth(){try{
|
||
// Helpers
|
||
function isHealthy(st){return st==='running'||st==='online'||(st&&st.indexOf('remote')===0);}
|
||
// Service metadata: label / layer / impact (per-instance key → metadata)
|
||
var M={};
|
||
M['agent-001:xmpp_bot']={n:'研发助理-XMPP Bot',l:'通信层',i:'小小莫(Windows)XMPP 消息收发中断',spec:'xmpp_bot'};
|
||
M['agent-002:hermes_gateway']={n:'Hermes网关-莫荷',l:'AI网关',i:'莫荷智能体(246 Linux)不可用',spec:'hermes_gateway'};
|
||
M['agent-003:xmpp_bot']={n:'本地推理-XMPP Bot',l:'通信层',i:'小果(Mac)XMPP 消息收发中断',spec:'xmpp_bot'};
|
||
M['agent-003:omlx_server']={n:'OMLX推理服务(小果)',l:'AI网关',i:'小果(Mac)本地推理服务不可用',spec:'omlx_server'};
|
||
M['agent-004:hermes_gateway']={n:'Hermes网关-知微',l:'AI网关',i:'知微智能体(246 Linux)不可用',spec:'hermes_gateway'};
|
||
M.wechat_bridge={n:'莫荷微信桥接',l:'通信层',i:'微信消息收发中断',spec:'wechat_bridge'};
|
||
M.article_processor={n:'文章抓取服务',l:'辅助服务',i:'微信长文章无法读取',spec:'article_processor'};
|
||
// 兼容层:/api/services 返回的旧名称仍然映射到友好标签
|
||
M.xmpp_bot={n:'XMPP消息收发',l:'通信层',i:'消息收发中断',spec:'xmpp_bot'};
|
||
M.hermes_gateway={n:'Hermes网关',l:'AI网关',i:'Hermes Agent不可用',spec:'hermes_gateway'};
|
||
M.ejabberd={n:'XMPP服务器',l:'通信层',i:'所有XMPP连接不可用'};
|
||
M.gateway_mohe={n:'Gateway-莫荷',l:'AI网关',i:'莫荷智能体不可用',spec:'hermes_gateway'};
|
||
M.gateway_zhiwei={n:'Gateway-知微',l:'AI网关',i:'知微智能体不可用',spec:'hermes_gateway'};
|
||
M.dashboard={n:'管理门户',l:'辅助服务',i:'Dashboard无法访问',spec:'dashboard'};
|
||
M.watchdog={n:'进程看门狗',l:'辅助服务',i:'异常进程无自动恢复'};
|
||
M.ejabberd={n:'XMPP服务器',l:'通信层',i:'所有XMPP连接不可用',spec:'ejabberd'};
|
||
M['agents-health-check']={n:'健康检查(5min)',l:'定时任务',i:'5分钟级健康检测未运行',spec:'health_check'};
|
||
M['agents-daily-health']={n:'日报健康检查(每日)',l:'定时任务',i:'日报统计未运行',spec:'daily_health'};
|
||
M['agents-todo-executor']={n:'TODO自修复(10min)',l:'定时任务',i:'服务异常无法自动恢复',spec:'todo_executor'};
|
||
var layerOrder=['通信层','AI网关','辅助服务','定时任务'];
|
||
|
||
var h='<h2>F 系统健康<span class="help-btn" onclick="showModuleHelp(\'health\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'health\',\'ai\')" title="AI Spec">§</span></h2>';
|
||
var all=await Promise.all([
|
||
fetch('/api/services').then(function(r){return r.json()}).catch(function(){return{}}),
|
||
fetch('/api/expected').then(function(r){return r.json()}).catch(function(){return{}}),
|
||
fetch('/api/monitor').then(function(r){return r.json()}).catch(function(){return{}})
|
||
]);
|
||
var sD=all[0]||{},eD=all[1]||{},mD=all[2]||{};
|
||
var svcs=sD.services||[],exp=eD.expected||[],act=eD.actual||{};
|
||
|
||
// --- Layer 1: 系统概览 ---
|
||
var hCount=svcs.filter(function(s){return s.health&&s.health.ok}).length;
|
||
var cDown=exp.filter(function(e){return e.critical&&e.expected.indexOf('running')===0&&!isHealthy(act[e.name])}).length;
|
||
var st=cDown>0?'critical':hCount<svcs.length?'degraded':'good';
|
||
var stL={good:'正常',degraded:'部分异常',critical:'严重异常'};
|
||
var stC={good:'var(--green)',degraded:'var(--yellow)',critical:'var(--red)'};
|
||
h+='<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">';
|
||
h+='<div style="background:'+stC[st]+';color:#000;border-radius:8px;padding:12px 20px;flex:1;min-width:160px;text-align:center"><div style=font-size:28px;font-weight:700>'+stL[st]+'</div><div style=font-size:17px;margin-top:4px;opacity:.8>'+hCount+'/'+svcs.length+' 服务健康</div></div>';
|
||
h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px;text-align:center"><div style=font-size:28px;font-weight:700;color:'+stC[st]+'>'+cDown+'</div><div style=font-size:17px;color:var(--dim)>关键异常</div></div>';
|
||
if(mD.tier1&&mD.tier1.summary){var t1=mD.tier1.summary;var t1c=t1.ok===t1.total?'var(--green)':'var(--red)';
|
||
h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px;text-align:center"><div style=font-size:28px;font-weight:700;color:'+t1c+'>'+t1.ok+'/'+t1.total+'</div><div style=font-size:17px;color:var(--dim)>T1监控</div></div>';}
|
||
h+='</div>';
|
||
|
||
// --- Layer 2: 异常服务 ---
|
||
var issues=[];
|
||
for(var i=0;i<exp.length;i++){var e=exp[i];
|
||
if(e.critical&&e.expected.indexOf('running')===0&&!isHealthy(act[e.name])){
|
||
var ma=M[e.key||e.name]||{n:e.name,i:'未知影响'};
|
||
issues.push({name:e.name,label:ma.n,impact:ma.i,status:act[e.name]||'stopped'});}}
|
||
for(var i=0;i<svcs.length;i++){var s=svcs[i];
|
||
if(!s.health||!s.health.ok){
|
||
if(!issues.some(function(x){return x.name===s.name})){
|
||
var m=M[s.name]||{n:s.name,i:'服务不可用'};
|
||
issues.push({name:s.name,label:m.n,impact:m.i,status:'down'});}}}
|
||
if(issues.length){
|
||
h+='<div class=section><h2 style="color:var(--red);margin-bottom:8px">异常服务</h2>';
|
||
for(var i=0;i<issues.length;i++){var iv=issues[i];
|
||
h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:8px;padding:10px 14px;margin-bottom:6px">';
|
||
h+='<div style=display:flex;align-items:center;gap:8px><span class="sd r"></span><strong>'+esc(iv.label)+'</strong><span class="tag r" style=font-size:18px>'+esc(iv.status)+'</span></div>';
|
||
h+='<div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(iv.impact)+'</div></div>';}
|
||
h+='</div>';}
|
||
|
||
// --- Layer 3: 全部服务 (merge expected + services) ---
|
||
var layers={};
|
||
var added={};
|
||
for(var i=0;i<exp.length;i++){var e=exp[i];
|
||
if(e.check==='scheduled')continue; // 定时任务由下方单独区块渲染,此处跳过
|
||
var ma=M[e.key||e.name]||{n:e.name,l:'辅助服务',i:'',spec:''};
|
||
var ly=ma.l||'其他';
|
||
if(!layers[ly])layers[ly]=[];
|
||
added[e.name]=true;
|
||
if(e.key) added[e.key]=true; // 同时标记 key,防止 services 同名重复
|
||
layers[ly].push({name:e.name,label:ma.n,port:e.port,status:isHealthy(act[e.name])?'running':act[e.name],critical:e.critical,spec:ma.spec||e.spec||''});}
|
||
for(var i=0;i<svcs.length;i++){var s=svcs[i];
|
||
if(!added[s.name]){
|
||
var rawL=(M[s.name]||{}).l||(s.type==='infra'?'通信层':s.type==='gateway'?'AI网关':'辅助服务');
|
||
var rawN=(M[s.name]||{}).n||s.name;
|
||
if(!layers[rawL])layers[rawL]=[];
|
||
var hStatus=s.health&&s.health.ok?'running':'down';
|
||
var mSpec=(M[s.name]||{}).spec||'';
|
||
layers[rawL].push({name:s.name,label:rawN,port:s.port,status:hStatus,critical:false,spec:mSpec});}}
|
||
h+='<div class=section><h2 style=margin-bottom:8px>全部服务</h2>';
|
||
for(var li=0;li<layerOrder.length;li++){var ln=layerOrder[li];
|
||
var items=layers[ln];
|
||
if(!items||!items.length)continue;
|
||
h+='<div style=margin:8px 0 4px><strong style=font-size:19px;color:var(--accent)>'+esc(ln)+'</strong></div>';
|
||
for(var j=0;j<items.length;j++){var it=items[j];
|
||
var run=it.status==='running';
|
||
h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;background:var(--bg);border-radius:4px;margin:2px 0">';
|
||
h+='<span class="sd '+(run?'g':'r')+'"></span>';
|
||
h+='<span style=flex:1>'+esc(it.label)+'</span>';
|
||
if(it.port)h+='<span style=color:var(--dim);font-size:18px>:'+it.port+'</span>';
|
||
if(it.spec){h+='<span class="help-btn" onclick="showModuleHelp(\''+it.spec+'\',\'human\')" title="使用说明">?</span>';
|
||
h+='<span class="help-btn ai" onclick="showModuleHelp(\''+it.spec+'\',\'ai\')" title="AI Spec">§</span>';}
|
||
h+='<span class="tag '+(run?'g':'r')+'">'+(run?'运行中':esc(it.status))+'</span>';
|
||
h+='</div>';}
|
||
}
|
||
h+='</div>';
|
||
|
||
// --- 定时任务状态 ---
|
||
if(mD.tasks&&mD.tasks.length){
|
||
h+='<div class=section><h2 style=margin-bottom:8px>定时任务</h2>';
|
||
for(var i=0;i<mD.tasks.length;i++){var t=mD.tasks[i];
|
||
var m=M[t.name]||{n:t.name};
|
||
var ms=m.spec||'';
|
||
var tk=t.status==='ok'||t.status==='timer_ok'||t.status==='cron_ok'||t.status==='schtasks_ok';
|
||
var tn=t.status==='not_deployed';
|
||
h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;margin:2px 0">';
|
||
h+='<span class="sd '+(tk?'g':tn?'y':'r')+'"></span>';
|
||
h+='<span style=flex:1>'+esc(m.n)+'</span>';
|
||
if(ms){h+='<span class="help-btn" onclick="showModuleHelp(\''+ms+'\',\'human\')" title="使用说明">?</span>';
|
||
h+='<span class="help-btn ai" onclick="showModuleHelp(\''+ms+'\',\'ai\')" title="AI Spec">§</span>';}
|
||
h+='<span class="tag '+(tk?'g':tn?'y':'r')+'">'+(tk?'正常':tn?'未部署':'异常')+'</span></div>';}
|
||
h+='</div>';}
|
||
|
||
// --- 自动修复记录 ---
|
||
try{var ahR=await fetch('/api/autoheal');var ahD=await ahR.json();
|
||
if(ahD&&ahD.time&&ahD.actions_taken&&ahD.actions_taken.length>0){
|
||
h+='<div class=section><h2 style=margin-bottom:8px>自动修复记录 <span style=font-size:18px;color:var(--dim)>'+esc(ahD.time.slice(0,16).replace('T',' '))+'</span></h2>';
|
||
for(var ai=0;ai<ahD.actions_taken.length;ai++){var a=ahD.actions_taken[ai];
|
||
var isOK=a.detail&&a.detail.indexOf('OK')>=0;
|
||
h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;margin:2px 0;background:var(--card);border-radius:4px">';
|
||
h+='<span class="sd '+(isOK?'g':'r')+'"></span>';
|
||
h+='<span style=flex:1>'+esc(a.action||'修复')+'</span>';
|
||
h+='<span style=color:var(--dim);font-size:18px>'+esc(a.service||'')+'</span>';
|
||
h+='<span class="tag '+(isOK?'g':'r')+'">'+(isOK?'成功':esc(a.detail||'失败'))+'</span></div>';}
|
||
h+='</div>';}}
|
||
catch(ahE){/* autoheal fetch failed, skip */}
|
||
|
||
if(!h)h='<span class=dim>No data</span>';
|
||
fill('health',h)}catch(e){fill('health','<span class=tag r>Error: '+esc(e.message)+'</span>')}}
|
||
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json();var tasks=d.tasks||[];var s=document.getElementById('kf-status');var a=document.getElementById('kf-assignee');var fs=s?s.value:'';var fa=a?a.value:'';
|
||
if(tasks.length==0){fill('kanban','<h2>Kanban 看板<span class="help-btn" onclick="showModuleHelp(\'kanban\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'kanban\',\'ai\')" title="AI Spec">§</span></h2><div style=text-align:center;padding:40px;color:var(--dim)>No tasks</div>');return}
|
||
var html='<h2>Kanban 看板<span class="help-btn" onclick="showModuleHelp(\'kanban\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'kanban\',\'ai\')" title="AI Spec">§</span></h2><div style=display:flex;gap:8px;margin-bottom:16px>';html+='<select id=kf-status style=background:var(--card);border:1px solid var(--border);border-radius:5px;padding:6px 10px;color:var(--text);font-size:18px><option value="">All Status</option><option value=ready>Ready</option><option value=progress>In Progress</option><option value=blocked>Blocked</option><option value=done>Done</option></select>';
|
||
html+='<select id=kf-assignee style=background:var(--card);border:1px solid var(--border);border-radius:5px;padding:6px 10px;color:var(--text);font-size:18px><option value="">All</option><option value=mohe>mohe</option><option value=zhiwei>zhiwei</option><option value=xxm>xxm</option><option value=xiaoguo>xiaoguo</option></select>';
|
||
html+='<span style=color:var(--dim);font-size:18px;align-self:center>'+tasks.length+' tasks</span></div>';
|
||
var filtered=tasks;if(fs)filtered=filtered.filter(function(t){return t.status===fs});if(fa)filtered=filtered.filter(function(t){return t.assignee===fa});
|
||
var colors={ready:'var(--green)',progress:'var(--yellow)',blocked:'var(--red)',done:'var(--dim)'};var labels={ready:'Ready',progress:'In Progress',blocked:'Blocked',done:'Done'};
|
||
html+=filtered.map(function(t){return'<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;border-left:3px solid '+(colors[t.status]||'var(--dim)')+'" id=kc-'+t.id+'"><div style=font-size:18px;font-weight:600>'+esc(t.title)+'</div><div style=display:flex;gap:12px;font-size:19px;color:var(--dim);margin-bottom:6px>'
|
||
+'<span style="display:inline-block;padding:1px 6px;border-radius:3px;font-size:18px;font-weight:600;background:'+(colors[t.status]||'var(--dim)')+';color:#000">'+(labels[t.status]||t.status)+'</span>'
|
||
+'<span>'+(t.assignee?'@'+esc(t.assignee):'')+'</span>'
|
||
+'<span>'+(t.created_at?new Date(t.created_at*1000).toLocaleDateString():'')+'</span></div>'
|
||
+(t.body?'<div style=font-size:18px;color:var(--dim);white-space:pre-wrap;max-height:60px;overflow:hidden;cursor:pointer onclick="this.classList.toggle(\'expanded\');if(this.style.maxHeight!==\'none\')this.style.maxHeight=\'none\';else this.style.maxHeight=\'60px\'">'+esc(t.body)+'</div>':'')
|
||
+'</div>'}).join('');
|
||
fill('kanban',html);
|
||
var es=document.getElementById('kf-status');var ea=document.getElementById('kf-assignee');
|
||
if(es&&!s)es.value=fs;if(ea&&!a)ea.value=fa;
|
||
if(es)es.onchange=fK;if(ea)ea.onchange=fK;}catch(e){}}
|
||
async function fI(){
|
||
try{
|
||
var ci=document.getElementById('ct-infra');if(!ci)return;
|
||
var ls=ci.querySelector('span.dim');if(ls)ls.remove();
|
||
/* --- Stable wrapper for Platform+Ejabberd (rebuilt each cycle, no flicker since no state to preserve) --- */
|
||
var pe=document.getElementById('infra-pe');
|
||
if(!pe){pe=document.createElement('div');pe.id='infra-pe';ci.appendChild(pe);}
|
||
var h='';
|
||
try{var r=await fetch('/api/platform'),d=await r.json();
|
||
h+='<div class="ps"><h2>Platform<span class="help-btn" onclick="showModuleHelp(\'infra\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'infra\',\'ai\')" title="AI Spec">§</span></h2><div class="psv">';
|
||
for(var i=0;i<d.length;i++){var s=d[i];
|
||
var dc='stopped',ds='',dt='';
|
||
if(s.status==='running'){dc='ok';dt=s.message?' <span style=color:var(--green)>'+esc(s.message)+'</span>':'';}
|
||
else if(s.status==='logged_out'){dc='ok';ds='background:var(--yellow)';dt=' <span style=color:var(--yellow)>'+esc(s.message||'\u9700\u626b\u7801')+'</span>';}
|
||
h+='<div class="i"><span class="d '+dc+'"'+(ds?' style='+ds:'')+'></span><span class="sn">'+esc(s.name)+dt+'</span><span class="help-btn" onclick="showModuleHelp(\''+s.id+'\',\'human\')" title="\u4f7f\u7528\u8bf4\u660e">?</span><span class="help-btn ai" onclick="showModuleHelp(\''+s.id+'\',\'ai\')" title="AI Spec">\u00a7</span></div>';
|
||
if(s.id==='wechat_bridge'&&s.status==='logged_out'){
|
||
h+='<div style="margin:0 0 10px 18px;padding:12px 16px;background:#1a1400;border:1px solid var(--yellow);border-radius:8px;display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap">';
|
||
if(s.qr_url){
|
||
h+='<div style=text-align:center;flex-shrink:0>';
|
||
h+='<img id=wechat-qr-img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data='+encodeURIComponent(s.qr_url||'')+'" style="border:2px solid var(--yellow);border-radius:6px">';
|
||
h+='<div id=wechat-qr-status style="margin-top:6px;font-size:14px;color:var(--dim)">';
|
||
h+='<span id=wechat-qr-age>\u4e8c\u7ef4\u7801\u7ea65\u5206\u949f\u6709\u6548</span></div>';
|
||
h+='<button id=wechat-qr-refresh class="btn r" style="margin-top:4px;font-size:14px;padding:3px 10px" onclick="refreshWechatQR()">\u5237\u65b0\u4e8c\u7ef4\u7801</button></div>';
|
||
h+='<div style=flex:1;min-width:160px;font-size:14px;color:var(--dim)>';
|
||
h+='<p style="margin:0 0 4px;color:var(--yellow);font-weight:600">莫荷微信需要扫码登录</p>';
|
||
h+='<p style=margin:0;line-height:1.5>'+esc(s.message||'')+'。用微信扫描左侧二维码重新登录。</p>';
|
||
h+='<p style=margin-top:4px>链接: <code style=word-break:break-all;font-size:13px>'+esc(s.qr_url||'')+'</code></p>';
|
||
h+='</div>';
|
||
}else{
|
||
h+='<div style=text-align:center;flex-shrink:0>';
|
||
h+='<button id=wechat-qr-get class="btn s" style="font-size:14px;padding:8px 18px" onclick="getWechatQR()">\u83b7\u53d6\u767b\u5f55\u4e8c\u7ef4\u7801</button>';
|
||
h+='<div id=wechat-qr-get-status style="margin-top:6px;font-size:13px;color:var(--dim)">\u70b9\u51fb\u83b7\u53d6\u65b0\u7684\u767b\u5f55\u4e8c\u7ef4\u7801</div></div>';
|
||
h+='<div style=flex:1;min-width:160px;font-size:14px;color:var(--dim)>';
|
||
h+='<p style="margin:0 0 4px;color:var(--yellow);font-weight:600">\u83ab\u8377\u5fae\u4fe1\u5df2\u6389\u7ebf</p>';
|
||
h+='<p style=margin:0;line-height:1.5>'+esc(s.message||'')+'\u3002\u70b9\u51fb\u5de6\u4fa7\u6309\u94ae\u89e6\u53d1\u767b\u5f55\uff0c\u751f\u6210\u4e8c\u7ef4\u7801\u540e\u7528\u5fae\u4fe1\u626b\u7801\u3002</p>';
|
||
h+='</div>';
|
||
}
|
||
h+='</div>';
|
||
}
|
||
}
|
||
try{var r2=await fetch('/api/ejabberd'),ej=await r2.json();var ejA=ej.alive||ej.xmpp_bot_connected;
|
||
h+='<div class="i"><span class="d '+(ejA?'ok':'stopped')+'"></span><span class="sn">Ejabberd XMPP</span><span class="help-btn" onclick="showModuleHelp(\'ejabberd\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'ejabberd\',\'ai\')" title="AI Spec">§</span></div>';
|
||
}catch(ejErr){}
|
||
h+='</div></div>';
|
||
}catch(e){}
|
||
pe.innerHTML=h;
|
||
|
||
/* --- EasyTier section: create once, update state each cycle (no flicker) --- */
|
||
var et=document.getElementById('easytier-section');
|
||
if(!et){
|
||
et=document.createElement('div');et.id='easytier-section';et.className='ps';et.style.marginTop='16px';
|
||
et.innerHTML='<h2>EasyTier VPN<span class="help-btn" onclick="showModuleHelp(\'easytier\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'easytier\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=et-w></span>Windows <span id=et-w-ip></span></div><div class=i><span class="d" id=et-246></span>246 <span id=et-246-ip></span></div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-et-on>Turn On</button><button class="btn x" id=btn-et-off>Turn Off</button><span id=et-status style="font-size:12px;color:var(--dim)">-</span></div>';
|
||
ci.appendChild(et);
|
||
document.getElementById('btn-et-on').onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){fI()}).catch(function(){toast('ET Fail','err')})};
|
||
document.getElementById('btn-et-off').onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){fI()}).catch(function(){toast('ET Fail','err')})};
|
||
}
|
||
try{var e3=await fetch('/api/easytier').then(function(r){return r.json()});var s=e3.status||{};
|
||
var e1=document.getElementById('et-w');if(e1)e1.className='d '+(s.windows==='running'?'ok':'stopped');
|
||
var e2=document.getElementById('et-246');if(e2)e2.className='d '+(s['246']==='running'?'ok':'stopped');
|
||
if(e3.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=e3.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=e3.virtual_ips['246']||'';}
|
||
var allRun=Object.values(s).every(function(v){return v==='running'||v==='pending'});
|
||
document.getElementById('btn-et-on').disabled=allRun;
|
||
document.getElementById('btn-et-off').disabled=!allRun;
|
||
document.getElementById('et-status').textContent=allRun?'Connected':'Off';
|
||
}catch(e3){}
|
||
|
||
/* --- RDP section: create once, update state each cycle (no flicker) --- */
|
||
var rd=document.getElementById('rdp-section');
|
||
if(!rd){
|
||
rd=document.createElement('div');rd.id='rdp-section';rd.className='ps';rd.style.marginTop='16px';
|
||
rd.innerHTML='<h2>RDP Remote Desktop<span class="help-btn" onclick="showModuleHelp(\'rdp\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'rdp\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=rdp-st></span>RDP <span id=rdp-info></span></div><div class=i><span class="d" id=rdp-tun></span>Tunnel</div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-rdp-on>Enable</button><button class="btn x" id=btn-rdp-off>Disable</button><span id=rdp-status-txt style="font-size:12px;color:var(--dim)">-</span><a href="javascript:void(0)" id="rdp-log-link" style="font-size:11px;color:var(--dim);margin-left:auto">日志</a></div><div id="rdp-progress" style="margin-top:10px;display:none;font-size:12px"><div id="rdp-prog-msg" style="color:var(--accent);margin-bottom:6px;font-weight:600"></div><div id="rdp-prog-steps" style="line-height:1.7"></div></div><div id="rdp-logbox" style="margin-top:8px;display:none;font-size:11px;color:var(--dim);background:rgba(0,0,0,.2);padding:8px;border-radius:6px;max-height:180px;overflow:auto;white-space:pre-wrap;font-family:monospace"></div>';
|
||
ci.appendChild(rd);
|
||
document.getElementById('btn-rdp-on').onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){startRdpProgress()}).catch(function(){toast('RDP Fail','err')})};
|
||
document.getElementById('btn-rdp-off').onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){fI()}).catch(function(){toast('RDP Fail','err')})};
|
||
document.getElementById('rdp-log-link').onclick=function(){var b=document.getElementById('rdp-logbox');if(b.style.display==='none'||!b.style.display){b.style.display='block';loadRdpLog()}else{b.style.display='none'}};
|
||
}
|
||
try{var e4=await fetch('/api/rdp').then(function(r){return r.json()});
|
||
document.getElementById('rdp-st').className='d '+(e4.rdp_enabled?'ok':'stopped');
|
||
document.getElementById('rdp-tun').className='d '+(e4.tunnel_running?'ok':'stopped');
|
||
document.getElementById('rdp-info').textContent=e4.public_endpoint||'';
|
||
var fOn=e4.rdp_enabled&&e4.tunnel_running;
|
||
document.getElementById('btn-rdp-on').disabled=fOn;
|
||
document.getElementById('btn-rdp-off').disabled=!fOn;
|
||
document.getElementById('rdp-status-txt').textContent=fOn?'Connected':e4.rdp_enabled?'Tunnel pending':'Disconnected';
|
||
}catch(e4){}
|
||
|
||
/* --- RDP enable 实时进度轮询 + 结构化日志 --- */
|
||
var _rdpProgTimer=null;
|
||
function startRdpProgress(){
|
||
var box=document.getElementById('rdp-progress'); if(box) box.style.display='block';
|
||
if(_rdpProgTimer) clearInterval(_rdpProgTimer);
|
||
pollRdpProgress();
|
||
_rdpProgTimer=setInterval(pollRdpProgress,1500);
|
||
}
|
||
async function pollRdpProgress(){
|
||
try{
|
||
var d=await fetch('/api/rdp/progress').then(function(r){return r.json()});
|
||
var msg=document.getElementById('rdp-prog-msg');
|
||
if(msg) msg.textContent=(d.state==='running'?'⏳ ':'')+(d.message||'');
|
||
var st=document.getElementById('rdp-prog-steps');
|
||
if(st && d.steps){
|
||
st.innerHTML=d.steps.map(function(s){
|
||
var icon=s.status==='ok'?'✓':(s.status==='fail'?'✗':(s.status==='warn'?'⚠':(s.status==='skip'?'—':'…')));
|
||
var color=s.status==='ok'?'#4caf50':(s.status==='fail'?'#f44336':(s.status==='warn'?'#ff9800':'#9aa'));
|
||
return '<div style="color:'+color+'">'+icon+' '+s.name+' <span style="color:#778;font-size:11px">'+(s.detail||'')+'</span></div>';
|
||
}).join('');
|
||
}
|
||
if(d.state==='done'||d.state==='failed'){
|
||
if(_rdpProgTimer){clearInterval(_rdpProgTimer);_rdpProgTimer=null;}
|
||
if(msg) msg.textContent=(d.state==='done'?'✓ ':'✗ ')+(d.message||'');
|
||
fI();
|
||
}
|
||
}catch(e){}
|
||
}
|
||
async function loadRdpLog(){
|
||
try{
|
||
var d=await fetch('/api/rdp/enable_log?lines=80').then(function(r){return r.json()});
|
||
var b=document.getElementById('rdp-logbox');
|
||
if(b) b.textContent=(d.lines&&d.lines.length)?d.lines.join('\n'):'(暂无日志)';
|
||
}catch(e){var b=document.getElementById('rdp-logbox');if(b)b.textContent='日志加载失败';}
|
||
}
|
||
|
||
/* --- OpenCode Go Usage section: create once, update state each cycle (no flicker) --- */
|
||
var us=document.getElementById('usage-section');
|
||
if(!us){
|
||
us=document.createElement('div');us.id='usage-section';us.className='ps';us.style.marginTop='16px';
|
||
us.innerHTML='<h2>API 用量<span class="help-btn" onclick="showModuleHelp(\'usage_monitor\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'usage_monitor\',\'ai\')" title="AI Spec">§</span></h2>'
|
||
+'<div id="usage-cards" style="display:flex;flex-wrap:wrap;gap:10px;"></div>'
|
||
+'<div style="margin-top:8px;display:flex;gap:8px;align-items:center">'
|
||
+'<button class="btn s" id="btn-usage-refresh">Refresh Now</button>'
|
||
+'<span id="usage-last" style="font-size:12px;color:var(--dim)">-</span>'
|
||
+'</div>';
|
||
ci.appendChild(us);
|
||
document.getElementById('btn-usage-refresh').onclick=function(){
|
||
var btn=this;btn.disabled=true;
|
||
document.getElementById('usage-last').textContent='采集中…';
|
||
fetch('/api/usage/refresh',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})
|
||
.then(function(r){return r.json()})
|
||
.then(function(d){
|
||
if(d.ok){toast('采集已触发,10-15s 后自动刷新');}
|
||
else{toast('Refresh 失败: '+(d.error||'unknown'),'err');}
|
||
setTimeout(function(){btn.disabled=false;fI()},8000);
|
||
})
|
||
.catch(function(e){toast('Network error','err');btn.disabled=false;});
|
||
};
|
||
}
|
||
try{
|
||
var e5=await fetch('/api/usage').then(function(r){return r.json()});
|
||
var cards=document.getElementById('usage-cards');
|
||
var lu=document.getElementById('usage-last');
|
||
if(!e5||!e5.ok){
|
||
if(lu)lu.textContent='—'+(e5&&e5.error?' '+e5.error:'');
|
||
if(cards)cards.innerHTML='<div style="color:var(--dim);padding:8px">'+esc(e5&&e5.error||'未采集')+'</span>';
|
||
}else{
|
||
// Update last refresh time
|
||
if(lu){
|
||
var ts=e5.last_refresh_iso;
|
||
var d = new Date(ts);
|
||
var pad=function(n){return String(n).padStart(2,'0')};
|
||
lu.textContent='上次采集: '+d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())
|
||
+' '+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds())+' CST';
|
||
}
|
||
// Render cards grouped by provider
|
||
var html='';
|
||
var accts=e5.accounts||[];
|
||
// Group by provider
|
||
var groups={},order=[];
|
||
for(var i=0;i<accts.length;i++){
|
||
var a=accts[i];
|
||
var p=a.provider||'other';
|
||
if(!groups[p]){groups[p]=[];order.push(p);}
|
||
groups[p].push(a);
|
||
}
|
||
// Provider names
|
||
var pn={opencode_go:'OpenCode Go',kimi:'Kimi',sensenova:'SenseNova'};
|
||
for(var gi=0;gi<order.length;gi++){
|
||
var p=order[gi];
|
||
if(groups[p].length>0){
|
||
html+='<div style="font-size:19px;color:var(--accent);margin:8px 0 4px;font-weight:600">'+esc(pn[p]||p)+'</div>';
|
||
html+='<div style="display:flex;flex-wrap:wrap;gap:10px;margin-bottom:12px">';
|
||
var ag=groups[p];
|
||
for(var i=0;i<ag.length;i++){var a=ag[i];
|
||
var maxPct=0;
|
||
if(a.rolling&&a.rolling.usage_percent!=null)maxPct=Math.max(maxPct,a.rolling.usage_percent);
|
||
if(a.weekly&&a.weekly.usage_percent!=null)maxPct=Math.max(maxPct,a.weekly.usage_percent);
|
||
if(a.monthly&&a.monthly.usage_percent!=null)maxPct=Math.max(maxPct,a.monthly.usage_percent);
|
||
var edge='var(--border)';
|
||
var barColor='var(--accent)';
|
||
if(maxPct>=95){edge='#e53935';barColor='#e53935';}
|
||
else if(maxPct>=80){edge='#fb8c00';barColor='#fb8c00';}
|
||
html+='<div style="background:var(--card);border:1px solid '+edge+';border-radius:8px;padding:10px 12px;min-width:200px;flex:1;max-width:260px">'
|
||
+'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">'
|
||
+'<strong style="font-size:14px">'+esc(a.label||a.key_id||a.workspace_id||'?')+'</strong>';
|
||
if(a.session_expired){
|
||
html+='<span style="font-size:11px;color:#e53935;background:rgba(229,57,53,.1);padding:2px 6px;border-radius:4px">SESSION EXPIRED</span>';
|
||
}else if(a.error){
|
||
html+='<span style="font-size:11px;color:var(--dim)" title="'+esc(a.error)+'">⚠</span>';
|
||
}else{
|
||
var statusTags='';
|
||
if(a.subscribed===false){
|
||
statusTags+='<span style="font-size:11px;color:#e53935;background:rgba(229,57,53,.1);padding:2px 6px;border-radius:4px;margin-right:4px">UNSUBSCRIBED</span>';
|
||
}else if(a.renewal==='cancelled'){
|
||
statusTags+='<span style="font-size:11px;color:#fb8c00;background:rgba(251,140,0,.1);padding:2px 6px;border-radius:4px;margin-right:4px" title="续订已取消·当前周期仍有效">⏳ 续订取消</span>';
|
||
}
|
||
html+=statusTags+'<span style="font-size:11px;color:var(--dim)">'+maxPct+'% max</span>';
|
||
}
|
||
html+='</div>';
|
||
if(a.session_expired||a.error){
|
||
html+='<div style="font-size:12px;color:var(--dim);padding:4px 0">'+esc(a.error||'请重新登录此账号')+'</div>';
|
||
}else{
|
||
html+=_renderUsageMetric('5h rolling',a.rolling,barColor);
|
||
html+=_renderUsageMetric('Weekly',a.weekly,barColor);
|
||
html+=_renderUsageMetric('Monthly',a.monthly,barColor);
|
||
}
|
||
html+='</div>';}
|
||
html+='</div>';}}
|
||
if(!accts.length){html='<div style="color:var(--dim);padding:8px">未配置账号 (accounts.json 未就绪,或 cookies 未提取)</div>';}
|
||
if(cards)cards.innerHTML=html;
|
||
}
|
||
}catch(e5){}
|
||
|
||
/* --- OCG Router Proxy section --- */
|
||
var pr=document.getElementById('proxy-router-section');
|
||
if(!pr){
|
||
pr=document.createElement('div');pr.id='proxy-router-section';pr.className='ps';pr.style.marginTop='16px';
|
||
pr.innerHTML='<h2>OCG 路由代理<span class="help-btn" onclick="showModuleHelp(\'ocg_router\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'ocg_router\',\'ai\')" title="AI Spec">\u00a7</span></h2>'
|
||
+'<div id="proxy-status-line" style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:10px;font-size:18px"></div>'
|
||
+'<div id="proxy-pool" style="display:flex;flex-wrap:wrap;gap:6px"></div>';
|
||
ci.appendChild(pr);
|
||
}
|
||
try{
|
||
await _loadProxyStatus();
|
||
}catch(prE){}
|
||
}catch(e){}
|
||
}
|
||
async function _loadProxyStatus(){
|
||
var prr=await fetch('/api/proxy/status');var prd=await prr.json();
|
||
var pl=document.getElementById('proxy-status-line');
|
||
var pp=document.getElementById('proxy-pool');
|
||
if(!prd.ok||prd.proxy_running===false){
|
||
if(pl)pl.innerHTML='<span style="color:var(--red)">\u2717 代理未运行</span>';
|
||
if(pp)pp.innerHTML='<span style="color:var(--dim);font-size:18px">'+esc(prd.error||'无法连接 :19878')+'</span>';
|
||
}else{
|
||
if(pl)pl.innerHTML='<span style="color:var(--green);font-weight:600">当前路由: '+esc(prd.current_key||'—')+'</span>'
|
||
+'<span style="color:var(--dim)">'+prd.total_requests+' 请求</span>'
|
||
+'<span style="color:var(--dim)">'+prd.pool_healthy+'/'+prd.pool_size+' healthy</span>';
|
||
if(pp){
|
||
var ph='';
|
||
for(var i=0;i<prd.pool.length;i++){
|
||
var k=prd.pool[i];
|
||
var mp=k.monthly_pct!=null?k.monthly_pct:0;
|
||
var wp=k.weekly_pct!=null?k.weekly_pct:0;
|
||
var ok=k.healthy&&!k.in_cooldown;
|
||
var edge=ok?'var(--green)':'var(--red)';
|
||
var bg=ok?'var(--card)':(k.in_cooldown?'rgba(251,140,0,.1)':'rgba(229,57,53,.06)');
|
||
ph+='<div style="background:'+bg+';border:1px solid '+edge+';border-radius:6px;padding:6px 10px;min-width:150px;font-size:17px">'
|
||
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px">'
|
||
+'<strong style="font-size:16px">'+esc(k.key_id||'?')+'</strong>'
|
||
+(k.hits>0?'<span style="font-size:14px;color:var(--accent)">'+k.hits+' hits</span>':'')
|
||
+'</div>'
|
||
+'<div style="font-size:14px;color:var(--dim)">M '+mp+'% \u00b7 W '+wp+'%</div>'
|
||
+(k.in_cooldown?'<div style="font-size:13px;color:var(--yellow);margin-top:1px">冷却中</div>':'')
|
||
+(k.error?'<div style="font-size:13px;color:var(--red);margin-top:1px">\u2717 '+esc(k.error)+'</div>':'')
|
||
+'<button onclick="toggleOcgKey(\''+esc(k.key_id)+'\',true)" style="margin-top:4px;font-size:13px;padding:1px 10px;border:1px solid var(--green);background:var(--card);color:var(--green);border-radius:4px;cursor:pointer">启用</button>'
|
||
+'<button onclick="toggleOcgKey(\''+esc(k.key_id)+'\',false)" style="margin-left:6px;font-size:13px;padding:1px 10px;border:1px solid var(--red);background:var(--card);color:var(--red);border-radius:4px;cursor:pointer">停用</button>'
|
||
+'</div>';
|
||
}
|
||
pp.innerHTML=ph;
|
||
}
|
||
}
|
||
}
|
||
function toggleOcgKey(kid,enabled){
|
||
if(!kid)return;
|
||
var action=enabled?'启用':'停用';
|
||
if(!confirm('确认'+action+' key '+kid+'?'+(enabled?'':'停用后该 key 将不再参与 OCG 路由。')))return;
|
||
fetch('/api/proxy/toggle-key',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({key_id:kid,enabled:enabled})})
|
||
.then(function(r){return r.json();})
|
||
.then(function(d){
|
||
if(d.ok){
|
||
_loadProxyStatus();
|
||
if(window.showToast)showToast('key '+kid+' 已'+action);
|
||
}else{
|
||
alert('操作失败: '+(d.error||'未知错误'));
|
||
}
|
||
})
|
||
.catch(function(e){alert('请求失败: '+e);});
|
||
}
|
||
function _fmtCountdown(sec){
|
||
if(sec==null)return '—';
|
||
if(sec<=0)return '即将重置';
|
||
sec=Math.floor(sec);
|
||
var d=Math.floor(sec/86400),h=Math.floor((sec%86400)/3600),m=Math.floor((sec%3600)/60),s=sec%60;
|
||
if(d>0)return d+'d '+h+'h';
|
||
if(h>0)return h+'h '+m+'m';
|
||
if(m>0)return m+'m '+s+'s';
|
||
return s+'s';
|
||
}
|
||
function _renderUsageMetric(label,m,barColor){
|
||
if(!m||m.usage_percent==null){
|
||
return '<div style="margin-bottom:6px"><div style="font-size:11px;color:var(--dim);display:flex;justify-content:space-between"><span>'+esc(label)+'</span></div><div style="background:var(--bg);border-radius:4px;height:6px;margin-top:2px"></div><div style="font-size:10px;color:var(--dim);margin-top:1px">—</div></div>';
|
||
}
|
||
var pct=m.usage_percent;
|
||
var txt=pct+'% · '+_fmtCountdown(m.reset_in_sec);
|
||
return '<div style="margin-bottom:6px">'
|
||
+'<div style="font-size:11px;color:var(--dim);display:flex;justify-content:space-between"><span>'+esc(label)+'</span><span>'+(pct>=95?'CRITICAL':pct>=80?'WARNING':'OK')+'</span></div>'
|
||
+'<div style="background:var(--bg);border-radius:4px;height:6px;margin-top:2px;overflow:hidden">'
|
||
+'<div style="background:'+barColor+';height:100%;width:'+pct+'%;transition:width .3s"></div>'
|
||
+'</div>'
|
||
+'<div style="font-size:10px;color:var(--dim);margin-top:1px">'+esc(txt)+'</div>'
|
||
+'</div>';
|
||
}
|
||
async function fH(){try{
|
||
var r=await fetch('/api/prd'),d=await r.json();
|
||
if(!d.ok||!d.content){fill('prd','Failed');return}
|
||
var h='<h2>H 产品需求<span class="help-btn" onclick="showModuleHelp(\'prd\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'prd\',\'ai\')" title="AI Spec">§</span></h2><div style=padding:8px 0>'+mdRender(d.content)+'</div>';
|
||
try{var rh=await fetch('/api/prd/history'),dh=await rh.json();
|
||
if(dh.ok&&dh.log&&dh.log.length){
|
||
h+='<div class=section style=margin-top:16px><h2 style=font-size:17px;cursor:pointer onclick="var e=document.getElementById(\'prd-history\');e.style.display=e.style.display===\'none\'?\'block\':\'none\'">历史版本 <span style=color:var(--dim);font-size:18px>('+dh.count+' commits)</span></h2><div id=prd-history style=display:none;margin-top:8px>';
|
||
for(var i=0;i<dh.log.length;i++){h+='<div class=log-line>'+esc(dh.log[i])+'</div>';}
|
||
h+='</div></div>';}
|
||
}catch(e){}
|
||
fill('prd',h)}catch(e){fill('prd','Error')}}
|
||
|
||
async function fTests(){try{var r=await fetch('/api/tests'),d=await r.json();var h='<h2>K 测试报告<span class="help-btn" onclick="showModuleHelp(\'tests\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'tests\',\'ai\')" title="AI Spec">§</span></h2>';if(!d.ok||!d.tests){fill('tests','<span class=tag r>Error</span>');return}
|
||
var s=d.summary;h+='<div style=display:flex;gap:12px;margin-bottom:16px><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--accent)>'+s.total+'</div><div style=color:var(--dim);font-size:19px>Total</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--green)>'+s.passed+'</div><div style=color:var(--dim);font-size:19px>Passed</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--red)>'+s.failed+'</div><div style=color:var(--dim);font-size:19px>Failed</div></div></div>';
|
||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--yellow)>Expected Fail ('+s.failed+')</div>';
|
||
d.tests.filter(function(t){return!t.ok&&t.expected}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--yellow);border-radius:6px;padding:6px 12px;margin-bottom:4px"><span style="background:var(--yellow);color:#000;padding:1px 6px;border-radius:3px;font-weight:700;font-size:18px">EXPECTED</span> <span style=font-size:19px>'+esc(t.name)+'</span> <span style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</span></div>';});
|
||
var realFails=d.tests.filter(function(t){return!t.ok&&!t.expected});
|
||
if(realFails.length){h+='<div style=margin-bottom:8px;font-weight:600;color:var(--red)>Real Fail ('+realFails.length+')</div>';realFails.forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:6px;padding:8px 12px;margin-bottom:6px"><div style=display:flex;align-items:center;gap:8px><span style="background:var(--red);color:#fff;padding:2px 8px;border-radius:4px;font-weight:700;font-size:19px">FAIL</span><strong>'+esc(t.name)+'</strong></div><div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(t.detail||'')+'</div></div>';});}
|
||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--green)>Passed ('+s.passed+')</div>';
|
||
d.tests.filter(function(t){return t.ok}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:6px 12px;margin-bottom:4px"><span style="background:var(--green);color:#fff;padding:1px 6px;border-radius:3px;font-weight:700;font-size:18px">PASS</span> <span style=font-size:19px>'+esc(t.name)+'</span> <span style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</span></div>';});
|
||
h+='<div style=margin-top:8px;color:var(--dim);font-size:19px>Last run: '+esc(d.time||'')+'</div>';
|
||
fill('tests',h)}catch(e){fill('tests','<span class=tag r>Error: '+esc(e.message)+'</span>')}}
|
||
|
||
var FETCHES={agents:fa,spec:fS2,tests:fTests,health:fHealth,prd:fH,kanban:fK,infra:fI,easytier:fE2,rdp:fR,principles:function(){return(FETCHES[as]||function(){})()}};
|
||
async function loadAll(){var d=new Date();qs('subtitle').textContent=d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();var skip=['spec','prd','health'];await Promise.all(Object.keys(FETCHES).filter(function(k){return skip.indexOf(k)<0}).map(function(k){return FETCHES[k]()}));}
|
||
|
||
async function fE2(){try{var r=await fetch('/api/easytier'),d=await r.json(),s=d.status||{};var e1=document.getElementById('et-w');if(e1){e1.className='d '+(s.windows==='running'?'ok':'stopped')}var e2=document.getElementById('et-246');if(e2){e2.className='d '+(s['246']==='running'?'ok':'stopped')}if(d.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=d.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=d.virtual_ips['246']||''}}catch(e){}}
|
||
async function fR(){try{var r=await fetch('/api/rdp'),d=await r.json();var e1=document.getElementById('rdp-st');if(e1){e1.className='d '+(d.rdp_enabled?'ok':'stopped')}var e2=document.getElementById('rdp-tun');if(e2){e2.className='d '+(d.tunnel_running?'ok':'stopped')}var info=document.getElementById('rdp-info');if(info)info.textContent=d.public_endpoint||''}catch(e){}}
|
||
var fS2_c=null;var fS2_last='';async function fS2(){try{
|
||
var r=await fetch('/api/spec'),d=await r.json();
|
||
if(!d.ok||!d.content){fill('spec','Failed');return}
|
||
var h='<h2>G 开发规范<span class="help-btn" onclick="showModuleHelp(\'dev_spec\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'dev_spec\',\'ai\')" title="AI Spec">§</span></h2><div style=padding:8px 0>'+mdRender(d.content)+'</div>';
|
||
try{var rh=await fetch('/api/spec/history'),dh=await rh.json();
|
||
if(dh.ok&&dh.log&&dh.log.length){
|
||
h+='<div class=section style=margin-top:16px><h2 style=font-size:17px;cursor:pointer onclick="var e=document.getElementById(\'spec-history\');e.style.display=e.style.display===\'none\'?\'block\':\'none\'">历史版本 <span style=color:var(--dim);font-size:18px>('+dh.count+' commits)</span></h2><div id=spec-history style=display:none;margin-top:8px>';
|
||
for(var i=0;i<dh.log.length;i++){h+='<div class=log-line>'+esc(dh.log[i])+'</div>';}
|
||
h+='</div></div>';}
|
||
}catch(e){}
|
||
fill('spec',h)}catch(e){fill('spec','Error')}}
|
||
|
||
function mdRender(md){if(!md)return'';var h='';var blocks=md.split('\n\n');for(var b=0;b<blocks.length;b++){var block=blocks[b].trim();if(!block)continue;
|
||
if(block.startsWith('```')){var code=block.replace(/^```[\s\S]*?\n/,'').replace(/```$/,'').trim();h+='<pre class=mono style=background:var(--bg);padding:12px;border-radius:6px;overflow-x:auto>'+esc(code)+'</pre>';continue;}
|
||
if(block.startsWith('### ')){var txt=block.slice(4);var anno=txt.match(/<!--(.*?)-->/);if(anno){var status=anno[1].trim();txt=txt.replace(/<!--.*?-->/,'').trim();var badge=status.indexOf('✅')>=0?'<span class=tag g style=margin-left:8px>'+esc(status)+'</span>':status.indexOf('🔄')>=0?'<span class=tag y style=margin-left:8px>'+esc(status)+'</span>':'<span class=tag r style=margin-left:8px>'+esc(status)+'</span>';h+='<h3 style=color:var(--accent);margin:16px 0 6px;font-size:19px>'+esc(txt)+badge+'</h3>'}else{h+='<h3 style=color:var(--accent);margin:16px 0 6px;font-size:19px>'+esc(txt)+'</h3>'}continue;}
|
||
if(block.startsWith('## ')){var txt=block.slice(3);var anno=txt.match(/<!--(.*?)-->/);if(anno){var status=anno[1].trim();txt=txt.replace(/<!--.*?-->/,'').trim();var badge=status.indexOf('✅')>=0?'<span class=tag g style=margin-left:8px>'+esc(status)+'</span>':status.indexOf('🔄')>=0?'<span class=tag y style=margin-left:8px>'+esc(status)+'</span>':'<span class=tag r style=margin-left:8px>'+esc(status)+'</span>';h+='<h2 style=color:var(--accent);margin:20px 0 8px;font-size:19px;border-bottom:1px solid var(--border);padding-bottom:4px>'+esc(txt)+badge+'</h2>'}else{h+='<h2 style=color:var(--accent);margin:20px 0 8px;font-size:19px;border-bottom:1px solid var(--border);padding-bottom:4px>'+esc(block.slice(3))+'</h2>'}continue;}
|
||
if(block.startsWith('# ')){h+='<h1 style=color:var(--accent);margin:24px 0 10px;font-size:24px>'+esc(block.slice(2))+'</h1>';continue;}
|
||
if(block.startsWith('> ')){h+='<blockquote style="border-left:3px solid var(--accent);padding:8px 16px;margin:8px 0;color:var(--dim);background:var(--bg);border-radius:4px">'+esc(block.slice(2))+'</blockquote>';continue;}
|
||
if(block.startsWith('---')||block.startsWith('***')){h+='<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">';continue;}
|
||
if(block.startsWith('- ')||block.startsWith('* ')){var items=block.split('\n'),inList=false;for(var i=0;i<items.length;i++){var li=items[i].replace(/^[-*]\s*/,'').trim();if(li){if(!inList){h+='<ul style=margin:4px 0;padding-left:20px>';inList=true}h+='<li style=margin:2px 0>'+inMd(li)+'</li>'}}if(inList)h+='</ul>';continue;}
|
||
if(/^\d+\.\s/.test(block)){var items=block.split('\n'),inList=false;for(var i=0;i<items.length;i++){var li=items[i].replace(/^\d+\.\s*/,'').trim();if(li){if(!inList){h+='<ol style=margin:4px 0;padding-left:20px>';inList=true}h+='<li style=margin:2px 0>'+inMd(li)+'</li>'}}if(inList)h+='</ol>';continue;}
|
||
if(block.indexOf('|')>=0&&block.indexOf('\n|')>=0){var rows=block.split('\n'),inTable=false;for(var i=0;i<rows.length;i++){if(rows[i].indexOf('---')>=0)continue;var cells=rows[i].split('|').filter(function(c){return c.trim()});if(cells.length<2)continue;if(!inTable){h+='<table style=margin:8px 0;font-size:18px><tr>';for(var c=0;c<cells.length;c++){h+='<th style="padding:4px 8px;border:1px solid var(--border);color:var(--dim);font-weight:600;background:var(--bg)">'+inMd(cells[c].trim())+'</th>'}h+='</tr>';inTable=true}else{h+='<tr>';for(var c=0;c<cells.length;c++){h+='<td style="padding:4px 8px;border:1px solid var(--border)">'+inMd(cells[c].trim())+'</td>'}h+='</tr>'}}if(inTable)h+='</table>';continue;}
|
||
h+='<p style="line-height:1.7;margin:6px 0">'+inMd(block)+'</p>';}
|
||
return h;
|
||
function inMd(t){return t.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\*([^*]+)\*/g,'<em>$1</em>').replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:1px 4px;border-radius:3px;font-size:0.9em">$1</code>').replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent)" target=_blank>$1</a>')}}
|
||
init();loadAll();setInterval(loadAll,10000);setInterval(fK,15000);
|
||
|
||
/* --- WeChat QR: 获取二维码(触发容器 /api/bot/login) + 刷新 --- */
|
||
function renderWechatQR(qrUrl){
|
||
var box=document.getElementById('wechat-qr-get-status');
|
||
if(box) box.innerHTML='<img id=wechat-qr-img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data='+encodeURIComponent(qrUrl)+'" style="border:2px solid var(--green);border-radius:6px">'
|
||
+'<div style="margin-top:6px;font-size:13px;color:var(--dim)">用微信扫描上方二维码登录(约5分钟有效,过期请点下方"刷新二维码")</div>'
|
||
+'<button class="btn r" style="margin-top:6px;font-size:13px;padding:3px 10px" onclick="refreshWechatQR()">刷新二维码</button>';
|
||
var getBtn=document.getElementById('wechat-qr-get');
|
||
if(getBtn) getBtn.style.display='none';
|
||
}
|
||
async function getWechatQR(){
|
||
var st=document.getElementById('wechat-qr-get-status');
|
||
var btn=document.getElementById('wechat-qr-get');
|
||
if(st)st.innerHTML='<span style=color:var(--yellow)>\u6b63\u5728\u83b7\u53d6\u4e8c\u7ef4\u7801...</span>';
|
||
if(btn)btn.disabled=true;
|
||
try{
|
||
var r=await fetch('/api/wechat/trigger-login',{method:'POST'});
|
||
var d=await r.json();
|
||
if(d.ok){
|
||
if(st)st.innerHTML='<span style=color:var(--green)>\u5df2\u89e6\u53d1\uff0c\u6b63\u5728\u751f\u6210\u4e8c\u7ef4\u7801...</span>';
|
||
// 触发后轮询 status 拿 qr_url(容器生成二维码需要几秒)
|
||
for(var i=0;i<10;i++){
|
||
await new Promise(res=>setTimeout(res,2000));
|
||
try{
|
||
var rs=await fetch('/api/wechat/status'),sd=await rs.json();
|
||
if(sd.qr_url){renderWechatQR(sd.qr_url);return;}
|
||
}catch(e){}
|
||
}
|
||
if(st)st.innerHTML='<span style=color:var(--red)>\u4e8c\u7ef4\u7801\u672a\u751f\u6210\uff0c\u8bf7\u91cd\u8bd5</span>';
|
||
if(btn)btn.disabled=false;
|
||
}else{
|
||
if(st)st.innerHTML='<span style=color:var(--red)>\u83b7\u53d6\u5931\u8d25: '+esc(d.error||'')+'</span>';
|
||
if(btn)btn.disabled=false;
|
||
}
|
||
}catch(e){
|
||
if(st)st.innerHTML='<span style=color:var(--red)>\u83b7\u53d6\u5931\u8d25: '+esc(e.message||'')+'</span>';
|
||
if(btn)btn.disabled=false;
|
||
}
|
||
}
|
||
async function refreshWechatQR(){
|
||
var st=document.getElementById('wechat-qr-get-status');
|
||
if(st && !document.getElementById('wechat-qr-img')) st.innerHTML='<span style=color:var(--yellow)>正在刷新二维码...</span>';
|
||
try{
|
||
await fetch('/api/wechat/trigger-login',{method:'POST'});
|
||
for(var i=0;i<10;i++){
|
||
await new Promise(res=>setTimeout(res,2000));
|
||
try{
|
||
var rs=await fetch('/api/wechat/status'),sd=await rs.json();
|
||
if(sd.qr_url){ renderWechatQR(sd.qr_url); return; }
|
||
}catch(e){}
|
||
}
|
||
}catch(e){}
|
||
}
|
||
</script></body></html> |