mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-28 02:02:27 +02:00
Add files via upload
This commit is contained in:
@@ -12970,6 +12970,7 @@ header {
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
margin: 2px 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.webshell-tree-row.active {
|
||||
@@ -13041,6 +13042,12 @@ header {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.webshell-tree-row.selected-file .webshell-dir-item {
|
||||
background: rgba(0, 102, 255, 0.08);
|
||||
color: var(--accent-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.webshell-file-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -13237,6 +13244,11 @@ header {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background 0.15s ease;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.webshell-col-name {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.webshell-file-empty-state {
|
||||
@@ -13256,6 +13268,15 @@ header {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.webshell-file-table tbody tr.webshell-file-row-selected {
|
||||
background: rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.webshell-file-table tbody tr.webshell-file-row-selected a.webshell-file-link {
|
||||
color: var(--accent-hover);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.webshell-file-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -13267,6 +13288,12 @@ header {
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.webshell-file-table a.webshell-file-link:hover {
|
||||
@@ -13430,6 +13457,17 @@ header {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.webshell-file-content-path {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 8px 12px;
|
||||
font-family: ui-monospace, monospace;
|
||||
word-break: break-all;
|
||||
background: var(--bg-secondary, rgba(0, 0, 0, 0.04));
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.webshell-file-content .btn-ghost {
|
||||
margin-top: 12px;
|
||||
padding: 8px 16px;
|
||||
|
||||
+184
-47
@@ -34,6 +34,7 @@ let webshellDbConfigByConn = {};
|
||||
let webshellDirTreeByConn = {};
|
||||
let webshellDirExpandedByConn = {};
|
||||
let webshellDirLoadedByConn = {};
|
||||
let webshellSelectedFileByConn = {};
|
||||
// 流式打字机效果:当前会话的 response 序号,用于中止过期的打字
|
||||
let webshellStreamingTypingId = 0;
|
||||
let webshellProbeStatusById = {};
|
||||
@@ -70,6 +71,23 @@ function webshellConnOS(conn) {
|
||||
return normalizeWebshellOS(conn && conn.os);
|
||||
}
|
||||
|
||||
/** 生成一次性探活 token,避免固定回显值被包装时误判 */
|
||||
function buildWebshellProbeToken() {
|
||||
return '__CSAI_PROBE_' + Math.random().toString(36).slice(2, 10) + '_' + Date.now().toString(36) + '__';
|
||||
}
|
||||
|
||||
/** 构造跨 Windows/Linux 都可执行的探活命令 */
|
||||
function buildWebshellProbeCommand(token) {
|
||||
return 'echo ' + token;
|
||||
}
|
||||
|
||||
/** 探活成功判定:HTTP 成功且输出中包含本次 token */
|
||||
function isWebshellProbeOutputMatched(output, token) {
|
||||
if (!token) return false;
|
||||
var text = (output == null) ? '' : String(output);
|
||||
return text.indexOf(token) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装 /api/webshell/file 的公共请求体。
|
||||
* 所有文件管理调用点都应走此函数,避免遗漏字段(如 connection_id)。
|
||||
@@ -816,6 +834,7 @@ function probeWebshellConnection(conn) {
|
||||
if (!conn || typeof apiFetch === 'undefined') {
|
||||
return Promise.resolve({ ok: false, message: wsT('webshell.testFailed') || '连通性测试失败' });
|
||||
}
|
||||
var probeToken = buildWebshellProbeToken();
|
||||
return apiFetch('/api/webshell/exec', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -827,13 +846,13 @@ function probeWebshellConnection(conn) {
|
||||
cmd_param: conn.cmdParam || '',
|
||||
encoding: webshellConnEncoding(conn),
|
||||
os: webshellConnOS(conn),
|
||||
command: 'echo 1'
|
||||
command: buildWebshellProbeCommand(probeToken)
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
var output = (data && data.output != null) ? String(data.output).trim() : '';
|
||||
var ok = !!(data && data.ok && output === '1');
|
||||
var output = (data && data.output != null) ? String(data.output) : '';
|
||||
var ok = !!(data && data.ok && isWebshellProbeOutputMatched(output, probeToken));
|
||||
if (ok) return { ok: true, message: wsT('webshell.testSuccess') || '连通性正常,Shell 可访问' };
|
||||
var msg = (data && data.error) ? data.error : (wsT('webshell.testFailed') || '连通性测试失败');
|
||||
return { ok: false, message: msg };
|
||||
@@ -931,11 +950,61 @@ function normalizeWebshellPath(path) {
|
||||
var p = path == null ? '.' : String(path).trim();
|
||||
if (!p || p === '/') return '.';
|
||||
p = p.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/');
|
||||
// Windows 盘符根目录保持为 "C:/",避免被裁成 "C:" 后父级计算异常
|
||||
if (/^[A-Za-z]:\/?$/.test(p)) {
|
||||
return p.slice(0, 2) + '/';
|
||||
}
|
||||
if (!p || p === '.') return '.';
|
||||
if (p.endsWith('/')) p = p.slice(0, -1);
|
||||
return p || '.';
|
||||
}
|
||||
|
||||
function getWebshellSelectedFile(conn) {
|
||||
if (!conn || !conn.id) return '';
|
||||
var p = webshellSelectedFileByConn[conn.id];
|
||||
if (!p) return '';
|
||||
return normalizeWebshellPath(p);
|
||||
}
|
||||
|
||||
function setWebshellSelectedFile(conn, path) {
|
||||
if (!conn || !conn.id) return;
|
||||
if (!path) {
|
||||
delete webshellSelectedFileByConn[conn.id];
|
||||
return;
|
||||
}
|
||||
webshellSelectedFileByConn[conn.id] = normalizeWebshellPath(path);
|
||||
}
|
||||
|
||||
function getWebshellParentPath(path) {
|
||||
var p = normalizeWebshellPath(path);
|
||||
// Windows 盘符根目录不可再上探
|
||||
if (/^[A-Za-z]:\/$/.test(p)) return p;
|
||||
// 允许从当前目录持续上探:. -> .. -> ../.. -> ../../..
|
||||
if (p === '.') return '..';
|
||||
if (/^(?:\.\.\/)*\.\.$/.test(p)) return p + '/..';
|
||||
// 已经是相对上探时,先维持链路;后续 list 成功后会用远端真实路径回填
|
||||
var idx = p.lastIndexOf('/');
|
||||
if (idx < 0) return '.';
|
||||
var parent = p.slice(0, idx) || '.';
|
||||
if (/^[A-Za-z]:$/.test(parent)) return parent + '/';
|
||||
return parent;
|
||||
}
|
||||
|
||||
function inferPathFromWindowsDirOutput(rawOutput) {
|
||||
var text = String(rawOutput || '').replace(/\r/g, '');
|
||||
var lines = text.split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = String(lines[i] || '').trim();
|
||||
// 中文: C:\xxx 的目录
|
||||
var zh = line.match(/^([A-Za-z]:\\.*)\s+的目录$/);
|
||||
if (zh && zh[1]) return normalizeWebshellPath(zh[1]);
|
||||
// 英文: Directory of C:\xxx
|
||||
var en = line.match(/^Directory of\s+([A-Za-z]:\\.*)$/i);
|
||||
if (en && en[1]) return normalizeWebshellPath(en[1]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getWebshellTerminalSessionKey(connId, sessionId) {
|
||||
if (!connId || !sessionId) return '';
|
||||
return String(connId) + '::' + String(sessionId);
|
||||
@@ -2047,11 +2116,7 @@ function selectWebshell(id, stateReady) {
|
||||
});
|
||||
document.getElementById('webshell-parent-dir').addEventListener('click', function () {
|
||||
const p = (pathInput && pathInput.value.trim()) || '.';
|
||||
if (p === '.' || p === '/') {
|
||||
pathInput.value = '..';
|
||||
} else {
|
||||
pathInput.value = p.replace(/\/[^/]+$/, '') || '.';
|
||||
}
|
||||
pathInput.value = getWebshellParentPath(p);
|
||||
webshellFileListDir(webshellCurrentConn, pathInput.value || '.');
|
||||
});
|
||||
|
||||
@@ -3578,9 +3643,14 @@ function webshellFileListDir(conn, path) {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(data.error) + '</div><pre class="webshell-file-raw">' + escapeHtml(data.output || '') + '</pre>';
|
||||
return;
|
||||
}
|
||||
listEl.dataset.currentPath = path;
|
||||
var normalizedPath = normalizeWebshellPath(path);
|
||||
var inferredPath = inferPathFromWindowsDirOutput(data.output || '');
|
||||
var displayPath = inferredPath || normalizedPath;
|
||||
listEl.dataset.currentPath = displayPath;
|
||||
listEl.dataset.rawOutput = data.output || '';
|
||||
renderFileList(listEl, path, data.output || '', conn);
|
||||
var pathInput = document.getElementById('webshell-file-path');
|
||||
if (pathInput) pathInput.value = displayPath;
|
||||
renderFileList(listEl, displayPath, data.output || '', conn);
|
||||
})
|
||||
.catch(function (err) {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(err && err.message ? err.message : wsT('webshell.execError')) + '</div>';
|
||||
@@ -3619,6 +3689,27 @@ function modeToType(mode) {
|
||||
return c;
|
||||
}
|
||||
|
||||
function parseWindowsDirEntry(line) {
|
||||
var m = String(line || '').match(/^(\d{4}[\/-]\d{1,2}[\/-]\d{1,2})\s+(\d{1,2}:\d{2})(?:\s*(AM|PM))?\s+(<[^>]+>|[\d,]+)\s+(.+?)\s*$/i);
|
||||
if (!m) return null;
|
||||
var kind = (m[4] || '').trim();
|
||||
var name = (m[5] || '').trim();
|
||||
if (!name || name === '.' || name === '..') return null;
|
||||
var isDir = /^<(dir|junction|symlinkd)>$/i.test(kind);
|
||||
var size = isDir ? '' : kind.replace(/,/g, '');
|
||||
var mtime = (m[1] + ' ' + m[2] + (m[3] ? (' ' + m[3].toUpperCase()) : '')).trim();
|
||||
return {
|
||||
name: name,
|
||||
isDir: isDir,
|
||||
size: size,
|
||||
mtime: mtime,
|
||||
mode: isDir ? 'd' : '-',
|
||||
owner: '',
|
||||
group: '',
|
||||
type: isDir ? 'dir' : 'file'
|
||||
};
|
||||
}
|
||||
|
||||
function parseWebshellListItems(rawOutput) {
|
||||
var lines = (rawOutput || '').split(/\n/).filter(function (l) { return l.trim(); });
|
||||
var items = [];
|
||||
@@ -3627,6 +3718,12 @@ function parseWebshellListItems(rawOutput) {
|
||||
var trimmedLine = String(line || '').trim();
|
||||
// `ls -la` 首行常见 "total 12"(中文环境为 "总计 12"),不是文件项。
|
||||
if (/^(total|总计)\s+\d+$/i.test(trimmedLine)) continue;
|
||||
// `dir` 头尾信息(中英文)与 shell 提示符,不是目录项。
|
||||
if (/^(驱动器|卷的序列号是|volume in drive|volume serial number is|directory of)/i.test(trimmedLine)) continue;
|
||||
if (/^[A-Za-z]:\\.*\s+的目录$/i.test(trimmedLine)) continue;
|
||||
if (/^\d+\s+(个文件|file\(s\))\s+[\d,]+\s+(字节|bytes?)$/i.test(trimmedLine)) continue;
|
||||
if (/^\d+\s+(个目录|dir\(s\))\s+[\d,]+\s+(可用字节|bytes free)$/i.test(trimmedLine)) continue;
|
||||
if (/^[^>\n]*>\s*dir(?:\s|$)/i.test(trimmedLine)) continue;
|
||||
var name = '';
|
||||
var isDir = false;
|
||||
var size = '';
|
||||
@@ -3646,16 +3743,38 @@ function parseWebshellListItems(rawOutput) {
|
||||
isDir = mode && mode.startsWith('d');
|
||||
type = modeToType(mode);
|
||||
} else {
|
||||
var mName = line.match(/\s*(\S+)\s*$/);
|
||||
name = mName ? mName[1].trim() : line.trim();
|
||||
if (name === '.' || name === '..') continue;
|
||||
isDir = line.startsWith('d') || line.toLowerCase().indexOf('<dir>') !== -1;
|
||||
if (line.startsWith('-') || line.startsWith('d')) {
|
||||
var parts = line.split(/\s+/);
|
||||
var winItem = parseWindowsDirEntry(line);
|
||||
if (winItem) {
|
||||
items.push({
|
||||
name: winItem.name,
|
||||
isDir: winItem.isDir,
|
||||
line: line,
|
||||
size: winItem.size,
|
||||
mode: winItem.mode,
|
||||
mtime: winItem.mtime,
|
||||
owner: winItem.owner,
|
||||
group: winItem.group,
|
||||
type: winItem.type
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// 仅兜底解析 Unix 权限格式,避免把 `dir` 统计行误识别为文件。
|
||||
if (/^[-dlcbsp]/.test(line)) {
|
||||
var parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 9) {
|
||||
name = parts.slice(8).join(' ').trim();
|
||||
} else {
|
||||
name = parts.length ? parts[parts.length - 1].trim() : line.trim();
|
||||
}
|
||||
if (name === '.' || name === '..') continue;
|
||||
isDir = line.startsWith('d');
|
||||
parts = line.split(/\s+/);
|
||||
if (parts.length >= 5) { mode = parts[0]; size = parts[4]; }
|
||||
if (parts.length >= 4) { owner = parts[2] || ''; group = parts[3] || ''; }
|
||||
if (parts.length >= 8 && /^[A-Za-z]{3}$/.test(parts[5])) mtime = normalizeLsMtime(parts[5], parts[6], parts[7]);
|
||||
type = modeToType(mode);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (name === '.' || name === '..') continue;
|
||||
@@ -3680,7 +3799,9 @@ function fetchWebshellDirectoryItems(conn, path) {
|
||||
}
|
||||
|
||||
function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) {
|
||||
currentPath = normalizeWebshellPath(currentPath);
|
||||
var items = parseWebshellListItems(rawOutput);
|
||||
var selectedPath = getWebshellSelectedFile(conn || webshellCurrentConn);
|
||||
if (nameFilter && nameFilter.trim()) {
|
||||
var f = nameFilter.trim().toLowerCase();
|
||||
items = items.filter(function (item) { return item.name.toLowerCase().indexOf(f) !== -1; });
|
||||
@@ -3713,10 +3834,11 @@ function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) {
|
||||
}
|
||||
items.forEach(function (item) {
|
||||
var pathNext = currentPath === '.' ? item.name : currentPath + '/' + item.name;
|
||||
var pathNextNorm = normalizeWebshellPath(pathNext);
|
||||
var nameClass = item.isDir ? 'is-dir' : 'is-file';
|
||||
html += '<tr><td class="webshell-col-check">';
|
||||
html += '<tr class="' + (!item.isDir && selectedPath === pathNextNorm ? 'webshell-file-row-selected' : '') + '"><td class="webshell-col-check">';
|
||||
if (!item.isDir) html += '<input type="checkbox" class="webshell-file-cb" data-path="' + escapeHtml(pathNext) + '" />';
|
||||
html += '</td><td><a href="#" class="webshell-file-link ' + nameClass + '" data-path="' + escapeHtml(pathNext) + '" data-isdir="' + (item.isDir ? '1' : '0') + '">' + escapeHtml(item.name) + (item.isDir ? '/' : '') + '</a></td>';
|
||||
html += '</td><td class="webshell-col-name"><a href="#" class="webshell-file-link ' + nameClass + '" title="' + escapeHtml(item.name) + '" data-path="' + escapeHtml(pathNext) + '" data-isdir="' + (item.isDir ? '1' : '0') + '">' + escapeHtml(item.name) + (item.isDir ? '/' : '') + '</a></td>';
|
||||
html += '<td class="webshell-col-size">' + escapeHtml(item.size) + '</td>';
|
||||
html += '<td class="webshell-col-mtime">' + escapeHtml(item.mtime || '') + '</td>';
|
||||
html += '<td class="webshell-col-owner">' + escapeHtml(item.owner || '') + '</td>';
|
||||
@@ -3748,10 +3870,13 @@ function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) {
|
||||
const isDir = a.getAttribute('data-isdir') === '1';
|
||||
const pathInput = document.getElementById('webshell-file-path');
|
||||
if (isDir) {
|
||||
setWebshellSelectedFile(webshellCurrentConn, '');
|
||||
if (pathInput) pathInput.value = path;
|
||||
webshellFileListDir(webshellCurrentConn, path);
|
||||
} else {
|
||||
// 打开文件时保留当前“浏览目录”上下文,避免返回时落到单文件视图
|
||||
setWebshellSelectedFile(webshellCurrentConn, path);
|
||||
renderDirectoryTree(currentPath, items, conn || webshellCurrentConn);
|
||||
webshellFileRead(webshellCurrentConn, path, listEl, currentPath);
|
||||
}
|
||||
});
|
||||
@@ -3759,7 +3884,10 @@ function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) {
|
||||
listEl.querySelectorAll('.webshell-file-read').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
webshellFileRead(webshellCurrentConn, btn.getAttribute('data-path'), listEl, currentPath);
|
||||
var filePath = btn.getAttribute('data-path');
|
||||
setWebshellSelectedFile(webshellCurrentConn, filePath);
|
||||
renderDirectoryTree(currentPath, items, conn || webshellCurrentConn);
|
||||
webshellFileRead(webshellCurrentConn, filePath, listEl, currentPath);
|
||||
});
|
||||
});
|
||||
listEl.querySelectorAll('.webshell-file-download').forEach(function (btn) {
|
||||
@@ -3821,6 +3949,7 @@ function renderDirectoryTree(currentPath, items, conn) {
|
||||
var tree = state.tree;
|
||||
var expanded = state.expanded;
|
||||
var loaded = state.loaded;
|
||||
var selectedPath = getWebshellSelectedFile(conn || webshellCurrentConn);
|
||||
if (!tree['.']) tree['.'] = [];
|
||||
if (expanded['.'] !== false) expanded['.'] = true;
|
||||
|
||||
@@ -3844,26 +3973,29 @@ function renderDirectoryTree(currentPath, items, conn) {
|
||||
if (node.isDir && !tree[node.path]) tree[node.path] = [];
|
||||
});
|
||||
|
||||
// 确保当前路径祖先链存在并展开
|
||||
// 仅对“真实路径”补祖先链;相对上探链(../..)不构建,避免出现假层级。
|
||||
var isRelativeUpChain = /^(?:\.\.\/)*\.\.$/.test(curr);
|
||||
var parts = curr === '.' ? [] : curr.split('/');
|
||||
var parentPath = '.';
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var nextPath = parentPath === '.' ? parts[i] : parentPath + '/' + parts[i];
|
||||
if (!tree[parentPath]) tree[parentPath] = [];
|
||||
var parentChildren = tree[parentPath];
|
||||
var hasAncestorNode = parentChildren.some(function (n) { return n && n.path === nextPath; });
|
||||
if (!hasAncestorNode) {
|
||||
parentChildren.push({ path: nextPath, name: parts[i], isDir: true });
|
||||
parentChildren.sort(function (a, b) {
|
||||
if (!!a.isDir !== !!b.isDir) return a.isDir ? -1 : 1;
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
});
|
||||
if (!isRelativeUpChain) {
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var nextPath = parentPath === '.' ? parts[i] : parentPath + '/' + parts[i];
|
||||
if (!tree[parentPath]) tree[parentPath] = [];
|
||||
var parentChildren = tree[parentPath];
|
||||
var hasAncestorNode = parentChildren.some(function (n) { return n && n.path === nextPath; });
|
||||
if (!hasAncestorNode) {
|
||||
parentChildren.push({ path: nextPath, name: parts[i], isDir: true });
|
||||
parentChildren.sort(function (a, b) {
|
||||
if (!!a.isDir !== !!b.isDir) return a.isDir ? -1 : 1;
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
});
|
||||
}
|
||||
if (!tree[nextPath]) tree[nextPath] = [];
|
||||
expanded[parentPath] = true;
|
||||
parentPath = nextPath;
|
||||
}
|
||||
if (!tree[nextPath]) tree[nextPath] = [];
|
||||
expanded[parentPath] = true;
|
||||
parentPath = nextPath;
|
||||
}
|
||||
expanded[curr] = true;
|
||||
if (expanded[curr] == null) expanded[curr] = true;
|
||||
|
||||
function renderNode(node, depth) {
|
||||
var path = node.path;
|
||||
@@ -3872,15 +4004,16 @@ function renderDirectoryTree(currentPath, items, conn) {
|
||||
var hasLoadedChildren = isDir ? (loaded[path] === true) : true;
|
||||
var canExpand = isDir && (path === '.' || !hasLoadedChildren || children.length > 0);
|
||||
var hasChildren = children.length > 0;
|
||||
var isExpanded = isDir ? (expanded[path] !== false) : false;
|
||||
var isExpanded = isDir ? (expanded[path] === true) : false;
|
||||
var isActive = path === curr;
|
||||
var isSelectedFile = !isDir && path === selectedPath;
|
||||
var name = node.name;
|
||||
var icon = isDir ? (path === '.' ? '🗂' : '📁') : '📄';
|
||||
var nodeHtml =
|
||||
'<div class="webshell-tree-node" data-depth="' + depth + '">' +
|
||||
'<div class="webshell-tree-row' + (isActive ? ' active' : '') + '">' +
|
||||
'<div class="webshell-tree-row' + (isActive ? ' active' : '') + (isSelectedFile ? ' selected-file' : '') + '">' +
|
||||
'<button type="button" class="webshell-tree-toggle' + (canExpand ? '' : ' empty') + '" data-path="' + escapeHtml(path) + '">' + (canExpand ? (isExpanded ? '▾' : '▸') : '·') + '</button>' +
|
||||
'<button type="button" class="webshell-dir-item' + (isDir ? ' is-dir' : ' is-file') + '" data-path="' + escapeHtml(path) + '" data-isdir="' + (isDir ? '1' : '0') + '"><span class="webshell-tree-icon">' + icon + '</span><span class="webshell-tree-name">' + escapeHtml(name) + '</span></button>' +
|
||||
'<button type="button" class="webshell-dir-item' + (isDir ? ' is-dir' : ' is-file') + '" title="' + escapeHtml(name) + '" data-path="' + escapeHtml(path) + '" data-isdir="' + (isDir ? '1' : '0') + '"><span class="webshell-tree-icon">' + icon + '</span><span class="webshell-tree-name">' + escapeHtml(name) + '</span></button>' +
|
||||
'</div>';
|
||||
if (isDir && hasChildren && isExpanded) {
|
||||
nodeHtml += '<div class="webshell-tree-children">';
|
||||
@@ -3899,7 +4032,7 @@ function renderDirectoryTree(currentPath, items, conn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var p = normalizeWebshellPath(btn.getAttribute('data-path') || '.');
|
||||
if (expanded[p] !== false) {
|
||||
if (expanded[p] === true) {
|
||||
expanded[p] = false;
|
||||
renderDirectoryTree(curr, items, conn || webshellCurrentConn);
|
||||
return;
|
||||
@@ -3939,12 +4072,15 @@ function renderDirectoryTree(currentPath, items, conn) {
|
||||
var isDir = btn.getAttribute('data-isdir') === '1';
|
||||
var pathInput = document.getElementById('webshell-file-path');
|
||||
if (isDir) {
|
||||
setWebshellSelectedFile(webshellCurrentConn, '');
|
||||
if (pathInput) pathInput.value = p;
|
||||
webshellFileListDir(webshellCurrentConn, p);
|
||||
return;
|
||||
}
|
||||
var listEl = document.getElementById('webshell-file-list');
|
||||
var browsePath = p.replace(/\/[^/]+$/, '') || '.';
|
||||
setWebshellSelectedFile(webshellCurrentConn, p);
|
||||
renderDirectoryTree(curr, items, conn || webshellCurrentConn);
|
||||
if (listEl) webshellFileRead(webshellCurrentConn, p, listEl, browsePath);
|
||||
});
|
||||
});
|
||||
@@ -4101,7 +4237,7 @@ function webshellFileRead(conn, path, listEl, browsePath) {
|
||||
// 兜底:若路径被污染成文件路径,回退到父目录
|
||||
backPath = path.replace(/\/[^/]+$/, '') || '.';
|
||||
}
|
||||
listEl.innerHTML = '<div class="webshell-file-content"><pre>' + escapeHtml(out) + '</pre><button type="button" class="btn-ghost" id="webshell-file-back-btn" data-back-path="' + escapeHtml(backPath) + '">' + wsT('webshell.back') + '</button></div>';
|
||||
listEl.innerHTML = '<div class="webshell-file-content"><div class="webshell-file-content-path">' + escapeHtml(path) + '</div><pre>' + escapeHtml(out) + '</pre><button type="button" class="btn-ghost" id="webshell-file-back-btn" data-back-path="' + escapeHtml(backPath) + '">' + wsT('webshell.back') + '</button></div>';
|
||||
var backBtn = document.getElementById('webshell-file-back-btn');
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
@@ -4467,7 +4603,7 @@ document.addEventListener('conversation-deleted', function (e) {
|
||||
}
|
||||
});
|
||||
|
||||
// 测试连通性(不保存,仅用当前表单参数请求 Shell 执行 echo 1)
|
||||
// 测试连通性(不保存,仅用当前表单参数请求 Shell 执行一次性探活命令)
|
||||
function testWebshellConnection() {
|
||||
var url = (document.getElementById('webshell-url') || {}).value;
|
||||
if (url && typeof url.trim === 'function') url = url.trim();
|
||||
@@ -4484,13 +4620,14 @@ function testWebshellConnection() {
|
||||
var osTag = normalizeWebshellOS((document.getElementById('webshell-os') || {}).value);
|
||||
var encoding = normalizeWebshellEncoding((document.getElementById('webshell-encoding') || {}).value);
|
||||
var btn = document.getElementById('webshell-test-btn');
|
||||
var probeToken = buildWebshellProbeToken();
|
||||
if (btn) { btn.disabled = true; btn.textContent = (typeof wsT === 'function' ? wsT('common.refresh') : '刷新') + '...'; }
|
||||
if (typeof apiFetch === 'undefined') {
|
||||
if (btn) { btn.disabled = false; btn.textContent = wsT('webshell.testConnectivity'); }
|
||||
alert(wsT('webshell.testFailed') || '连通性测试失败');
|
||||
return;
|
||||
}
|
||||
// 连通性使用 Windows/Linux 都识别的最小内建命令作为探测(echo 1 在 cmd 和 sh 下行为等价)
|
||||
// 连通性使用 Windows/Linux 都识别的最小内建命令作为探测(echo token 在 cmd 和 sh 下行为等价)
|
||||
apiFetch('/api/webshell/exec', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -4502,7 +4639,7 @@ function testWebshellConnection() {
|
||||
cmd_param: cmdParam || '',
|
||||
encoding: encoding,
|
||||
os: osTag,
|
||||
command: 'echo 1'
|
||||
command: buildWebshellProbeCommand(probeToken)
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
@@ -4512,14 +4649,14 @@ function testWebshellConnection() {
|
||||
alert(wsT('webshell.testFailed') || '连通性测试失败');
|
||||
return;
|
||||
}
|
||||
// 仅 HTTP 200 不算通过,需校验是否真的执行了 echo 1(响应体 trim 后应为 "1")
|
||||
var output = (data.output != null) ? String(data.output).trim() : '';
|
||||
var reallyOk = data.ok && output === '1';
|
||||
// 仅 HTTP 200 不算通过,需校验响应中是否包含本次一次性探活 token
|
||||
var output = (data.output != null) ? String(data.output) : '';
|
||||
var reallyOk = data.ok && isWebshellProbeOutputMatched(output, probeToken);
|
||||
if (reallyOk) {
|
||||
alert(wsT('webshell.testSuccess') || '连通性正常,Shell 可访问');
|
||||
} else {
|
||||
var msg;
|
||||
if (data.ok && output !== '1')
|
||||
if (data.ok && !isWebshellProbeOutputMatched(output, probeToken))
|
||||
msg = wsT('webshell.testNoExpectedOutput') || 'Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名';
|
||||
else
|
||||
msg = (data.error) ? data.error : (wsT('webshell.testFailed') || '连通性测试失败');
|
||||
|
||||
Reference in New Issue
Block a user