diff --git a/__pycache__/mcp_server.cpython-310.pyc b/__pycache__/mcp_server.cpython-310.pyc index fcea44c9..e4a163a0 100644 Binary files a/__pycache__/mcp_server.cpython-310.pyc and b/__pycache__/mcp_server.cpython-310.pyc differ diff --git a/mcp_server.py b/mcp_server.py index ad090926..2e47acec 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -2168,10 +2168,37 @@ class MCPAgentIntegrated: if not successful_results: return "很抱歉,所有步骤都执行失败,无法生成分析报告。" - # 构建结果文本(精简版) + def safe_truncate_result(result, max_length=600): + """安全截取结果,避免截断 JSON 到不完整状态""" + result_str = str(result) + if len(result_str) <= max_length: + return result_str + + # 截取到 max_length + truncated = result_str[:max_length] + + # 尝试找到最后一个完整的 JSON 边界(}, ] 或 ,) + # 从后往前找一个安全的截断点 + safe_endings = ['},', '},\n', '}\n', '],', '],\n', ']\n', '",', '",\n', '"\n'] + best_pos = -1 + for ending in safe_endings: + pos = truncated.rfind(ending) + if pos > best_pos: + best_pos = pos + + if best_pos > max_length // 2: # 只有找到的位置超过一半时才使用 + truncated = truncated[:best_pos + 1] + + # 如果结果看起来像 JSON,添加省略提示 + if truncated.strip().startswith('{') or truncated.strip().startswith('['): + return truncated + "\n...(数据已截断)" + else: + return truncated + "..." + + # 构建结果文本(精简版,安全截取) results_text = "\n\n".join([ f"**步骤 {r.step_index + 1}: {r.tool}**\n" - f"结果: {str(r.result)[:800]}..." + f"结果: {safe_truncate_result(r.result)}" for r in successful_results[:3] # 只取前3个,避免超长 ]) diff --git a/src/components/ChatBot/MarkdownWithCharts.js b/src/components/ChatBot/MarkdownWithCharts.js index 0f9dc0dd..0efdd1ec 100644 --- a/src/components/ChatBot/MarkdownWithCharts.js +++ b/src/components/ChatBot/MarkdownWithCharts.js @@ -8,6 +8,35 @@ import remarkGfm from 'remark-gfm'; import { EChartsRenderer } from './EChartsRenderer'; import { logger } from '@utils/logger'; +/** + * 清理消息中可能存在的残缺 JSON 片段 + * 这种情况通常是模型生成时意外混入的工具返回数据 + * @param {string} text - 原始文本 + * @returns {string} - 清理后的文本 + */ +const cleanBrokenJson = (text) => { + if (!text) return text; + + // 移除可能的残缺 JSON 对象片段(没有开头的 { 但有结尾的 }) + // 例如: "...一些文字itemStyle": {"color": "#ee6666"}, "smooth": true}]\n}" + let cleaned = text; + + // 模式1: 移除以 JSON 属性开始的残缺片段 (如 itemStyle": {...}) + cleaned = cleaned.replace(/[a-zA-Z_][a-zA-Z0-9_]*"\s*:\s*\{[^{}]*\}(\s*,\s*"[a-zA-Z_][a-zA-Z0-9_]*"\s*:\s*[^,}\]]+)*\s*\}\s*\]\s*\}/g, ''); + + // 模式2: 移除孤立的 JSON 数组/对象结尾 (如 ]\n}) + cleaned = cleaned.replace(/\s*\]\s*\}\s*```\s*$/g, ''); + + // 模式3: 移除不完整的 echarts 代码块残留 + // 匹配没有开始标记的残缺内容 + cleaned = cleaned.replace(/[a-zA-Z_][a-zA-Z0-9_]*"\s*:\s*\[[^\]]*\]\s*\}\s*```/g, ''); + + // 模式4: 清理残留的属性片段 + cleaned = cleaned.replace(/,?\s*"[a-zA-Z_][a-zA-Z0-9_]*"\s*:\s*(?:true|false|null|\d+|"[^"]*")\s*\}\s*\]\s*\}\s*$/g, ''); + + return cleaned.trim(); +}; + /** * 解析 Markdown 内容,提取 ECharts 代码块 * @param {string} markdown - Markdown 文本 @@ -16,15 +45,18 @@ import { logger } from '@utils/logger'; const parseMarkdownWithCharts = (markdown) => { if (!markdown) return []; + // 先清理可能的残缺 JSON + const cleanedMarkdown = cleanBrokenJson(markdown); + const parts = []; const echartsRegex = /```echarts\s*\n([\s\S]*?)```/g; let lastIndex = 0; let match; - while ((match = echartsRegex.exec(markdown)) !== null) { + while ((match = echartsRegex.exec(cleanedMarkdown)) !== null) { // 添加代码块前的文本 if (match.index > lastIndex) { - const textBefore = markdown.substring(lastIndex, match.index).trim(); + const textBefore = cleanedMarkdown.substring(lastIndex, match.index).trim(); if (textBefore) { parts.push({ type: 'text', content: textBefore }); } @@ -38,8 +70,8 @@ const parseMarkdownWithCharts = (markdown) => { } // 添加剩余文本 - if (lastIndex < markdown.length) { - const textAfter = markdown.substring(lastIndex).trim(); + if (lastIndex < cleanedMarkdown.length) { + const textAfter = cleanedMarkdown.substring(lastIndex).trim(); if (textAfter) { parts.push({ type: 'text', content: textAfter }); } @@ -47,7 +79,7 @@ const parseMarkdownWithCharts = (markdown) => { // 如果没有找到图表,返回整个 markdown 作为文本 if (parts.length === 0) { - parts.push({ type: 'text', content: markdown }); + parts.push({ type: 'text', content: cleanedMarkdown }); } return parts;