// ==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 = {
'<': '<',
'>': '>',
'&': '&'
};
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 ? `` : ``;
}
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();
});
})();