Greasy Fork

Greasy Fork is available in English.

ChatShare - AI 对话下载器

支持几乎所有的ai对话网页的 Markdown 导出 + 图像导出。需要支持新网址,在@match中自行追加使用。主要用于:claude|chatgpt|gemini|grok|doubao|qwen|deepseek|minimax|kimi|yuanbao|chatglm|openrouter|poe等。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatShare - AI 对话下载器
// @namespace    https://github.com/mr-hanlu/chatshare
// @version      0.2.1
// @description  支持几乎所有的ai对话网页的 Markdown 导出 + 图像导出。需要支持新网址,在@match中自行追加使用。主要用于:claude|chatgpt|gemini|grok|doubao|qwen|deepseek|minimax|kimi|yuanbao|chatglm|openrouter|poe等。
// @author       hanlu
// @license      MIT
// @icon         data:image/png;base64,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
// @match        https://aistudio.google.com/*
// @match        https://gemini.google.com/*
// @match        https://notebooklm.google.com/*
// @match        https://claude.ai/*
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @match        https://x.com/*
// @match        https://x.com/i/grok*
// @match        https://grok.com/*
// @match        https://*.grok.com/*
// @match        https://*.doubao.com/*
// @match        https://qwen.ai/*
// @match        https://chat.qwen.ai/*
// @match        https://www.qianwen.com/*
// @match        https://bailian.console.aliyun.com/*
// @match        https://www.kimi.com/*
// @match        https://chat.deepseek.com/*
// @match        https://hunyuan.tencent.com/*
// @match        https://yuanbao.tencent.com/*
// @match        https://chat.z.ai/*
// @match        https://chatglm.cn/*
// @match        https://agent.minimaxi.com/*
// @match        https://aistudio.xiaomimimo.com/*
// @match        https://openrouter.ai/*
// @match        https://poe.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/turndown.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/turndown-plugin-gfm.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/html-to-image.min.js
// @resource     katexCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css
// @grant        GM_getResourceText
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    if (window.self !== window.top) return;
    if (document.getElementById('chatshare-extractor-host')) return;

    let isSelectMode = false;
    let selectedElements = new Set();
    let currentHoverElement = null;
    let isPanelOpen = false;
    let isDragging = false, hasDragged = false;
    let startX, startY, initLeft, initTop;
    let isProcessing = false;
    let hasWarnedLongImage = false;

    // ================= UI 构建与守护 =================
    const host = document.createElement('div');
    host.id = 'chatshare-extractor-host';
    host.style.cssText = `position:fixed;z-index:2147483647;top:30%;left:auto;right:10px;transition:left .35s cubic-bezier(.22,1,.36,1),right .35s cubic-bezier(.22,1,.36,1);`;
    const shadow = host.attachShadow({ mode: 'open' });

    function mountHost() {
        if (!document.documentElement.contains(host)) document.documentElement.appendChild(host);
    }
    mountHost();
    const spaGuardObserver = new MutationObserver(mountHost);
    spaGuardObserver.observe(document.documentElement, { childList: true, subtree: false });

    const style = document.createElement('style');
    style.textContent = `
        :host{all:initial}
        .fab{width:40px;height:40px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer;background:rgba(255,255,255,.9);backdrop-filter:blur(10px);border:1px solid rgba(0,0,0,.06);box-shadow:0 4px 16px rgba(0,0,0,.15);transition:all .2s ease;position:relative;animation:float 3s ease-in-out infinite}
        @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}
        .fab:hover{transform:scale(1.08);box-shadow:0 8px 24px rgba(0,0,0,.2)}
        .panel{position:absolute;top:0;width:260px;border-radius:14px;overflow:hidden;background:rgba(255,255,255,.96);backdrop-filter:blur(16px);border:1px solid rgba(0,0,0,.08);box-shadow:0 12px 40px rgba(0,0,0,.25);display:flex;flex-direction:column;font-family:system-ui,-apple-system,sans-serif;user-select:none;visibility:hidden;pointer-events:none;transform:scale(.92);opacity:0;transition:all .25s ease}
        .panel.show{visibility:visible;pointer-events:auto;transform:scale(1);opacity:1}
        .panel.left{left:48px}.panel.right{right:48px}
        .header{padding:12px 14px;font-size:13px;font-weight:600;background:#f9fafb;cursor:grab;display:flex;justify-content:space-between;align-items:center;color:#374151;border-bottom:1px solid rgba(0,0,0,.05)}
        .close-btn{cursor:pointer;color:#9ca3af;font-size:18px;padding:2px;border-radius:4px}.close-btn:hover{color:#ef4444;background:rgba(0,0,0,.05)}
        .body{padding:12px;display:flex;flex-direction:column;gap:8px}
        .stats{font-size:12px;color:#6b7280;text-align:center;margin:2px 0;padding-bottom:4px;border-bottom:1px dashed #e5e7eb}
        .stats span{color:#3b82f6;font-weight:700;font-size:14px}
        button{border:none;padding:10px;border-radius:8px;font-weight:600;cursor:pointer;transition:all .15s;font-size:13px}
        button:active{transform:scale(.98)}button:disabled{opacity:.5;cursor:not-allowed;transform:none}
        .btn-md{background:#10b981;color:#fff}.btn-md:hover{background:#059669}
        .btn-img{background:#f59e0b;color:#fff}.btn-img:hover{background:#d97706}
        .btn-md,.btn-img{display:none}

        .sc-toast { position: fixed; top: 20px; left: 50%; transform: translate(-50%, -20px); background: rgba(17,24,39,.95); color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px; font-family: system-ui, sans-serif; font-weight: 500; opacity: 0; pointer-events: none; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); z-index: 2147483647; box-shadow: 0 8px 24px rgba(0,0,0,.15); display: flex; align-items: center; gap: 8px; white-space: nowrap; }
        .sc-toast.show { transform: translate(-50%, 0); opacity: 1; }
        .sc-toast.error { background: #ef4444; }
        .sc-toast.warn { background: #f59e0b; }
        .sc-toast.info { background: #3b82f6; }
    `;
    shadow.appendChild(style);

    const fab = document.createElement('div'); fab.className = 'fab right'; fab.textContent = '🤖';
    const panel = document.createElement('div'); panel.className = 'panel';
    const header = document.createElement('div'); header.className = 'header'; header.id = 'drag-handle';
    const titleSpan = document.createElement('span'); titleSpan.textContent = '📦 对话提取器';
    const btnClose = document.createElement('span'); btnClose.className = 'close-btn'; btnClose.textContent = '×';
    header.append(titleSpan, btnClose);
    const body = document.createElement('div'); body.className = 'body';
    const statsContainer = document.createElement('div'); statsContainer.className = 'stats';
    statsContainer.append(document.createTextNode('已选中 '));
    const countSpan = document.createElement('span'); countSpan.textContent = '0';
    statsContainer.append(countSpan, document.createTextNode(' 段'));
    const btnMd = document.createElement('button'); btnMd.className = 'btn-md'; btnMd.textContent = '📄 导出 Markdown';
    const btnImg = document.createElement('button'); btnImg.className = 'btn-img'; btnImg.textContent = '🖼️ 导出图像';

    body.append(statsContainer, btnMd, btnImg);
    panel.append(header, body);

    const toastEl = document.createElement('div'); toastEl.className = 'sc-toast';
    shadow.appendChild(toastEl);
    shadow.appendChild(fab); shadow.appendChild(panel);

    const dragHandle = header;

    // ================= 全局 UI Toast 系统 =================
    let toastTimer = null;
    function showToast(msg, type = 'info') {
        toastEl.textContent = msg; toastEl.className = `sc-toast show ${type}`;
        clearTimeout(toastTimer);
        toastTimer = setTimeout(() => { toastEl.classList.remove('show'); }, 3500);
    }

    // ================= 拖拽及面板控制 =================
    function attachDrag(t) {
        t.addEventListener('mousedown', e => {
            if (e.button !== 0 || isProcessing) return;
            isDragging = true; hasDragged = false;
            startX = e.clientX; startY = e.clientY;
            const r = host.getBoundingClientRect(); initLeft = r.left; initTop = r.top;
            host.style.transition = 'none';
            host.style.left = r.left + 'px'; host.style.right = 'auto'; e.preventDefault();
        });
    }
    attachDrag(fab); attachDrag(dragHandle);

    window.addEventListener('mousemove', e => {
        if (!isDragging) return;
        if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) {
            hasDragged = true; host.style.left = (initLeft + e.clientX - startX) + 'px'; host.style.top = (initTop + e.clientY - startY) + 'px';
        }
    });
    window.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; snapToEdge(); } });

    let resizeTimer = null;
    window.addEventListener('resize', () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => {
            if (!isDragging && host.getBoundingClientRect) {
                snapToEdge();
                if (isPanelOpen) adjustPanelPosition();
            }
        }, 150);
    });

    function snapToEdge() {
        host.style.transition = '';
        const r = host.getBoundingClientRect();
        if (r.left + r.width / 2 < window.innerWidth / 2) { host.style.left = '10px'; fab.classList.replace('right', 'left'); }
        else { host.style.left = (window.innerWidth - 48) + 'px'; fab.classList.replace('left', 'right'); }
    }
    function adjustPanelPosition() {
        const r = host.getBoundingClientRect();
        panel.classList.remove('left', 'right'); panel.classList.add(window.innerWidth - r.right < 270 ? 'right' : 'left');
    }

    fab.addEventListener('click', () => {
        if (hasDragged || isProcessing) return;
        isPanelOpen = !isPanelOpen;
        if (isPanelOpen) { adjustPanelPosition(); panel.classList.add('show'); fab.style.transform = 'scale(.85)'; enterSelectMode(); }
        else closePanelAndExit();
    });
    btnClose.addEventListener('click', () => { if (!isProcessing) closePanelAndExit(); });
    document.addEventListener('keydown', e => { if (e.key === 'Escape' && isPanelOpen && !isProcessing) closePanelAndExit(); });
    function closePanelAndExit() { isPanelOpen = false; panel.classList.remove('show'); fab.style.transform = ''; exitSelectMode(); }

    // ================= 选取模式 =================
    function applyHighlight(el) { if (el && el.style) { el.style.backgroundColor = 'rgba(16, 185, 129, 0.1)'; el.style.boxShadow = 'inset 0 0 0 2px #10b981'; el.style.outline = 'none'; el.setAttribute('data-sc-selected', 'true'); } }
    function removeHighlight(el) { if (el && el.style) { el.style.removeProperty('background-color'); el.style.removeProperty('box-shadow'); el.style.removeProperty('outline'); el.removeAttribute('data-sc-selected'); } }
    function applyHover(el) { if (el && el.style && el.getAttribute('data-sc-selected') !== 'true') el.style.outline = '2px dashed #3b82f6'; }
    function removeHover(el) { if (el && el.style && el.getAttribute('data-sc-selected') !== 'true') el.style.removeProperty('outline'); }
    function forceClearAll() {
        if (currentHoverElement) { removeHover(currentHoverElement); currentHoverElement = null; }
        selectedElements.forEach(el => removeHighlight(el)); selectedElements.clear(); countSpan.textContent = '0';
        hasWarnedLongImage = false;
    }

    function enterSelectMode() {
        isSelectMode = true; document.addEventListener('mouseover', onMouseOver, true); document.addEventListener('mouseout', onMouseOut, true); document.addEventListener('click', onDocClick, true);
        statsContainer.style.display = 'block'; btnMd.style.display = 'block'; btnImg.style.display = 'block';
    }
    function exitSelectMode() {
        isSelectMode = false; document.removeEventListener('mouseover', onMouseOver, true); document.removeEventListener('mouseout', onMouseOut, true); document.removeEventListener('click', onDocClick, true);
        statsContainer.style.display = 'none'; btnMd.style.display = 'none'; btnImg.style.display = 'none'; forceClearAll();
    }

    function onMouseOver(e) {
        if (isProcessing) return;
        if (e.composedPath().includes(host)) return; const el = e.target.closest('div, article, li, p, section') || e.target;
        if (currentHoverElement === el) return; removeHover(currentHoverElement); currentHoverElement = el; applyHover(el);
    }
    function onMouseOut(e) {
        if (isProcessing) return;
        if (e.composedPath().includes(host)) return; removeHover(currentHoverElement); currentHoverElement = null;
    }

    function calcTotalEstimatedHeight() {
        let total = 0; selectedElements.forEach(s => { total += (s.offsetHeight || 0) + 70; }); return total;
    }

    function onDocClick(e) {
        if (isProcessing) { e.preventDefault(); e.stopPropagation(); return; }
        if (!e.isTrusted || e.composedPath().includes(host)) return;

        if (e.target.closest('a, button, [role="button"], input, textarea, select')) return;

        e.preventDefault(); e.stopPropagation();
        const el = currentHoverElement || e.target.closest('div, article, li, p, section') || e.target; if (!el) return;

        const isSelected = el.getAttribute('data-sc-selected') === 'true';

        if (isSelected) {
            removeHighlight(el); selectedElements.delete(el); if (currentHoverElement === el) applyHover(el);
        } else {
            const toRemove = []; selectedElements.forEach(s => { if (s.contains(el) || el.contains(s)) toRemove.push(s); });
            toRemove.forEach(s => { removeHighlight(s); selectedElements.delete(s); });
            applyHighlight(el); selectedElements.add(el); if (currentHoverElement === el) removeHover(el);

            const th = calcTotalEstimatedHeight();
            if (th > 16000 && !hasWarnedLongImage) {
                showToast('📏 已超出长图安全极限,将无法导出图像,但可无限导出 Markdown!', 'warn'); hasWarnedLongImage = true;
            }
        }
        countSpan.textContent = selectedElements.size;
    }

    // ================= 基础工具 =================
    const STYLE_WHITELIST = ['color', 'backgroundColor', 'borderColor', 'outlineColor', 'fill', 'stroke', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'lineHeight', 'letterSpacing', 'textDecoration', 'textAlign', 'whiteSpace', 'display', 'width', 'maxWidth', 'minWidth', 'overflow', 'overflowX', 'overflowY', 'boxSizing', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink', 'flexBasis', 'alignItems', 'alignSelf', 'justifyContent', 'gap', 'gridTemplateColumns', 'gridTemplateRows', 'gridColumn', 'gridRow', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderStyle', 'borderRadius', 'opacity', 'verticalAlign'];

    function guessRole(el) {
        const r = (el.dataset.messageAuthorRole || el.dataset.role || el.dataset.sender || '').toLowerCase();
        if (r.includes('user')) return 'user'; if (r.includes('assistant') || r.includes('ai')) return 'ai';
        const html = el.innerHTML.toLowerCase(); if (html.includes('author-role="user"')) return 'user'; if (html.includes('author-role="assistant"')) return 'ai';
        return (el.querySelectorAll('pre,code,table,.katex').length > 0 || el.innerText.trim().length > 150) ? 'ai' : 'user';
    }

    function getSortedElements() { return Array.from(selectedElements).sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1); }

    function isMathNode(n) {
        if (!n || n.nodeType !== Node.ELEMENT_NODE) return false;
        if (n.tagName.toUpperCase() === 'MATH' || n.ownerSVGElement) return true;
        const c = typeof n.className === 'string' ? n.className : '';
        return c.includes('katex') || c.includes('MathJax') || !!n.closest('.katex, .MathJax');
    }

    // ================= 💎 Markdown 专属深度外科手术 =================

    function purifyCodeBlocksForMD(node) {
        const pres = Array.from(node.querySelectorAll('pre'));
        pres.forEach(pre => {
            let codeText = '';
            const table = pre.querySelector('table.hljs-ln, table.linenumber');
            if (table) {
                const codeTds = table.querySelectorAll('td.hljs-ln-code, td.code');
                codeText = Array.from(codeTds).map(td => td.textContent).join('\n');
            } else {
                const codeEl = pre.querySelector('code');
                codeText = (codeEl || pre).textContent;
            }

            let lang = '';
            const codeEl = pre.querySelector('code');
            if (codeEl && codeEl.className) {
                const match = codeEl.className.match(/language-(\w+)/i);
                if (match) lang = match[1];
            }

            let widget = pre;
            let current = pre.parentElement;
            while (current && current !== node && current.tagName === 'DIV') {
                if (current.querySelectorAll('pre').length > 1) break;
                const extraTextLength = current.textContent.length - pre.textContent.length;
                if (extraTextLength < 150) {
                    widget = current;
                    if (!lang) {
                        const langMatch = current.textContent.match(/(python|javascript|js|html|css|bash|shell|json|markdown|md|java|cpp|c|go|rust|ruby|php|sql|yaml|xml|typescript|ts)\b/i);
                        if (langMatch) lang = langMatch[1].toLowerCase();
                    }
                    current = current.parentElement;
                } else {
                    break;
                }
            }

            const cleanPre = document.createElement('pre');
            const cleanCode = document.createElement('code');
            if (lang) cleanCode.className = `language-${lang}`;
            cleanCode.textContent = codeText;
            cleanPre.appendChild(cleanCode);

            try { widget.parentElement.replaceChild(cleanPre, widget); } catch (e) { }
        });
    }

    function purifyMathForMD(node) {
        const replaceWithMath = (target, tex, isBlock) => {
            const span = document.createElement('span');
            span.className = 'chatshare-math';
            span.textContent = isBlock ? `\n$$ ${tex} $$\n` : `$${tex}$`;
            try { target.parentElement.replaceChild(span, target); } catch (e) { }
        };

        node.querySelectorAll('.katex').forEach(k => {
            const annotation = k.querySelector('annotation[encoding="application/x-tex"]');
            if (annotation) {
                const isBlock = k.parentElement?.classList.contains('katex-display');
                replaceWithMath(isBlock ? k.parentElement : k, annotation.textContent, isBlock);
            }
        });

        node.querySelectorAll('mjx-container').forEach(m => {
            const mathElement = m.querySelector('math');
            let tex = mathElement ? mathElement.getAttribute('alttext') : '';
            if (!tex) {
                const script = m.nextElementSibling;
                if (script && script.tagName === 'SCRIPT' && script.type.includes('math/tex')) tex = script.textContent;
            }
            if (tex) replaceWithMath(m, tex, m.getAttribute('display') === 'block');
        });
    }

    function purifyHeadingsForMD(node) {
        const els = Array.from(node.querySelectorAll('div, p, span'));
        els.forEach(el => {
            if (!node.contains(el)) return;
            if (el.closest('h1, h2, h3, h4, h5, h6, strong, b')) return;
            if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'STRONG', 'B', 'PRE', 'CODE', 'TH', 'TD', 'LI'].includes(el.tagName)) return;

            const fw = el.style.fontWeight;
            const fz = el.style.fontSize;
            const isBold = fw === 'bold' || fw === '700' || fw === '800' || fw === '900' || parseInt(fw, 10) >= 600;

            if (isBold) {
                const text = el.textContent.trim();
                if (text.length > 0 && text.length < 150 && el.children.length <= 2) {
                    let isHeader = false;
                    if (fz && fz.endsWith('px') && parseFloat(fz) >= 18) isHeader = true;
                    else if (fz && (fz.endsWith('em') || fz.endsWith('rem')) && parseFloat(fz) >= 1.2) isHeader = true;

                    if (isHeader) {
                        const h = document.createElement('h3');
                        while (el.firstChild) { h.appendChild(el.firstChild); }
                        try { el.parentElement.replaceChild(h, el); } catch (e) { }
                    } else {
                        if (el.tagName === 'DIV' || el.tagName === 'P') {
                            if (!el.querySelector('strong')) {
                                const strong = document.createElement('strong');
                                while (el.firstChild) { strong.appendChild(el.firstChild); }
                                el.appendChild(strong);
                            }
                        } else if (el.tagName === 'SPAN') {
                            const strong = document.createElement('strong');
                            while (el.firstChild) { strong.appendChild(el.firstChild); }
                            try { el.parentElement.replaceChild(strong, el); } catch (e) { }
                        }
                    }
                }
            }
        });
    }

    // ================= Markdown 核心导出引擎 =================
    btnMd.addEventListener('click', () => {
        if (selectedElements.size === 0) {
            showToast('⚠️ 请先选择内容!', 'warn'); return;
        }

        const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
        if (typeof turndownPluginGfm !== 'undefined') td.use(turndownPluginGfm.gfm);

        td.addRule('math', {
            filter: function (node) { return node.nodeName === 'SPAN' && node.className === 'chatshare-math'; },
            replacement: function (content, node) { return node.textContent; }
        });

        td.addRule('hr', {
            filter: 'hr',
            replacement: function () { return '\n\n---\n\n'; }
        });

        let md = '';
        getSortedElements().forEach(el => {
            const role = guessRole(el);

            const dummyParent = document.createElement('div');
            const clone = el.cloneNode(true);

            inlineComputedStylesIterative(el, clone);
            dummyParent.appendChild(clone);

            purifyCodeBlocksForMD(dummyParent);
            purifyMathForMD(dummyParent);
            purifyHeadingsForMD(dummyParent);

            dummyParent.querySelectorAll('button,[role="button"],script,style,.action-btn,.copy-btn,svg').forEach(n => n.remove());

            let block = td.turndown(dummyParent);
            md += role === 'user' ? `### 👤 User\n\n> ${block.replace(/\n/g, '\n> ')}\n\n---\n\n` : `### 🤖 AI\n\n${block}\n\n---\n\n`;
        });

        downloadFile(md, 'md', 'text/markdown;charset=utf-8');
        showToast('✅ Markdown 导出成功!', 'info');
        closePanelAndExit();
    });

    // ================= 图像导出引擎逻辑 =================
    function inlineComputedStylesIterative(origRoot, cloneRoot) {
        const stack = [{ orig: origRoot, clone: cloneRoot }];
        while (stack.length > 0) {
            const { orig, clone } = stack.pop();
            if (clone.nodeType === Node.ELEMENT_NODE) {
                const tag = clone.tagName.toUpperCase();
                if (tag === 'BUTTON' || tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' ||
                    clone.classList.contains('copy-btn') || clone.classList.contains('action-btn')) {
                    clone.remove(); continue;
                }
                if (tag === 'IMG') { clone.dataset.origW = orig.clientWidth || 0; clone.dataset.origH = orig.clientHeight || 0; }
            }
            if (orig.nodeType === Node.ELEMENT_NODE && clone.nodeType === Node.ELEMENT_NODE && !isMathNode(orig)) {
                const comp = window.getComputedStyle(orig);
                if (comp) {
                    if (comp.display === 'none') { clone.style.display = 'none'; continue; }
                    for (const p of STYLE_WHITELIST) {
                        let v = comp[p];
                        if (!v || (p === 'width' && (v === 'auto' || v.endsWith('%')))) continue;
                        clone.style[p] = v;
                    }
                }
            }
            if (orig.nodeType === Node.ELEMENT_NODE && orig.tagName.toUpperCase() === 'SVG') continue;

            if (orig.nodeType === Node.ELEMENT_NODE && !isMathNode(orig)) {
                const minLen = Math.min(orig.childNodes.length, clone.childNodes.length);
                for (let i = 0; i < minLen; i++) stack.push({ orig: orig.childNodes[i], clone: clone.childNodes[i] });
            }
        }
    }

    function stripExternalAttrs(root) {
        const walk = (n) => {
            if (n.nodeType !== Node.ELEMENT_NODE || isMathNode(n)) return;
            if (n.tagName.toUpperCase() === 'SVG' && !n.getAttribute('xmlns')) {
                n.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            }
            n.removeAttribute('class'); n.removeAttribute('id');
            for (let c of n.childNodes) walk(c);
        }; walk(root);
    }

    function resetLayout(root) {
        const allNodes = [root, ...root.querySelectorAll('*')];
        allNodes.forEach(el => {
            if (el.tagName === 'IMG' || el.ownerSVGElement || isMathNode(el)) return;

            el.style.overflow = 'visible';
            el.style.maxHeight = 'none';
            el.style.height = 'auto';

            if (['fixed', 'sticky', 'absolute'].includes(el.style.position)) {
                el.style.position = 'relative'; el.style.top = el.style.left = el.style.bottom = el.style.right = el.style.zIndex = '';
            }
        });
    }

    // 🟢 核心防御1:提前铲除多媒体炸弹和 HTTP 不安全图片
    function sanitizeDOMForImage(root) {
        // 彻底移除会导致 htmlToImage 崩溃的多媒体标签
        root.querySelectorAll('iframe, video, audio, object, embed').forEach(el => el.remove());

        // 如果在安全环境(HTTPS)下,强行干掉一切 HTTP 的图片引用,用透明占位代替
        if (location.protocol === 'https:') {
            const blankBase64 = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

            // 处理标签属性
            root.querySelectorAll('[src],[href],[poster],[data-src]').forEach(el => {
                ['src', 'href', 'poster', 'data-src'].forEach(attr => {
                    const v = el.getAttribute(attr);
                    if (v && v.startsWith('http://')) {
                        el.setAttribute(attr, blankBase64);
                    }
                });
            });

            // 处理 CSS 背景图
            root.querySelectorAll('*').forEach(el => {
                if (el.style && el.style.backgroundImage && el.style.backgroundImage.includes('http://')) {
                    el.style.backgroundImage = 'none';
                }
            });
        }
    }

    function fixRelativeUrls(root) {
        const base = location.origin + location.pathname;
        root.querySelectorAll('[src],[href],[poster],[data-src]').forEach(el => {
            ['src', 'href', 'poster', 'xlink:href', 'data-src'].forEach(attr => {
                const v = el.getAttribute(attr); if (v && !v.startsWith('data:') && !v.match(/^https?:\/\//)) { try { el.setAttribute(attr, new URL(v, base).href); } catch { } }
            });
        });
    }

    async function preProcessImages(root) {
        const imgs = Array.from(root.querySelectorAll('img'));
        const promises = imgs.map(async (img) => {
            const src = img.getAttribute('src') || img.getAttribute('data-src');
            if (!src || src.startsWith('data:')) return;
            return new Promise((resolve) => {
                const imgObj = new Image(); imgObj.crossOrigin = 'anonymous';
                const timerId = setTimeout(() => resolve(), 3000);

                imgObj.onload = () => {
                    clearTimeout(timerId);
                    let w = parseInt(img.dataset.origW) || imgObj.naturalWidth; let h = parseInt(img.dataset.origH) || imgObj.naturalHeight;
                    if (w === 0) w = imgObj.naturalWidth; if (h === 0) h = imgObj.naturalHeight;
                    const MAX_DIM = 800;
                    if (w > MAX_DIM || h > MAX_DIM) {
                        const ratio = Math.min(MAX_DIM / w, MAX_DIM / h); w = Math.round(w * ratio); h = Math.round(h * ratio);
                    }
                    if (w <= 0 || h <= 0) return resolve();
                    const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h;
                    const ctx = canvas.getContext('2d'); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, w, h); ctx.drawImage(imgObj, 0, 0, w, h);
                    try { img.src = canvas.toDataURL('image/jpeg', 0.85); img.removeAttribute('srcset'); } catch (e) { }
                    resolve();
                };
                imgObj.onerror = () => {
                    clearTimeout(timerId);
                    // 出错时立刻使用空白图,避免污染
                    try { img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; } catch (e) { }
                    resolve();
                };
                imgObj.src = !src.startsWith('http') ? new URL(src, location.origin).href : src;
            });
        });
        await Promise.all(promises);
    }

    btnImg.addEventListener('click', async () => {
        if (selectedElements.size === 0) { showToast('⚠️ 请先选择内容!', 'warn'); return; }
        if (typeof window.htmlToImage === 'undefined') { showToast('❌ 渲染库加载失败,请检查网络。', 'error'); return; }

        const estHeight = calcTotalEstimatedHeight();
        if (estHeight > 20000) { showToast('❌ 当前内容过长,超出图像生成极限。请减少选中段落,或直接使用 Markdown 导出!', 'error'); return; }

        if (isProcessing) return;
        isProcessing = true;

        if (currentHoverElement) { removeHover(currentHoverElement); currentHoverElement = null; }

        btnImg.disabled = true; btnImg.textContent = '⏳ 拉取字体/资源中(初次较慢)...';

        try {
            const blobData = await renderToCanvas();
            if (blobData) { downloadFile(blobData, 'jpg', 'image/jpeg'); showToast('✅ 图像已成功保存!', 'info'); }
        } catch (e) {
            console.error('[ChatShare Error]:', e);
            // 🟢 核心防御3:如果依然有错,捕获真实的错误信息而不只是 undefined
            const msg = (e && e.message) ? e.message : (typeof e === 'string' ? e : '资源跨域拦截或被污染,建议仅导出 Markdown');
            showToast(`❌ 生成失败:${msg}`, 'error');
        } finally { isProcessing = false; btnImg.disabled = false; btnImg.textContent = '🖼️ 导出图像'; closePanelAndExit(); }
    });

    async function renderToCanvas() {
        const isDark = window.matchMedia('(prefers-color-scheme:dark)').matches;
        const bg = isDark ? '#111827' : '#f3f4f6'; const cardBg = isDark ? '#1f2937' : '#ffffff';
        const border = isDark ? 'rgba(255,255,255,.08)' : 'rgba(0,0,0,.06)'; const text = isDark ? '#e5e7eb' : '#111827';

        await loadCSS();

        const outerWrapper = document.createElement('div');
        outerWrapper.className = 'sc-export-wrapper';
        outerWrapper.style.cssText = `position: fixed; left: -99999px; top: 0; display: inline-block; flex-direction: column;`;

        const wrapper = document.createElement('div');
        wrapper.style.cssText = `padding: 40px; background: ${bg}; font-family: system-ui, -apple-system, sans-serif; color: ${text}; line-height: 1.6; overflow: visible; box-sizing: border-box; display: inline-block; min-width: 850px;`;
        outerWrapper.appendChild(wrapper);

        const card = document.createElement('div');
        card.style.cssText = `background: ${cardBg}; border-radius: 16px; border: 1px solid ${border}; box-shadow: 0 8px 30px rgba(0,0,0,.12); box-sizing: border-box; width: 100%;`;
        wrapper.appendChild(card);

        const fragment = document.createDocumentFragment();
        const sortedEls = getSortedElements();

        for (let i = 0; i < sortedEls.length; i++) {
            const el = sortedEls[i]; const role = guessRole(el); const clone = el.cloneNode(true);
            stripExternalAttrs(clone);
            inlineComputedStylesIterative(el, clone);
            resetLayout(clone);
            fixRelativeUrls(clone);
            sanitizeDOMForImage(clone); // 🟢 介入:洗劫引发崩溃的多媒体和 HTTP 图片节点

            clone.style.removeProperty('background-color'); clone.style.removeProperty('box-shadow'); clone.style.removeProperty('outline');
            const wrap = document.createElement('div');
            wrap.style.cssText = `box-sizing: border-box; padding: 24px 30px; ${i < sortedEls.length - 1 ? `border-bottom: 1px solid ${border}` : ''}; overflow: visible;`;
            const head = document.createElement('div');
            head.style.cssText = `font-weight: 600; margin-bottom: 10px; font-size: 14px; color: ${role === 'user' ? '#3b82f6' : '#10b981'};`;
            head.textContent = role === 'user' ? '👤 User' : '🤖 AI';
            wrap.append(head, clone); fragment.appendChild(wrap);

            if ((i + 1) % 5 === 0 || i === sortedEls.length - 1) {
                btnImg.textContent = `⏳ 处理节点 ${i + 1}/${sortedEls.length}...`; await new Promise(r => setTimeout(r, 0));
            }
        }
        card.appendChild(fragment);

        btnImg.textContent = `⏳ 压缩图像资源...`; await preProcessImages(card);
        btnImg.textContent = `⏳ 计算排版边界...`; document.body.appendChild(outerWrapper);

        await document.fonts.ready; wrapper.offsetHeight; await new Promise(r => requestAnimationFrame(r));
        let realCardW = card.scrollWidth; const MIN_CARD_W = 770; if (realCardW < MIN_CARD_W) realCardW = MIN_CARD_W;
        const finalWidth = realCardW + 80; const finalHeight = wrapper.scrollHeight;
        outerWrapper.style.width = finalWidth + 'px'; wrapper.style.width = finalWidth + 'px'; card.style.width = realCardW + 'px';
        await new Promise(r => requestAnimationFrame(r));

        btnImg.textContent = `⏳ 极速生成图层...`;
        let pr = 1.5;
        if (finalHeight * 1.5 <= 16000) pr = 1.5; else if (finalHeight * 1.25 <= 16000) pr = 1.25; else if (finalHeight * 1.0 <= 16000) pr = 1.0; else if (finalHeight * 0.8 <= 16000) pr = 0.8;
        else { showToast('🚧 最终排版高度极大超限,为防崩溃已取消。请使用 Markdown 导出!', 'error'); if (document.body.contains(outerWrapper)) document.body.removeChild(outerWrapper); return null; }

        try {
            // 🟢 核心防御2:allowTaint 改为 false,严禁污染的 Canvas,并利用 imagePlaceholder 安全跳过坏图
            const blob = await window.htmlToImage.toBlob(wrapper, {
                quality: 0.88,
                backgroundColor: bg,
                pixelRatio: pr,
                width: finalWidth,
                height: finalHeight,
                cacheBust: false,
                useCORS: true,
                allowTaint: false,
                imagePlaceholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
                filter: (node) => {
                    if (node.tagName) {
                        const t = node.tagName.toUpperCase();
                        if (['SCRIPT', 'NOSCRIPT', 'VIDEO', 'AUDIO', 'IFRAME', 'OBJECT'].includes(t)) return false;
                    }
                    return true;
                }
            });
            return blob;
        } finally { if (document.body.contains(outerWrapper)) document.body.removeChild(outerWrapper); }
    }

    // 🟢 增强版 loadCSS:加入防重复机制,彻底修复相对路径 404 引发的漫长超时问题
    function loadCSS() {
        return new Promise((resolve) => {
            if (document.getElementById('chatshare-katex-style')) return resolve();
            try {
                let cssText = GM_getResourceText("katexCSS");
                if (cssText) {
                    // 将相对路径替换为 CDN 绝对路径
                    cssText = cssText.replace(/url\((?:'|")?fonts\//g, 'url(https://cdn.jsdelivr.net/npm/[email protected]/dist/fonts/');
                    const style = document.createElement('style');
                    style.id = 'chatshare-katex-style';
                    style.textContent = cssText;
                    document.head.appendChild(style);
                }
            } catch (e) {
                console.error('[ChatShare] loadCSS Error:', e);
            }
            resolve();
        });
    }

    function downloadFile(content, ext, type, isDataUrl = false) {
        const a = document.createElement('a');
        a.download = `ChatShare_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.${ext}`;

        if (content instanceof Blob) {
            a.href = URL.createObjectURL(content);
        } else if (isDataUrl) {
            a.href = content;
        } else {
            a.href = URL.createObjectURL(new Blob([content], { type }));
        }

        document.body.appendChild(a);

        setTimeout(() => {
            a.click();
            setTimeout(() => {
                if (document.body.contains(a)) document.body.removeChild(a);
                URL.revokeObjectURL(a.href);
            }, 1000);
        }, 0);
    }

    function cleanup() { if (isPanelOpen) closePanelAndExit(); forceClearAll(); }
    window.addEventListener('popstate', cleanup); window.addEventListener('beforeunload', cleanup); document.addEventListener('turbo:before-cache', cleanup);

    const targetHistory = typeof unsafeWindow !== 'undefined' ? unsafeWindow.history : history;
    const origPush = targetHistory.pushState;
    targetHistory.pushState = function (...args) {
        origPush.apply(this, args);
        cleanup();
    };
    window.addEventListener('hashchange', cleanup);

    // ================= 🚀 后台无感预热引擎 =================
    function warmUpHtmlToImage() {
        if (typeof window.htmlToImage === 'undefined') return;

        console.log('[ChatShare Extractor] 开始后台静默预热渲染引擎...');
        // 创建一个极其微小、不可见的“幽灵”节点
        const ghostNode = document.createElement('div');
        ghostNode.style.cssText = 'position:fixed; top:-9999px; left:-9999px; width:1px; height:1px; opacity:0; pointer-events:none;';
        ghostNode.textContent = ' '; // 赋予空文本,激活字体解析体系
        document.body.appendChild(ghostNode);

        // 强迫 html-to-image 渲染幽灵节点,迫使底层引擎加载所有 CSS 并将其缓存为 Base64
        window.htmlToImage.toPixelData(ghostNode, {
            cacheBust: false,
            useCORS: true,
            allowTaint: false,
            // 屏蔽不需要的重型资源请求,加速预热
            filter: (node) => node.tagName ? !['IMG', 'SCRIPT', 'VIDEO', 'IFRAME'].includes(node.tagName.toUpperCase()) : true
        }).then(() => {
            console.log('[ChatShare Extractor] 预热完成,字体及样式已全部进入内存缓存!');
        }).catch((e) => {
            // 预热阶段的跨域报错静默吞掉即可,不影响使用
            console.log('[ChatShare Extractor] 预热部分结束(含预期内跨域阻断)。');
        }).finally(() => {
            if (document.body.contains(ghostNode)) {
                document.body.removeChild(ghostNode);
            }
        });
    }

    function scheduleWarmUp() {
        // 先确保公式样式存在,以便被一并预热
        loadCSS();

        // 充分利用主线程空闲时间,绝不造成页面卡顿
        if ('requestIdleCallback' in window) {
            window.requestIdleCallback(() => {
                setTimeout(warmUpHtmlToImage, 2000);
            }, { timeout: 10000 });
        } else {
            setTimeout(warmUpHtmlToImage, 4000);
        }
    }

    // 监控页面路由的初期重构,保证即使单页应用重新加载也能兜底预热
    const warmUpObserver = new MutationObserver(() => {
        if (typeof window.htmlToImage !== 'undefined' && !window._chatshare_warmed) {
            window._chatshare_warmed = true;
            scheduleWarmUp();
        }
    });
    warmUpObserver.observe(document.head, { childList: true });

    // 启动首次预热
    scheduleWarmUp();

    console.log('[ChatShare Extractor] 挂载成功');
})();