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 = '';
}
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');