DeepSeek Chat Exporter (Markdown & PDF & PNG)

导出 DeepSeek 聊天记录为 Markdown、PDF 和 PNG 格式

  1. // ==UserScript==
  2. // @name DeepSeek Chat Exporter (Markdown & PDF & PNG)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.7.1
  5. // @description 导出 DeepSeek 聊天记录为 Markdown、PDF 和 PNG 格式
  6. // @author HSyuf/Blueberrycongee
  7. // @match https://chat.deepseek.com/*
  8. // @grant GM_addStyle
  9. // @grant GM_download
  10. // @license MIT
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // =====================
  18. // 配置
  19. // =====================
  20. const config = {
  21. chatContainerSelector: '.dad65929', // 聊天框容器
  22. userClassPrefix: 'fa81', // 用户消息 class 前缀
  23. aiClassPrefix: 'f9bf7997', // AI消息相关 class 前缀
  24. aiReplyContainer: 'edb250b1', // AI回复的主要容器
  25. searchHintSelector: '.a6d716f5.db5991dd', // 搜索/思考时间
  26. thinkingChainSelector: '.e1675d8b', // 思考链
  27. finalAnswerSelector: 'div.ds-markdown.ds-markdown--block', // 正式回答
  28. exportFileName: 'DeepSeek_Chat_Export',
  29. };
  30.  
  31. let __exportPNGLock = false; // 全局锁,防止重复点击
  32.  
  33. // =====================
  34. // 工具函数
  35. // =====================
  36. function isUserMessage(node) {
  37. return node.classList.contains(config.userClassPrefix);
  38. }
  39.  
  40. function isAIMessage(node) {
  41. return node.classList.contains(config.aiClassPrefix);
  42. }
  43.  
  44. function extractSearchOrThinking(node) {
  45. const hintNode = node.querySelector(config.searchHintSelector);
  46. return hintNode ? `**${hintNode.textContent.trim()}**` : null;
  47. }
  48.  
  49. function extractThinkingChain(node) {
  50. const thinkingNode = node.querySelector(config.thinkingChainSelector);
  51. return thinkingNode ? `**思考链**\n${thinkingNode.textContent.trim()}` : null;
  52. }
  53.  
  54. function extractFinalAnswer(node) {
  55. const answerNode = node.querySelector(config.finalAnswerSelector);
  56. if (!answerNode) return null;
  57.  
  58. let answerContent = '';
  59. const elements = answerNode.querySelectorAll('.ds-markdown--block p, .ds-markdown--block h3, .katex-display.ds-markdown-math, hr');
  60.  
  61. elements.forEach((element) => {
  62. if (element.tagName.toLowerCase() === 'p') {
  63. element.childNodes.forEach((childNode) => {
  64. if (childNode.nodeType === Node.TEXT_NODE) {
  65. answerContent += childNode.textContent.trim();
  66. } else if (childNode.classList && childNode.classList.contains('katex')) {
  67. const tex = childNode.querySelector('annotation[encoding="application/x-tex"]');
  68. if (tex) {
  69. answerContent += `$$$${tex.textContent.trim()}$$$`;
  70. }
  71. } else if (childNode.tagName === 'STRONG') {
  72. answerContent += `**${childNode.textContent.trim()}**`;
  73. } else if (childNode.tagName === 'EM') {
  74. answerContent += `*${childNode.textContent.trim()}*`;
  75. } else if (childNode.tagName === 'A') {
  76. const href = childNode.getAttribute('href');
  77. answerContent += `[${childNode.textContent.trim()}](${href})`;
  78. } else if (childNode.nodeType === Node.ELEMENT_NODE) {
  79. answerContent += childNode.textContent.trim();
  80. }
  81. });
  82. answerContent += '\n\n';
  83. }
  84. else if (element.tagName.toLowerCase() === 'h3') {
  85. answerContent += `### ${element.textContent.trim()}\n\n`;
  86. }
  87. else if (element.classList.contains('katex-display')) {
  88. const tex = element.querySelector('annotation[encoding="application/x-tex"]');
  89. if (tex) {
  90. answerContent += `$$${tex.textContent.trim()}$$\n\n`;
  91. }
  92. }
  93. else if (element.tagName.toLowerCase() === 'hr') {
  94. answerContent += '\n---\n';
  95. }
  96. });
  97.  
  98. return `**正式回答**\n${answerContent.trim()}`;
  99. }
  100.  
  101. function getOrderedMessages() {
  102. const messages = [];
  103. const chatContainer = document.querySelector(config.chatContainerSelector);
  104. if (!chatContainer) {
  105. console.error('未找到聊天容器');
  106. return messages;
  107. }
  108.  
  109. for (const node of chatContainer.children) {
  110. if (isUserMessage(node)) {
  111. messages.push(`**用户:**\n${node.textContent.trim()}`);
  112. } else if (isAIMessage(node)) {
  113. let output = '';
  114. const aiReplyContainer = node.querySelector(`.${config.aiReplyContainer}`);
  115. if (aiReplyContainer) {
  116. const searchHint = extractSearchOrThinking(aiReplyContainer);
  117. if (searchHint) output += `${searchHint}\n\n`;
  118. const thinkingChain = extractThinkingChain(aiReplyContainer);
  119. if (thinkingChain) output += `${thinkingChain}\n\n`;
  120. } else {
  121. const searchHint = extractSearchOrThinking(node);
  122. if (searchHint) output += `${searchHint}\n\n`;
  123. }
  124. const finalAnswer = extractFinalAnswer(node);
  125. if (finalAnswer) output += `${finalAnswer}\n\n`;
  126. if (output.trim()) {
  127. messages.push(output.trim());
  128. }
  129. }
  130. }
  131. return messages;
  132. }
  133.  
  134. function generateMdContent() {
  135. const messages = getOrderedMessages();
  136. return messages.length ? messages.join('\n\n---\n\n') : '';
  137. }
  138.  
  139. // =====================
  140. // 导出功能
  141. // =====================
  142. function exportMarkdown() {
  143. const mdContent = generateMdContent();
  144. if (!mdContent) {
  145. alert("未找到聊天记录!");
  146. return;
  147. }
  148.  
  149. const fixedMdContent = mdContent.replace(/(\*\*.*?\*\*)/g, '<strong>$1</strong>')
  150. .replace(/\(\s*([^)]*)\s*\)/g, '\\($1\\)')
  151. .replace(/\$\$\s*([^$]*)\s*\$\$/g, '$$$1$$');
  152.  
  153. const blob = new Blob([fixedMdContent], { type: 'text/markdown' });
  154. const url = URL.createObjectURL(blob);
  155. const a = document.createElement('a');
  156. a.href = url;
  157. a.download = `${config.exportFileName}_${Date.now()}.md`;
  158. a.click();
  159. setTimeout(() => URL.revokeObjectURL(url), 5000);
  160. }
  161.  
  162. function exportPDF() {
  163. const mdContent = generateMdContent();
  164. if (!mdContent) return;
  165.  
  166. const fixedMdContent = mdContent.replace(/(\*\*.*?\*\*)/g, '<strong>$1</strong>')
  167. .replace(/\(\s*([^)]*)\s*\)/g, '\\($1\\)')
  168. .replace(/\$\$\s*([^$]*)\s*\$\$/g, '$$$1$$');
  169.  
  170. const printContent = `
  171. <html>
  172. <head>
  173. <title>DeepSeek Chat Export</title>
  174. <style>
  175. body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: 0 auto; }
  176. h2 { color: #2c3e50; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
  177. .ai-answer { color: #1a7f37; margin: 15px 0; }
  178. .ai-chain { color: #666; font-style: italic; margin: 10px 0; }
  179. hr { border: 0; border-top: 1px solid #eee; margin: 25px 0; }
  180. </style>
  181. </head>
  182. <body>
  183. ${fixedMdContent.replace(/\*\*用户:\*\*\n/g, '<h2>用户提问</h2><div class="user-question">')
  184. .replace(/\*\*正式回答\*\*\n/g, '</div><h2>AI 回答</h2><div class="ai-answer">')
  185. .replace(/\*\*思考链\*\*\n/g, '</div><h2>思维链</h2><div class="ai-chain">')
  186. .replace(/\n/g, '<br>')
  187. .replace(/---/g, '</div><hr>')}
  188. </body>
  189. </html>
  190. `;
  191.  
  192. const printWindow = window.open("", "_blank");
  193. printWindow.document.write(printContent);
  194. printWindow.document.close();
  195. setTimeout(() => { printWindow.print(); printWindow.close(); }, 500);
  196. }
  197.  
  198. function exportPNG() {
  199. if (__exportPNGLock) return; // 如果当前正在导出,跳过
  200. __exportPNGLock = true;
  201.  
  202. const chatContainer = document.querySelector(config.chatContainerSelector);
  203. if (!chatContainer) {
  204. alert("未找到聊天容器!");
  205. __exportPNGLock = false;
  206. return;
  207. }
  208.  
  209. // 创建沙盒容器
  210. const sandbox = document.createElement('iframe');
  211. sandbox.style.cssText = `
  212. position: fixed;
  213. left: -9999px;
  214. top: 0;
  215. width: 800px;
  216. height: ${window.innerHeight}px;
  217. border: 0;
  218. visibility: hidden;
  219. `;
  220. document.body.appendChild(sandbox);
  221.  
  222. // 深度克隆与样式处理
  223. const cloneNode = chatContainer.cloneNode(true);
  224. cloneNode.style.cssText = `
  225. width: 800px !important;
  226. transform: none !important;
  227. overflow: visible !important;
  228. position: static !important;
  229. background: white !important;
  230. max-height: none !important;
  231. padding: 20px !important;
  232. margin: 0 !important;
  233. box-sizing: border-box !important;
  234. `;
  235.  
  236. // 清理干扰元素,排除图标
  237. ['button', 'input', '.ds-message-feedback-container', '.eb23581b.dfa60d66'].forEach(selector => {
  238. cloneNode.querySelectorAll(selector).forEach(el => el.remove());
  239. });
  240.  
  241. // 数学公式修复
  242. cloneNode.querySelectorAll('.katex-display').forEach(mathEl => {
  243. mathEl.style.transform = 'none !important';
  244. mathEl.style.position = 'relative !important';
  245. });
  246.  
  247. // 注入沙盒
  248. sandbox.contentDocument.body.appendChild(cloneNode);
  249. sandbox.contentDocument.body.style.background = 'white';
  250.  
  251. // 等待资源加载
  252. const waitReady = () => Promise.all([document.fonts.ready, new Promise(resolve => setTimeout(resolve, 300))]);
  253.  
  254. waitReady().then(() => {
  255. return html2canvas(cloneNode, {
  256. scale: 2,
  257. useCORS: true,
  258. logging: true,
  259. backgroundColor: "#FFFFFF"
  260. });
  261. }).then(canvas => {
  262. canvas.toBlob(blob => {
  263. const url = URL.createObjectURL(blob);
  264. const a = document.createElement('a');
  265. a.href = url;
  266. a.download = `${config.exportFileName}_${Date.now()}.png`;
  267. a.click();
  268. setTimeout(() => {
  269. URL.revokeObjectURL(url);
  270. sandbox.remove();
  271. }, 1000);
  272. }, 'image/png');
  273. }).catch(err => {
  274. console.error('截图失败:', err);
  275. alert(`导出失败:${err.message}`);
  276. }).finally(() => {
  277. __exportPNGLock = false;
  278. });
  279. }
  280.  
  281. // =====================
  282. // 创建导出菜单
  283. // =====================
  284. function createExportMenu() {
  285. const menu = document.createElement("div");
  286. menu.className = "ds-exporter-menu";
  287. menu.innerHTML = `
  288. <button class="export-btn" id="md-btn">导出为 Markdown</button>
  289. <button class="export-btn" id="pdf-btn">导出为 PDF</button>
  290. <button class="export-btn" id="png-btn">导出图片</button>
  291. `;
  292.  
  293. menu.querySelector("#md-btn").addEventListener("click", exportMarkdown);
  294. menu.querySelector("#pdf-btn").addEventListener("click", exportPDF);
  295. menu.querySelector("#png-btn").addEventListener("click", exportPNG);
  296. document.body.appendChild(menu);
  297. }
  298.  
  299. // =====================
  300. // 样式
  301. // =====================
  302. GM_addStyle(`
  303. .ds-exporter-menu {
  304. position: fixed;
  305. top: 20px;
  306. right: 20px;
  307. z-index: 999999;
  308. 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>');
  309. border: 2px solid #ff93ac;
  310. border-radius: 15px;
  311. box-shadow: 0 4px 20px rgba(255, 65, 108, 0.3);
  312. backdrop-filter: blur(8px);
  313. padding: 15px;
  314. display: flex;
  315. flex-direction: column;
  316. gap: 12px;
  317. align-items: flex-start; /* 确保按钮左对齐 */
  318. }
  319.  
  320. .export-btn {
  321. background: linear-gradient(145deg, #ff7eb3 0%, #ff758c 100%);
  322. color: white;
  323. border: 2px solid #fff;
  324. border-radius: 12px;
  325. padding: 12px 24px;
  326. font-family: 'Comic Sans MS', cursive;
  327. font-size: 16px;
  328. text-shadow: 1px 1px 2px rgba(255, 65, 108, 0.5);
  329. position: relative;
  330. overflow: hidden;
  331. transition: all 0.3s ease;
  332. cursor: pointer;
  333. width: 200px; /* 定义按钮宽度 */
  334. margin-bottom: 8px; /* 添加按钮之间的间距 */
  335. }
  336.  
  337. .export-btn::before {
  338. content: '';
  339. position: absolute;
  340. top: -50%;
  341. left: -50%;
  342. width: 200%;
  343. height: 200%;
  344. background: linear-gradient(45deg, transparent 33%, rgba(255,255,255,0.3) 50%, transparent 66%);
  345. transform: rotate(45deg);
  346. animation: sparkle 3s infinite linear;
  347. }
  348.  
  349. .export-btn:hover {
  350. transform: scale(1.05) rotate(-2deg);
  351. box-shadow: 0 6px 24px rgba(255, 65, 108, 0.4);
  352. background: linear-gradient(145deg, #ff6b9d 0%, #ff677e 100%);
  353. }
  354.  
  355. .export-btn:active {
  356. transform: scale(0.95) rotate(2deg);
  357. }
  358.  
  359. #md-btn::after {
  360. content: '📁';
  361. margin-left: 8px;
  362. filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.2));
  363. }
  364.  
  365. #pdf-btn::after {
  366. content: '📄';
  367. margin-left: 8px;
  368. }
  369.  
  370. #png-btn::after {
  371. content: '🖼️';
  372. margin-left: 8px;
  373. }
  374.  
  375. @keyframes sparkle {
  376. 0% { transform: translate(-100%, -100%) rotate(45deg); }
  377. 100% { transform: translate(100%, 100%) rotate(45deg); }
  378. }
  379.  
  380. /* 添加卡通对话框提示 */
  381. .ds-exporter-menu::before {
  382. position: absolute;
  383. top: -40px;
  384. left: 50%;
  385. transform: translateX(-50%);
  386. background: white;
  387. padding: 8px 16px;
  388. border-radius: 10px;
  389. border: 2px solid #ff93ac;
  390. font-family: 'Comic Sans MS', cursive;
  391. color: #ff6b9d;
  392. white-space: nowrap;
  393. box-shadow: 0 3px 10px rgba(0,0,0,0.1);
  394. }
  395.  
  396. /* 添加漂浮的装饰元素 */
  397. .ds-exporter-menu::after {
  398. content: '';
  399. position: absolute;
  400. width: 30px;
  401. height: 30px;
  402. 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>');
  403. top: -20px;
  404. right: -15px;
  405. animation: float 2s ease-in-out infinite;
  406. }
  407.  
  408. @keyframes float {
  409. 0%, 100% { transform: translateY(0) rotate(10deg); }
  410. 50% { transform: translateY(-10px) rotate(-10deg); }
  411. }
  412. `);
  413.  
  414.  
  415.  
  416. // =====================
  417. // 初始化
  418. // =====================
  419. function init() {
  420. const checkInterval = setInterval(() => {
  421. if (document.querySelector(config.chatContainerSelector)) {
  422. clearInterval(checkInterval);
  423. createExportMenu();
  424. }
  425. }, 500);
  426. }
  427.  
  428. init();
  429. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址