From 7b65cac35811f5c2f4f2ac5521d3e85b5f0627ff Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Sun, 30 Nov 2025 18:45:36 +0800 Subject: [PATCH] update pay function --- src/components/ChatBot/MarkdownWithCharts.js | 133 ++++++++++--------- 1 file changed, 72 insertions(+), 61 deletions(-) diff --git a/src/components/ChatBot/MarkdownWithCharts.js b/src/components/ChatBot/MarkdownWithCharts.js index 11ad572b..8a021c8d 100644 --- a/src/components/ChatBot/MarkdownWithCharts.js +++ b/src/components/ChatBot/MarkdownWithCharts.js @@ -8,78 +8,81 @@ 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 代码块 - * 支持处理不完整的代码块(LLM 输出被截断的情况) + * 支持处理: + * 1. 正常的换行符 \n + * 2. 转义的换行符 \\n(后端 JSON 序列化产生) + * 3. 不完整的代码块(LLM 输出被截断) + * * @param {string} markdown - Markdown 文本 * @returns {Array} - 包含文本和图表的数组 */ const parseMarkdownWithCharts = (markdown) => { if (!markdown) return []; - // 先清理可能的残缺 JSON - const cleanedMarkdown = cleanBrokenJson(markdown); + let content = markdown; + + // 处理转义的换行符(后端返回的 JSON 字符串可能包含 \\n) + // 只处理代码块标记周围的换行符,不破坏 JSON 内部结构 + // 将 ```echarts\\n 转换为 ```echarts\n + content = content.replace(/```echarts\\n/g, '```echarts\n'); + // 将 \\n``` 转换为 \n``` + content = content.replace(/\\n```/g, '\n```'); + + // 如果整个内容都是转义的换行符格式,进行全局替换 + // 检测:如果内容中没有真正的换行符但有 \\n,则进行全局替换 + if (!content.includes('\n') && content.includes('\\n')) { + content = content.replace(/\\n/g, '\n'); + } const parts = []; - // 方案1: 匹配完整的 echarts 代码块 - const completeEchartsRegex = /```echarts\s*\n([\s\S]*?)```/g; - // 方案2: 匹配不完整的 echarts 代码块(没有结束的 ```) - const incompleteEchartsRegex = /```echarts\s*\n([\s\S]*?)$/; + // 匹配 echarts 代码块的正则表达式 + // 支持多种格式: + // 1. ```echarts\n{...}\n``` + // 2. ```echarts\n{...}```(末尾无换行) + // 3. ```echarts {...}```(同一行开始,虽不推荐但兼容) + const echartsBlockRegex = /```echarts\s*\n?([\s\S]*?)```/g; let lastIndex = 0; let match; - // 首先尝试匹配完整的代码块 - while ((match = completeEchartsRegex.exec(cleanedMarkdown)) !== null) { + // 匹配所有 echarts 代码块 + while ((match = echartsBlockRegex.exec(content)) !== null) { // 添加代码块前的文本 if (match.index > lastIndex) { - const textBefore = cleanedMarkdown.substring(lastIndex, match.index).trim(); + const textBefore = content.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 }); + // 提取 ECharts 配置内容 + let chartConfig = match[1].trim(); + + // 处理 JSON 内部的转义换行符(恢复为真正的换行,便于后续解析) + // 注意:这里的 \\n 在 JSON 内部应该保持为 \n(换行符),不是字面量 + if (chartConfig.includes('\\n')) { + chartConfig = chartConfig.replace(/\\n/g, '\n'); + } + if (chartConfig.includes('\\t')) { + chartConfig = chartConfig.replace(/\\t/g, '\t'); + } + + if (chartConfig) { + parts.push({ type: 'chart', content: chartConfig }); + } lastIndex = match.index + match[0].length; } - // 检查剩余内容是否包含不完整的 echarts 代码块 - if (lastIndex < cleanedMarkdown.length) { - const remainingText = cleanedMarkdown.substring(lastIndex); - const incompleteMatch = remainingText.match(incompleteEchartsRegex); + // 检查剩余内容 + if (lastIndex < content.length) { + const remainingText = content.substring(lastIndex); + + // 检查是否有不完整的 echarts 代码块(没有结束的 ```) + const incompleteMatch = remainingText.match(/```echarts\s*\n?([\s\S]*?)$/); if (incompleteMatch) { // 提取不完整代码块之前的文本 @@ -89,9 +92,14 @@ const parseMarkdownWithCharts = (markdown) => { } // 提取不完整的 echarts 内容 - const incompleteChartConfig = incompleteMatch[1].trim(); + let incompleteChartConfig = incompleteMatch[1].trim(); + // 同样处理转义换行符 + if (incompleteChartConfig.includes('\\n')) { + incompleteChartConfig = incompleteChartConfig.replace(/\\n/g, '\n'); + } + if (incompleteChartConfig) { - logger.warn('检测到不完整的 echarts 代码块(缺少结束符)', { + logger.warn('[MarkdownWithCharts] 检测到不完整的 echarts 代码块', { contentPreview: incompleteChartConfig.substring(0, 100), }); parts.push({ type: 'chart', content: incompleteChartConfig }); @@ -105,9 +113,23 @@ const parseMarkdownWithCharts = (markdown) => { } } - // 如果没有找到任何图表,返回整个 markdown 作为文本 + // 如果没有找到任何部分,返回整个 markdown 作为文本 if (parts.length === 0) { - parts.push({ type: 'text', content: cleanedMarkdown }); + parts.push({ type: 'text', content: content }); + } + + // 开发环境调试 + if (process.env.NODE_ENV === 'development') { + const chartParts = parts.filter(p => p.type === 'chart'); + if (chartParts.length > 0 || content.includes('echarts')) { + logger.info('[MarkdownWithCharts] 解析结果', { + inputLength: markdown?.length, + hasEchartsKeyword: content.includes('echarts'), + hasCodeBlock: content.includes('```'), + partsCount: parts.length, + partTypes: parts.map(p => p.type), + }); + } } return parts; @@ -121,17 +143,6 @@ const parseMarkdownWithCharts = (markdown) => { export const MarkdownWithCharts = ({ content, variant = 'auto' }) => { const parts = parseMarkdownWithCharts(content); - // 开发环境调试日志 - if (process.env.NODE_ENV === 'development' && content?.includes('echarts')) { - logger.info('[MarkdownWithCharts] 解析结果', { - contentLength: content?.length, - contentPreview: content?.substring(0, 100), - partsCount: parts.length, - partTypes: parts.map(p => p.type), - hasChartPart: parts.some(p => p.type === 'chart'), - }); - } - // 系统颜色模式 const systemTextColor = useColorModeValue('gray.700', 'gray.100'); const systemHeadingColor = useColorModeValue('gray.800', 'gray.50');