fix: remove human input terminal, change to read-only operation log
This commit is contained in:
parent
f6e14acbad
commit
da7ac1a195
@ -6,7 +6,7 @@
|
||||
<title>光湖服务器主控台</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;--border:#1e3251;--accent:#4f8ef7;--text:#e2e8f0;--dim:#64748b;--ok:#34d399;--warn:#fbbf24;--err:#f87171;--r:14px;--rs:8px;--term-bg:#0a0e1a;--term-text:#a8d8a8;--term-green:#5faf5f;--term-dim:#4a5568}
|
||||
:root{--bg:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;--border:#1e3251;--accent:#4f8ef7;--text:#e2e8f0;--dim:#64748b;--ok:#34d399;--warn:#fbbf24;--err:#f87171;--r:14px;--rs:8px;--log-bg:#0a0e1a;--log-text:#a8d8a8;--log-green:#5faf5f}
|
||||
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;font-size:14px;padding:24px;max-width:1200px;margin:0 auto}
|
||||
h1{font-size:22px;margin-bottom:4px;display:flex;align-items:center;gap:10px}
|
||||
.sub{color:var(--dim);font-size:13px;margin-bottom:24px}
|
||||
@ -36,11 +36,6 @@ h1{font-size:22px;margin-bottom:4px;display:flex;align-items:center;gap:10px}
|
||||
.btn-ok:hover{opacity:.85}
|
||||
.btn-no{background:var(--s3);color:var(--text);border:1px solid var(--border);margin-left:8px}
|
||||
.btn-no:hover{border-color:var(--err)}
|
||||
.btn-accent{background:var(--accent);color:#fff}
|
||||
.btn-accent:hover{opacity:.85}
|
||||
.btn-warn{background:var(--err);color:#fff}
|
||||
.btn-warn:hover{opacity:.85}
|
||||
.btn-sm{padding:4px 10px;font-size:11px}
|
||||
.empty{color:var(--dim);font-size:13px;text-align:center;padding:20px}
|
||||
.refresh-btn{background:var(--s3);border:1px solid var(--border);color:var(--text);padding:6px 14px;border-radius:var(--rs);cursor:pointer;font-size:12px;font-family:inherit}
|
||||
.refresh-btn:hover{border-color:var(--accent)}
|
||||
@ -50,34 +45,29 @@ h1{font-size:22px;margin-bottom:4px;display:flex;align-items:center;gap:10px}
|
||||
.breathing{animation:breathe 2s ease-in-out infinite}
|
||||
@keyframes breathe{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
|
||||
/* Terminal */
|
||||
.term-bar{display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
|
||||
.term-bar select{background:var(--s2);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:var(--rs);font-size:13px;font-family:inherit;cursor:pointer;min-width:200px}
|
||||
.term-bar select:focus{outline:none;border-color:var(--accent)}
|
||||
.term-bar .status-badge{font-size:12px;padding:4px 10px;border-radius:12px;background:var(--s2);border:1px solid var(--border)}
|
||||
.term-bar .status-badge.ready{color:var(--ok);border-color:var(--ok)}
|
||||
.term-bar .status-badge.waiting{color:var(--warn);border-color:var(--warn)}
|
||||
.term-bar .status-badge.rejected{color:var(--err);border-color:var(--err)}
|
||||
.term-bar .status-badge.closed{color:var(--dim);border-color:var(--dim)}
|
||||
.term-output{background:var(--term-bg);border:1px solid var(--border);border-radius:var(--rs);padding:12px 16px;min-height:200px;max-height:400px;overflow-y:auto;font-family:'SF Mono','Cascadia Code','Fira Code','Menlo',monospace;font-size:13px;line-height:1.6;color:var(--term-text);white-space:pre-wrap;word-break:break-all;margin-bottom:8px}
|
||||
.term-output .prompt{color:var(--term-green)}
|
||||
.term-output .stdout{color:var(--term-text)}
|
||||
.term-output .stderr{color:var(--err)}
|
||||
.term-output .system{color:var(--dim);font-style:italic}
|
||||
.term-output .result-ok{color:var(--ok)}
|
||||
.term-output .result-err{color:var(--err)}
|
||||
.term-output .cursor{display:inline-block;width:8px;height:14px;background:var(--term-green);animation:blink 1s step-end infinite;vertical-align:text-bottom;margin-left:2px}
|
||||
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
|
||||
.term-input-row{display:flex;gap:8px;align-items:center}
|
||||
.term-input-row input{flex:1;background:var(--term-bg);border:1px solid var(--border);color:var(--term-text);padding:8px 12px;border-radius:var(--rs);font-family:'SF Mono','Cascadia Code','Fira Code','Menlo',monospace;font-size:13px;outline:none}
|
||||
.term-input-row input:focus{border-color:var(--accent)}
|
||||
.term-input-row input:disabled{opacity:.5;cursor:not-allowed}
|
||||
.term-hint{font-size:11px;color:var(--dim);margin-top:4px}
|
||||
.tabs{display:flex;gap:4px;margin-bottom:8px;flex-wrap:wrap}
|
||||
.tab{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs) var(--rs) 0 0;padding:6px 14px;font-size:12px;color:var(--dim);cursor:pointer;border-bottom:none;margin-bottom:-1px}
|
||||
.tab.active{background:var(--term-bg);color:var(--text);border-color:var(--border)}
|
||||
.tab .close-tab{margin-left:6px;color:var(--dim);font-size:14px;cursor:pointer}
|
||||
.tab .close-tab:hover{color:var(--err)}
|
||||
/* Operation Log */
|
||||
.log-panel{background:var(--log-bg);border:1px solid var(--border);border-radius:var(--rs);min-height:180px;max-height:420px;overflow-y:auto;font-family:'SF Mono','Cascadia Code','Fira Code','Menlo',monospace;font-size:13px;line-height:1.7;color:var(--log-text);padding:12px 16px;margin-bottom:8px;white-space:pre-wrap;word-break:break-all}
|
||||
.log-panel .entry{display:flex;gap:8px;align-items:flex-start;padding:2px 0}
|
||||
.log-panel .time{color:var(--dim);font-size:11px;flex-shrink:0;min-width:60px}
|
||||
.log-panel .tag{font-size:11px;padding:1px 6px;border-radius:3px;flex-shrink:0;min-width:36px;text-align:center}
|
||||
.log-panel .tag.knock{color:var(--warn);border:1px solid var(--warn)}
|
||||
.log-panel .tag.auth{color:var(--ok);border:1px solid var(--ok)}
|
||||
.log-panel .tag.exec{color:var(--accent);border:1px solid var(--accent)}
|
||||
.log-panel .tag.done{color:var(--ok)}
|
||||
.log-panel .tag.fail{color:var(--err);border:1px solid var(--err)}
|
||||
.log-panel .tag.info{color:var(--dim);border:1px solid var(--dim)}
|
||||
.log-panel .msg{flex:1;font-size:13px}
|
||||
.log-panel .msg .cmd{color:var(--log-green)}
|
||||
.log-panel .msg .output{color:var(--log-text);display:block;padding-left:12px;white-space:pre-wrap;border-left:1px solid rgba(52,211,153,.2);margin-top:2px}
|
||||
.log-panel .msg .err{color:var(--err)}
|
||||
.log-empty{color:var(--dim);font-size:13px;text-align:center;padding:40px 20px}
|
||||
|
||||
/* Active Session Cards */
|
||||
.session-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:12px}
|
||||
.session-card{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px}
|
||||
.session-card .srv{font-size:13px;font-weight:500}
|
||||
.session-card .meta{font-size:11px;color:var(--dim);margin-top:4px;display:flex;gap:12px}
|
||||
.session-card .dot{width:8px;height:8px;display:inline-block;margin-right:4px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -95,34 +85,21 @@ h1{font-size:22px;margin-bottom:4px;display:flex;align-items:center;gap:10px}
|
||||
<!-- Dashboard -->
|
||||
<div class="dashboard" id="dashboard"></div>
|
||||
|
||||
<!-- Knock Panel -->
|
||||
<!-- Auth Requests -->
|
||||
<div class="panel" id="knock-panel">
|
||||
<h2 id="pending-title">铸渊操作请求</h2>
|
||||
<div id="pending-requests"><div class="empty">暂无待处理的请求</div></div>
|
||||
</div>
|
||||
|
||||
<!-- MCP Terminal Panel -->
|
||||
<div class="panel" id="terminal-panel">
|
||||
<!-- Operation Log (read-only) -->
|
||||
<div class="panel">
|
||||
<h2 style="display:flex;align-items:center;gap:8px">
|
||||
<span>MCP 命令终端</span>
|
||||
<span style="font-size:11px;color:var(--dim);font-weight:400">v2.0 · 输入命令直接执行</span>
|
||||
<span>铸渊操作日志</span>
|
||||
<span style="font-size:11px;color:var(--dim);font-weight:400">只读 · 铸渊通过对话驱动</span>
|
||||
</h2>
|
||||
<div class="term-bar">
|
||||
<select id="term-server"><option value="">-- 选择目标服务器 --</option></select>
|
||||
<button class="btn btn-accent btn-sm" id="term-open-btn" onclick="openTerminal()">打开终端</button>
|
||||
<span class="status-badge" id="term-status" style="display:none">-</span>
|
||||
</div>
|
||||
<div id="term-container" style="display:none">
|
||||
<div class="tabs" id="term-tabs"></div>
|
||||
<div class="term-output" id="term-output">
|
||||
<span class="system">// 选择服务器并点击 "打开终端" 开始</span>
|
||||
</div>
|
||||
<div class="term-input-row">
|
||||
<input type="text" id="term-input" placeholder="输入命令..." disabled>
|
||||
<button class="btn btn-accent btn-sm" id="term-send-btn" onclick="sendTermCmd()" disabled>发送</button>
|
||||
<button class="btn btn-warn btn-sm" id="term-close-btn" onclick="closeTerminal()" style="display:none">关闭终端</button>
|
||||
</div>
|
||||
<div class="term-hint" id="term-hint">提示: 打开终端后冰朔会在控制台看到敲门请求,授权后即可输入命令</div>
|
||||
<div id="active-sessions"></div>
|
||||
<div class="log-panel" id="log-output">
|
||||
<div class="log-empty">等待铸渊发起操作...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -130,16 +107,16 @@ h1{font-size:22px;margin-bottom:4px;display:flex;align-items:center;gap:10px}
|
||||
|
||||
<script>
|
||||
const API='/console/api';
|
||||
let iv, termIv;
|
||||
let termSessionId = null;
|
||||
let termReady = false;
|
||||
const serverList = [];
|
||||
let iv, logIv;
|
||||
const MAX_LOG = 200;
|
||||
|
||||
function toast(m,c){const t=document.getElementById('toast');t.textContent=m;t.style.borderColor=c||'var(--ok)';t.style.color=c||'var(--ok)';t.style.display='block';setTimeout(()=>t.style.display='none',4000)}
|
||||
|
||||
async function fetchS(){try{const r=await fetch(API+'/servers');return await r.json()}catch(e){return{servers:[]}}}
|
||||
async function fetchP(){try{const r=await fetch(API+'/pending');return await r.json()}catch(e){return{requests:[]}}}
|
||||
async function fetchSessions(){try{const r=await fetch(API+'/terminal/sessions');return await r.json()}catch(e){return{sessions:[]}}}
|
||||
|
||||
// ===== Dashboard =====
|
||||
function renderS(data){
|
||||
const d=document.getElementById('dashboard'),sv=data.servers||[];
|
||||
if(!sv.length){d.innerHTML='<div class="empty">正在采集...</div>';return}
|
||||
@ -151,213 +128,80 @@ function renderS(data){
|
||||
return '<div class="card '+c+'"><div class="card-head"><span class="card-name">'+s.name+'</span><span class="dot '+c+(s.alive?' breathing':'')+'"></span></div><div class="card-meta"><span>'+s.code+'</span><span>'+s.ip+'</span>'+(s.latency?'<span>'+s.latency+'ms</span>':'')+'</div><div class="card-status"><span class="'+(s.alive?'ok':'err')+'">'+st+lt+'</span></div><div class="last-seen">最后: '+ls+'</div></div>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
// ===== Auth Requests =====
|
||||
function renderP(data){
|
||||
const d=document.getElementById('pending-requests'),req=data.requests||[],t=document.getElementById('pending-title');
|
||||
if(!req.length){t.textContent='暂无操作请求';d.innerHTML='<div class="empty">铸渊没有待处理的请求</div>';return}
|
||||
t.textContent=req.length+' 条待处理';
|
||||
d.innerHTML=req.map(r=>'<div class="request-box"><div class="info"><div class="target">'+r.target_name+'</div><div class="reason">'+r.reason+'</div><div style="font-size:11px;color:var(--dim);margin-top:4px">'+new Date(r.created_at).toLocaleTimeString('zh-CN',{hour12:false})+'</div></div><div><button class="btn btn-ok" onclick="auth(\''+r.id+'\')">授权</button><button class="btn btn-no" onclick="rej(\''+r.id+'\')">拒绝</button></div></div>').join('')
|
||||
d.innerHTML=req.map(r=>{
|
||||
const ago=Math.floor((Date.now()-new Date(r.created_at).getTime())/1000);
|
||||
return '<div class="request-box"><div class="info"><div class="target">'+r.target_name+'</div><div class="reason">'+r.reason+'</div><div style="font-size:11px;color:var(--dim);margin-top:4px">'+ago+'秒前</div></div><div><button class="btn btn-ok" onclick="auth(\''+r.id+'\')">授权</button><button class="btn btn-no" onclick="rej(\''+r.id+'\')">拒绝</button></div></div>'
|
||||
}).join('')
|
||||
}
|
||||
async function auth(id){
|
||||
try{const r=await fetch(API+'/authorize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});const d=await r.json();if(d.ok){toast('已授权');refreshAll()}}catch(e){toast('失败: '+e.message,'var(--err)')}
|
||||
try{const r=await fetch(API+'/authorize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});const d=await r.json();if(d.ok){toast('已授权');addLog('auth','','授权成功');refreshAll()}}catch(e){toast('失败: '+e.message,'var(--err)')}
|
||||
}
|
||||
async function rej(id){
|
||||
try{await fetch(API+'/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});toast('已拒绝');refreshAll()}catch(e){}
|
||||
}
|
||||
async function refreshAll(){const[s,p]=await Promise.all([fetchS(),fetchP()]);renderS(s);renderP(p)}
|
||||
|
||||
// Terminal Module
|
||||
function initServerSelect(servers){
|
||||
const sel=document.getElementById('term-server');
|
||||
sel.innerHTML='<option value="">-- 选择目标服务器 --</option>';
|
||||
servers.forEach(s=>{
|
||||
const opt=document.createElement('option');
|
||||
opt.value=s.code;
|
||||
opt.textContent=s.name+' ('+s.code+(s.alive?' 在线':' 离线')+')';
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
serverList.length=0;
|
||||
serverList.push(...servers);
|
||||
try{await fetch(API+'/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});toast('已拒绝');addLog('fail','','已拒绝授权');refreshAll()}catch(e){}
|
||||
}
|
||||
|
||||
async function openTerminal(){
|
||||
if(termSessionId){toast('已有活跃终端会话,请先关闭','var(--warn)');return}
|
||||
// ===== Active Sessions =====
|
||||
function renderSessions(data){
|
||||
const container=document.getElementById('active-sessions');
|
||||
const sessions=data.sessions||[];
|
||||
if(!sessions.length){container.innerHTML='';return}
|
||||
|
||||
const sel=document.getElementById('term-server');
|
||||
const target=sel.value;
|
||||
if(!target){toast('请先选择目标服务器','var(--warn)');return}
|
||||
const statusClass={ready:'ok',awaiting_auth:'warn',rejected:'err',closed:'dim'};
|
||||
const statusLabel={ready:'就绪',awaiting_auth:'等待授权',rejected:'已拒绝',closed:'已关闭'};
|
||||
|
||||
const srv=serverList.find(s=>s.code===target);
|
||||
if(!srv){toast('未找到服务器','var(--err)');return}
|
||||
|
||||
setTermStatus('waiting','敲门中...');
|
||||
termSessionId=null;
|
||||
termReady=false;
|
||||
|
||||
document.getElementById('term-open-btn').disabled=true;
|
||||
document.getElementById('term-open-btn').textContent='连接中...';
|
||||
|
||||
appendOutput('system','> 向 '+srv.name+' ('+srv.code+') 发起终端会话请求...');
|
||||
appendOutput('system','> 正在敲门,等待冰朔授权...');
|
||||
|
||||
try{
|
||||
const r=await fetch(API+'/terminal/open',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({target,reason:'MCP命令终端'})
|
||||
});
|
||||
const d=await r.json();
|
||||
if(!d.ok){toast('打开终端失败','var(--err)');resetTermButtons();return}
|
||||
|
||||
termSessionId=d.session_id;
|
||||
setTermStatus('waiting','等待冰朔授权...');
|
||||
appendOutput('system','> 敲门已发送 (请求ID: '+d.request_id+')');
|
||||
appendOutput('system','> 冰朔会在上方操作请求面板看到此请求');
|
||||
appendOutput('system','> 授权后终端自动激活...');
|
||||
|
||||
document.getElementById('term-container').style.display='block';
|
||||
document.getElementById('term-close-btn').style.display='inline-block';
|
||||
document.getElementById('term-hint').textContent='等待冰朔在操作请求面板中点击"授权"...';
|
||||
|
||||
startPolling();
|
||||
}catch(e){
|
||||
toast('连接失败: '+e.message,'var(--err)');
|
||||
resetTermButtons();
|
||||
}
|
||||
container.innerHTML='<div style="font-size:12px;color:var(--dim);margin-bottom:8px">活跃会话:</div><div class="session-grid">'+
|
||||
sessions.map(s=>{
|
||||
const sc=statusClass[s.status]||'dim';
|
||||
const sl=statusLabel[s.status]||s.status;
|
||||
return '<div class="session-card"><div class="srv"><span class="dot" style="background:var(--'+sc+')"></span>'+s.server_name+' ('+s.server+')</div><div class="meta"><span>'+sl+'</span><span>'+s.age_seconds+'s</span><span>'+s.session_id.slice(0,16)+'...</span></div></div>'
|
||||
}).join('')+'</div>'
|
||||
}
|
||||
|
||||
function startPolling(){
|
||||
if(termIv) clearInterval(termIv);
|
||||
termIv=setInterval(async()=>{
|
||||
if(!termSessionId) return;
|
||||
try{
|
||||
const r=await fetch(API+'/terminal/status/'+termSessionId);
|
||||
const d=await r.json();
|
||||
if(!d.ok){return}
|
||||
|
||||
if(d.status==='ready' && !termReady){
|
||||
termReady=true;
|
||||
setTermStatus('ready','已授权 · 就绪');
|
||||
appendOutput('system','> 冰朔已授权!终端就绪');
|
||||
appendOutput('system','> 你可以输入命令了');
|
||||
document.getElementById('term-input').disabled=false;
|
||||
document.getElementById('term-send-btn').disabled=false;
|
||||
document.getElementById('term-hint').textContent='输入命令,按 Enter 发送 · 有效期为 5 分钟';
|
||||
toast('终端已就绪,可以输入命令','var(--ok)');
|
||||
document.getElementById('term-input').focus();
|
||||
clearInterval(termIv);
|
||||
termIv=null;
|
||||
}else if(d.status==='rejected'){
|
||||
setTermStatus('rejected','已拒绝');
|
||||
appendOutput('system','> 冰朔拒绝了授权请求');
|
||||
document.getElementById('term-hint').textContent='授权被拒绝,可以重新点击"打开终端"';
|
||||
clearInterval(termIv);
|
||||
termIv=null;
|
||||
resetTermButtons();
|
||||
}
|
||||
}catch(e){}
|
||||
},2000);
|
||||
}
|
||||
|
||||
async function sendTermCmd(){
|
||||
const input=document.getElementById('term-input');
|
||||
const cmd=input.value.trim();
|
||||
if(!cmd){return}
|
||||
if(!termSessionId || !termReady){toast('终端尚未就绪','var(--warn)');return}
|
||||
// ===== Operation Log =====
|
||||
function addLog(tag,msg,details){
|
||||
const log=document.getElementById('log-output');
|
||||
const empty=log.querySelector('.log-empty');
|
||||
if(empty) empty.remove();
|
||||
|
||||
input.value='';
|
||||
appendOutput('prompt','$ '+cmd);
|
||||
document.getElementById('term-send-btn').disabled=true;
|
||||
document.getElementById('term-input').disabled=true;
|
||||
const now=new Date();
|
||||
const time=now.toLocaleTimeString('zh-CN',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||||
|
||||
try{
|
||||
const r=await fetch(API+'/terminal/exec',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({session_id:termSessionId,cmd})
|
||||
});
|
||||
const d=await r.json();
|
||||
|
||||
if(d.ok && d.result){
|
||||
if(d.result.output !== undefined){
|
||||
appendOutput('stdout',d.result.output);
|
||||
}else if(d.result.stdout !== undefined){
|
||||
if(d.result.stdout) appendOutput('stdout',d.result.stdout);
|
||||
if(d.result.stderr) appendOutput('stderr',d.result.stderr);
|
||||
}else if(d.result.result !== undefined){
|
||||
appendOutput('stdout',d.result.result);
|
||||
}
|
||||
appendOutput('system','(完成 '+d.elapsed_ms+'ms)');
|
||||
}else if(d.error){
|
||||
appendOutput('stderr','错误: '+d.error);
|
||||
}
|
||||
}catch(e){
|
||||
appendOutput('stderr','请求失败: '+e.message);
|
||||
}
|
||||
const entry=document.createElement('div');
|
||||
entry.className='entry';
|
||||
entry.innerHTML='<span class="time">'+time+'</span><span class="tag '+tag+'">'+tag+'</span><span class="msg">'+(msg?msg:'')+(details?'<span class="output">'+details+'</span>':'')+'</span>';
|
||||
log.appendChild(entry);
|
||||
|
||||
document.getElementById('term-send-btn').disabled=false;
|
||||
document.getElementById('term-input').disabled=false;
|
||||
document.getElementById('term-input').focus();
|
||||
|
||||
const output=document.getElementById('term-output');
|
||||
output.scrollTop=output.scrollHeight;
|
||||
while(log.children.length>MAX_LOG) log.removeChild(log.firstChild);
|
||||
log.scrollTop=log.scrollHeight;
|
||||
}
|
||||
|
||||
async function closeTerminal(){
|
||||
if(termSessionId){
|
||||
try{await fetch(API+'/terminal/close/'+termSessionId,{method:'POST'})}catch(e){}
|
||||
}
|
||||
termSessionId=null;
|
||||
termReady=false;
|
||||
if(termIv){clearInterval(termIv);termIv=null}
|
||||
|
||||
document.getElementById('term-container').style.display='none';
|
||||
document.getElementById('term-close-btn').style.display='none';
|
||||
document.getElementById('term-status').style.display='none';
|
||||
document.getElementById('term-input').disabled=true;
|
||||
document.getElementById('term-send-btn').disabled=true;
|
||||
document.getElementById('term-hint').textContent='提示: 打开终端后冰朔会在控制台看到敲门请求,授权后即可输入命令';
|
||||
resetTermButtons();
|
||||
toast('终端已关闭');
|
||||
document.getElementById('term-output').innerHTML='<span class="system">// 选择服务器并点击 "打开终端" 开始</span>';
|
||||
// ===== Poll =====
|
||||
async function pollAll(){
|
||||
const[s,p,sess]=await Promise.all([fetchS(),fetchP(),fetchSessions()]);
|
||||
renderS(s);
|
||||
renderP(p);
|
||||
renderSessions(sess);
|
||||
}
|
||||
|
||||
function setTermStatus(status,text){
|
||||
const badge=document.getElementById('term-status');
|
||||
badge.style.display='inline-block';
|
||||
badge.className='status-badge '+status;
|
||||
badge.textContent=text;
|
||||
}
|
||||
|
||||
function appendOutput(type,text){
|
||||
const output=document.getElementById('term-output');
|
||||
const line=document.createElement('div');
|
||||
line.className=type;
|
||||
line.textContent=text;
|
||||
output.appendChild(line);
|
||||
output.scrollTop=output.scrollHeight;
|
||||
}
|
||||
|
||||
function resetTermButtons(){
|
||||
document.getElementById('term-open-btn').disabled=false;
|
||||
document.getElementById('term-open-btn').textContent='打开终端';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded',()=>{
|
||||
document.getElementById('term-input').addEventListener('keydown',(e)=>{
|
||||
if(e.key==='Enter') sendTermCmd();
|
||||
});
|
||||
});
|
||||
async function refreshAll(){await pollAll()}
|
||||
|
||||
function clock(){document.getElementById('clock').textContent=new Date().toLocaleString('zh-CN',{hour12:false})}
|
||||
|
||||
(async function init(){
|
||||
const s=await fetchS();
|
||||
renderS(s);
|
||||
initServerSelect(s.servers);
|
||||
const p=await fetchP();
|
||||
renderP(p);
|
||||
const sess=await fetchSessions();
|
||||
renderSessions(sess);
|
||||
clock();
|
||||
iv=setInterval(refreshAll,10000);
|
||||
iv=setInterval(pollAll,5000);
|
||||
setInterval(clock,1000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user