mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-06 04:27:55 +02:00
Add files via upload
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ function switchSettingsSection(section) {
|
||||
if (activeContent) {
|
||||
activeContent.classList.add('active');
|
||||
}
|
||||
if (section === 'terminal' && typeof initTerminal === 'function') {
|
||||
setTimeout(initTerminal, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 打开设置
|
||||
|
||||
@@ -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;
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user