From 4b105e0bb7c498c81ba2afe4ba2386d30dfa93f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:24:33 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 106 ++++++++++++++++++++++++++++++++++++ web/static/i18n/en-US.json | 6 +++ web/static/i18n/zh-CN.json | 6 +++ web/static/js/webshell.js | 107 +++++++++++++++++++++++++++++++++++-- 4 files changed, 222 insertions(+), 3 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index bdb9342c..889da714 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -9670,6 +9670,112 @@ header { flex-direction: column; min-height: 0; } +.webshell-pane-memo { + padding: 14px; + background: linear-gradient(180deg, rgba(248, 250, 252, 0.55) 0%, rgba(241, 245, 249, 0.28) 100%); +} +.webshell-memo-layout { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 14px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 250, 252, 0.9) 100%); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.85); + overflow: hidden; +} +.webshell-memo-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 14px; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + font-size: 0.92rem; + font-weight: 600; + color: var(--text-primary); + background: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(6px); +} +.webshell-memo-input { + flex: 1; + min-height: 0; + margin: 12px 14px 8px; + padding: 12px 13px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.14); + background: #fff; + font-size: 0.9rem; + line-height: 1.55; + font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + color: var(--text-primary); + resize: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} +.webshell-memo-input:focus { + border-color: rgba(37, 99, 235, 0.5); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); + outline: none; +} +.webshell-memo-status { + margin: 0 14px 12px auto; + padding: 3px 10px; + border-radius: 999px; + font-size: 0.74rem; + color: var(--text-secondary); + background: rgba(148, 163, 184, 0.14); + border: 1px solid rgba(148, 163, 184, 0.25); +} +.webshell-memo-status.error { + color: #b91c1c; + background: rgba(239, 68, 68, 0.14); + border-color: rgba(239, 68, 68, 0.28); +} +.webshell-ai-memo { + flex-shrink: 0; + width: 300px; + min-width: 220px; + display: flex; + flex-direction: column; + border-left: 1px solid var(--border-color); + background: var(--bg-secondary); + min-height: 0; +} +.webshell-ai-memo-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid var(--border-color); + font-size: 0.9rem; + color: var(--text-primary); +} +.webshell-ai-memo-input { + flex: 1; + min-height: 0; + margin: 10px 12px 8px; + resize: none; +} +.webshell-ai-memo-status { + padding: 0 12px 10px; + font-size: 0.75rem; + color: var(--text-secondary); +} +.webshell-ai-memo-status.error { + color: #dc2626; +} +@media (max-width: 1280px) { + .webshell-ai-memo { + width: 260px; + } +} +@media (max-width: 980px) { + .webshell-ai-memo { + display: none; + } +} .webshell-ai-hint { flex-shrink: 0; padding: 10px 14px; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 3658c853..341a1796 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -374,6 +374,7 @@ "tabFileManager": "File manager", "tabAiAssistant": "AI Assistant", "tabDbManager": "Database Manager", + "tabMemo": "Memo", "dbType": "Database type", "dbHost": "Host", "dbPort": "Port", @@ -414,6 +415,11 @@ "aiDeleteConversationConfirm": "Delete this conversation?", "aiPlaceholder": "e.g. List files in the current directory", "aiSend": "Send", + "aiMemo": "Memo", + "aiMemoPlaceholder": "Save key commands, testing ideas, and repro steps...", + "aiMemoClear": "Clear", + "aiMemoSaving": "Saving...", + "aiMemoSaved": "Saved locally", "quickCommands": "Quick commands", "downloadFile": "Download", "terminalWelcome": "WebShell virtual terminal — type a command and press Enter (Ctrl+L clear)", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 61adece6..6cc7d816 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -374,6 +374,7 @@ "tabFileManager": "文件管理", "tabAiAssistant": "AI 助手", "tabDbManager": "数据库管理", + "tabMemo": "备忘录", "dbType": "数据库类型", "dbHost": "主机", "dbPort": "端口", @@ -414,6 +415,11 @@ "aiDeleteConversationConfirm": "确定删除当前对话记录?", "aiPlaceholder": "例如:列出当前目录下的文件", "aiSend": "发送", + "aiMemo": "备忘录", + "aiMemoPlaceholder": "记录关键命令、测试思路、复现步骤...", + "aiMemoClear": "清空", + "aiMemoSaving": "保存中...", + "aiMemoSaved": "已保存到本地", "quickCommands": "快捷命令", "downloadFile": "下载", "terminalWelcome": "WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)", diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index 110e08ba..27834502 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -99,6 +99,7 @@ function wsT(key) { 'webshell.tabFileManager': '文件管理', 'webshell.tabAiAssistant': 'AI 助手', 'webshell.tabDbManager': '数据库管理', + 'webshell.tabMemo': '备忘录', 'webshell.dbType': '数据库类型', 'webshell.dbHost': '主机', 'webshell.dbPort': '端口', @@ -135,6 +136,11 @@ function wsT(key) { 'webshell.aiSystemReadyMessage': '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。', 'webshell.aiPlaceholder': '例如:列出当前目录下的文件', 'webshell.aiSend': '发送', + 'webshell.aiMemo': '备忘录', + 'webshell.aiMemoPlaceholder': '记录关键命令、测试思路、复现步骤...', + 'webshell.aiMemoClear': '清空', + 'webshell.aiMemoSaving': '保存中...', + 'webshell.aiMemoSaved': '已保存到本地', 'webshell.terminalWelcome': 'WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)', 'webshell.quickCommands': '快捷命令', 'webshell.downloadFile': '下载', @@ -802,7 +808,9 @@ function normalizeWebshellDbState(rawState) { if (!profiles.some(function (p) { return p.id === activeProfileId; })) { activeProfileId = profiles[0].id; } - return { profiles: profiles, activeProfileId: activeProfileId }; + var aiMemo = typeof state.aiMemo === 'string' ? state.aiMemo : ''; + if (aiMemo.length > 100000) aiMemo = aiMemo.slice(0, 100000); + return { profiles: profiles, activeProfileId: activeProfileId, aiMemo: aiMemo }; } function getWebshellDbState(conn) { @@ -838,6 +846,18 @@ function saveWebshellDbConfig(conn, cfg) { saveWebshellDbState(conn, state); } +function getWebshellAiMemo(conn) { + var state = getWebshellDbState(conn); + return typeof state.aiMemo === 'string' ? state.aiMemo : ''; +} + +function saveWebshellAiMemo(conn, text) { + var state = getWebshellDbState(conn); + state.aiMemo = String(text || ''); + if (state.aiMemo.length > 100000) state.aiMemo = state.aiMemo.slice(0, 100000); + saveWebshellDbState(conn, state); +} + function webshellDbGetFieldValue(id) { var el = document.getElementById(id); return el && typeof el.value === 'string' ? el.value.trim() : ''; @@ -1230,6 +1250,17 @@ function renderWebshellAiErrorMessage(targetEl, rawMessage) { } } +function isLikelyWebshellAiErrorMessage(content, msg) { + var text = String(content || '').trim(); + if (!text) return false; + var lower = text.toLowerCase(); + if (/^(执行失败|请求失败|请求异常|error)\s*[::]/i.test(text)) return true; + if (/(status code\s*:\s*4\d{2}|unauthorized|forbidden|apikey|api key|invalid api key)/i.test(lower)) return true; + if (/(noderunerror|tool[-_ ]?error|agent[-_ ]?error|执行失败)/i.test(lower)) return true; + var details = msg && Array.isArray(msg.processDetails) ? msg.processDetails : []; + return details.some(function (d) { return String((d && d.eventType) || '').toLowerCase() === 'error'; }); +} + function formatWebshellAiConvDate(updatedAt) { if (!updatedAt) return ''; var d = typeof updatedAt === 'string' ? new Date(updatedAt) : updatedAt; @@ -1404,7 +1435,9 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) { if (role === 'user') { div.textContent = content; } else { - if (typeof formatMarkdown === 'function') { + if (isLikelyWebshellAiErrorMessage(content, msg)) { + renderWebshellAiErrorMessage(div, content); + } else if (typeof formatMarkdown === 'function') { div.innerHTML = formatMarkdown(content); } else { div.textContent = content; @@ -1455,6 +1488,7 @@ function selectWebshell(id, stateReady) { '' + '' + '' + + '' + '' + '