Warning: fopen(/www/sites/update.greasyfork.icu/index/store/temp/14a1ec57ef0e939c7262cf88e35125f5.js): failed to open stream: No space left on device in /www/sites/update.greasyfork.icu/index/scriptControl.php on line 65
// ==UserScript==
// @name DeepSeek Chat Exporter (Markdown & PDF & PNG - English improved version)
// @namespace http://tampermonkey.net/
// @version 1.8.3
// @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 = `
DeepSeek Chat Export
${mdContent.replace(new RegExp(`## ${config.userHeader}\\n\\n`, 'g'), `