diff --git a/mcp_server.py b/mcp_server.py
index 371e2825..971a94e5 100644
--- a/mcp_server.py
+++ b/mcp_server.py
@@ -1298,7 +1298,50 @@ class MCPAgentIntegrated:
messages = [
{
"role": "system",
- "content": "你是专业的金融研究助手。根据执行结果,生成简洁清晰的报告。"
+ "content": """你是专业的金融研究助手。根据执行结果,生成简洁清晰的报告。
+
+## 数据可视化能力
+如果执行结果中包含数值型数据(如财务指标、交易数据、时间序列等),你可以使用 ECharts 生成图表来增强报告的可读性。
+
+支持的图表类型:
+- 折线图(line):适合时间序列数据(如股价走势、财务指标趋势)
+- 柱状图(bar):适合对比数据(如不同年份的收入、利润对比)
+- 饼图(pie):适合占比数据(如业务结构、资产分布)
+
+### 图表格式(使用 Markdown 代码块)
+在报告中插入图表时,使用以下格式:
+
+```echarts
+{
+ "title": {"text": "图表标题"},
+ "tooltip": {},
+ "xAxis": {"type": "category", "data": ["类别1", "类别2"]},
+ "yAxis": {"type": "value"},
+ "series": [{"name": "数据系列", "type": "line", "data": [100, 200]}]
+}
+```
+
+### 示例
+如果有股价数据,可以这样呈现:
+
+**股价走势分析**
+
+近30日股价呈现上涨趋势,最高达到1850元。
+
+```echarts
+{
+ "title": {"text": "近30日股价走势", "left": "center"},
+ "tooltip": {"trigger": "axis"},
+ "xAxis": {"type": "category", "data": ["2024-01-01", "2024-01-02", "2024-01-03"]},
+ "yAxis": {"type": "value", "name": "股价(元)"},
+ "series": [{"name": "收盘价", "type": "line", "data": [1800, 1820, 1850], "smooth": true}]
+}
+```
+
+**重要提示**:
+- ECharts 配置必须是合法的 JSON 格式
+- 只在有明确数值数据时才生成图表
+- 不要凭空捏造数据"""
},
{
"role": "user",
@@ -1309,7 +1352,7 @@ class MCPAgentIntegrated:
执行结果:
{results_text}
-请生成专业的分析报告(300字以内)。"""
+请生成专业的分析报告(500字以内)。如果结果中包含数值型数据,请使用 ECharts 图表进行可视化展示。"""
},
]
@@ -1318,7 +1361,7 @@ class MCPAgentIntegrated:
model="kimi-k2-turbo-preview", # 使用非思考模型,更快
messages=messages,
temperature=0.7,
- max_tokens=1000,
+ max_tokens=2000, # 增加 token 限制以支持图表配置
)
summary = response.choices[0].message.content
@@ -1632,7 +1675,7 @@ async def agent_chat(request: AgentChatRequest):
try:
# 将执行步骤转换为JSON字符串
steps_json = json.dumps(
- [{"tool": step.tool, "result": step.result} for step in response.steps],
+ [{"tool": step.tool, "status": step.status, "result": step.result} for step in response.step_results],
ensure_ascii=False
)
@@ -1642,12 +1685,12 @@ async def agent_chat(request: AgentChatRequest):
user_nickname=request.user_nickname or "匿名用户",
user_avatar=request.user_avatar or "",
message_type="assistant",
- message=response.final_answer,
- plan=response.plan,
+ message=response.final_summary, # 使用 final_summary 而不是 final_answer
+ plan=response.plan.dict() if response.plan else None, # 转换为字典
steps=steps_json,
)
except Exception as e:
- logger.error(f"保存 Agent 回复失败: {e}")
+ logger.error(f"保存 Agent 回复失败: {e}", exc_info=True)
# 在响应中返回 session_id
response_dict = response.dict()
diff --git a/src/components/ChatBot/EChartsRenderer.js b/src/components/ChatBot/EChartsRenderer.js
new file mode 100644
index 00000000..396b4ef5
--- /dev/null
+++ b/src/components/ChatBot/EChartsRenderer.js
@@ -0,0 +1,72 @@
+// src/components/ChatBot/EChartsRenderer.js
+// ECharts 图表渲染组件
+
+import React, { useEffect, useRef } from 'react';
+import { Box, useColorModeValue } from '@chakra-ui/react';
+import * as echarts from 'echarts';
+
+/**
+ * ECharts 图表渲染组件
+ * @param {Object} option - ECharts 配置对象
+ * @param {number} height - 图表高度(默认 400px)
+ */
+export const EChartsRenderer = ({ option, height = 400 }) => {
+ const chartRef = useRef(null);
+ const chartInstance = useRef(null);
+ const bgColor = useColorModeValue('white', 'gray.800');
+
+ useEffect(() => {
+ if (!chartRef.current || !option) return;
+
+ // 初始化图表
+ if (!chartInstance.current) {
+ chartInstance.current = echarts.init(chartRef.current);
+ }
+
+ // 设置默认主题配置
+ const defaultOption = {
+ backgroundColor: 'transparent',
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true,
+ },
+ ...option,
+ };
+
+ // 设置图表配置
+ chartInstance.current.setOption(defaultOption, true);
+
+ // 响应式调整大小
+ const handleResize = () => {
+ chartInstance.current?.resize();
+ };
+
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ // chartInstance.current?.dispose(); // 不要销毁,避免重新渲染时闪烁
+ };
+ }, [option]);
+
+ // 组件卸载时销毁图表
+ useEffect(() => {
+ return () => {
+ chartInstance.current?.dispose();
+ chartInstance.current = null;
+ };
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/src/components/ChatBot/MarkdownWithCharts.js b/src/components/ChatBot/MarkdownWithCharts.js
new file mode 100644
index 00000000..6f140d14
--- /dev/null
+++ b/src/components/ChatBot/MarkdownWithCharts.js
@@ -0,0 +1,166 @@
+// src/components/ChatBot/MarkdownWithCharts.js
+// 支持 ECharts 图表的 Markdown 渲染组件
+
+import React from 'react';
+import { Box, Alert, AlertIcon, Text, VStack, Code } from '@chakra-ui/react';
+import ReactMarkdown from 'react-markdown';
+import { EChartsRenderer } from './EChartsRenderer';
+import { logger } from '@utils/logger';
+
+/**
+ * 解析 Markdown 内容,提取 ECharts 代码块
+ * @param {string} markdown - Markdown 文本
+ * @returns {Array} - 包含文本和图表的数组
+ */
+const parseMarkdownWithCharts = (markdown) => {
+ if (!markdown) return [];
+
+ const parts = [];
+ const echartsRegex = /```echarts\s*\n([\s\S]*?)```/g;
+ let lastIndex = 0;
+ let match;
+
+ while ((match = echartsRegex.exec(markdown)) !== null) {
+ // 添加代码块前的文本
+ if (match.index > lastIndex) {
+ const textBefore = markdown.substring(lastIndex, match.index).trim();
+ if (textBefore) {
+ parts.push({ type: 'text', content: textBefore });
+ }
+ }
+
+ // 添加 ECharts 配置
+ const chartConfig = match[1].trim();
+ parts.push({ type: 'chart', content: chartConfig });
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ // 添加剩余文本
+ if (lastIndex < markdown.length) {
+ const textAfter = markdown.substring(lastIndex).trim();
+ if (textAfter) {
+ parts.push({ type: 'text', content: textAfter });
+ }
+ }
+
+ // 如果没有找到图表,返回整个 markdown 作为文本
+ if (parts.length === 0) {
+ parts.push({ type: 'text', content: markdown });
+ }
+
+ return parts;
+};
+
+/**
+ * 支持 ECharts 图表的 Markdown 渲染组件
+ * @param {string} content - Markdown 文本
+ */
+export const MarkdownWithCharts = ({ content }) => {
+ const parts = parseMarkdownWithCharts(content);
+
+ return (
+
+ {parts.map((part, index) => {
+ if (part.type === 'text') {
+ // 渲染普通 Markdown
+ return (
+
+ (
+
+ {children}
+
+ ),
+ h1: ({ children }) => (
+
+ {children}
+
+ ),
+ h2: ({ children }) => (
+
+ {children}
+
+ ),
+ h3: ({ children }) => (
+
+ {children}
+
+ ),
+ ul: ({ children }) => (
+
+ {children}
+
+ ),
+ ol: ({ children }) => (
+
+ {children}
+
+ ),
+ li: ({ children }) => (
+
+ {children}
+
+ ),
+ code: ({ inline, children }) =>
+ inline ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ ),
+ blockquote: ({ children }) => (
+
+ {children}
+
+ ),
+ }}
+ >
+ {part.content}
+
+
+ );
+ } else if (part.type === 'chart') {
+ // 渲染 ECharts 图表
+ try {
+ const chartOption = JSON.parse(part.content);
+ return (
+
+
+
+ );
+ } catch (error) {
+ logger.error('解析 ECharts 配置失败', error, part.content);
+ return (
+
+
+
+
+ 图表配置解析失败
+
+
+ {part.content.substring(0, 200)}
+ {part.content.length > 200 ? '...' : ''}
+
+
+
+ );
+ }
+ }
+ return null;
+ })}
+
+ );
+};
diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js
index 56cd16d1..738e255a 100644
--- a/src/views/AgentChat/index.js
+++ b/src/views/AgentChat/index.js
@@ -53,6 +53,7 @@ import {
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';
@@ -312,25 +313,15 @@ const AgentChatV3 = () => {
}
// 显示执行步骤
- if (data.steps && data.steps.length > 0) {
- stepResults = data.steps;
- addMessage({
- type: MessageTypes.AGENT_EXECUTING,
- content: '正在执行步骤...',
- plan: currentPlan,
- stepResults: stepResults,
- timestamp: new Date().toISOString(),
- });
+ if (data.step_results && data.step_results.length > 0) {
+ stepResults = data.step_results;
setCurrentProgress(70);
}
- // 移除执行中消息
- setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING));
-
- // 显示最终结果
+ // 显示最终结果(包含执行步骤)
addMessage({
type: MessageTypes.AGENT_RESPONSE,
- content: data.final_answer || data.message || '处理完成',
+ content: data.final_summary || data.message || '处理完成',
plan: currentPlan,
stepResults: stepResults,
metadata: data.metadata,
@@ -801,7 +792,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
} />
- {/* 最终总结 */}
+ {/* 最终总结(支持 Markdown + ECharts) */}
{
borderColor={borderColor}
boxShadow="md"
>
-
- {message.content}
-
+
{/* 元数据 */}
{message.metadata && (