Add files via upload

This commit is contained in:
公明
2026-03-03 01:28:30 +08:00
committed by GitHub
parent c265e66afb
commit acadbe19c6
8 changed files with 1074 additions and 0 deletions
+158
View File
@@ -3132,6 +3132,164 @@ header {
line-height: 1.6;
}
/* 系统设置 - 终端 */
.terminal-wrapper {
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
background: #0d1117;
}
.terminal-tabs {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0;
padding: 6px 10px 0;
background: #161b22;
border-bottom: 1px solid var(--border-color);
min-height: 36px;
}
.terminal-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px 5px 12px;
font-size: 0.8125rem;
color: #8b949e;
background: transparent;
border: none;
border-radius: 4px 4px 0 0;
cursor: pointer;
margin-right: 2px;
}
.terminal-tab-label {
cursor: pointer;
}
.terminal-tab-close {
padding: 0;
width: 18px;
height: 18px;
font-size: 1.1rem;
line-height: 1;
color: #8b949e;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-tab-close:hover {
color: #ff7b72;
background: rgba(255, 123, 114, 0.15);
}
.terminal-tab:hover {
color: #e6edf3;
background: rgba(255, 255, 255, 0.06);
}
.terminal-tab.active {
color: #e6edf3;
background: #0d1117;
font-weight: 500;
}
.terminal-tab-new {
margin-left: 4px;
width: 28px;
height: 28px;
font-size: 1.125rem;
line-height: 1;
color: #8b949e;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-tab-new:hover {
color: #58a6ff;
background: rgba(88, 166, 255, 0.1);
border-color: rgba(88, 166, 255, 0.3);
}
.terminal-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #161b22;
border-bottom: 1px solid var(--border-color);
}
.terminal-toolbar-title {
font-size: 0.8125rem;
font-weight: 600;
color: #8b949e;
letter-spacing: 0.02em;
}
.terminal-btn {
padding: 6px 12px;
font-size: 0.8125rem;
color: var(--text-secondary);
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.terminal-btn:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.06);
border-color: #8b949e;
}
.terminal-panes {
position: relative;
min-height: 400px;
}
.terminal-pane {
display: none;
min-height: 400px;
}
.terminal-pane.active {
display: block;
}
.terminal-container {
min-height: 400px;
padding: 8px;
box-sizing: border-box;
}
.terminal-container .xterm {
padding: 0;
}
.terminal-container .xterm-viewport {
border-radius: 0;
}
.terminal-error {
color: #ff7b72;
padding: 16px;
font-size: 0.875rem;
}
.settings-section {
margin-bottom: 32px;
}
+3
View File
@@ -46,6 +46,9 @@ function switchSettingsSection(section) {
if (activeContent) {
activeContent.classList.add('active');
}
if (section === 'terminal' && typeof initTerminal === 'function') {
setTimeout(initTerminal, 0);
}
}
// 打开设置
+525
View File
@@ -0,0 +1,525 @@
/**
* 系统设置 - 终端多标签流式输出命令历史Ctrl+L 清屏长时间可取消
*/
(function () {
var getContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, attrs) {
if (type === '2d') {
attrs = (attrs && typeof attrs === 'object') ? Object.assign({ willReadFrequently: true }, attrs) : { willReadFrequently: true };
return getContext.call(this, type, attrs);
}
return getContext.apply(this, arguments);
};
var terminals = [];
var currentTabId = 1;
var inited = false;
var tabIdCounter = 1;
var PROMPT = '\x1b[32m$\x1b[0m ';
var HISTORY_MAX = 100;
var CANCEL_AFTER_MS = 125000;
function getCurrent() {
for (var i = 0; i < terminals.length; i++) {
if (terminals[i].id === currentTabId) return terminals[i];
}
return terminals[0] || null;
}
var WELCOME_LINE = 'CyberStrikeAI 终端 - 直接输入命令,Enter 执行;↑↓ 历史;Ctrl+L 清屏\r\n';
function writePrompt(tab) {
var t = tab || getCurrent();
if (t && t.term) t.term.write(PROMPT);
}
function redrawTabDisplay(t) {
if (!t || !t.term) return;
t.term.clear();
t.lineBuffer = '';
if (t.cursorIndex !== undefined) t.cursorIndex = 0;
t.term.write(WELCOME_LINE);
t.term.write(PROMPT);
}
function writeln(tabOrS, s) {
var t, text;
if (arguments.length === 1) { text = tabOrS; t = getCurrent(); } else { t = tabOrS; text = s; }
if (!t || !t.term) return;
if (text) t.term.writeln(text);
else t.term.writeln('');
}
function writeOutput(tab, text, isError) {
var t = tab || getCurrent();
if (!t || !t.term || !text) return;
var s = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
var lines = s.split('\n');
var prefix = isError ? '\x1b[31m' : '';
var suffix = isError ? '\x1b[0m' : '';
t.term.write(prefix);
for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/\r/g, '');
t.term.writeln(line);
}
t.term.write(suffix);
}
function getAuthHeaders() {
var h = new Headers();
h.set('Content-Type', 'application/json');
try {
var auth = localStorage.getItem('cyberstrike-auth');
if (auth) {
var o = JSON.parse(auth);
if (o && o.token) h.set('Authorization', 'Bearer ' + o.token);
}
} catch (e) {}
return h;
}
function runCommand(cmd, tab) {
var t = tab || getCurrent();
if (!t) return;
if (t.running) return;
runCommandImpl(cmd, t);
}
function runCommandImpl(cmd, t) {
t.running = true;
t.abortController = new AbortController();
var cancelTimer = setTimeout(function () {
if (!t.running) return;
t.running = false;
writeln(t, '\x1b[2m(已取消 可继续输入)\x1b[0m');
writePrompt(t);
}, CANCEL_AFTER_MS);
var done = function () {
clearTimeout(cancelTimer);
t.running = false;
t.abortController = null;
writePrompt(t);
};
fetch('/api/terminal/run/stream', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ command: cmd }),
signal: t.abortController.signal
}).then(function (res) {
if (!res.ok) return res.json().then(function (d) { throw new Error(d.error || 'HTTP ' + res.status); });
var ct = res.headers.get('Content-Type') || '';
if (ct.indexOf('text/event-stream') !== -1 && res.body) {
return readSSEStream(res.body, t).then(done).catch(function () { done(); });
}
return res.json().then(function (data) {
if (data.stdout) writeOutput(t, data.stdout, false);
if (data.stderr) writeOutput(t, data.stderr, true);
done();
});
}).catch(function (err) {
if (err.name === 'AbortError') {
writeln(t, '\x1b[2m(已取消)\x1b[0m');
} else {
writeln(t, '\x1b[31m错误: ' + (err.message || String(err)) + '\x1b[0m');
}
done();
});
}
function readSSEStream(body, t) {
return new Promise(function (resolve, reject) {
var reader = body.getReader();
var decoder = new TextDecoder();
var buf = '';
function read() {
reader.read().then(function (result) {
if (result.done) { resolve(); return; }
buf += decoder.decode(result.value, { stream: true });
var i;
while ((i = buf.indexOf('\n\n')) !== -1) {
var block = buf.slice(0, i);
buf = buf.slice(i + 2);
var dataLine = block.match(/data:\s*(.+)/);
if (dataLine) {
try {
var ev = JSON.parse(dataLine[1]);
if (ev.t === 'out' && ev.d !== undefined) t.term.writeln(ev.d);
else if (ev.t === 'err' && ev.d !== undefined) t.term.write('\x1b[31m' + ev.d + '\x1b[0m\n');
else if (ev.t === 'exit') {
resolve();
return;
}
} catch (e) {}
}
}
read();
}).catch(reject);
}
read();
});
}
function createTerminalInContainer(container, tab) {
if (typeof Terminal === 'undefined') return null;
if (!tab.history) tab.history = [];
if (tab.historyIndex === undefined) tab.historyIndex = -1;
if (tab.cursorIndex === undefined) tab.cursorIndex = 0;
var term = new Terminal({
cursorBlink: true,
cursorStyle: 'bar',
fontSize: 13,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
lineHeight: 1.2,
scrollback: 1000,
theme: {
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#58a6ff',
cursorAccent: '#0d1117',
selection: 'rgba(88, 166, 255, 0.3)',
black: '#484f58',
red: '#ff7b72',
green: '#3fb950',
yellow: '#d29922',
blue: '#58a6ff',
magenta: '#bc8cff',
cyan: '#39c5cf',
white: '#e6edf3',
brightBlack: '#6e7681',
brightRed: '#ffa198',
brightGreen: '#56d364',
brightYellow: '#e3b341',
brightBlue: '#79c0ff',
brightMagenta: '#d2a8ff',
brightCyan: '#56d4dd',
brightWhite: '#f0f6fc'
}
});
var fitAddon = null;
if (typeof FitAddon !== 'undefined') {
var FitCtor = (FitAddon.FitAddon || FitAddon);
fitAddon = new FitCtor();
term.loadAddon(fitAddon);
}
term.open(container);
term.write(WELCOME_LINE);
term.write(PROMPT);
container.addEventListener('click', function () {
switchTerminalTab(tab.id);
if (term) term.focus();
});
container.setAttribute('tabindex', '0');
container.title = '点击此处后输入命令';
function redrawLine(t) {
if (!t || !t.term) return;
var n = t.lineBuffer.length - t.cursorIndex;
t.term.write('\r\x1b[K' + PROMPT + t.lineBuffer);
if (n > 0) t.term.write('\x1b[' + n + 'D');
}
term.onData(function (data) {
if (data === '\x0c') {
term.clear();
tab.lineBuffer = '';
tab.cursorIndex = 0;
writePrompt(tab);
return;
}
if (data === '\x1b[A') {
if (tab.history.length === 0) return;
if (tab.historyIndex < 0) tab.historyIndex = tab.history.length;
tab.historyIndex--;
if (tab.historyIndex < 0) tab.historyIndex = 0;
tab.lineBuffer = tab.history[tab.historyIndex];
tab.cursorIndex = tab.lineBuffer.length;
term.write('\r\x1b[K' + PROMPT + tab.lineBuffer);
return;
}
if (data === '\x1b[B') {
if (tab.history.length === 0) return;
tab.historyIndex++;
if (tab.historyIndex >= tab.history.length) {
tab.historyIndex = -1;
tab.lineBuffer = '';
tab.cursorIndex = 0;
term.write('\r\x1b[K' + PROMPT);
} else {
tab.lineBuffer = tab.history[tab.historyIndex];
tab.cursorIndex = tab.lineBuffer.length;
term.write('\r\x1b[K' + PROMPT + tab.lineBuffer);
}
return;
}
if (data === '\x1b[D') {
if (tab.cursorIndex > 0) {
tab.cursorIndex--;
term.write('\x1b[D');
}
return;
}
if (data === '\x1b[C') {
if (tab.cursorIndex < tab.lineBuffer.length) {
tab.cursorIndex++;
term.write('\x1b[C');
}
return;
}
var code = data.charCodeAt(0);
if (code === 13 || code === 10) {
var cmd = tab.lineBuffer.trim();
tab.lineBuffer = '';
tab.cursorIndex = 0;
tab.historyIndex = -1;
term.writeln('');
if (cmd) {
if (tab.history.indexOf(cmd) === -1) {
tab.history.push(cmd);
if (tab.history.length > HISTORY_MAX) tab.history.shift();
}
runCommand(cmd, tab);
} else {
writePrompt(tab);
}
return;
}
if (code === 127) {
if (tab.cursorIndex > 0) {
tab.lineBuffer = tab.lineBuffer.slice(0, tab.cursorIndex - 1) + tab.lineBuffer.slice(tab.cursorIndex);
tab.cursorIndex--;
redrawLine(tab);
}
return;
}
if (code === 3) {
if (tab.running && tab.abortController) {
tab.abortController.abort();
}
tab.lineBuffer = '';
tab.cursorIndex = 0;
term.writeln('^C');
writePrompt(tab);
return;
}
if (data.length === 1 && code >= 32) {
tab.lineBuffer = tab.lineBuffer.slice(0, tab.cursorIndex) + data + tab.lineBuffer.slice(tab.cursorIndex);
tab.cursorIndex++;
redrawLine(tab);
return;
}
tab.lineBuffer += data;
tab.cursorIndex = tab.lineBuffer.length;
term.write(data);
});
tab.term = term;
tab.fitAddon = fitAddon;
return term;
}
function switchTerminalTab(id) {
var prevId = currentTabId;
currentTabId = id;
document.querySelectorAll('.terminal-tab').forEach(function (el) {
el.classList.toggle('active', parseInt(el.getAttribute('data-tab-id'), 10) === id);
});
document.querySelectorAll('.terminal-pane').forEach(function (el) {
var paneId = el.getAttribute('id');
var match = paneId && paneId.match(/terminal-pane-(\d+)/);
var paneTabId = match ? parseInt(match[1], 10) : 0;
el.classList.toggle('active', paneTabId === id);
});
var t = getCurrent();
if (t && t.term) {
if (prevId !== id) {
requestAnimationFrame(function () {
if (currentTabId === id && t.term) t.term.focus();
});
} else {
t.term.focus();
}
}
}
function addTerminalTab() {
if (typeof Terminal === 'undefined') return;
tabIdCounter += 1;
var id = tabIdCounter;
var paneId = 'terminal-pane-' + id;
var containerId = 'terminal-container-' + id;
var tabsEl = document.querySelector('.terminal-tabs');
var panesEl = document.querySelector('.terminal-panes');
if (!tabsEl || !panesEl) return;
var tabDiv = document.createElement('div');
tabDiv.className = 'terminal-tab';
tabDiv.setAttribute('data-tab-id', String(id));
var label = document.createElement('span');
label.className = 'terminal-tab-label';
label.textContent = '终端 ' + id;
label.onclick = function () { switchTerminalTab(id); };
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'terminal-tab-close';
closeBtn.title = '关闭';
closeBtn.textContent = '×';
closeBtn.onclick = function (e) { e.stopPropagation(); removeTerminalTab(id); };
tabDiv.appendChild(label);
tabDiv.appendChild(closeBtn);
var plusBtn = tabsEl.querySelector('.terminal-tab-new');
tabsEl.insertBefore(tabDiv, plusBtn);
var paneDiv = document.createElement('div');
paneDiv.id = paneId;
paneDiv.className = 'terminal-pane';
var containerDiv = document.createElement('div');
containerDiv.id = containerId;
containerDiv.className = 'terminal-container';
paneDiv.appendChild(containerDiv);
panesEl.appendChild(paneDiv);
var tab = { id: id, paneId: paneId, containerId: containerId, lineBuffer: '', cursorIndex: 0, running: false, term: null, fitAddon: null, history: [], historyIndex: -1 };
terminals.push(tab);
createTerminalInContainer(containerDiv, tab);
switchTerminalTab(id);
updateTerminalTabCloseVisibility();
setTimeout(function () {
try { if (tab.fitAddon) tab.fitAddon.fit(); if (tab.term) tab.term.focus(); } catch (e) {}
}, 50);
}
function updateTerminalTabCloseVisibility() {
var tabsEl = document.querySelector('.terminal-tabs');
if (!tabsEl) return;
var tabDivs = tabsEl.querySelectorAll('.terminal-tab');
var showClose = terminals.length > 1;
for (var i = 0; i < tabDivs.length; i++) {
var btn = tabDivs[i].querySelector('.terminal-tab-close');
if (btn) btn.style.display = showClose ? '' : 'none';
}
}
function removeTerminalTab(id) {
if (terminals.length <= 1) return;
var idx = -1;
for (var i = 0; i < terminals.length; i++) { if (terminals[i].id === id) { idx = i; break; } }
if (idx < 0) return;
var deletingCurrent = (currentTabId === id);
var switchToIndex = deletingCurrent ? (idx > 0 ? idx - 1 : 0) : -1;
var tab = terminals[idx];
if (tab.term && tab.term.dispose) tab.term.dispose();
tab.term = null;
tab.fitAddon = null;
terminals.splice(idx, 1);
var tabDiv = document.querySelector('.terminal-tab[data-tab-id="' + id + '"]');
var paneDiv = document.getElementById('terminal-pane-' + id);
if (tabDiv && tabDiv.parentNode) tabDiv.parentNode.removeChild(tabDiv);
if (paneDiv && paneDiv.parentNode) paneDiv.parentNode.removeChild(paneDiv);
var curIdxBeforeRenumber = -1;
if (!deletingCurrent) {
for (var i = 0; i < terminals.length; i++) {
if (terminals[i].id === currentTabId) { curIdxBeforeRenumber = i; break; }
}
}
for (var i = 0; i < terminals.length; i++) {
var t = terminals[i];
t.id = i + 1;
t.paneId = 'terminal-pane-' + (i + 1);
t.containerId = 'terminal-container-' + (i + 1);
}
tabIdCounter = terminals.length;
if (curIdxBeforeRenumber >= 0) currentTabId = terminals[curIdxBeforeRenumber].id;
var tabsEl = document.querySelector('.terminal-tabs');
var panesEl = document.querySelector('.terminal-panes');
if (tabsEl) {
var tabDivs = tabsEl.querySelectorAll('.terminal-tab');
for (var i = 0; i < tabDivs.length; i++) {
var t = terminals[i];
tabDivs[i].setAttribute('data-tab-id', String(t.id));
var lbl = tabDivs[i].querySelector('.terminal-tab-label');
if (lbl) lbl.textContent = '终端 ' + t.id;
if (lbl) lbl.onclick = (function (tid) { return function () { switchTerminalTab(tid); }; })(t.id);
var cb = tabDivs[i].querySelector('.terminal-tab-close');
if (cb) cb.onclick = (function (tid) { return function (e) { e.stopPropagation(); removeTerminalTab(tid); }; })(t.id);
}
}
if (panesEl) {
var paneDivs = panesEl.querySelectorAll('.terminal-pane');
for (var i = 0; i < paneDivs.length; i++) {
var t = terminals[i];
paneDivs[i].id = t.paneId;
var cont = paneDivs[i].querySelector('.terminal-container');
if (cont) cont.id = t.containerId;
}
}
updateTerminalTabCloseVisibility();
if (deletingCurrent && terminals.length > 0) {
currentTabId = terminals[switchToIndex].id;
switchTerminalTab(currentTabId);
}
}
function initTerminal() {
var pane1 = document.getElementById('terminal-pane-1');
var container1 = document.getElementById('terminal-container-1');
if (!pane1 || !container1) return;
if (inited) {
var t = getCurrent();
if (t && t.term) t.term.focus();
terminals.forEach(function (tab) { try { if (tab.fitAddon) tab.fitAddon.fit(); } catch (e) {} });
return;
}
inited = true;
if (typeof Terminal === 'undefined') {
container1.innerHTML = '<p class="terminal-error">未加载 xterm.js,请刷新页面或检查网络。</p>';
return;
}
currentTabId = 1;
var tab = { id: 1, paneId: 'terminal-pane-1', containerId: 'terminal-container-1', lineBuffer: '', cursorIndex: 0, running: false, term: null, fitAddon: null, history: [], historyIndex: -1 };
terminals.push(tab);
createTerminalInContainer(container1, tab);
updateTerminalTabCloseVisibility();
setTimeout(function () {
try { if (tab.fitAddon) tab.fitAddon.fit(); if (tab.term) tab.term.focus(); } catch (e) {}
}, 100);
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
terminals.forEach(function (t) { try { if (t.fitAddon) t.fitAddon.fit(); } catch (e) {} });
}, 150);
});
}
function terminalClear() {
var t = getCurrent();
if (!t || !t.term) return;
t.term.clear();
t.lineBuffer = '';
if (t.cursorIndex !== undefined) t.cursorIndex = 0;
writePrompt(t);
t.term.focus();
}
window.initTerminal = initTerminal;
window.terminalClear = terminalClear;
window.switchTerminalTab = switchTerminalTab;
window.addTerminalTab = addTerminalTab;
window.removeTerminalTab = removeTerminalTab;
})();
+26
View File
@@ -7,6 +7,7 @@
<link rel="icon" type="image/png" href="/static/logo.png">
<link rel="shortcut icon" type="image/png" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
</head>
<body>
<div id="login-overlay" class="login-overlay" style="display: none;">
@@ -1074,6 +1075,9 @@
<div class="settings-nav-item" data-section="robots" onclick="switchSettingsSection('robots')">
<span>机器人设置</span>
</div>
<div class="settings-nav-item" data-section="terminal" onclick="switchSettingsSection('terminal')">
<span>终端</span>
</div>
<div class="settings-nav-item" data-section="security" onclick="switchSettingsSection('security')">
<span>安全设置</span>
</div>
@@ -1320,6 +1324,25 @@
</div>
</div>
<!-- 终端 -->
<div id="settings-section-terminal" class="settings-section-content">
<div class="settings-section-header">
<h3>终端</h3>
<p class="settings-description">在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。</p>
</div>
<div class="terminal-wrapper">
<div class="terminal-tabs">
<div class="terminal-tab active" data-tab-id="1"><span class="terminal-tab-label" onclick="switchTerminalTab(1)">终端 1</span><button type="button" class="terminal-tab-close" onclick="removeTerminalTab(1); event.stopPropagation();" title="关闭">×</button></div>
<button type="button" class="terminal-tab-new" onclick="addTerminalTab()" title="新终端">+</button>
</div>
<div class="terminal-panes">
<div id="terminal-pane-1" class="terminal-pane active">
<div id="terminal-container-1" class="terminal-container"></div>
</div>
</div>
</div>
</div>
<!-- 安全设置 -->
<div id="settings-section-security" class="settings-section-content">
<div class="settings-section-header">
@@ -2144,6 +2167,9 @@ version: 1.0.0<br>
<script src="/static/js/monitor.js"></script>
<script src="/static/js/chat.js"></script>
<script src="/static/js/settings.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
<script src="/static/js/terminal.js"></script>
<script src="/static/js/knowledge.js"></script>
<script src="/static/js/skills.js"></script>
<script src="/static/js/vulnerability.js?v=4"></script>