2026-05-08 01:04:23 +08:00
|
|
|
|
import { escapeHtml } from '@/utils/html'
|
|
|
|
|
|
import { renderMarkdown as renderMarkdownPreview } from '@/utils/markdown_preview'
|
2026-01-15 06:01:34 +08:00
|
|
|
|
import dayjs, { parseToShanghai } from '@/utils/time'
|
|
|
|
|
|
import chatExportTemplate from './templates/chat-export-template.html?raw'
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
2025-08-25 13:57:47 +08:00
|
|
|
|
export class ChatExporter {
|
|
|
|
|
|
/**
|
2025-10-13 11:12:25 +08:00
|
|
|
|
* 导出聊天对话为 HTML 文件
|
2025-08-25 13:57:47 +08:00
|
|
|
|
* @param {Object} options 导出选项
|
|
|
|
|
|
*/
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static async exportToHTML(options = {}) {
|
2025-08-25 13:57:47 +08:00
|
|
|
|
const {
|
|
|
|
|
|
chatTitle = '新对话',
|
|
|
|
|
|
agentName = '智能助手',
|
|
|
|
|
|
agentDescription = '',
|
2025-10-13 11:12:25 +08:00
|
|
|
|
messages = []
|
2026-01-15 06:01:34 +08:00
|
|
|
|
} = options || {}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-08 01:04:23 +08:00
|
|
|
|
const htmlContent = await this.generateHTML({
|
2025-08-25 13:57:47 +08:00
|
|
|
|
chatTitle,
|
|
|
|
|
|
agentName,
|
|
|
|
|
|
agentDescription,
|
2025-10-13 11:12:25 +08:00
|
|
|
|
messages
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' })
|
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
const timestamp = dayjs().tz('Asia/Shanghai').format('YYYYMMDD-HHmmss')
|
|
|
|
|
|
const safeTitle = chatTitle.replace(/[\\/:*?"<>|]/g, '_')
|
|
|
|
|
|
const filename = `${safeTitle}-${timestamp}.html`
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
link.href = url
|
|
|
|
|
|
link.download = filename
|
|
|
|
|
|
link.style.display = 'none'
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
URL.revokeObjectURL(url)
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return { success: true, filename }
|
2025-08-25 13:57:47 +08:00
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
console.error('导出对话失败:', error)
|
2026-05-07 11:58:25 +08:00
|
|
|
|
throw new Error(`导出失败: ${error.message}`, { cause: error })
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-13 11:12:25 +08:00
|
|
|
|
* 生成完整 HTML 内容
|
2025-08-25 13:57:47 +08:00
|
|
|
|
*/
|
2026-05-08 01:04:23 +08:00
|
|
|
|
static async generateHTML(options) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const { chatTitle, agentName, agentDescription, messages } = options
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const flattenedMessages = this.flattenMessages(messages)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (flattenedMessages.length === 0) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
throw new Error('没有可导出的对话内容')
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 01:04:23 +08:00
|
|
|
|
const messagesHTML = await this.generateMessagesHTML(flattenedMessages, agentName)
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
|
|
|
|
|
return this.generateHTMLTemplate({
|
|
|
|
|
|
chatTitle,
|
|
|
|
|
|
agentName,
|
|
|
|
|
|
agentDescription,
|
2025-10-13 15:08:54 +08:00
|
|
|
|
exportTime: dayjs().tz('Asia/Shanghai').format('YYYY年MM月DD日 HH:mm:ss'),
|
2025-08-25 13:57:47 +08:00
|
|
|
|
messagesHTML
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-13 11:12:25 +08:00
|
|
|
|
* 扁平化消息列表
|
2025-08-25 13:57:47 +08:00
|
|
|
|
*/
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static flattenMessages(messages = []) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const result = []
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
|
|
|
|
|
console.log('[ChatExporter] flattenMessages input:', {
|
|
|
|
|
|
messagesLength: messages?.length || 0,
|
|
|
|
|
|
messagesType: Array.isArray(messages) ? 'array' : typeof messages,
|
2026-01-15 06:01:34 +08:00
|
|
|
|
firstMessage: messages?.[0]
|
|
|
|
|
|
? {
|
|
|
|
|
|
hasMessages: Array.isArray(messages[0].messages),
|
|
|
|
|
|
hasType: !!messages[0].type,
|
|
|
|
|
|
hasRole: !!messages[0].role,
|
|
|
|
|
|
hasContent: !!messages[0].content,
|
|
|
|
|
|
keys: Object.keys(messages[0])
|
|
|
|
|
|
}
|
|
|
|
|
|
: null
|
|
|
|
|
|
})
|
|
|
|
|
|
;(messages || []).forEach((item) => {
|
|
|
|
|
|
if (!item) return
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(item.messages)) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
item.messages.forEach((msg) => {
|
|
|
|
|
|
if (msg) result.push(msg)
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
// 支持直接传入消息扁平数组
|
|
|
|
|
|
if (item.type || item.role || item.content) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
result.push(item)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return result
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成对话消息的 HTML 片段
|
|
|
|
|
|
*/
|
2026-05-08 01:04:23 +08:00
|
|
|
|
static async generateMessagesHTML(messages, agentName) {
|
|
|
|
|
|
const messageSegments = await Promise.all(
|
|
|
|
|
|
messages.map(async (msg) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const isUserMessage = ['human', 'user'].includes(msg?.type) || msg?.role === 'user'
|
|
|
|
|
|
const avatar = isUserMessage ? '👤' : '🤖'
|
|
|
|
|
|
const senderLabel = isUserMessage ? '用户' : agentName || '智能助手'
|
|
|
|
|
|
const messageClass = isUserMessage ? 'user-message' : 'ai-message'
|
|
|
|
|
|
const timestampRaw = this.getMessageTimestamp(msg)
|
|
|
|
|
|
const timestamp = this.escapeHtml(this.formatTimestamp(timestampRaw))
|
|
|
|
|
|
|
|
|
|
|
|
const { content, reasoning } = this.extractMessageContent(msg)
|
2026-05-08 01:04:23 +08:00
|
|
|
|
const [contentHTML, reasoningHTML] = await Promise.all([
|
|
|
|
|
|
content ? this.renderMarkdown(content) : '',
|
|
|
|
|
|
!isUserMessage ? this.generateReasoningHTML(reasoning) : ''
|
|
|
|
|
|
])
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const toolCallsHTML = !isUserMessage ? this.generateToolCallsHTML(msg) : ''
|
|
|
|
|
|
|
|
|
|
|
|
const bodySegments = [
|
|
|
|
|
|
reasoningHTML,
|
|
|
|
|
|
contentHTML ? `<div class="markdown-body">${contentHTML}</div>` : '',
|
|
|
|
|
|
toolCallsHTML
|
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
2025-08-25 13:57:47 +08:00
|
|
|
|
<div class="message ${messageClass}">
|
|
|
|
|
|
<div class="message-header">
|
|
|
|
|
|
<span class="avatar">${avatar}</span>
|
2025-10-13 11:12:25 +08:00
|
|
|
|
<span class="sender">${this.escapeHtml(senderLabel)}</span>
|
|
|
|
|
|
<span class="time">${timestamp}</span>
|
2025-08-25 13:57:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="message-content">
|
2025-10-13 11:12:25 +08:00
|
|
|
|
${bodySegments.length > 0 ? bodySegments.join('') : '<div class="empty-message">(此消息暂无可展示内容)</div>'}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
`
|
|
|
|
|
|
})
|
2026-05-08 01:04:23 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return messageSegments.join('')
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-13 11:12:25 +08:00
|
|
|
|
* 拆分消息内容与推理文本
|
2025-08-25 13:57:47 +08:00
|
|
|
|
*/
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static extractMessageContent(msg = {}) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const content = this.normalizeContent(msg?.content)
|
|
|
|
|
|
let reasoning = msg?.additional_kwargs?.reasoning_content || msg?.reasoning_content || ''
|
|
|
|
|
|
let visibleContent = content
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
|
|
|
|
|
if (!reasoning && content.includes('<think')) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const thinkRegex = /<think>([\s\S]*?)<\/think>|<think>([\s\S]*)$/i
|
|
|
|
|
|
const match = content.match(thinkRegex)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (match) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
reasoning = (match[1] || match[2] || '').trim()
|
|
|
|
|
|
visibleContent = content.replace(match[0], '').trim()
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
return {
|
|
|
|
|
|
content: visibleContent,
|
|
|
|
|
|
reasoning
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-13 11:12:25 +08:00
|
|
|
|
* 标准化消息内容
|
2025-08-25 13:57:47 +08:00
|
|
|
|
*/
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static normalizeContent(raw) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (raw == null) return ''
|
|
|
|
|
|
if (typeof raw === 'string') return raw
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(raw)) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return raw
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
if (!item) return ''
|
|
|
|
|
|
if (typeof item === 'string') return item
|
|
|
|
|
|
if (typeof item === 'object') {
|
|
|
|
|
|
return item.text || item.content || item.value || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return String(item)
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join('\n')
|
|
|
|
|
|
.trim()
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof raw === 'object') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (typeof raw.text === 'string') return raw.text
|
|
|
|
|
|
if (typeof raw.content === 'string') return raw.content
|
|
|
|
|
|
if (Array.isArray(raw.content)) return this.normalizeContent(raw.content)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return JSON.stringify(raw, null, 2)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
} catch {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return String(raw)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return String(raw)
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-13 11:12:25 +08:00
|
|
|
|
* 生成推理过程 HTML
|
2025-08-25 13:57:47 +08:00
|
|
|
|
*/
|
2026-05-08 01:04:23 +08:00
|
|
|
|
static async generateReasoningHTML(reasoning) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (!reasoning) return ''
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-05-08 01:04:23 +08:00
|
|
|
|
const reasoningHTML = await this.renderMarkdown(reasoning)
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (!reasoningHTML) return ''
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<details class="reasoning-section">
|
|
|
|
|
|
<summary class="reasoning-summary">💭 思考过程</summary>
|
|
|
|
|
|
<div class="reasoning-content markdown-body">
|
|
|
|
|
|
${reasoningHTML}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
</div>
|
2025-10-13 11:12:25 +08:00
|
|
|
|
</details>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
`
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-13 11:12:25 +08:00
|
|
|
|
* 生成工具调用 HTML
|
2025-08-25 13:57:47 +08:00
|
|
|
|
*/
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static generateToolCallsHTML(msg = {}) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const toolCalls = this.normalizeToolCalls(msg)
|
|
|
|
|
|
if (toolCalls.length === 0) return ''
|
|
|
|
|
|
|
|
|
|
|
|
const sections = toolCalls
|
|
|
|
|
|
.map((toolCall) => {
|
|
|
|
|
|
const toolName = this.escapeHtml(toolCall?.function?.name || toolCall?.name || '工具调用')
|
|
|
|
|
|
const argsSource = toolCall?.args ?? toolCall?.function?.arguments
|
|
|
|
|
|
const args = this.stringifyToolArgs(argsSource)
|
|
|
|
|
|
const result = this.normalizeToolResult(toolCall?.tool_call_result?.content)
|
|
|
|
|
|
const isFinished = toolCall?.status === 'success'
|
|
|
|
|
|
const stateClass = isFinished ? 'done' : 'pending'
|
|
|
|
|
|
const stateLabel = isFinished ? '已完成' : '执行中'
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
2025-10-13 11:12:25 +08:00
|
|
|
|
<details class="tool-call" ${isFinished ? '' : 'open'}>
|
|
|
|
|
|
<summary>
|
|
|
|
|
|
<span class="tool-call-title">🔧 ${toolName}</span>
|
|
|
|
|
|
<span class="tool-call-state ${stateClass}">${stateLabel}</span>
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<div class="tool-call-body">
|
2026-01-15 06:01:34 +08:00
|
|
|
|
${
|
|
|
|
|
|
args
|
|
|
|
|
|
? `
|
2025-10-13 11:12:25 +08:00
|
|
|
|
<div class="tool-call-args">
|
|
|
|
|
|
<strong>参数</strong>
|
|
|
|
|
|
<pre>${this.escapeHtml(args)}</pre>
|
|
|
|
|
|
</div>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
${
|
|
|
|
|
|
isFinished && result
|
|
|
|
|
|
? `
|
2025-10-13 11:12:25 +08:00
|
|
|
|
<div class="tool-call-result">
|
|
|
|
|
|
<strong>结果</strong>
|
|
|
|
|
|
<pre>${this.escapeHtml(result)}</pre>
|
|
|
|
|
|
</div>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
}
|
2025-10-13 11:12:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</details>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
`
|
|
|
|
|
|
})
|
|
|
|
|
|
.join('')
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return `<div class="tool-calls">${sections}</div>`
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static normalizeToolCalls(msg = {}) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const rawCalls = msg.tool_calls || msg.additional_kwargs?.tool_calls
|
|
|
|
|
|
if (!rawCalls) return []
|
|
|
|
|
|
if (Array.isArray(rawCalls)) return rawCalls.filter(Boolean)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (typeof rawCalls === 'object') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return Object.values(rawCalls).filter(Boolean)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return []
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static stringifyToolArgs(rawArgs) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (rawArgs == null || rawArgs === '') return ''
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (typeof rawArgs === 'string') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const trimmed = rawArgs.trim()
|
|
|
|
|
|
if (!trimmed) return ''
|
2025-10-13 11:12:25 +08:00
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return JSON.stringify(JSON.parse(trimmed), null, 2)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
} catch {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return trimmed
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (typeof rawArgs === 'object') {
|
|
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return JSON.stringify(rawArgs, null, 2)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
} catch {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return String(rawArgs)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return String(rawArgs)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static normalizeToolResult(result) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (!result) return ''
|
|
|
|
|
|
if (typeof result === 'string') return result.trim()
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (Array.isArray(result)) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return result
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
if (!item) return ''
|
|
|
|
|
|
if (typeof item === 'string') return item
|
|
|
|
|
|
if (typeof item === 'object') {
|
|
|
|
|
|
return item.text || item.content || JSON.stringify(item, null, 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
return String(item)
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join('\n\n')
|
|
|
|
|
|
.trim()
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (typeof result === 'object') {
|
|
|
|
|
|
if (typeof result.content !== 'undefined') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return this.normalizeToolResult(result.content)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
try {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return JSON.stringify(result, null, 2)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
} catch {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return String(result)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return String(result)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 统一的 Markdown 渲染,失败时回退到简单换行
|
|
|
|
|
|
*/
|
2026-05-08 01:04:23 +08:00
|
|
|
|
static async renderMarkdown(content) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (!content) return ''
|
2025-10-13 11:12:25 +08:00
|
|
|
|
try {
|
2026-05-08 01:04:23 +08:00
|
|
|
|
return (await renderMarkdownPreview(content)).trim()
|
2025-10-13 11:12:25 +08:00
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
console.warn('Markdown 渲染失败,回退为纯文本:', error)
|
|
|
|
|
|
return this.escapeHtml(content).replace(/\n/g, '<br>')
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
static escapeHtml(value) {
|
2026-05-08 01:04:23 +08:00
|
|
|
|
return escapeHtml(value)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 提取消息时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
static getMessageTimestamp(msg = {}) {
|
|
|
|
|
|
const candidates = [
|
|
|
|
|
|
msg.timestamp,
|
|
|
|
|
|
msg.created_at,
|
|
|
|
|
|
msg.createdAt,
|
|
|
|
|
|
msg.createdTime,
|
|
|
|
|
|
msg.time,
|
|
|
|
|
|
msg.datetime,
|
|
|
|
|
|
msg.date,
|
|
|
|
|
|
msg.additional_kwargs?.timestamp,
|
|
|
|
|
|
msg.additional_kwargs?.created_at
|
2026-01-15 06:01:34 +08:00
|
|
|
|
]
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return candidates.find((value) => value !== undefined && value !== null)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 格式化时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
static formatTimestamp(raw) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const fallback = dayjs().tz('Asia/Shanghai')
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
if (raw instanceof Date) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return dayjs(raw).tz('Asia/Shanghai').format('YYYY年MM月DD日 HH:mm:ss')
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 15:08:54 +08:00
|
|
|
|
if (raw || raw === 0) {
|
|
|
|
|
|
if (typeof raw === 'number') {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const value = raw < 1e12 ? raw * 1000 : raw
|
|
|
|
|
|
return dayjs(value).tz('Asia/Shanghai').format('YYYY年MM月DD日 HH:mm:ss')
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-10-13 15:08:54 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const parsed = parseToShanghai(raw)
|
2025-10-13 15:08:54 +08:00
|
|
|
|
if (parsed) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return parsed.format('YYYY年MM月DD日 HH:mm:ss')
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
return fallback.format('YYYY年MM月DD日 HH:mm:ss')
|
2025-10-13 11:12:25 +08:00
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-13 11:12:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成完整 HTML 文档骨架
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateHTMLTemplate(options) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const { chatTitle, agentName, agentDescription, exportTime, messagesHTML } = options
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const safeTitle = this.escapeHtml(chatTitle)
|
|
|
|
|
|
const safeAgentName = this.escapeHtml(agentName)
|
|
|
|
|
|
const safeDescription = this.escapeHtml(agentDescription).replace(/\n/g, '<br>')
|
|
|
|
|
|
const safeExportTime = this.escapeHtml(exportTime)
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const descriptionBlock = agentDescription ? `<br><strong>描述:</strong> ${safeDescription}` : ''
|
2025-10-13 11:12:25 +08:00
|
|
|
|
|
|
|
|
|
|
return chatExportTemplate
|
|
|
|
|
|
.replace(/{{TITLE}}/g, safeTitle)
|
|
|
|
|
|
.replace('{{AGENT_NAME}}', safeAgentName)
|
|
|
|
|
|
.replace('{{DESCRIPTION_BLOCK}}', descriptionBlock)
|
|
|
|
|
|
.replace('{{EXPORT_TIME}}', safeExportTime)
|
2026-01-15 06:01:34 +08:00
|
|
|
|
.replace('{{MESSAGES}}', messagesHTML)
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
export default ChatExporter
|