From 2d8ef3a1b0550c517158f88b7167451def9c2bbe 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, 20 Apr 2026 19:42:11 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 61 +++- web/static/i18n/en-US.json | 80 ++++- web/static/i18n/zh-CN.json | 80 ++++- web/static/js/webshell.js | 587 +++++++++++++++++++++++++++++++------ 4 files changed, 715 insertions(+), 93 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 44f2aa32..e1efd181 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -11353,12 +11353,53 @@ header { .webshell-ai-msg ol { padding-left: 20px; } -.webshell-ai-input-row { +/* WebShell AI 输入区域:选择器 + 输入框同行 */ +.webshell-ai-input-area { flex-shrink: 0; display: flex; - gap: 10px; + flex-direction: row; + align-items: center; + gap: 8px; padding: 8px 14px; border-top: 1px solid var(--border-color); +} +.webshell-ai-selectors-row { + display: flex; + gap: 6px; + align-items: center; + flex-shrink: 0; +} +.webshell-ai-selectors-row .role-selector-btn { + height: 36px; + padding: 4px 10px; + font-size: 0.8125rem; + border-radius: 8px; +} +.webshell-ai-selectors-row .role-selector-icon { + font-size: 0.85rem; +} +.webshell-ai-selectors-row .role-selector-text { + font-size: 0.8125rem; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ws-role-selector-wrapper { + position: relative; + flex-shrink: 0; +} +.ws-agent-mode-wrapper { + flex-shrink: 0; +} +.ws-agent-mode-wrapper .agent-mode-inner { + position: relative; +} +.webshell-ai-input-row { + flex: 1; + min-width: 0; + display: flex; + gap: 10px; align-items: center; } .webshell-ai-input { @@ -11391,7 +11432,8 @@ header { .webshell-ai-input::-webkit-scrollbar-thumb:hover { background: rgba(15, 23, 42, 0.4); } -.webshell-ai-input-row .btn-primary { +.webshell-ai-input-row .btn-primary, +.webshell-ai-input-row .webshell-ai-stop-btn { flex-shrink: 0; height: 36px; min-width: 72px; @@ -11400,6 +11442,18 @@ header { align-items: center; justify-content: center; } +.webshell-ai-stop-btn { + background: #ef4444; + color: #fff; + border: none; + border-radius: 8px; + font-size: 0.9rem; + cursor: pointer; + transition: background 0.2s; +} +.webshell-ai-stop-btn:hover { + background: #dc2626; +} /* WebShell 数据库管理 Tab */ .webshell-pane-db { @@ -13465,6 +13519,7 @@ header { min-width: 0; flex: 1; padding-top: 2px; + text-align: left; } .role-selection-item-name-main { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index d004d74a..05e9dd10 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -845,7 +845,13 @@ "externalMCPManagement": "External MCP Management", "attackChain": "Attack Chain", "knowledgeBase": "Knowledge Base", - "mcp": "MCP" + "mcp": "MCP", + "fofaRecon": "FOFA Recon", + "terminal": "Terminal", + "webshellManagement": "WebShell Management", + "chatUploads": "Chat Uploads", + "robotIntegration": "Robot Integration", + "markdownAgents": "Markdown Agents" }, "summary": { "login": "User login", @@ -945,7 +951,53 @@ "invokeTool": "Invoke tool", "initConnection": "Initialize connection", "successResponse": "Success response", - "errorResponse": "Error response" + "errorResponse": "Error response", + "deleteConversationTurn": "Delete conversation turn", + "getMessageProcessDetails": "Get message process details", + "rerunBatchQueue": "Rerun batch task queue", + "updateBatchQueueMetadata": "Update queue metadata", + "updateBatchQueueSchedule": "Update queue schedule", + "setBatchQueueScheduleEnabled": "Toggle cron auto-schedule", + "getAllGroupMappings": "Get all group mappings", + "fofaSearch": "FOFA search", + "fofaParse": "Parse natural language to FOFA syntax", + "testOpenAI": "Test OpenAI API connection", + "terminalRun": "Run terminal command", + "terminalRunStream": "Run terminal command (stream)", + "terminalWS": "WebSocket terminal", + "listWebshellConnections": "List WebShell connections", + "createWebshellConnection": "Create WebShell connection", + "updateWebshellConnection": "Update WebShell connection", + "deleteWebshellConnection": "Delete WebShell connection", + "getWebshellConnectionState": "Get connection state", + "saveWebshellConnectionState": "Save connection state", + "getWebshellAIHistory": "Get AI chat history", + "listWebshellAIConversations": "List AI conversations", + "webshellExec": "Execute WebShell command", + "webshellFileOp": "WebShell file operation", + "listChatUploads": "List uploads", + "uploadChatFile": "Upload file", + "deleteChatUpload": "Delete upload", + "downloadChatUpload": "Download upload", + "getChatUploadContent": "Get file text content", + "putChatUploadContent": "Write file text content", + "mkdirChatUpload": "Create upload directory", + "renameChatUpload": "Rename upload", + "wecomCallbackVerify": "WeCom callback verification", + "wecomCallbackMessage": "WeCom message callback", + "dingtalkCallback": "DingTalk message callback", + "larkCallback": "Lark message callback", + "testRobot": "Test robot message processing", + "listMarkdownAgents": "List Markdown agents", + "createMarkdownAgent": "Create Markdown agent", + "getMarkdownAgent": "Get Markdown agent detail", + "updateMarkdownAgent": "Update Markdown agent", + "deleteMarkdownAgent": "Delete Markdown agent", + "listSkillPackageFiles": "List skill package files", + "getSkillPackageFile": "Get skill package file content", + "putSkillPackageFile": "Write skill package file", + "batchGetToolNames": "Batch get tool names", + "getKnowledgeStats": "Get knowledge base stats" }, "response": { "getSuccess": "Success", @@ -980,7 +1032,29 @@ "cancelSubmitted": "Cancel request submitted", "noRunningTask": "No running task found", "messageSent": "Message sent, AI reply returned", - "streamResponse": "Stream response (Server-Sent Events)" + "streamResponse": "Stream response (Server-Sent Events)", + "badRequestOrDeleteFailed": "Bad request or delete failed", + "paramError": "Invalid parameters", + "onlyCompletedOrCancelledCanRerun": "Only completed or cancelled queues can be rerun", + "badRequestOrQueueRunning": "Bad request or queue is running", + "setSuccess": "Set successfully", + "searchSuccess": "Search successful", + "parseSuccess": "Parse successful", + "testResult": "Test result", + "executionDone": "Execution completed", + "sseEventStream": "SSE event stream", + "wsEstablished": "WebSocket connection established", + "fileDownload": "File download", + "fileNotFound": "File not found", + "writeSuccess": "Written successfully", + "renameSuccess": "Renamed successfully", + "wecomVerifySuccess": "Verification successful, decrypted echostr returned", + "processSuccess": "Processed successfully", + "agentNotFound": "Agent not found", + "saveSuccess": "Saved successfully", + "operationResult": "Operation result", + "executionResult": "Execution result", + "connectionNotFound": "Connection not found" } }, "chatGroup": { diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index d8b08436..5c2018a3 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -845,7 +845,13 @@ "externalMCPManagement": "外部MCP管理", "attackChain": "攻击链", "knowledgeBase": "知识库", - "mcp": "MCP" + "mcp": "MCP", + "fofaRecon": "FOFA信息收集", + "terminal": "终端", + "webshellManagement": "WebShell管理", + "chatUploads": "对话附件", + "robotIntegration": "机器人集成", + "markdownAgents": "多代理Markdown" }, "summary": { "login": "用户登录", @@ -945,7 +951,53 @@ "invokeTool": "调用工具", "initConnection": "初始化连接", "successResponse": "成功响应", - "errorResponse": "错误响应" + "errorResponse": "错误响应", + "deleteConversationTurn": "删除对话轮次", + "getMessageProcessDetails": "获取消息过程详情", + "rerunBatchQueue": "重跑批量任务队列", + "updateBatchQueueMetadata": "修改队列元数据", + "updateBatchQueueSchedule": "修改队列调度配置", + "setBatchQueueScheduleEnabled": "开关Cron自动调度", + "getAllGroupMappings": "获取所有分组映射", + "fofaSearch": "FOFA搜索", + "fofaParse": "自然语言解析为FOFA语法", + "testOpenAI": "测试OpenAI API连接", + "terminalRun": "执行终端命令", + "terminalRunStream": "流式执行终端命令", + "terminalWS": "WebSocket终端", + "listWebshellConnections": "列出WebShell连接", + "createWebshellConnection": "创建WebShell连接", + "updateWebshellConnection": "更新WebShell连接", + "deleteWebshellConnection": "删除WebShell连接", + "getWebshellConnectionState": "获取连接状态", + "saveWebshellConnectionState": "保存连接状态", + "getWebshellAIHistory": "获取AI对话历史", + "listWebshellAIConversations": "列出AI对话", + "webshellExec": "执行WebShell命令", + "webshellFileOp": "WebShell文件操作", + "listChatUploads": "列出附件", + "uploadChatFile": "上传附件", + "deleteChatUpload": "删除附件", + "downloadChatUpload": "下载附件", + "getChatUploadContent": "获取附件文本内容", + "putChatUploadContent": "写入附件文本内容", + "mkdirChatUpload": "创建附件目录", + "renameChatUpload": "重命名附件", + "wecomCallbackVerify": "企业微信回调验证", + "wecomCallbackMessage": "企业微信消息回调", + "dingtalkCallback": "钉钉消息回调", + "larkCallback": "飞书消息回调", + "testRobot": "测试机器人消息处理", + "listMarkdownAgents": "列出Markdown代理", + "createMarkdownAgent": "创建Markdown代理", + "getMarkdownAgent": "获取Markdown代理详情", + "updateMarkdownAgent": "更新Markdown代理", + "deleteMarkdownAgent": "删除Markdown代理", + "listSkillPackageFiles": "列出技能包文件", + "getSkillPackageFile": "获取技能包文件内容", + "putSkillPackageFile": "写入技能包文件", + "batchGetToolNames": "批量获取工具名称", + "getKnowledgeStats": "获取知识库统计" }, "response": { "getSuccess": "获取成功", @@ -980,7 +1032,29 @@ "cancelSubmitted": "取消请求已提交", "noRunningTask": "未找到正在执行的任务", "messageSent": "消息发送成功,返回AI回复", - "streamResponse": "流式响应(Server-Sent Events)" + "streamResponse": "流式响应(Server-Sent Events)", + "badRequestOrDeleteFailed": "参数错误或删除失败", + "paramError": "参数错误", + "onlyCompletedOrCancelledCanRerun": "仅已完成或已取消的队列可以重跑", + "badRequestOrQueueRunning": "参数错误或队列正在运行中", + "setSuccess": "设置成功", + "searchSuccess": "搜索成功", + "parseSuccess": "解析成功", + "testResult": "测试结果", + "executionDone": "执行完成", + "sseEventStream": "SSE事件流", + "wsEstablished": "WebSocket连接已建立", + "fileDownload": "文件下载", + "fileNotFound": "文件不存在", + "writeSuccess": "写入成功", + "renameSuccess": "重命名成功", + "wecomVerifySuccess": "验证成功,返回解密后的echostr", + "processSuccess": "处理成功", + "agentNotFound": "代理不存在", + "saveSuccess": "保存成功", + "operationResult": "操作结果", + "executionResult": "执行结果", + "connectionNotFound": "连接不存在" } }, "chatGroup": { diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index 846ebdcb..eef8dc13 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -28,6 +28,8 @@ let webshellClearInProgress = false; // AI 助手:按连接 ID 保存对话 ID,便于多轮对话 let webshellAiConvMap = {}; let webshellAiSending = false; +let webshellAiAbortController = null; // AbortController for current AI stream +let webshellAiStreamReader = null; // Current ReadableStreamDefaultReader let webshellDbConfigByConn = {}; let webshellDirTreeByConn = {}; let webshellDirExpandedByConn = {}; @@ -70,6 +72,237 @@ function resolveWebshellAiStreamRequest() { }); } +// ─── WebShell AI 助手:角色 + 对话模式选择器(与主「对话」页对齐) ─── + +let wsRolesCache = null; // 缓存 /api/roles 结果 + +function wsLoadRoles() { + if (typeof apiFetch === 'undefined') return; + apiFetch('/api/roles').then(function (r) { return r.json(); }).then(function (data) { + wsRolesCache = (data && Array.isArray(data.roles)) ? data.roles : []; + wsRenderRoleList(); + wsUpdateRoleSelectorDisplay(); + }).catch(function () { /* ignore */ }); +} + +function wsUpdateRoleSelectorDisplay() { + var iconEl = document.getElementById('ws-role-selector-icon'); + var textEl = document.getElementById('ws-role-selector-text'); + if (!iconEl || !textEl) return; + var cur = (typeof getCurrentRole === 'function') ? getCurrentRole() : (localStorage.getItem('currentRole') || ''); + if (!cur) { + iconEl.textContent = '\ud83d\udd35'; + textEl.textContent = (typeof window.t === 'function' ? window.t('chat.defaultRole') : '') || '默认'; + return; + } + if (wsRolesCache) { + for (var i = 0; i < wsRolesCache.length; i++) { + if (wsRolesCache[i].name === cur) { + iconEl.textContent = wsRolesCache[i].icon || '\ud83d\udd35'; + textEl.textContent = cur; + return; + } + } + } + iconEl.textContent = '\ud83d\udd35'; + textEl.textContent = cur; +} + +function wsRenderRoleList() { + var listEl = document.getElementById('ws-role-selection-list'); + if (!listEl) return; + var cur = (typeof getCurrentRole === 'function') ? getCurrentRole() : (localStorage.getItem('currentRole') || ''); + var html = ''; + // 默认角色 + var defSelected = !cur ? ' selected' : ''; + html += ''; + if (wsRolesCache) { + for (var i = 0; i < wsRolesCache.length; i++) { + var r = wsRolesCache[i]; + if (!r.enabled) continue; + if (r.name === '默认') continue; // 已在上方硬编码默认角色,跳过 API 返回的默认项 + var sel = (r.name === cur) ? ' selected' : ''; + html += ''; + } + } + listEl.innerHTML = html; +} + +function wsSelectRole(name) { + var roleName = name || ''; + // 使用主页的 handleRoleChange 来同步 roles.js 内部状态和 localStorage + if (typeof handleRoleChange === 'function') { + try { handleRoleChange(roleName); } catch (e) { /* */ } + } else { + try { localStorage.setItem('currentRole', roleName); } catch (e) { /* */ } + } + if (typeof window.currentSelectedRole !== 'undefined') window.currentSelectedRole = roleName; + wsUpdateRoleSelectorDisplay(); + wsRenderRoleList(); + wsCloseRolePanel(); +} + +function wsToggleRolePanel() { + var panel = document.getElementById('ws-role-selection-panel'); + if (!panel) return; + var isOpen = panel.style.display === 'flex'; + if (isOpen) { wsCloseRolePanel(); return; } + wsCloseAgentModePanel(); + panel.style.display = 'flex'; +} +function wsCloseRolePanel() { + var panel = document.getElementById('ws-role-selection-panel'); + if (panel) panel.style.display = 'none'; +} + +// ─── 对话模式选择器 ─── + +function wsInitAgentMode() { + if (typeof apiFetch === 'undefined') return; + apiFetch('/api/config').then(function (r) { return r.ok ? r.json() : null; }).then(function (cfg) { + var wrapper = document.getElementById('ws-agent-mode-wrapper'); + if (!wrapper) return; + wrapper.style.display = ''; + // 是否启用多代理 + var multiOn = cfg && cfg.multi_agent && cfg.multi_agent.enabled; + // 隐藏/显示多代理选项 + var opts = wrapper.querySelectorAll('.ws-agent-mode-option'); + opts.forEach(function (el) { + var v = el.getAttribute('data-value'); + if (v === 'deep' || v === 'plan_execute' || v === 'supervisor') { + el.style.display = multiOn ? '' : 'none'; + } + }); + // 标准化当前值 + var stored = localStorage.getItem('cyberstrike-chat-agent-mode'); + var norm; + if (typeof window.csaiChatAgentMode === 'object' && typeof window.csaiChatAgentMode.normalizeStored === 'function') { + norm = window.csaiChatAgentMode.normalizeStored(stored, cfg); + } else { + norm = stored || 'react'; + if (norm === 'single') norm = 'react'; + if (norm === 'multi') norm = 'deep'; + } + wsSyncAgentMode(norm); + }).catch(function () { + var wrapper = document.getElementById('ws-agent-mode-wrapper'); + if (wrapper) wrapper.style.display = ''; + wsSyncAgentMode('react'); + }); +} + +function wsSyncAgentMode(value) { + var hid = document.getElementById('ws-agent-mode-select'); + var label = document.getElementById('ws-agent-mode-text'); + var icon = document.getElementById('ws-agent-mode-icon'); + if (hid) hid.value = value; + if (label) label.textContent = (typeof getAgentModeLabelForValue === 'function') ? getAgentModeLabelForValue(value) : value; + if (icon) icon.textContent = (typeof getAgentModeIconForValue === 'function') ? getAgentModeIconForValue(value) : '\ud83e\udd16'; + var wrapper = document.getElementById('ws-agent-mode-wrapper'); + if (wrapper) { + wrapper.querySelectorAll('.ws-agent-mode-option').forEach(function (el) { + el.classList.toggle('selected', el.getAttribute('data-value') === value); + }); + } +} + +function wsSelectAgentMode(mode) { + try { localStorage.setItem('cyberstrike-chat-agent-mode', mode); } catch (e) { /* */ } + wsSyncAgentMode(mode); + wsCloseAgentModePanel(); + // 同步主页模式选择器 + if (typeof syncAgentModeFromValue === 'function') try { syncAgentModeFromValue(mode); } catch (e) { /* */ } +} + +function wsToggleAgentModePanel() { + var panel = document.getElementById('ws-agent-mode-panel'); + if (!panel) return; + var isOpen = panel.style.display === 'flex'; + if (isOpen) { wsCloseAgentModePanel(); return; } + wsCloseRolePanel(); + panel.style.display = 'flex'; +} +function wsCloseAgentModePanel() { + var panel = document.getElementById('ws-agent-mode-panel'); + if (panel) panel.style.display = 'none'; +} + +/** 当 WebShell AI Tab 可见时刷新选择器显示(同步主页可能的更改) */ +function wsRefreshSelectors() { + wsUpdateRoleSelectorDisplay(); + wsRenderRoleList(); + var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'react'; + wsSyncAgentMode(stored); +} + +// 点击面板外部关闭 +document.addEventListener('click', function (e) { + var rolePanel = document.getElementById('ws-role-selection-panel'); + var roleBtn = document.getElementById('ws-role-selector-btn'); + if (rolePanel && rolePanel.style.display !== 'none' && roleBtn && !rolePanel.contains(e.target) && !roleBtn.contains(e.target)) { + wsCloseRolePanel(); + } + var modePanel = document.getElementById('ws-agent-mode-panel'); + var modeBtn = document.getElementById('ws-agent-mode-btn'); + if (modePanel && modePanel.style.display !== 'none' && modeBtn && !modePanel.contains(e.target) && !modeBtn.contains(e.target)) { + wsCloseAgentModePanel(); + } +}); + +// ─── end WebShell AI 选择器 ─── + +/** 停止当前 WebShell AI 流式请求 */ +function wsStopAiStream(conn) { + // 1. Abort the fetch + if (webshellAiAbortController) { + try { webshellAiAbortController.abort(); } catch (e) { /* */ } + webshellAiAbortController = null; + } + // 2. Cancel the reader + if (webshellAiStreamReader) { + try { webshellAiStreamReader.cancel(); } catch (e) { /* */ } + webshellAiStreamReader = null; + } + // 3. Call backend cancel API if we have a conversation + var convId = conn && conn.id ? (webshellAiConvMap[conn.id] || '') : ''; + if (convId && typeof apiFetch === 'function') { + apiFetch('/api/agent-loop/cancel', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ conversationId: convId }) + }).catch(function () { /* ignore */ }); + } + // 4. Reset UI state + wsSetAiSendingState(false); +} + +/** 切换发送/停止按钮状态 */ +function wsSetAiSendingState(sending) { + webshellAiSending = sending; + var sendBtn = document.getElementById('webshell-ai-send'); + var stopBtn = document.getElementById('webshell-ai-stop'); + if (sendBtn) { + sendBtn.disabled = sending; + sendBtn.style.display = sending ? 'none' : ''; + } + if (stopBtn) { + stopBtn.style.display = sending ? '' : 'none'; + } +} + // 从服务端(SQLite)拉取连接列表 function getWebshellConnections() { if (typeof apiFetch === 'undefined') { @@ -1441,7 +1674,7 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) { el.classList.toggle('active', el.dataset.convId === convId); }); if (typeof apiFetch !== 'function') return; - apiFetch('/api/conversations/' + encodeURIComponent(convId), { method: 'GET' }) + apiFetch('/api/conversations/' + encodeURIComponent(convId) + '?include_process_details=1', { method: 'GET' }) .then(function (r) { return r.json(); }) .then(function (data) { messagesContainer.innerHTML = ''; @@ -1572,9 +1805,45 @@ function selectWebshell(id, stateReady) { '' + '
' + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '' + + '
' + '
' + '' + '' + + '' + + '
' + '
' + '
' + '' + @@ -1635,6 +1904,9 @@ function selectWebshell(id, stateReady) { if (tab === 'terminal' && webshellTerminalInstance && webshellTerminalFitAddon) { try { webshellTerminalFitAddon.fit(); } catch (e) {} } + if (tab === 'ai') { + try { wsRefreshSelectors(); } catch (e) {} + } }); }); @@ -1710,6 +1982,10 @@ function selectWebshell(id, stateReady) { var aiMessages = document.getElementById('webshell-ai-messages'); var aiNewConvBtn = document.getElementById('webshell-ai-new-conv'); var aiConvListEl = document.getElementById('webshell-ai-conv-list'); + + // 初始化角色 + 模式选择器 + wsLoadRoles(); + wsInitAgentMode(); var aiMemoInput = document.getElementById('webshell-ai-memo-input'); var aiMemoStatus = document.getElementById('webshell-ai-memo-status'); var aiMemoClearBtn = document.getElementById('webshell-ai-memo-clear'); @@ -1770,7 +2046,11 @@ function selectWebshell(id, stateReady) { }); } if (aiSendBtn && aiInput && aiMessages) { + var aiStopBtn = document.getElementById('webshell-ai-stop'); aiSendBtn.addEventListener('click', function () { runWebshellAiSend(conn, aiInput, aiSendBtn, aiMessages); }); + if (aiStopBtn) { + aiStopBtn.addEventListener('click', function () { wsStopAiStream(conn); }); + } aiInput.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -2347,8 +2627,8 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { return; } - webshellAiSending = true; - if (sendBtn) sendBtn.disabled = true; + webshellAiAbortController = new AbortController(); + wsSetAiSendingState(true); var userDiv = document.createElement('div'); userDiv.className = 'webshell-ai-msg user'; @@ -2427,14 +2707,18 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { } var einoSubReplyStreams = new Map(); + var wsThinkingStreams = new Map(); // streamId → { el, buf } + var wsToolResultStreams = new Map(); // toolCallId → { el, buf } if (inputEl) inputEl.value = ''; var convId = webshellAiConvMap[conn.id] || ''; + var wsRole = (typeof getCurrentRole === 'function') ? getCurrentRole() : (localStorage.getItem('currentRole') || ''); var body = { message: message, webshellConnectionId: conn.id, - conversationId: convId + conversationId: convId, + role: wsRole }; // 流式输出:支持 progress 实时更新、response 打字机效果;若后端发送多段 response 则追加 @@ -2448,7 +2732,8 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { return apiFetch(info.path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) + body: JSON.stringify(body), + signal: webshellAiAbortController ? webshellAiAbortController.signal : undefined }); }).then(function (response) { if (!response.ok) { @@ -2458,6 +2743,7 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { return response.body.getReader(); }).then(function (reader) { if (!reader) return; + webshellAiStreamReader = reader; var decoder = new TextDecoder(); var buffer = ''; return reader.read().then(function processChunk(result) { @@ -2470,9 +2756,12 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { if (line.indexOf('data: ') !== 0) continue; try { var eventData = JSON.parse(line.slice(6)); - if (eventData.type === 'conversation' && eventData.data && eventData.data.conversationId) { - // 先把 conversationId 拿出来,避免后续异步回调里 eventData 被后续事件覆盖导致 undefined 报错 - var convId = eventData.data.conversationId; + var _et = eventData.type; + var _ed = eventData.data || {}; + var _em = eventData.message || ''; + + if (_et === 'conversation' && _ed.conversationId) { + var convId = _ed.conversationId; webshellAiConvMap[conn.id] = convId; var listEl = document.getElementById('webshell-ai-conv-list'); if (listEl) fetchAndRenderWebshellAiConvList(conn, listEl).then(function () { @@ -2480,100 +2769,219 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { el.classList.toggle('active', el.dataset.convId === convId); }); }); - } else if (eventData.type === 'response_start') { + + // ─── Response streaming ─── + } else if (_et === 'response_start') { streamingTarget = ''; webshellStreamingTypingId += 1; streamingTypingId = webshellStreamingTypingId; assistantDiv.textContent = '…'; messagesContainer.scrollTop = messagesContainer.scrollHeight; - } else if (eventData.type === 'response_delta') { - var deltaText = (eventData.message != null && eventData.message !== '') ? String(eventData.message) : ''; + } else if (_et === 'response_delta') { + var deltaText = (_em != null && _em !== '') ? String(_em) : ''; if (deltaText) { streamingTarget += deltaText; webshellStreamingTypingId += 1; streamingTypingId = webshellStreamingTypingId; runWebshellAiStreamingTyping(assistantDiv, streamingTarget, streamingTypingId, messagesContainer); } - } else if (eventData.type === 'response') { - var text = (eventData.message != null && eventData.message !== '') ? eventData.message : (eventData.data && typeof eventData.data === 'string' ? eventData.data : ''); + } else if (_et === 'response') { + var text = (_em != null && _em !== '') ? _em : (typeof _ed === 'string' ? _ed : ''); if (text) { - // response 为最终完整内容:避免与增量重复拼接 streamingTarget = String(text); webshellStreamingTypingId += 1; streamingTypingId = webshellStreamingTypingId; runWebshellAiStreamingTyping(assistantDiv, streamingTarget, streamingTypingId, messagesContainer); } - } else if (eventData.type === 'error' && eventData.message) { + + // ─── Terminal events ─── + } else if (_et === 'error' && _em) { streamingTypingId += 1; - var errLabel = (typeof window.t === 'function') ? window.t('chat.error') : '错误'; - appendTimelineItem('error', '❌ ' + errLabel, eventData.message, eventData.data); - renderWebshellAiErrorMessage(assistantDiv, errLabel + ': ' + eventData.message); - } else if (eventData.type === 'progress' && eventData.message) { + var errLabel = wsTOr('chat.error', '错误'); + appendTimelineItem('error', '❌ ' + errLabel, _em, _ed); + renderWebshellAiErrorMessage(assistantDiv, errLabel + ': ' + _em); + } else if (_et === 'cancelled') { + streamingTypingId += 1; + var cancelLabel = wsTOr('chat.taskCancelled', '任务已取消'); + appendTimelineItem('cancelled', '⛔ ' + cancelLabel, _em, _ed); + if (!streamingTarget && !assistantDiv.dataset.hasContent) { + assistantDiv.textContent = cancelLabel; + } + } else if (_et === 'done') { + // 清理流式状态 + wsThinkingStreams.clear(); + wsToolResultStreams.clear(); + einoSubReplyStreams.clear(); + + // ─── Iteration / Progress ─── + } else if (_et === 'progress' && _em) { var progressMsg = (typeof window.translateProgressMessage === 'function') - ? window.translateProgressMessage(eventData.message) - : eventData.message; - appendTimelineItem('progress', '🔍 ' + progressMsg, '', eventData.data); + ? window.translateProgressMessage(_em) : _em; + appendTimelineItem('progress', '🔍 ' + progressMsg, '', _ed); if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'iteration') { - var iterN = (eventData.data && eventData.data.iteration) || 0; - var iterTitle = (typeof window.t === 'function') - ? window.t('chat.iterationRound', { n: iterN || 1 }) - : (iterN ? ('第 ' + iterN + ' 轮迭代') : (eventData.message || '迭代')); - var iterMessage = eventData.message || ''; + } else if (_et === 'iteration') { + var iterN = _ed.iteration || 0; + var iterTitle = wsTOr('chat.iterationRound', '') || (iterN ? ('第 ' + iterN + ' 轮迭代') : (_em || '迭代')); + if (typeof window.t === 'function' && iterN) { + iterTitle = window.t('chat.iterationRound', { n: iterN }); + } + var iterMessage = _em || ''; if (iterMessage && typeof window.translateProgressMessage === 'function') { iterMessage = window.translateProgressMessage(iterMessage); } - appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, eventData.data); + appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, _ed); if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'thinking' && eventData.message) { - var thinkLabel = (typeof window.t === 'function') ? window.t('chat.aiThinking') : 'AI 思考'; - var thinkD = eventData.data || {}; - appendTimelineItem('thinking', webshellAgentPx(thinkD) + '🤔 ' + thinkLabel, eventData.message, thinkD); + + // ─── Thinking (non-stream + stream) ─── + } else if (_et === 'thinking_stream_start' && _ed.streamId) { + var thinkSLabel = wsTOr('chat.aiThinking', 'AI 思考'); + var thinkSItem = document.createElement('div'); + thinkSItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-thinking'; + thinkSItem.innerHTML = '' + escapeHtml(webshellAgentPx(_ed) + '🤔 ' + thinkSLabel) + ''; + var thinkSPre = document.createElement('div'); + thinkSPre.className = 'webshell-ai-timeline-msg webshell-thinking-stream-body'; + thinkSItem.appendChild(thinkSPre); + timelineContainer.appendChild(thinkSItem); + timelineContainer.classList.add('has-items'); + wsThinkingStreams.set(_ed.streamId, { el: thinkSItem, body: thinkSPre, buf: '' }); if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'tool_calls_detected' && eventData.data) { - var count = eventData.data.count || 0; - var detectedLabel = (typeof window.t === 'function') - ? window.t('chat.toolCallsDetected', { count: count }) - : ('检测到 ' + count + ' 个工具调用'); - appendTimelineItem('tool_calls_detected', webshellAgentPx(eventData.data) + '🔧 ' + detectedLabel, eventData.message || '', eventData.data); + } else if (_et === 'thinking_stream_delta' && _ed.streamId) { + var tsD = wsThinkingStreams.get(_ed.streamId); + if (tsD) { + tsD.buf += (_em || ''); + if (typeof formatMarkdown === 'function') { + tsD.body.innerHTML = formatMarkdown(tsD.buf); + } else { + tsD.body.textContent = tsD.buf; + } + } if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'tool_call' && eventData.data) { - var d = eventData.data; - var tn = d.toolName || '未知工具'; - var idx = d.index || 0; - var total = d.total || 0; - var callTitle = (typeof window.t === 'function') - ? window.t('chat.callTool', { name: tn, index: idx, total: total }) - : ('调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : '')); - var title = webshellAgentPx(d) + '🔧 ' + callTitle; - appendTimelineItem('tool_call', title, eventData.message || '', eventData.data); + } else if (_et === 'thinking_stream_end' && _ed.streamId) { + var tsE = wsThinkingStreams.get(_ed.streamId); + if (tsE) { + var fullThink = (_em != null && _em !== '') ? String(_em) : tsE.buf; + if (typeof formatMarkdown === 'function') { + tsE.body.innerHTML = formatMarkdown(fullThink); + } else { + tsE.body.textContent = fullThink; + } + wsThinkingStreams.delete(_ed.streamId); + } + } else if (_et === 'thinking' && _em) { + // 如果有 streamId 且已存在流式条目,跳过避免重复 + if (_ed.streamId && wsThinkingStreams.has(_ed.streamId)) { + // 已由 thinking_stream_* 处理 + } else { + var thinkLabel = wsTOr('chat.aiThinking', 'AI 思考'); + appendTimelineItem('thinking', webshellAgentPx(_ed) + '🤔 ' + thinkLabel, _em, _ed); + } if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'tool_result' && eventData.data) { - var dr = eventData.data; - var success = dr.success !== false; - var tname = dr.toolName || '工具'; - var titleText = (typeof window.t === 'function') - ? (success ? window.t('chat.toolExecComplete', { name: tname }) : window.t('chat.toolExecFailed', { name: tname })) - : (tname + (success ? ' 执行完成' : ' 执行失败')); - var title = webshellAgentPx(dr) + (success ? '✅ ' : '❌ ') + titleText; - var sub = eventData.message || (dr.result ? String(dr.result).slice(0, 300) : ''); - appendTimelineItem('tool_result', title, sub, eventData.data); + + // ─── Warning ─── + } else if (_et === 'warning') { + appendTimelineItem('warning', '⚠️ ' + (_em || ''), '', _ed); + + // ─── Eino recovery ─── + } else if (_et === 'eino_recovery') { + var runIdx = _ed.runIndex != null ? _ed.runIndex : (_ed.einoRetry != null ? _ed.einoRetry + 1 : 1); + var maxRuns = _ed.maxRuns != null ? _ed.maxRuns : 3; + var recTitle = wsTOr('chat.einoRecoveryTitle', '') || + ('🔄 工具参数无效 · 第 ' + runIdx + '/' + maxRuns + ' 轮(已追加提示)'); + if (typeof window.t === 'function') { + try { recTitle = window.t('chat.einoRecoveryTitle', { n: runIdx, max: maxRuns }); } catch (e) { /* */ } + } + appendTimelineItem('eino_recovery', recTitle, _em, _ed); + + // ─── Tool calls ─── + } else if (_et === 'tool_calls_detected' && _ed) { + var count = _ed.count || 0; + var detectedLabel = wsTOr('chat.toolCallsDetected', '') || ('检测到 ' + count + ' 个工具调用'); + if (typeof window.t === 'function') { + try { detectedLabel = window.t('chat.toolCallsDetected', { count: count }); } catch (e) { /* */ } + } + appendTimelineItem('tool_calls_detected', webshellAgentPx(_ed) + '🔧 ' + detectedLabel, _em || '', _ed); if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'eino_agent_reply_stream_start' && eventData.data && eventData.data.streamId) { - var rdS = eventData.data; - var repTS = (typeof window.t === 'function') ? window.t('chat.einoAgentReplyTitle') : '子代理回复'; - var runTS = (typeof window.t === 'function') ? window.t('timeline.running') : '执行中...'; + } else if (_et === 'tool_call' && _ed) { + var tn = _ed.toolName || '未知工具'; + var idx = _ed.index || 0; + var total = _ed.total || 0; + var callTitle = wsTOr('chat.callTool', '') || ('调用工具: ' + tn + (total ? ' (' + idx + '/' + total + ')' : '')); + if (typeof window.t === 'function') { + try { callTitle = window.t('chat.callTool', { name: tn, index: idx, total: total }); } catch (e) { /* */ } + } + appendTimelineItem('tool_call', webshellAgentPx(_ed) + '🔧 ' + callTitle, _em || '', _ed); + if (!streamingTarget) assistantDiv.textContent = '…'; + + // ─── Tool result delta (streaming output) ─── + } else if (_et === 'tool_result_delta' && _ed.toolCallId) { + var trdKey = _ed.toolCallId; + var trdDelta = _em || ''; + if (trdDelta) { + var trdState = wsToolResultStreams.get(trdKey); + if (!trdState) { + var trdName = _ed.toolName || '工具'; + var runLabel = wsTOr('timeline.running', '执行中...'); + var trdItem = document.createElement('div'); + trdItem.className = 'webshell-ai-timeline-item webshell-ai-timeline-tool_result'; + trdItem.innerHTML = '' + + escapeHtml(webshellAgentPx(_ed) + '⏳ ' + runLabel + ' ' + trdName) + + '
' + + '
'; + timelineContainer.appendChild(trdItem); + timelineContainer.classList.add('has-items'); + trdState = { el: trdItem, buf: '' }; + wsToolResultStreams.set(trdKey, trdState); + } + trdState.buf += trdDelta; + var trdPre = trdState.el.querySelector('pre.tool-result'); + if (trdPre) trdPre.textContent = trdState.buf; + } + if (!streamingTarget) assistantDiv.textContent = '…'; + + // ─── Tool result (final) ─── + } else if (_et === 'tool_result' && _ed) { + var success = _ed.success !== false; + var tname = _ed.toolName || '工具'; + var titleText = wsTOr(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', '') || + (tname + (success ? ' 执行完成' : ' 执行失败')); + if (typeof window.t === 'function') { + try { titleText = window.t(success ? 'chat.toolExecComplete' : 'chat.toolExecFailed', { name: tname }); } catch (e) { /* */ } + } + // 如果有流式占位条目,更新标题 + var trdExist = _ed.toolCallId ? wsToolResultStreams.get(_ed.toolCallId) : null; + if (trdExist) { + var trdTitleEl = trdExist.el.querySelector('.webshell-ai-timeline-title'); + if (trdTitleEl) trdTitleEl.textContent = webshellAgentPx(_ed) + (success ? '✅ ' : '❌ ') + titleText; + // 更新结果内容 + var resultText = _ed.result ? String(_ed.result) : (_em || ''); + var trdPreEl = trdExist.el.querySelector('pre.tool-result'); + if (trdPreEl && resultText) trdPreEl.textContent = resultText; + // 更新 section class + var trdSection = trdExist.el.querySelector('.tool-result-section'); + if (trdSection) { trdSection.className = 'tool-result-section ' + (success ? 'success' : 'error'); } + wsToolResultStreams.delete(_ed.toolCallId); + } else { + var title = webshellAgentPx(_ed) + (success ? '✅ ' : '❌ ') + titleText; + var sub = _em || (_ed.result ? String(_ed.result).slice(0, 300) : ''); + appendTimelineItem('tool_result', title, sub, _ed); + } + if (!streamingTarget) assistantDiv.textContent = '…'; + + // ─── Eino sub-agent reply streaming ─── + } else if (_et === 'eino_agent_reply_stream_start' && _ed.streamId) { + var repTS = wsTOr('chat.einoAgentReplyTitle', '子代理回复'); + var runTS = wsTOr('timeline.running', '执行中...'); var itemS = document.createElement('div'); itemS.className = 'webshell-ai-timeline-item webshell-ai-timeline-eino_agent_reply'; - itemS.innerHTML = '' + escapeHtml(webshellAgentPx(rdS) + '💬 ' + repTS + ' · ' + runTS) + ''; + itemS.innerHTML = '' + escapeHtml(webshellAgentPx(_ed) + '💬 ' + repTS + ' · ' + runTS) + ''; timelineContainer.appendChild(itemS); timelineContainer.classList.add('has-items'); - einoSubReplyStreams.set(rdS.streamId, { el: itemS, buf: '' }); + einoSubReplyStreams.set(_ed.streamId, { el: itemS, buf: '' }); if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'eino_agent_reply_stream_delta' && eventData.data && eventData.data.streamId) { - var stD = einoSubReplyStreams.get(eventData.data.streamId); + } else if (_et === 'eino_agent_reply_stream_delta' && _ed.streamId) { + var stD = einoSubReplyStreams.get(_ed.streamId); if (stD) { - stD.buf += (eventData.message || ''); + stD.buf += (_em || ''); var preD = stD.el.querySelector('.webshell-eino-reply-stream-body'); if (!preD) { preD = document.createElement('pre'); @@ -2581,17 +2989,20 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { preD.style.whiteSpace = 'pre-wrap'; stD.el.appendChild(preD); } - preD.textContent = stD.buf; + if (typeof formatMarkdown === 'function') { + preD.innerHTML = formatMarkdown(stD.buf); + } else { + preD.textContent = stD.buf; + } } if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'eino_agent_reply_stream_end' && eventData.data && eventData.data.streamId) { - var stE = einoSubReplyStreams.get(eventData.data.streamId); + } else if (_et === 'eino_agent_reply_stream_end' && _ed.streamId) { + var stE = einoSubReplyStreams.get(_ed.streamId); if (stE) { - var fullE = (eventData.message != null && eventData.message !== '') ? String(eventData.message) : stE.buf; - stE.buf = fullE; - var repTE = (typeof window.t === 'function') ? window.t('chat.einoAgentReplyTitle') : '子代理回复'; + var fullE = (_em != null && _em !== '') ? String(_em) : stE.buf; + var repTE = wsTOr('chat.einoAgentReplyTitle', '子代理回复'); var titE = stE.el.querySelector('.webshell-ai-timeline-title'); - if (titE) titE.textContent = webshellAgentPx(eventData.data) + '💬 ' + repTE; + if (titE) titE.textContent = webshellAgentPx(_ed) + '💬 ' + repTE; var preE = stE.el.querySelector('.webshell-eino-reply-stream-body'); if (!preE) { preE = document.createElement('pre'); @@ -2599,14 +3010,17 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { preE.style.whiteSpace = 'pre-wrap'; stE.el.appendChild(preE); } - preE.textContent = fullE; - einoSubReplyStreams.delete(eventData.data.streamId); + if (typeof formatMarkdown === 'function') { + preE.innerHTML = formatMarkdown(fullE); + } else { + preE.textContent = fullE; + } + einoSubReplyStreams.delete(_ed.streamId); } if (!streamingTarget) assistantDiv.textContent = '…'; - } else if (eventData.type === 'eino_agent_reply' && eventData.message) { - var rd = eventData.data || {}; - var replyT = (typeof window.t === 'function') ? window.t('chat.einoAgentReplyTitle') : '子代理回复'; - appendTimelineItem('eino_agent_reply', webshellAgentPx(rd) + '💬 ' + replyT, eventData.message, rd); + } else if (_et === 'eino_agent_reply' && _em) { + var replyT = wsTOr('chat.einoAgentReplyTitle', '子代理回复'); + appendTimelineItem('eino_agent_reply', webshellAgentPx(_ed) + '💬 ' + replyT, _em, _ed); if (!streamingTarget) assistantDiv.textContent = '…'; } } catch (e) { /* ignore parse error */ } @@ -2615,10 +3029,15 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { return reader.read().then(processChunk); }); }).catch(function (err) { - renderWebshellAiErrorMessage(assistantDiv, '请求异常: ' + (err && err.message ? err.message : String(err))); + var msg = err && err.message ? err.message : String(err); + var isAbort = /abort/i.test(msg); + if (!isAbort) { + renderWebshellAiErrorMessage(assistantDiv, '请求异常: ' + msg); + } }).then(function () { - webshellAiSending = false; - if (sendBtn) sendBtn.disabled = false; + webshellAiAbortController = null; + webshellAiStreamReader = null; + wsSetAiSendingState(false); if (assistantDiv.textContent === '…' && !streamingTarget) { // 没有任何 response 内容,保持纯文本提示 assistantDiv.textContent = '无回复内容';