您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
导出 DeepSeek 聊天记录为 PDF、HTML、Markdown、JSON、TXT 和 Word
// ==UserScript== // @name DeepSeek Chat Exporter // @namespace http://tampermonkey.net/ // @version 1.0.9 // @description 导出 DeepSeek 聊天记录为 PDF、HTML、Markdown、JSON、TXT 和 Word // @author deepseek-ai.online // @match *://chat.deepseek.com/* // @match *://deepseek.com/* // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js // @grant GM_addStyle // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_getResourceText // @resource hljsCSS https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css // ==/UserScript== (function() { 'use strict'; // 国际化支持 const i18n = { "export_btn": "导出聊天", "export_to_pdf": "导出为 PDF", "export_to_html": "导出为 HTML", "export_to_markdown": "导出为 Markdown", "export_to_json": "导出为 JSON", "export_to_txt": "导出为 TXT", "export_to_word": "导出为 Word", "export_progress": "导出中", "export_user": "用户", "export_pdf_error": "PDF导出失败,请重试", "export_success": "导出成功!" }; // 添加样式 GM_addStyle(` .export-button { position: fixed; top: 30px; right: 20px; background: #4a90e2; color: white; padding: 12px 20px; border-radius: 30px; cursor: pointer; display: flex; align-items: center; gap: 8px; font-weight: 500; font-size: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 9999; transition: all 0.3s ease; border: none; } .export-button:hover { background: #3a7bc8; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2); } .export-icon { font-size: 18px; font-weight: bold; } .export-menu { position: fixed; top: 70px; right: 20px; background: white; border-radius: 12px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); padding: 10px 0; z-index: 9998; opacity: 0; visibility: hidden; transform: translateY(10px); transition: all 0.3s ease; width: 220px; } .export-menu.show { opacity: 1; visibility: visible; transform: translateY(0); } .export-option { padding: 12px 20px; cursor: pointer; display: flex; align-items: center; gap: 12px; font-size: 15px; transition: all 0.2s; } .export-option:hover { background: #f5f7fa; } .export-option-icon { font-size: 18px; width: 24px; text-align: center; } .message { margin-bottom: 20px; padding: 15px; border-radius: 8px; } .user { background-color: #f5f5f5; border-left: 4px solid #4a90e2; } .assistant { background-color: #f8f9fa; border-left: 4px solid #6c757d; } .role { font-weight: bold; margin-bottom: 8px; color: #333; } .content { white-space: pre-wrap; color: #2c3e50; line-height: 1.6; } pre { background-color: #f8f9fa; padding: 15px; border-radius: 6px; overflow-x: auto; margin: 15px 0; border-left: 3px solid #4a90e2; } code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; } .hljs { display: block; overflow-x: auto; padding: 1em; border-radius: 4px; } .citation-link { color: #4a90e2; text-decoration: none; font-weight: bold; margin: 0 2px; } .citation-link:hover { text-decoration: underline; } .progress-indicator { position: fixed; top: 70px; right: 20px; background: #4a90e2; color: white; padding: 10px 20px; border-radius: 20px; z-index: 10000; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); animation: pulse 1.5s infinite; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } } `); // 添加highlight.js样式 const hljsCSS = GM_getResourceText("hljsCSS"); GM_addStyle(hljsCSS); // 初始化 $(document).ready(function() { // 只在DeepSeek网站上添加导出按钮 if (window.location.hostname === 'chat.deepseek.com' || window.location.hostname === 'deepseek.com') { // 如果元素存在且按钮还没有添加,则添加导出按钮 if (!document.querySelector('.export-button')) { addExportButton(); } } }); // 页面加载完成后开始检查 document.addEventListener('DOMContentLoaded', function() { // 检查是否在正确的域名下 if (window.location.hostname === 'chat.deepseek.com' || window.location.hostname === 'deepseek.com') { // 开始等待聊天元素加载 waitForChatElements(); // 监听DOM变化,处理动态加载的内容 const observer = new MutationObserver(function(mutations) { if (!document.querySelector('.export-button')) { waitForChatElements(); } }); observer.observe(document.body, { childList: true, subtree: true }); } }); function waitForChatElements() { const maxAttempts = 10; let attempts = 0; const checkInterval = setInterval(() => { const chatContainer = document.querySelector('.fbb737a4, .f9bf7997, ._4f9bf79'); if (chatContainer || attempts >= maxAttempts) { clearInterval(checkInterval); if (chatContainer) { addExportButton(); } } attempts++; }, 500); } function addExportButton() { const button = $(` <div class="export-button"> <span class="export-icon">↓</span> <span class="export-text">${i18n.export_btn}</span> </div> `); const menu = $(` <div class="export-menu"> <div class="export-option" data-format="pdf"> <span class="export-option-icon">📄</span> ${i18n.export_to_pdf} </div> <div class="export-option" data-format="html"> <span class="export-option-icon">🌐</span> ${i18n.export_to_html} </div> <div class="export-option" data-format="markdown"> <span class="export-option-icon">📝</span> ${i18n.export_to_markdown} </div> <div class="export-option" data-format="json"> <span class="export-option-icon">{ }</span> ${i18n.export_to_json} </div> <div class="export-option" data-format="txt"> <span class="export-option-icon">📃</span> ${i18n.export_to_txt} </div> <div class="export-option" data-format="word"> <span class="export-option-icon">📎</span> ${i18n.export_to_word} </div> </div> `); $('body').append(button).append(menu); // 当用户开始导出时显示加载状态 function showExporting(format) { button.html(` <span class="export-icon">⭕</span> <span class="export-text">${i18n.export_progress}...</span> `); button.css('pointer-events', 'none'); } // 显示进度指示器 function showProgressIndicator() { const progress = $(` <div class="progress-indicator"> ${i18n.export_progress}... </div> `); $('body').append(progress); return progress; } // 导出完成后恢复按钮状态 function resetButton() { button.html(` <span class="export-icon">↓</span> <span class="export-text">${i18n.export_btn}</span> `); button.css('pointer-events', 'auto'); } // 点击按钮显示/隐藏菜单 button.click(function(e) { e.stopPropagation(); menu.toggleClass('show'); }); // 点击其他地方关闭菜单 $(document).click(function() { menu.removeClass('show'); }); // 处理导出选项点击 $('.export-option').click(async function() { const format = $(this).data('format'); const $this = $(this); // 保存当前按钮的引用 menu.removeClass('show'); showExporting(format); // 添加进度指示器 const progress = showProgressIndicator(); // 禁用按钮防止重复点击 $('.export-option').prop('disabled', true); try { // 使用setTimeout确保UI更新 await new Promise(resolve => setTimeout(resolve, 100)); // 等待导出完成 await exportChat(format); // 显示成功消息 progress.text(i18n.export_success); setTimeout(() => progress.remove(), 2000); } catch (error) { console.error('导出错误:', error); progress.text('导出失败: ' + error.message); progress.css('background', '#e74c3c'); setTimeout(() => progress.remove(), 3000); } finally { resetButton(); // 重新启用按钮 $('.export-option').prop('disabled', false); } }); } function convertCitationsToLinks(htmlString, urlList) { // 正则表达式匹配 <span class="ds-markdown-cite">数字</span> const regex = /<span class="ds-markdown-cite">(\d+)<\/span>/g; // 替换匹配项 return htmlString.replace(regex, (match, number) => { const index = parseInt(number) - 1; // 转换为数组索引 // 验证索引有效性 if (index >= 0 && index < urlList.length && urlList[index]) { return `[<a href="${urlList[index]}" class="citation-link" target="_blank">${number}</a>]`; } else { return match; // 返回原始内容 } }); } function removeTokenTags(str) { // 匹配class以"token"开头后跟多个单词的span标签 const regex = /<span\s+[^>]*class=(['"])token\s[^'"]*\1[^>]*>([\s\S]*?)<\/span>/g; let prev; do { prev = str; str = str.replace(regex, (_, quote, content) => { // 无条件转换所有实体 return content .replace(/</g, '<') // 转换所有<为< .replace(/>/g, '>'); // 转换所有>为> }); } while (str !== prev); return str; } function clickSearchButton(button){ const propKey = Object.keys(button).filter(key => key.startsWith('__reactProps$'))[0]; if (propKey) { button[propKey].onClick(); } else { console.warn(`button has no react prop`); } return new Promise(resolve => setTimeout(resolve, 800)); } async function getChatContent() { const messages = []; let urlList = []; const btnList = Array.from(document.querySelectorAll('._58a6d71._19db599')).filter((_, index) => index % 2 === 0); const messageElements = document.querySelectorAll('.fbb737a4, .f9bf7997, ._4f9bf79'); let cnt = 0; const propKey = Object.keys(btnList[0]).filter(key => key.startsWith('__reactProps$'))[0]; const isSearched = btnList[0][propKey].onClick ? 1 : 0; function waitForElement(selector, timeout = 3000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const checkInterval = setInterval(() => { const element = document.querySelector(selector); if (element) { clearInterval(checkInterval); resolve(element); } else if (Date.now() - startTime > timeout) { clearInterval(checkInterval); reject(new Error(`Element ${selector} not found within ${timeout}ms`)); } }, 100); }); } // 使用 for...of 循环替代 forEach 以支持 await for (const element of messageElements) { const role = element.classList.contains('fbb737a4') ? 'user' : 'assistant'; let content = ''; if (role === 'user') { // 获取用户消息内容 urlList = []; content = element.textContent || ''; if (cnt < btnList.length && isSearched === 1) { const btnNow = btnList[cnt]; cnt++; // 点击按钮展开引用 await clickSearchButton(btnNow); try { // 等待引用加载 await waitForElement('._426ebf9._79fcd13._5130389', 1000); const citationList = document.querySelectorAll('._426ebf9._79fcd13._5130389'); console.log(`${citationList.length} citations found`); // 收集引用链接 for (const node of citationList) { const fiberKey = Object.keys(node).find(key => key.startsWith('__reactFiber$')); if (fiberKey) { const fiber = node[fiberKey]; if (fiber?.return?.key) { urlList.push(fiber.return.key); } } } // 再次点击关闭引用 await clickSearchButton(btnNow); } catch (error) { console.error('Error processing citations:', error); } } } else { // 处理助手消息 const thinkElement = element.querySelector('.e1675d8b'); if (thinkElement) { content += "<p>思考:</p><blockquote>" + thinkElement.innerHTML + "</blockquote><br/>"; } const contentElement = element.querySelector('.ds-markdown'); if (contentElement) { content += contentElement.innerHTML; } // 处理代码块 content = content.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/g, (_, code) => { return '\n```\n' + code + '\n```\n'; }); // 处理行内代码 content = content.replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`'); // 转换引用标记为链接 content = convertCitationsToLinks(content, urlList); content = removeTokenTags(content); } messages.push({ role, content: content.trim(), timestamp: new Date().toISOString() }); } return messages; } async function exportAsPDF(content, contentName) { // 创建一个临时的 HTML 容器 const container = document.createElement('div'); container.innerHTML = formatContentAsHTML(content); // 将容器添加到文档中,但不可见 container.style.position = 'absolute'; container.style.left = '-9999px'; document.body.appendChild(container); // 配置 html2pdf 选项 const opt = { margin: [0.5, 0.75, 0.5, 0.75], filename: `${contentName}.pdf`, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 1, useCORS: true, logging: false }, jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' } }; try { // 执行导出 await html2pdf().set(opt).from(container).save(); } catch (error) { console.error('PDF导出失败:', error); alert(i18n.export_pdf_error); } finally { // 清理临时容器 document.body.removeChild(container); } } function exportAsHTML(content, contentName) { const html = formatContentAsHTML(content); downloadFile(html, `${contentName}.html`, 'text/html'); } function exportAsJSON(content, contentName) { // 格式化 content 中每个消息,移除 HTML 标签和样式 const formattedContent = content.map(msg => { const parser = new DOMParser(); const doc = parser.parseFromString(msg.content, 'text/html'); const strippedContent = doc.body.textContent || doc.body.innerText; // 提取纯文本内容 return { ...msg, content: strippedContent }; // 返回格式化后的消息 }); const json = JSON.stringify(formattedContent, null, 2); // 格式化 JSON 输出 downloadFile(json, `${contentName}.json`, 'application/json'); } function exportAsText(content, contentName) { const text = content.map(msg => { // 使用 DOMParser 解析 HTML 并提取纯文本 const parser = new DOMParser(); const doc = parser.parseFromString(msg.content, 'text/html'); const strippedContent = doc.body.textContent || doc.body.innerText; // 提取纯文本内容 return `${msg.role === 'user' ? i18n.export_user : 'DeepSeek AI'}: ${strippedContent}`; }).join('\n\n'); downloadFile(text, `${contentName}.txt`, 'text/plain'); } function exportAsMarkdown(content, contentName) { const markdown = content.map(msg => { return `### ${msg.role === 'user' ? i18n.export_user : 'DeepSeek AI'}\n\n${msg.content}\n\n`; }).join('---\n\n'); downloadFile(markdown, `${contentName}.md`, 'text/markdown'); } function exportAsWord(content, contentName) { // 格式化内容为适合 Word 的 HTML 格式 const html = formatContentAsHTML(content); // 为了避免样式错乱,嵌入一些基本的样式和格式 const wordHtml = ` <html xmlns:w="urn:schemas-microsoft-com:office:word"> <head> <meta charset="UTF-8"> <title>${contentName}</title> <style> body { font-family: Calibri, sans-serif; font-size: 12pt; margin: 20px; } .message { margin-bottom: 20px; } .role { font-weight: bold; margin-bottom: 5px; } pre { background: #f5f5f5; padding: 10px; border-radius: 4px; } code { font-family: Consolas, monospace; } </style> </head> <body> <div>${html}</div> </body> </html> `; downloadFile(wordHtml, `${contentName}.doc`, 'application/msword'); } async function exportChat(format) { // 获取聊天内容 const chatContent = await getChatContent(); //let chatContent = {}; const contentName = getChatContentName(); switch (format) { case 'pdf': exportAsPDF(chatContent, contentName); break; case 'html': exportAsHTML(chatContent, contentName); break; case 'markdown': exportAsMarkdown(chatContent, contentName); break; case 'json': exportAsJSON(chatContent, contentName); break; case 'txt': exportAsText(chatContent, contentName); break; case 'word': exportAsWord(chatContent, contentName); break; } } function formatContentAsHTML(content) { return ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>DeepSeek Chat Export</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; color: #333; } .message { margin-bottom: 25px; padding: 18px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } .user { background-color: #f0f7ff; border-left: 4px solid #4a90e2; } .assistant { background-color: #f8f9fa; border-left: 4px solid #6c757d; } .role { font-weight: bold; margin-bottom: 10px; color: #2c3e50; font-size: 16px; } .content { white-space: pre-wrap; color: #2c3e50; line-height: 1.7; } pre { background-color: #f8f9fa; padding: 15px; border-radius: 6px; overflow-x: auto; margin: 15px 0; border-left: 3px solid #4a90e2; } code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; } .citation-link { color: #4a90e2; text-decoration: none; font-weight: bold; margin: 0 2px; } .citation-link:hover { text-decoration: underline; } </style> </head> <body> <h1 style="text-align: center; margin-bottom: 30px; color: #2c3e50;">DeepSeek 聊天记录</h1> ${content.map(msg => ` <div class="message ${msg.role}"> <div class="role">${msg.role === 'user' ? i18n.export_user : 'DeepSeek AI'}</div> <div class="content">${formatMessageContent(msg.content)}</div> </div> `).join('')} <div style="text-align: center; margin-top: 40px; color: #7f8c8d; font-size: 14px;"> 使用 DeepSeek Chat Exporter 导出 - ${new Date().toLocaleDateString()} </div> </body> </html> `; } // 添加消息内容格式化函数 function formatMessageContent(content) { // 使用marked解析Markdown内容 marked.setOptions({ highlight: function(code, lang) { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; }, breaks: true, gfm: true }); try { // 先处理可能的HTML实体 content = content .replace(/>/g, '>') .replace(/</g, '<') .replace(/&/g, '&'); // 使用marked解析Markdown return marked.parse(content); } catch (error) { console.error('Markdown解析错误:', error); return content; } } function downloadFile(content, filename, type) { const blob = new Blob([content], { type: type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 100); } function getChatContentName() { let content = ""; // 尝试获取标题元素 const titleElement = document.querySelector('h1, .d8ed659a, .b64fb9ae'); if (titleElement) { content = titleElement.innerText.trim(); } // 如果仍然没有内容,使用默认值 if (!content) { content = "DeepSeek-Chat-Export"; } else { // 清理文件名中的无效字符 content = content.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '-'); content = `DeepSeek-${content}`; } // 添加日期时间戳 const now = new Date(); const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`; const timeStr = `${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}`; return `${content}-${dateStr}-${timeStr}`; } })();