// ==UserScript== // @name DeepSeek Chat Exporter (Markdown & PDF & PNG - English improved version) // @namespace http://tampermonkey.net/ // @version 1.8.2 // @description Export DeepSeek chat history to Markdown, PDF and PNG formats // @author HSyuf/Blueberrycongee/endolith // @match https://chat.deepseek.com/* // @grant GM_addStyle // @grant GM_download // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js // @downloadURL https://update.greasyfork.icu/scripts/529946/DeepSeek%20Chat%20Exporter%20%28Markdown%20%20PDF%20%20PNG%20-%20English%20improved%20version%29.user.js // @updateURL https://update.greasyfork.icu/scripts/529946/DeepSeek%20Chat%20Exporter%20%28Markdown%20%20PDF%20%20PNG%20-%20English%20improved%20version%29.meta.js // ==/UserScript== (function () { 'use strict'; // ===================== // Configuration // ===================== const config = { chatContainerSelector: '.dad65929', // Chat container userMessageSelector: '._9663006 > .fbb737a4', // Direct selector for user message content aiClassPrefix: '_4f9bf79', // AI message related class prefix aiReplyContainer: '_43c05b5', // Main container for AI replies searchHintSelector: '._58a6d71._19db599', // Search/thinking time thinkingChainSelector: '.e1675d8b', // Thinking chain finalAnswerSelector: 'div.ds-markdown.ds-markdown--block', // Final answer titleSelector: '.d8ed659a', // Chat title selector exportFileName: 'DeepSeek', // Changed from DeepSeek_Chat_Export // Header strings used in exports userHeader: 'User', assistantHeader: 'Assistant', thoughtsHeader: 'Thought Process', }; // User preferences with defaults const preferences = { convertLatexDelimiters: GM_getValue('convertLatexDelimiters', true), }; // Register menu command for toggling LaTeX delimiter conversion GM_registerMenuCommand('Toggle LaTeX Delimiter Conversion', () => { preferences.convertLatexDelimiters = !preferences.convertLatexDelimiters; GM_setValue('convertLatexDelimiters', preferences.convertLatexDelimiters); alert(`LaTeX delimiter conversion is now ${preferences.convertLatexDelimiters ? 'enabled' : 'disabled'}`); }); let __exportPNGLock = false; // Global lock to prevent duplicate clicks // ===================== // Tool functions // ===================== /** * Gets the message content if the node contains a user message, null otherwise * @param {HTMLElement} node - The DOM node to check * @returns {string|null} The user message content if found, null otherwise */ function getUserMessage(node) { const messageDiv = node.querySelector(config.userMessageSelector); return messageDiv ? messageDiv.firstChild.textContent.trim() : null; } /** * Checks if a DOM node represents an AI message * @param {HTMLElement} node - The DOM node to check * @returns {boolean} True if the node is an AI message */ function isAIMessage(node) { return node.classList.contains(config.aiClassPrefix); } /** * Extracts search or thinking time information from a node * @param {HTMLElement} node - The DOM node to extract from * @returns {string|null} Markdown formatted search/thinking info or null if not found */ function extractSearchOrThinking(node) { const hintNode = node.querySelector(config.searchHintSelector); return hintNode ? `**${hintNode.textContent.trim()}**` : null; } /** * Extracts and formats the AI's thinking chain as blockquotes * @param {HTMLElement} node - The DOM node containing the thinking chain * @returns {string|null} Markdown formatted thinking chain with header or null if not found */ function extractThinkingChain(node) { // Get the parent container first - this is the main AI reply container const containerNode = node; // Node is already the thinking chain container if (!containerNode) { console.debug('Could not find aiReplyContainer parent container'); return null; } // Get its React fiber - this connects the DOM to React's internal tree const fiberKey = Object.keys(containerNode).find(key => key.startsWith('__reactFiber$')); if (!fiberKey) return null; // Navigate the React fiber tree to find the content: let current = containerNode[fiberKey]; // Start at container div current = current.child; // First child: Empty div._9ecc93a current = current.sibling; // Sibling: Anonymous component current = current.child; // Child: Component with content prop // Check if we found the content if (!current?.memoizedProps?.content) { console.debug('Could not find markdown content in Memo'); return null; } return `### ${config.thoughtsHeader}\n\n> ${current.memoizedProps.content.split('\n').join('\n> ')}`; } /** * Extracts the final answer content from React fiber's memoizedProps * @param {HTMLElement} node - The DOM node containing the answer * @returns {string|null} Raw markdown content or null if not found */ function extractFinalAnswer(node) { const answerNode = node.querySelector(config.finalAnswerSelector); if (!answerNode) { console.debug('No answer node found'); return null; } // Get React fiber const fiberKey = Object.keys(answerNode).find(key => key.startsWith('__reactFiber$')); if (!fiberKey) { console.error('React fiber not found'); return null; } // Navigate directly to the markdown component (2 levels up) const fiber = answerNode[fiberKey]; // Start at div const level1 = fiber.return; // First parent const markdownComponent = level1?.return; // Second parent (has markdown) // If any navigation step failed or the component doesn't have markdown, return null if (!markdownComponent?.memoizedProps?.markdown) { console.error('Could not find markdown at expected location in React tree'); return null; } return markdownComponent.memoizedProps.markdown; } /** * Collects and formats all messages in the chat in chronological order * @returns {string[]} Array of markdown formatted messages */ function getOrderedMessages() { const messages = []; const chatContainer = document.querySelector(config.chatContainerSelector); if (!chatContainer) { console.error('Chat container not found'); return messages; } for (const node of chatContainer.children) { const userMessage = getUserMessage(node); if (userMessage) { messages.push(`## ${config.userHeader}\n\n${userMessage}`); } else if (isAIMessage(node)) { let output = ''; const searchHint = extractSearchOrThinking(node); if (searchHint) output += `${searchHint}\n\n`; const thinkingChainNode = node.querySelector(config.thinkingChainSelector); if (thinkingChainNode) { const thinkingChain = extractThinkingChain(thinkingChainNode); if (thinkingChain) output += `${thinkingChain}\n\n`; } const finalAnswer = extractFinalAnswer(node); if (finalAnswer) output += `${finalAnswer}\n\n`; if (output.trim()) { messages.push(`## ${config.assistantHeader}\n\n${output.trim()}`); } } } return messages; } /** * Extracts the chat title from the page * @returns {string|null} The chat title if found, null otherwise */ function getChatTitle() { const titleElement = document.querySelector(config.titleSelector); return titleElement ? titleElement.textContent.trim() : null; } /** * Generates the complete markdown content from all messages * @returns {string} Complete markdown formatted chat history */ function generateMdContent() { const messages = getOrderedMessages(); const title = getChatTitle(); let content = title ? `# ${title}\n\n` : ''; content += messages.length ? messages.join('\n\n---\n\n') : ''; // Convert LaTeX formats only if enabled if (preferences.convertLatexDelimiters) { // Use replacement functions to properly handle newlines and whitespace content = content // Inline math: \( ... \) → $ ... $ .replace(/\\\(\s*(.*?)\s*\\\)/g, (match, group) => `$${group}$`) // Display math: \[ ... \] → $$ ... $$ .replace(/\\\[([\s\S]*?)\\\]/g, (match, group) => `$$${group}$$`); } return content; } /** * Creates a filename-safe version of a string * @param {string} str - The string to make filename-safe * @param {number} maxLength - Maximum length of the resulting string * @returns {string} A filename-safe version of the input string */ function makeFilenameSafe(str, maxLength = 50) { if (!str) return ''; return str .replace(/[^a-zA-Z0-9-_\s]/g, '') // Remove special characters .replace(/\s+/g, '_') // Replace spaces with underscores .slice(0, maxLength) // Truncate to maxLength .replace(/_+$/, '') // Remove trailing underscores .trim(); } /** * Generates a filename-safe ISO 8601 timestamp * @returns {string} Formatted timestamp YYYY-MM-DD_HH_MM_SS */ function getFormattedTimestamp() { const now = new Date(); return now.toISOString() .replace(/[T:]/g, '_') // Replace T and : with _ .replace(/\..+/, ''); // Remove milliseconds and timezone } // ===================== // Export functions // ===================== /** * Exports the chat history as a markdown file * Handles math expressions and creates a downloadable .md file */ function exportMarkdown() { const mdContent = generateMdContent(); if (!mdContent) { alert("No chat history found!"); return; } const title = getChatTitle(); const safeTitle = makeFilenameSafe(title, 30); const titlePart = safeTitle ? `_${safeTitle}` : ''; const blob = new Blob([mdContent], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${config.exportFileName}${titlePart}_${getFormattedTimestamp()}.md`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); } /** * Exports the chat history as a PDF * Creates a styled HTML version and opens the browser's print dialog */ function exportPDF() { const mdContent = generateMdContent(); if (!mdContent) return; const printContent = `
`) .replace(/>\s/g, '') // Remove the blockquote markers for HTML .replace(/\n/g, '
') .replace(/---/g, '