// src/views/AgentChat/index_v4.js // Agent聊天页面 V4 - 黑金毛玻璃设计,带模型选择和工具选择 import React, { useState, useEffect, useRef } from 'react'; import { Global, css } from '@emotion/react'; import { Box, Flex, VStack, HStack, Text, Input, IconButton, Button, Avatar, Heading, Divider, Spinner, Badge, useToast, Progress, Fade, Collapse, InputGroup, InputLeftElement, Menu, MenuButton, MenuList, MenuItem, Tooltip, Select, Checkbox, CheckboxGroup, Stack, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon, useDisclosure, } from '@chakra-ui/react'; import { FiSend, FiSearch, FiPlus, FiMessageSquare, FiTrash2, FiMoreVertical, FiRefreshCw, FiDownload, FiCpu, FiUser, FiZap, FiClock, FiSettings, FiCheckCircle, FiChevronRight, FiTool, } from 'react-icons/fi'; import { useAuth } from '@contexts/AuthContext'; import { PlanCard } from '@components/ChatBot/PlanCard'; import { StepResultCard } from '@components/ChatBot/StepResultCard'; import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts'; import { logger } from '@utils/logger'; import axios from 'axios'; import HomeNavbar from '@components/Navbars/HomeNavbar'; /** * Agent消息类型 */ const MessageTypes = { USER: 'user', AGENT_THINKING: 'agent_thinking', AGENT_PLAN: 'agent_plan', AGENT_EXECUTING: 'agent_executing', AGENT_RESPONSE: 'agent_response', ERROR: 'error', }; /** * 可用模型配置 */ const AVAILABLE_MODELS = [ { id: 'kimi-k2', name: 'Kimi K2', description: '快速响应,适合日常对话', icon: '🚀', provider: 'Moonshot', }, { id: 'kimi-k2-thinking', name: 'Kimi K2 Thinking', description: '深度思考,提供详细推理过程', icon: '🧠', provider: 'Moonshot', recommended: true, }, { id: 'glm-4.6', name: 'GLM 4.6', description: '智谱AI最新模型,性能强大', icon: '⚡', provider: 'ZhipuAI', }, { id: 'deepmoney', name: 'DeepMoney', description: '金融专业模型,擅长财经分析', icon: '💰', provider: 'Custom', }, { id: 'gemini-3', name: 'Gemini 3', description: 'Google最新多模态模型', icon: '✨', provider: 'Google', }, ]; /** * MCP工具分类配置 */ const MCP_TOOL_CATEGORIES = [ { name: '新闻搜索', icon: '📰', tools: [ { id: 'search_news', name: '全球新闻搜索', description: '搜索国际新闻、行业动态' }, { id: 'search_china_news', name: '中国新闻搜索', description: 'KNN语义搜索中国新闻' }, { id: 'search_medical_news', name: '医疗新闻搜索', description: '医药、医疗设备、生物技术' }, ], }, { name: '股票分析', icon: '📈', tools: [ { id: 'search_limit_up_stocks', name: '涨停股票搜索', description: '搜索涨停股票及原因分析' }, { id: 'get_stock_analysis', name: '个股分析', description: '获取股票深度分析报告' }, { id: 'get_stock_concepts', name: '股票概念查询', description: '查询股票相关概念板块' }, ], }, { name: '概念板块', icon: '🏢', tools: [ { id: 'search_concepts', name: '概念搜索', description: '搜索股票概念板块' }, { id: 'get_concept_details', name: '概念详情', description: '获取概念板块详细信息' }, { id: 'get_concept_statistics', name: '概念统计', description: '涨幅榜、活跃榜、连涨榜' }, ], }, { name: '公司信息', icon: '🏭', tools: [ { id: 'search_roadshows', name: '路演搜索', description: '搜索上市公司路演活动' }, { id: 'get_company_info', name: '公司信息', description: '获取公司基本面信息' }, ], }, { name: '数据分析', icon: '📊', tools: [ { id: 'query_database', name: '数据库查询', description: 'SQL查询金融数据' }, { id: 'get_market_overview', name: '市场概况', description: '获取市场整体行情' }, ], }, ]; /** * Agent聊天页面 V4 - 黑金毛玻璃设计 */ const AgentChatV4 = () => { const { user } = useAuth(); const toast = useToast(); // 确保组件总是返回有效的 React 元素 if (!user) { return ( 正在加载... ); } // 会话相关状态 const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(null); const [isLoadingSessions, setIsLoadingSessions] = useState(true); // 消息相关状态 const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [currentProgress, setCurrentProgress] = useState(0); // 模型和工具配置状态 const [selectedModel, setSelectedModel] = useState('kimi-k2-thinking'); const [selectedTools, setSelectedTools] = useState(() => { // 默认全选所有工具 const allToolIds = MCP_TOOL_CATEGORIES.flatMap(cat => cat.tools.map(t => t.id)); return allToolIds; }); // UI 状态 const [searchQuery, setSearchQuery] = useState(''); // 检测是否为移动设备(屏幕宽度小于 768px) const [isMobile, setIsMobile] = useState(window.innerWidth < 768); // 根据设备类型设置侧边栏默认状态(移动端默认收起) const { isOpen: isSidebarOpen, onToggle: toggleSidebar } = useDisclosure({ defaultIsOpen: !isMobile }); const { isOpen: isRightPanelOpen, onToggle: toggleRightPanel } = useDisclosure({ defaultIsOpen: !isMobile }); // Refs const messagesEndRef = useRef(null); const inputRef = useRef(null); // 毛玻璃深灰金配色主题(类似编程工具的深色主题) const glassBg = 'rgba(30, 35, 40, 0.85)'; // 深灰色毛玻璃 const glassHoverBg = 'rgba(40, 45, 50, 0.9)'; const goldAccent = '#FFD700'; const goldGradient = 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)'; const darkBg = '#1a1d23'; // VS Code 风格的深灰背景 const borderGold = 'rgba(255, 215, 0, 0.3)'; const textGold = '#FFD700'; const textWhite = '#E8E8E8'; // 柔和的白色 const textGray = '#9BA1A6'; // 柔和的灰色 const cardBg = 'rgba(40, 45, 50, 0.6)'; // 卡片背景(深灰毛玻璃) // ==================== 会话管理函数 ==================== const loadSessions = async () => { if (!user?.id) return; setIsLoadingSessions(true); try { const response = await axios.get('/mcp/agent/sessions', { params: { user_id: String(user.id), limit: 50 }, }); if (response.data.success) { setSessions(response.data.data); logger.info('会话列表加载成功', response.data.data); } } catch (error) { logger.error('加载会话列表失败', error); toast({ title: '加载失败', description: '无法加载会话列表', status: 'error', duration: 3000, }); } finally { setIsLoadingSessions(false); } }; const loadSessionHistory = async (sessionId) => { if (!sessionId) return; try { const response = await axios.get(`/mcp/agent/history/${sessionId}`, { params: { limit: 100 }, }); if (response.data.success) { const history = response.data.data; const formattedMessages = history.map((msg, idx) => ({ id: `${sessionId}-${idx}`, type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE, content: msg.message, plan: msg.plan ? JSON.parse(msg.plan) : null, stepResults: msg.steps ? JSON.parse(msg.steps) : null, timestamp: msg.timestamp, })); setMessages(formattedMessages); logger.info('会话历史加载成功', formattedMessages); } } catch (error) { logger.error('加载会话历史失败', error); toast({ title: '加载失败', description: '无法加载会话历史', status: 'error', duration: 3000, }); } }; const createNewSession = () => { setCurrentSessionId(null); setMessages([ { id: Date.now(), type: MessageTypes.AGENT_RESPONSE, content: `你好${user?.nickname || ''}!我是价小前,北京价值前沿科技公司的AI投研助手。\n\n我会通过多步骤分析来帮助你深入了解金融市场。\n\n你可以问我:\n• 全面分析某只股票\n• 某个行业的投资机会\n• 今日市场热点\n• 某个概念板块的表现`, timestamp: new Date().toISOString(), }, ]); }; const switchSession = (sessionId) => { setCurrentSessionId(sessionId); loadSessionHistory(sessionId); }; const deleteSession = async (sessionId) => { toast({ title: '删除会话', description: '此功能尚未实现', status: 'info', duration: 2000, }); }; // ==================== 消息处理函数 ==================== const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); // 监听窗口大小变化,动态更新移动端状态 useEffect(() => { const handleResize = () => { const mobile = window.innerWidth < 768; setIsMobile(mobile); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // 在 AgentChat 页面隐藏 Bytedesk 客服插件(避免遮挡页面) useEffect(() => { // 隐藏 Bytedesk - 更安全的方式 const hideBytedeskElements = () => { try { // 查找所有 Bytedesk 相关元素 const bytedeskElements = document.querySelectorAll( '[class*="bytedesk"], [id*="bytedesk"], [class*="BytedeskWeb"], .bytedesk-widget' ); // 保存原始 display 值 const originalDisplays = new Map(); bytedeskElements.forEach(el => { if (el && el.style) { originalDisplays.set(el, el.style.display); el.style.display = 'none'; } }); // 返回清理函数 return () => { bytedeskElements.forEach(el => { if (el && el.style) { const originalDisplay = originalDisplays.get(el); if (originalDisplay !== undefined) { el.style.display = originalDisplay; } else { el.style.display = ''; } } }); }; } catch (error) { console.warn('Failed to hide Bytedesk elements:', error); return () => {}; // 返回空清理函数 } }; const cleanup = hideBytedeskElements(); // 组件卸载时恢复显示 return cleanup; }, []); const addMessage = (message) => { setMessages((prev) => [...prev, { ...message, id: Date.now() }]); }; const handleSendMessage = async () => { if (!inputValue.trim() || isProcessing) return; const hasAccess = user?.subscription_type === 'max'; if (!hasAccess) { logger.warn('AgentChat', '权限检查失败', { userId: user?.id, subscription_type: user?.subscription_type, }); toast({ title: '订阅升级', description: '「价小前投研」功能需要 Max 订阅。请前往设置页面升级您的订阅。', status: 'warning', duration: 5000, isClosable: true, }); return; } const userMessage = { type: MessageTypes.USER, content: inputValue, timestamp: new Date().toISOString(), }; addMessage(userMessage); const userInput = inputValue; setInputValue(''); setIsProcessing(true); setCurrentProgress(0); let currentPlan = null; let stepResults = []; let executingMessageId = null; try { addMessage({ type: MessageTypes.AGENT_THINKING, content: '正在分析你的问题...', timestamp: new Date().toISOString(), }); setCurrentProgress(10); const requestBody = { message: userInput, conversation_history: messages .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) .map((m) => ({ isUser: m.type === MessageTypes.USER, content: m.content, })), user_id: user?.id ? String(user.id) : 'anonymous', user_nickname: user?.nickname || user?.username || '匿名用户', user_avatar: user?.avatar || '', subscription_type: user?.subscription_type || 'free', session_id: currentSessionId, model: selectedModel, // 传递选中的模型 tools: selectedTools, // 传递选中的工具 }; const response = await fetch('/mcp/agent/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let thinkingMessageId = null; let thinkingContent = ''; let summaryMessageId = null; let summaryContent = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); let currentEvent = null; for (const line of lines) { if (!line.trim() || line.startsWith(':')) continue; if (line.startsWith('event:')) { currentEvent = line.substring(6).trim(); continue; } if (line.startsWith('data:')) { try { const data = JSON.parse(line.substring(5).trim()); if (currentEvent === 'thinking') { if (!thinkingMessageId) { thinkingMessageId = Date.now(); thinkingContent = ''; setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); addMessage({ id: thinkingMessageId, type: MessageTypes.AGENT_THINKING, content: '', timestamp: new Date().toISOString(), }); } thinkingContent += data.content; setMessages((prev) => prev.map((m) => m.id === thinkingMessageId ? { ...m, content: thinkingContent } : m ) ); } else if (currentEvent === 'plan') { currentPlan = data; thinkingMessageId = null; thinkingContent = ''; setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); addMessage({ type: MessageTypes.AGENT_PLAN, content: '已制定执行计划', plan: data, timestamp: new Date().toISOString(), }); setCurrentProgress(30); } else if (currentEvent === 'step_complete') { const stepResult = { step_index: data.step_index, tool: data.tool, status: data.status, result: data.result, error: data.error, execution_time: data.execution_time, }; stepResults.push(stepResult); setMessages((prev) => prev.map((m) => m.id === executingMessageId ? { ...m, stepResults: [...stepResults] } : m ) ); const progress = 40 + (stepResults.length / (currentPlan?.steps?.length || 5)) * 40; setCurrentProgress(Math.min(progress, 80)); } else if (currentEvent === 'summary_chunk') { if (!summaryMessageId) { summaryMessageId = Date.now(); summaryContent = ''; setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING) ); addMessage({ id: summaryMessageId, type: MessageTypes.AGENT_RESPONSE, content: '', plan: currentPlan, stepResults: stepResults, isStreaming: true, timestamp: new Date().toISOString(), }); setCurrentProgress(85); } summaryContent += data.content; setMessages((prev) => prev.map((m) => m.id === summaryMessageId ? { ...m, content: summaryContent } : m ) ); } else if (currentEvent === 'summary') { if (summaryMessageId) { setMessages((prev) => prev.map((m) => m.id === summaryMessageId ? { ...m, content: data.content || summaryContent, metadata: data.metadata, isStreaming: false, } : m ) ); } else { setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING) ); addMessage({ type: MessageTypes.AGENT_RESPONSE, content: data.content, plan: currentPlan, stepResults: stepResults, metadata: data.metadata, isStreaming: false, timestamp: new Date().toISOString(), }); } setCurrentProgress(100); } else if (currentEvent === 'status') { if (data.stage === 'planning') { setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_THINKING)); addMessage({ type: MessageTypes.AGENT_THINKING, content: data.message, timestamp: new Date().toISOString(), }); setCurrentProgress(10); } else if (data.stage === 'executing') { const msgId = Date.now(); executingMessageId = msgId; addMessage({ id: msgId, type: MessageTypes.AGENT_EXECUTING, content: data.message, plan: currentPlan, stepResults: [], timestamp: new Date().toISOString(), }); setCurrentProgress(40); } else if (data.stage === 'summarizing') { setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); addMessage({ type: MessageTypes.AGENT_THINKING, content: data.message, timestamp: new Date().toISOString(), }); setCurrentProgress(80); } } } catch (e) { logger.error('解析 SSE 数据失败', e); } } } } loadSessions(); } catch (error) { logger.error('Agent chat error', error); setMessages((prev) => prev.filter( (m) => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING ) ); const errorMessage = error.response?.data?.detail || error.message || '处理失败'; addMessage({ type: MessageTypes.ERROR, content: `处理失败:${errorMessage}`, timestamp: new Date().toISOString(), }); toast({ title: '处理失败', description: errorMessage, status: 'error', duration: 5000, isClosable: true, }); } finally { setIsProcessing(false); setCurrentProgress(0); inputRef.current?.focus(); } }; const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; const handleClearChat = () => { createNewSession(); }; const handleExportChat = () => { const chatText = messages .filter((m) => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE) .map((msg) => `[${msg.type === MessageTypes.USER ? '用户' : '价小前'}] ${msg.content}`) .join('\n\n'); const blob = new Blob([chatText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chat_${new Date().toISOString().slice(0, 10)}.txt`; a.click(); URL.revokeObjectURL(url); }; // ==================== 工具选择处理 ==================== const handleToolToggle = (toolId, isChecked) => { if (isChecked) { setSelectedTools((prev) => [...prev, toolId]); } else { setSelectedTools((prev) => prev.filter((id) => id !== toolId)); } }; const handleCategoryToggle = (categoryTools, isAllSelected) => { const toolIds = categoryTools.map((t) => t.id); if (isAllSelected) { // 全部取消选中 setSelectedTools((prev) => prev.filter((id) => !toolIds.includes(id))); } else { // 全部选中 setSelectedTools((prev) => { const newTools = [...prev]; toolIds.forEach((id) => { if (!newTools.includes(id)) { newTools.push(id); } }); return newTools; }); } }; // ==================== 初始化 ==================== useEffect(() => { if (user) { loadSessions(); createNewSession(); } }, [user]); // ==================== 渲染 ==================== const quickQuestions = [ '全面分析贵州茅台这只股票', '今日涨停股票有哪些亮点', '新能源概念板块的投资机会', '半导体行业最新动态', ]; const filteredSessions = sessions.filter( (session) => !searchQuery || session.last_message?.toLowerCase().includes(searchQuery.toLowerCase()) ); return ( {/* 顶部导航栏 */} {/* 全局样式:确保页面正确显示 */} {/* 主内容区域 */} {/* 左侧会话列表 */} {/* 侧边栏头部 */} setSearchQuery(e.target.value)} bg="rgba(255, 255, 255, 0.05)" border="1px solid" borderColor={borderGold} color={textWhite} _placeholder={{ color: textGray }} _hover={{ borderColor: goldAccent }} _focus={{ borderColor: goldAccent, boxShadow: `0 0 0 1px ${goldAccent}` }} /> {/* 会话列表 */} {isLoadingSessions ? ( ) : filteredSessions.length === 0 ? ( {searchQuery ? '没有找到匹配的对话' : '暂无对话记录'} ) : ( filteredSessions.map((session) => ( switchSession(session.session_id)} transition="all 0.2s" > {session.last_message || '新对话'} {new Date(session.last_timestamp).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', })} {session.message_count} 条 } size="xs" variant="ghost" color={textGray} _hover={{ color: goldAccent }} onClick={(e) => e.stopPropagation()} /> } color="red.400" bg="transparent" _hover={{ bg: 'rgba(255, 0, 0, 0.1)' }} onClick={(e) => { e.stopPropagation(); deleteSession(session.session_id); }} > 删除对话 )) )} {/* 用户信息 */} {user?.nickname || '未登录'} MAX 订阅 {/* 主聊天区域 */} {/* 聊天头部 */} 价小前投研 AI 深度分析 {AVAILABLE_MODELS.find(m => m.id === selectedModel)?.name || '智能模型'} } size="sm" variant="ghost" color={goldAccent} _hover={{ bg: 'rgba(255, 215, 0, 0.1)' }} aria-label="历史对话" onClick={toggleSidebar} /> } size="sm" variant="ghost" color={textGray} _hover={{ color: goldAccent, bg: 'rgba(255, 215, 0, 0.1)' }} aria-label="清空对话" onClick={handleClearChat} /> } size="sm" variant="ghost" color={textGray} _hover={{ color: goldAccent, bg: 'rgba(255, 215, 0, 0.1)' }} aria-label="导出对话" onClick={handleExportChat} display={{ base: "none", sm: "inline-flex" }} /> } size="sm" variant="ghost" color={goldAccent} _hover={{ bg: 'rgba(255, 215, 0, 0.1)' }} aria-label="设置" onClick={toggleRightPanel} /> {/* 进度条 */} {isProcessing && ( div': { background: goldGradient, }, }} isAnimated /> )} {/* 消息列表 */} {messages.map((message) => ( ))}
{/* 快捷问题 */} {messages.length <= 2 && !isProcessing && ( 💡 试试这些问题: {quickQuestions.map((question, idx) => ( ))} )} {/* 输入框 */} setInputValue(e.target.value)} onKeyPress={handleKeyPress} placeholder={isMobile ? "输入问题..." : "输入你的问题,我会进行深度分析..."} bg="rgba(255, 255, 255, 0.05)" border="1px solid" borderColor={borderGold} color={textWhite} _placeholder={{ color: textGray }} _focus={{ borderColor: goldAccent, boxShadow: `0 0 0 1px ${goldAccent}` }} mr={2} disabled={isProcessing} size={{ base: "md", md: "lg" }} /> {/* 右侧配置面板 */} {/* 模型选择 */} 选择模型 {AVAILABLE_MODELS.map((model) => ( setSelectedModel(model.id)} transition="all 0.2s" _hover={{ bg: 'rgba(255, 215, 0, 0.1)', borderColor: goldAccent, transform: 'translateX(4px)', }} position="relative" > {model.recommended && ( 推荐 )} {model.icon} {model.name} {model.description} {selectedModel === model.id && ( )} ))} {/* 工具选择 */} MCP 工具 {selectedTools.length} 个已选 {MCP_TOOL_CATEGORIES.map((category, catIdx) => { const categoryToolIds = category.tools.map((t) => t.id); const selectedInCategory = categoryToolIds.filter((id) => selectedTools.includes(id)); const isAllSelected = selectedInCategory.length === categoryToolIds.length; const isSomeSelected = selectedInCategory.length > 0 && !isAllSelected; return ( {category.icon} {category.name} {selectedInCategory.length}/{category.tools.length} {/* 全选按钮 */} {category.tools.map((tool) => ( handleToolToggle(tool.id, e.target.checked)} colorScheme="yellow" size="sm" sx={{ '.chakra-checkbox__control': { borderColor: borderGold, bg: 'rgba(255, 255, 255, 0.05)', _checked: { bg: goldGradient, borderColor: goldAccent, }, }, }} > {tool.name} {tool.description} ))} ); })} ); }; /** * 消息渲染器(深灰毛玻璃风格) */ const MessageRenderer = ({ message, userAvatar }) => { const glassBg = 'rgba(30, 35, 40, 0.85)'; // 深灰色毛玻璃 const cardBg = 'rgba(40, 45, 50, 0.6)'; // 卡片背景 const goldAccent = '#FFD700'; const goldGradient = 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)'; const darkBg = '#1a1d23'; const borderGold = 'rgba(255, 215, 0, 0.3)'; const textWhite = '#E8E8E8'; // 柔和的白色 const textGray = '#9BA1A6'; // 柔和的灰色 switch (message.type) { case MessageTypes.USER: return ( {message.content} } border="2px solid" borderColor={goldAccent} /> ); case MessageTypes.AGENT_THINKING: return ( {message.content} ); case MessageTypes.AGENT_PLAN: return ( ); case MessageTypes.AGENT_EXECUTING: return ( {message.stepResults?.map((result, idx) => ( ))} ); case MessageTypes.AGENT_RESPONSE: return ( {/* 最终总结 */} {message.isStreaming ? ( {message.content} ) : ( )} {message.metadata && ( 总步骤: {message.metadata.total_steps} ✓ {message.metadata.successful_steps} {message.metadata.failed_steps > 0 && ( ✗ {message.metadata.failed_steps} )} 耗时: {message.metadata.total_execution_time?.toFixed(1)}s )} {/* 执行详情(可选) */} {message.plan && message.stepResults && message.stepResults.length > 0 && ( 📊 执行详情(点击展开查看) {message.stepResults.map((result, idx) => ( ))} )} ); case MessageTypes.ERROR: return ( {message.content} ); default: return null; } }; export default AgentChatV4;