agent功能开发增加MCP后端

This commit is contained in:
2025-11-07 18:11:29 +08:00
parent a1c76a257c
commit e7ba8c4c2d
11 changed files with 936 additions and 1 deletions

View File

@@ -2958,6 +2958,29 @@ refactor(components): 将 EventCard 拆分为原子组件
> - **Community 页面**: [docs/Community.md](./docs/Community.md) - 页面架构、组件结构、数据流、变更历史 > - **Community 页面**: [docs/Community.md](./docs/Community.md) - 页面架构、组件结构、数据流、变更历史
> - **其他页面**: 根据需要创建独立的页面文档 > - **其他页面**: 根据需要创建独立的页面文档
### 2025-10-30: EventList.js 组件化重构
**影响范围**: Community 页面核心组件
**重构成果**:
- 将 1095 行的 `EventList.js` 拆分为 497 行主组件 + 10 个子组件
- 代码行数减少 **54.6%** (598 行)
- 创建了 7 个原子组件 (Atoms) 和 2 个组合组件 (Molecules)
**新增组件**:
- `EventCard/` - 统一入口,智能路由紧凑/详细模式
- `CompactEventCard.js` - 紧凑模式事件卡片
- `DetailedEventCard.js` - 详细模式事件卡片
- 7 个原子组件: EventTimeline, EventImportanceBadge, EventStats, EventFollowButton, EventPriceDisplay, EventDescription, EventHeader
**新增工具函数**:
- `src/utils/priceFormatters.js` - 价格格式化工具 (getPriceChangeColor, formatPriceChange, PriceArrow)
- `src/constants/animations.js` - 动画常量 (pulseAnimation, fadeIn, slideInUp)
**优势**: 提高了代码可维护性、可复用性、可测试性和性能
**详细文档**: 参见 [docs/Community.md](./docs/Community.md)
--- ---
## 更新本文档 ## 更新本文档

View File

@@ -0,0 +1,376 @@
// src/components/ChatBot/ChatInterface.js
// 聊天界面主组件
import React, { useState, useRef, useEffect } from 'react';
import {
Box,
Flex,
Input,
IconButton,
VStack,
HStack,
Text,
Spinner,
useColorModeValue,
useToast,
Divider,
Badge,
Menu,
MenuButton,
MenuList,
MenuItem,
Button,
} from '@chakra-ui/react';
import { FiSend, FiRefreshCw, FiSettings, FiDownload } from 'react-icons/fi';
import { ChevronDownIcon } from '@chakra-ui/icons';
import MessageBubble from './MessageBubble';
import { mcpService } from '../../services/mcpService';
import { logger } from '../../utils/logger';
/**
* 聊天界面组件
*/
export const ChatInterface = () => {
const [messages, setMessages] = useState([
{
id: 1,
content: '你好我是AI投资助手我可以帮你查询股票信息、新闻资讯、概念板块、涨停分析等。请问有什么可以帮到你的',
isUser: false,
type: 'text',
timestamp: new Date().toISOString(),
},
]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [availableTools, setAvailableTools] = useState([]);
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
const toast = useToast();
// 颜色主题
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const inputBg = useColorModeValue('gray.50', 'gray.700');
// 加载可用工具列表
useEffect(() => {
const loadTools = async () => {
const result = await mcpService.listTools();
if (result.success) {
setAvailableTools(result.data);
logger.info('ChatInterface', '已加载MCP工具', { count: result.data.length });
}
};
loadTools();
}, []);
// 自动滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// 发送消息
const handleSendMessage = async () => {
if (!inputValue.trim() || isLoading) return;
const userMessage = {
id: Date.now(),
content: inputValue,
isUser: true,
type: 'text',
timestamp: new Date().toISOString(),
};
setMessages((prev) => [...prev, userMessage]);
setInputValue('');
setIsLoading(true);
try {
// 调用MCP服务
const response = await mcpService.chat(inputValue, messages);
let botMessage;
if (response.success) {
// 根据返回的数据类型构造消息
const data = response.data;
if (typeof data === 'string') {
botMessage = {
id: Date.now() + 1,
content: data,
isUser: false,
type: 'text',
timestamp: new Date().toISOString(),
};
} else if (Array.isArray(data)) {
// 数据列表
botMessage = {
id: Date.now() + 1,
content: `找到 ${data.length} 条结果:`,
isUser: false,
type: 'data',
data: data,
timestamp: new Date().toISOString(),
};
} else if (typeof data === 'object') {
// 对象数据
botMessage = {
id: Date.now() + 1,
content: JSON.stringify(data, null, 2),
isUser: false,
type: 'markdown',
timestamp: new Date().toISOString(),
};
} else {
botMessage = {
id: Date.now() + 1,
content: '抱歉,我无法理解这个查询结果。',
isUser: false,
type: 'text',
timestamp: new Date().toISOString(),
};
}
} else {
botMessage = {
id: Date.now() + 1,
content: `抱歉,查询失败:${response.error}`,
isUser: false,
type: 'text',
timestamp: new Date().toISOString(),
};
}
setMessages((prev) => [...prev, botMessage]);
} catch (error) {
logger.error('ChatInterface', 'handleSendMessage', error);
const errorMessage = {
id: Date.now() + 1,
content: `抱歉,发生了错误:${error.message}`,
isUser: false,
type: 'text',
timestamp: new Date().toISOString(),
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
inputRef.current?.focus();
}
};
// 处理键盘事件
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
// 清空对话
const handleClearChat = () => {
setMessages([
{
id: 1,
content: '对话已清空。有什么可以帮到你的?',
isUser: false,
type: 'text',
timestamp: new Date().toISOString(),
},
]);
};
// 复制消息
const handleCopyMessage = () => {
toast({
title: '已复制',
status: 'success',
duration: 2000,
isClosable: true,
});
};
// 反馈
const handleFeedback = (type) => {
logger.info('ChatInterface', 'Feedback', { type });
toast({
title: type === 'positive' ? '感谢反馈!' : '我们会改进',
status: 'info',
duration: 2000,
isClosable: true,
});
};
// 快捷问题
const quickQuestions = [
'查询贵州茅台的股票信息',
'搜索人工智能相关新闻',
'今日涨停股票有哪些',
'新能源概念板块分析',
];
const handleQuickQuestion = (question) => {
setInputValue(question);
inputRef.current?.focus();
};
// 导出对话
const handleExportChat = () => {
const chatText = messages
.map((msg) => `[${msg.isUser ? '用户' : '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);
};
return (
<Flex direction="column" h="100%" bg={bgColor}>
{/* 头部工具栏 */}
<Flex
px={4}
py={3}
borderBottom="1px"
borderColor={borderColor}
align="center"
justify="space-between"
>
<HStack>
<Text fontWeight="bold" fontSize="lg">AI投资助手</Text>
<Badge colorScheme="green">在线</Badge>
{availableTools.length > 0 && (
<Badge colorScheme="blue">{availableTools.length} 个工具</Badge>
)}
</HStack>
<HStack>
<IconButton
icon={<FiRefreshCw />}
size="sm"
variant="ghost"
aria-label="清空对话"
onClick={handleClearChat}
/>
<IconButton
icon={<FiDownload />}
size="sm"
variant="ghost"
aria-label="导出对话"
onClick={handleExportChat}
/>
<Menu>
<MenuButton
as={IconButton}
icon={<FiSettings />}
size="sm"
variant="ghost"
aria-label="设置"
/>
<MenuList>
<MenuItem>模型设置</MenuItem>
<MenuItem>快捷指令</MenuItem>
<MenuItem>历史记录</MenuItem>
</MenuList>
</Menu>
</HStack>
</Flex>
{/* 消息列表 */}
<Box
flex="1"
overflowY="auto"
px={4}
py={4}
css={{
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
},
'&::-webkit-scrollbar-thumb': {
background: '#CBD5E0',
borderRadius: '4px',
},
}}
>
<VStack spacing={0} align="stretch">
{messages.map((message) => (
<MessageBubble
key={message.id}
message={message}
isUser={message.isUser}
onCopy={handleCopyMessage}
onFeedback={handleFeedback}
/>
))}
{isLoading && (
<Flex justify="flex-start" mb={4}>
<Flex align="center" bg={inputBg} px={4} py={3} borderRadius="lg">
<Spinner size="sm" mr={2} />
<Text fontSize="sm">AI正在思考...</Text>
</Flex>
</Flex>
)}
<div ref={messagesEndRef} />
</VStack>
</Box>
{/* 快捷问题(仅在消息较少时显示) */}
{messages.length <= 2 && (
<Box px={4} py={2}>
<Text fontSize="xs" color="gray.500" mb={2}>快捷问题</Text>
<Flex wrap="wrap" gap={2}>
{quickQuestions.map((question, idx) => (
<Button
key={idx}
size="xs"
variant="outline"
onClick={() => handleQuickQuestion(question)}
>
{question}
</Button>
))}
</Flex>
</Box>
)}
<Divider />
{/* 输入框 */}
<Box px={4} py={3} borderTop="1px" borderColor={borderColor}>
<Flex>
<Input
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入消息... (Shift+Enter换行Enter发送)"
bg={inputBg}
border="none"
_focus={{ boxShadow: 'none' }}
mr={2}
disabled={isLoading}
/>
<IconButton
icon={<FiSend />}
colorScheme="blue"
aria-label="发送"
onClick={handleSendMessage}
isLoading={isLoading}
disabled={!inputValue.trim()}
/>
</Flex>
</Box>
</Flex>
);
};
export default ChatInterface;

View File

@@ -0,0 +1,149 @@
// src/components/ChatBot/MessageBubble.js
// 聊天消息气泡组件
import React from 'react';
import {
Box,
Flex,
Text,
Avatar,
useColorModeValue,
IconButton,
HStack,
Code,
Badge,
VStack,
} from '@chakra-ui/react';
import { FiCopy, FiThumbsUp, FiThumbsDown } from 'react-icons/fi';
import ReactMarkdown from 'react-markdown';
/**
* 消息气泡组件
* @param {Object} props
* @param {Object} props.message - 消息对象
* @param {boolean} props.isUser - 是否是用户消息
* @param {Function} props.onCopy - 复制消息回调
* @param {Function} props.onFeedback - 反馈回调
*/
export const MessageBubble = ({ message, isUser, onCopy, onFeedback }) => {
const userBg = useColorModeValue('blue.500', 'blue.600');
const botBg = useColorModeValue('gray.100', 'gray.700');
const userColor = 'white';
const botColor = useColorModeValue('gray.800', 'white');
const handleCopy = () => {
navigator.clipboard.writeText(message.content);
onCopy?.();
};
return (
<Flex
w="100%"
justify={isUser ? 'flex-end' : 'flex-start'}
mb={4}
>
<Flex
maxW="75%"
flexDirection={isUser ? 'row-reverse' : 'row'}
align="flex-start"
>
{/* 头像 */}
<Avatar
size="sm"
name={isUser ? '用户' : 'AI助手'}
bg={isUser ? 'blue.500' : 'green.500'}
color="white"
mx={3}
/>
{/* 消息内容 */}
<Box>
<Box
bg={isUser ? userBg : botBg}
color={isUser ? userColor : botColor}
px={4}
py={3}
borderRadius="lg"
boxShadow="sm"
>
{message.type === 'text' ? (
<Text fontSize="sm" whiteSpace="pre-wrap">
{message.content}
</Text>
) : message.type === 'markdown' ? (
<Box fontSize="sm" className="markdown-content">
<ReactMarkdown>{message.content}</ReactMarkdown>
</Box>
) : message.type === 'data' ? (
<VStack align="stretch" spacing={2}>
{message.data && Array.isArray(message.data) && message.data.slice(0, 5).map((item, idx) => (
<Box
key={idx}
p={3}
bg={useColorModeValue('white', 'gray.600')}
borderRadius="md"
fontSize="xs"
>
{Object.entries(item).map(([key, value]) => (
<Flex key={key} justify="space-between" mb={1}>
<Text fontWeight="bold" mr={2}>{key}:</Text>
<Text>{String(value)}</Text>
</Flex>
))}
</Box>
))}
{message.data && message.data.length > 5 && (
<Badge colorScheme="blue" alignSelf="center">
+{message.data.length - 5} 更多结果
</Badge>
)}
</VStack>
) : null}
</Box>
{/* 消息操作按钮仅AI消息 */}
{!isUser && (
<HStack mt={2} spacing={2}>
<IconButton
icon={<FiCopy />}
size="xs"
variant="ghost"
aria-label="复制"
onClick={handleCopy}
/>
<IconButton
icon={<FiThumbsUp />}
size="xs"
variant="ghost"
aria-label="赞"
onClick={() => onFeedback?.('positive')}
/>
<IconButton
icon={<FiThumbsDown />}
size="xs"
variant="ghost"
aria-label="踩"
onClick={() => onFeedback?.('negative')}
/>
</HStack>
)}
{/* 时间戳 */}
<Text
fontSize="xs"
color="gray.500"
mt={1}
textAlign={isUser ? 'right' : 'left'}
>
{message.timestamp ? new Date(message.timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
}) : ''}
</Text>
</Box>
</Flex>
</Flex>
);
};
export default MessageBubble;

View File

@@ -0,0 +1,7 @@
// src/components/ChatBot/index.js
// 聊天机器人组件统一导出
export { ChatInterface } from './ChatInterface';
export { MessageBubble } from './MessageBubble';
export { ChatInterface as default } from './ChatInterface';

View File

@@ -243,6 +243,26 @@ const MobileDrawer = memo(({
<Box> <Box>
<Text fontWeight="bold" mb={2}>AGENT社群</Text> <Text fontWeight="bold" mb={2}>AGENT社群</Text>
<VStack spacing={2} align="stretch"> <VStack spacing={2} align="stretch">
<Link
onClick={() => handleNavigate('/agent-chat')}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
bg={location.pathname.includes('/agent-chat') ? 'blue.50' : 'transparent'}
borderLeft={location.pathname.includes('/agent-chat') ? '3px solid' : 'none'}
borderColor="blue.600"
fontWeight={location.pathname.includes('/agent-chat') ? 'bold' : 'normal'}
>
<HStack justify="space-between">
<Text fontSize="sm">AI聊天助手</Text>
<HStack spacing={1}>
<Badge size="xs" colorScheme="green">AI</Badge>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</HStack>
</Link>
<Link <Link
py={1} py={1}
px={3} px={3}

View File

@@ -199,6 +199,12 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
as={Button} as={Button}
variant="ghost" variant="ghost"
rightIcon={<ChevronDownIcon />} rightIcon={<ChevronDownIcon />}
bg={isActive(['/agent-chat']) ? 'blue.50' : 'transparent'}
color={isActive(['/agent-chat']) ? 'blue.600' : 'inherit'}
fontWeight={isActive(['/agent-chat']) ? 'bold' : 'normal'}
borderBottom={isActive(['/agent-chat']) ? '2px solid' : 'none'}
borderColor="blue.600"
_hover={{ bg: isActive(['/agent-chat']) ? 'blue.100' : 'gray.50' }}
onMouseEnter={agentCommunityMenu.handleMouseEnter} onMouseEnter={agentCommunityMenu.handleMouseEnter}
onMouseLeave={agentCommunityMenu.handleMouseLeave} onMouseLeave={agentCommunityMenu.handleMouseLeave}
onClick={agentCommunityMenu.handleClick} onClick={agentCommunityMenu.handleClick}
@@ -207,10 +213,31 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
</MenuButton> </MenuButton>
<MenuList <MenuList
minW="300px" minW="300px"
p={4} p={2}
onMouseEnter={agentCommunityMenu.handleMouseEnter} onMouseEnter={agentCommunityMenu.handleMouseEnter}
onMouseLeave={agentCommunityMenu.handleMouseLeave} onMouseLeave={agentCommunityMenu.handleMouseLeave}
> >
<MenuItem
onClick={() => {
// 🎯 追踪菜单项点击
navEvents.trackMenuItemClicked('AI聊天助手', 'dropdown', '/agent-chat');
navigate('/agent-chat');
agentCommunityMenu.onClose(); // 跳转后关闭菜单
}}
borderRadius="md"
bg={location.pathname.includes('/agent-chat') ? 'blue.50' : 'transparent'}
borderLeft={location.pathname.includes('/agent-chat') ? '3px solid' : 'none'}
borderColor="blue.600"
fontWeight={location.pathname.includes('/agent-chat') ? 'bold' : 'normal'}
>
<Flex justify="space-between" align="center" w="100%">
<Text fontSize="sm">AI聊天助手</Text>
<HStack spacing={1}>
<Badge size="sm" colorScheme="green">AI</Badge>
<Badge size="sm" colorScheme="red">NEW</Badge>
</HStack>
</Flex>
</MenuItem>
<MenuItem <MenuItem
isDisabled isDisabled
cursor="not-allowed" cursor="not-allowed"

View File

@@ -139,6 +139,22 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
{/* AGENT社群组 */} {/* AGENT社群组 */}
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">AGENT社群</Text> <Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">AGENT社群</Text>
<MenuItem
onClick={() => {
moreMenu.onClose(); // 先关闭菜单
navigate('/agent-chat');
}}
borderRadius="md"
bg={location.pathname.includes('/agent-chat') ? 'blue.50' : 'transparent'}
>
<Flex justify="space-between" align="center" w="100%">
<Text fontSize="sm">AI聊天助手</Text>
<HStack spacing={1}>
<Badge size="sm" colorScheme="green">AI</Badge>
<Badge size="sm" colorScheme="red">NEW</Badge>
</HStack>
</Flex>
</MenuItem>
<MenuItem isDisabled cursor="not-allowed" color="gray.400"> <MenuItem isDisabled cursor="not-allowed" color="gray.400">
<Text fontSize="sm" color="gray.400">今日热议</Text> <Text fontSize="sm" color="gray.400">今日热议</Text>
</MenuItem> </MenuItem>

View File

@@ -35,6 +35,9 @@ export const lazyComponents = {
ForecastReport: React.lazy(() => import('../views/Company/ForecastReport')), ForecastReport: React.lazy(() => import('../views/Company/ForecastReport')),
FinancialPanorama: React.lazy(() => import('../views/Company/FinancialPanorama')), FinancialPanorama: React.lazy(() => import('../views/Company/FinancialPanorama')),
MarketDataView: React.lazy(() => import('../views/Company/MarketDataView')), MarketDataView: React.lazy(() => import('../views/Company/MarketDataView')),
// Agent模块
AgentChat: React.lazy(() => import('../views/AgentChat')),
}; };
/** /**
@@ -59,4 +62,5 @@ export const {
ForecastReport, ForecastReport,
FinancialPanorama, FinancialPanorama,
MarketDataView, MarketDataView,
AgentChat,
} = lazyComponents; } = lazyComponents;

View File

@@ -149,6 +149,18 @@ export const routeConfig = [
description: '实时市场数据' description: '实时市场数据'
} }
}, },
// ==================== Agent模块 ====================
{
path: 'agent-chat',
component: lazyComponents.AgentChat,
protection: PROTECTION_MODES.MODAL,
layout: 'main',
meta: {
title: 'AI投资助手',
description: '基于MCP的智能投资顾问'
}
},
]; ];
/** /**

248
src/services/mcpService.js Normal file
View File

@@ -0,0 +1,248 @@
// src/services/mcpService.js
// MCP (Model Context Protocol) 服务层
// 用于与FastAPI后端的MCP工具进行交互
import axios from 'axios';
import { getApiBase } from '../utils/apiConfig';
import { logger } from '../utils/logger';
/**
* MCP API客户端
*/
class MCPService {
constructor() {
this.baseURL = `${getApiBase()}/mcp`;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 60000, // 60秒超时MCP工具可能需要较长时间
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
logger.debug('MCPService', 'Request', {
url: config.url,
method: config.method,
data: config.data,
});
return config;
},
(error) => {
logger.error('MCPService', 'Request Error', error);
return Promise.reject(error);
}
);
// 响应拦截器
this.client.interceptors.response.use(
(response) => {
logger.debug('MCPService', 'Response', {
url: response.config.url,
status: response.status,
data: response.data,
});
return response.data;
},
(error) => {
logger.error('MCPService', 'Response Error', {
url: error.config?.url,
status: error.response?.status,
message: error.message,
});
return Promise.reject(error);
}
);
}
/**
* 列出所有可用的MCP工具
* @returns {Promise<Object>} 工具列表
*/
async listTools() {
try {
const response = await this.client.get('/tools');
return {
success: true,
data: response.tools || [],
};
} catch (error) {
return {
success: false,
error: error.message || '获取工具列表失败',
};
}
}
/**
* 获取特定工具的定义
* @param {string} toolName - 工具名称
* @returns {Promise<Object>} 工具定义
*/
async getTool(toolName) {
try {
const response = await this.client.get(`/tools/${toolName}`);
return {
success: true,
data: response,
};
} catch (error) {
return {
success: false,
error: error.message || '获取工具定义失败',
};
}
}
/**
* 调用MCP工具
* @param {string} toolName - 工具名称
* @param {Object} arguments - 工具参数
* @returns {Promise<Object>} 工具执行结果
*/
async callTool(toolName, toolArguments) {
try {
const response = await this.client.post('/tools/call', {
tool: toolName,
arguments: toolArguments,
});
return {
success: true,
data: response.data || response,
};
} catch (error) {
return {
success: false,
error: error.response?.data?.detail || error.message || '工具调用失败',
};
}
}
/**
* 智能对话 - 根据用户输入自动选择合适的工具
* @param {string} userMessage - 用户消息
* @param {Array} conversationHistory - 对话历史(可选)
* @returns {Promise<Object>} AI响应
*/
async chat(userMessage, conversationHistory = []) {
try {
// 这里可以实现智能路由逻辑
// 根据用户输入判断应该调用哪个工具
// 示例:关键词匹配
if (userMessage.includes('新闻') || userMessage.includes('资讯')) {
return await this.callTool('search_china_news', {
query: userMessage.replace(/新闻|资讯/g, '').trim(),
top_k: 5,
});
} else if (userMessage.includes('概念') || userMessage.includes('板块')) {
const query = userMessage.replace(/概念|板块/g, '').trim();
return await this.callTool('search_concepts', {
query,
size: 5,
sort_by: 'change_pct',
});
} else if (userMessage.includes('涨停')) {
const query = userMessage.replace(/涨停/g, '').trim();
return await this.callTool('search_limit_up_stocks', {
query,
mode: 'hybrid',
page_size: 5,
});
} else if (/^[0-9]{6}$/.test(userMessage.trim())) {
// 6位数字 = 股票代码
return await this.callTool('get_stock_basic_info', {
seccode: userMessage.trim(),
});
} else {
// 默认:搜索新闻
return await this.callTool('search_china_news', {
query: userMessage,
top_k: 5,
});
}
} catch (error) {
return {
success: false,
error: error.message || '对话处理失败',
};
}
}
/**
* 工具类别枚举
*/
static TOOL_CATEGORIES = {
NEWS: 'news', // 新闻搜索
STOCK: 'stock', // 股票信息
CONCEPT: 'concept', // 概念板块
LIMIT_UP: 'limit_up', // 涨停分析
RESEARCH: 'research', // 研报搜索
ROADSHOW: 'roadshow', // 路演信息
FINANCIAL: 'financial', // 财务数据
TRADE: 'trade', // 交易数据
};
/**
* 常用工具快捷方式
*/
async searchNews(query, topK = 5, exactMatch = false) {
return await this.callTool('search_china_news', {
query,
top_k: topK,
exact_match: exactMatch,
});
}
async searchConcepts(query, size = 10, sortBy = 'change_pct') {
return await this.callTool('search_concepts', {
query,
size,
sort_by: sortBy,
});
}
async searchLimitUpStocks(query, mode = 'hybrid', pageSize = 10) {
return await this.callTool('search_limit_up_stocks', {
query,
mode,
page_size: pageSize,
});
}
async getStockInfo(seccode) {
return await this.callTool('get_stock_basic_info', {
seccode,
});
}
async getStockConcepts(stockCode, size = 10) {
return await this.callTool('get_stock_concepts', {
stock_code: stockCode,
size,
});
}
async searchResearchReports(query, mode = 'hybrid', size = 5) {
return await this.callTool('search_research_reports', {
query,
mode,
size,
});
}
async getConceptStatistics(days = 7, minStockCount = 3) {
return await this.callTool('get_concept_statistics', {
days,
min_stock_count: minStockCount,
});
}
}
// 导出单例实例
export const mcpService = new MCPService();
// 导出类(供测试使用)
export default MCPService;

View File

@@ -0,0 +1,53 @@
// src/views/AgentChat/index.js
// Agent聊天页面
import React from 'react';
import {
Box,
Container,
Heading,
Text,
VStack,
useColorModeValue,
} from '@chakra-ui/react';
import ChatInterface from '../../components/ChatBot';
/**
* Agent聊天页面
* 提供基于MCP的AI助手对话功能
*/
const AgentChat = () => {
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
return (
<Box minH="calc(100vh - 200px)" bg={bgColor} py={8}>
<Container maxW="container.xl" h="100%">
<VStack spacing={6} align="stretch" h="100%">
{/* 页面标题 */}
<Box>
<Heading size="lg" mb={2}>AI投资助手</Heading>
<Text color="gray.600" fontSize="sm">
基于MCP协议的智能投资顾问支持股票查询新闻搜索概念分析等多种功能
</Text>
</Box>
{/* 聊天界面 */}
<Box
flex="1"
bg={cardBg}
borderRadius="xl"
boxShadow="xl"
overflow="hidden"
h="calc(100vh - 300px)"
minH="600px"
>
<ChatInterface />
</Box>
</VStack>
</Container>
</Box>
);
};
export default AgentChat;