Commit 5e75750d authored by 水玉婷's avatar 水玉婷
Browse files

feat:优化md中有序、无序列表输出格式问题

parent f8edf3b5
...@@ -40,8 +40,8 @@ ...@@ -40,8 +40,8 @@
stage: 'wechat-demo', stage: 'wechat-demo',
}; };
// const dialogSessionId = '20251127180914709-00043912'; const dialogSessionId = '20251127180914709-00043912';
const dialogSessionId = ''; // const dialogSessionId = '';
const detailData = ref({ const detailData = ref({
title: '国械小智', title: '国械小智',
}); });
......
...@@ -478,6 +478,7 @@ const getChatRecord = async (dialogSessionId: string) => { ...@@ -478,6 +478,7 @@ const getChatRecord = async (dialogSessionId: string) => {
const response = await fetch(`${props.apiBaseUrl}/aiService/ask/list/chat/${dialogSessionId}`, { const response = await fetch(`${props.apiBaseUrl}/aiService/ask/list/chat/${dialogSessionId}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json',
'token': props.token, 'token': props.token,
'x-session-id': props.token, 'x-session-id': props.token,
'x-app-code': props.appCode || '' 'x-app-code': props.appCode || ''
......
...@@ -1060,8 +1060,7 @@ li { ...@@ -1060,8 +1060,7 @@ li {
// 表格样式(如果Markdown中包含表格) // 表格样式(如果Markdown中包含表格)
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 8px;
font-size: 13px; font-size: 13px;
background-color: @white; background-color: @white;
...@@ -1093,7 +1092,6 @@ li { ...@@ -1093,7 +1092,6 @@ li {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
border-radius: 8px;
font-size:0; font-size:0;
// 滚动条样式 // 滚动条样式
......
...@@ -21,13 +21,13 @@ const processMarkdownFormat = (text: string): string => { ...@@ -21,13 +21,13 @@ const processMarkdownFormat = (text: string): string => {
processedText = processedText.replace(/\*(.*?)\*/gim, '<em>$1</em>'); processedText = processedText.replace(/\*(.*?)\*/gim, '<em>$1</em>');
processedText = processedText.replace(/__(.*?)__/gim, '<strong>$1</strong>'); processedText = processedText.replace(/__(.*?)__/gim, '<strong>$1</strong>');
processedText = processedText.replace(/_(.*?)_/gim, '<em>$1</em>'); processedText = processedText.replace(/_(.*?)_/gim, '<em>$1</em>');
// 处理删除线 // 处理删除线
processedText = processedText.replace(/~~(.*?)~~/gim, '<del>$1</del>'); processedText = processedText.replace(/~~(.*?)~~/gim, '<del>$1</del>');
// 处理行内代码 // 处理行内代码
processedText = processedText.replace(/`([^`]+)`/gim, '<code>$1</code>'); processedText = processedText.replace(/`([^`]+)`/gim, '<code>$1</code>');
return processedText; return processedText;
}; };
...@@ -41,7 +41,7 @@ const processMarkdownFormat = (text: string): string => { ...@@ -41,7 +41,7 @@ const processMarkdownFormat = (text: string): string => {
const generateCompleteTable = (header: string[], alignments: string[], dataRows: string[][]): string => { const generateCompleteTable = (header: string[], alignments: string[], dataRows: string[][]): string => {
let tableHtml = '<div class="table-container">\n'; let tableHtml = '<div class="table-container">\n';
tableHtml += ' <table class="markdown-table" style="border-collapse: collapse; width: auto; min-width: 100%; border: 1px solid #ddd;">\n'; tableHtml += ' <table class="markdown-table" style="border-collapse: collapse; width: auto; min-width: 100%; border: 1px solid #ddd;">\n';
// 表头 // 表头
tableHtml += ' <thead>\n <tr>\n'; tableHtml += ' <thead>\n <tr>\n';
header.forEach((cell, index) => { header.forEach((cell, index) => {
...@@ -50,7 +50,7 @@ const generateCompleteTable = (header: string[], alignments: string[], dataRows: ...@@ -50,7 +50,7 @@ const generateCompleteTable = (header: string[], alignments: string[], dataRows:
tableHtml += ` <th style="text-align: ${align}; border: 1px solid #ddd; padding: 8px; background-color: #f5f5f5; font-weight: bold;">${processedCell}</th>\n`; tableHtml += ` <th style="text-align: ${align}; border: 1px solid #ddd; padding: 8px; background-color: #f5f5f5; font-weight: bold;">${processedCell}</th>\n`;
}); });
tableHtml += ' </tr>\n </thead>\n'; tableHtml += ' </tr>\n </thead>\n';
// 表体 // 表体
if (dataRows.length > 0) { if (dataRows.length > 0) {
tableHtml += ' <tbody>\n'; tableHtml += ' <tbody>\n';
...@@ -65,7 +65,7 @@ const generateCompleteTable = (header: string[], alignments: string[], dataRows: ...@@ -65,7 +65,7 @@ const generateCompleteTable = (header: string[], alignments: string[], dataRows:
}); });
tableHtml += ' </tbody>\n'; tableHtml += ' </tbody>\n';
} }
tableHtml += ' </table>\n'; tableHtml += ' </table>\n';
tableHtml += '</div>'; tableHtml += '</div>';
return tableHtml; return tableHtml;
...@@ -98,16 +98,16 @@ export const parseMarkdown = (text: string): string => { ...@@ -98,16 +98,16 @@ export const parseMarkdown = (text: string): string => {
text = text.replace(/(\|[^\n]+\|(?:\r?\n|\r))+/g, (match) => { text = text.replace(/(\|[^\n]+\|(?:\r?\n|\r))+/g, (match) => {
const lines = match.trim().split(/\r?\n/).filter(line => line.trim() && line.includes('|')); const lines = match.trim().split(/\r?\n/).filter(line => line.trim() && line.includes('|'));
if (lines.length < 2) return match; if (lines.length < 2) return match;
try { try {
// 解析表头 // 解析表头
const headerRow = lines[0]; const headerRow = lines[0];
const headerCells = headerRow.split('|').slice(1, -1).map(cell => cell.trim()); const headerCells = headerRow.split('|').slice(1, -1).map(cell => cell.trim());
// 解析对齐方式 // 解析对齐方式
let alignments: string[] = []; let alignments: string[] = [];
let dataStartIndex = 1; let dataStartIndex = 1;
// 查找分隔线 // 查找分隔线
for (let i = 1; i < lines.length; i++) { for (let i = 1; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
...@@ -122,12 +122,12 @@ export const parseMarkdown = (text: string): string => { ...@@ -122,12 +122,12 @@ export const parseMarkdown = (text: string): string => {
break; break;
} }
} }
// 如果没有找到分隔线,使用默认对齐方式 // 如果没有找到分隔线,使用默认对齐方式
if (alignments.length === 0) { if (alignments.length === 0) {
alignments = headerCells.map(() => 'left'); alignments = headerCells.map(() => 'left');
} }
// 解析数据行 // 解析数据行
const dataRows: string[][] = []; const dataRows: string[][] = [];
for (let i = dataStartIndex; i < lines.length; i++) { for (let i = dataStartIndex; i < lines.length; i++) {
...@@ -135,7 +135,7 @@ export const parseMarkdown = (text: string): string => { ...@@ -135,7 +135,7 @@ export const parseMarkdown = (text: string): string => {
const cells = row.split('|').slice(1, -1).map(cell => cell.trim()); const cells = row.split('|').slice(1, -1).map(cell => cell.trim());
dataRows.push(cells); dataRows.push(cells);
} }
// 生成完整表格HTML // 生成完整表格HTML
return generateCompleteTable(headerCells, alignments, dataRows); return generateCompleteTable(headerCells, alignments, dataRows);
} catch (error) { } catch (error) {
...@@ -159,11 +159,6 @@ export const parseMarkdown = (text: string): string => { ...@@ -159,11 +159,6 @@ export const parseMarkdown = (text: string): string => {
return `<a href="${url}" ${target}>${text}</a>`; return `<a href="${url}" ${target}>${text}</a>`;
}); });
// 处理图片(添加加载失败处理)
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/gim, (match, alt, src) => {
return `<img src="${src}" alt="${alt || '图片'}" style="max-width: 100%; height: auto;" onerror="this.style.display='none'" />`;
});
// 处理有序列表 // 处理有序列表
text = text.replace(/^\d+\.\s+(.*)$/gim, '<li>$1</li>'); text = text.replace(/^\d+\.\s+(.*)$/gim, '<li>$1</li>');
text = text.replace(/(<li>.*<\/li>)(?=\s*<li>)/gim, '$1'); text = text.replace(/(<li>.*<\/li>)(?=\s*<li>)/gim, '$1');
...@@ -286,7 +281,7 @@ export const isTableSeparator = (text: string): boolean => { ...@@ -286,7 +281,7 @@ export const isTableSeparator = (text: string): boolean => {
if (!trimmedText.startsWith('|') || !trimmedText.endsWith('|')) { if (!trimmedText.startsWith('|') || !trimmedText.endsWith('|')) {
return false; return false;
} }
const cells = trimmedText.split('|').slice(1, -1).map(cell => cell.trim()); const cells = trimmedText.split('|').slice(1, -1).map(cell => cell.trim());
return cells.every(cell => /^:?-+:?$/.test(cell)); return cells.every(cell => /^:?-+:?$/.test(cell));
}; };
...@@ -311,7 +306,7 @@ export const isTableStart = (text: string): boolean => { ...@@ -311,7 +306,7 @@ export const isTableStart = (text: string): boolean => {
class StreamingTableProcessor { class StreamingTableProcessor {
private tableData: string[] = []; private tableData: string[] = [];
private isInTable: boolean = false; private isInTable: boolean = false;
/** /**
* 处理新的文本内容 * 处理新的文本内容
* @param text 新的文本内容 * @param text 新的文本内容
...@@ -321,9 +316,9 @@ class StreamingTableProcessor { ...@@ -321,9 +316,9 @@ class StreamingTableProcessor {
if (!text || typeof text !== 'string') { if (!text || typeof text !== 'string') {
return null; return null;
} }
const trimmedText = text.trim(); const trimmedText = text.trim();
// 检查是否是表格行 // 检查是否是表格行
if (isTableRow(trimmedText)) { if (isTableRow(trimmedText)) {
if (!this.isInTable) { if (!this.isInTable) {
...@@ -334,7 +329,7 @@ class StreamingTableProcessor { ...@@ -334,7 +329,7 @@ class StreamingTableProcessor {
} else { } else {
// 继续当前表格,收集所有表格行 // 继续当前表格,收集所有表格行
this.tableData.push(trimmedText); this.tableData.push(trimmedText);
// 不立即生成表格,继续收集直到遇到非表格行 // 不立即生成表格,继续收集直到遇到非表格行
return null; // 继续收集表格行 return null; // 继续收集表格行
} }
...@@ -343,7 +338,7 @@ class StreamingTableProcessor { ...@@ -343,7 +338,7 @@ class StreamingTableProcessor {
if (this.isInTable && this.tableData.length > 0) { if (this.isInTable && this.tableData.length > 0) {
// 表格结束,处理缓存的表格数据 // 表格结束,处理缓存的表格数据
let result: string | null = null; let result: string | null = null;
// 检查表格是否至少包含表头和数据行(至少2行) // 检查表格是否至少包含表头和数据行(至少2行)
if (this.tableData.length >= 2) { if (this.tableData.length >= 2) {
// 表格完整,生成完整表格 // 表格完整,生成完整表格
...@@ -352,16 +347,16 @@ class StreamingTableProcessor { ...@@ -352,16 +347,16 @@ class StreamingTableProcessor {
// 表格不完整,显示加载提示 // 表格不完整,显示加载提示
result = '<div class="table-loading" style="border: 1px solid #ddd; padding: 10px; background-color: #f9f9f9; margin: 10px 0;">表格数据加载中...</div>'; result = '<div class="table-loading" style="border: 1px solid #ddd; padding: 10px; background-color: #f9f9f9; margin: 10px 0;">表格数据加载中...</div>';
} }
this.reset(); this.reset();
return result; return result;
} }
// 普通文本,直接返回 // 普通文本,直接返回
return text; return text;
} }
} }
/** /**
* 生成完整表格HTML * 生成完整表格HTML
* @returns 表格HTML * @returns 表格HTML
...@@ -370,7 +365,7 @@ class StreamingTableProcessor { ...@@ -370,7 +365,7 @@ class StreamingTableProcessor {
if (this.tableData.length < 2) { if (this.tableData.length < 2) {
return ''; return '';
} }
try { try {
// 解析表头 // 解析表头
const headerRow = this.tableData[0]; const headerRow = this.tableData[0];
...@@ -378,11 +373,11 @@ class StreamingTableProcessor { ...@@ -378,11 +373,11 @@ class StreamingTableProcessor {
// 对表头单元格进行Markdown格式处理 // 对表头单元格进行Markdown格式处理
return processMarkdownFormat(cell.trim()); return processMarkdownFormat(cell.trim());
}); });
// 解析对齐方式(从第二行) // 解析对齐方式(从第二行)
let alignments: string[] = []; let alignments: string[] = [];
let dataStartIndex = 1; let dataStartIndex = 1;
// 查找分隔线 // 查找分隔线
for (let i = 1; i < this.tableData.length; i++) { for (let i = 1; i < this.tableData.length; i++) {
if (isTableSeparator(this.tableData[i])) { if (isTableSeparator(this.tableData[i])) {
...@@ -397,13 +392,13 @@ class StreamingTableProcessor { ...@@ -397,13 +392,13 @@ class StreamingTableProcessor {
break; break;
} }
} }
// 如果没有找到分隔线,使用默认对齐方式,数据从第二行开始 // 如果没有找到分隔线,使用默认对齐方式,数据从第二行开始
if (alignments.length === 0) { if (alignments.length === 0) {
alignments = headerCells.map(() => 'left'); alignments = headerCells.map(() => 'left');
dataStartIndex = 1; // 对于无分隔线表格,数据从第二行开始 dataStartIndex = 1; // 对于无分隔线表格,数据从第二行开始
} }
// 解析数据行 // 解析数据行
const dataRows: string[][] = []; const dataRows: string[][] = [];
for (let i = dataStartIndex; i < this.tableData.length; i++) { for (let i = dataStartIndex; i < this.tableData.length; i++) {
...@@ -414,7 +409,7 @@ class StreamingTableProcessor { ...@@ -414,7 +409,7 @@ class StreamingTableProcessor {
}); });
dataRows.push(cells); dataRows.push(cells);
} }
// 生成完整表格HTML // 生成完整表格HTML
return generateCompleteTable(headerCells, alignments, dataRows); return generateCompleteTable(headerCells, alignments, dataRows);
} catch (error) { } catch (error) {
...@@ -422,7 +417,7 @@ class StreamingTableProcessor { ...@@ -422,7 +417,7 @@ class StreamingTableProcessor {
return '<div class="table-error" style="border: 1px solid #ff6b6b; padding: 10px; background-color: #ffeaea; margin: 10px 0;">表格渲染失败</div>'; return '<div class="table-error" style="border: 1px solid #ff6b6b; padding: 10px; background-color: #ffeaea; margin: 10px 0;">表格渲染失败</div>';
} }
} }
/** /**
* 重置表格处理器状态 * 重置表格处理器状态
*/ */
...@@ -430,7 +425,7 @@ class StreamingTableProcessor { ...@@ -430,7 +425,7 @@ class StreamingTableProcessor {
this.tableData = []; this.tableData = [];
this.isInTable = false; this.isInTable = false;
} }
/** /**
* 获取当前表格状态 * 获取当前表格状态
*/ */
...@@ -445,6 +440,175 @@ class StreamingTableProcessor { ...@@ -445,6 +440,175 @@ class StreamingTableProcessor {
// 创建全局表格处理器实例 // 创建全局表格处理器实例
const streamingTableProcessor = new StreamingTableProcessor(); const streamingTableProcessor = new StreamingTableProcessor();
/**
* 检查文本是否是列表项(有序或无序)
* @param text 要检查的文本
* @returns 是否是列表项
*/
export const isListItem = (text: string): boolean => {
if (!text || typeof text !== 'string') {
return false;
}
const trimmedText = text.trim();
// 匹配有序列表(1. 2. 等)或无序列表(- * +)
return /^\d+\.\s+/.test(trimmedText) || /^[-*+]\s+/.test(trimmedText);
};
/**
* 检查文本是否是有序列表项
* @param text 要检查的文本
* @returns 是否是有序列表项
*/
export const isOrderedListItem = (text: string): boolean => {
if (!text || typeof text !== 'string') {
return false;
}
const trimmedText = text.trim();
return /^\d+\.\s+/.test(trimmedText);
};
/**
* 检查文本是否是无序列表项
* @param text 要检查的文本
* @returns 是否是无序列表项
*/
export const isUnorderedListItem = (text: string): boolean => {
if (!text || typeof text !== 'string') {
return false;
}
const trimmedText = text.trim();
return /^[-*+]\s+/.test(trimmedText);
};
/**
* 流式列表处理器
* 用于处理SSE逐条返回的列表数据
*/
class StreamingListProcessor {
private listItems: string[] = [];
private isInList: boolean = false;
private listType: 'ordered' | 'unordered' | null = null;
private orderedListStartNumber: number = 1; // 记录有序列表的起始序号
/**
* 处理新的文本内容
* @param text 新的文本内容
* @returns 处理结果:如果是列表项返回null,列表完成时返回完整列表HTML
*/
public processText(text: string): string | null {
if (!text || typeof text !== 'string') {
return null;
}
const trimmedText = text.trim();
// 检查是否是列表项
if (isListItem(trimmedText)) {
const isOrdered = isOrderedListItem(trimmedText);
const currentListType = isOrdered ? 'ordered' : 'unordered';
if (!this.isInList) {
// 开始新的列表
this.isInList = true;
this.listType = currentListType;
this.listItems = [trimmedText];
// 如果是有序列表,记录起始序号
if (isOrdered) {
const match = trimmedText.match(/^(\d+)\.\s+/);
this.orderedListStartNumber = match ? parseInt(match[1]) : 1;
}
return null; // 列表开始,不立即渲染
} else if (this.listType === currentListType) {
// 继续当前列表,收集所有列表项
this.listItems.push(trimmedText);
return null; // 继续收集列表项
} else {
// 列表类型改变,结束当前列表并开始新的列表
const result = this.generateCompleteList();
this.isInList = true;
this.listType = currentListType;
this.listItems = [trimmedText];
// 如果是有序列表,记录起始序号
if (isOrdered) {
const match = trimmedText.match(/^(\d+)\.\s+/);
this.orderedListStartNumber = match ? parseInt(match[1]) : 1;
}
return result;
}
} else {
// 非列表项
if (this.isInList && this.listItems.length > 0) {
// 列表结束,处理缓存的列表数据
const result = this.generateCompleteList();
this.reset();
return result;
}
// 普通文本,直接返回
return text;
}
}
/**
* 生成完整列表HTML
* @returns 列表HTML
*/
private generateCompleteList(): string {
if (this.listItems.length === 0) {
return '';
}
try {
// 处理每个列表项的内容
const processedItems = this.listItems.map((item, index) => {
if (this.listType === 'ordered') {
// 有序列表:移除数字标记,保留内容,但保持正确的序号
const content = item.replace(/^\d+\.\s+/, '').trim();
const processedContent = processMarkdownFormat(content);
return `<li value="${this.orderedListStartNumber + index}">${processedContent}</li>`;
} else {
// 无序列表:移除标记,保留内容
const content = item.replace(/^[-*+]\s+/, '').trim();
const processedContent = processMarkdownFormat(content);
return `<li>${processedContent}</li>`;
}
});
// 根据列表类型生成完整列表
const listTag = this.listType === 'ordered' ? 'ol' : 'ul';
return `<${listTag}>${processedItems.join('')}</${listTag}>`;
} catch (error) {
console.warn('列表生成失败:', error);
return '<div class="list-error" style="border: 1px solid #ff6b6b; padding: 10px; background-color: #ffeaea; margin: 10px 0;">列表渲染失败</div>';
}
}
/**
* 重置列表处理器状态
*/
private reset(): void {
this.listItems = [];
this.isInList = false;
this.listType = null;
this.orderedListStartNumber = 1;
}
/**
* 获取当前列表状态
*/
public getListState(): { isInList: boolean; listType: 'ordered' | 'unordered' | null; listItems: string[]; orderedListStartNumber: number } {
return {
isInList: this.isInList,
listType: this.listType,
listItems: [...this.listItems],
orderedListStartNumber: this.orderedListStartNumber
};
}
}
// 创建全局列表处理器实例
const streamingListProcessor = new StreamingListProcessor();
/** /**
* 处理流式Markdown文本(专门用于SSE逐条返回的数据) * 处理流式Markdown文本(专门用于SSE逐条返回的数据)
* @param text 新的文本内容 * @param text 新的文本内容
...@@ -454,23 +618,36 @@ export const processStreamingMarkdown = (text: string): string => { ...@@ -454,23 +618,36 @@ export const processStreamingMarkdown = (text: string): string => {
// 清理HTML标签,防止XSS攻击 // 清理HTML标签,防止XSS攻击
text = text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); text = text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
text = text.replace(/<[^>]*>/g, ''); text = text.replace(/<[^>]*>/g, '');
// 使用流式列表处理器处理文本
const listResult = streamingListProcessor.processText(text);
if (listResult === null) {
// 列表项被收集,不立即渲染
return '';
}
// 如果列表处理器返回了完整列表,直接返回
if (listResult && (listResult.includes('<ol>') || listResult.includes('<ul>'))) {
return listResult;
}
// 使用流式表格处理器处理文本 // 使用流式表格处理器处理文本
const result = streamingTableProcessor.processText(text); const tableResult = streamingTableProcessor.processText(text);
if (result === null) { if (tableResult === null) {
// 表格行被收集,不立即渲染 // 表格行被收集,不立即渲染
return ''; return '';
} }
// 如果表格处理器返回了完整表格,需要确保表格内容中的Markdown格式也被处理 // 如果表格处理器返回了完整表格,需要确保表格内容中的Markdown格式也被处理
if (result.includes('<table')) { if (tableResult.includes('<table')) {
// 对表格内容进行Markdown格式处理 // 对表格内容进行Markdown格式处理
return processMarkdownFormat(result); return processMarkdownFormat(tableResult);
} }
// 否则,处理其他Markdown语法 // 否则,处理其他Markdown语法
return parseMarkdown(result); return parseMarkdown(tableResult || text);
}; };
/** /**
...@@ -525,11 +702,14 @@ export const mergeMarkdownContent = (existingContent: string, newContent: string ...@@ -525,11 +702,14 @@ export const mergeMarkdownContent = (existingContent: string, newContent: string
}; };
export default { export default {
parseMarkdown, parseMarkdown, // 解析Markdown文本为HTML
markdownTemplate, markdownTemplate, // 应用Markdown模板
markdown, markdown, // 完整的Markdown处理函数
isMarkdownBlock, isMarkdownBlock, // 检查是否为Markdown块
isLastBlockMarkdown, isLastBlockMarkdown, // 检查最后一个块是否为Markdown
getLastMarkdownBlockIndex, getLastMarkdownBlockIndex, // 获取最后一个Markdown块的索引
mergeMarkdownContent mergeMarkdownContent, // 合并Markdown内容
isListItem, // 检查是否为列表项
isOrderedListItem, // 检查是否为有序列表项
isUnorderedListItem // 检查是否为无序列表项
}; };
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment