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" => {
if let Some(tool_calls) = &message.tool_calls {
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(
channel.to_string(),
chat_id.to_string(),
@ -585,7 +586,7 @@ impl OutboundMessage {
}
outbound.extend(tool_calls.iter().map(|tool_call| {
Self::tool_call(
let mut tc = Self::tool_call(
channel.to_string(),
chat_id.to_string(),
session_id.clone(),
@ -594,7 +595,9 @@ impl OutboundMessage {
tool_call.arguments.clone(),
reply_to.clone(),
metadata.clone(),
)
);
tc.reasoning_content = message.reasoning_content.clone();
tc
}));
outbound
} else {

View File

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

View File

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

View File

@ -15,7 +15,8 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
"assistant" => {
if let Some(tool_calls) = &message.tool_calls {
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 {
id: message.id.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,
topic_id: None,
timestamp: None,
reasoning_content: message.reasoning_content.clone(),
}));
outbound
} 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(),
topic_id: message.metadata.get("topic_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: message.reasoning_content.clone(),
}],
OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult {
id: message

View File

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

View File

@ -242,11 +242,11 @@ function ThinkingSection({ content }: { content: string }) {
return (
<div className="mb-3 rounded-lg border border-purple-500/20 bg-purple-500/5 overflow-hidden">
<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"
>
<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 ? (
<ChevronDown className="h-3 w-3 ml-auto flex-shrink-0" />
) : (
@ -260,7 +260,7 @@ function ThinkingSection({ content }: { content: string }) {
</button>
{expanded && (
<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}
</div>
</div>
@ -437,6 +437,12 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</span>
</div>
{/* 模型思考内容(工具调用时展示) */}
{message.reasoningContent && !toolExpanded && (
<div className="px-3 pb-1">
<ThinkingSection content={message.reasoningContent} />
</div>
)}
{/* Collapsed preview */}
{!toolExpanded && (
<>
@ -495,6 +501,10 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{/* Expanded */}
{toolExpanded && (
<div className="border-t border-[var(--border-color)] px-3 py-2 space-y-2">
{/* 模型思考内容(展开时也展示) */}
{message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} />
)}
{taskResult ? (
<>
{taskPrompt && (
@ -646,7 +656,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{!isTool && message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} />
)}
// AI 和工具消息使用 Markdown 渲染
{/* AI 和工具消息使用 Markdown 渲染 */}
<div className="markdown-content text-sm leading-relaxed">
<ReactMarkdown
remarkPlugins={[remarkGfm]}

View File

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

View File

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