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 = '无回复内容';