Add files via upload

This commit is contained in:
公明
2026-04-20 19:42:11 +08:00
committed by GitHub
parent 5e227a34cf
commit 2d8ef3a1b0
4 changed files with 715 additions and 93 deletions
+58 -3
View File
@@ -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 {
+77 -3
View File
@@ -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": {
+77 -3
View File
@@ -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": {
+503 -84
View File
@@ -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 += '<button type="button" class="role-selection-item-main' + defSelected + '" onclick="wsSelectRole(\'\')">' +
'<div class="role-selection-item-icon-main">\ud83d\udd35</div>' +
'<div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' +
(wsTOr('chat.defaultRole', '默认')) +
'</div><div class="role-selection-item-description-main">' +
(wsTOr('roles.defaultRoleDescription', '默认角色,不额外携带用户提示词,使用所有工具')) +
'</div></div>' +
(defSelected ? '<div class="role-selection-checkmark-main">\u2713</div>' : '') +
'</button>';
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 += '<button type="button" class="role-selection-item-main' + sel + '" onclick="wsSelectRole(\'' + r.name.replace(/'/g, "\\'") + '\')">' +
'<div class="role-selection-item-icon-main">' + (r.icon || '\ud83d\udd35') + '</div>' +
'<div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + r.name + '</div>' +
'<div class="role-selection-item-description-main">' + (r.description || '').substring(0, 60) + '</div></div>' +
(sel ? '<div class="role-selection-checkmark-main">\u2713</div>' : '') +
'</button>';
}
}
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) {
'</div>' +
'<div class="webshell-ai-main">' +
'<div id="webshell-ai-messages" class="webshell-ai-messages"></div>' +
'<div class="webshell-ai-input-area">' +
'<div class="webshell-ai-selectors-row">' +
'<div class="ws-role-selector-wrapper">' +
'<button type="button" class="role-selector-btn ws-role-selector-btn" id="ws-role-selector-btn" onclick="wsToggleRolePanel()">' +
'<span id="ws-role-selector-icon" class="role-selector-icon">\ud83d\udd35</span>' +
'<span id="ws-role-selector-text" class="role-selector-text">' + (wsT('chat.defaultRole') || '默认') + '</span>' +
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
'</button>' +
'<div id="ws-role-selection-panel" class="role-selection-panel" style="display:none;">' +
'<div class="role-selection-panel-header"><h3 class="role-selection-panel-title">' + (wsT('chatGroup.rolePanelTitle') || '选择角色') + '</h3>' +
'<button type="button" class="role-selection-panel-close" onclick="wsCloseRolePanel()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
'</div><div id="ws-role-selection-list" class="role-selection-list-main"></div></div>' +
'</div>' +
'<div class="ws-agent-mode-wrapper" id="ws-agent-mode-wrapper" style="display:none;">' +
'<div class="agent-mode-inner">' +
'<button type="button" class="role-selector-btn agent-mode-btn" id="ws-agent-mode-btn" onclick="wsToggleAgentModePanel()">' +
'<span id="ws-agent-mode-icon" class="role-selector-icon">\ud83e\udd16</span>' +
'<span id="ws-agent-mode-text" class="role-selector-text">' + (wsT('chat.agentModeReactNative') || '原生 ReAct') + '</span>' +
'<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
'</button>' +
'<div id="ws-agent-mode-panel" class="agent-mode-panel" style="display:none;" role="listbox">' +
'<div class="role-selection-panel-header agent-mode-panel-header"><h3 class="role-selection-panel-title">' + (wsT('chat.agentModePanelTitle') || '对话模式') + '</h3>' +
'<button type="button" class="role-selection-panel-close" onclick="wsCloseAgentModePanel()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
'</div>' +
'<div class="agent-mode-options">' +
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="react" role="option" onclick="wsSelectAgentMode(\'react\')"><div class="role-selection-item-icon-main">\ud83e\udd16</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeReactNative') || '原生 ReAct 模式') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeReactNativeHint') || '经典单代理 ReAct 与 MCP 工具') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="react">\u2713</div></button>' +
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="eino_single" role="option" onclick="wsSelectAgentMode(\'eino_single\')"><div class="role-selection-item-icon-main">\u26a1</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeEinoSingle') || 'Eino 单代理(ADK') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeEinoSingleHint') || 'Eino ChatModelAgent + Runner') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="eino_single">\u2713</div></button>' +
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="deep" role="option" onclick="wsSelectAgentMode(\'deep\')"><div class="role-selection-item-icon-main">\ud83e\udde9</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeDeep') || 'DeepDeepAgent') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeDeepHint') || 'Eino DeepAgenttask 调度子代理') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="deep">\u2713</div></button>' +
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="plan_execute" role="option" onclick="wsSelectAgentMode(\'plan_execute\')"><div class="role-selection-item-icon-main">\ud83d\udccb</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModePlanExecuteLabel') || 'Plan-Execute') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModePlanExecuteHint') || '规划 → 执行 → 重规划') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="plan_execute">\u2713</div></button>' +
'<button type="button" class="role-selection-item-main agent-mode-option ws-agent-mode-option" data-value="supervisor" role="option" onclick="wsSelectAgentMode(\'supervisor\')"><div class="role-selection-item-icon-main">\ud83c\udfaf</div><div class="role-selection-item-content-main"><div class="role-selection-item-name-main">' + (wsT('chat.agentModeSupervisorLabel') || 'Supervisor') + '</div><div class="role-selection-item-description-main">' + (wsT('chat.agentModeSupervisorHint') || '监督者协调,transfer 委派子代理') + '</div></div><div class="role-selection-checkmark-main agent-mode-check" data-agent-mode-check="supervisor">\u2713</div></button>' +
'</div></div></div>' +
'<input type="hidden" id="ws-agent-mode-select" value="react" autocomplete="off" />' +
'</div>' +
'</div>' +
'<div class="webshell-ai-input-row">' +
'<textarea id="webshell-ai-input" class="webshell-ai-input form-control" rows="2" placeholder="' + (wsT('webshell.aiPlaceholder') || '例如:列出当前目录下的文件') + '"></textarea>' +
'<button type="button" class="btn-primary" id="webshell-ai-send">' + (wsT('webshell.aiSend') || '发送') + '</button>' +
'<button type="button" class="btn-danger webshell-ai-stop-btn" id="webshell-ai-stop" style="display:none;">' + wsTOr('webshell.aiStop', '停止') + '</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
@@ -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 = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + '🤔 ' + thinkSLabel) + '</span>';
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 = '<span class="webshell-ai-timeline-title">' +
escapeHtml(webshellAgentPx(_ed) + '⏳ ' + runLabel + ' ' + trdName) +
'</span><div class="webshell-ai-timeline-msg"><div class="tool-result-section success">' +
'<pre class="tool-result"></pre></div></div>';
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 = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(rdS) + '💬 ' + repTS + ' · ' + runTS) + '</span>';
itemS.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(webshellAgentPx(_ed) + '💬 ' + repTS + ' · ' + runTS) + '</span>';
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 = '无回复内容';