From 5cc53b1076e68c73a638c7ff7401421482e7762c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=85=AC=E6=98=8E?=
<83812544+Ed1s0nZ@users.noreply.github.com>
Date: Wed, 27 May 2026 21:14:37 +0800
Subject: [PATCH] Add files via upload
---
web/static/css/style.css | 38 +++++++
web/static/js/webshell.js | 231 ++++++++++++++++++++++++++++++--------
2 files changed, 222 insertions(+), 47 deletions(-)
diff --git a/web/static/css/style.css b/web/static/css/style.css
index e4cefcee..2ca1c495 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -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;
diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js
index a54ba187..a0e5d96e 100644
--- a/web/static/js/webshell.js
+++ b/web/static/js/webshell.js
@@ -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 = '
' + escapeHtml(data.error) + '
' + escapeHtml(data.output || '') + '
';
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 = '' + escapeHtml(err && err.message ? err.message : wsT('webshell.execError')) + '
';
@@ -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('') !== -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 += '| ';
+ html += ' |
| ';
if (!item.isDir) html += '';
- html += ' | ' + escapeHtml(item.name) + (item.isDir ? '/' : '') + ' | ';
+ html += '' + escapeHtml(item.name) + (item.isDir ? '/' : '') + ' | ';
html += '' + escapeHtml(item.size) + ' | ';
html += '' + escapeHtml(item.mtime || '') + ' | ';
html += '' + escapeHtml(item.owner || '') + ' | ';
@@ -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 =
'' +
- '
' +
+ '
' +
'' +
- '' +
+ '' +
'
';
if (isDir && hasChildren && isExpanded) {
nodeHtml += '
';
@@ -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 = '
' + escapeHtml(out) + '
';
+ listEl.innerHTML = '
' + escapeHtml(path) + '
' + escapeHtml(out) + '
';
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') || '连通性测试失败');