// ==UserScript== // @name DeepSeek 纯净导出(带对话名称版 v3.1) // @namespace https://chat.deepseek.com/ // @version 3.1 // @description 直接解析 DOM 将对话还原为 Markdown,文件名自动附带对话名称 // @match https://chat.deepseek.com/* // @grant none // @require https://cdn.jsdelivr.net/npm/turndown@7.1.2/dist/turndown.min.js // @require https://cdn.jsdelivr.net/npm/turndown-plugin-gfm@1.0.2/dist/turndown-plugin-gfm.min.js // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; const DEBUG = true; const log = (...args) => { if (DEBUG) console.log('[DS-Export]', ...args); }; /* ===== 初始化 Turndown 引擎 ===== */ const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', bulletListMarker: '-' }); // 启用 GFM 插件(支持表格、删除线等) if (typeof turndownPluginGfm !== 'undefined') { turndownService.use(turndownPluginGfm.tables); turndownService.use(turndownPluginGfm.strikethrough); } // 自定义规则:处理代码块和 Mermaid turndownService.addRule('codeBlock', { filter: function (node) { return node.nodeName === 'PRE' && node.querySelector('code'); }, replacement: function (content, node) { const codeNode = node.querySelector('code'); let code = codeNode.textContent; let lang = ''; const className = codeNode.className || ''; const langMatch = className.match(/language-(\w+)/); if (langMatch) lang = langMatch[1]; if (className.includes('mermaid') || node.querySelector('.mermaid')) { lang = 'mermaid'; } return '\n```' + lang + '\n' + code + '\n```\n'; } }); // 自定义规则:处理已经被渲染成 SVG 的 Mermaid turndownService.addRule('mermaidSvg', { filter: function (node) { return node.nodeName === 'SVG' || (node.nodeName === 'DIV' && node.querySelector('svg')); }, replacement: function (content, node) { if (node.getAttribute('data-code')) { return '\n```mermaid\n' + node.getAttribute('data-code') + '\n```\n'; } if (node.closest('pre')) return ''; return '\n```mermaid\n%% Mermaid 图表(源码未在DOM中保留)\n```\n'; } }); /* ===== 工具函数 ===== */ // 获取对话名称并清理非法字符 function getChatTitle() { // 优先从浏览器标题获取(最稳定) let title = document.title.replace(/ - DeepSeek$/i, '').trim(); // 如果标题为空或者是默认的,尝试从页面顶部找 if (!title || title === 'DeepSeek') { const titleEl = document.querySelector('.header-title, [class*="title"]'); if (titleEl && titleEl.textContent.trim()) { title = titleEl.textContent.trim(); } } // 兜底 if (!title) title = '对话'; // 清理 Windows/Mac 不支持的非法文件名字符,并限制长度 return title.replace(/[\\/:*?"<>|]/g, '_').slice(0, 50); } function download(filename, content) { const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function timestamp() { const d = new Date(); const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`; } /* ===== 注入按钮 ===== */ function injectButtons() { const mdBlocks = document.querySelectorAll('.ds-markdown'); if (mdBlocks.length === 0) return; mdBlocks.forEach((md, index) => { if (md.dataset.dsExportInjected) return; md.dataset.dsExportInjected = '1'; const btnWrapper = document.createElement('div'); btnWrapper.style.cssText = 'display: flex; gap: 8px; margin-top: 10px; margin-bottom: 4px;'; // 1. 导出单条 AI 回复 const singleBtn = document.createElement('button'); singleBtn.textContent = '导出本条'; singleBtn.style.cssText = 'padding: 4px 10px; font-size: 12px; background: #f0f4ff; border: 1px solid #d0d9ff; color: #4d6bfe; border-radius: 6px; cursor: pointer; transition: all 0.2s;'; singleBtn.onmouseover = () => singleBtn.style.background = '#e8eeff'; singleBtn.onmouseout = () => singleBtn.style.background = '#f0f4ff'; singleBtn.onclick = (e) => { e.stopPropagation(); const mdText = turndownService.turndown(md.innerHTML); // 文件名加入名称:DeepSeek_名称_AI_时间.md const title = getChatTitle(); download(`DeepSeek_${title}_AI_${timestamp()}.md`, mdText); }; btnWrapper.appendChild(singleBtn); // 2. 导出本轮(向上寻找用户消息) const roundBtn = document.createElement('button'); roundBtn.textContent = '导出本轮'; roundBtn.style.cssText = 'padding: 4px 10px; font-size: 12px; background: #4d6bfe; border: none; color: #fff; border-radius: 6px; cursor: pointer;'; roundBtn.onclick = (e) => { e.stopPropagation(); let userMd = ''; let parent = md.parentElement; for (let i = 0; i < 6; i++) { if (!parent) break; const textNodes = [...parent.querySelectorAll('div, p')].filter(n => !n.closest('.ds-markdown') && n.textContent.trim().length > 0 && n.children.length === 0 ); if (textNodes.length > 0) { const potentialUserText = textNodes[0].textContent.trim(); if (potentialUserText && potentialUserText.length < 2000) { userMd = potentialUserText; break; } } parent = parent.parentElement; } const aiMd = turndownService.turndown(md.innerHTML); const finalContent = userMd ? (userMd + '\n\n' + aiMd) : aiMd; // 文件名加入名称:DeepSeek_名称_本轮_时间.md const title = getChatTitle(); download(`DeepSeek_${title}_本轮_${timestamp()}.md`, finalContent); }; btnWrapper.appendChild(roundBtn); md.parentElement.insertBefore(btnWrapper, md.nextSibling); }); } /* ===== 监听 DOM 与初始化 ===== */ function start() { injectButtons(); setInterval(injectButtons, 2000); const observer = new MutationObserver(() => { injectButtons(); }); observer.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } })();