diff --git a/web/src/utils/chatExporter.js b/web/src/utils/chatExporter.js
deleted file mode 100644
index ad799a63..00000000
--- a/web/src/utils/chatExporter.js
+++ /dev/null
@@ -1,435 +0,0 @@
-import { escapeHtml } from '@/utils/html'
-import { renderMarkdown as renderMarkdownPreview } from '@/utils/markdown_preview'
-import dayjs, { parseToShanghai } from '@/utils/time'
-import chatExportTemplate from './templates/chat-export-template.html?raw'
-
-export class ChatExporter {
- /**
- * 导出聊天对话为 HTML 文件
- * @param {Object} options 导出选项
- */
- static async exportToHTML(options = {}) {
- const {
- chatTitle = '新对话',
- agentName = '智能助手',
- agentDescription = '',
- messages = []
- } = options || {}
-
- try {
- const htmlContent = await this.generateHTML({
- chatTitle,
- agentName,
- agentDescription,
- messages
- })
-
- 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`
-
- link.href = url
- link.download = filename
- link.style.display = 'none'
-
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- URL.revokeObjectURL(url)
-
- return { success: true, filename }
- } catch (error) {
- console.error('导出对话失败:', error)
- throw new Error(`导出失败: ${error.message}`, { cause: error })
- }
- }
-
- /**
- * 生成完整 HTML 内容
- */
- static async generateHTML(options) {
- const { chatTitle, agentName, agentDescription, messages } = options
-
- const flattenedMessages = this.flattenMessages(messages)
- if (flattenedMessages.length === 0) {
- throw new Error('没有可导出的对话内容')
- }
-
- const messagesHTML = await this.generateMessagesHTML(flattenedMessages, agentName)
-
- return this.generateHTMLTemplate({
- chatTitle,
- agentName,
- agentDescription,
- exportTime: dayjs().tz('Asia/Shanghai').format('YYYY年MM月DD日 HH:mm:ss'),
- messagesHTML
- })
- }
-
- /**
- * 扁平化消息列表
- */
- static flattenMessages(messages = []) {
- const result = []
-
- console.log('[ChatExporter] flattenMessages input:', {
- messagesLength: messages?.length || 0,
- messagesType: Array.isArray(messages) ? 'array' : typeof messages,
- 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
-
- if (Array.isArray(item.messages)) {
- item.messages.forEach((msg) => {
- if (msg) result.push(msg)
- })
- return
- }
-
- // 支持直接传入消息扁平数组
- if (item.type || item.role || item.content) {
- result.push(item)
- }
- })
-
- return result
- }
-
- /**
- * 生成对话消息的 HTML 片段
- */
- static async generateMessagesHTML(messages, agentName) {
- const messageSegments = await Promise.all(
- messages.map(async (msg) => {
- 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)
- const [contentHTML, reasoningHTML] = await Promise.all([
- content ? this.renderMarkdown(content) : '',
- !isUserMessage ? this.generateReasoningHTML(reasoning) : ''
- ])
- const toolCallsHTML = !isUserMessage ? this.generateToolCallsHTML(msg) : ''
-
- const bodySegments = [
- reasoningHTML,
- contentHTML ? `
${contentHTML}
` : '',
- toolCallsHTML
- ].filter(Boolean)
-
- return `
-
-
-
- ${bodySegments.length > 0 ? bodySegments.join('') : '
(此消息暂无可展示内容)
'}
-
-
- `
- })
- )
-
- return messageSegments.join('')
- }
-
- /**
- * 拆分消息内容与推理文本
- */
- static extractMessageContent(msg = {}) {
- const content = this.normalizeContent(msg?.content)
- let reasoning = msg?.additional_kwargs?.reasoning_content || msg?.reasoning_content || ''
- let visibleContent = content
-
- if (!reasoning && content.includes('([\s\S]*?)<\/think>|([\s\S]*)$/i
- const match = content.match(thinkRegex)
- if (match) {
- reasoning = (match[1] || match[2] || '').trim()
- visibleContent = content.replace(match[0], '').trim()
- }
- }
-
- return {
- content: visibleContent,
- reasoning
- }
- }
-
- /**
- * 标准化消息内容
- */
- static normalizeContent(raw) {
- if (raw == null) return ''
- if (typeof raw === 'string') return raw
-
- if (Array.isArray(raw)) {
- 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()
- }
-
- if (typeof raw === 'object') {
- 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)
- try {
- return JSON.stringify(raw, null, 2)
- } catch {
- return String(raw)
- }
- }
-
- return String(raw)
- }
-
- /**
- * 生成推理过程 HTML
- */
- static async generateReasoningHTML(reasoning) {
- if (!reasoning) return ''
-
- const reasoningHTML = await this.renderMarkdown(reasoning)
- if (!reasoningHTML) return ''
-
- return `
-
- 💭 思考过程
-
- ${reasoningHTML}
-
-
- `
- }
-
- /**
- * 生成工具调用 HTML
- */
- static generateToolCallsHTML(msg = {}) {
- 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 `
-
-
- 🔧 ${toolName}
- ${stateLabel}
-
-
- ${
- args
- ? `
-
- `
- : ''
- }
- ${
- isFinished && result
- ? `
-
- `
- : ''
- }
-
-
- `
- })
- .join('')
-
- return `${sections}
`
- }
-
- static normalizeToolCalls(msg = {}) {
- const rawCalls = msg.tool_calls || msg.additional_kwargs?.tool_calls
- if (!rawCalls) return []
- if (Array.isArray(rawCalls)) return rawCalls.filter(Boolean)
- if (typeof rawCalls === 'object') {
- return Object.values(rawCalls).filter(Boolean)
- }
- return []
- }
-
- static stringifyToolArgs(rawArgs) {
- if (rawArgs == null || rawArgs === '') return ''
-
- if (typeof rawArgs === 'string') {
- const trimmed = rawArgs.trim()
- if (!trimmed) return ''
- try {
- return JSON.stringify(JSON.parse(trimmed), null, 2)
- } catch {
- return trimmed
- }
- }
-
- if (typeof rawArgs === 'object') {
- try {
- return JSON.stringify(rawArgs, null, 2)
- } catch {
- return String(rawArgs)
- }
- }
-
- return String(rawArgs)
- }
-
- static normalizeToolResult(result) {
- if (!result) return ''
- if (typeof result === 'string') return result.trim()
-
- if (Array.isArray(result)) {
- 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()
- }
-
- if (typeof result === 'object') {
- if (typeof result.content !== 'undefined') {
- return this.normalizeToolResult(result.content)
- }
- try {
- return JSON.stringify(result, null, 2)
- } catch {
- return String(result)
- }
- }
-
- return String(result)
- }
-
- /**
- * 统一的 Markdown 渲染,失败时回退到简单换行
- */
- static async renderMarkdown(content) {
- if (!content) return ''
- try {
- return (await renderMarkdownPreview(content)).trim()
- } catch (error) {
- console.warn('Markdown 渲染失败,回退为纯文本:', error)
- return this.escapeHtml(content).replace(/\n/g, '
')
- }
- }
-
- static escapeHtml(value) {
- return escapeHtml(value)
- }
-
- /**
- * 提取消息时间戳
- */
- 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
- ]
-
- return candidates.find((value) => value !== undefined && value !== null)
- }
-
- /**
- * 格式化时间戳
- */
- static formatTimestamp(raw) {
- const fallback = dayjs().tz('Asia/Shanghai')
-
- if (raw instanceof Date) {
- return dayjs(raw).tz('Asia/Shanghai').format('YYYY年MM月DD日 HH:mm:ss')
- }
-
- if (raw || raw === 0) {
- if (typeof raw === 'number') {
- const value = raw < 1e12 ? raw * 1000 : raw
- return dayjs(value).tz('Asia/Shanghai').format('YYYY年MM月DD日 HH:mm:ss')
- }
-
- const parsed = parseToShanghai(raw)
- if (parsed) {
- return parsed.format('YYYY年MM月DD日 HH:mm:ss')
- }
- }
-
- return fallback.format('YYYY年MM月DD日 HH:mm:ss')
- }
-
- /**
- * 生成完整 HTML 文档骨架
- */
- static generateHTMLTemplate(options) {
- const { chatTitle, agentName, agentDescription, exportTime, messagesHTML } = options
-
- const safeTitle = this.escapeHtml(chatTitle)
- const safeAgentName = this.escapeHtml(agentName)
- const safeDescription = this.escapeHtml(agentDescription).replace(/\n/g, '
')
- const safeExportTime = this.escapeHtml(exportTime)
-
- const descriptionBlock = agentDescription ? `
描述: ${safeDescription}` : ''
-
- return chatExportTemplate
- .replace(/{{TITLE}}/g, safeTitle)
- .replace('{{AGENT_NAME}}', safeAgentName)
- .replace('{{DESCRIPTION_BLOCK}}', descriptionBlock)
- .replace('{{EXPORT_TIME}}', safeExportTime)
- .replace('{{MESSAGES}}', messagesHTML)
- }
-}
-
-export default ChatExporter
diff --git a/web/src/utils/templates/chat-export-template.html b/web/src/utils/templates/chat-export-template.html
deleted file mode 100644
index f842874b..00000000
--- a/web/src/utils/templates/chat-export-template.html
+++ /dev/null
@@ -1,318 +0,0 @@
-
-
-
-
-
- {{TITLE}} - 对话导出
-
-
-
-
-
-
diff --git a/web/src/views/AgentView.vue b/web/src/views/AgentView.vue
index 51cf77d6..1cd51e28 100644
--- a/web/src/views/AgentView.vue
+++ b/web/src/views/AgentView.vue
@@ -87,79 +87,30 @@
文件
-
-
-
-
-
-
-
-
-
-
-