agent功能开发增加MCP后端

This commit is contained in:
2025-11-07 19:11:58 +08:00
parent 446d8f0870
commit 315d606945
15 changed files with 3639 additions and 4 deletions

View File

@@ -0,0 +1,576 @@
// src/components/ChatBot/ChatInterfaceV2.js
// 重新设计的聊天界面 - 更漂亮、支持Agent模式
import React, { useState, useRef, useEffect } from 'react';
import {
Box,
Flex,
Input,
IconButton,
VStack,
HStack,
Text,
Spinner,
useColorModeValue,
useToast,
Divider,
Badge,
Button,
Avatar,
Heading,
Progress,
Fade,
} from '@chakra-ui/react';
import { FiSend, FiRefreshCw, FiDownload, FiCpu, FiUser, FiZap } from 'react-icons/fi';
import { PlanCard } from './PlanCard';
import { StepResultCard } from './StepResultCard';
import { mcpService } from '../../services/mcpService';
import { logger } from '../../utils/logger';
/**
* Agent消息类型
*/
const MessageTypes = {
USER: 'user',
AGENT_THINKING: 'agent_thinking',
AGENT_PLAN: 'agent_plan',
AGENT_EXECUTING: 'agent_executing',
AGENT_RESPONSE: 'agent_response',
ERROR: 'error',
};
/**
* 聊天界面V2组件 - Agent模式
*/
export const ChatInterfaceV2 = () => {
const [messages, setMessages] = useState([
{
id: 1,
type: MessageTypes.AGENT_RESPONSE,
content: '你好我是AI投资研究助手。我会通过多步骤分析来帮助你深入了解金融市场。\n\n你可以问我\n• 全面分析某只股票\n• 某个行业的投资机会\n• 今日市场热点\n• 某个概念板块的表现',
timestamp: new Date().toISOString(),
},
]);
const [inputValue, setInputValue] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [currentProgress, setCurrentProgress] = useState(0);
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
const toast = useToast();
// 颜色主题
const bgColor = useColorModeValue('gray.50', 'gray.900');
const chatBg = useColorModeValue('white', 'gray.800');
const inputBg = useColorModeValue('white', 'gray.700');
const userBubbleBg = useColorModeValue('blue.500', 'blue.600');
const agentBubbleBg = useColorModeValue('white', 'gray.700');
const borderColor = useColorModeValue('gray.200', 'gray.600');
// 自动滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// 添加消息
const addMessage = (message) => {
setMessages((prev) => [...prev, { ...message, id: Date.now() }]);
};
// 更新最后一条消息
const updateLastMessage = (updates) => {
setMessages((prev) => {
const newMessages = [...prev];
if (newMessages.length > 0) {
newMessages[newMessages.length - 1] = {
...newMessages[newMessages.length - 1],
...updates,
};
}
return newMessages;
});
};
// 发送消息Agent模式
const handleSendMessage = async () => {
if (!inputValue.trim() || isProcessing) return;
const userMessage = {
type: MessageTypes.USER,
content: inputValue,
timestamp: new Date().toISOString(),
};
addMessage(userMessage);
setInputValue('');
setIsProcessing(true);
setCurrentProgress(0);
try {
// 1. 显示思考状态
addMessage({
type: MessageTypes.AGENT_THINKING,
content: '正在分析你的问题...',
timestamp: new Date().toISOString(),
});
setCurrentProgress(10);
// 调用 Agent API
const response = await fetch(`${mcpService.baseURL.replace('/mcp', '')}/mcp/agent/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: inputValue,
conversation_history: messages.filter(m => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE).map(m => ({
isUser: m.type === MessageTypes.USER,
content: m.content,
})),
}),
});
if (!response.ok) {
throw new Error('Agent请求失败');
}
const agentResponse = await response.json();
logger.info('Agent response', agentResponse);
// 移除思考消息
setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_THINKING));
if (!agentResponse.success) {
throw new Error(agentResponse.message || '处理失败');
}
setCurrentProgress(30);
// 2. 显示执行计划
if (agentResponse.plan) {
addMessage({
type: MessageTypes.AGENT_PLAN,
content: '已制定执行计划',
plan: agentResponse.plan,
timestamp: new Date().toISOString(),
});
}
setCurrentProgress(40);
// 3. 显示执行过程
if (agentResponse.step_results && agentResponse.step_results.length > 0) {
addMessage({
type: MessageTypes.AGENT_EXECUTING,
content: '正在执行步骤...',
plan: agentResponse.plan,
stepResults: agentResponse.step_results,
timestamp: new Date().toISOString(),
});
// 模拟进度更新
for (let i = 0; i < agentResponse.step_results.length; i++) {
setCurrentProgress(40 + (i + 1) / agentResponse.step_results.length * 50);
await new Promise(resolve => setTimeout(resolve, 100));
}
}
setCurrentProgress(100);
// 移除执行中消息
setMessages(prev => prev.filter(m => m.type !== MessageTypes.AGENT_EXECUTING));
// 4. 显示最终结果
addMessage({
type: MessageTypes.AGENT_RESPONSE,
content: agentResponse.message || agentResponse.final_summary,
plan: agentResponse.plan,
stepResults: agentResponse.step_results,
metadata: agentResponse.metadata,
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error('Agent chat error', error);
// 移除思考/执行中消息
setMessages(prev => prev.filter(
m => m.type !== MessageTypes.AGENT_THINKING && m.type !== MessageTypes.AGENT_EXECUTING
));
addMessage({
type: MessageTypes.ERROR,
content: `处理失败:${error.message}`,
timestamp: new Date().toISOString(),
});
toast({
title: '处理失败',
description: error.message,
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setIsProcessing(false);
setCurrentProgress(0);
inputRef.current?.focus();
}
};
// 处理键盘事件
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
// 清空对话
const handleClearChat = () => {
setMessages([
{
id: 1,
type: MessageTypes.AGENT_RESPONSE,
content: '对话已清空。有什么可以帮到你的?',
timestamp: new Date().toISOString(),
},
]);
};
// 导出对话
const handleExportChat = () => {
const chatText = messages
.filter(m => m.type === MessageTypes.USER || m.type === MessageTypes.AGENT_RESPONSE)
.map((msg) => `[${msg.type === MessageTypes.USER ? '用户' : 'AI助手'}] ${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 quickQuestions = [
'全面分析贵州茅台这只股票',
'今日涨停股票有哪些亮点',
'新能源概念板块的投资机会',
'半导体行业最新动态',
];
return (
<Flex direction="column" h="100%" bg={bgColor}>
{/* 头部 */}
<Box
bg={chatBg}
borderBottom="1px"
borderColor={borderColor}
px={6}
py={4}
>
<HStack justify="space-between">
<HStack spacing={4}>
<Avatar
size="md"
bg="blue.500"
icon={<FiCpu fontSize="1.5rem" />}
/>
<VStack align="start" spacing={0}>
<Heading size="md">AI投资研究助手</Heading>
<HStack>
<Badge colorScheme="green" fontSize="xs">
<HStack spacing={1}>
<FiZap size={10} />
<span>智能分析</span>
</HStack>
</Badge>
<Text fontSize="xs" color="gray.500">
多步骤深度研究
</Text>
</HStack>
</VStack>
</HStack>
<HStack>
<IconButton
icon={<FiRefreshCw />}
size="sm"
variant="ghost"
aria-label="清空对话"
onClick={handleClearChat}
/>
<IconButton
icon={<FiDownload />}
size="sm"
variant="ghost"
aria-label="导出对话"
onClick={handleExportChat}
/>
</HStack>
</HStack>
{/* 进度条 */}
{isProcessing && (
<Progress
value={currentProgress}
size="xs"
colorScheme="blue"
mt={3}
borderRadius="full"
isAnimated
/>
)}
</Box>
{/* 消息列表 */}
<Box
flex="1"
overflowY="auto"
px={6}
py={4}
css={{
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
},
'&::-webkit-scrollbar-thumb': {
background: '#CBD5E0',
borderRadius: '4px',
},
}}
>
<VStack spacing={4} align="stretch">
{messages.map((message) => (
<Fade in key={message.id}>
<MessageRenderer message={message} />
</Fade>
))}
<div ref={messagesEndRef} />
</VStack>
</Box>
{/* 快捷问题 */}
{messages.length <= 2 && !isProcessing && (
<Box px={6} py={3} bg={chatBg} borderTop="1px" borderColor={borderColor}>
<Text fontSize="xs" color="gray.500" mb={2}>💡 试试这些问题</Text>
<Flex wrap="wrap" gap={2}>
{quickQuestions.map((question, idx) => (
<Button
key={idx}
size="sm"
variant="outline"
colorScheme="blue"
fontSize="xs"
onClick={() => {
setInputValue(question);
inputRef.current?.focus();
}}
>
{question}
</Button>
))}
</Flex>
</Box>
)}
{/* 输入框 */}
<Box px={6} py={4} bg={chatBg} borderTop="1px" borderColor={borderColor}>
<Flex>
<Input
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入你的问题,我会进行深度分析..."
bg={inputBg}
border="1px"
borderColor={borderColor}
_focus={{ borderColor: 'blue.500', boxShadow: '0 0 0 1px #3182CE' }}
mr={2}
disabled={isProcessing}
size="lg"
/>
<IconButton
icon={isProcessing ? <Spinner size="sm" /> : <FiSend />}
colorScheme="blue"
aria-label="发送"
onClick={handleSendMessage}
isLoading={isProcessing}
disabled={!inputValue.trim() || isProcessing}
size="lg"
/>
</Flex>
</Box>
</Flex>
);
};
/**
* 消息渲染器
*/
const MessageRenderer = ({ message }) => {
const userBubbleBg = useColorModeValue('blue.500', 'blue.600');
const agentBubbleBg = useColorModeValue('white', 'gray.700');
const borderColor = useColorModeValue('gray.200', 'gray.600');
switch (message.type) {
case MessageTypes.USER:
return (
<Flex justify="flex-end">
<HStack align="flex-start" maxW="75%">
<Box
bg={userBubbleBg}
color="white"
px={4}
py={3}
borderRadius="lg"
boxShadow="md"
>
<Text fontSize="sm" whiteSpace="pre-wrap">
{message.content}
</Text>
</Box>
<Avatar size="sm" bg="blue.500" icon={<FiUser fontSize="1rem" />} />
</HStack>
</Flex>
);
case MessageTypes.AGENT_THINKING:
return (
<Flex justify="flex-start">
<HStack align="flex-start" maxW="75%">
<Avatar size="sm" bg="purple.500" icon={<FiCpu fontSize="1rem" />} />
<Box
bg={agentBubbleBg}
px={4}
py={3}
borderRadius="lg"
borderWidth="1px"
borderColor={borderColor}
boxShadow="sm"
>
<HStack>
<Spinner size="sm" color="purple.500" />
<Text fontSize="sm" color="purple.600">
{message.content}
</Text>
</HStack>
</Box>
</HStack>
</Flex>
);
case MessageTypes.AGENT_PLAN:
return (
<Flex justify="flex-start">
<HStack align="flex-start" maxW="85%">
<Avatar size="sm" bg="blue.500" icon={<FiCpu fontSize="1rem" />} />
<VStack align="stretch" flex={1}>
<PlanCard plan={message.plan} stepResults={[]} />
</VStack>
</HStack>
</Flex>
);
case MessageTypes.AGENT_EXECUTING:
return (
<Flex justify="flex-start">
<HStack align="flex-start" maxW="85%">
<Avatar size="sm" bg="orange.500" icon={<FiCpu fontSize="1rem" />} />
<VStack align="stretch" flex={1} spacing={3}>
<PlanCard plan={message.plan} stepResults={message.stepResults} />
{message.stepResults?.map((result, idx) => (
<StepResultCard key={idx} stepResult={result} />
))}
</VStack>
</HStack>
</Flex>
);
case MessageTypes.AGENT_RESPONSE:
return (
<Flex justify="flex-start">
<HStack align="flex-start" maxW="85%">
<Avatar size="sm" bg="green.500" icon={<FiCpu fontSize="1rem" />} />
<VStack align="stretch" flex={1} spacing={3}>
{/* 最终总结 */}
<Box
bg={agentBubbleBg}
px={4}
py={3}
borderRadius="lg"
borderWidth="1px"
borderColor={borderColor}
boxShadow="md"
>
<Text fontSize="sm" whiteSpace="pre-wrap">
{message.content}
</Text>
{/* 元数据 */}
{message.metadata && (
<HStack mt={3} spacing={4} fontSize="xs" color="gray.500">
<Text>总步骤: {message.metadata.total_steps}</Text>
<Text> {message.metadata.successful_steps}</Text>
{message.metadata.failed_steps > 0 && (
<Text> {message.metadata.failed_steps}</Text>
)}
<Text>耗时: {message.metadata.total_execution_time?.toFixed(1)}s</Text>
</HStack>
)}
</Box>
{/* 执行详情(可选) */}
{message.plan && message.stepResults && message.stepResults.length > 0 && (
<VStack align="stretch" spacing={2}>
<Divider />
<Text fontSize="xs" fontWeight="bold" color="gray.500">
📊 执行详情点击展开查看
</Text>
{message.stepResults.map((result, idx) => (
<StepResultCard key={idx} stepResult={result} />
))}
</VStack>
)}
</VStack>
</HStack>
</Flex>
);
case MessageTypes.ERROR:
return (
<Flex justify="flex-start">
<HStack align="flex-start" maxW="75%">
<Avatar size="sm" bg="red.500" icon={<FiCpu fontSize="1rem" />} />
<Box
bg="red.50"
color="red.700"
px={4}
py={3}
borderRadius="lg"
borderWidth="1px"
borderColor="red.200"
>
<Text fontSize="sm">{message.content}</Text>
</Box>
</HStack>
</Flex>
);
default:
return null;
}
};
export default ChatInterfaceV2;