From 0ce89a0e4edfe71f4c6c73bac2f235543a577943 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 11 Jun 2026 12:04:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20reasoning=5Fconten?= =?UTF-8?q?t=20=E5=AD=97=E6=AE=B5=E5=88=B0=E5=A4=9A=E4=B8=AA=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=BB=93=E6=9E=84=EF=BC=8C=E6=94=AF=E6=8C=81=E6=80=9D?= =?UTF-8?q?=E8=80=83=E8=BF=87=E7=A8=8B=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bus/message.rs | 17 ++++++--- src/command/adapters/websocket.rs | 7 ++++ src/gateway/ws.rs | 2 ++ src/protocol/mod.rs | 2 ++ src/protocol/ws_adapter.rs | 3 ++ src/tools/task/runtime.rs | 1 + tests/test_request_format.rs | 4 +++ web/src/components/Chat/MessageBubble.tsx | 42 +++++++++++++++++++++-- web/src/hooks/useChat.ts | 2 ++ web/src/index.css | 7 ++++ web/src/types/protocol.ts | 2 ++ 11 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/bus/message.rs b/src/bus/message.rs index 49b0912..d4d2d21 100644 --- a/src/bus/message.rs +++ b/src/bus/message.rs @@ -400,6 +400,7 @@ pub struct OutboundMessage { pub tool_call_id: Option, pub tool_name: Option, pub tool_arguments: Option, + pub reasoning_content: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -439,6 +440,7 @@ impl OutboundMessage { tool_call_id: None, tool_name: None, tool_arguments: None, + reasoning_content: None, } } @@ -493,6 +495,7 @@ impl OutboundMessage { tool_call_id: Some(message_id.into()), tool_name: Some(tool_name), tool_arguments: Some(tool_arguments), + reasoning_content: None, } } @@ -522,6 +525,7 @@ impl OutboundMessage { tool_call_id: Some(tool_call_id.into()), tool_name: Some(tool_name), tool_arguments: None, + reasoning_content: None, } } @@ -551,6 +555,7 @@ impl OutboundMessage { tool_call_id: Some(tool_call_id.into()), tool_name: Some(tool_name), tool_arguments: None, + reasoning_content: None, } } @@ -567,14 +572,16 @@ impl OutboundMessage { if let Some(tool_calls) = &message.tool_calls { let mut outbound = Vec::new(); if !message.content.trim().is_empty() { - outbound.push(Self::assistant( + let mut resp = Self::assistant( channel.to_string(), chat_id.to_string(), session_id.clone(), message.content.clone(), reply_to.clone(), metadata.clone(), - )); + ); + resp.reasoning_content = message.reasoning_content.clone(); + outbound.push(resp); } outbound.extend(tool_calls.iter().map(|tool_call| { @@ -591,14 +598,16 @@ impl OutboundMessage { })); outbound } else { - vec![Self::assistant( + let mut resp = Self::assistant( channel.to_string(), chat_id.to_string(), session_id, message.content.clone(), reply_to, metadata.clone(), - )] + ); + resp.reasoning_content = message.reasoning_content.clone(); + vec![resp] } } "tool" => match message diff --git a/src/command/adapters/websocket.rs b/src/command/adapters/websocket.rs index e089978..e3cfffb 100644 --- a/src/command/adapters/websocket.rs +++ b/src/command/adapters/websocket.rs @@ -79,6 +79,7 @@ impl OutputAdapter for WebSocketOutputAdapter { content: msg.content.clone(), role: "assistant".to_string(), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: None, }, MessageKind::Notification => { // 根据元数据判断具体类型 @@ -99,6 +100,7 @@ impl OutputAdapter for WebSocketOutputAdapter { content: msg.content.clone(), role: "assistant".to_string(), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: None, }, } } else if let Some(session_id) = response.metadata.get("session_id") { @@ -138,6 +140,7 @@ impl OutputAdapter for WebSocketOutputAdapter { content: msg.content.clone(), role: "assistant".to_string(), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: None, }, } } else if let Some(sessions_json) = response.metadata.get("sessions") { @@ -156,6 +159,7 @@ impl OutputAdapter for WebSocketOutputAdapter { content: msg.content.clone(), role: "assistant".to_string(), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: None, }, } } else if let Some(topics_json) = response.metadata.get("topics") { @@ -175,6 +179,7 @@ impl OutputAdapter for WebSocketOutputAdapter { content: msg.content.clone(), role: "assistant".to_string(), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: None, }, } } else { @@ -184,6 +189,7 @@ impl OutputAdapter for WebSocketOutputAdapter { content: msg.content.clone(), role: "assistant".to_string(), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: None, } } } @@ -197,6 +203,7 @@ impl OutputAdapter for WebSocketOutputAdapter { content: msg.content.clone(), role: "assistant".to_string(), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: None, }, }; outbounds.push(outbound); diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 93fac45..1cacf97 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -774,6 +774,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option { @@ -811,6 +812,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option None, } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index d8eb43f..f780ad5 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -139,6 +139,8 @@ pub enum WsOutbound { topic_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] timestamp: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + reasoning_content: Option, }, #[serde(rename = "tool_call")] ToolCall { diff --git a/src/protocol/ws_adapter.rs b/src/protocol/ws_adapter.rs index 5a7097b..a9d5e1d 100644 --- a/src/protocol/ws_adapter.rs +++ b/src/protocol/ws_adapter.rs @@ -24,6 +24,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec Vec Ve subagent_task_id: message.metadata.get("subagent_task_id").cloned(), topic_id: message.metadata.get("topic_id").cloned(), timestamp: Some(crate::protocol::now_timestamp()), + reasoning_content: message.reasoning_content.clone(), }] } OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall { diff --git a/src/tools/task/runtime.rs b/src/tools/task/runtime.rs index 94f5ddd..c984aea 100644 --- a/src/tools/task/runtime.rs +++ b/src/tools/task/runtime.rs @@ -438,6 +438,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime { tool_call_id: None, tool_name: None, tool_arguments: None, + reasoning_content: None, }; if let Err(e) = bus.publish_outbound(event).await { diff --git a/tests/test_request_format.rs b/tests/test_request_format.rs index 3db4278..0c39a14 100644 --- a/tests/test_request_format.rs +++ b/tests/test_request_format.rs @@ -123,6 +123,8 @@ fn test_tool_call_outbound_serialization() { content: "调用工具: calculator".to_string(), role: "assistant".to_string(), subagent_task_id: None, + topic_id: None, + timestamp: None, }; let json = serde_json::to_string(&msg).unwrap(); @@ -156,6 +158,8 @@ fn test_tool_result_outbound_serialization() { role: "tool".to_string(), subagent_task_id: None, duration_ms: None, + topic_id: None, + timestamp: None, }; let json = serde_json::to_string(&msg).unwrap(); diff --git a/web/src/components/Chat/MessageBubble.tsx b/web/src/components/Chat/MessageBubble.tsx index 37f54ee..f347af9 100644 --- a/web/src/components/Chat/MessageBubble.tsx +++ b/web/src/components/Chat/MessageBubble.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader, X } from 'lucide-react' +import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader, X, Brain } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import type { ChatMessage, Attachment, TaskToolResult } from '../../types/protocol' @@ -236,6 +236,39 @@ function CopyButton({ text, className = '' }: { text: string; className?: string ) } +function ThinkingSection({ content }: { content: string }) { + const [expanded, setExpanded] = useState(false) + + return ( +
+ + {expanded && ( +
+
+ {content} +
+
+ )} +
+ ) +} + function parseTaskResult(content: string): TaskToolResult | null { if (!content) return null try { @@ -608,6 +641,11 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr // 用户消息保持纯文本
{message.content}
) : ( + // 模型思考内容(仅助手消息,非工具消息) + <> + {!isTool && message.reasoningContent && ( + + )} // AI 和工具消息使用 Markdown 渲染
- )} + )} {message.attachments && message.attachments.length > 0 && (
{message.attachments.some(att => att.media_type === 'image' && att.content_base64) && ( diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index ce3d834..4694e75 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -195,6 +195,7 @@ export function useChat(): UseChatReturn { type: 'message', attachments: msg.attachments, subagentTaskId: msg.subagent_task_id, + reasoningContent: msg.reasoning_content, } } case 'tool_call': { @@ -430,6 +431,7 @@ export function useChat(): UseChatReturn { timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'message', attachments: msg.attachments, + reasoningContent: msg.reasoning_content, }, ]) setIsLoading(false) diff --git a/web/src/index.css b/web/src/index.css index 958a5a3..4997e93 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -258,6 +258,13 @@ body { .animate-fade-in { animation: fade-in 0.2s ease-out; } .animate-scale-in { animation: scale-in 0.3s ease-out; } +@keyframes thinking-reveal { + from { max-height: 0; opacity: 0; } + to { max-height: 300px; opacity: 1; } +} + +.animate-thinking-reveal { animation: thinking-reveal 0.25s ease-out; } + .typing-indicator span { animation: typing-dot 1.4s infinite; display: inline-block; diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index fc83e60..db23870 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -45,6 +45,7 @@ export interface AssistantResponse { subagent_task_id?: string topic_id?: string timestamp?: number + reasoning_content?: string } export interface ToolCall { @@ -411,6 +412,7 @@ export interface ChatMessage { callContent?: string subagentTaskId?: string durationMs?: number + reasoningContent?: string } /** task 工具返回的 JSON 结构 */