feat: 添加 reasoning_content 字段到多个消息结构,支持思考过程展示

This commit is contained in:
oudecheng 2026-06-11 14:33:29 +08:00
parent 0ce89a0e4e
commit 6ff5907616
8 changed files with 31 additions and 8 deletions

View File

@ -571,7 +571,8 @@ impl OutboundMessage {
"assistant" => { "assistant" => {
if let Some(tool_calls) = &message.tool_calls { if let Some(tool_calls) = &message.tool_calls {
let mut outbound = Vec::new(); let mut outbound = Vec::new();
if !message.content.trim().is_empty() { let has_content_or_reasoning = !message.content.trim().is_empty() || message.reasoning_content.is_some();
if has_content_or_reasoning {
let mut resp = Self::assistant( let mut resp = Self::assistant(
channel.to_string(), channel.to_string(),
chat_id.to_string(), chat_id.to_string(),
@ -585,7 +586,7 @@ impl OutboundMessage {
} }
outbound.extend(tool_calls.iter().map(|tool_call| { outbound.extend(tool_calls.iter().map(|tool_call| {
Self::tool_call( let mut tc = Self::tool_call(
channel.to_string(), channel.to_string(),
chat_id.to_string(), chat_id.to_string(),
session_id.clone(), session_id.clone(),
@ -594,7 +595,9 @@ impl OutboundMessage {
tool_call.arguments.clone(), tool_call.arguments.clone(),
reply_to.clone(), reply_to.clone(),
metadata.clone(), metadata.clone(),
) );
tc.reasoning_content = message.reasoning_content.clone();
tc
})); }));
outbound outbound
} else { } else {

View File

@ -762,6 +762,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(msg.timestamp / 1000), timestamp: Some(msg.timestamp / 1000),
reasoning_content: msg.reasoning_content.clone(),
}); });
} }
} }

View File

@ -156,6 +156,8 @@ pub enum WsOutbound {
topic_id: Option<String>, topic_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>, timestamp: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
reasoning_content: Option<String>,
}, },
#[serde(rename = "tool_result")] #[serde(rename = "tool_result")]
ToolResult { ToolResult {

View File

@ -15,7 +15,8 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
"assistant" => { "assistant" => {
if let Some(tool_calls) = &message.tool_calls { if let Some(tool_calls) = &message.tool_calls {
let mut outbound = Vec::new(); let mut outbound = Vec::new();
if !message.content.trim().is_empty() { let has_content_or_reasoning = !message.content.trim().is_empty() || message.reasoning_content.is_some();
if has_content_or_reasoning {
outbound.push(WsOutbound::AssistantResponse { outbound.push(WsOutbound::AssistantResponse {
id: message.id.clone(), id: message.id.clone(),
content: message.content.clone(), content: message.content.clone(),
@ -38,6 +39,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: message.reasoning_content.clone(),
})); }));
outbound outbound
} else { } else {
@ -126,6 +128,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
subagent_task_id: message.metadata.get("subagent_task_id").cloned(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
topic_id: message.metadata.get("topic_id").cloned(), topic_id: message.metadata.get("topic_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: message.reasoning_content.clone(),
}], }],
OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult { OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult {
id: message id: message

View File

@ -125,6 +125,7 @@ fn test_tool_call_outbound_serialization() {
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: None,
}; };
let json = serde_json::to_string(&msg).unwrap(); let json = serde_json::to_string(&msg).unwrap();

View File

@ -242,11 +242,11 @@ function ThinkingSection({ content }: { content: string }) {
return ( return (
<div className="mb-3 rounded-lg border border-purple-500/20 bg-purple-500/5 overflow-hidden"> <div className="mb-3 rounded-lg border border-purple-500/20 bg-purple-500/5 overflow-hidden">
<button <button
onClick={() => setExpanded(!expanded)} onClick={(e) => { e.stopPropagation(); setExpanded(!expanded) }}
className="flex items-center gap-2 w-full px-3 py-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-purple-500/5 transition-colors cursor-pointer select-none" className="flex items-center gap-2 w-full px-3 py-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-purple-500/5 transition-colors cursor-pointer select-none"
> >
<Brain className="h-3.5 w-3.5 text-purple-400 flex-shrink-0" /> <Brain className="h-3.5 w-3.5 text-purple-400 flex-shrink-0" />
<span className="font-medium text-purple-300"></span> <span className="font-medium text-purple-300">Thinking</span>
{expanded ? ( {expanded ? (
<ChevronDown className="h-3 w-3 ml-auto flex-shrink-0" /> <ChevronDown className="h-3 w-3 ml-auto flex-shrink-0" />
) : ( ) : (
@ -260,7 +260,7 @@ function ThinkingSection({ content }: { content: string }) {
</button> </button>
{expanded && ( {expanded && (
<div className="px-3 py-2 border-t border-purple-500/10 animate-thinking-reveal"> <div className="px-3 py-2 border-t border-purple-500/10 animate-thinking-reveal">
<div className="text-xs text-purple-200/80 italic whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto scrollbar-thin"> <div className="text-xs text-purple-200/80 whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto scrollbar-thin">
{content} {content}
</div> </div>
</div> </div>
@ -437,6 +437,12 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</span> </span>
</div> </div>
{/* 模型思考内容(工具调用时展示) */}
{message.reasoningContent && !toolExpanded && (
<div className="px-3 pb-1">
<ThinkingSection content={message.reasoningContent} />
</div>
)}
{/* Collapsed preview */} {/* Collapsed preview */}
{!toolExpanded && ( {!toolExpanded && (
<> <>
@ -495,6 +501,10 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{/* Expanded */} {/* Expanded */}
{toolExpanded && ( {toolExpanded && (
<div className="border-t border-[var(--border-color)] px-3 py-2 space-y-2"> <div className="border-t border-[var(--border-color)] px-3 py-2 space-y-2">
{/* 模型思考内容(展开时也展示) */}
{message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} />
)}
{taskResult ? ( {taskResult ? (
<> <>
{taskPrompt && ( {taskPrompt && (
@ -646,7 +656,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{!isTool && message.reasoningContent && ( {!isTool && message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} /> <ThinkingSection content={message.reasoningContent} />
)} )}
// AI 和工具消息使用 Markdown 渲染 {/* AI 和工具消息使用 Markdown 渲染 */}
<div className="markdown-content text-sm leading-relaxed"> <div className="markdown-content text-sm leading-relaxed">
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}

View File

@ -210,6 +210,7 @@ export function useChat(): UseChatReturn {
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
arguments: msg.arguments, arguments: msg.arguments,
subagentTaskId: msg.subagent_task_id, subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
} }
} }
case 'tool_result': { case 'tool_result': {
@ -460,6 +461,7 @@ export function useChat(): UseChatReturn {
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
arguments: msg.arguments, arguments: msg.arguments,
subagentTaskId: msg.subagent_task_id, subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
}, },
]) ])
break break

View File

@ -59,6 +59,7 @@ export interface ToolCall {
subagent_task_id?: string subagent_task_id?: string
topic_id?: string topic_id?: string
timestamp?: number timestamp?: number
reasoning_content?: string
} }
export interface ToolResult { export interface ToolResult {