DeepSeek 对话导出

将 Deepseek 对话导出与复制的工具

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         DeepSeek 对话导出
// @name:en      DeepSeek Chat Export
// @namespace    http://tampermonkey.net/
// @version      1.25.0313
// @description  将 Deepseek 对话导出与复制的工具
// @description:en  The tool for exporting and copying dialogues in Deepseek
// @author       木炭
// @copyright	 © 2025 木炭
// @license      MIT
// @supportURL   https://github.com/woodcoal/deepseek-chat-export
// @homeUrl      https://www.mutan.vip/
// @lastmodified 2025-02-27
// @match        https://chat.deepseek.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deepseek.com
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @run-at       document-body
// ==/UserScript==

(function () {
	('use strict');
	const BUTTON_ID = 'DS_MarkdownExport';
	let isProcessing = false;

	GM_addStyle(`
        #${BUTTON_ID}-container {
            position: fixed !important;
            top: 20px !important;
            right: 20px !important;
            z-index: 2147483647 !important;
            display: flex !important;
            gap: 8px !important;
        }
        #${BUTTON_ID}, #${BUTTON_ID}-copy {
            padding: 4px !important;
            cursor: pointer !important;
            transition: all 0.2s ease !important;
            opacity: 0.3 !important;
            background: none !important;
            border: none !important;
            font-size: 20px !important;
            position: relative !important;
        }
        #${BUTTON_ID}:hover, #${BUTTON_ID}-copy:hover {
            opacity: 1 !important;
            transform: scale(1.1) !important;
        }
        #${BUTTON_ID}:hover::after, #${BUTTON_ID}-copy:hover::after {
            content: attr(title) !important;
            position: absolute !important;
            top: 100% !important;
            left: 50% !important;
            transform: translateX(-50%) !important;
            background: rgba(0, 0, 0, 0.8) !important;
            color: white !important;
            padding: 4px 8px !important;
            border-radius: 4px !important;
            font-size: 12px !important;
            white-space: nowrap !important;
            z-index: 1000 !important;
        }
        .ds-toast {
            position: fixed !important;
            top: 20px !important;
            left: 50% !important;
            transform: translateX(-50%) !important;
            color: white !important;
            padding: 8px 16px !important;
            border-radius: 4px !important;
            font-size: 14px !important;
            z-index: 2147483647 !important;
            animation: toast-in-out 2s ease !important;
        }
        .ds-toast.error {
            background: rgba(255, 0, 0, 0.8) !important;
        }
        .ds-toast.success {
            background: rgba(0, 100, 255, 0.8) !important;
        }
        @keyframes toast-in-out {
            0% { opacity: 0; transform: translate(-50%, -20px); }
            20% { opacity: 1; transform: translate(-50%, 0); }
            80% { opacity: 1; transform: translate(-50%, 0); }
            100% { opacity: 0; transform: translate(-50%, 20px); }
        }
    `);

	const SELECTORS = {
		MESSAGE: 'dad65929', // 消息内容区域
		USER_PROMPT: 'fa81', // 用户提问
		AI_ANSWER: 'f9bf7997', // AI回答区域
		AI_THINKING: 'e1675d8b', // 思考区域
		AI_RESPONSE: 'ds-markdown', // 回答内容区域
		TITLE: 'd8ed659a' // 标题
	};

	function createUI() {
		if (document.getElementById(BUTTON_ID)) return;

		// 检查当前是否为首页
		if (isHomePage()) {
			// 如果是首页,移除已存在的按钮
			const existingContainer = document.getElementById(`${BUTTON_ID}-container`);
			if (existingContainer) {
				existingContainer.remove();
			}
			return;
		}

		const container = document.createElement('div');
		container.id = `${BUTTON_ID}-container`;

		const copyBtn = document.createElement('button');
		copyBtn.id = `${BUTTON_ID}-copy`;
		copyBtn.textContent = '📋';
		copyBtn.title = '复制到剪贴板';
		copyBtn.onclick = () => handleExport('clipboard');

		const exportBtn = document.createElement('button');
		exportBtn.id = BUTTON_ID;
		exportBtn.textContent = '💾';
		exportBtn.title = '导出对话';
		exportBtn.onclick = () => handleExport('file');

		container.append(copyBtn, exportBtn);
		document.body.append(container);
	}

	// 添加判断是否为首页的函数
	function isHomePage() {
		// 检查URL是否为首页
		if (
			window.location.pathname === '/' ||
			window.location.href === 'https://chat.deepseek.com/'
		) {
			return true;
		}

		// 检查是否存在对话内容元素
		const hasConversation = !!document.querySelector(`.${SELECTORS.MESSAGE}`);
		return !hasConversation;
	}

	async function handleExport(mode) {
		if (isProcessing) return;
		isProcessing = true;

		try {
			const conversations = await extractConversations();
			if (!conversations.length) {
				showToast('未检测到有效对话内容', true);
				return;
			}

			const content = formatMarkdown(conversations);

			if (mode === 'file') {
				downloadMarkdown(content);
			} else {
				GM_setClipboard(content, 'text');
				showToast('对话内容已复制到剪贴板');
			}
		} catch (error) {
			console.error('[导出错误]', error);
			showToast(`操作失败: ${error.message}`, true);
		} finally {
			isProcessing = false;
		}
	}

	function extractConversations() {
		return new Promise((resolve) => {
			requestAnimationFrame(() => {
				const conversations = [];
				const blocks = document.querySelector(`.${SELECTORS.MESSAGE}`)?.childNodes;

				blocks.forEach((block) => {
					try {
						if (block.classList.contains(SELECTORS.USER_PROMPT)) {
							conversations.push({
								content: cleanContent(block, 'prompt'),
								type: 'user'
							});
						} else if (block.classList.contains(SELECTORS.AI_ANSWER)) {
							const thinkingNode = block.querySelector(`.${SELECTORS.AI_THINKING}`);
							const responseNode = block.querySelector(`.${SELECTORS.AI_RESPONSE}`);
							conversations.push({
								content: {
									thinking: thinkingNode
										? cleanContent(thinkingNode, 'thinking')
										: '',
									response: responseNode
										? cleanContent(responseNode, 'response')
										: ''
								},
								type: 'ai'
							});
						}
					} catch (e) {
						console.warn('[对话解析错误]', e);
					}
				});

				resolve(conversations);
			});
		});
	}

	function cleanContent(node, type) {
		const clone = node.cloneNode(true);
		clone
			.querySelectorAll('button, .ds-flex, .ds-icon, .ds-icon-button, .ds-button,svg')
			.forEach((el) => el.remove());

		switch (type) {
			case 'prompt':
				var content = clone.textContent.replace(/\n{2,}/g, '\n').trim();

				// 转义 HTML 代码
				return content.replace(/[<>&]/g, function (match) {
					const escapeMap = {
						'<': '&lt;',
						'>': '&gt;',
						'&': '&amp;'
					};
					return escapeMap[match];
				});

			case 'thinking':
				return clone.innerHTML
					.replace(/<\/p>/gi, '\n')
					.replace(/<br\s*\/?>/gi, '\n')
					.replace(/<\/?[^>]+(>|$)/g, '')
					.replace(/\n+/g, '\n')
					.trim();
			case 'response':
				return clone.innerHTML;
			default:
				return clone.textContent.trim();
		}
	}

	function formatMarkdown(conversations) {
		// 获取页面标题
		const titleElement = document.querySelector(`.${SELECTORS.TITLE}`);
		const title = titleElement ? titleElement.textContent.trim() : 'DeepSeek对话';

		let md = `# ${title}\n\n`;

		conversations.forEach((conv, idx) => {
			if (conv.type === 'user') {
				if (idx > 0) md += '\n---\n';
				// md += `## 第 *${idx + 1}#* 轮对话\n`;

				let ask = conv.content.split('\n').join('\n> ');
				md += `\n> [!info] 提问\n> ${ask}\n\n`;
			}

			if (conv.type === 'ai' && conv.content) {
				if (conv.content.thinking) {
					let thinking = conv.content.thinking.split('\n').join('\n> ');

					md += `\n> [!success] 思考\n${thinking}\n`;
				}

				if (conv.content.response) {
					md += `\n${enhancedHtmlToMarkdown(conv.content.response)}\n`;
				}
			}
		});

		return md;
	}

	function enhancedHtmlToMarkdown(html) {
		const tempDiv = document.createElement('div');
		tempDiv.innerHTML = html;

		// 预处理代码块
		tempDiv.querySelectorAll('.md-code-block').forEach((codeBlock) => {
			const lang =
				codeBlock.querySelector('.md-code-block-infostring')?.textContent?.trim() || '';
			const codeContent = codeBlock.querySelector('pre')?.textContent || '';
			codeBlock.replaceWith(`[_code_:]${lang}\n${codeContent}[:_code_]`);
		});

		// 预处理数学公式
		tempDiv.querySelectorAll('.math-inline').forEach((math) => {
			math.replaceWith(`$${math.textContent}$`);
		});
		tempDiv.querySelectorAll('.math-display').forEach((math) => {
			math.replaceWith(`\n$$\n${math.textContent}\n$$\n`);
		});

		return Array.from(tempDiv.childNodes)
			.map((node) => convertNodeToMarkdown(node))
			.join('')
			.replace(/\[_code_\:\]/g, '\n```')
			.replace(/\[\:_code_\]/g, '\n```\n')
			.trim();
	}

	function convertNodeToMarkdown(node, level = 0, processedNodes = new WeakSet()) {
		if (!node || processedNodes.has(node)) return '';
		processedNodes.add(node);

		const handlers = {
			P: (n) => {
				const text = processInlineElements(n);
				return text ? `${text}\n` : '';
			},
			STRONG: (n) => `**${n.textContent}**`,
			EM: (n) => `*${n.textContent}*`,
			HR: () => '\n---\n',
			BR: () => '\n',
			A: (n) => processLinkElement(n),
			IMG: (n) => processImageElement(n),
			BLOCKQUOTE: (n) => {
				const content = Array.from(n.childNodes)
					.map((child) => convertNodeToMarkdown(child, level, processedNodes))
					.join('')
					.split('\n')
					.filter((line) => line.trim())
					.map((line) => `> ${line}`)
					.join('\n');
				return `\n${content}\n`;
			},
			UL: (n) => processListItems(n, level, '-'),
			OL: (n) => processListItems(n, level, null, n.getAttribute('start') || 1),
			PRE: (n) => `[_code_:]${n.textContent.trim()}[:_code_]`,
			CODE: (n) => `\`${n.textContent.trim()}\``,
			H1: (n) => `# ${processInlineElements(n)}\n`,
			H2: (n) => `## ${processInlineElements(n)}\n`,
			H3: (n) => `### ${processInlineElements(n)}\n`,
			H4: (n) => `#### ${processInlineElements(n)}\n`,
			H5: (n) => `##### ${processInlineElements(n)}\n`,
			H6: (n) => `###### ${processInlineElements(n)}\n`,
			TABLE: processTable,
			DIV: (n) =>
				Array.from(n.childNodes)
					.map((child) => convertNodeToMarkdown(child, level, processedNodes))
					.join(''),
			'#text': (n) => n.textContent.trim(),
			_default: (n) =>
				Array.from(n.childNodes)
					.map((child) => convertNodeToMarkdown(child, level, processedNodes))
					.join('')
		};

		return handlers[node.nodeName]?.(node) || handlers._default(node);
	}

	function processInlineElements(node) {
		return Array.from(node.childNodes)
			.map((child) => {
				if (child.nodeType === 3) return child.textContent.trim();
				if (child.nodeType === 1) {
					if (child.matches('strong')) return `**${child.textContent}**`;
					if (child.matches('em')) return `*${child.textContent}*`;
					if (child.matches('code')) return `\`${child.textContent}\``;
					if (child.matches('a')) return processLinkElement(child);
					if (child.matches('img')) return processImageElement(child);
				}
				return child.textContent;
			})
			.join('');
	}

	function processImageElement(node) {
		const alt = node.getAttribute('alt') || '';
		const title = node.getAttribute('title') || '';
		const src = node.getAttribute('src') || '';
		return title ? `![${alt}](${src} "${title}")` : `![${alt}](${src})`;
	}

	function processLinkElement(node) {
		const href = node.getAttribute('href') || '';
		const title = node.getAttribute('title') || '';
		const content = Array.from(node.childNodes)
			.map((child) => convertNodeToMarkdown(child))
			.join('');
		return title ? `[${content}](${href} "${title}")` : `[${content}](${href})`;
	}

	function processListItems(node, level, marker, start = null) {
		let result = '';
		const indent = '  '.repeat(level);
		Array.from(node.children).forEach((li, idx) => {
			const prefix = marker ? `${marker} ` : `${parseInt(start) + idx}. `;
			// 先处理li节点的直接文本内容
			const mainContent = Array.from(li.childNodes)
				.filter((child) => child.nodeType === 1 && !child.matches('ul, ol'))
				.map((child) => convertNodeToMarkdown(child, level))
				.join('')
				.trim();

			if (mainContent) {
				result += `${indent}${prefix}${mainContent}\n`;
			}

			// 单独处理嵌套列表
			const nestedLists = li.querySelectorAll(':scope > ul, :scope > ol');
			nestedLists.forEach((list) => {
				result += convertNodeToMarkdown(list, level + 1);
			});
		});
		return result;
	}

	function processTable(node) {
		const rows = Array.from(node.querySelectorAll('tr'));
		if (!rows.length) return '';

		const headers = Array.from(rows[0].querySelectorAll('th,td')).map((cell) =>
			cell.textContent.trim()
		);

		let markdown = `\n| ${headers.join(' | ')} |\n| ${headers
			.map(() => '---')
			.join(' | ')} |\n`;

		for (let i = 1; i < rows.length; i++) {
			const cells = Array.from(rows[i].querySelectorAll('td')).map((cell) =>
				processInlineElements(cell)
			);
			markdown += `| ${cells.join(' | ')} |\n`;
		}

		return markdown + '\n';
	}

	function downloadMarkdown(content) {
		const titleElement = document.querySelector(`.${SELECTORS.TITLE}`);
		const title = titleElement ? titleElement.textContent.trim() : 'DeepSeek对话';
		const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
		const link = document.createElement('a');
		link.href = URL.createObjectURL(blob);
		link.download = `${title}.md`;
		link.style.display = 'none';
		document.body.appendChild(link);
		link.click();
		setTimeout(() => {
			document.body.removeChild(link);
			URL.revokeObjectURL(link.href);
		}, 1000);
	}

	function showToast(message, isError = false) {
		const toast = document.createElement('div');
		toast.className = `ds-toast ${isError ? 'error' : 'success'}`;
		toast.textContent = message;
		document.body.appendChild(toast);

		toast.addEventListener('animationend', () => {
			document.body.removeChild(toast);
		});
	}

	// 添加 URL 变化监听
	function setupUrlChangeListener() {
		let lastUrl = window.location.href;

		// 监听 URL 变化
		setInterval(() => {
			if (lastUrl !== window.location.href) {
				lastUrl = window.location.href;
				const existingContainer = document.getElementById(`${BUTTON_ID}-container`);
				if (existingContainer) {
					existingContainer.remove();
				}
				createUI();
			}
		}, 1000);

		// 监听 history 变化
		const pushState = history.pushState;
		history.pushState = function () {
			pushState.apply(history, arguments);
			const existingContainer = document.getElementById(`${BUTTON_ID}-container`);
			if (existingContainer) {
				existingContainer.remove();
			}
			createUI();
		};
	}

	const observer = new MutationObserver(() => createUI());
	observer.observe(document, { childList: true, subtree: true });
	// window.addEventListener('load', createUI);
	// setInterval(createUI, 3000);
	window.addEventListener('load', () => {
		createUI();
		setupUrlChangeListener();
	});
})();