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(JSON.stringify(args, null, 2)) + + '
' + + escapeHtml(resultStr) + + '' + + (data.executionId ? '
' +
+ escapeHtml(String(data.executionId)) +
+ '