DeepSeek 对话导出

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

目前为 2025-03-05 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name DeepSeek 对话导出
  3. // @name:en DeepSeek Chat Export
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.25.0305
  6. // @description 将 Deepseek 对话导出与复制的工具
  7. // @description:en The tool for exporting and copying dialogues in Deepseek
  8. // @author 木炭
  9. // @copyright © 2025 木炭
  10. // @license MIT
  11. // @supportURL https://github.com/woodcoal/deepseek-chat-export
  12. // @homeUrl https://www.mutan.vip/
  13. // @lastmodified 2025-02-27
  14. // @match https://chat.deepseek.com/*
  15. // @icon https://www.google.com/s2/favicons?sz=64&domain=deepseek.com
  16. // @grant GM_addStyle
  17. // @grant GM_setClipboard
  18. // @run-at document-body
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. ('use strict');
  23. const BUTTON_ID = 'DS_MarkdownExport';
  24. let isProcessing = false;
  25.  
  26. GM_addStyle(`
  27. #${BUTTON_ID}-container {
  28. position: fixed !important;
  29. top: 20px !important;
  30. right: 20px !important;
  31. z-index: 2147483647 !important;
  32. display: flex !important;
  33. gap: 8px !important;
  34. }
  35. #${BUTTON_ID}, #${BUTTON_ID}-copy {
  36. padding: 4px !important;
  37. cursor: pointer !important;
  38. transition: all 0.2s ease !important;
  39. opacity: 0.3 !important;
  40. background: none !important;
  41. border: none !important;
  42. font-size: 20px !important;
  43. position: relative !important;
  44. }
  45. #${BUTTON_ID}:hover, #${BUTTON_ID}-copy:hover {
  46. opacity: 1 !important;
  47. transform: scale(1.1) !important;
  48. }
  49. #${BUTTON_ID}:hover::after, #${BUTTON_ID}-copy:hover::after {
  50. content: attr(title) !important;
  51. position: absolute !important;
  52. top: 100% !important;
  53. left: 50% !important;
  54. transform: translateX(-50%) !important;
  55. background: rgba(0, 0, 0, 0.8) !important;
  56. color: white !important;
  57. padding: 4px 8px !important;
  58. border-radius: 4px !important;
  59. font-size: 12px !important;
  60. white-space: nowrap !important;
  61. z-index: 1000 !important;
  62. }
  63. .ds-toast {
  64. position: fixed !important;
  65. top: 20px !important;
  66. left: 50% !important;
  67. transform: translateX(-50%) !important;
  68. color: white !important;
  69. padding: 8px 16px !important;
  70. border-radius: 4px !important;
  71. font-size: 14px !important;
  72. z-index: 2147483647 !important;
  73. animation: toast-in-out 2s ease !important;
  74. }
  75. .ds-toast.error {
  76. background: rgba(255, 0, 0, 0.8) !important;
  77. }
  78. .ds-toast.success {
  79. background: rgba(0, 100, 255, 0.8) !important;
  80. }
  81. @keyframes toast-in-out {
  82. 0% { opacity: 0; transform: translate(-50%, -20px); }
  83. 20% { opacity: 1; transform: translate(-50%, 0); }
  84. 80% { opacity: 1; transform: translate(-50%, 0); }
  85. 100% { opacity: 0; transform: translate(-50%, 20px); }
  86. }
  87. `);
  88.  
  89. const SELECTORS = {
  90. MESSAGE: 'dad65929', // 消息内容区域
  91. USER_PROMPT: 'fa81', // 用户提问
  92. AI_ANSWER: 'f9bf7997', // AI回答区域
  93. AI_THINKING: 'e1675d8b', // 思考区域
  94. AI_RESPONSE: 'ds-markdown', // 回答内容区域
  95. TITLE: 'd8ed659a' // 标题
  96. };
  97.  
  98. function createUI() {
  99. if (document.getElementById(BUTTON_ID)) return;
  100.  
  101. // 检查当前是否为首页
  102. if (isHomePage()) {
  103. // 如果是首页,移除已存在的按钮
  104. const existingContainer = document.getElementById(`${BUTTON_ID}-container`);
  105. if (existingContainer) {
  106. existingContainer.remove();
  107. }
  108. return;
  109. }
  110.  
  111. const container = document.createElement('div');
  112. container.id = `${BUTTON_ID}-container`;
  113.  
  114. const copyBtn = document.createElement('button');
  115. copyBtn.id = `${BUTTON_ID}-copy`;
  116. copyBtn.textContent = '📋';
  117. copyBtn.title = '复制到剪贴板';
  118. copyBtn.onclick = () => handleExport('clipboard');
  119.  
  120. const exportBtn = document.createElement('button');
  121. exportBtn.id = BUTTON_ID;
  122. exportBtn.textContent = '💾';
  123. exportBtn.title = '导出对话';
  124. exportBtn.onclick = () => handleExport('file');
  125.  
  126. container.append(copyBtn, exportBtn);
  127. document.body.append(container);
  128. }
  129.  
  130. // 添加判断是否为首页的函数
  131. function isHomePage() {
  132. // 检查URL是否为首页
  133. if (
  134. window.location.pathname === '/' ||
  135. window.location.href === 'https://chat.deepseek.com/'
  136. ) {
  137. return true;
  138. }
  139.  
  140. // 检查是否存在对话内容元素
  141. const hasConversation = !!document.querySelector(`.${SELECTORS.MESSAGE}`);
  142. return !hasConversation;
  143. }
  144.  
  145. async function handleExport(mode) {
  146. if (isProcessing) return;
  147. isProcessing = true;
  148.  
  149. try {
  150. const conversations = await extractConversations();
  151. if (!conversations.length) {
  152. showToast('未检测到有效对话内容', true);
  153. return;
  154. }
  155.  
  156. const content = formatMarkdown(conversations);
  157.  
  158. if (mode === 'file') {
  159. downloadMarkdown(content);
  160. } else {
  161. GM_setClipboard(content, 'text');
  162. showToast('对话内容已复制到剪贴板');
  163. }
  164. } catch (error) {
  165. console.error('[导出错误]', error);
  166. showToast(`操作失败: ${error.message}`, true);
  167. } finally {
  168. isProcessing = false;
  169. }
  170. }
  171.  
  172. function extractConversations() {
  173. return new Promise((resolve) => {
  174. requestAnimationFrame(() => {
  175. const conversations = [];
  176. const blocks = document.querySelector(`.${SELECTORS.MESSAGE}`)?.childNodes;
  177.  
  178. blocks.forEach((block) => {
  179. try {
  180. if (block.classList.contains(SELECTORS.USER_PROMPT)) {
  181. conversations.push({
  182. content: cleanContent(block, 'prompt'),
  183. type: 'user'
  184. });
  185. } else if (block.classList.contains(SELECTORS.AI_ANSWER)) {
  186. const thinkingNode = block.querySelector(`.${SELECTORS.AI_THINKING}`);
  187. const responseNode = block.querySelector(`.${SELECTORS.AI_RESPONSE}`);
  188. conversations.push({
  189. content: {
  190. thinking: thinkingNode
  191. ? cleanContent(thinkingNode, 'thinking')
  192. : '',
  193. response: responseNode
  194. ? cleanContent(responseNode, 'response')
  195. : ''
  196. },
  197. type: 'ai'
  198. });
  199. }
  200. } catch (e) {
  201. console.warn('[对话解析错误]', e);
  202. }
  203. });
  204.  
  205. resolve(conversations);
  206. });
  207. });
  208. }
  209.  
  210. function cleanContent(node, type) {
  211. const clone = node.cloneNode(true);
  212. clone
  213. .querySelectorAll('button, .ds-flex, .ds-icon, .ds-icon-button, .ds-button,svg')
  214. .forEach((el) => el.remove());
  215.  
  216. switch (type) {
  217. case 'prompt':
  218. var content = clone.textContent.replace(/\n{2,}/g, '\n').trim();
  219.  
  220. // 转义 HTML 代码
  221. return content.replace(/[<>&]/g, function (match) {
  222. const escapeMap = {
  223. '<': '&lt;',
  224. '>': '&gt;',
  225. '&': '&amp;'
  226. };
  227. return escapeMap[match];
  228. });
  229.  
  230. case 'thinking':
  231. return clone.innerHTML
  232. .replace(/<\/p>/gi, '\n')
  233. .replace(/<br\s*\/?>/gi, '\n')
  234. .replace(/<\/?[^>]+(>|$)/g, '')
  235. .replace(/\n+/g, '\n')
  236. .trim();
  237. case 'response':
  238. return clone.innerHTML;
  239. default:
  240. return clone.textContent.trim();
  241. }
  242. }
  243.  
  244. function formatMarkdown(conversations) {
  245. // 获取页面标题
  246. const titleElement = document.querySelector(`.${SELECTORS.TITLE}`);
  247. const title = titleElement ? titleElement.textContent.trim() : 'DeepSeek对话';
  248.  
  249. let md = `# ${title}\n\n`;
  250.  
  251. conversations.forEach((conv, idx) => {
  252. if (conv.type === 'user') {
  253. if (idx > 0) md += '\n---\n';
  254. // md += `## 第 *${idx + 1}#* 轮对话\n`;
  255.  
  256. let ask = conv.content.split('\n').join('\n> ');
  257. md += `\n> [!info] 提问\n> ${ask}\n\n`;
  258. }
  259.  
  260. if (conv.type === 'ai' && conv.content) {
  261. if (conv.content.thinking) {
  262. let thinking = conv.content.thinking.split('\n').join('\n> ');
  263.  
  264. md += `\n> [!success] 思考\n${thinking}\n`;
  265. }
  266.  
  267. if (conv.content.response) {
  268. md += `\n${enhancedHtmlToMarkdown(conv.content.response)}\n`;
  269. }
  270. }
  271. });
  272.  
  273. return md;
  274. }
  275.  
  276. function enhancedHtmlToMarkdown(html) {
  277. const tempDiv = document.createElement('div');
  278. tempDiv.innerHTML = html;
  279.  
  280. // 预处理代码块
  281. tempDiv.querySelectorAll('.md-code-block').forEach((codeBlock) => {
  282. const lang =
  283. codeBlock.querySelector('.md-code-block-infostring')?.textContent?.trim() || '';
  284. const codeContent = codeBlock.querySelector('pre')?.textContent || '';
  285. codeBlock.replaceWith(`\n\n\`\`\`${lang}\n${codeContent}\n\`\`\`\n\n`);
  286. });
  287.  
  288. // 预处理数学公式
  289. tempDiv.querySelectorAll('.math-inline').forEach((math) => {
  290. math.replaceWith(`$${math.textContent}$`);
  291. });
  292. tempDiv.querySelectorAll('.math-display').forEach((math) => {
  293. math.replaceWith(`\n$$\n${math.textContent}\n$$\n`);
  294. });
  295.  
  296. return Array.from(tempDiv.childNodes)
  297. .map((node) => convertNodeToMarkdown(node))
  298. .join('')
  299. .trim();
  300. }
  301.  
  302. function convertNodeToMarkdown(node, level = 0, processedNodes = new WeakSet()) {
  303. if (!node || processedNodes.has(node)) return '';
  304. processedNodes.add(node);
  305.  
  306. const handlers = {
  307. P: (n) => {
  308. const text = processInlineElements(n);
  309. return text ? `${text}\n` : '';
  310. },
  311. STRONG: (n) => `**${n.textContent}**`,
  312. EM: (n) => `*${n.textContent}*`,
  313. HR: () => '\n---\n',
  314. BR: () => '\n',
  315. A: (n) => processLinkElement(n),
  316. IMG: (n) => processImageElement(n),
  317. BLOCKQUOTE: (n) => {
  318. const content = Array.from(n.childNodes)
  319. .map((child) => convertNodeToMarkdown(child, level, processedNodes))
  320. .join('')
  321. .split('\n')
  322. .filter((line) => line.trim())
  323. .map((line) => `> ${line}`)
  324. .join('\n');
  325. return `\n${content}\n`;
  326. },
  327. UL: (n) => processListItems(n, level, '-'),
  328. OL: (n) => processListItems(n, level, null, n.getAttribute('start') || 1),
  329. PRE: (n) => `\n\`\`\`\n${n.textContent.trim()}\n\`\`\`\n\n`,
  330. CODE: (n) => `\`${n.textContent.trim()}\``,
  331. H1: (n) => `# ${processInlineElements(n)}\n`,
  332. H2: (n) => `## ${processInlineElements(n)}\n`,
  333. H3: (n) => `### ${processInlineElements(n)}\n`,
  334. H4: (n) => `#### ${processInlineElements(n)}\n`,
  335. H5: (n) => `##### ${processInlineElements(n)}\n`,
  336. H6: (n) => `###### ${processInlineElements(n)}\n`,
  337. TABLE: processTable,
  338. DIV: (n) =>
  339. Array.from(n.childNodes)
  340. .map((child) => convertNodeToMarkdown(child, level, processedNodes))
  341. .join(''),
  342. '#text': (n) => n.textContent.trim(),
  343. _default: (n) =>
  344. Array.from(n.childNodes)
  345. .map((child) => convertNodeToMarkdown(child, level, processedNodes))
  346. .join('')
  347. };
  348.  
  349. return handlers[node.nodeName]?.(node) || handlers._default(node);
  350. }
  351.  
  352. function processInlineElements(node) {
  353. return Array.from(node.childNodes)
  354. .map((child) => {
  355. if (child.nodeType === 3) return child.textContent.trim();
  356. if (child.nodeType === 1) {
  357. if (child.matches('strong')) return `**${child.textContent}**`;
  358. if (child.matches('em')) return `*${child.textContent}*`;
  359. if (child.matches('code')) return `\`${child.textContent}\``;
  360. if (child.matches('a')) return processLinkElement(child);
  361. if (child.matches('img')) return processImageElement(child);
  362. }
  363. return child.textContent;
  364. })
  365. .join('');
  366. }
  367.  
  368. function processImageElement(node) {
  369. const alt = node.getAttribute('alt') || '';
  370. const title = node.getAttribute('title') || '';
  371. const src = node.getAttribute('src') || '';
  372. return title ? `![${alt}](${src} "${title}")` : `![${alt}](${src})`;
  373. }
  374.  
  375. function processLinkElement(node) {
  376. const href = node.getAttribute('href') || '';
  377. const title = node.getAttribute('title') || '';
  378. const content = Array.from(node.childNodes)
  379. .map((child) => convertNodeToMarkdown(child))
  380. .join('');
  381. return title ? `[${content}](${href} "${title}")` : `[${content}](${href})`;
  382. }
  383.  
  384. function processListItems(node, level, marker, start = null) {
  385. let result = '';
  386. const indent = ' '.repeat(level);
  387. Array.from(node.children).forEach((li, idx) => {
  388. const prefix = marker ? `${marker} ` : `${parseInt(start) + idx}. `;
  389. // 先处理li节点的直接文本内容
  390. const mainContent = Array.from(li.childNodes)
  391. .filter((child) => child.nodeType === 1 && !child.matches('ul, ol'))
  392. .map((child) => convertNodeToMarkdown(child, level))
  393. .join('')
  394. .trim();
  395.  
  396. if (mainContent) {
  397. result += `${indent}${prefix}${mainContent}\n`;
  398. }
  399.  
  400. // 单独处理嵌套列表
  401. const nestedLists = li.querySelectorAll(':scope > ul, :scope > ol');
  402. nestedLists.forEach((list) => {
  403. result += convertNodeToMarkdown(list, level + 1);
  404. });
  405. });
  406. return result;
  407. }
  408.  
  409. function processTable(node) {
  410. const rows = Array.from(node.querySelectorAll('tr'));
  411. if (!rows.length) return '';
  412.  
  413. const headers = Array.from(rows[0].querySelectorAll('th,td')).map((cell) =>
  414. cell.textContent.trim()
  415. );
  416.  
  417. let markdown = `\n| ${headers.join(' | ')} |\n| ${headers
  418. .map(() => '---')
  419. .join(' | ')} |\n`;
  420.  
  421. for (let i = 1; i < rows.length; i++) {
  422. const cells = Array.from(rows[i].querySelectorAll('td')).map((cell) =>
  423. processInlineElements(cell)
  424. );
  425. markdown += `| ${cells.join(' | ')} |\n`;
  426. }
  427.  
  428. return markdown + '\n';
  429. }
  430.  
  431. function downloadMarkdown(content) {
  432. const titleElement = document.querySelector(`.${SELECTORS.TITLE}`);
  433. const title = titleElement ? titleElement.textContent.trim() : 'DeepSeek对话';
  434. const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
  435. const link = document.createElement('a');
  436. link.href = URL.createObjectURL(blob);
  437. link.download = `${title}.md`;
  438. link.style.display = 'none';
  439. document.body.appendChild(link);
  440. link.click();
  441. setTimeout(() => {
  442. document.body.removeChild(link);
  443. URL.revokeObjectURL(link.href);
  444. }, 1000);
  445. }
  446.  
  447. function showToast(message, isError = false) {
  448. const toast = document.createElement('div');
  449. toast.className = `ds-toast ${isError ? 'error' : 'success'}`;
  450. toast.textContent = message;
  451. document.body.appendChild(toast);
  452.  
  453. toast.addEventListener('animationend', () => {
  454. document.body.removeChild(toast);
  455. });
  456. }
  457.  
  458. // 添加 URL 变化监听
  459. function setupUrlChangeListener() {
  460. let lastUrl = window.location.href;
  461.  
  462. // 监听 URL 变化
  463. setInterval(() => {
  464. if (lastUrl !== window.location.href) {
  465. lastUrl = window.location.href;
  466. const existingContainer = document.getElementById(`${BUTTON_ID}-container`);
  467. if (existingContainer) {
  468. existingContainer.remove();
  469. }
  470. createUI();
  471. }
  472. }, 1000);
  473.  
  474. // 监听 history 变化
  475. const pushState = history.pushState;
  476. history.pushState = function () {
  477. pushState.apply(history, arguments);
  478. const existingContainer = document.getElementById(`${BUTTON_ID}-container`);
  479. if (existingContainer) {
  480. existingContainer.remove();
  481. }
  482. createUI();
  483. };
  484. }
  485.  
  486. const observer = new MutationObserver(() => createUI());
  487. observer.observe(document, { childList: true, subtree: true });
  488. // window.addEventListener('load', createUI);
  489. // setInterval(createUI, 3000);
  490. window.addEventListener('load', () => {
  491. createUI();
  492. setupUrlChangeListener();
  493. });
  494. })();

QingJ © 2025

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