// ==UserScript== // @name DeepSeek 全能增强助手 // @namespace https://github.com/shiyi312/deepseek-enhancer // @version 2026.9.5.1 // @description 全功能:代码折叠、表格导出、思考折叠、防撤回、多格式导出、文件夹管理、快捷键、图形设置面板,优化体验 // @author 辻弌20 // @match https://chat.deepseek.com/* // @icon https://raw.githubusercontent.com/shiyi312/deepseek-enhancer/main/favicon.ico // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // ======================================================================== // 配置与存储 // ======================================================================== const STORAGE = { FOLD_THRESHOLD: 'ds_fold_threshold', PREVIEW_LINES: 'ds_preview_lines', TABLE_BUTTONS: 'ds_table_buttons', AUTO_COLLAPSE_THINK: 'ds_auto_collapse_think', WIDE_SCREEN: 'ds_wide_screen', CTRL_ENTER: 'ds_ctrl_enter', ANTI_RETRACT: 'ds_anti_retract', EXPORT_SETTINGS: 'ds_export_settings', FOLDER_MANAGER: 'ds_folder_manager', FOLDER_DATA: 'ds_folder_data_v2', SHORTCUT_EXPORT: 'ds_shortcut_export', COPY_BUTTON: 'ds_copy_button', ALL_CODE_FOLDED: 'ds_all_code_folded', }; const KEY_MAP = { foldThreshold: 'FOLD_THRESHOLD', previewLines: 'PREVIEW_LINES', tableButtons: 'TABLE_BUTTONS', autoCollapseThink: 'AUTO_COLLAPSE_THINK', wideScreen: 'WIDE_SCREEN', ctrlEnter: 'CTRL_ENTER', antiRetract: 'ANTI_RETRACT', folderManager: 'FOLDER_MANAGER', shortcutExport: 'SHORTCUT_EXPORT', copyButton: 'COPY_BUTTON', allCodeFolded: 'ALL_CODE_FOLDED', }; const DEFAULTS = { foldThreshold: 20, previewLines: 3, tableButtons: true, autoCollapseThink: true, wideScreen: false, ctrlEnter: false, antiRetract: true, folderManager: false, shortcutExport: 'Ctrl+Shift+E', copyButton: true, allCodeFolded: false, exportSettings: { includeUser: true, includeThink: true, onlyReply: false, exportRefs: true, }, }; function getVal(key, def) { const val = GM_getValue(key, null); if (val === null) return def; try { return JSON.parse(val); } catch { return val; } } function setVal(key, val) { GM_setValue(key, JSON.stringify(val)); } let config = { foldThreshold: getVal(STORAGE.FOLD_THRESHOLD, DEFAULTS.foldThreshold), previewLines: getVal(STORAGE.PREVIEW_LINES, DEFAULTS.previewLines), tableButtons: getVal(STORAGE.TABLE_BUTTONS, DEFAULTS.tableButtons), autoCollapseThink: getVal(STORAGE.AUTO_COLLAPSE_THINK, DEFAULTS.autoCollapseThink), wideScreen: getVal(STORAGE.WIDE_SCREEN, DEFAULTS.wideScreen), ctrlEnter: getVal(STORAGE.CTRL_ENTER, DEFAULTS.ctrlEnter), antiRetract: getVal(STORAGE.ANTI_RETRACT, DEFAULTS.antiRetract), folderManager: getVal(STORAGE.FOLDER_MANAGER, DEFAULTS.folderManager), shortcutExport: getVal(STORAGE.SHORTCUT_EXPORT, DEFAULTS.shortcutExport), copyButton: getVal(STORAGE.COPY_BUTTON, DEFAULTS.copyButton), allCodeFolded: getVal(STORAGE.ALL_CODE_FOLDED, DEFAULTS.allCodeFolded), exportSettings: getVal(STORAGE.EXPORT_SETTINGS, DEFAULTS.exportSettings), }; // ======================================================================== // 工具函数 // ======================================================================== function showToast(msg, duration = 2000, type = 'info') { const existing = document.getElementById('ds-toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.id = 'ds-toast'; toast.textContent = msg; const colors = { info: 'rgba(0,0,0,0.8)', success: 'rgba(46,125,50,0.9)', error: 'rgba(198,40,40,0.9)', warning: 'rgba(237,108,2,0.9)' }; Object.assign(toast.style, { position: 'fixed', bottom: '30px', left: '50%', transform: 'translateX(-50%)', background: colors[type] || colors.info, color: 'white', padding: '10px 24px', borderRadius: '10px', fontSize: '14px', fontFamily: 'system-ui, sans-serif', zIndex: 10001, opacity: 0, transition: 'opacity 0.25s ease', pointerEvents: 'none', boxShadow: '0 4px 16px rgba(0,0,0,0.25)', maxWidth: '90vw', textAlign: 'center', }); document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; }); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 250); }, duration); } function getText(el) { return (el?.textContent || '').trim(); } function findAllElements(selectors, context = document) { const results = []; const set = new Set(); for (const sel of selectors) { const els = context.querySelectorAll(sel); for (const el of els) { if (!set.has(el)) { set.add(el); results.push(el); } } } return results; } // ======================================================================== // 完整样式 // ======================================================================== GM_addStyle(` .ds-fold-btn { background: transparent; border: none; border-radius: 8px; font-size: 12px; padding: 2px 8px; cursor: pointer; transition: all 0.2s; font-family: system-ui, sans-serif; user-select: none; display: inline-flex; align-items: center; gap: 4px; opacity: 0.6; color: inherit; position: relative; z-index: 5; } .ds-fold-btn:hover { background: rgba(128,128,128,0.15); opacity: 1; } .ds-fold-btn svg { width: 18px; height: 18px; display: block; fill: currentColor; } .ds-fold-preview { position: relative; overflow: hidden; transition: max-height 0.3s ease; } .ds-fold-preview::after { content: ' ⋯'; display: block; text-align: center; opacity: 0.5; margin-top: 2px; font-size: 12px; } .table-internal-buttons { position: absolute; bottom: 8px; right: 8px; display: flex; flex-direction: column; gap: 6px; z-index: 10; opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; pointer-events: none; } .ds-markdown table:hover .table-internal-buttons, .table-internal-buttons:hover { opacity: 1; visibility: visible; pointer-events: auto; } .internal-export-btn { width: 30px; height: 30px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 6px rgba(0,0,0,0.1); transition: all 0.2s; font-size: 14px; background: rgba(255,255,255,0.92); border: 1px solid rgba(0,0,0,0.08); } .internal-export-btn:hover { background: #fff; transform: scale(1.05); } html.ds-wide-screen [class*="ds-virtual-list-items"][style*="--message-list-max-width"] { --message-list-max-width: 1000px !important; } .ds-think-content.ds-collapsed { display: none !important; } .ds-copy-btn { position: absolute; top: 8px; right: 8px; background: rgba(255,255,255,0.85); backdrop-filter: blur(4px); border: none; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; color: #333; opacity: 0; transition: opacity 0.2s; z-index: 5; box-shadow: 0 2px 6px rgba(0,0,0,0.08); font-family: system-ui, sans-serif; } .ds-message:hover .ds-copy-btn, .ds-copy-btn:hover { opacity: 1; } @media (prefers-color-scheme: dark) { .ds-copy-btn { background: rgba(30,30,40,0.85); color: #ddd; } } #ds-panel-overlay { position: fixed; inset: 0; z-index: 99999; background: rgba(0,0,0,0.4); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; animation: dsFadeIn 0.25s ease; } #ds-panel-overlay.fade-out { animation: dsFadeOut 0.25s ease forwards; } @keyframes dsFadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes dsFadeOut { from { opacity: 1; } to { opacity: 0; } } #ds-panel { background: #1a1a2e; border-radius: 20px; width: 520px; max-width: 94vw; max-height: 88vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.5); font-family: system-ui, -apple-system, sans-serif; color: #e8e8ec; padding: 0; animation: dsModalIn 0.3s ease; } @keyframes dsModalIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } } #ds-panel::-webkit-scrollbar { width: 6px; } #ds-panel::-webkit-scrollbar-track { background: transparent; } #ds-panel::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; } .ds-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; border-bottom: 1px solid rgba(255,255,255,0.06); position: sticky; top: 0; background: #1a1a2e; z-index: 2; border-radius: 20px 20px 0 0; } .ds-panel-header h2 { margin: 0; font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 8px; } .ds-panel-close { background: none; border: none; color: rgba(255,255,255,0.4); font-size: 22px; cursor: pointer; padding: 4px 8px; border-radius: 6px; transition: all 0.2s; } .ds-panel-close:hover { background: rgba(255,255,255,0.08); color: #fff; } .ds-panel-body { padding: 16px 24px 24px; } .ds-card { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 14px 16px; margin-bottom: 12px; } .ds-card-title { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.4; margin-bottom: 10px; } .ds-control { margin-bottom: 12px; } .ds-control:last-child { margin-bottom: 0; } .ds-control-label { font-size: 13px; font-weight: 500; margin-bottom: 2px; display: flex; align-items: center; gap: 6px; } .ds-control-desc { font-size: 11px; opacity: 0.4; margin-bottom: 6px; line-height: 1.4; } .ds-toggle-row { display: flex; align-items: center; justify-content: space-between; cursor: pointer; padding: 4px 0; } .ds-toggle-track { width: 40px; height: 22px; border-radius: 11px; background: rgba(255,255,255,0.15); transition: background 0.25s; flex-shrink: 0; position: relative; } .ds-toggle-track.active { background: #4f46e5; } .ds-toggle-thumb { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: white; transition: transform 0.25s; box-shadow: 0 1px 4px rgba(0,0,0,0.2); } .ds-toggle-track.active .ds-toggle-thumb { transform: translateX(18px); } .ds-input-number { width: 70px; padding: 4px 8px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.06); color: #e8e8ec; font-size: 13px; outline: none; } .ds-input-number:focus { border-color: #4f46e5; } .ds-input-text { width: 180px; padding: 4px 8px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.06); color: #e8e8ec; font-size: 13px; outline: none; } .ds-input-text:focus { border-color: #4f46e5; } .ds-panel-footer { padding: 12px 24px 18px; border-top: 1px solid rgba(255,255,255,0.06); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; } .ds-btn { padding: 8px 18px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; font-family: inherit; } .ds-btn-primary { background: #4f46e5; color: #fff; } .ds-btn-primary:hover { background: #6366f1; } .ds-btn-secondary { background: rgba(255,255,255,0.08); color: #e8e8ec; } .ds-btn-secondary:hover { background: rgba(255,255,255,0.15); } .ds-btn-danger { background: rgba(239,68,68,0.2); color: #f87171; } .ds-btn-danger:hover { background: rgba(239,68,68,0.3); } .ds-btn-reset { background: none; border: none; color: rgba(255,255,255,0.3); font-size: 12px; cursor: pointer; text-decoration: underline; } .ds-btn-reset:hover { color: rgba(255,255,255,0.6); } #ds-export-btn { background: rgba(255,255,255,0.85); backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; padding: 6px 14px; font-size: 13px; font-weight: 500; cursor: pointer; color: #333; display: flex; align-items: center; gap: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); transition: all 0.2s; font-family: system-ui, sans-serif; } #ds-export-btn:hover { background: rgba(255,255,255,0.95); box-shadow: 0 4px 14px rgba(0,0,0,0.1); } #ds-export-btn .arrow { transition: transform 0.25s; display: inline-block; } #ds-export-btn .arrow.open { transform: rotate(180deg); } #ds-global-fold-btn { background: rgba(255,255,255,0.85); backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; padding: 6px 12px; font-size: 13px; font-weight: 500; cursor: pointer; color: #333; display: flex; align-items: center; gap: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); transition: all 0.2s; font-family: system-ui, sans-serif; } #ds-global-fold-btn:hover { background: rgba(255,255,255,0.95); box-shadow: 0 4px 14px rgba(0,0,0,0.1); } @media (prefers-color-scheme: dark) { #ds-global-fold-btn, #ds-export-btn { background: rgba(30,30,40,0.85) !important; color: #ddd !important; border-color: rgba(255,255,255,0.1) !important; } #ds-global-fold-btn:hover, #ds-export-btn:hover { background: rgba(30,30,40,0.95) !important; } } .ds-export-dropdown { position: fixed; top: 48px; right: 80px; z-index: 9999; background: rgba(255,255,255,0.96); backdrop-filter: blur(12px); border: 1px solid rgba(0,0,0,0.08); border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); display: none; flex-direction: column; padding: 6px 0; min-width: 200px; max-height: 80vh; overflow-y: auto; font-family: system-ui, sans-serif; } .ds-export-dropdown .opt { padding: 8px 16px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: background 0.12s; color: #333; } .ds-export-dropdown .opt:hover { background: rgba(0,0,0,0.04); } .ds-export-dropdown .sep { height: 1px; background: #e8e8e8; margin: 4px 8px; } .ds-export-dropdown .filter-label { padding: 6px 16px 2px; font-size: 10px; color: #999; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .ds-export-dropdown .filter-item { padding: 4px 16px; display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; color: #333; } .ds-export-dropdown .filter-item input[type="checkbox"] { margin: 0; width: 15px; height: 15px; cursor: pointer; } .ds-export-dropdown .filter-item:hover { background: rgba(0,0,0,0.02); } @media (prefers-color-scheme: dark) { .ds-export-dropdown { background: rgba(30,30,40,0.95); border-color: rgba(255,255,255,0.08); color: #ddd; } .ds-export-dropdown .opt { color: #ddd; } .ds-export-dropdown .opt:hover { background: rgba(255,255,255,0.05); } .ds-export-dropdown .filter-item { color: #ddd; } .ds-export-dropdown .sep { background: #444; } } #ds-export-loading { position: fixed; inset: 0; z-index: 99999; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.35); backdrop-filter: blur(4px); } .ds-loading-box { background: #1e1e2e; padding: 28px 36px; border-radius: 16px; box-shadow: 0 8px 40px rgba(0,0,0,0.3); text-align: center; color: #e8e8ec; font-family: system-ui, sans-serif; min-width: 180px; } .ds-loading-spinner { width: 36px; height: 36px; border: 3px solid rgba(255,255,255,0.1); border-top-color: #4f46e5; border-radius: 50%; animation: dsSpin 0.8s linear infinite; margin: 0 auto 12px; } @keyframes dsSpin { to { transform: rotate(360deg); } } .ds-folder-panel { margin: 4px 0 8px; padding: 4px 8px; font-size: 13px; color: #e8e8ec; background: rgba(255,255,255,0.03); border-radius: 8px; } .ds-folder-panel .ds-fh { display: flex; align-items: center; justify-content: space-between; padding: 4px 4px 6px; } .ds-folder-panel .ds-fh-title { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 12px; opacity: 0.6; } .ds-folder-panel .ds-fh-title:hover { opacity: 0.9; } .ds-folder-panel .ds-fh-new { background: rgba(255,255,255,0.06); border: none; color: #aaa; border-radius: 12px; padding: 2px 10px; font-size: 11px; cursor: pointer; } .ds-folder-panel .ds-fh-new:hover { background: rgba(255,255,255,0.1); color: #fff; } .ds-folder-panel .ds-folder-item { display: flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; } .ds-folder-panel .ds-folder-item:hover { background: rgba(255,255,255,0.05); } .ds-folder-panel .ds-folder-item .ds-fname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ds-folder-panel .ds-folder-item .ds-fcount { opacity: 0.4; font-size: 11px; } .ds-folder-panel .ds-folder-item .ds-fops { display: none; gap: 4px; } .ds-folder-panel .ds-folder-item:hover .ds-fops { display: flex; } .ds-folder-panel .ds-fops button { background: none; border: none; color: #888; font-size: 11px; cursor: pointer; padding: 0 4px; } .ds-folder-panel .ds-fops button:hover { color: #fff; } .ds-folder-panel .ds-conv-row { display: flex; align-items: center; gap: 6px; padding: 3px 8px 3px 24px; border-radius: 6px; font-size: 12px; cursor: pointer; } .ds-folder-panel .ds-conv-row:hover { background: rgba(255,255,255,0.04); } .ds-folder-panel .ds-conv-row .ds-ctitle { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ds-folder-panel .ds-conv-row .ds-cout { background: rgba(255,255,255,0.06); border: none; color: #888; border-radius: 4px; padding: 0 6px; font-size: 10px; cursor: pointer; } .ds-folder-panel .ds-conv-row .ds-cout:hover { color: #f87171; } .ds-folder-panel .ds-empty { opacity: 0.3; font-size: 12px; padding: 6px 10px; } .ds-folder-panel .ds-caret { display: inline-block; transition: transform 0.2s; font-size: 10px; opacity: 0.5; } .ds-folder-panel .ds-caret.open { transform: rotate(90deg); } .ds-folder-panel .ds-children { padding-left: 8px; } .ds-folder-panel .ds-children.collapsed { display: none; } `); // ======================================================================== // 核心功能:代码折叠(支持全局控制) // ======================================================================== const lineHeightCache = new WeakMap(); function getLineCount(pre) { const text = pre.textContent || ''; const lines = text.split('\n'); if (lines.length && lines[lines.length-1] === '') lines.pop(); return lines.length; } function getLineHeight(pre) { if (lineHeightCache.has(pre)) return lineHeightCache.get(pre); const style = getComputedStyle(pre); let lh = style.lineHeight; if (lh === 'normal') lh = parseFloat(style.fontSize) * 1.2 + 'px'; const result = parseFloat(lh); lineHeightCache.set(pre, result); return result; } function createFoldButton(pre) { const btn = document.createElement('button'); btn.className = 'ds-fold-btn'; const lineCount = getLineCount(pre); const shouldFold = config.foldThreshold > 0 && lineCount > config.foldThreshold; let folded = false; const icons = { down: ``, up: `` }; const updateUI = (isFolded) => { btn.innerHTML = `${isFolded ? icons.up : icons.down}${isFolded ? '展开' : '折叠'}`; btn.setAttribute('aria-label', isFolded ? '展开代码块' : '折叠代码块'); folded = isFolded; }; const fold = () => { if (config.previewLines > 0 && lineCount > config.previewLines) { const lh = getLineHeight(pre); const maxH = lh * config.previewLines; pre.style.maxHeight = maxH + 'px'; pre.style.overflow = 'hidden'; pre.classList.add('ds-fold-preview'); } else { pre.style.display = 'none'; pre.classList.remove('ds-fold-preview'); } updateUI(true); }; const expand = () => { pre.style.maxHeight = ''; pre.style.overflow = ''; pre.style.display = ''; pre.classList.remove('ds-fold-preview'); updateUI(false); }; btn.addEventListener('click', (e) => { e.stopPropagation(); if (folded) expand(); else fold(); }); // 挂载到 pre 元素,供全局控制调用 pre.__dsFold = { fold, expand, isFolded: () => folded }; if (shouldFold) { requestAnimationFrame(fold); } else { updateUI(false); } return btn; } function processCodeBlock(pre) { if (pre.dataset.dsFoldProcessed) return; pre.dataset.dsFoldProcessed = 'true'; const container = pre.closest('.md-code-block, .code-block, [class*="code-block"]'); if (!container) return; const oldBtn = container.querySelector('.ds-fold-btn'); if (oldBtn) oldBtn.remove(); const btn = createFoldButton(pre); let toolbar = container.querySelector('.md-code-block-banner-wrap, .code-info-button-text, [class*="banner"]'); if (toolbar && toolbar.parentElement) { toolbar.parentElement.appendChild(btn); } else { btn.style.position = 'absolute'; btn.style.top = '4px'; btn.style.right = '8px'; btn.style.background = 'rgba(0,0,0,0.05)'; btn.style.borderRadius = '6px'; btn.style.padding = '2px 8px'; container.style.position = 'relative'; container.appendChild(btn); } } function processPreElements(pres) { pres.forEach(pre => { if (!pre.dataset.dsFoldProcessed) processCodeBlock(pre); }); } // ======================================================================== // 表格导出 // ======================================================================== function getCleanTableClone(table) { const clone = table.cloneNode(true); const btns = clone.querySelector('.table-internal-buttons'); if (btns) btns.remove(); clone.style.tableLayout = ''; clone.style.width = ''; clone.style.maxWidth = ''; clone.style.position = ''; clone.querySelectorAll('th,td').forEach(cell => { cell.style.width = ''; cell.style.whiteSpace = ''; cell.style.overflowWrap = ''; cell.style.wordBreak = ''; }); return clone; } function getCellText(cell) { let t = ''; cell.childNodes.forEach(n => { if (n.nodeType === 3) t += n.textContent; else if (n.nodeName === 'BR') t += '\n'; else if (n.nodeType === 1) t += getCellText(n); }); return t.replace(/[^\S\n]+/g, ' ').replace(/ *\n */g, '\n').trim(); } function exportTableAsCSV(table) { const clone = getCleanTableClone(table); const rows = []; const thead = clone.querySelector('thead'); if (thead) thead.querySelectorAll('tr').forEach(tr => { const rd = []; tr.querySelectorAll('th').forEach(th => rd.push(getCellText(th))); if (rd.length) rows.push(rd); }); const tbody = clone.querySelector('tbody'); if (tbody) tbody.querySelectorAll('tr').forEach(tr => { const rd = []; tr.querySelectorAll('td').forEach(td => rd.push(getCellText(td))); if (rd.length) rows.push(rd); }); else clone.querySelectorAll('tr').forEach(tr => { const rd = []; tr.querySelectorAll('td,th').forEach(c => rd.push(getCellText(c))); if (rd.length) rows.push(rd); }); if (!rows.length) { showToast('无数据', 1500, 'warning'); return; } const csv = rows.map(r => r.map(c => { if (typeof c !== 'string') c = String(c); if (c.includes(',') || c.includes('"') || c.includes('\n')) c = '"' + c.replace(/"/g,'""') + '"'; return c; }).join(',')).join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `table_${Date.now()}.csv`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 100); showToast('CSV 导出成功', 1200, 'success'); } function exportTableAsMarkdown(table) { const clone = getCleanTableClone(table); let md = ''; const rows = clone.querySelectorAll('tr'); if (!rows.length) { showToast('无数据', 1500, 'warning'); return; } const headerCells = rows[0].querySelectorAll('th,td'); md += '| ' + Array.from(headerCells).map(th => getCellText(th).replace(/\|/g,'\\|')).join(' | ') + ' |\n'; md += '| ' + Array.from(headerCells).map(() => '---').join(' | ') + ' |\n'; for (let i = 1; i < rows.length; i++) { const cells = rows[i].querySelectorAll('td,th'); md += '| ' + Array.from(cells).map(td => getCellText(td).replace(/\|/g,'\\|')).join(' | ') + ' |\n'; } navigator.clipboard.writeText(md).then(() => showToast('表格已复制为 Markdown', 1200, 'success')).catch(() => showToast('复制失败', 1500, 'error')); } async function exportTableAsPNG(table) { if (typeof html2canvas === 'undefined') { showToast('html2canvas 未加载', 2000, 'error'); return; } const clone = getCleanTableClone(table); const wrapper = document.createElement('div'); wrapper.style.cssText = 'position:fixed;left:-9999px;top:0;background:white;padding:16px;'; wrapper.appendChild(clone); document.body.appendChild(wrapper); try { const canvas = await html2canvas(wrapper, { scale: 2, backgroundColor: '#fff', logging: false, useCORS: true }); canvas.toBlob(blob => { if (blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.download = `table_${Date.now()}.png`; a.href = url; a.click(); setTimeout(() => URL.revokeObjectURL(url), 100); showToast('PNG 导出成功', 1200, 'success'); } else showToast('PNG 导出失败', 1500, 'error'); }, 'image/png'); } catch(e) { showToast('PNG 异常: ' + e.message, 2000, 'error'); } finally { wrapper.remove(); } } function addTableButtons(table) { if (!config.tableButtons || table.dataset.dsTableProcessed) return; table.dataset.dsTableProcessed = 'true'; const old = table.querySelector('.table-internal-buttons'); if (old) old.remove(); const container = document.createElement('div'); container.className = 'table-internal-buttons'; const png = createExportBtn('📸', '导出 PNG', () => exportTableAsPNG(table)); const csv = createExportBtn('📄', '导出 CSV', () => exportTableAsCSV(table)); const md = createExportBtn('📝', '复制 Markdown', () => exportTableAsMarkdown(table)); container.append(png, csv, md); table.style.position = 'relative'; table.appendChild(container); } function createExportBtn(icon, tooltip, action) { const btn = document.createElement('button'); btn.className = 'internal-export-btn'; btn.textContent = icon; btn.title = tooltip; btn.addEventListener('click', e => { e.stopPropagation(); action(); }); return btn; } function processTableElements(tables) { tables.forEach(table => { if (!table.dataset.dsTableProcessed && table.closest('.ds-markdown')) { addTableButtons(table); } }); } // ======================================================================== // 思考折叠(修复:点击仅展开当前,不移除全局样式) // ======================================================================== let thinkStyle = null; let thinkClickHandler = null; function setupThinkCollapse() { if (!thinkStyle) { thinkStyle = document.createElement('style'); thinkStyle.id = 'ds-think-hide'; thinkStyle.textContent = '.ds-think-content { display: none !important; }'; document.head.appendChild(thinkStyle); } if (thinkClickHandler) { document.removeEventListener('click', thinkClickHandler, true); } thinkClickHandler = function(e) { const title = e.target.closest('[class*="_5ab5d64"], [class*="think-title"], [class*="chain-of-thought"]'); if (title && (getText(title).includes('已思考') || getText(title).includes('思考'))) { const content = title.closest('.ds-message, [class*="message"]')?.querySelector('.ds-think-content, [class*="think-content"]'); if (content) { content.classList.remove('ds-collapsed'); } } }; document.addEventListener('click', thinkClickHandler, true); } function processThinkContents(contents) { if (!config.autoCollapseThink) return; contents.forEach(content => { const wrapper = content.closest('.ds-message, [class*="message"]'); if (!wrapper) return; const title = wrapper.querySelector('[class*="_5ab5d64"], [class*="think-title"], [class*="chain-of-thought"]'); if (title && (getText(title).includes('已思考') || getText(title).includes('思考'))) { if (!title.dataset.dsCollapsed) { title.dataset.dsCollapsed = 'true'; content.classList.add('ds-collapsed'); } } }); } // ======================================================================== // 防撤回(增强:尝试从 IndexedDB 恢复) // ======================================================================== const msgCache = new WeakMap(); const pendingUpdates = new Map(); function scheduleCacheUpdate(msgContainer, html, text) { const key = msgContainer; if (pendingUpdates.has(key)) { clearTimeout(pendingUpdates.get(key)); pendingUpdates.delete(key); } const timer = setTimeout(() => { msgCache.set(msgContainer, { html, text }); pendingUpdates.delete(key); }, 1000); pendingUpdates.set(key, timer); } async function fetchMessageFromDB(chatId, msgIdx) { try { const data = await readDeepSeekDB(chatId); const msgs = parseDBData(data); if (msgs && msgs.length > msgIdx) return msgs[msgIdx].content || null; } catch(e) {} return null; } function antiRetractScanForNodes(nodes) { if (!config.antiRetract) return; const aiMessages = []; nodes.forEach(node => { if (node.nodeType !== 1) return; if (node.matches && node.matches('.ds-message:not(.ds-message-user), [class*="assistant-message"]')) { aiMessages.push(node); } if (node.querySelectorAll) { const msgs = node.querySelectorAll('.ds-message:not(.ds-message-user), [class*="assistant-message"]'); msgs.forEach(msg => aiMessages.push(msg)); } }); const chatId = getChatIdFromURL(); aiMessages.forEach((msgContainer, index) => { const contentEl = msgContainer.querySelector('.ds-markdown, [class*="markdown"]'); if (!contentEl) return; const currentText = contentEl.textContent.trim(); const cached = msgCache.get(msgContainer); if (cached === undefined) { scheduleCacheUpdate(msgContainer, contentEl.innerHTML, currentText); return; } const retractKeywords = ['这个问题我暂时无法回答', '我还没学会这个问题', '内容已撤回', '已撤回', '无法回答']; const isRetracted = retractKeywords.some(kw => currentText.includes(kw)); if (isRetracted && cached.text !== currentText) { (async function() { try { let recoveredText = null; if (chatId) { const dbMsg = await fetchMessageFromDB(chatId, index); if (dbMsg) recoveredText = dbMsg; } if (recoveredText) { contentEl.textContent = recoveredText; } else { contentEl.innerHTML = cached.html; } const parent = msgContainer; if (!parent.querySelector('.ds-retract-restore')) { const mark = document.createElement('div'); mark.className = 'ds-retract-restore'; mark.style.cssText = 'color: #f87171; font-size: 12px; margin-top: 6px; opacity: 0.7;'; mark.textContent = '↻ 原回复已被撤回,已自动恢复'; parent.appendChild(mark); } showToast('已恢复被撤回的消息', 1500, 'success'); msgCache.delete(msgContainer); } catch (e) { console.warn('DeepSeek 增强助手: 防撤回恢复失败:', e); // 降级:仍然尝试使用缓存恢复 try { contentEl.innerHTML = cached.html; const parent = msgContainer; if (!parent.querySelector('.ds-retract-restore')) { const mark = document.createElement('div'); mark.className = 'ds-retract-restore'; mark.style.cssText = 'color: #f87171; font-size: 12px; margin-top: 6px; opacity: 0.7;'; mark.textContent = '↻ 原回复已被撤回,已自动恢复(缓存版本)'; parent.appendChild(mark); } showToast('已恢复被撤回的消息(缓存版本)', 1500, 'success'); msgCache.delete(msgContainer); } catch (e2) { console.error('DeepSeek 增强助手: 防撤回降级恢复也失败:', e2); } } })(); } else if (!isRetracted && currentText !== cached.text) { scheduleCacheUpdate(msgContainer, contentEl.innerHTML, currentText); } }); } // ======================================================================== // 复制按钮 // ======================================================================== function addCopyButton(messageElement) { if (!config.copyButton) return; if (messageElement.querySelector('.ds-copy-btn')) return; const btn = document.createElement('button'); btn.className = 'ds-copy-btn'; btn.textContent = '📋 复制'; btn.title = '复制此回复内容'; const contentEl = messageElement.querySelector('.ds-markdown, [class*="markdown"]'); if (!contentEl) return; btn.addEventListener('click', (e) => { e.stopPropagation(); const text = contentEl.textContent.trim(); if (!text) { showToast('无内容可复制', 1000, 'warning'); return; } navigator.clipboard.writeText(text).then(() => { btn.textContent = '✅ 已复制'; setTimeout(() => btn.textContent = '📋 复制', 1500); }).catch(() => { const area = document.createElement('textarea'); area.value = text; document.body.appendChild(area); area.select(); document.execCommand('copy'); document.body.removeChild(area); btn.textContent = '✅ 已复制'; setTimeout(() => btn.textContent = '📋 复制', 1500); }); }); if (getComputedStyle(messageElement).position === 'static') { messageElement.style.position = 'relative'; } messageElement.appendChild(btn); } function processCopyButtons(nodes) { if (!config.copyButton) return; nodes.forEach(node => { if (node.nodeType !== 1) return; if (node.matches && node.matches('.ds-message:not(.ds-message-user), [class*="assistant-message"]')) { if (!node.querySelector('.ds-copy-btn')) addCopyButton(node); } if (node.querySelectorAll) { const msgs = node.querySelectorAll('.ds-message:not(.ds-message-user), [class*="assistant-message"]'); msgs.forEach(msg => { if (!msg.querySelector('.ds-copy-btn')) addCopyButton(msg); }); } }); } // ======================================================================== // 导出对话(包括从 IndexedDB 读取) // ======================================================================== function getChatIdFromURL() { const hash = location.hash; let m = hash.match(/[/]chat[/]([a-f0-9-]+)/i); if (m) return m[1]; const parts = location.pathname.split('/'); return parts[parts.length-1] || ''; } function readDeepSeekDB(chatId) { return new Promise((resolve, reject) => { const req = indexedDB.open('deepseek-chat'); req.onerror = () => reject(new Error('无法打开数据库')); req.onsuccess = () => { const db = req.result; try { const tx = db.transaction('history-message', 'readonly'); const store = tx.objectStore('history-message'); const getReq = store.get(chatId); getReq.onsuccess = () => { const data = getReq.result; db.close(); if (!data) reject(new Error('数据库中无此聊天记录')); else resolve(data); }; getReq.onerror = () => { db.close(); reject(new Error('读取失败')); }; } catch(e) { db.close(); reject(e); } }; }); } function parseDBData(data) { const messages = []; try { const raw = data.chat?.data?.chat_messages || data.chat_messages || data.messages || []; raw.forEach(v => { const content = v.fragments?.[0]?.content || v.content || v.text || ''; const role = v.role || 'user'; const think = v.fragments?.[0]?.reasoning_content || v.reasoning_content || ''; if (content && content.trim()) { const msg = { role, content: content.trim() }; if (think && think.trim()) msg.chain_of_thought = think.trim(); messages.push(msg); } }); } catch(e) { console.error('Parse error', e); } return messages; } function extractMessagesFromDOM() { const messages = []; const userEls = findAllElements(['.fbb737a4', '[class*="user-message"]', '[class*="user-msg"]'], document); const aiEls = findAllElements([ '.ds-message .ds-markdown:not(.ds-think-content .ds-markdown)', '.ds-message [class*="markdown"]', '[class*="assistant-message"] [class*="markdown"]', ], document); const thinkEls = findAllElements(['.ds-think-content', '[class*="think-content"]'], document); const all = []; userEls.forEach(el => all.push({ type: 'user', el })); aiEls.forEach(el => all.push({ type: 'ai', el })); thinkEls.forEach(el => all.push({ type: 'think', el })); all.sort((a, b) => { const pos = a.el.compareDocumentPosition(b.el); if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); let currentThink = ''; all.forEach(item => { if (item.type === 'user') { const content = getText(item.el); if (content) messages.push({ role: 'user', content }); } else if (item.type === 'think') { currentThink = getText(item.el); } else if (item.type === 'ai') { const content = getText(item.el); if (content) { const msg = { role: 'assistant', content }; if (currentThink) { msg.chain_of_thought = currentThink; currentThink = ''; } messages.push(msg); } } }); return messages; } function getConversationTitle() { const titles = ['.f8d1e4c0 .afa34042', '.f8d1e4c0', '[class*="chat-title"]', '[class*="conversation-title"]']; for (const sel of titles) { const el = document.querySelector(sel); if (el) { const text = getText(el); if (text) return text; } } return 'DeepSeek Chat'; } async function collectMessages() { try { const chatId = getChatIdFromURL(); if (chatId) { const data = await readDeepSeekDB(chatId); const msgs = parseDBData(data); if (msgs && msgs.length) return { title: getConversationTitle(), messages: msgs }; } } catch(e) { console.log('IndexedDB fallback:', e.message); } const msgs = extractMessagesFromDOM(); return { title: getConversationTitle(), messages: msgs }; } function filterMessages(messages, settings) { let filtered = messages; if (settings.onlyReply) { filtered = filtered.filter(m => m.role === 'assistant'); filtered = filtered.map(m => ({ role: m.role, content: m.content })); } else { if (!settings.includeUser) filtered = filtered.filter(m => m.role !== 'user'); if (!settings.includeThink) { filtered = filtered.map(m => { if (m.role === 'assistant' && m.chain_of_thought) { const { chain_of_thought, ...rest } = m; return rest; } return m; }); } } return filtered; } function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function convertToMarkdown(data, settings) { let md = ''; if (!settings.onlyReply) { md += `# ${data.title}\n\n- **URL**: ${data.url}\n- **Date**: ${new Date(data.date).toLocaleString()}\n\n---\n\n`; } data.messages.forEach((msg, i) => { if (!settings.onlyReply) { const icon = msg.role === 'user' ? '👤' : '🤖'; md += `## ${icon} ${msg.role === 'user' ? 'User' : 'Assistant'}\n\n`; } if (msg.chain_of_thought && !settings.onlyReply) { md += `**思考过程:**\n${msg.chain_of_thought}\n\n`; } md += `${msg.content}\n\n`; if (!settings.onlyReply && i < data.messages.length-1) md += '---\n\n'; }); return md; } function convertToPlain(data, settings) { let txt = ''; if (!settings.onlyReply) { txt += `${data.title}\n\nURL: ${data.url}\nDate: ${new Date(data.date).toLocaleString()}\n\n${'='.repeat(50)}\n\n`; } data.messages.forEach((msg, i) => { if (!settings.onlyReply) txt += `${msg.role === 'user' ? 'User' : 'Assistant'}:\n\n`; if (msg.chain_of_thought && !settings.onlyReply) txt += `[思考过程]\n${msg.chain_of_thought}\n\n`; txt += `${msg.content}\n\n`; if (!settings.onlyReply && i < data.messages.length-1) txt += '-'.repeat(40) + '\n\n'; }); return txt; } function convertToHTML(data, settings) { const safeTitle = escapeHtml(data.title || 'DeepSeek Chat'); let html = `${safeTitle}
${safeTitle}
🔗 ${escapeHtml(data.url)}
📅 ${new Date(data.date).toLocaleString()}
`; data.messages.forEach((msg, idx) => { const cls = msg.role === 'user' ? 'msg-user' : 'msg-assistant'; const icon = msg.role === 'user' ? '👤' : '🤖'; const label = msg.role === 'user' ? 'User' : 'Assistant'; html += `
`; if (!settings.onlyReply) html += `
${icon} ${label}
`; if (msg.chain_of_thought && !settings.onlyReply) { html += `
💭 思考过程
${escapeHtml(msg.chain_of_thought).replace(/\n/g, '
')}
`; } html += `
${escapeHtml(msg.content || '').replace(/\n/g, '
')}
`; html += `
`; if (!settings.onlyReply && idx < data.messages.length-1) html += `
`; }); html += `