From 0076aaed474dd1f785c46a4f12845771ec2f1d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:39:32 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 222 ++++++++++++++++++++++++- web/static/i18n/en-US.json | 17 +- web/static/i18n/zh-CN.json | 17 +- web/static/js/webshell.js | 333 +++++++++++++++++++++++++++++++++---- web/templates/index.html | 3 + 5 files changed, 545 insertions(+), 47 deletions(-) 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(/'; + } else if (probe && probe.state === 'ok') { + probeHtml = '' + (wsT('webshell.probeOnline') || '在线') + ''; + } else if (probe && probe.state === 'fail') { + probeHtml = '' + (wsT('webshell.probeOffline') || '离线') + ''; + } return ( '
' + - '
' + remark + '
' + + '
' + remark + '
' + probeHtml + '
' + '
' + 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) { '
' + '
' + '
' + - '' + + '
' + + '' + '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '' + (wsT('webshell.moreActions') || '更多操作') + '' + + '
' + + '' + + '' + + '' + + '' + '' + + '
' + + '
' + '
' + '
' + '
' + @@ -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 = '' + + '' + + '
' + wsT('webshell.filePath') + '大小' + (wsT('webshell.colModifiedAt') || '修改时间') + '' + (wsT('webshell.colOwner') || '所有者') + '' + (wsT('webshell.colGroup') || '用户组') + '' + (wsT('webshell.colPerms') || '权限') + '' + (wsT('webshell.colType') || '类型') + '
' + (wsT('common.noData') || '暂无文件') + '
'; + } } else { - html = ''; + html = '
' + wsT('webshell.filePath') + '大小
'; if (currentPath !== '.' && currentPath !== '') { - html += ''; + html += ''; } items.forEach(function (item) { var pathNext = currentPath === '.' ? item.name : currentPath + '/' + item.name; html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + 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="搜索连接..." /> +
+ +
暂无连接,请点击「添加连接」
' + wsT('webshell.filePath') + '大小' + (wsT('webshell.colModifiedAt') || '修改时间') + '' + (wsT('webshell.colOwner') || '所有者') + '' + (wsT('webshell.colGroup') || '用户组') + '' + (wsT('webshell.colPerms') || '权限') + '' + (wsT('webshell.colType') || '类型') + '
..
..
'; if (!item.isDir) html += ''; - html += '' + escapeHtml(item.name) + (item.isDir ? '/' : '') + '' + escapeHtml(item.size) + ''; + html += '' + escapeHtml(item.name) + (item.isDir ? '/' : '') + '' + escapeHtml(item.size) + '' + escapeHtml(item.mtime || '') + '' + escapeHtml(item.owner || '') + '' + escapeHtml(item.group || '') + '' + escapeHtml(item.mode || '') + '' + escapeHtml(item.type || '') + ''; if (item.isDir) { html += ''; } else { - html += ' '; - html += ' '; - html += ' '; - html += ' '; - html += ''; + var actionsLabel = wsT('common.actions') || '操作'; + html += '
' + actionsLabel + '' + + '
' + + '' + + '' + + '' + + '' + + '' + + '
'; } html += '