const defaultDelimiters = [ { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false }, ]; // 转义括号规则 - 通用数学公式解析器 function escapedBracketRule(delimiters: any) { return (text: string, startPos: number = 0) => { const max = text.length; const start = startPos; for (const { left, right, display } of delimiters) { // 检查是否以左标记开始 if (!text.slice(start).startsWith(left)) continue; // 跳过左标记的长度 let pos = start + left.length; // 寻找匹配的右标记 while (pos < max) { if (text.slice(pos).startsWith(right)) { break; } pos++; } // 没找到匹配的右标记,跳过,进入下个匹配 if (pos >= max) continue; // 提取数学公式内容 const content = text.slice(start + left.length, pos); const endPos = pos + right.length; return { formula: content, display, start, end: endPos, left, right, success: true, }; } return { formula: '', display: false, start: 0, end: 0, left: '', right: '', success: false, }; }; } /** * 根据规则将连续的过程标签(ToolCall, Skill等)合并 * 规则: * 1. 连续 2 个及以上的过程标签合并 * 2. 中间包含“执行计划”的不合并(作为分隔符) * 3. 标签间只包含空白字符时不中断合并 * 4. 支持 :::container 语法和 PC 端的 HTML 标签语法 * @param text - 待处理的 Markdown 文本 * @returns 处理后的文本 */ export function groupMarkdownContainers(text : string) : string { if (!text) return ''; // 1. 匹配 HTML 格式的过程标签 (markdown-custom-process) // 2. 匹配 :::container 格式的标签 const blockRegex = /(?:[\r\n\s]*)(?:(?:<(?:div|p)\b[^>]*>)?\s*(]*?>(?:<\/markdown-custom-process>|\/>)?)\s*(?:<\/(?:div|p)>)?|(:::container\b[^\n]*?\n:::))/g; let result = ''; let lastIndex = 0; let currentGroup : string[] = []; let match : RegExpExecArray | null; const flushGroup = () => { if (currentGroup.length > 0) { if (currentGroup.length >= 2) { // 多个时,为了支持层级,必须使用规范化后的 HTML 标签 const groupContent = `\n\n\n${normalizedGroup.join('\n')}\n\n\n`; result += groupContent; } else { // 只有一个工具时,保持原样输出,最稳妥 result += `\n\n${currentGroup[0]}\n\n`; } currentGroup = []; normalizedGroup = []; } }; let count = 0; let normalizedGroup : string[] = []; while ((match = blockRegex.exec(text)) !== null) { count++; const textBefore = text.slice(lastIndex, match.index); if (textBefore.trim() !== '') { flushGroup(); result += textBefore; } else { if (currentGroup.length === 0) { result += textBefore; } } let tagMatch = match[1] || match[2] || ''; // 为了分组显示,我们依然需要一份规范化后的 HTML 标签 let normalizedTag = ''; if (tagMatch.startsWith(':::container')) { const executeId = extractAttribute(tagMatch, 'executeId'); const type = extractAttribute(tagMatch, 'type'); const status = extractAttribute(tagMatch, 'status'); const name = extractAttribute(tagMatch, 'name'); const safeName = name.replace(/"/g, '"'); normalizedTag = ``; } else { normalizedTag = tagMatch; if (!normalizedTag.includes('') && !normalizedTag.endsWith('/>')) { normalizedTag = normalizedTag.replace('>', '>'); } } const isPlan = /type=["']Plan["']/.test(normalizedTag); if (isPlan) { flushGroup(); // Plan 类型保持原样输出 result += `\n\n${tagMatch}\n\n`; } else { currentGroup.push(tagMatch); normalizedGroup.push(normalizedTag); } lastIndex = blockRegex.lastIndex; } flushGroup(); result += text.slice(lastIndex); const finalResult = result.replace(/\n{3,}/g, '\n\n').trim(); return finalResult; } /** * 从属性字符串中提取指定属性的值 */ function extractAttribute(attrString : string, attrName : string) : string { const regex = new RegExp(`${attrName}=["']([^"']*)["']`); const match = attrString.match(regex); if (match != null) { const val = match[1]; // 还原 HTML 实体,避免在后续处理中出现双重转义 return val.replace(/"/g, '"'); } return ''; } // 新的数学公式替换函数 - 直接替换为 $$ 分隔符 export function replaceMathBracket(text : string) : string { // return text; //临时关闭 // 创建只包含非美元符号分隔符的选项 const nonDollarDelimiters = defaultDelimiters.filter( (delimiter) => !delimiter.left.includes('$') && !delimiter.right.includes('$'), ); const rule = escapedBracketRule(nonDollarDelimiters); let result = ''; let pos = 0; while (pos < text.length) { const match = rule(text, pos); if (match.success) { // 添加匹配前的文本 result += text.slice(pos, match.start); // 替换为 $$ 分隔符 const delimiter = match.display ? '$$' : '$'; result += `${delimiter}${match.formula}${delimiter}`; pos = match.end; } else { // 没有匹配,添加当前字符 result += text[pos]; pos++; } } return result; }