DeepSeek Chat Exporter (Markdown & PDF)

导出 DeepSeek 聊天记录为 Markdown 和 PDF 格式

目前为 2025-02-01 提交的版本。查看 最新版本

// ==UserScript==
// @name         DeepSeek Chat Exporter (Markdown & PDF)
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  导出 DeepSeek 聊天记录为 Markdown 和 PDF 格式
// @author       HSyuf/Blueberrycongee
// @match        https://chat.deepseek.com/*
// @grant        GM_addStyle
// @grant        GM_download
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';

    // =====================
    // 配置
    // =====================
    const config = {
        chatContainerSelector: '.dad65929', // 聊天框容器
        userClassPrefix: 'fa81',             // 用户消息 class 前缀
        aiClassPrefix: 'f9bf7997',           // AI消息相关 class 前缀
        aiReplyContainer: 'edb250b1',        // AI回复的主要容器
        searchHintSelector: '.a6d716f5.db5991dd', // 搜索/思考时间
        thinkingChainSelector: '.e1675d8b',  // 思考链
        finalAnswerSelector: 'div.ds-markdown.ds-markdown--block', // 正式回答
        exportFileName: 'DeepSeek_Chat_Export',
    };

    function isUserMessage(node) {
        return node.classList.contains(config.userClassPrefix);
    }

    function isAIMessage(node) {
        return node.classList.contains(config.aiClassPrefix);
    }

    function extractSearchOrThinking(node) {
        const hintNode = node.querySelector(config.searchHintSelector);
        return hintNode ? `**${hintNode.textContent.trim()}**` : null;
    }

    function extractThinkingChain(node) {
        const thinkingNode = node.querySelector(config.thinkingChainSelector);
        return thinkingNode ? `**思考链**\n${thinkingNode.textContent.trim()}` : null;
    }

    function extractFinalAnswer(node) {
        const answerNode = node.querySelector(config.finalAnswerSelector);
        if (!answerNode) return null;

        let answerContent = '';

        // 遍历ds-markdown--block中的每个p、h3、hr和数学公式
        const elements = answerNode.querySelectorAll('.ds-markdown--block p, .ds-markdown--block h3, .katex-display.ds-markdown-math, hr');

        elements.forEach((element) => {
            // 如果是段落<p>,遍历其中的text和数学公式
            if (element.tagName.toLowerCase() === 'p') {
                element.childNodes.forEach((childNode) => {
                    if (childNode.nodeType === Node.TEXT_NODE) {
                        answerContent += childNode.textContent.trim();  // 文本节点
                    } else if (childNode.classList && childNode.classList.contains('katex')) {
                        // KaTeX公式提取
                        const tex = childNode.querySelector('annotation[encoding="application/x-tex"]');
                        if (tex) {
                            answerContent += `$$$${tex.textContent.trim()}$$$`; // 用$$包裹所有公式
                        }
                    }
                });
                answerContent += '\n\n';  // 段落结束后添加换行
            }
            // 如果是h3标签,处理为Markdown标题
            else if (element.tagName.toLowerCase() === 'h3') {
                answerContent += `### ${element.textContent.trim()}\n\n`;  // 将h3转为Markdown的三级标题
            }
            // 处理块级数学公式
            else if (element.classList.contains('katex-display')) {
                const tex = element.querySelector('annotation[encoding="application/x-tex"]');
                if (tex) {
                    answerContent += `$$$${tex.textContent.trim()}$$$\n\n`;  // 块级数学公式
                }
            }
            // 如果是<hr>标签,转换为Markdown分割线
            else if (element.tagName.toLowerCase() === 'hr') {
                answerContent += '\n---\n';  // 转换为Markdown分割线
            }
        });

        // 添加Markdown标题
        return `**正式回答**\n${answerContent.trim()}`;
    }





    function getOrderedMessages() {
        const messages = [];
        const chatContainer = document.querySelector(config.chatContainerSelector);
        if (!chatContainer) {
            console.error('未找到聊天容器');
            return messages;
        }

        for (const node of chatContainer.children) {
            if (isUserMessage(node)) {
                messages.push(`**用户:**\n${node.textContent.trim()}`);
            } else if (isAIMessage(node)) {
                let output = '';
                const aiReplyContainer = node.querySelector(`.${config.aiReplyContainer}`);
                if (aiReplyContainer) {
                    const searchHint = extractSearchOrThinking(aiReplyContainer);
                    if (searchHint) output += `${searchHint}\n\n`;
                    const thinkingChain = extractThinkingChain(aiReplyContainer);
                    if (thinkingChain) output += `${thinkingChain}\n\n`;
                } else {
                    const searchHint = extractSearchOrThinking(node);
                    if (searchHint) output += `${searchHint}\n\n`;
                }
                const finalAnswer = extractFinalAnswer(node);
                if (finalAnswer) output += `${finalAnswer}\n\n`;
                if (output.trim()) {
                    messages.push(output.trim());
                }
            }
        }
        return messages;
    }

    function generateMdContent() {
        const messages = getOrderedMessages();
        return messages.length ? messages.join('\n\n---\n\n') : '';
    }

    function exportMarkdown() {
        const mdContent = generateMdContent();
        if (!mdContent) {
            alert("未找到聊天记录!");
            return;
        }

        // Fix for inline math and block math rendering
        const fixedMdContent = mdContent.replace(/(\*\*.*?\*\*)/g, '<strong>$1</strong>') // Ensures **bold** in Markdown
        .replace(/\(\s*([^)]*)\s*\)/g, '\\($1\\)') // Inline math: \( f(x,y) \)
        .replace(/\$\$\s*([^$]*)\s*\$\$/g, '$$$1$$'); // Block math: $$ \frac{dy}{dx} = \frac{y^2}{x^2 + 1} $$

        const blob = new Blob([fixedMdContent], { type: 'text/markdown' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${config.exportFileName}_${Date.now()}.md`;
        a.click();
        setTimeout(() => URL.revokeObjectURL(url), 5000);
    }

    function exportPDF() {
        const mdContent = generateMdContent();
        if (!mdContent) return;

        // Fix for inline math and block math rendering in HTML for PDF
        const fixedMdContent = mdContent.replace(/(\*\*.*?\*\*)/g, '<strong>$1</strong>') // Bold text
        .replace(/\(\s*([^)]*)\s*\)/g, '\\($1\\)') // Inline math
        .replace(/\$\$\s*([^$]*)\s*\$\$/g, '$$$1$$'); // Block math

        const printContent = `
      <html>
        <head>
          <title>DeepSeek Chat Export</title>
          <style>
            body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: 0 auto; }
            h2 { color: #2c3e50; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
            .ai-answer { color: #1a7f37; margin: 15px 0; }
            .ai-chain { color: #666; font-style: italic; margin: 10px 0; }
            hr { border: 0; border-top: 1px solid #eee; margin: 25px 0; }
          </style>
        </head>
        <body>
          ${fixedMdContent.replace(/\*\*用户:\*\*\n/g, '<h2>用户提问</h2><div class="user-question">')
        .replace(/\*\*正式回答\*\*\n/g, '</div><h2>AI 回答</h2><div class="ai-answer">')
        .replace(/\*\*思考链\*\*\n/g, '</div><h2>思维链</h2><div class="ai-chain">')
        .replace(/\n/g, '<br>')
        .replace(/---/g, '</div><hr>')}
        </body>
      </html>
    `;

        const printWindow = window.open("", "_blank");
        printWindow.document.write(printContent);
        printWindow.document.close();
        setTimeout(() => { printWindow.print(); printWindow.close(); }, 500);
    }

  // =====================
  // 添加导出菜单
  // =====================
  function createExportMenu() {
    const menu = document.createElement("div");
    menu.className = "ds-exporter-menu";
    menu.innerHTML = `
      <button class="export-btn" id="md-btn">导出为 Markdown</button>
      <button class="export-btn" id="pdf-btn">导出为 PDF</button>
    `;

    menu.querySelector("#md-btn").addEventListener("click", exportMarkdown);
    menu.querySelector("#pdf-btn").addEventListener("click", exportPDF);
    document.body.appendChild(menu);
  }

  // =====================
  // 样式注入
  // =====================
  GM_addStyle(`
    .ds-exporter-menu {
      position: fixed;
      top: 20px;
      right: 20px;
      z-index: 9999;
      background: rgba(255, 255, 255, 0.95);
      padding: 12px;
      border-radius: 8px;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
      display: flex;
      flex-direction: column;
      gap: 8px;
      backdrop-filter: blur(4px);
    }
    .export-btn {
      background: #2196F3;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
      transition: all 0.2s;
    }
    .export-btn:hover {
      background: #1976D2;
      transform: translateY(-1px);
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
    }
  `);

  // =====================
  // 初始化脚本
  // =====================
  function init() {
    const checkInterval = setInterval(() => {
      if (document.querySelector(".fa81")) {
        clearInterval(checkInterval);
        createExportMenu();
      }
    }, 500);
  }

  init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址