diff --git a/web/static/css/style.css b/web/static/css/style.css
index 090e97fb..3b254183 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -8600,6 +8600,18 @@ header {
flex-shrink: 0;
}
+.webshell-sidebar-tools {
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--border-color);
+ background: rgba(255, 255, 255, 0.45);
+}
+
+.webshell-sidebar-tools .btn-ghost {
+ width: 100%;
+ border: 1px solid var(--border-color);
+ background: #fff;
+}
+
.webshell-conn-search-input {
width: 100%;
padding: 8px 12px;
@@ -8680,6 +8692,41 @@ header {
white-space: nowrap;
}
+.webshell-item-remark-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.webshell-probe-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ font-size: 0.72rem;
+ padding: 2px 8px;
+ border: 1px solid transparent;
+ flex-shrink: 0;
+}
+
+.webshell-probe-badge.probing {
+ color: #a16207;
+ background: #fef3c7;
+ border-color: #fde68a;
+}
+
+.webshell-probe-badge.ok {
+ color: #166534;
+ background: #dcfce7;
+ border-color: #86efac;
+}
+
+.webshell-probe-badge.fail {
+ color: #b91c1c;
+ background: #fee2e2;
+ border-color: #fca5a5;
+}
+
.webshell-item-url {
font-size: 0.78rem;
color: var(--text-secondary);
@@ -8693,6 +8740,8 @@ header {
.webshell-item-actions {
margin-top: 6px;
flex-shrink: 0;
+ display: flex;
+ justify-content: flex-end;
}
.webshell-delete-btn {
@@ -8721,7 +8770,7 @@ header {
.webshell-workspace {
flex: 1;
overflow: auto;
- padding: 20px 24px;
+ padding: 16px 18px;
display: flex;
flex-direction: column;
min-height: 0;
@@ -8895,10 +8944,10 @@ header {
.webshell-file-toolbar {
display: flex;
align-items: center;
- gap: 14px;
- margin-bottom: 16px;
+ gap: 10px;
+ margin-bottom: 12px;
flex-wrap: wrap;
- padding: 14px 16px;
+ padding: 12px 14px;
background: var(--bg-secondary);
border-radius: 10px;
border: 1px solid var(--border-color);
@@ -8907,6 +8956,15 @@ header {
box-sizing: border-box;
}
+.webshell-file-toolbar-main {
+ display: flex;
+ flex: 1 1 720px;
+ min-width: 0;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
.webshell-file-toolbar label {
display: inline-flex;
align-items: center;
@@ -8931,6 +8989,10 @@ header {
border: 1px solid var(--border-color);
}
+.webshell-file-path-field {
+ flex: 1 1 260px !important;
+}
+
.webshell-file-toolbar .btn-secondary,
.webshell-file-toolbar .btn-ghost {
padding: 8px 16px;
@@ -8940,6 +9002,13 @@ header {
flex-shrink: 0;
}
+.webshell-file-toolbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-left: auto;
+}
+
.webshell-file-breadcrumb {
width: 100%;
flex: 0 0 100%;
@@ -8959,8 +9028,8 @@ header {
}
.webshell-file-filter {
min-width: 0 !important;
- flex: 0 1 140px;
- max-width: 200px;
+ flex: 1 1 180px;
+ max-width: 260px;
}
.webshell-col-check {
width: 36px;
@@ -8968,11 +9037,49 @@ header {
vertical-align: middle;
}
.webshell-col-size {
- width: 80px;
+ width: 90px;
color: var(--text-secondary);
font-size: 0.85rem;
}
+.webshell-col-mtime {
+ width: 170px;
+ color: var(--text-secondary);
+ font-size: 0.82rem;
+ white-space: nowrap;
+}
+
+.webshell-col-owner,
+.webshell-col-group {
+ width: 110px;
+ color: var(--text-secondary);
+ font-size: 0.82rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.webshell-col-perms {
+ width: 150px;
+ font-family: ui-monospace, monospace;
+ font-size: 0.82rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--text-secondary);
+}
+
+.webshell-col-type {
+ width: 72px;
+ color: var(--text-secondary);
+ font-size: 0.82rem;
+ text-transform: lowercase;
+}
+
+.webshell-col-actions {
+ width: 90px;
+}
+
.webshell-file-list {
flex: 1;
min-height: 0;
@@ -8989,6 +9096,7 @@ header {
.webshell-file-table {
width: 100%;
border-collapse: collapse;
+ table-layout: fixed;
font-size: 0.9rem;
}
@@ -9011,6 +9119,19 @@ header {
transition: background 0.15s ease;
}
+.webshell-file-empty-state {
+ padding: 28px 16px;
+ text-align: center;
+ color: var(--text-muted);
+ font-size: 0.92rem;
+ background: var(--bg-primary);
+}
+
+.webshell-file-table td:last-child {
+ white-space: nowrap;
+ width: auto;
+}
+
.webshell-file-table tbody tr:hover {
background: var(--bg-secondary);
}
@@ -9056,6 +9177,93 @@ header {
background: rgba(220, 53, 69, 0.08);
}
+/* WebShell 行内“操作”下拉菜单(替代一堆按钮) */
+.webshell-conn-actions,
+.webshell-row-actions {
+ display: inline-block;
+ position: relative;
+}
+
+.webshell-conn-actions summary,
+.webshell-row-actions summary {
+ list-style: none;
+ cursor: pointer;
+ user-select: none;
+}
+
+.webshell-conn-actions summary::-webkit-details-marker,
+.webshell-row-actions summary::-webkit-details-marker {
+ display: none;
+}
+
+.webshell-row-actions-menu {
+ display: none;
+ position: absolute;
+ right: 0;
+ top: calc(100% + 6px);
+ z-index: 20;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 10px;
+ padding: 6px;
+ box-shadow: var(--shadow-md);
+ min-width: 180px;
+ gap: 6px;
+ flex-direction: column;
+}
+
+.webshell-toolbar-actions {
+ position: relative;
+}
+
+.webshell-toolbar-actions .webshell-row-actions-menu {
+ min-width: 220px;
+}
+
+.webshell-conn-actions[open] .webshell-row-actions-menu,
+.webshell-row-actions[open] .webshell-row-actions-menu,
+.webshell-toolbar-actions[open] .webshell-row-actions-menu {
+ display: flex;
+}
+
+.webshell-row-actions-menu .btn-ghost {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ margin: 0;
+ padding: 8px 12px;
+ border-radius: 8px;
+ border: 1px solid transparent;
+ font-size: 0.86rem;
+ white-space: nowrap;
+}
+
+.webshell-row-actions-menu .btn-ghost:hover {
+ background: rgba(0, 102, 255, 0.08);
+ border-color: rgba(0, 102, 255, 0.22);
+}
+
+.webshell-row-actions-menu .webshell-file-del:hover {
+ background: rgba(220, 53, 69, 0.08);
+ border-color: rgba(220, 53, 69, 0.25);
+}
+
+.webshell-conn-actions-btn,
+.webshell-row-actions-btn,
+.webshell-toolbar-actions-btn {
+ min-width: 72px;
+ text-align: center;
+ border: 1px solid var(--border-color);
+ background: #fff;
+}
+
+.webshell-conn-actions-btn:hover,
+.webshell-row-actions-btn:hover,
+.webshell-toolbar-actions-btn:hover {
+ background: var(--bg-secondary);
+}
+
.webshell-loading {
padding: 24px 20px;
color: var(--text-muted);
diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index 94ce2e9d..1d9fd0fc 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -19,7 +19,8 @@
"copy": "Copy",
"copied": "Copied",
"copyFailed": "Copy failed",
- "view": "View"
+ "view": "View",
+ "actions": "Actions"
},
"header": {
"title": "CyberStrikeAI",
@@ -409,7 +410,19 @@
"selectAll": "Select all",
"searchPlaceholder": "Search connections...",
"noMatchConnections": "No matching connections",
- "breadcrumbHome": "Root"
+ "breadcrumbHome": "Root",
+ "back": "Back",
+ "moreActions": "More actions",
+ "batchProbe": "Batch probe",
+ "probeRunning": "Probing",
+ "probeOnline": "Online",
+ "probeOffline": "Offline",
+ "probeNoConnections": "No connections to probe",
+ "colModifiedAt": "Modified",
+ "colPerms": "Permissions",
+ "colOwner": "Owner",
+ "colGroup": "Group",
+ "colType": "Type"
},
"mcp": {
"monitorTitle": "MCP Status Monitor",
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index b8bb4f87..3395c1ce 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -19,7 +19,8 @@
"copy": "复制",
"copied": "已复制",
"copyFailed": "复制失败",
- "view": "查看"
+ "view": "查看",
+ "actions": "操作"
},
"header": {
"title": "CyberStrikeAI",
@@ -409,7 +410,19 @@
"selectAll": "全选",
"searchPlaceholder": "搜索连接...",
"noMatchConnections": "暂无匹配连接",
- "breadcrumbHome": "根"
+ "breadcrumbHome": "根",
+ "back": "返回",
+ "moreActions": "更多操作",
+ "batchProbe": "一键批量探活",
+ "probeRunning": "探活中",
+ "probeOnline": "在线",
+ "probeOffline": "离线",
+ "probeNoConnections": "暂无可探活连接",
+ "colModifiedAt": "修改时间",
+ "colPerms": "权限",
+ "colOwner": "所有者",
+ "colGroup": "用户组",
+ "colType": "类型"
},
"mcp": {
"monitorTitle": "MCP 状态监控",
diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js
index 3d040667..c487c3b0 100644
--- a/web/static/js/webshell.js
+++ b/web/static/js/webshell.js
@@ -25,6 +25,8 @@ let webshellAiConvMap = {};
let webshellAiSending = false;
// 流式打字机效果:当前会话的 response 序号,用于中止过期的打字
let webshellStreamingTypingId = 0;
+let webshellProbeStatusById = {};
+let webshellBatchProbeRunning = false;
/** 与主对话页一致:multi_agent.enabled 且本地模式为 multi 时使用 /api/multi-agent/stream */
function resolveWebshellAiStreamPath() {
@@ -116,13 +118,26 @@ function wsT(key) {
'webshell.filterPlaceholder': '过滤文件名',
'webshell.batchDelete': '批量删除',
'webshell.batchDownload': '批量下载',
+ 'webshell.moreActions': '更多操作',
'webshell.refresh': '刷新',
'webshell.selectAll': '全选',
'webshell.breadcrumbHome': '根',
'webshell.searchPlaceholder': '搜索连接...',
'webshell.noMatchConnections': '暂无匹配连接',
+ 'webshell.batchProbe': '一键批量探活',
+ 'webshell.probeRunning': '探活中',
+ 'webshell.probeOnline': '在线',
+ 'webshell.probeOffline': '离线',
+ 'webshell.probeNoConnections': '暂无可探活连接',
+ 'webshell.back': '返回',
+ 'webshell.colModifiedAt': '修改时间',
+ 'webshell.colPerms': '权限',
+ 'webshell.colOwner': '所有者',
+ 'webshell.colGroup': '用户组',
+ 'webshell.colType': '类型',
'common.delete': '删除',
- 'common.refresh': '刷新'
+ 'common.refresh': '刷新',
+ 'common.actions': '操作'
};
return fallback[key] || key;
}
@@ -149,9 +164,30 @@ function bindWebshellClearOnce() {
}, true);
}
+// WebShell 行内/工具栏“操作”下拉:点击菜单外自动收起
+function bindWebshellActionMenusAutoCloseOnce() {
+ if (window._webshellActionMenusAutoCloseBound) return;
+ window._webshellActionMenusAutoCloseBound = true;
+ document.addEventListener('click', function (e) {
+ // 只要点在 details 内部,就让浏览器自行切换(open/close)
+ var clickedInMenu = e.target && e.target.closest && (
+ e.target.closest('details.webshell-conn-actions') ||
+ e.target.closest('details.webshell-row-actions') ||
+ e.target.closest('details.webshell-toolbar-actions')
+ );
+ if (clickedInMenu) return;
+
+ var openDetails = document.querySelectorAll(
+ 'details.webshell-conn-actions[open],details.webshell-row-actions[open],details.webshell-toolbar-actions[open]'
+ );
+ openDetails.forEach(function (d) { d.open = false; });
+ }, true);
+}
+
// 初始化 WebShell 管理页面(从 SQLite 拉取连接列表)
function initWebshellPage() {
bindWebshellClearOnce();
+ bindWebshellActionMenusAutoCloseOnce();
destroyWebshellTerminal();
webshellCurrentConn = null;
currentWebshellId = null;
@@ -177,6 +213,15 @@ function initWebshellPage() {
webshellConnections = list;
renderWebshellList();
});
+
+ var batchProbeBtn = document.getElementById('webshell-batch-probe-btn');
+ if (batchProbeBtn && batchProbeBtn.dataset.bound !== '1') {
+ batchProbeBtn.dataset.bound = '1';
+ batchProbeBtn.addEventListener('click', function () {
+ runBatchProbeWebshellConnections();
+ });
+ }
+ updateWebshellBatchProbeButton();
}
function getWebshellSidebarWidth() {
@@ -287,13 +332,26 @@ function renderWebshellList() {
const urlTitle = (conn.url || '').replace(/&/g, '&').replace(/"/g, '"').replace(/' + (wsT('webshell.probeRunning') || '探活中') + '';
+ } else if (probe && probe.state === 'ok') {
+ probeHtml = '' + (wsT('webshell.probeOnline') || '在线') + '';
+ } else if (probe && probe.state === 'fail') {
+ probeHtml = '' + (wsT('webshell.probeOffline') || '离线') + '';
+ }
return (
'
' +
- '' +
+ '' +
'
' + url + '
' +
'
' +
- ' ' +
+ '' + actionsLabel + '
' +
+ ' ' +
'
' +
'
'
);
@@ -301,7 +359,7 @@ function renderWebshellList() {
listEl.querySelectorAll('.webshell-item').forEach(el => {
el.addEventListener('click', function (e) {
- if (e.target.closest('.webshell-delete-btn') || e.target.closest('.webshell-edit-conn-btn')) return;
+ if (e.target.closest('.webshell-delete-btn') || e.target.closest('.webshell-edit-conn-btn') || e.target.closest('.webshell-conn-actions-btn')) return;
selectWebshell(el.getAttribute('data-id'));
});
});
@@ -319,6 +377,102 @@ function renderWebshellList() {
});
}
+function probeWebshellConnection(conn) {
+ if (!conn || typeof apiFetch === 'undefined') {
+ return Promise.resolve({ ok: false, message: wsT('webshell.testFailed') || '连通性测试失败' });
+ }
+ return apiFetch('/api/webshell/exec', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ url: conn.url,
+ password: conn.password || '',
+ type: conn.type || 'php',
+ method: ((conn.method || 'post').toLowerCase() === 'get') ? 'get' : 'post',
+ cmd_param: conn.cmdParam || '',
+ command: 'echo 1'
+ })
+ })
+ .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');
+ 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 };
+ })
+ .catch(function (e) {
+ return { ok: false, message: (e && e.message) ? e.message : String(e) };
+ });
+}
+
+function updateWebshellBatchProbeButton(done, total, okCount) {
+ var btn = document.getElementById('webshell-batch-probe-btn');
+ if (!btn) return;
+ if (webshellBatchProbeRunning) {
+ var d = typeof done === 'number' ? done : 0;
+ var t = typeof total === 'number' ? total : webshellConnections.length;
+ btn.disabled = true;
+ btn.textContent = (wsT('webshell.probeRunning') || '探活中') + ' ' + d + '/' + t;
+ return;
+ }
+ btn.disabled = false;
+ if (typeof done === 'number' && typeof total === 'number' && total > 0 && typeof okCount === 'number') {
+ btn.textContent = (wsT('webshell.batchProbe') || '一键批量探活') + ' (' + okCount + '/' + total + ')';
+ } else {
+ btn.textContent = wsT('webshell.batchProbe') || '一键批量探活';
+ }
+}
+
+function runBatchProbeWebshellConnections() {
+ if (webshellBatchProbeRunning) return;
+ if (!Array.isArray(webshellConnections) || webshellConnections.length === 0) {
+ alert(wsT('webshell.probeNoConnections') || '暂无可探活连接');
+ return;
+ }
+ webshellBatchProbeRunning = true;
+ var total = webshellConnections.length;
+ var done = 0;
+ var okCount = 0;
+
+ webshellConnections.forEach(function (conn) {
+ if (!conn || !conn.id) return;
+ webshellProbeStatusById[conn.id] = { state: 'probing', message: '' };
+ });
+ renderWebshellList();
+ updateWebshellBatchProbeButton(done, total, okCount);
+
+ var idx = 0;
+ var concurrency = Math.min(4, total);
+
+ function runOne() {
+ if (idx >= total) return Promise.resolve();
+ var conn = webshellConnections[idx++];
+ if (!conn || !conn.id) {
+ done++;
+ updateWebshellBatchProbeButton(done, total, okCount);
+ return runOne();
+ }
+ return probeWebshellConnection(conn).then(function (res) {
+ if (res.ok) okCount++;
+ webshellProbeStatusById[conn.id] = {
+ state: res.ok ? 'ok' : 'fail',
+ message: res.message || ''
+ };
+ done++;
+ renderWebshellList();
+ updateWebshellBatchProbeButton(done, total, okCount);
+ }).then(runOne);
+ }
+
+ var workers = [];
+ for (var i = 0; i < concurrency; i++) workers.push(runOne());
+ Promise.all(workers).finally(function () {
+ webshellBatchProbeRunning = false;
+ updateWebshellBatchProbeButton(done, total, okCount);
+ });
+}
+
function escapeHtml(s) {
if (!s) return '';
const div = document.createElement('div');
@@ -566,16 +720,24 @@ function selectWebshell(id) {
'' +
@@ -1343,21 +1505,86 @@ function webshellFileListDir(conn, path) {
}
function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) {
+ function normalizeLsMtime(month, day, timeOrYear) {
+ if (!month || !day || !timeOrYear) return '';
+ var token = String(timeOrYear).trim();
+ if (/^\d{4}$/.test(token)) {
+ return token + ' ' + month + ' ' + day;
+ }
+ var now = new Date();
+ var year = now.getFullYear();
+ if (/^\d{1,2}:\d{2}$/.test(token)) {
+ // ls -l 在半年内通常只显示 HH:MM;推断年份(避免未来日期)
+ var monthMap = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
+ var m = monthMap[month];
+ var d = parseInt(day, 10);
+ if (m != null && !isNaN(d)) {
+ var inferred = new Date(year, m, d);
+ if (inferred.getTime() > now.getTime()) year = year - 1;
+ }
+ return year + ' ' + month + ' ' + day + ' ' + token;
+ }
+ return month + ' ' + day + ' ' + token;
+ }
+
+ function modeToType(mode) {
+ if (!mode || !mode.length) return '';
+ var c = mode.charAt(0);
+ if (c === 'd') return 'dir';
+ if (c === '-') return 'file';
+ if (c === 'l') return 'link';
+ if (c === 'c') return 'char';
+ if (c === 'b') return 'block';
+ if (c === 's') return 'socket';
+ if (c === 'p') return 'pipe';
+ return c;
+ }
+
var lines = rawOutput.split(/\n/).filter(function (l) { return l.trim(); });
var items = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
- var m = line.match(/\s*(\S+)\s*$/);
- var name = m ? m[1].trim() : line.trim();
- if (name === '.' || name === '..') continue;
- var isDir = line.startsWith('d') || line.toLowerCase().indexOf('') !== -1;
+ var name = '';
+ var isDir = false;
var size = '';
var mode = '';
- if (line.startsWith('-') || line.startsWith('d')) {
- var parts = line.split(/\s+/);
- if (parts.length >= 5) { mode = parts[0]; size = parts[4]; }
+ var mtime = '';
+ var owner = '';
+ var group = '';
+ var type = '';
+
+ // 兼容典型:ls -la 输出(mode links owner group size month day time|year name)
+ // 示例:-rw-r--r-- 1 user group 1234 Mar 23 12:34 file.txt
+ var mLs = line.match(/^(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+([A-Za-z]{3})\s+(\d{1,2})\s+(\S+)\s+(.+)$/);
+ if (mLs) {
+ mode = mLs[1];
+ owner = mLs[3];
+ group = mLs[4];
+ size = mLs[5];
+ mtime = normalizeLsMtime(mLs[6], mLs[7], mLs[8]);
+ name = (mLs[9] || '').trim();
+ 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+/);
+ if (parts.length >= 5) { mode = parts[0]; size = parts[4]; }
+ if (parts.length >= 4) { owner = parts[2] || ''; group = parts[3] || ''; }
+ // 尝试解析 mtime:month day (time|year)
+ if (parts.length >= 8 && /^[A-Za-z]{3}$/.test(parts[5])) {
+ mtime = normalizeLsMtime(parts[5], parts[6], parts[7]);
+ }
+ type = modeToType(mode);
+ }
}
- items.push({ name: name, isDir: isDir, line: line, size: size, mode: mode });
+
+ if (name === '.' || name === '..') continue;
+ items.push({ name: name, isDir: isDir, line: line, size: size, mode: mode, mtime: mtime, owner: owner, group: group, type: type });
}
if (nameFilter && nameFilter.trim()) {
var f = nameFilter.trim().toLowerCase();
@@ -1374,26 +1601,44 @@ function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) {
}).join('');
}
var html = '';
- if (items.length === 0 && rawOutput.trim() && !nameFilter) {
- html = '' + escapeHtml(rawOutput) + '
';
+ if (items.length === 0) {
+ // 目录为空/过滤后为空时,给出明确空状态,避免 tbody 留白导致“整块抽象大白屏”
+ if (rawOutput.trim() && !nameFilter) {
+ html = '' + escapeHtml(rawOutput) + '
';
+ } else {
+ html = '';
+ }
} else {
- html = ' | ' + wsT('webshell.filePath') + ' | 大小 | |
';
+ html = ' | ' + wsT('webshell.filePath') + ' | 大小 | ' + (wsT('webshell.colModifiedAt') || '修改时间') + ' | ' + (wsT('webshell.colOwner') || '所有者') + ' | ' + (wsT('webshell.colGroup') || '用户组') + ' | ' + (wsT('webshell.colPerms') || '权限') + ' | ' + (wsT('webshell.colType') || '类型') + ' | |
';
if (currentPath !== '.' && currentPath !== '') {
- html += ' | .. | | |
';
+ html += ' | .. | | | | | | | |
';
}
items.forEach(function (item) {
var pathNext = currentPath === '.' ? item.name : currentPath + '/' + item.name;
html += '| ';
if (!item.isDir) html += '';
- html += ' | ' + escapeHtml(item.name) + (item.isDir ? '/' : '') + ' | ' + escapeHtml(item.size) + ' | ';
+ html += ' | ' + escapeHtml(item.name) + (item.isDir ? '/' : '') + ' | ';
+ html += '' + escapeHtml(item.size) + ' | ';
+ html += '' + escapeHtml(item.mtime || '') + ' | ';
+ html += '' + escapeHtml(item.owner || '') + ' | ';
+ html += '' + escapeHtml(item.group || '') + ' | ';
+ html += '' + escapeHtml(item.mode || '') + ' | ';
+ html += '' + escapeHtml(item.type || '') + ' | ';
+ html += '';
if (item.isDir) {
html += '';
} else {
- html += ' ';
- html += ' ';
- html += ' ';
- html += ' ';
- html += '';
+ var actionsLabel = wsT('common.actions') || '操作';
+ html += '' + actionsLabel + '' +
+ ' ';
}
html += ' |
';
});
@@ -1407,15 +1652,19 @@ function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) {
const path = a.getAttribute('data-path');
const isDir = a.getAttribute('data-isdir') === '1';
const pathInput = document.getElementById('webshell-file-path');
- if (pathInput) pathInput.value = path;
- if (isDir) webshellFileListDir(webshellCurrentConn, path);
- else webshellFileRead(webshellCurrentConn, path, listEl);
+ if (isDir) {
+ if (pathInput) pathInput.value = path;
+ webshellFileListDir(webshellCurrentConn, path);
+ } else {
+ // 打开文件时保留当前“浏览目录”上下文,避免返回时落到单文件视图
+ webshellFileRead(webshellCurrentConn, path, listEl, currentPath);
+ }
});
});
listEl.querySelectorAll('.webshell-file-read').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
- webshellFileRead(webshellCurrentConn, btn.getAttribute('data-path'), listEl);
+ webshellFileRead(webshellCurrentConn, btn.getAttribute('data-path'), listEl, currentPath);
});
});
listEl.querySelectorAll('.webshell-file-download').forEach(function (btn) {
@@ -1600,7 +1849,7 @@ function webshellFileDownload(conn, path) {
.catch(function (err) { alert(wsT('webshell.execError') + ': ' + (err && err.message ? err.message : '')); });
}
-function webshellFileRead(conn, path, listEl) {
+function webshellFileRead(conn, path, listEl, browsePath) {
if (typeof apiFetch === 'undefined') return;
listEl.innerHTML = '' + wsT('webshell.readFile') + '...
';
apiFetch('/api/webshell/file', {
@@ -1610,7 +1859,19 @@ function webshellFileRead(conn, path, listEl) {
}).then(function (r) { return r.json(); })
.then(function (data) {
const out = (data && data.output) ? data.output : (data.error || '');
- listEl.innerHTML = '' + escapeHtml(out) + '
';
+ var backPath = (browsePath && String(browsePath).trim()) ? String(browsePath).trim() : ((document.getElementById('webshell-file-path') && document.getElementById('webshell-file-path').value.trim()) || '.');
+ if (backPath === path) {
+ // 兜底:若路径被污染成文件路径,回退到父目录
+ backPath = path.replace(/\/[^/]+$/, '') || '.';
+ }
+ listEl.innerHTML = '' + escapeHtml(out) + '
';
+ var backBtn = document.getElementById('webshell-file-back-btn');
+ if (backBtn) {
+ backBtn.addEventListener('click', function () {
+ var p = backBtn.getAttribute('data-back-path') || '.';
+ webshellFileListDir(webshellCurrentConn, p);
+ });
+ }
})
.catch(function (err) {
listEl.innerHTML = '' + escapeHtml(err && err.message ? err.message : '') + '
';
diff --git a/web/templates/index.html b/web/templates/index.html
index 100b7961..cd72a52d 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -1052,6 +1052,9 @@
data-i18n-attr="placeholder"
placeholder="搜索连接..." />
+