mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 04:51:01 +02:00
Add files via upload
This commit is contained in:
@@ -8860,6 +8860,71 @@ header {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.webshell-terminal-sessions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 8px;
|
||||
height: 34px;
|
||||
background: #0b0f14;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.webshell-terminal-session {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-bottom: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
height: 30px;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.webshell-terminal-session.active {
|
||||
background: #0d1117;
|
||||
border-color: rgba(88, 166, 255, 0.45);
|
||||
color: #e6edf3;
|
||||
}
|
||||
.webshell-terminal-session-main {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.webshell-terminal-session-close {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #8b949e;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.webshell-terminal-session-close:hover {
|
||||
color: #f85149;
|
||||
background: rgba(248, 81, 73, 0.08);
|
||||
}
|
||||
.webshell-terminal-session-add {
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-bottom: none;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: #8b949e;
|
||||
height: 30px;
|
||||
width: 28px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.webshell-terminal-session-add:hover {
|
||||
color: #e6edf3;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.webshell-quick-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
@@ -8869,20 +8934,48 @@ header {
|
||||
.webshell-terminal-toolbar .btn-ghost {
|
||||
font-size: 12px;
|
||||
}
|
||||
.webshell-terminal-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.webshell-terminal-status.idle {
|
||||
color: #166534;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-color: rgba(34, 197, 94, 0.25);
|
||||
}
|
||||
.webshell-terminal-status.running {
|
||||
color: #9a3412;
|
||||
background: rgba(251, 146, 60, 0.14);
|
||||
border-color: rgba(251, 146, 60, 0.28);
|
||||
}
|
||||
|
||||
#webshell-pane-terminal {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 仅外框圆角,内部不做额外装饰,避免挡住文字 */
|
||||
.webshell-terminal-container {
|
||||
.webshell-terminal-shell {
|
||||
flex: 1;
|
||||
min-height: 360px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0d1117;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
.webshell-terminal-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transform: translateZ(0);
|
||||
|
||||
@@ -430,6 +430,13 @@
|
||||
"testFailed": "Connectivity test failed",
|
||||
"testNoExpectedOutput": "Shell responded but expected output was not found. Check password and command parameter name.",
|
||||
"clearScreen": "Clear",
|
||||
"copyTerminalLog": "Copy log",
|
||||
"terminalIdle": "Idle",
|
||||
"terminalRunning": "Running",
|
||||
"terminalCopyOk": "Log copied",
|
||||
"terminalCopyFail": "Copy failed",
|
||||
"terminalNewWindow": "New terminal",
|
||||
"terminalWindowPrefix": "Terminal",
|
||||
"running": "Running…",
|
||||
"waitFinish": "Please wait for the current command to finish",
|
||||
"newDir": "New directory",
|
||||
|
||||
@@ -430,6 +430,13 @@
|
||||
"testFailed": "连通性测试失败",
|
||||
"testNoExpectedOutput": "Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名",
|
||||
"clearScreen": "清屏",
|
||||
"copyTerminalLog": "复制日志",
|
||||
"terminalIdle": "空闲",
|
||||
"terminalRunning": "执行中",
|
||||
"terminalCopyOk": "日志已复制",
|
||||
"terminalCopyFail": "复制失败",
|
||||
"terminalNewWindow": "新终端",
|
||||
"terminalWindowPrefix": "终端",
|
||||
"running": "执行中…",
|
||||
"waitFinish": "请等待当前命令执行完成",
|
||||
"newDir": "新建目录",
|
||||
|
||||
+352
-19
@@ -14,6 +14,11 @@ let webshellTerminalResizeContainer = null;
|
||||
let webshellCurrentConn = null;
|
||||
let webshellLineBuffer = '';
|
||||
let webshellRunning = false;
|
||||
let webshellTerminalRunning = false;
|
||||
let webshellTerminalLogsByConn = {};
|
||||
let webshellTerminalSessionsByConn = {};
|
||||
let webshellPersistLoadedByConn = {};
|
||||
let webshellPersistSaveTimersByConn = {};
|
||||
// 按连接保存命令历史,用于上下键
|
||||
let webshellHistoryByConn = {};
|
||||
let webshellHistoryIndex = -1;
|
||||
@@ -146,6 +151,13 @@ function wsT(key) {
|
||||
'webshell.testFailed': '连通性测试失败',
|
||||
'webshell.testNoExpectedOutput': 'Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名',
|
||||
'webshell.clearScreen': '清屏',
|
||||
'webshell.copyTerminalLog': '复制日志',
|
||||
'webshell.terminalIdle': '空闲',
|
||||
'webshell.terminalRunning': '执行中',
|
||||
'webshell.terminalCopyOk': '日志已复制',
|
||||
'webshell.terminalCopyFail': '复制失败',
|
||||
'webshell.terminalNewWindow': '新终端',
|
||||
'webshell.terminalWindowPrefix': '终端',
|
||||
'webshell.running': '执行中…',
|
||||
'webshell.waitFinish': '请等待当前命令执行完成',
|
||||
'webshell.newDir': '新建目录',
|
||||
@@ -201,6 +213,10 @@ function bindWebshellClearOnce() {
|
||||
destroyWebshellTerminal();
|
||||
webshellLineBuffer = '';
|
||||
webshellHistoryIndex = -1;
|
||||
if (webshellCurrentConn && webshellCurrentConn.id) {
|
||||
var sid = getActiveWebshellTerminalSessionId(webshellCurrentConn.id);
|
||||
clearWebshellTerminalLog(getWebshellTerminalSessionKey(webshellCurrentConn.id, sid));
|
||||
}
|
||||
initWebshellTerminal(webshellCurrentConn);
|
||||
} finally {
|
||||
setTimeout(function () { webshellClearInProgress = false; }, 100);
|
||||
@@ -341,6 +357,8 @@ function destroyWebshellTerminal() {
|
||||
webshellTerminalFitAddon = null;
|
||||
webshellLineBuffer = '';
|
||||
webshellRunning = false;
|
||||
webshellTerminalRunning = false;
|
||||
setWebshellTerminalStatus(false);
|
||||
}
|
||||
|
||||
// 渲染连接列表
|
||||
@@ -543,6 +561,195 @@ function normalizeWebshellPath(path) {
|
||||
return p || '.';
|
||||
}
|
||||
|
||||
function getWebshellTerminalSessionKey(connId, sessionId) {
|
||||
if (!connId || !sessionId) return '';
|
||||
return String(connId) + '::' + String(sessionId);
|
||||
}
|
||||
|
||||
function normalizeWebshellTerminalSessions(raw) {
|
||||
var state = raw && typeof raw === 'object' ? raw : {};
|
||||
var list = Array.isArray(state.sessions) ? state.sessions.slice() : [];
|
||||
if (!list.length) {
|
||||
list = [{ id: 't1', name: (wsT('webshell.terminalWindowPrefix') || '终端') + '1' }];
|
||||
}
|
||||
list = list.map(function (s, i) {
|
||||
var id = (s && s.id ? String(s.id) : ('t' + (i + 1)));
|
||||
var name = (s && s.name ? String(s.name) : ((wsT('webshell.terminalWindowPrefix') || '终端') + (i + 1)));
|
||||
return { id: id, name: name };
|
||||
});
|
||||
var activeId = state.activeId;
|
||||
if (!activeId || !list.some(function (s) { return s.id === activeId; })) activeId = list[0].id;
|
||||
return { sessions: list, activeId: activeId };
|
||||
}
|
||||
|
||||
function getWebshellTerminalSessions(connId) {
|
||||
if (!connId) return normalizeWebshellTerminalSessions(null);
|
||||
if (webshellTerminalSessionsByConn[connId]) return webshellTerminalSessionsByConn[connId];
|
||||
var state = normalizeWebshellTerminalSessions(null);
|
||||
webshellTerminalSessionsByConn[connId] = state;
|
||||
return state;
|
||||
}
|
||||
|
||||
function saveWebshellTerminalSessions(connId, state) {
|
||||
if (!connId || !state) return;
|
||||
var normalized = normalizeWebshellTerminalSessions(state);
|
||||
webshellTerminalSessionsByConn[connId] = normalized;
|
||||
queueWebshellPersistStateSave(connId);
|
||||
}
|
||||
|
||||
function getActiveWebshellTerminalSessionId(connId) {
|
||||
return getWebshellTerminalSessions(connId).activeId;
|
||||
}
|
||||
|
||||
function getWebshellTerminalLog(connId) {
|
||||
if (!connId) return '';
|
||||
if (typeof webshellTerminalLogsByConn[connId] === 'string') return webshellTerminalLogsByConn[connId];
|
||||
webshellTerminalLogsByConn[connId] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
function saveWebshellTerminalLog(connId, content) {
|
||||
if (!connId) return;
|
||||
var text = String(content || '');
|
||||
var maxLen = 50000; // keep recent terminal output only
|
||||
if (text.length > maxLen) text = text.slice(text.length - maxLen);
|
||||
webshellTerminalLogsByConn[connId] = text;
|
||||
}
|
||||
|
||||
function appendWebshellTerminalLog(connId, chunk) {
|
||||
if (!connId || !chunk) return;
|
||||
var current = getWebshellTerminalLog(connId);
|
||||
saveWebshellTerminalLog(connId, current + String(chunk));
|
||||
}
|
||||
|
||||
function clearWebshellTerminalLog(connId) {
|
||||
if (!connId) return;
|
||||
webshellTerminalLogsByConn[connId] = '';
|
||||
}
|
||||
|
||||
function buildWebshellPersistState(connId) {
|
||||
var dbState = getWebshellDbState({ id: connId });
|
||||
var terminalSessions = getWebshellTerminalSessions(connId);
|
||||
return {
|
||||
dbState: dbState || null,
|
||||
terminalSessions: terminalSessions || null
|
||||
};
|
||||
}
|
||||
|
||||
function applyWebshellPersistState(connId, state) {
|
||||
if (!connId || !state || typeof state !== 'object') return;
|
||||
if (state.dbState && typeof state.dbState === 'object') {
|
||||
var key = getWebshellDbStateStorageKey({ id: connId });
|
||||
webshellDbConfigByConn[key] = normalizeWebshellDbState(state.dbState);
|
||||
}
|
||||
if (state.terminalSessions && typeof state.terminalSessions === 'object') {
|
||||
webshellTerminalSessionsByConn[connId] = normalizeWebshellTerminalSessions(state.terminalSessions);
|
||||
}
|
||||
}
|
||||
|
||||
function queueWebshellPersistStateSave(connId) {
|
||||
if (!connId || typeof apiFetch !== 'function') return;
|
||||
if (webshellPersistSaveTimersByConn[connId]) clearTimeout(webshellPersistSaveTimersByConn[connId]);
|
||||
webshellPersistSaveTimersByConn[connId] = setTimeout(function () {
|
||||
delete webshellPersistSaveTimersByConn[connId];
|
||||
var payload = buildWebshellPersistState(connId);
|
||||
apiFetch('/api/webshell/connections/' + encodeURIComponent(connId) + '/state', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ state: payload })
|
||||
}).catch(function () {});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function ensureWebshellPersistStateLoaded(conn) {
|
||||
if (!conn || !conn.id || typeof apiFetch !== 'function') return Promise.resolve();
|
||||
if (webshellPersistLoadedByConn[conn.id]) return Promise.resolve();
|
||||
return apiFetch('/api/webshell/connections/' + encodeURIComponent(conn.id) + '/state', { method: 'GET' })
|
||||
.then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('load state failed')); })
|
||||
.then(function (data) {
|
||||
applyWebshellPersistState(conn.id, data && data.state ? data.state : {});
|
||||
webshellPersistLoadedByConn[conn.id] = true;
|
||||
})
|
||||
.catch(function () {
|
||||
webshellPersistLoadedByConn[conn.id] = true;
|
||||
});
|
||||
}
|
||||
|
||||
function setWebshellTerminalStatus(running) {
|
||||
webshellTerminalRunning = !!running;
|
||||
var el = document.getElementById('webshell-terminal-status');
|
||||
if (!el) return;
|
||||
el.classList.toggle('running', !!running);
|
||||
el.classList.toggle('idle', !running);
|
||||
el.textContent = running ? (wsT('webshell.terminalRunning') || '执行中') : (wsT('webshell.terminalIdle') || '空闲');
|
||||
}
|
||||
|
||||
function renderWebshellTerminalSessions(conn) {
|
||||
if (!conn || !conn.id) return;
|
||||
var tabsEl = document.getElementById('webshell-terminal-sessions');
|
||||
if (!tabsEl) return;
|
||||
var connId = conn.id;
|
||||
var state = getWebshellTerminalSessions(connId);
|
||||
var html = '';
|
||||
state.sessions.forEach(function (s) {
|
||||
var active = s.id === state.activeId;
|
||||
html += '<div class="webshell-terminal-session' + (active ? ' active' : '') + '">' +
|
||||
'<button type="button" class="webshell-terminal-session-main" data-action="switch" data-terminal-id="' + escapeHtml(s.id) + '">' + escapeHtml(s.name) + '</button>' +
|
||||
'<button type="button" class="webshell-terminal-session-close" data-action="close" data-terminal-id="' + escapeHtml(s.id) + '" title="' + escapeHtml(wsT('common.close') || '关闭') + '">×</button>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '<button type="button" class="webshell-terminal-session-add" data-action="add" title="' + escapeHtml(wsT('webshell.terminalNewWindow') || '新终端') + '">+</button>';
|
||||
tabsEl.innerHTML = html;
|
||||
tabsEl.querySelectorAll('[data-action]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var action = btn.getAttribute('data-action') || '';
|
||||
var targetId = btn.getAttribute('data-terminal-id') || '';
|
||||
if (webshellRunning || webshellTerminalRunning) return;
|
||||
if (action === 'add') {
|
||||
var nextState = getWebshellTerminalSessions(connId);
|
||||
var seq = nextState.sessions.length + 1;
|
||||
var nextId = 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
var prefix = wsT('webshell.terminalWindowPrefix') || '终端';
|
||||
nextState.sessions.push({ id: nextId, name: prefix + seq });
|
||||
nextState.activeId = nextId;
|
||||
saveWebshellTerminalSessions(connId, nextState);
|
||||
destroyWebshellTerminal();
|
||||
initWebshellTerminal(conn);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
return;
|
||||
}
|
||||
if (!targetId) return;
|
||||
if (action === 'close') {
|
||||
var curr2 = getWebshellTerminalSessions(connId);
|
||||
if (curr2.sessions.length <= 1) return;
|
||||
var idx2 = curr2.sessions.findIndex(function (s) { return s.id === targetId; });
|
||||
if (idx2 < 0) return;
|
||||
curr2.sessions.splice(idx2, 1);
|
||||
if (curr2.activeId === targetId) {
|
||||
var fallback = curr2.sessions[Math.max(0, idx2 - 1)] || curr2.sessions[0];
|
||||
curr2.activeId = fallback.id;
|
||||
}
|
||||
saveWebshellTerminalSessions(connId, curr2);
|
||||
// 清理该终端的日志与历史
|
||||
var terminalKey = getWebshellTerminalSessionKey(connId, targetId);
|
||||
clearWebshellTerminalLog(terminalKey);
|
||||
delete webshellHistoryByConn[terminalKey];
|
||||
destroyWebshellTerminal();
|
||||
initWebshellTerminal(conn);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
return;
|
||||
}
|
||||
if (targetId === getActiveWebshellTerminalSessionId(connId)) return;
|
||||
var curr = getWebshellTerminalSessions(connId);
|
||||
curr.activeId = targetId;
|
||||
saveWebshellTerminalSessions(connId, curr);
|
||||
destroyWebshellTerminal();
|
||||
initWebshellTerminal(conn);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getWebshellTreeState(conn) {
|
||||
var key = safeConnIdForStorage(conn);
|
||||
if (!key) return null;
|
||||
@@ -602,10 +809,6 @@ function getWebshellDbState(conn) {
|
||||
if (!key) return normalizeWebshellDbState(null);
|
||||
if (webshellDbConfigByConn[key]) return webshellDbConfigByConn[key];
|
||||
var state = normalizeWebshellDbState(null);
|
||||
try {
|
||||
var raw = localStorage.getItem(key);
|
||||
if (raw) state = normalizeWebshellDbState(JSON.parse(raw));
|
||||
} catch (e) {}
|
||||
webshellDbConfigByConn[key] = state;
|
||||
return state;
|
||||
}
|
||||
@@ -615,7 +818,7 @@ function saveWebshellDbState(conn, state) {
|
||||
if (!key || !state) return;
|
||||
var normalized = normalizeWebshellDbState(state);
|
||||
webshellDbConfigByConn[key] = normalized;
|
||||
try { localStorage.setItem(key, JSON.stringify(normalized)); } catch (e) {}
|
||||
if (conn && conn.id) queueWebshellPersistStateSave(conn.id);
|
||||
}
|
||||
|
||||
function getWebshellDbConfig(conn) {
|
||||
@@ -1084,7 +1287,7 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
|
||||
}
|
||||
|
||||
// 选择连接:渲染终端 + 文件管理 Tab,并初始化终端
|
||||
function selectWebshell(id) {
|
||||
function selectWebshell(id, stateReady) {
|
||||
currentWebshellId = id;
|
||||
renderWebshellList();
|
||||
const conn = webshellConnections.find(c => c.id === id);
|
||||
@@ -1094,6 +1297,12 @@ function selectWebshell(id) {
|
||||
workspace.innerHTML = '<div class="webshell-workspace-placeholder">' + wsT('webshell.selectOrAdd') + '</div>';
|
||||
return;
|
||||
}
|
||||
if (!stateReady) {
|
||||
ensureWebshellPersistStateLoaded(conn).then(function () {
|
||||
if (currentWebshellId === id) selectWebshell(id, true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
destroyWebshellTerminal();
|
||||
webshellCurrentConn = conn;
|
||||
@@ -1108,6 +1317,8 @@ function selectWebshell(id) {
|
||||
'<div id="webshell-pane-terminal" class="webshell-pane active">' +
|
||||
'<div class="webshell-terminal-toolbar">' +
|
||||
'<button type="button" class="btn-ghost btn-sm" id="webshell-terminal-clear" title="' + (wsT('webshell.clearScreen') || '清屏') + '">' + (wsT('webshell.clearScreen') || '清屏') + '</button> ' +
|
||||
'<button type="button" class="btn-ghost btn-sm" id="webshell-terminal-copy-log" title="' + (wsT('webshell.copyTerminalLog') || '复制日志') + '">' + (wsT('webshell.copyTerminalLog') || '复制日志') + '</button> ' +
|
||||
'<span id="webshell-terminal-status" class="webshell-terminal-status idle">' + (wsT('webshell.terminalIdle') || '空闲') + '</span> ' +
|
||||
'<span class="webshell-quick-label">' + (wsT('webshell.quickCommands') || '快捷命令') + ':</span> ' +
|
||||
'<button type="button" class="btn-ghost btn-sm webshell-quick-cmd" data-cmd="whoami">whoami</button> ' +
|
||||
'<button type="button" class="btn-ghost btn-sm webshell-quick-cmd" data-cmd="id">id</button> ' +
|
||||
@@ -1121,8 +1332,11 @@ function selectWebshell(id) {
|
||||
'<button type="button" class="btn-ghost btn-sm webshell-quick-cmd" data-cmd="ps aux">ps aux</button> ' +
|
||||
'<button type="button" class="btn-ghost btn-sm webshell-quick-cmd" data-cmd="netstat -tulnp">netstat</button>' +
|
||||
'</div>' +
|
||||
'<div class="webshell-terminal-shell">' +
|
||||
'<div id="webshell-terminal-sessions" class="webshell-terminal-sessions"></div>' +
|
||||
'<div id="webshell-terminal-container" class="webshell-terminal-container"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="webshell-pane-file" class="webshell-pane">' +
|
||||
'<div class="webshell-file-layout">' +
|
||||
'<aside class="webshell-file-sidebar">' +
|
||||
@@ -1229,6 +1443,34 @@ function selectWebshell(id) {
|
||||
});
|
||||
|
||||
// 清屏由 bindWebshellClearOnce 统一事件委托处理,此处不再绑定,避免重复绑定导致一次点击出现多个 shell>
|
||||
var terminalCopyLogBtn = document.getElementById('webshell-terminal-copy-log');
|
||||
if (terminalCopyLogBtn) {
|
||||
terminalCopyLogBtn.addEventListener('click', function () {
|
||||
if (!webshellCurrentConn || !webshellCurrentConn.id) return;
|
||||
var activeId = getActiveWebshellTerminalSessionId(webshellCurrentConn.id);
|
||||
var log = getWebshellTerminalLog(getWebshellTerminalSessionKey(webshellCurrentConn.id, activeId)) || '';
|
||||
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(log).then(function () {
|
||||
terminalCopyLogBtn.title = wsT('webshell.terminalCopyOk') || '日志已复制';
|
||||
setTimeout(function () {
|
||||
terminalCopyLogBtn.title = wsT('webshell.copyTerminalLog') || '复制日志';
|
||||
}, 1200);
|
||||
}).catch(function () {
|
||||
terminalCopyLogBtn.title = wsT('webshell.terminalCopyFail') || '复制失败';
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = log;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
renderWebshellTerminalSessions(conn);
|
||||
// 快捷命令:点击后执行并输出到终端
|
||||
workspace.querySelectorAll('.webshell-quick-cmd').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
@@ -1996,19 +2238,31 @@ function pushWebshellHistory(connId, cmd) {
|
||||
// 执行快捷命令并将输出写入当前终端
|
||||
function runQuickCommand(cmd) {
|
||||
if (!webshellCurrentConn || !webshellTerminalInstance) return;
|
||||
if (webshellRunning) return;
|
||||
if (webshellRunning || webshellTerminalRunning) return;
|
||||
var term = webshellTerminalInstance;
|
||||
var connId = webshellCurrentConn.id;
|
||||
var sessionId = getActiveWebshellTerminalSessionId(connId);
|
||||
var terminalKey = getWebshellTerminalSessionKey(connId, sessionId);
|
||||
term.writeln('');
|
||||
pushWebshellHistory(webshellCurrentConn.id, cmd);
|
||||
pushWebshellHistory(terminalKey, cmd);
|
||||
appendWebshellTerminalLog(terminalKey, '\n$ ' + cmd + '\n');
|
||||
webshellRunning = true;
|
||||
setWebshellTerminalStatus(true);
|
||||
execWebshellCommand(webshellCurrentConn, cmd).then(function (out) {
|
||||
var s = String(out || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
s.split('\n').forEach(function (line) { term.writeln(line.replace(/\r/g, '')); });
|
||||
appendWebshellTerminalLog(terminalKey, s + '\n');
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
}).catch(function (err) {
|
||||
term.writeln('\x1b[31m' + (err && err.message ? err.message : wsT('webshell.execError')) + '\x1b[0m');
|
||||
var em = (err && err.message ? err.message : wsT('webshell.execError'));
|
||||
term.writeln('\x1b[31m' + em + '\x1b[0m');
|
||||
appendWebshellTerminalLog(terminalKey, em + '\n');
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
}).finally(function () { webshellRunning = false; });
|
||||
}).finally(function () {
|
||||
webshellRunning = false;
|
||||
setWebshellTerminalStatus(false);
|
||||
renderWebshellTerminalSessions(webshellCurrentConn);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 虚拟终端(xterm + 按行执行) ----------
|
||||
@@ -2049,7 +2303,15 @@ function initWebshellTerminal(conn) {
|
||||
try {
|
||||
if (fitAddon) fitAddon.fit();
|
||||
} catch (e) {}
|
||||
// 不再输出欢迎行,避免占用空间、挡住输入
|
||||
setWebshellTerminalStatus(false);
|
||||
var connId = conn && conn.id ? conn.id : '';
|
||||
var sessionId = getActiveWebshellTerminalSessionId(connId);
|
||||
var terminalKey = getWebshellTerminalSessionKey(connId, sessionId);
|
||||
var cachedLog = getWebshellTerminalLog(terminalKey);
|
||||
if (cachedLog) {
|
||||
// xterm 恢复内容时统一使用 CRLF,避免切换窗口后出现“斜排”错位
|
||||
term.write(String(cachedLog).replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n'));
|
||||
}
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
|
||||
// 按行写入输出,与系统设置终端 writeOutput 一致,避免 ls 等输出错位
|
||||
@@ -2073,11 +2335,29 @@ function initWebshellTerminal(conn) {
|
||||
webshellLineBuffer = '';
|
||||
webshellHistoryIndex = -1;
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
clearWebshellTerminalLog(terminalKey);
|
||||
return;
|
||||
}
|
||||
// Ctrl+C:当前实现不支持远程中断,给出提示并回到提示符
|
||||
if (data === '\x03') {
|
||||
if (webshellTerminalRunning) {
|
||||
writeWebshellOutput(term, '^C (当前版本暂不支持中断远程命令)', true);
|
||||
appendWebshellTerminalLog(terminalKey, '^C (当前版本暂不支持中断远程命令)\n');
|
||||
}
|
||||
webshellLineBuffer = '';
|
||||
webshellHistoryIndex = -1;
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
return;
|
||||
}
|
||||
// Ctrl+U:清空当前输入行
|
||||
if (data === '\x15') {
|
||||
webshellLineBuffer = '';
|
||||
term.write('\x1b[2K\r' + WEBSHELL_PROMPT);
|
||||
return;
|
||||
}
|
||||
// 上/下键:命令历史
|
||||
if (data === '\x1b[A' || data === '\x1bOA') {
|
||||
var hist = getWebshellHistory(webshellCurrentConn ? webshellCurrentConn.id : '');
|
||||
var hist = getWebshellHistory(terminalKey);
|
||||
if (hist.length === 0) return;
|
||||
webshellHistoryIndex = webshellHistoryIndex < 0 ? hist.length : Math.max(0, webshellHistoryIndex - 1);
|
||||
webshellLineBuffer = hist[webshellHistoryIndex] || '';
|
||||
@@ -2085,7 +2365,7 @@ function initWebshellTerminal(conn) {
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[B' || data === '\x1bOB') {
|
||||
var hist2 = getWebshellHistory(webshellCurrentConn ? webshellCurrentConn.id : '');
|
||||
var hist2 = getWebshellHistory(terminalKey);
|
||||
if (hist2.length === 0) return;
|
||||
webshellHistoryIndex = webshellHistoryIndex < 0 ? -1 : Math.min(hist2.length - 1, webshellHistoryIndex + 1);
|
||||
if (webshellHistoryIndex < 0) webshellLineBuffer = '';
|
||||
@@ -2102,18 +2382,31 @@ function initWebshellTerminal(conn) {
|
||||
if (cmd) {
|
||||
if (webshellRunning) {
|
||||
writeWebshellOutput(term, wsT('webshell.waitFinish'), true);
|
||||
appendWebshellTerminalLog(terminalKey, (wsT('webshell.waitFinish') || '请等待当前命令执行完成') + '\n');
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
return;
|
||||
}
|
||||
pushWebshellHistory(webshellCurrentConn ? webshellCurrentConn.id : '', cmd);
|
||||
pushWebshellHistory(terminalKey, cmd);
|
||||
appendWebshellTerminalLog(terminalKey, '$ ' + cmd + '\n');
|
||||
webshellRunning = true;
|
||||
setWebshellTerminalStatus(true);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
execWebshellCommand(webshellCurrentConn, cmd).then(function (out) {
|
||||
webshellRunning = false;
|
||||
if (out && out.length) writeWebshellOutput(term, out, false);
|
||||
setWebshellTerminalStatus(false);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
if (out && out.length) {
|
||||
writeWebshellOutput(term, out, false);
|
||||
appendWebshellTerminalLog(terminalKey, String(out).replace(/\r\n/g, '\n').replace(/\r/g, '\n') + '\n');
|
||||
}
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
}).catch(function (err) {
|
||||
webshellRunning = false;
|
||||
writeWebshellOutput(term, err && err.message ? err.message : wsT('webshell.execError'), true);
|
||||
setWebshellTerminalStatus(false);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
var errMsg = err && err.message ? err.message : wsT('webshell.execError');
|
||||
writeWebshellOutput(term, errMsg, true);
|
||||
appendWebshellTerminalLog(terminalKey, String(errMsg || '') + '\n');
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
});
|
||||
} else {
|
||||
@@ -2134,15 +2427,27 @@ function initWebshellTerminal(conn) {
|
||||
}
|
||||
var line = lines[idx].trim();
|
||||
if (!line) { runNext(idx + 1); return; }
|
||||
pushWebshellHistory(webshellCurrentConn.id, line);
|
||||
pushWebshellHistory(terminalKey, line);
|
||||
appendWebshellTerminalLog(terminalKey, '$ ' + line + '\n');
|
||||
webshellRunning = true;
|
||||
setWebshellTerminalStatus(true);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
execWebshellCommand(webshellCurrentConn, line).then(function (out) {
|
||||
if (out && out.length) writeWebshellOutput(term, out, false);
|
||||
if (out && out.length) {
|
||||
writeWebshellOutput(term, out, false);
|
||||
appendWebshellTerminalLog(terminalKey, String(out).replace(/\r\n/g, '\n').replace(/\r/g, '\n') + '\n');
|
||||
}
|
||||
webshellRunning = false;
|
||||
setWebshellTerminalStatus(false);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
runNext(idx + 1);
|
||||
}).catch(function (err) {
|
||||
writeWebshellOutput(term, err && err.message ? err.message : wsT('webshell.execError'), true);
|
||||
var em = err && err.message ? err.message : wsT('webshell.execError');
|
||||
writeWebshellOutput(term, em, true);
|
||||
appendWebshellTerminalLog(terminalKey, String(em || '') + '\n');
|
||||
webshellRunning = false;
|
||||
setWebshellTerminalStatus(false);
|
||||
renderWebshellTerminalSessions(conn);
|
||||
runNext(idx + 1);
|
||||
});
|
||||
};
|
||||
@@ -2178,6 +2483,7 @@ function initWebshellTerminal(conn) {
|
||||
});
|
||||
webshellTerminalResizeObserver.observe(container);
|
||||
}
|
||||
renderWebshellTerminalSessions(conn);
|
||||
}
|
||||
|
||||
// 调用后端执行命令
|
||||
@@ -2851,6 +3157,21 @@ function deleteWebshell(id) {
|
||||
if (!confirm(wsT('webshell.deleteConfirm'))) return;
|
||||
if (currentWebshellId === id) destroyWebshellTerminal();
|
||||
if (currentWebshellId === id) currentWebshellId = null;
|
||||
// 清理本地缓存(服务端会级联删除 SQLite 里的状态)
|
||||
delete webshellPersistLoadedByConn[id];
|
||||
if (webshellPersistSaveTimersByConn[id]) {
|
||||
clearTimeout(webshellPersistSaveTimersByConn[id]);
|
||||
delete webshellPersistSaveTimersByConn[id];
|
||||
}
|
||||
delete webshellTerminalSessionsByConn[id];
|
||||
var dbStateKey = getWebshellDbStateStorageKey({ id: id });
|
||||
if (dbStateKey) delete webshellDbConfigByConn[dbStateKey];
|
||||
Object.keys(webshellTerminalLogsByConn).forEach(function (k) {
|
||||
if (k === id || k.indexOf(id + '::') === 0) delete webshellTerminalLogsByConn[k];
|
||||
});
|
||||
Object.keys(webshellHistoryByConn).forEach(function (k) {
|
||||
if (k === id || k.indexOf(id + '::') === 0) delete webshellHistoryByConn[k];
|
||||
});
|
||||
if (typeof apiFetch === 'undefined') return;
|
||||
apiFetch('/api/webshell/connections/' + encodeURIComponent(id), { method: 'DELETE' })
|
||||
.then(function () {
|
||||
@@ -2933,6 +3254,18 @@ function refreshWebshellUIOnLanguageChange() {
|
||||
|
||||
var quickLabel = workspace.querySelector('.webshell-quick-label');
|
||||
if (quickLabel) quickLabel.textContent = (wsT('webshell.quickCommands') || '快捷命令') + ':';
|
||||
var terminalClearBtn = document.getElementById('webshell-terminal-clear');
|
||||
if (terminalClearBtn) {
|
||||
terminalClearBtn.title = wsT('webshell.clearScreen') || '清屏';
|
||||
terminalClearBtn.textContent = wsT('webshell.clearScreen') || '清屏';
|
||||
}
|
||||
var terminalCopyBtn = document.getElementById('webshell-terminal-copy-log');
|
||||
if (terminalCopyBtn) {
|
||||
terminalCopyBtn.title = wsT('webshell.copyTerminalLog') || '复制日志';
|
||||
terminalCopyBtn.textContent = wsT('webshell.copyTerminalLog') || '复制日志';
|
||||
}
|
||||
setWebshellTerminalStatus(webshellTerminalRunning);
|
||||
if (webshellCurrentConn) renderWebshellTerminalSessions(webshellCurrentConn);
|
||||
var pathLabel = workspace.querySelector('.webshell-file-toolbar label span');
|
||||
var listDirBtn = document.getElementById('webshell-list-dir');
|
||||
var parentDirBtn = document.getElementById('webshell-parent-dir');
|
||||
|
||||
Reference in New Issue
Block a user