feat: 添加 reasoning_content 字段到多个消息结构,支持思考过程展示
This commit is contained in:
parent
694b3ce0e0
commit
0ce89a0e4e
@ -400,6 +400,7 @@ pub struct OutboundMessage {
|
||||
pub tool_call_id: Option<String>,
|
||||
pub tool_name: Option<String>,
|
||||
pub tool_arguments: Option<serde_json::Value>,
|
||||
pub reasoning_content: Option<String>,
|
||||
}
|
||||
|
||||
#[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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -774,6 +774,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(),
|
||||
})
|
||||
}
|
||||
"tool" => {
|
||||
@ -811,6 +812,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: None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@ -139,6 +139,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_call")]
|
||||
ToolCall {
|
||||
|
||||
@ -24,6 +24,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(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -48,6 +49,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(),
|
||||
}]
|
||||
}
|
||||
}
|
||||
@ -105,6 +107,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::ToolCall => vec![WsOutbound::ToolCall {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 (
|
||||
<div className="mb-3 rounded-lg border border-purple-500/20 bg-purple-500/5 overflow-hidden">
|
||||
<button
|
||||
onClick={() => 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>
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-3 w-3 ml-auto flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 ml-auto flex-shrink-0" />
|
||||
)}
|
||||
{!expanded && (
|
||||
<span className="text-[var(--text-muted)] truncate flex-1 text-left">
|
||||
{content.slice(0, 60)}{content.length > 60 ? '…' : ''}
|
||||
</span>
|
||||
)}
|
||||
</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">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function parseTaskResult(content: string): TaskToolResult | null {
|
||||
if (!content) return null
|
||||
try {
|
||||
@ -608,6 +641,11 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
||||
// 用户消息保持纯文本
|
||||
<div className="whitespace-pre-wrap text-sm leading-relaxed">{message.content}</div>
|
||||
) : (
|
||||
// 模型思考内容(仅助手消息,非工具消息)
|
||||
<>
|
||||
{!isTool && message.reasoningContent && (
|
||||
<ThinkingSection content={message.reasoningContent} />
|
||||
)}
|
||||
// AI 和工具消息使用 Markdown 渲染
|
||||
<div className="markdown-content text-sm leading-relaxed">
|
||||
<ReactMarkdown
|
||||
@ -688,7 +726,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</>)}
|
||||
{message.attachments && message.attachments.length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{message.attachments.some(att => att.media_type === 'image' && att.content_base64) && (
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 结构 */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user