From 7222466cffb9a6f4510e2f9f875ab472107eaf0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:57:30 +0800 Subject: [PATCH] Add files via upload --- internal/handler/webshell.go | 64 +++++- web/static/css/style.css | 56 +++++ web/static/i18n/en-US.json | 17 +- web/static/i18n/zh-CN.json | 17 +- web/static/js/webshell.js | 430 ++++++++++++++++++++++++++++++++--- 5 files changed, 543 insertions(+), 41 deletions(-) diff --git a/internal/handler/webshell.go b/internal/handler/webshell.go index da7ecd22..34356c68 100644 --- a/internal/handler/webshell.go +++ b/internal/handler/webshell.go @@ -217,14 +217,16 @@ type ExecResponse struct { // FileOpRequest 文件操作请求 type FileOpRequest struct { - URL string `json:"url" binding:"required"` - Password string `json:"password"` - Type string `json:"type"` - Method string `json:"method"` // GET 或 POST,空则默认 POST - CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd - Action string `json:"action" binding:"required"` // list, read, delete, write - Path string `json:"path"` - Content string `json:"content"` // write 时使用 + URL string `json:"url" binding:"required"` + Password string `json:"password"` + Type string `json:"type"` + Method string `json:"method"` // GET 或 POST,空则默认 POST + CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd + Action string `json:"action" binding:"required"` // list, read, delete, write, mkdir, rename, upload, upload_chunk + Path string `json:"path"` + TargetPath string `json:"target_path"` // rename 时目标路径 + Content string `json:"content"` // write/upload 时使用 + ChunkIndex int `json:"chunk_index"` // upload_chunk 时,0 表示首块 } // FileOpResponse 文件操作响应 @@ -371,6 +373,52 @@ func (h *WebShellHandler) FileOp(c *gin.Context) { case "write": path := h.escapePath(strings.TrimSpace(req.Path)) command = "echo " + h.escapeForEcho(req.Content) + " > " + path + case "mkdir": + path := strings.TrimSpace(req.Path) + if path == "" { + c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path is required for mkdir"}) + return + } + if shellType == "asp" || shellType == "aspx" { + command = "md " + h.escapePath(path) + } else { + command = "mkdir -p " + h.escapePath(path) + } + case "rename": + oldPath := strings.TrimSpace(req.Path) + newPath := strings.TrimSpace(req.TargetPath) + if oldPath == "" || newPath == "" { + c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path and target_path are required for rename"}) + return + } + if shellType == "asp" || shellType == "aspx" { + command = "move /y " + h.escapePath(oldPath) + " " + h.escapePath(newPath) + } else { + command = "mv " + h.escapePath(oldPath) + " " + h.escapePath(newPath) + } + case "upload": + path := strings.TrimSpace(req.Path) + if path == "" { + c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path is required for upload"}) + return + } + if len(req.Content) > 512*1024 { + c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "upload content too large (max 512KB base64)"}) + return + } + // base64 仅含 A-Za-z0-9+/=,用单引号包裹安全 + command = "echo " + "'" + req.Content + "'" + " | base64 -d > " + h.escapePath(path) + case "upload_chunk": + path := strings.TrimSpace(req.Path) + if path == "" { + c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path is required for upload_chunk"}) + return + } + redir := ">>" + if req.ChunkIndex == 0 { + redir = ">" + } + command = "echo " + "'" + req.Content + "'" + " | base64 -d " + redir + " " + h.escapePath(path) default: c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "unsupported action: " + req.Action}) return diff --git a/web/static/css/style.css b/web/static/css/style.css index d8a3f90e..7c6eceae 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -8745,6 +8745,31 @@ header { display: flex; } +.webshell-terminal-toolbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + padding: 10px 14px; + margin-bottom: 10px; + background: var(--bg-secondary); + border-radius: 10px; + border: 1px solid var(--border-color); +} +.webshell-quick-label { + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + margin-right: 4px; +} +.webshell-terminal-toolbar .btn-ghost { + font-size: 12px; +} + +#webshell-pane-terminal { + flex-direction: column; +} + /* 仅外框圆角,内部不做额外装饰,避免挡住文字 */ .webshell-terminal-container { flex: 1; @@ -8851,6 +8876,37 @@ header { transition: all 0.2s ease; } +.webshell-file-breadcrumb { + width: 100%; + flex: 1 1 100%; + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 4px; +} +.webshell-file-breadcrumb a.webshell-breadcrumb-item { + color: var(--accent-color); + text-decoration: none; + padding: 2px 4px; + border-radius: 4px; +} +.webshell-file-breadcrumb a.webshell-breadcrumb-item:hover { + background: rgba(0, 102, 255, 0.08); +} +.webshell-file-filter { + min-width: 140px !important; + max-width: 200px !important; +} +.webshell-col-check { + width: 36px; + text-align: center; + vertical-align: middle; +} +.webshell-col-size { + width: 80px; + color: var(--text-secondary); + font-size: 0.85rem; +} + .webshell-file-list { flex: 1; overflow: auto; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index da7b5c1a..3802d33e 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -355,6 +355,8 @@ "editConnectionTitle": "Edit connection", "tabTerminal": "Virtual terminal", "tabFileManager": "File manager", + "quickCommands": "Quick commands", + "downloadFile": "Download", "terminalWelcome": "WebShell virtual terminal — type a command and press Enter (Ctrl+L clear)", "filePath": "Current path", "listDir": "List directory", @@ -368,7 +370,20 @@ "testConnectivity": "Test connectivity", "testSuccess": "Connection OK, shell is reachable", "testFailed": "Connectivity test failed", - "testNoExpectedOutput": "Shell responded but expected output was not found. Check password and command parameter name." + "testNoExpectedOutput": "Shell responded but expected output was not found. Check password and command parameter name.", + "clearScreen": "Clear", + "running": "Running…", + "waitFinish": "Please wait for the current command to finish", + "newDir": "New directory", + "rename": "Rename", + "upload": "Upload", + "newFile": "New file", + "filterPlaceholder": "Filter by name", + "batchDelete": "Batch delete", + "batchDownload": "Batch download", + "refresh": "Refresh", + "selectAll": "Select all", + "breadcrumbHome": "Root" }, "mcp": { "monitorTitle": "MCP Status Monitor", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index b629d63b..e266c3b7 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -355,6 +355,8 @@ "editConnectionTitle": "编辑连接", "tabTerminal": "虚拟终端", "tabFileManager": "文件管理", + "quickCommands": "快捷命令", + "downloadFile": "下载", "terminalWelcome": "WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)", "filePath": "当前路径", "listDir": "列出目录", @@ -368,7 +370,20 @@ "testConnectivity": "测试连通性", "testSuccess": "连通性正常,Shell 可访问", "testFailed": "连通性测试失败", - "testNoExpectedOutput": "Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名" + "testNoExpectedOutput": "Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名", + "clearScreen": "清屏", + "running": "执行中…", + "waitFinish": "请等待当前命令执行完成", + "newDir": "新建目录", + "rename": "重命名", + "upload": "上传", + "newFile": "新建文件", + "filterPlaceholder": "过滤文件名", + "batchDelete": "批量删除", + "batchDownload": "批量下载", + "refresh": "刷新", + "selectAll": "全选", + "breadcrumbHome": "根" }, "mcp": { "monitorTitle": "MCP 状态监控", diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index 388f2b27..feb4829d 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -12,6 +12,10 @@ let webshellTerminalResizeContainer = null; let webshellCurrentConn = null; let webshellLineBuffer = ''; let webshellRunning = false; +// 按连接保存命令历史,用于上下键 +let webshellHistoryByConn = {}; +let webshellHistoryIndex = -1; +const WEBSHELL_HISTORY_MAX = 100; // 从服务端(SQLite)拉取连接列表 function getWebshellConnections() { @@ -54,6 +58,8 @@ function wsT(key) { 'webshell.tabTerminal': '虚拟终端', 'webshell.tabFileManager': '文件管理', 'webshell.terminalWelcome': 'WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)', + 'webshell.quickCommands': '快捷命令', + 'webshell.downloadFile': '下载', 'webshell.filePath': '当前路径', 'webshell.listDir': '列出目录', 'webshell.readFile': '读取', @@ -67,6 +73,19 @@ function wsT(key) { 'webshell.testSuccess': '连通性正常,Shell 可访问', 'webshell.testFailed': '连通性测试失败', 'webshell.testNoExpectedOutput': 'Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名', + 'webshell.clearScreen': '清屏', + 'webshell.running': '执行中…', + 'webshell.waitFinish': '请等待当前命令执行完成', + 'webshell.newDir': '新建目录', + 'webshell.rename': '重命名', + 'webshell.upload': '上传', + 'webshell.newFile': '新建文件', + 'webshell.filterPlaceholder': '过滤文件名', + 'webshell.batchDelete': '批量删除', + 'webshell.batchDownload': '批量下载', + 'webshell.refresh': '刷新', + 'webshell.selectAll': '全选', + 'webshell.breadcrumbHome': '根', 'common.delete': '删除', 'common.refresh': '刷新' }; @@ -238,13 +257,36 @@ function selectWebshell(id) { '' + '' + '
' + + '
' + + ' ' + + '' + (wsT('webshell.quickCommands') || '快捷命令') + ': ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + + '
' + '
' + '
' + '
' + '
' + + '
' + '' + - '' + - '' + + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + '
' + '
' + '
'; @@ -280,9 +322,71 @@ function selectWebshell(id) { webshellFileListDir(webshellCurrentConn, pathInput.value || '.'); }); + // 清屏 + var clearBtn = document.getElementById('webshell-terminal-clear'); + if (clearBtn) clearBtn.addEventListener('click', function () { + if (webshellTerminalInstance) { + webshellTerminalInstance.clear(); + webshellLineBuffer = ''; + webshellTerminalInstance.write(WEBSHELL_PROMPT); + } + }); + // 快捷命令:点击后执行并输出到终端 + workspace.querySelectorAll('.webshell-quick-cmd').forEach(function (btn) { + btn.addEventListener('click', function () { + var cmd = btn.getAttribute('data-cmd'); + if (cmd) runQuickCommand(cmd); + }); + }); + // 文件:刷新、新建目录、新建文件、上传、批量操作 + var filterInput = document.getElementById('webshell-file-filter'); + document.getElementById('webshell-file-refresh').addEventListener('click', function () { + webshellFileListDir(webshellCurrentConn, pathInput ? pathInput.value.trim() || '.' : '.'); + }); + if (filterInput) filterInput.addEventListener('input', function () { + webshellFileListApplyFilter(); + }); + document.getElementById('webshell-mkdir-btn').addEventListener('click', function () { webshellFileMkdir(webshellCurrentConn, pathInput); }); + document.getElementById('webshell-newfile-btn').addEventListener('click', function () { webshellFileNewFile(webshellCurrentConn, pathInput); }); + document.getElementById('webshell-upload-btn').addEventListener('click', function () { webshellFileUpload(webshellCurrentConn, pathInput); }); + document.getElementById('webshell-batch-delete-btn').addEventListener('click', function () { webshellBatchDelete(webshellCurrentConn, pathInput); }); + document.getElementById('webshell-batch-download-btn').addEventListener('click', function () { webshellBatchDownload(webshellCurrentConn, pathInput); }); + initWebshellTerminal(conn); } +function getWebshellHistory(connId) { + if (!connId) return []; + if (!webshellHistoryByConn[connId]) webshellHistoryByConn[connId] = []; + return webshellHistoryByConn[connId]; +} +function pushWebshellHistory(connId, cmd) { + if (!connId || !cmd) return; + if (!webshellHistoryByConn[connId]) webshellHistoryByConn[connId] = []; + var h = webshellHistoryByConn[connId]; + if (h[h.length - 1] === cmd) return; + h.push(cmd); + if (h.length > WEBSHELL_HISTORY_MAX) h.shift(); +} + +// 执行快捷命令并将输出写入当前终端 +function runQuickCommand(cmd) { + if (!webshellCurrentConn || !webshellTerminalInstance) return; + if (webshellRunning) return; + var term = webshellTerminalInstance; + term.writeln(''); + pushWebshellHistory(webshellCurrentConn.id, cmd); + webshellRunning = true; + execWebshellCommand(webshellCurrentConn, cmd).then(function (out) { + var s = String(out || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + s.split('\n').forEach(function (line) { term.writeln(line.replace(/\r/g, '')); }); + term.write(WEBSHELL_PROMPT); + }).catch(function (err) { + term.writeln('\x1b[31m' + (err && err.message ? err.message : wsT('webshell.execError')) + '\x1b[0m'); + term.write(WEBSHELL_PROMPT); + }).finally(function () { webshellRunning = false; }); +} + // ---------- 虚拟终端(xterm + 按行执行) ---------- function initWebshellTerminal(conn) { const container = document.getElementById('webshell-terminal-container'); @@ -343,17 +447,42 @@ function initWebshellTerminal(conn) { if (data === '\x0c') { term.clear(); webshellLineBuffer = ''; + webshellHistoryIndex = -1; term.write(WEBSHELL_PROMPT); return; } + // 上/下键:命令历史 + if (data === '\x1b[A' || data === '\x1bOA') { + var hist = getWebshellHistory(webshellCurrentConn ? webshellCurrentConn.id : ''); + if (hist.length === 0) return; + webshellHistoryIndex = webshellHistoryIndex < 0 ? hist.length : Math.max(0, webshellHistoryIndex - 1); + webshellLineBuffer = hist[webshellHistoryIndex] || ''; + term.write('\x1b[2K\r' + WEBSHELL_PROMPT + webshellLineBuffer); + return; + } + if (data === '\x1b[B' || data === '\x1bOB') { + var hist2 = getWebshellHistory(webshellCurrentConn ? webshellCurrentConn.id : ''); + if (hist2.length === 0) return; + webshellHistoryIndex = webshellHistoryIndex < 0 ? -1 : Math.min(hist2.length - 1, webshellHistoryIndex + 1); + if (webshellHistoryIndex < 0) webshellLineBuffer = ''; + else webshellLineBuffer = hist2[webshellHistoryIndex] || ''; + term.write('\x1b[2K\r' + WEBSHELL_PROMPT + webshellLineBuffer); + return; + } // 回车:发送当前行到后端执行 if (data === '\r' || data === '\n') { term.writeln(''); - const cmd = webshellLineBuffer.trim(); + var cmd = webshellLineBuffer.trim(); webshellLineBuffer = ''; + webshellHistoryIndex = -1; if (cmd) { + if (webshellRunning) { + writeWebshellOutput(term, wsT('webshell.waitFinish'), true); + term.write(WEBSHELL_PROMPT); + return; + } + pushWebshellHistory(webshellCurrentConn ? webshellCurrentConn.id : '', cmd); webshellRunning = true; - // 执行时用当前连接(编辑保存后 webshellCurrentConn 已更新),避免闭包持有旧 conn execWebshellCommand(webshellCurrentConn, cmd).then(function (out) { webshellRunning = false; if (out && out.length) writeWebshellOutput(term, out, false); @@ -368,6 +497,37 @@ function initWebshellTerminal(conn) { } return; } + // 多行粘贴:按行依次执行 + if (data.indexOf('\n') !== -1 || data.indexOf('\r') !== -1) { + var full = (webshellLineBuffer + data).replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + var lines = full.split('\n'); + webshellLineBuffer = lines.pop() || ''; + if (lines.length > 0 && !webshellRunning && webshellCurrentConn) { + var runNext = function (idx) { + if (idx >= lines.length) { + term.write(WEBSHELL_PROMPT + webshellLineBuffer); + return; + } + var line = lines[idx].trim(); + if (!line) { runNext(idx + 1); return; } + pushWebshellHistory(webshellCurrentConn.id, line); + webshellRunning = true; + execWebshellCommand(webshellCurrentConn, line).then(function (out) { + if (out && out.length) writeWebshellOutput(term, out, false); + webshellRunning = false; + runNext(idx + 1); + }).catch(function (err) { + writeWebshellOutput(term, err && err.message ? err.message : wsT('webshell.execError'), true); + webshellRunning = false; + runNext(idx + 1); + }); + }; + runNext(0); + } else { + term.write(data); + } + return; + } // 退格 if (data === '\x7f' || data === '\b') { if (webshellLineBuffer.length > 0) { @@ -453,6 +613,8 @@ function webshellFileListDir(conn, path) { listEl.innerHTML = '
' + escapeHtml(data.error) + '
' + escapeHtml(data.output || '') + '
'; return; } + listEl.dataset.currentPath = path; + listEl.dataset.rawOutput = data.output || ''; renderFileList(listEl, path, data.output || '', conn); }) .catch(function (err) { @@ -460,38 +622,62 @@ function webshellFileListDir(conn, path) { }); } -function renderFileList(listEl, currentPath, rawOutput, conn) { - // 解析 ls -la 风格输出为简单列表(兼容不同格式) - const lines = rawOutput.split(/\n/).filter(function (l) { return l.trim(); }); - const items = []; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const m = line.match(/\s*(\S+)\s*$/); // 最后一列作为名称 - const name = m ? m[1].trim() : line.trim(); +function renderFileList(listEl, currentPath, rawOutput, conn, nameFilter) { + 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; - const isDir = line.startsWith('d') || line.toLowerCase().indexOf('') !== -1; - items.push({ name: name, isDir: isDir, line: line }); + var isDir = line.startsWith('d') || line.toLowerCase().indexOf('') !== -1; + 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]; } + } + items.push({ name: name, isDir: isDir, line: line, size: size, mode: mode }); } - - let html = ''; - if (items.length === 0 && rawOutput.trim()) { + if (nameFilter && nameFilter.trim()) { + var f = nameFilter.trim().toLowerCase(); + items = items.filter(function (item) { return item.name.toLowerCase().indexOf(f) !== -1; }); + } + // 面包屑 + var breadcrumbEl = document.getElementById('webshell-file-breadcrumb'); + if (breadcrumbEl) { + var parts = (currentPath === '.' || currentPath === '') ? [] : currentPath.replace(/^\//, '').split('/'); + breadcrumbEl.innerHTML = '' + (wsT('webshell.breadcrumbHome') || '根') + '' + + parts.map(function (p, idx) { + var path = parts.slice(0, idx + 1).join('/'); + return ' / ' + escapeHtml(p) + ''; + }).join(''); + } + var html = ''; + if (items.length === 0 && rawOutput.trim() && !nameFilter) { html = '
' + escapeHtml(rawOutput) + '
'; } else { - html = ''; - if (currentPath !== '.' && currentPath !== '') { - html += ''; - } - items.forEach(function (item) { - const pathNext = currentPath === '.' ? item.name : currentPath + '/' + item.name; - html += ''; + }); + html += '
' + wsT('webshell.filePath') + '
..
' + escapeHtml(item.name) + (item.isDir ? '/' : '') + ''; - if (!item.isDir) { - html += ' '; - html += ' '; - html += ''; + html = ''; + if (currentPath !== '.' && currentPath !== '') { + html += ''; } - html += ''; - }); - html += '
' + wsT('webshell.filePath') + '大小
..
'; + 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) + ''; + if (item.isDir) { + html += ''; + } else { + html += ' '; + html += ' '; + html += ' '; + html += ' '; + html += ''; + } + html += '
'; } listEl.innerHTML = html; @@ -512,6 +698,12 @@ function renderFileList(listEl, currentPath, rawOutput, conn) { webshellFileRead(webshellCurrentConn, btn.getAttribute('data-path'), listEl); }); }); + listEl.querySelectorAll('.webshell-file-download').forEach(function (btn) { + btn.addEventListener('click', function (e) { + e.preventDefault(); + webshellFileDownload(webshellCurrentConn, btn.getAttribute('data-path')); + }); + }); listEl.querySelectorAll('.webshell-file-edit').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); @@ -527,6 +719,165 @@ function renderFileList(listEl, currentPath, rawOutput, conn) { }); }); }); + listEl.querySelectorAll('.webshell-file-rename').forEach(function (btn) { + btn.addEventListener('click', function (e) { + e.preventDefault(); + webshellFileRename(webshellCurrentConn, btn.getAttribute('data-path'), btn.getAttribute('data-name'), listEl); + }); + }); + var selectAll = document.getElementById('webshell-file-select-all'); + if (selectAll) { + selectAll.addEventListener('change', function () { + listEl.querySelectorAll('.webshell-file-cb').forEach(function (cb) { cb.checked = selectAll.checked; }); + }); + } + if (breadcrumbEl) { + breadcrumbEl.querySelectorAll('.webshell-breadcrumb-item').forEach(function (a) { + a.addEventListener('click', function (e) { + e.preventDefault(); + var p = a.getAttribute('data-path'); + var pathInput = document.getElementById('webshell-file-path'); + if (pathInput) pathInput.value = p; + webshellFileListDir(webshellCurrentConn, p); + }); + }); + } +} + +function webshellFileListApplyFilter() { + var listEl = document.getElementById('webshell-file-list'); + var path = listEl && listEl.dataset.currentPath ? listEl.dataset.currentPath : (document.getElementById('webshell-file-path') && document.getElementById('webshell-file-path').value.trim()) || '.'; + var raw = listEl && listEl.dataset.rawOutput ? listEl.dataset.rawOutput : ''; + var filterInput = document.getElementById('webshell-file-filter'); + var filter = filterInput ? filterInput.value : ''; + if (!listEl || !raw) return; + renderFileList(listEl, path, raw, webshellCurrentConn, filter); +} + +function webshellFileMkdir(conn, pathInput) { + if (!conn || typeof apiFetch === 'undefined') return; + var base = (pathInput && pathInput.value.trim()) || '.'; + var name = prompt(wsT('webshell.newDir') || '新建目录', 'newdir'); + if (name == null || !name.trim()) return; + var path = base === '.' ? name.trim() : base + '/' + name.trim(); + apiFetch('/api/webshell/file', { 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(), cmd_param: conn.cmdParam || '', action: 'mkdir', path: path }) }) + .then(function (r) { return r.json(); }) + .then(function () { webshellFileListDir(conn, base); }) + .catch(function () { webshellFileListDir(conn, base); }); +} + +function webshellFileNewFile(conn, pathInput) { + if (!conn || typeof apiFetch === 'undefined') return; + var base = (pathInput && pathInput.value.trim()) || '.'; + var name = prompt(wsT('webshell.newFile') || '新建文件', 'newfile.txt'); + if (name == null || !name.trim()) return; + var path = base === '.' ? name.trim() : base + '/' + name.trim(); + var content = prompt('初始内容(可选)', ''); + if (content === null) return; + var listEl = document.getElementById('webshell-file-list'); + webshellFileWrite(conn, path, content || '', function () { webshellFileListDir(conn, base); }, listEl); +} + +function webshellFileUpload(conn, pathInput) { + if (!conn || typeof apiFetch === 'undefined') return; + var base = (pathInput && pathInput.value.trim()) || '.'; + var input = document.createElement('input'); + input.type = 'file'; + input.multiple = false; + input.onchange = function () { + var file = input.files && input.files[0]; + if (!file) return; + var reader = new FileReader(); + reader.onload = function () { + var buf = reader.result; + var bin = new Uint8Array(buf); + var CHUNK = 32000; + var base64Chunks = []; + for (var i = 0; i < bin.length; i += CHUNK) { + var slice = bin.subarray(i, Math.min(i + CHUNK, bin.length)); + var b64 = btoa(String.fromCharCode.apply(null, slice)); + base64Chunks.push(b64); + } + var path = base === '.' ? file.name : base + '/' + file.name; + var listEl = document.getElementById('webshell-file-list'); + if (listEl) listEl.innerHTML = '
' + (wsT('webshell.upload') || '上传') + '...
'; + var idx = 0; + function sendNext() { + if (idx >= base64Chunks.length) { + webshellFileListDir(conn, base); + return; + } + apiFetch('/api/webshell/file', { 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(), cmd_param: conn.cmdParam || '', action: 'upload_chunk', path: path, content: base64Chunks[idx], chunk_index: idx }) }) + .then(function (r) { return r.json(); }) + .then(function () { idx++; sendNext(); }) + .catch(function () { idx++; sendNext(); }); + } + sendNext(); + }; + reader.readAsArrayBuffer(file); + }; + input.click(); +} + +function webshellFileRename(conn, oldPath, oldName, listEl) { + if (!conn || typeof apiFetch === 'undefined') return; + var newName = prompt((wsT('webshell.rename') || '重命名') + ': ' + oldName, oldName); + if (newName == null || newName.trim() === '') return; + var parts = oldPath.split('/'); + var dir = parts.length > 1 ? parts.slice(0, -1).join('/') + '/' : ''; + var newPath = dir + newName.trim(); + apiFetch('/api/webshell/file', { 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(), cmd_param: conn.cmdParam || '', action: 'rename', path: oldPath, target_path: newPath }) }) + .then(function (r) { return r.json(); }) + .then(function () { webshellFileListDir(conn, document.getElementById('webshell-file-path').value.trim() || '.'); }) + .catch(function () { webshellFileListDir(conn, document.getElementById('webshell-file-path').value.trim() || '.'); }); +} + +function webshellBatchDelete(conn, pathInput) { + if (!conn) return; + var listEl = document.getElementById('webshell-file-list'); + var checked = listEl ? listEl.querySelectorAll('.webshell-file-cb:checked') : []; + var paths = []; + checked.forEach(function (cb) { paths.push(cb.getAttribute('data-path')); }); + if (paths.length === 0) { alert(wsT('webshell.batchDelete') + ':请先勾选文件'); return; } + if (!confirm(wsT('webshell.batchDelete') + ':确定删除 ' + paths.length + ' 个文件?')) return; + var base = (pathInput && pathInput.value.trim()) || '.'; + var i = 0; + function delNext() { + if (i >= paths.length) { webshellFileListDir(conn, base); return; } + webshellFileDelete(conn, paths[i], function () { i++; delNext(); }); + } + delNext(); +} + +function webshellBatchDownload(conn, pathInput) { + if (!conn) return; + var listEl = document.getElementById('webshell-file-list'); + var checked = listEl ? listEl.querySelectorAll('.webshell-file-cb:checked') : []; + var paths = []; + checked.forEach(function (cb) { paths.push(cb.getAttribute('data-path')); }); + if (paths.length === 0) { alert(wsT('webshell.batchDownload') + ':请先勾选文件'); return; } + paths.forEach(function (path) { webshellFileDownload(conn, path); }); +} + +// 下载文件到本地(读取内容后触发浏览器下载) +function webshellFileDownload(conn, path) { + if (typeof apiFetch === 'undefined') return; + apiFetch('/api/webshell/file', { + 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(), cmd_param: conn.cmdParam || '', action: 'read', path: path }) + }).then(function (r) { return r.json(); }) + .then(function (data) { + var content = (data && data.output) != null ? data.output : (data.error || ''); + var name = path.replace(/^.*[/\\]/, '') || 'download.txt'; + var blob = new Blob([content], { type: 'application/octet-stream' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = name; + a.click(); + URL.revokeObjectURL(a.href); + }) + .catch(function (err) { alert(wsT('webshell.execError') + ': ' + (err && err.message ? err.message : '')); }); } function webshellFileRead(conn, path, listEl) { @@ -695,12 +1046,29 @@ function refreshWebshellUIOnLanguageChange() { if (tabTerminal) tabTerminal.textContent = wsT('webshell.tabTerminal'); if (tabFile) tabFile.textContent = wsT('webshell.tabFileManager'); + var quickLabel = workspace.querySelector('.webshell-quick-label'); + if (quickLabel) quickLabel.textContent = (wsT('webshell.quickCommands') || '快捷命令') + ':'; var pathLabel = workspace.querySelector('.webshell-file-toolbar label span'); var listDirBtn = document.getElementById('webshell-list-dir'); var parentDirBtn = document.getElementById('webshell-parent-dir'); if (pathLabel) pathLabel.textContent = wsT('webshell.filePath'); if (listDirBtn) listDirBtn.textContent = wsT('webshell.listDir'); if (parentDirBtn) parentDirBtn.textContent = wsT('webshell.parentDir'); + // 文件管理工具栏按钮(红框区域):切换语言时立即更新 + var refreshBtn = document.getElementById('webshell-file-refresh'); + var mkdirBtn = document.getElementById('webshell-mkdir-btn'); + var newFileBtn = document.getElementById('webshell-newfile-btn'); + var uploadBtn = document.getElementById('webshell-upload-btn'); + var batchDeleteBtn = document.getElementById('webshell-batch-delete-btn'); + var batchDownloadBtn = document.getElementById('webshell-batch-download-btn'); + var filterInput = document.getElementById('webshell-file-filter'); + if (refreshBtn) { refreshBtn.title = wsT('webshell.refresh') || '刷新'; refreshBtn.textContent = wsT('webshell.refresh') || '刷新'; } + if (mkdirBtn) mkdirBtn.textContent = wsT('webshell.newDir') || '新建目录'; + if (newFileBtn) newFileBtn.textContent = wsT('webshell.newFile') || '新建文件'; + if (uploadBtn) uploadBtn.textContent = wsT('webshell.upload') || '上传'; + if (batchDeleteBtn) batchDeleteBtn.textContent = wsT('webshell.batchDelete') || '批量删除'; + if (batchDownloadBtn) batchDownloadBtn.textContent = wsT('webshell.batchDownload') || '批量下载'; + if (filterInput) filterInput.placeholder = wsT('webshell.filterPlaceholder') || '过滤文件名'; var pathInput = document.getElementById('webshell-file-path'); var fileListEl = document.getElementById('webshell-file-list');