// ==UserScript== // @name Kimi K2 聊天记录导出器 // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 🚀 专为Kimi K2版本设计的聊天记录导出工具,支持Markdown格式导出,包含丰富的图标和格式化内容 // @author AI Assistant // @match https://www.kimi.com/chat/* // @match https://kimi.com/chat/* // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCA5TDEzLjA5IDE1Ljc0TDEyIDIyTDEwLjkxIDE1Ljc0TDQgOUwxMC45MSA4LjI2TDEyIDJaIiBmaWxsPSIjRkY2QjZCIi8+Cjwvc3ZnPgo= // @grant GM_addStyle // @grant GM_download // @grant GM_getValue // @grant GM_setValue // @grant GM_notification // @run-at document-end // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/552371/Kimi%20K2%20%E8%81%8A%E5%A4%A9%E8%AE%B0%E5%BD%95%E5%AF%BC%E5%87%BA%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/552371/Kimi%20K2%20%E8%81%8A%E5%A4%A9%E8%AE%B0%E5%BD%95%E5%AF%BC%E5%87%BA%E5%99%A8.meta.js // ==/UserScript== (function() { 'use strict'; // 🎨 配置常量 const CONFIG = { EXPORT_BUTTON_TEXT: '📥 导出聊天记录', EXPORTING_TEXT: '⏳ 正在导出...', SUCCESS_TEXT: '✅ 导出成功', ERROR_TEXT: '❌ 导出失败', BUTTON_POSITION: 'fixed', BUTTON_Z_INDEX: '9999', BUTTON_BG_COLOR: '#10a37f', BUTTON_HOVER_COLOR: '#0d8c6d', MARKDOWN_THEME: 'github', INCLUDE_TIMESTAMP: true, INCLUDE_METADATA: true }; // 📝 图标和样式定义 const ICONS = { USER: '👤', ASSISTANT: '🤖', KIMI: '🌙', TIME: '⏰', LINK: '🔗', CODE: '💻', QUOTE: '📖', LIST: '📋', WARNING: '⚠️', SUCCESS: '✅', ERROR: '❌', INFO: 'ℹ️', EXPORT: '📥', DOWNLOAD: '💾', COPY: '📋', SETTINGS: '⚙️', HELP: '❓', IMAGE: '🖼️', HEADING: '📝' }; // 🔍 选择器定义 const SELECTORS = { CHAT_CONTAINER: '.chat-content-list', MESSAGE_ITEM: '.chat-content-item', USER_MESSAGE: '.chat-content-item-user', ASSISTANT_MESSAGE: '.chat-content-item-assistant', MESSAGE_CONTENT: '.segment-content', MESSAGE_TIME: '.segment-time', CHAT_TITLE: '.chat-header-content h2', SIDEBAR: '.sidebar-nav', EXPORT_BUTTON: '.kimi-export-btn', TOOLBAR: '.chat-action' }; // 🛠️ 工具函数 const Utils = { // 等待元素出现 waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`元素 ${selector} 未找到`)); }, timeout); }); }, // 格式化时间戳 formatTimestamp(date) { const now = new Date(); const diff = now - date; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return '刚刚'; if (minutes < 60) return `${minutes}分钟前`; if (hours < 24) return `${hours}小时前`; if (days < 7) return `${days}天前`; return date.toLocaleString('zh-CN'); }, // 转义Markdown特殊字符 escapeMarkdown(text) { return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); }, // 检测代码块 isCodeBlock(text) { const codeIndicators = [ 'function', 'const ', 'let ', 'var ', 'if(', 'for(', 'while(', 'import ', 'export ', 'class ', 'def ', '```', '{', '}' ]; return codeIndicators.some(indicator => text.includes(indicator)); }, // 检测链接 extractLinks(text) { const urlRegex = /(https?:\/\/[^\s]+)/g; return text.match(urlRegex) || []; }, // 生成唯一文件名 generateFileName(chatTitle) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const safeTitle = chatTitle.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50); return `kimi-chat-${safeTitle}-${timestamp}.md`; }, // 显示通知 showNotification(message, type = 'success') { const notification = document.createElement('div'); notification.className = `kimi-notification kimi-notification-${type}`; notification.innerHTML = `
${type === 'success' ? ICONS.SUCCESS : ICONS.ERROR} ${message}
`; document.body.appendChild(notification); // 添加动画样式 setTimeout(() => notification.classList.add('show'), 100); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => notification.remove(), 300); }, 3000); } }; // 🎨 样式管理器 const StyleManager = { init() { const style = document.createElement('style'); style.textContent = ` /* 导出按钮样式 */ .kimi-export-btn { position: ${CONFIG.BUTTON_POSITION}; top: 20px; right: 20px; z-index: ${CONFIG.BUTTON_Z_INDEX}; background: ${CONFIG.BUTTON_BG_COLOR}; color: white; border: none; padding: 12px 20px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(16, 163, 127, 0.3); display: flex; align-items: center; gap: 8px; } .kimi-export-btn:hover { background: ${CONFIG.BUTTON_HOVER_COLOR}; transform: translateY(-2px); box-shadow: 0 6px 20px rgba(16, 163, 127, 0.4); } .kimi-export-btn:active { transform: translateY(0); } .kimi-export-btn.exporting { background: #6c757d; cursor: not-allowed; } /* 通知样式 */ .kimi-notification { position: fixed; top: 20px; right: 20px; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); padding: 16px 20px; z-index: 10000; transform: translateX(400px); transition: transform 0.3s ease; max-width: 300px; } .kimi-notification.show { transform: translateX(0); } .kimi-notification-success { border-left: 4px solid #28a745; } .kimi-notification-error { border-left: 4px solid #dc3545; } .kimi-notification-content { display: flex; align-items: center; gap: 12px; } .kimi-notification-icon { font-size: 18px; } .kimi-notification-text { font-size: 14px; color: #333; } /* 导出选项面板 */ .kimi-export-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); padding: 24px; z-index: 10001; max-width: 400px; width: 90%; } .kimi-export-panel h3 { margin: 0 0 20px 0; color: #333; font-size: 18px; display: flex; align-items: center; gap: 8px; } .kimi-export-options { display: flex; flex-direction: column; gap: 16px; margin-bottom: 24px; } .kimi-export-option { display: flex; align-items: center; gap: 12px; } .kimi-export-option input[type="checkbox"] { width: 18px; height: 18px; accent-color: #10a37f; } .kimi-export-option label { font-size: 14px; color: #555; cursor: pointer; } .kimi-export-actions { display: flex; gap: 12px; justify-content: flex-end; } .kimi-btn { padding: 8px 16px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; } .kimi-btn-primary { background: #10a37f; color: white; } .kimi-btn-secondary { background: #f8f9fa; color: #6c757d; border: 1px solid #dee2e6; } .kimi-btn:hover { transform: translateY(-1px); } /* 遮罩层 */ .kimi-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 10000; } /* 加载动画 */ .kimi-loading { display: inline-block; width: 16px; height: 16px; border: 2px solid #ffffff; border-radius: 50%; border-top-color: transparent; animation: kimi-spin 1s ease-in-out infinite; } @keyframes kimi-spin { to { transform: rotate(360deg); } } /* 响应式设计 */ @media (max-width: 768px) { .kimi-export-btn { top: auto; bottom: 20px; right: 20px; padding: 10px 16px; font-size: 12px; } } `; document.head.appendChild(style); } }; // 📊 数据提取器 const DataExtractor = { // 提取聊天消息(改进版,支持富文本格式) extractMessages() { const messages = []; const messageElements = document.querySelectorAll(SELECTORS.MESSAGE_ITEM); messageElements.forEach((element, index) => { try { const isUser = element.classList.contains('chat-content-item-user'); const isAssistant = element.classList.contains('chat-content-item-assistant'); // 提取富文本内容,保留格式 const content = this.extractRichContent(element); if (content && content.trim()) { messages.push({ id: `msg-${index}`, role: isUser ? 'user' : 'assistant', content: content.trim(), timestamp: this.extractTimestamp(element), type: this.detectRichContentType(element), metadata: this.extractRichMetadata(element) }); } } catch (error) { console.warn('提取消息失败:', error); } }); return messages; }, // 提取富文本内容(改进版) extractRichContent(element) { const contentElement = element.querySelector(SELECTORS.MESSAGE_CONTENT); if (!contentElement) return ''; // 克隆元素以避免修改原始DOM const clone = contentElement.cloneNode(true); // 处理不同类型的内容 let content = ''; // 1. 处理代码块 const codeBlocks = clone.querySelectorAll('pre'); codeBlocks.forEach(codeBlock => { const codeContent = codeBlock.textContent; const language = this.detectCodeLanguage(codeBlock); const codeMarkdown = `\n\`\`\`${language}\n${codeContent}\n\`\`\`\n`; // 用占位符替换代码块,避免重复处理 const placeholder = `__CODE_BLOCK_${Math.random().toString(36).substr(2, 9)}__`; codeBlock.outerHTML = placeholder; content = content || clone.innerHTML; content = content.replace(placeholder, codeMarkdown); }); // 2. 处理表格 const tables = clone.querySelectorAll('table'); tables.forEach(table => { const tableMarkdown = this.convertTableToMarkdown(table); const placeholder = `__TABLE_${Math.random().toString(36).substr(2, 9)}__`; table.outerHTML = placeholder; content = content || clone.innerHTML; content = content.replace(placeholder, tableMarkdown); }); // 3. 处理引用块 const quotes = clone.querySelectorAll('blockquote'); quotes.forEach(quote => { const quoteContent = this.cleanHtmlAndConvertInlineFormat(quote.innerHTML); const quotedLines = quoteContent.split('\n').map(line => `> ${line}`).join('\n'); const placeholder = `__QUOTE_${Math.random().toString(36).substr(2, 9)}__`; quote.outerHTML = placeholder; content = content || clone.innerHTML; content = content.replace(placeholder, `\n\n${quotedLines}\n\n`); }); // 4. 处理列表 const lists = clone.querySelectorAll('ol, ul'); lists.forEach(list => { const listMarkdown = this.convertListToMarkdown(list); const placeholder = `__LIST_${Math.random().toString(36).substr(2, 9)}__`; list.outerHTML = placeholder; content = content || clone.innerHTML; content = content.replace(placeholder, listMarkdown); }); // 5. 处理图片 const images = clone.querySelectorAll('img'); images.forEach(img => { const alt = img.alt || ''; const src = img.src || ''; const title = img.title || ''; const imageMarkdown = title ? `![${alt}](${src} "${title}")` : `![${alt}](${src})`; const placeholder = `__IMAGE_${Math.random().toString(36).substr(2, 9)}__`; img.outerHTML = placeholder; content = content || clone.innerHTML; content = content.replace(placeholder, `\n\n${imageMarkdown}\n\n`); }); // 6. 处理剩余内容 if (!content) { content = clone.innerHTML; } // 7. 清理HTML标签并转换内联格式 content = this.cleanHtmlAndConvertInlineFormat(content); return content.trim(); }, // 检测代码语言 detectCodeLanguage(codeBlock) { const codeElement = codeBlock.querySelector('code'); if (!codeElement) return ''; // 检查class属性 const classes = codeElement.className.split(' '); for (const cls of classes) { if (cls.startsWith('language-')) { return cls.replace('language-', ''); } } // 常见的代码语言关键词检测 const codeText = codeElement.textContent; const languagePatterns = { 'javascript': /\b(function|const|let|var|if|else|for|while|return)\b/, 'python': /\b(def|class|import|from|if __name__|print)\b/, 'java': /\b(public|private|class|void|static|String)\b/, 'cpp': /\b(#include|int main|std::|cout|cin)\b/, 'html': /\<\/?(div|span|p|a|img|html|head|body)/, 'css': /\{[^}]*:[^;]*;/, 'sql': /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b/i, 'bash': /\b(echo|cd|ls|mkdir|sudo|#!\/bin\/bash)\b/, 'json': /^\s*[\{\[]/, 'xml': /\<\?xml/, 'markdown': /\[.*\]\(.*\)|#{1,6}\s|\*\*.*\*\*/ }; for (const [lang, pattern] of Object.entries(languagePatterns)) { if (pattern.test(codeText)) { return lang; } } return ''; }, // 转换表格为Markdown convertTableToMarkdown(table) { const rows = table.querySelectorAll('tr'); if (rows.length === 0) return ''; let markdown = '\n'; const maxCols = Math.max(...Array.from(rows).map(row => row.querySelectorAll('th, td').length)); rows.forEach((row, rowIndex) => { const cells = Array.from(row.querySelectorAll('th, td')); const cellContents = cells.map(cell => cell.textContent.trim()); // 补齐缺失的单元格 while (cellContents.length < maxCols) { cellContents.push(''); } markdown += '| ' + cellContents.join(' | ') + ' |\n'; // 表头分隔线 if (rowIndex === 0) { markdown += '|' + Array(maxCols).fill('---').join('|') + '|\n'; } }); return markdown + '\n'; }, // 转换列表为Markdown(改进版,支持嵌套) convertListToMarkdown(list, level = 0) { const items = list.querySelectorAll(':scope > li'); if (items.length === 0) return ''; const isOrdered = list.tagName.toLowerCase() === 'ol'; let markdown = '\n'; const indent = ' '.repeat(level); items.forEach((item, index) => { const prefix = isOrdered ? `${index + 1}.` : '-'; const content = this.cleanHtmlAndConvertInlineFormat(item.innerHTML); const cleanContent = content.replace(/\n/g, ' ').trim(); markdown += `${indent}${prefix} ${cleanContent}\n`; // 处理嵌套列表 const nestedLists = item.querySelectorAll(':scope > ul, :scope > ol'); nestedLists.forEach(nestedList => { const nestedMarkdown = this.convertListToMarkdown(nestedList, level + 1); markdown += nestedMarkdown; }); }); return markdown + '\n'; }, // 清理HTML并转换内联格式(改进版) cleanHtmlAndConvertInlineFormat(content) { // 先处理链接,保留文本内容 content = this.processLinksInContent(content); // 处理图片 content = this.processImagesInContent(content); // 转换内联格式 const replacements = { '': '**', '': '**', '': '**', '': '**', '': '*', '': '*', '': '*', '': '*', '': '`', '': '`', '
': '\n', '
': '\n', '

': '\n\n', '

': '\n\n', '

': '# ', '

': '\n\n', '

': '## ', '

': '\n\n', '

': '### ', '

': '\n\n', '

': '#### ', '

': '\n\n', '
': '##### ', '
': '\n\n', '
': '###### ', '
': '\n\n', '
': '\n', '
': '\n', '': '', '': '' }; let cleaned = content; for (const [html, markdown] of Object.entries(replacements)) { const escaped = html.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(escaped, 'g'); cleaned = cleaned.replace(regex, markdown); } // 清理剩余的HTML标签,但保留实体 cleaned = cleaned.replace(/<[^>]*>/g, ''); // 转换HTML实体 const entities = { ' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ''': "'", '…': '...', '—': '—', '–': '–' }; for (const [entity, char] of Object.entries(entities)) { cleaned = cleaned.replace(new RegExp(entity, 'g'), char); } // 清理多余的空行和空格 cleaned = cleaned.replace(/\n{3,}/g, '\n\n'); cleaned = cleaned.replace(/[ \t]+/g, ' '); cleaned = cleaned.replace(/^\s+|\s+$/g, ''); return cleaned.trim(); }, // 处理内容中的链接 processLinksInContent(content) { // 匹配完整的标签 const linkRegex = /]*>(.*?)<\/a>/gi; return content.replace(linkRegex, (match, href, title, linkText) => { // 清理链接文本中的HTML标签 const cleanText = linkText.replace(/<[^>]*>/g, '').trim(); const linkTitle = title ? ` "${title}"` : ''; return `[${cleanText}](${href}${linkTitle})`; }); }, // 处理内容中的图片 processImagesInContent(content) { // 匹配完整的标签 const imgRegex = /]*>/gi; return content.replace(imgRegex, (match, src, alt, title) => { const imgAlt = alt || ''; const imgTitle = title ? ` "${title}"` : ''; return `![${imgAlt}](${src}${imgTitle})`; }); }, // 提取时间戳 extractTimestamp(element) { const timeElement = element.querySelector(SELECTORS.MESSAGE_TIME); if (timeElement) { return new Date(timeElement.textContent.trim()); } // 尝试从其他属性获取 const timeAttr = element.getAttribute('data-time') || element.getAttribute('data-timestamp'); if (timeAttr) { return new Date(parseInt(timeAttr)); } return new Date(); }, // 检测富文本内容类型(改进版) detectRichContentType(element) { const contentElement = element.querySelector(SELECTORS.MESSAGE_CONTENT); if (!contentElement) return 'text'; // 统计不同类型的元素数量 const codeBlocks = contentElement.querySelectorAll('pre').length; const inlineCodes = contentElement.querySelectorAll('code:not(pre code)').length; const tables = contentElement.querySelectorAll('table').length; const quotes = contentElement.querySelectorAll('blockquote').length; const orderedLists = contentElement.querySelectorAll('ol').length; const unorderedLists = contentElement.querySelectorAll('ul').length; const headings = contentElement.querySelectorAll('h1, h2, h3, h4, h5, h6').length; const images = contentElement.querySelectorAll('img').length; const links = contentElement.querySelectorAll('a').length; // 根据主要特征确定类型 if (codeBlocks > 0) return 'code-block'; if (tables > 0) return 'table'; if (quotes > 0) return 'quote'; if (orderedLists > 0) return 'ordered-list'; if (unorderedLists > 0) return 'unordered-list'; // 混合内容类型 if (headings > 0) return 'with-headings'; if (images > 0) return 'with-images'; if (links > 0) return 'with-links'; if (inlineCodes > 0) return 'inline-code'; return 'text'; }, // 提取富文本元数据 extractRichMetadata(element) { const metadata = {}; const contentElement = element.querySelector(SELECTORS.MESSAGE_CONTENT); if (!contentElement) return metadata; // 提取代码语言(如果有代码块) const codeBlock = contentElement.querySelector('pre code'); if (codeBlock) { metadata.language = this.detectCodeLanguage(codeBlock.closest('pre')); } // 提取表格结构(如果有表格) const table = contentElement.querySelector('table'); if (table) { metadata.hasTable = true; metadata.tableRows = table.querySelectorAll('tr').length; metadata.tableCols = Math.max(...Array.from(table.querySelectorAll('tr')).map(row => row.querySelectorAll('th, td').length)); } // 提取链接信息 const links = contentElement.querySelectorAll('a[href]'); if (links.length > 0) { metadata.links = Array.from(links).map(link => ({ text: link.textContent.trim(), url: link.href, title: link.title || '' })); } // 提取图片信息 const images = contentElement.querySelectorAll('img[src]'); if (images.length > 0) { metadata.images = Array.from(images).map(img => ({ alt: img.alt || '', src: img.src, title: img.title || '', placeholder: `__IMAGE_${Math.random().toString(36).substr(2, 9)}__` })); } // 提取引用信息 const quotes = contentElement.querySelectorAll('blockquote'); if (quotes.length > 0) { metadata.quotes = quotes.length; } // 提取列表信息 const lists = contentElement.querySelectorAll('ol, ul'); if (lists.length > 0) { metadata.lists = lists.length; metadata.listItems = Array.from(lists).reduce((total, list) => total + list.querySelectorAll('li').length, 0); } // 提取标题信息 const headings = contentElement.querySelectorAll('h1, h2, h3, h4, h5, h6'); if (headings.length > 0) { metadata.headings = Array.from(headings).map(h => ({ level: parseInt(h.tagName.charAt(1)), text: h.textContent.trim() })); } return metadata; }, // 提取聊天标题 extractChatTitle() { const titleElement = document.querySelector(SELECTORS.CHAT_TITLE); return titleElement ? titleElement.textContent.trim() : 'Kimi 聊天记录'; }, // 提取用户信息 extractUserInfo() { const userInfo = { name: '用户', avatar: null }; // 尝试从localStorage获取用户信息 try { const userData = localStorage.getItem('user_info'); if (userData) { const parsed = JSON.parse(userData); if (parsed.name) userInfo.name = parsed.name; if (parsed.avatar) userInfo.avatar = parsed.avatar; } } catch (error) { console.warn('无法解析用户信息:', error); } return userInfo; } }; // 📝 Markdown生成器 const MarkdownGenerator = { // 生成Markdown文档 generate(messages, chatTitle, userInfo, options = {}) { const { includeTimestamp = true, includeMetadata = true } = options; let markdown = ''; // 文档头部 markdown += this.generateHeader(chatTitle); // 元数据信息 if (includeMetadata) { markdown += this.generateMetadata(chatTitle, userInfo, messages.length); } // 目录 markdown += this.generateTableOfContents(messages); // 消息内容 markdown += this.generateMessages(messages, includeTimestamp); // 文档尾部 markdown += this.generateFooter(); return markdown; }, // 生成文档头部 generateHeader(title) { return `# ${ICONS.KIMI} ${title} > ${ICONS.INFO} 本文档由 Kimi K2 聊天记录导出器生成 > > 生成时间:${new Date().toLocaleString('zh-CN')} --- `; }, // 生成元数据 generateMetadata(chatTitle, userInfo, messageCount) { return `## ${ICONS.INFO} 会话信息 | 项目 | 内容 | |------|------| | ${ICONS.KIMI} 会话标题 | ${chatTitle} | | ${ICONS.USER} 用户 | ${userInfo.name} | | ${ICONS.TIME} 消息总数 | ${messageCount} 条 | | ${ICONS.TIME} 导出时间 | ${new Date().toLocaleString('zh-CN')} | --- `; }, // 生成目录 generateTableOfContents(messages) { let toc = `## ${ICONS.LIST} 目录\n\n`; messages.forEach((message, index) => { const roleIcon = message.role === 'user' ? ICONS.USER : ICONS.ASSISTANT; const preview = message.content.substring(0, 30).replace(/\n/g, ' '); const suffix = message.content.length > 30 ? '...' : ''; toc += `${index + 1}. [${roleIcon} ${message.role === 'user' ? '用户' : 'Kimi'}: ${preview}${suffix}](#message-${index + 1})\n`; }); return toc + '\n---\n\n'; }, // 生成消息内容(改进版,显示内容类型) generateMessages(messages, includeTimestamp) { let content = `## ${ICONS.KIMI} 对话内容\n\n`; messages.forEach((message, index) => { const roleIcon = message.role === 'user' ? ICONS.USER : ICONS.ASSISTANT; const roleName = message.role === 'user' ? '用户' : 'Kimi'; const contentTypeIcon = this.getContentTypeIcon(message.type); const contentTypeDesc = this.getContentTypeDescription(message.type); content += `### ${roleIcon} ${roleName}\n\n`; // 显示内容类型信息 if (message.type !== 'text') { content += `> ${contentTypeIcon} **内容类型**: ${contentTypeDesc}\n`; // 显示元数据信息 if (message.metadata) { if (message.metadata.language) { content += `> ${ICONS.CODE} **代码语言**: \`${message.metadata.language}\`\n`; } if (message.metadata.hasTable) { content += `> ${ICONS.TABLE} **表格**: ${message.metadata.tableRows}行 × ${message.metadata.tableCols}列\n`; } if (message.metadata.quotes) { content += `> ${ICONS.QUOTE} **引用块**: ${message.metadata.quotes}个\n`; } if (message.metadata.lists) { content += `> ${ICONS.LIST} **列表**: ${message.metadata.lists}个列表,共${message.metadata.listItems}项\n`; } if (message.metadata.headings) { content += `> ${ICONS.HEADING} **标题**: ${message.metadata.headings.length}个\n`; } if (message.metadata.images) { content += `> ${ICONS.IMAGE} **图片**: ${message.metadata.images.length}张\n`; } if (message.metadata.links) { content += `> ${ICONS.LINK} **链接**: ${message.metadata.links.length}个\n`; } } content += '\n'; } if (includeTimestamp && message.timestamp) { content += `> ${ICONS.TIME} ${Utils.formatTimestamp(message.timestamp)}\n\n`; } // 根据消息类型格式化内容 content += this.formatMessageContent(message); content += '\n---\n\n'; }); return content; }, // 获取内容类型图标 getContentTypeIcon(type) { const iconMap = { 'code-block': ICONS.CODE, 'inline-code': '💻', 'table': ICONS.TABLE, 'quote': ICONS.QUOTE, 'ordered-list': '📋', 'unordered-list': '•', 'with-headings': ICONS.HEADING, 'with-images': ICONS.IMAGE, 'with-links': ICONS.LINK, 'text': '📝' }; return iconMap[type] || '📝'; }, // 获取内容类型描述 getContentTypeDescription(type) { const descMap = { 'code-block': '代码块', 'inline-code': '内联代码', 'table': '表格', 'quote': '引用', 'ordered-list': '有序列表', 'unordered-list': '无序列表', 'with-headings': '包含标题', 'with-images': '包含图片', 'with-links': '包含链接', 'text': '普通文本' }; return descMap[type] || '普通文本'; }, // 格式化消息内容(改进版) formatMessageContent(message) { let content = message.content; // 根据消息类型进行格式化 switch (message.type) { case 'code-block': // 代码块已经在新提取器中处理过了,直接返回 return content + '\n'; case 'table': // 表格已经在新提取器中处理过了,直接返回 return content + '\n'; case 'quote': // 引用已经在新提取器中处理过了,直接返回 return content + '\n'; case 'ordered-list': case 'unordered-list': // 列表已经在新提取器中处理过了,直接返回 return content + '\n'; case 'with-headings': // 标题已经在提取器中转换为Markdown格式 return content + '\n'; case 'with-images': // 处理图片 - 使用元数据中保存的占位符 if (message.metadata?.images) { message.metadata.images.forEach(img => { const imgMarkdown = `![${img.alt}](${img.src})`; if (img.placeholder) { // 使用保存的占位符进行替换 content = content.replace(new RegExp(img.placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), imgMarkdown); } }); } return content + '\n'; case 'with-links': // 链接已经在提取器中处理过了 return content + '\n'; case 'inline-code': // 内联代码已经在提取器中处理过了 return content + '\n'; default: // 普通文本,按段落处理 return content.split('\n\n').map(paragraph => { return paragraph.replace(/\n/g, ' \n'); }).join('\n\n') + '\n'; } }, // 生成文档尾部 generateFooter() { return `--- ## ${ICONS.HELP} 使用说明 - 📋 **复制代码**:点击代码块右上角的复制按钮 - 🔗 **打开链接**:直接点击文档中的链接 - 📖 **导航**:使用文档开头的目录快速跳转到指定消息 - 💾 **保存**:建议将本文档保存为本地文件以便长期保存 ---
**${ICONS.KIMI} 本文档由 Kimi K2 聊天记录导出器自动生成** *生成时间:${new Date().toLocaleString('zh-CN')}*
`; } }; // 🎛️ 导出管理器 const ExportManager = { isExporting: false, // 初始化导出功能 async init() { try { await Utils.waitForElement(SELECTORS.CHAT_CONTAINER); this.createExportButton(); this.setupEventListeners(); console.log('🚀 Kimi K2 导出器已初始化'); } catch (error) { console.error('❌ 初始化导出器失败:', error); } }, // 创建导出按钮 createExportButton() { const button = document.createElement('button'); button.className = 'kimi-export-btn'; button.innerHTML = ` ${ICONS.EXPORT} ${CONFIG.EXPORT_BUTTON_TEXT} `; // 添加到页面 document.body.appendChild(button); // 保存按钮引用 this.exportButton = button; }, // 设置事件监听器 setupEventListeners() { this.exportButton.addEventListener('click', () => { this.showExportPanel(); }); // 监听聊天内容变化 this.observeChatChanges(); }, // 观察聊天内容变化 observeChatChanges() { const observer = new MutationObserver(() => { // 聊天内容发生变化时的处理 console.log('📊 检测到聊天内容变化'); }); const chatContainer = document.querySelector(SELECTORS.CHAT_CONTAINER); if (chatContainer) { observer.observe(chatContainer, { childList: true, subtree: true }); } }, // 显示导出面板 showExportPanel() { const overlay = document.createElement('div'); overlay.className = 'kimi-overlay'; const panel = document.createElement('div'); panel.className = 'kimi-export-panel'; panel.innerHTML = `

${ICONS.EXPORT} 导出选项

`; overlay.appendChild(panel); document.body.appendChild(overlay); // 设置事件监听器 this.setupPanelListeners(overlay, panel); }, // 设置面板事件监听器 setupPanelListeners(overlay, panel) { const cancelBtn = panel.querySelector('#cancel-export'); const confirmBtn = panel.querySelector('#confirm-export'); cancelBtn.addEventListener('click', () => { overlay.remove(); }); confirmBtn.addEventListener('click', () => { const options = { includeTimestamp: panel.querySelector('#include-timestamp').checked, includeMetadata: panel.querySelector('#include-metadata').checked, includeToc: panel.querySelector('#include-toc').checked }; overlay.remove(); this.performExport(options); }); // 点击遮罩关闭 overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); } }); }, // 执行导出 async performExport(options) { if (this.isExporting) return; this.isExporting = true; this.updateButtonState(true); try { console.log('🚀 开始导出聊天记录...'); // 提取数据 const messages = DataExtractor.extractMessages(); const chatTitle = DataExtractor.extractChatTitle(); const userInfo = DataExtractor.extractUserInfo(); console.log(`📊 提取到 ${messages.length} 条消息`); if (messages.length === 0) { throw new Error('未找到聊天消息'); } // 生成Markdown const markdown = MarkdownGenerator.generate(messages, chatTitle, userInfo, options); // 下载文件 const fileName = Utils.generateFileName(chatTitle); this.downloadFile(markdown, fileName); // 显示成功通知 Utils.showNotification(CONFIG.SUCCESS_TEXT, 'success'); console.log(`✅ 导出成功: ${fileName}`); } catch (error) { console.error('❌ 导出失败:', error); Utils.showNotification(`${CONFIG.ERROR_TEXT}: ${error.message}`, 'error'); } finally { this.isExporting = false; this.updateButtonState(false); } }, // 更新按钮状态 updateButtonState(isExporting) { if (isExporting) { this.exportButton.classList.add('exporting'); this.exportButton.innerHTML = ` ${CONFIG.EXPORTING_TEXT} `; this.exportButton.disabled = true; } else { this.exportButton.classList.remove('exporting'); this.exportButton.innerHTML = ` ${ICONS.EXPORT} ${CONFIG.EXPORT_BUTTON_TEXT} `; this.exportButton.disabled = false; } }, // 下载文件 downloadFile(content, fileName) { const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } }; // 🚀 主程序 class KimiK2Exporter { constructor() { this.version = '2.0.0'; this.initialized = false; } async init() { try { console.log(`🚀 初始化 Kimi K2 聊天记录导出器 v${this.version}`); // 初始化样式 StyleManager.init(); // 等待页面加载完成 await this.waitForPageLoad(); // 初始化导出管理器 await ExportManager.init(); this.initialized = true; console.log('✅ Kimi K2 导出器初始化完成'); // 显示欢迎通知 setTimeout(() => { Utils.showNotification('🎉 Kimi K2 聊天记录导出器已就绪!', 'success'); }, 1000); } catch (error) { console.error('❌ 初始化失败:', error); Utils.showNotification('初始化失败,请刷新页面重试', 'error'); } } // 等待页面加载完成 async waitForPageLoad() { return new Promise((resolve) => { if (document.readyState === 'complete') { resolve(); } else { window.addEventListener('load', resolve); } }); } // 获取版本信息 getVersion() { return this.version; } // 检查是否已初始化 isInitialized() { return this.initialized; } } // 🎯 启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { const exporter = new KimiK2Exporter(); exporter.init(); // 将导出器实例挂载到全局,便于调试 window.__kimiK2Exporter = exporter; }); } else { const exporter = new KimiK2Exporter(); exporter.init(); window.__kimiK2Exporter = exporter; } console.log('🌟 Kimi K2 聊天记录导出器脚本已加载'); })();