// ==UserScript==
// @name DeepSeek Chat Exporter (Markdown & PDF & PNG)
// @namespace http://tampermonkey.net/
// @version 1.7.1
// @description 导出 DeepSeek 聊天记录为 Markdown、PDF 和 PNG 格式
// @author HSyuf/Blueberrycongee
// @match https://chat.deepseek.com/*
// @grant GM_addStyle
// @grant GM_download
// @license MIT
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// ==/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',
};
let __exportPNGLock = false; // 全局锁,防止重复点击
// =====================
// 工具函数
// =====================
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 = '';
const elements = answerNode.querySelectorAll('.ds-markdown--block p, .ds-markdown--block h3, .katex-display.ds-markdown-math, hr');
elements.forEach((element) => {
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')) {
const tex = childNode.querySelector('annotation[encoding="application/x-tex"]');
if (tex) {
answerContent += `$$$${tex.textContent.trim()}$$$`;
}
} else if (childNode.tagName === 'STRONG') {
answerContent += `**${childNode.textContent.trim()}**`;
} else if (childNode.tagName === 'EM') {
answerContent += `*${childNode.textContent.trim()}*`;
} else if (childNode.tagName === 'A') {
const href = childNode.getAttribute('href');
answerContent += `[${childNode.textContent.trim()}](${href})`;
} else if (childNode.nodeType === Node.ELEMENT_NODE) {
answerContent += childNode.textContent.trim();
}
});
answerContent += '\n\n';
}
else if (element.tagName.toLowerCase() === 'h3') {
answerContent += `### ${element.textContent.trim()}\n\n`;
}
else if (element.classList.contains('katex-display')) {
const tex = element.querySelector('annotation[encoding="application/x-tex"]');
if (tex) {
answerContent += `$$${tex.textContent.trim()}$$\n\n`;
}
}
else if (element.tagName.toLowerCase() === 'hr') {
answerContent += '\n---\n';
}
});
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;
}
const fixedMdContent = mdContent.replace(/(\*\*.*?\*\*)/g, '<strong>$1</strong>')
.replace(/\(\s*([^)]*)\s*\)/g, '\\($1\\)')
.replace(/\$\$\s*([^$]*)\s*\$\$/g, '$$$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;
const fixedMdContent = mdContent.replace(/(\*\*.*?\*\*)/g, '<strong>$1</strong>')
.replace(/\(\s*([^)]*)\s*\)/g, '\\($1\\)')
.replace(/\$\$\s*([^$]*)\s*\$\$/g, '$$$1$$');
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 exportPNG() {
if (__exportPNGLock) return; // 如果当前正在导出,跳过
__exportPNGLock = true;
const chatContainer = document.querySelector(config.chatContainerSelector);
if (!chatContainer) {
alert("未找到聊天容器!");
__exportPNGLock = false;
return;
}
// 创建沙盒容器
const sandbox = document.createElement('iframe');
sandbox.style.cssText = `
position: fixed;
left: -9999px;
top: 0;
width: 800px;
height: ${window.innerHeight}px;
border: 0;
visibility: hidden;
`;
document.body.appendChild(sandbox);
// 深度克隆与样式处理
const cloneNode = chatContainer.cloneNode(true);
cloneNode.style.cssText = `
width: 800px !important;
transform: none !important;
overflow: visible !important;
position: static !important;
background: white !important;
max-height: none !important;
padding: 20px !important;
margin: 0 !important;
box-sizing: border-box !important;
`;
// 清理干扰元素,排除图标
['button', 'input', '.ds-message-feedback-container', '.eb23581b.dfa60d66'].forEach(selector => {
cloneNode.querySelectorAll(selector).forEach(el => el.remove());
});
// 数学公式修复
cloneNode.querySelectorAll('.katex-display').forEach(mathEl => {
mathEl.style.transform = 'none !important';
mathEl.style.position = 'relative !important';
});
// 注入沙盒
sandbox.contentDocument.body.appendChild(cloneNode);
sandbox.contentDocument.body.style.background = 'white';
// 等待资源加载
const waitReady = () => Promise.all([document.fonts.ready, new Promise(resolve => setTimeout(resolve, 300))]);
waitReady().then(() => {
return html2canvas(cloneNode, {
scale: 2,
useCORS: true,
logging: true,
backgroundColor: "#FFFFFF"
});
}).then(canvas => {
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${config.exportFileName}_${Date.now()}.png`;
a.click();
setTimeout(() => {
URL.revokeObjectURL(url);
sandbox.remove();
}, 1000);
}, 'image/png');
}).catch(err => {
console.error('截图失败:', err);
alert(`导出失败:${err.message}`);
}).finally(() => {
__exportPNGLock = false;
});
}
// =====================
// 创建导出菜单
// =====================
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>
<button class="export-btn" id="png-btn">导出图片</button>
`;
menu.querySelector("#md-btn").addEventListener("click", exportMarkdown);
menu.querySelector("#pdf-btn").addEventListener("click", exportPDF);
menu.querySelector("#png-btn").addEventListener("click", exportPNG);
document.body.appendChild(menu);
}
// =====================
// 样式
// =====================
GM_addStyle(`
.ds-exporter-menu {
position: fixed;
top: 20px;
right: 20px;
z-index: 999999;
background: rgba(255, 255, 255, 0.95) url('data:image/svg+xml;utf8,<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="40" fill="%23ff9a9e" opacity="0.2"/></svg>');
border: 2px solid #ff93ac;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(255, 65, 108, 0.3);
backdrop-filter: blur(8px);
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start; /* 确保按钮左对齐 */
}
.export-btn {
background: linear-gradient(145deg, #ff7eb3 0%, #ff758c 100%);
color: white;
border: 2px solid #fff;
border-radius: 12px;
padding: 12px 24px;
font-family: 'Comic Sans MS', cursive;
font-size: 16px;
text-shadow: 1px 1px 2px rgba(255, 65, 108, 0.5);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
width: 200px; /* 定义按钮宽度 */
margin-bottom: 8px; /* 添加按钮之间的间距 */
}
.export-btn::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent 33%, rgba(255,255,255,0.3) 50%, transparent 66%);
transform: rotate(45deg);
animation: sparkle 3s infinite linear;
}
.export-btn:hover {
transform: scale(1.05) rotate(-2deg);
box-shadow: 0 6px 24px rgba(255, 65, 108, 0.4);
background: linear-gradient(145deg, #ff6b9d 0%, #ff677e 100%);
}
.export-btn:active {
transform: scale(0.95) rotate(2deg);
}
#md-btn::after {
content: '📁';
margin-left: 8px;
filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.2));
}
#pdf-btn::after {
content: '📄';
margin-left: 8px;
}
#png-btn::after {
content: '🖼️';
margin-left: 8px;
}
@keyframes sparkle {
0% { transform: translate(-100%, -100%) rotate(45deg); }
100% { transform: translate(100%, 100%) rotate(45deg); }
}
/* 添加卡通对话框提示 */
.ds-exporter-menu::before {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
background: white;
padding: 8px 16px;
border-radius: 10px;
border: 2px solid #ff93ac;
font-family: 'Comic Sans MS', cursive;
color: #ff6b9d;
white-space: nowrap;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
/* 添加漂浮的装饰元素 */
.ds-exporter-menu::after {
content: '';
position: absolute;
width: 30px;
height: 30px;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23ff93ac" d="M12,2.5L15.3,8.6L22,9.7L17,14.5L18.5,21L12,17.5L5.5,21L7,14.5L2,9.7L8.7,8.6L12,2.5Z"/></svg>');
top: -20px;
right: -15px;
animation: float 2s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0) rotate(10deg); }
50% { transform: translateY(-10px) rotate(-10deg); }
}
`);
// =====================
// 初始化
// =====================
function init() {
const checkInterval = setInterval(() => {
if (document.querySelector(config.chatContainerSelector)) {
clearInterval(checkInterval);
createExportMenu();
}
}, 500);
}
init();
})();