|\\[choice\\])([\s\S]*?)(?:<\/choice>|\\[\/choice\\])/i
const match = content.match(choiceRegex)
if (!match) {
// 尝试匹配纯文本格式的选项列表
// 匹配格式: A: xxx\nB: xxx\nC: xxx 或 A. xxx\nB. xxx
const textChoiceRegex = /^([A-E])[.、::]\s*(.+?)(?=\n[A-E][.、::]|\n*$)/gm
const choices: Choice[] = []
let textMatch
while ((textMatch = textChoiceRegex.exec(content)) !== null) {
choices.push({
label: textMatch[1],
text: textMatch[2].trim()
})
}
// 如果找到了至少2个选项,认为是有效的选择列表
if (choices.length >= 2) {
// 移除选项列表,保留其他内容
const cleanContent = content.replace(textChoiceRegex, '').trim()
return { choices, cleanContent }
}
// 如果没有纯文本格式,尝试从HTML中提取选择项
const htmlChoiceRegex = /\s*(\d+|[A-Z])\s*[.、::]\s*([^<]+)/gi
const htmlChoices: Choice[] = []
let htmlMatch
while ((htmlMatch = htmlChoiceRegex.exec(content)) !== null) {
htmlChoices.push({
label: htmlMatch[1],
text: htmlMatch[2].trim()
})
}
if (htmlChoices.length > 0) {
return { choices: htmlChoices, cleanContent: content }
}
return { choices: [], cleanContent: content }
}
const choiceBlock = match[1]
const choices: Choice[] = []
// 匹配 A. text, B. text 等格式
const optionRegex = /^([A-Z])[.、::]\s*(.+)$/gm
let optionMatch
while ((optionMatch = optionRegex.exec(choiceBlock)) !== null) {
choices.push({
label: optionMatch[1],
text: optionMatch[2].trim()
})
}
// 移除选择块,返回清理后的内容
const cleanContent = content.replace(choiceRegex, '').trim()
return { choices, cleanContent }
}
// 清理脚本输出内容
function cleanScriptOutput(content: string): string {
// 移除 ... 块
let cleaned = content.replace(/[\s\S]*?<\/UpdateVariable>/gi, '')
// 移除 ... 块
cleaned = cleaned.replace(/[\s\S]*?<\/Analysis>/gi, '')
// 移除 _.set() 调用
cleaned = cleaned.replace(/^\s*_.set\([^)]+\);\s*$/gm, '')
return cleaned.trim()
}
// 解析状态面板数据
function parseStatusPanel(content: string): { status: any; cleanContent: string } {
const statusRegex = /([\s\S]*?)<\/status_current_variable>/i
const match = content.match(statusRegex)
if (!match) {
return { status: null, cleanContent: content }
}
try {
const statusData = JSON.parse(match[1].trim())
const cleanContent = content.replace(statusRegex, '').trim()
return { status: statusData, cleanContent }
} catch (e) {
console.error('解析状态面板失败:', e)
return { status: null, cleanContent: content }
}
}
export default function MessageContent({ content, role, onChoiceSelect }: MessageContentProps) {
const [showRaw, setShowRaw] = useState(false)
const [hasHtml, setHasHtml] = useState(false)
const [hasScript, setHasScript] = useState(false)
const [allowScript, setAllowScript] = useState(false) // 默认禁用脚本
const [choices, setChoices] = useState([])
const [displayContent, setDisplayContent] = useState(content)
const [statusPanel, setStatusPanel] = useState(null)
const iframeRef = useRef(null)
useEffect(() => {
console.log('[MessageContent] 原始内容:', content)
// 提取 markdown 代码块中的 HTML,并从原始内容中移除代码块标记
// 支持 ```html 和不带标识符的 ``` 包裹(后者在内容含 HTML 标签时识别为 HTML 块)
let processedContent = content
let remainingContent = content
let isHtmlCodeBlock = false
const htmlTagRegex = /<[a-zA-Z][^>]*>/
// 先尝试匹配 ```html 代码块
const explicitHtmlRegex = /```html\s*([\s\S]*?)```/gi
const htmlCodeBlocks: string[] = []
let htmlMatch
while ((htmlMatch = explicitHtmlRegex.exec(content)) !== null) {
htmlCodeBlocks.push(htmlMatch[1].trim())
}
if (htmlCodeBlocks.length > 0) {
// 有明确的 ```html 标识
processedContent = htmlCodeBlocks.join('\n')
remainingContent = content.replace(/```html\s*[\s\S]*?```/gi, '').trim()
isHtmlCodeBlock = true
console.log('[MessageContent] 提取到 ```html 代码块:', processedContent)
console.log('[MessageContent] 剩余内容:', remainingContent)
} else {
// 尝试匹配无语言标识的 ``` 代码块,内容含 HTML 标签时也视为 HTML 块
const genericCodeRegex = /```\s*\n?([\s\S]*?)```/g
const genericBlocks: string[] = []
let genericMatch
while ((genericMatch = genericCodeRegex.exec(content)) !== null) {
const blockContent = genericMatch[1].trim()
if (htmlTagRegex.test(blockContent)) {
genericBlocks.push(blockContent)
}
}
if (genericBlocks.length > 0) {
processedContent = genericBlocks.join('\n')
remainingContent = content.replace(/```\s*\n?[\s\S]*?```/g, '').trim()
isHtmlCodeBlock = true
console.log('[MessageContent] 提取到通用 HTML 代码块:', processedContent)
console.log('[MessageContent] 剩余内容:', remainingContent)
}
}
// 解析状态面板
const { status, cleanContent: contentAfterStatus } = parseStatusPanel(processedContent)
console.log('[MessageContent] 状态面板:', status)
setStatusPanel(status)
// 解析选择项(从剩余内容中解析,而不是从 HTML 中解析)
let parsedChoices: Choice[] = []
let cleanContent = contentAfterStatus
if (isHtmlCodeBlock && remainingContent) {
// 如果有 HTML 代码块,从剩余内容中解析选项
const choiceResult = parseChoices(remainingContent)
parsedChoices = choiceResult.choices
console.log('[MessageContent] 从剩余内容解析选择项:', parsedChoices)
} else if (!isHtmlCodeBlock) {
// 如果没有 HTML 代码块,正常解析
const choiceResult = parseChoices(contentAfterStatus)
parsedChoices = choiceResult.choices
cleanContent = choiceResult.cleanContent
console.log('[MessageContent] 选择项:', parsedChoices)
}
setChoices(parsedChoices)
// 清理脚本输出(只在非 HTML 代码块时清理)
// 如果是HTML代码块,直接使用提取的HTML内容(processedContent),而不是contentAfterStatus
const finalContent = isHtmlCodeBlock ? processedContent : cleanScriptOutput(cleanContent)
console.log('[MessageContent] 清理后内容:', finalContent)
// 先检测内容类型(需要在转换代码块之前检测,以便使用原始 finalContent)
const htmlRegex = /<[^>]+>/g
const scriptRegex = /