diff --git a/internal/handler/agent.go b/internal/handler/agent.go index f41b9803..e1582a4f 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -330,9 +330,19 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) { if remark == "" { remark = conn.URL } - finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写字件等操作时再调用上述工具。\n\n用户请求:%s", + finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base、list_skills、read_skill。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写文件、记录漏洞或检索知识库/查看 Skills 等操作时再调用上述工具。\n\n用户请求:%s", conn.ID, remark, conn.ID, req.Message) - roleTools = []string{builtin.ToolWebshellExec, builtin.ToolWebshellFileList, builtin.ToolWebshellFileRead, builtin.ToolWebshellFileWrite} + roleTools = []string{ + builtin.ToolWebshellExec, + builtin.ToolWebshellFileList, + builtin.ToolWebshellFileRead, + builtin.ToolWebshellFileWrite, + builtin.ToolRecordVulnerability, + builtin.ToolListKnowledgeRiskTypes, + builtin.ToolSearchKnowledgeBase, + builtin.ToolListSkills, + builtin.ToolReadSkill, + } roleSkills = nil } else if req.Role != "" && req.Role != "默认" { if h.config.Roles != nil { @@ -805,9 +815,19 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) { if remark == "" { remark = conn.URL } - finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写字件等操作时再调用上述工具。\n\n用户请求:%s", + finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base、list_skills、read_skill。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写文件、记录漏洞或检索知识库/查看 Skills 等操作时再调用上述工具。\n\n用户请求:%s", conn.ID, remark, conn.ID, req.Message) - roleTools = []string{builtin.ToolWebshellExec, builtin.ToolWebshellFileList, builtin.ToolWebshellFileRead, builtin.ToolWebshellFileWrite} + roleTools = []string{ + builtin.ToolWebshellExec, + builtin.ToolWebshellFileList, + builtin.ToolWebshellFileRead, + builtin.ToolWebshellFileWrite, + builtin.ToolRecordVulnerability, + builtin.ToolListKnowledgeRiskTypes, + builtin.ToolSearchKnowledgeBase, + builtin.ToolListSkills, + builtin.ToolReadSkill, + } } else if req.Role != "" && req.Role != "默认" { if h.config.Roles != nil { if role, exists := h.config.Roles[req.Role]; exists && role.Enabled { diff --git a/web/static/css/style.css b/web/static/css/style.css index f71c7e0b..a92a1acf 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -9285,6 +9285,69 @@ header { background: var(--bg-secondary); border: 1px solid var(--border-color); } +/* AI 助手 markdown 渲染优化:避免行间距离过大和内容横向溢出 */ +.webshell-ai-msg.assistant { + /* markdown 里已经有块级元素,不需要再整体 pre-wrap,否则容易在块之间产生“空行”感 */ + white-space: normal; +} +.webshell-ai-msg.assistant p, +.webshell-ai-msg.assistant ul, +.webshell-ai-msg.assistant ol, +.webshell-ai-msg.assistant pre, +.webshell-ai-msg.assistant blockquote { + margin-top: 0; + margin-bottom: 4px; +} +.webshell-ai-msg.assistant p:last-child, +.webshell-ai-msg.assistant ul:last-child, +.webshell-ai-msg.assistant ol:last-child, +.webshell-ai-msg.assistant pre:last-child, +.webshell-ai-msg.assistant blockquote:last-child { + margin-bottom: 0; +} +.webshell-ai-msg pre { + font-family: var(--font-mono, Menlo, Monaco, Consolas, "Courier New", monospace); + font-size: 0.82rem; + background: #020617; + color: #e5e7eb; + border-radius: 6px; + padding: 8px 10px; + overflow-x: auto; + max-width: 100%; + white-space: pre; + box-sizing: border-box; +} +.webshell-ai-msg pre code { + padding: 0; + background: transparent; +} +.webshell-ai-msg code { + font-family: var(--font-mono, Menlo, Monaco, Consolas, "Courier New", monospace); + font-size: 0.82rem; + background: rgba(15, 23, 42, 0.06); + color: inherit; + border-radius: 4px; + padding: 0.1em 0.4em; + white-space: normal; + box-sizing: border-box; +} +.webshell-ai-msg table { + width: 100%; + max-width: 100%; + border-collapse: collapse; + overflow-x: auto; + display: block; +} +.webshell-ai-msg table th, +.webshell-ai-msg table td { + border: 1px solid var(--border-color); + padding: 4px 6px; + font-size: 0.8rem; +} +.webshell-ai-msg ul, +.webshell-ai-msg ol { + padding-left: 20px; +} .webshell-ai-input-row { flex-shrink: 0; display: flex; diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index 6f8b81ef..7642922b 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -351,7 +351,15 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) { if (!content && role !== 'assistant') return; var div = document.createElement('div'); div.className = 'webshell-ai-msg ' + (role === 'user' ? 'user' : 'assistant'); - div.textContent = content; + if (role === 'user') { + div.textContent = content; + } else { + if (typeof formatMarkdown === 'function') { + div.innerHTML = formatMarkdown(content); + } else { + div.textContent = content; + } + } messagesContainer.appendChild(div); }); if (list.length === 0) { @@ -546,7 +554,15 @@ function loadWebshellAiHistory(conn, messagesContainer) { if (!content && role !== 'assistant') return; var div = document.createElement('div'); div.className = 'webshell-ai-msg ' + (role === 'user' ? 'user' : 'assistant'); - div.textContent = content; + if (role === 'user') { + div.textContent = content; + } else { + if (typeof formatMarkdown === 'function') { + div.innerHTML = formatMarkdown(content); + } else { + div.textContent = content; + } + } messagesContainer.appendChild(div); }); if (list.length === 0) { @@ -598,11 +614,51 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { messagesContainer.appendChild(assistantDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; - function appendTimelineItem(type, title, message) { + function appendTimelineItem(type, title, message, data) { var item = document.createElement('div'); item.className = 'webshell-ai-timeline-item webshell-ai-timeline-' + type; - item.innerHTML = '' + escapeHtml(title || message || '') + ''; - if (message && message !== title) item.innerHTML += '
' + escapeHtml(message) + '
'; + + var html = '' + escapeHtml(title || message || '') + ''; + + // 工具调用入参 + if (type === 'tool_call' && data) { + try { + var args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : null); + if (args && typeof args === 'object') { + var paramsLabel = (typeof window.t === 'function') ? window.t('timeline.params') : '参数:'; + html += '
' + + escapeHtml(paramsLabel) + + '
' +
+                        escapeHtml(JSON.stringify(args, null, 2)) +
+                        '
'; + } + } catch (e) { + // JSON 解析失败时忽略参数详情,避免打断主流程 + } + } else if (type === 'tool_result' && data) { + // 工具调用出参 + var isError = data.isError || data.success === false; + var noResultText = (typeof window.t === 'function') ? window.t('timeline.noResult') : '无结果'; + var result = data.result != null ? data.result : (data.error != null ? data.error : noResultText); + var resultStr = (typeof result === 'string') ? result : JSON.stringify(result); + var execResultLabel = (typeof window.t === 'function') ? window.t('timeline.executionResult') : '执行结果:'; + var execIdLabel = (typeof window.t === 'function') ? window.t('timeline.executionId') : '执行ID:'; + html += '
' + escapeHtml(execResultLabel) + '
' +
+                escapeHtml(resultStr) +
+                '
' + + (data.executionId ? '
' + + escapeHtml(execIdLabel) + + ' ' + + escapeHtml(String(data.executionId)) + + '
' : '') + + '
'; + } else if (message && message !== title) { + html += '
' + escapeHtml(message) + '
'; + } + + item.innerHTML = html; timelineContainer.appendChild(item); timelineContainer.classList.add('has-items'); messagesContainer.scrollTop = messagesContainer.scrollHeight; @@ -663,38 +719,54 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) { } } else if (eventData.type === 'error' && eventData.message) { streamingTypingId += 1; - appendTimelineItem('error', '❌ 错误', eventData.message); - assistantDiv.textContent = '错误: ' + eventData.message; + var errLabel = (typeof window.t === 'function') ? window.t('chat.error') : '错误'; + appendTimelineItem('error', '❌ ' + errLabel, eventData.message, eventData.data); + assistantDiv.textContent = errLabel + ': ' + eventData.message; } else if (eventData.type === 'progress' && eventData.message) { - appendTimelineItem('progress', '🔍 ' + eventData.message, ''); + var progressMsg = (typeof window.translateProgressMessage === 'function') + ? window.translateProgressMessage(eventData.message) + : eventData.message; + appendTimelineItem('progress', '🔍 ' + progressMsg, '', eventData.data); if (!streamingTarget) assistantDiv.textContent = '…'; } else if (eventData.type === 'iteration') { var iterN = (eventData.data && eventData.data.iteration) || 0; - var iterTitle = iterN ? '🔍 第 ' + iterN + ' 轮迭代' : ('🔍 ' + (eventData.message || '迭代')); - appendTimelineItem('iteration', iterTitle, eventData.message || ''); + var iterTitle = (typeof window.t === 'function') + ? window.t('chat.iterationRound', { n: iterN || 1 }) + : (iterN ? ('第 ' + iterN + ' 轮迭代') : (eventData.message || '迭代')); + appendTimelineItem('iteration', '🔍 ' + iterTitle, eventData.message || '', eventData.data); if (!streamingTarget) assistantDiv.textContent = '…'; } else if (eventData.type === 'thinking' && eventData.message) { - appendTimelineItem('thinking', '🤔 AI 思考', eventData.message); + var thinkLabel = (typeof window.t === 'function') ? window.t('chat.aiThinking') : 'AI 思考'; + appendTimelineItem('thinking', '🤔 ' + thinkLabel, eventData.message, eventData.data); if (!streamingTarget) assistantDiv.textContent = '…'; } else if (eventData.type === 'tool_calls_detected' && eventData.data) { var count = eventData.data.count || 0; - appendTimelineItem('tool_calls_detected', '🔧 检测到 ' + count + ' 个工具调用', eventData.message || ''); + var detectedLabel = (typeof window.t === 'function') + ? window.t('chat.toolCallsDetected', { count: count }) + : ('检测到 ' + count + ' 个工具调用'); + appendTimelineItem('tool_calls_detected', '🔧 ' + detectedLabel, eventData.message || '', eventData.data); 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 title = '🔧 调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : ''); - appendTimelineItem('tool_call', title, eventData.message || ''); + var callTitle = (typeof window.t === 'function') + ? window.t('chat.callTool', { name: tn, index: idx, total: total }) + : ('调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : '')); + var title = '🔧 ' + callTitle; + appendTimelineItem('tool_call', title, eventData.message || '', eventData.data); 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 title = (success ? '✅ ' : '❌ ') + tname + (success ? ' 执行完成' : ' 执行失败'); + var titleText = (typeof window.t === 'function') + ? (success ? window.t('chat.toolExecComplete', { name: tname }) : window.t('chat.toolExecFailed', { name: tname })) + : (tname + (success ? ' 执行完成' : ' 执行失败')); + var title = (success ? '✅ ' : '❌ ') + titleText; var sub = eventData.message || (dr.result ? String(dr.result).slice(0, 300) : ''); - appendTimelineItem('tool_result', title, sub); + appendTimelineItem('tool_result', title, sub, eventData.data); if (!streamingTarget) assistantDiv.textContent = '…'; } } catch (e) { /* ignore parse error */ }