支持Claude、Gemini、NotebookLM等多平台的全功能AI对话导出管理工具

Lyra's Exporter - AI对话导出器的配套脚本,专业的AI对话导出器 - 支持Claude、Gemini、NotebookLM等多平台,轻松管理数百个对话窗口,导出完整时间线、附加图片、思考过程、附件、工具调用和Artifacts。

// ==UserScript==
// @name         支持Claude、Gemini、NotebookLM等多平台的全功能AI对话导出管理工具
// @name:en      Lyra's Exporter Fetch - Enhanced
// @namespace    userscript://lyra-conversation-exporter
// @version      1.4
// @description  Lyra's Exporter - AI对话导出器的配套脚本,专业的AI对话导出器 - 支持Claude、Gemini、NotebookLM等多平台,轻松管理数百个对话窗口,导出完整时间线、附加图片、思考过程、附件、工具调用和Artifacts。
// @description:en The essential companion script for Lyra's Exporter, designed for unified management and export of your conversation histories across multiple platforms, including Claude, Gemini, NotebookLM, and more.
// @homepage     https://yalums.github.io/lyra-exporter/
// @supportURL   https://github.com/Yalums/lyra-exporter/issues
// @author       Yalums
// @match        https://pro.easychat.top/*
// @match        https://claude.ai/*
// @match        https://gemini.google.com/app/*
// @match        https://notebooklm.google.com/*
// @match        https://aistudio.google.com/*
// @include      *://gemini.google.com/*
// @include      *://notebooklm.google.com/*
// @include      *://aistudio.google.com/*
// @run-at       document-start
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @license      GNU General Public License v3.0
// ==/UserScript==

(function() {
    'use strict';

    // 防止重复初始化
    if (window.lyraFetchInitialized) {
        return;
    }
    window.lyraFetchInitialized = true;

    // ===== 调试模式 =====
    const DEBUG_MODE = true;
    const debugLog = (...args) => {
        if (DEBUG_MODE) {
            console.log('[Lyra Debug]', ...args);
        }
    };

    // ===== i18n 国际化系统 =====
    const i18n = {
        languages: {
            'zh': {
                name: '中文',
                translations: {
                    loading: '加载中...',
                    exporting: '导出中...',
                    processing: '处理中...',
                    preparing: '准备中...',
                    scanning: '扫描中...',
                    scanComplete: '扫描完成,正在提取...',
                    scrollToTop: '正在滚动到顶部...',
                    startScanning: '开始向下扫描...',
                    exportSuccess: '导出成功!',
                    exportCancelled: '导出已取消。',
                    operationCancelled: '操作已取消。',
                    noContent: '没有可导出的对话内容。',
                    saveConversation: '导出JSON',
                    viewOnline: '预览对话',
                    exportCurrentJSON: '导出JSON',
                    exportAllConversations: '导出所有',
                    branchMode: '分支模式',
                    includeImages: '包含图片',
                    claudeTitle: 'Claude 会话导出',
                    geminiTitle: 'Gemini 会话导出',
                    notebooklmTitle: 'NotebookLM',
                    aistudioTitle: 'AI Studio 导出',
                    defaultTitle: 'AI 导出器',
                    enterTitle: '请输入对话标题:',
                    defaultChatTitle: '对话',
                    untitledChat: '未命名对话',
                    uuidNotFound: '未找到对话UUID!',
                    userIdNotFound: '未能自动获取用户ID,请手动输入或刷新页面重试',
                    fetchFailed: '获取对话数据失败',
                    exportFailed: '导出失败: ',
                    loadFailed: '加载失败: ',
                    cannotOpenExporter: '无法打开 Lyra Exporter,请检查',
                    connectionTimeout: '连接超时,请检查 Lyra Exporter 是否正常运行',
                    gettingConversation: '获取对话',
                    withImages: ' (处理图片中...)',
                    foundItems: '已发现',
                    items: '条',
                    successExported: '成功导出',
                    conversations: '个对话!',
                    manualUserId: '手动设置ID',
                    enterUserId: '请输入您的组织ID (settings/account):',
                    userIdSaved: '用户ID已保存!',
                    autoDetecting: '自动检测中...',
                    detectedUserId: '检测到用户: '
                }
            },
            'en': {
                name: 'English',
                translations: {
                    loading: 'Loading...',
                    exporting: 'Exporting...',
                    processing: 'Processing...',
                    preparing: 'Preparing...',
                    scanning: 'Scanning...',
                    scanComplete: 'Scan complete, extracting...',
                    scrollToTop: 'Scrolling to top...',
                    startScanning: 'Starting to scan...',
                    exportSuccess: 'Export successful!',
                    exportCancelled: 'Export cancelled.',
                    operationCancelled: 'Operation cancelled.',
                    noContent: 'No conversation content to export.',
                    saveConversation: 'Save File',
                    viewOnline: 'Preview',
                    exportCurrentJSON: 'Export JSON',
                    exportAllConversations: 'Export All',
                    branchMode: 'Branch',
                    includeImages: 'Images',
                    claudeTitle: 'Claude Exporter',
                    geminiTitle: 'Gemini Exporter',
                    notebooklmTitle: 'NotebookLM',
                    aistudioTitle: 'AI Studio',
                    defaultTitle: 'Exporter',
                    enterTitle: 'Enter conversation title:',
                    defaultChatTitle: 'Chat',
                    untitledChat: 'Untitled Chat',
                    uuidNotFound: 'UUID not found!',
                    userIdNotFound: 'Unable to auto-detect user ID, please enter manually or refresh the page',
                    fetchFailed: 'Failed to fetch conversation data',
                    exportFailed: 'Export failed: ',
                    loadFailed: 'Load failed: ',
                    cannotOpenExporter: 'Cannot open Lyra Exporter, please check popup blocker',
                    connectionTimeout: 'Connection timeout, please check if Lyra Exporter is running',
                    gettingConversation: 'Getting conversation',
                    withImages: ' (processing images...)',
                    foundItems: 'Found',
                    items: 'items',
                    successExported: 'Successfully exported',
                    conversations: 'conversations!',
                    manualUserId: 'Input Account ID',
                    enterUserId: 'Organization ID can be found in settings/account',
                    userIdSaved: 'User ID saved!',
                    autoDetecting: 'Auto-detecting...',
                    detectedUserId: 'Account '
                }
            }
        },
        currentLang: null,
        init() {
            const savedLang = localStorage.getItem('lyraExporterLanguage');
            if (savedLang && this.languages[savedLang]) {
                this.currentLang = savedLang;
            } else {
                const browserLang = navigator.language || navigator.userLanguage;
                this.currentLang = browserLang.startsWith('zh') ? 'zh' : 'en';
            }
        },
        t(key) {
            const translations = this.languages[this.currentLang]?.translations;
            return translations?.[key] || key;
        },
        setLanguage(lang) {
            if (this.languages[lang]) {
                this.currentLang = lang;
                localStorage.setItem('lyraExporterLanguage', lang);
                return true;
            }
            return false;
        },
        getLanguage() {
            return this.currentLang;
        }
    };

    i18n.init();

    // ===== 平台检测 =====
    let currentPlatform = '';
    const hostname = window.location.hostname;
    if (hostname.includes('easychat.top') || hostname.includes('claude.ai')) {
        currentPlatform = 'claude';
    } else if (hostname.includes('gemini.google.com')) {
        currentPlatform = 'gemini';
    } else if (hostname.includes('notebooklm.google.com')) {
        currentPlatform = 'notebooklm';
    } else if (hostname.includes('aistudio.google.com')) {
        currentPlatform = 'aistudio';
    }

    debugLog('Platform detected:', currentPlatform);

    // ===== 全局变量 =====
    const LYRA_EXPORTER_URL = 'https://yalums.github.io/lyra-exporter/';
    const LYRA_EXPORTER_ORIGIN = 'https://yalums.github.io';
    let capturedUserId = '';
    let isPanelCollapsed = localStorage.getItem('lyraExporterCollapsed') === 'true';
    let includeImages = localStorage.getItem('lyraIncludeImages') === 'true';
    const CONTROL_ID = "lyra-universal-exporter-container";
    const TOGGLE_ID = "lyra-toggle-button";
    const TREE_SWITCH_ID = "lyra-tree-mode";
    const IMAGE_SWITCH_ID = "lyra-image-mode";
    const LANG_SWITCH_ID = "lyra-lang-switch";
    const MANUAL_ID_BTN = "lyra-manual-id-btn";
    let panelInjected = false;
    const SCROLL_DELAY_MS = 250;
    const SCROLL_TOP_WAIT_MS = 1000;
    let collectedData = new Map();

    // ===== Claude用户ID捕获增强版 =====
    if (currentPlatform === 'claude') {
        debugLog('Initializing Claude user ID capture...');

        const savedUserId = localStorage.getItem('lyraClaudeUserId');
        if (savedUserId) {
            capturedUserId = savedUserId;
            debugLog('Restored user ID from localStorage:', capturedUserId);
        }

        // 方法1:解决沙箱隔离问题
        const interceptorScript = document.createElement('script');
        interceptorScript.textContent = `
            (function() {
                console.log('[Lyra Page Script] Installing interceptors in page context...');

                const patterns = [
                    /api\\/organizations\\/([a-zA-Z0-9-]+)/,
                    /api\\/accounts\\/([a-zA-Z0-9-]+)/,
                    /organizations\\/([a-zA-Z0-9-]+)\\/chat/,
                    /user\\/([a-zA-Z0-9-]+)/
                ];

                function captureUserId(url) {
                    for (const pattern of patterns) {
                        const match = url.match(pattern);
                        if (match && match[1]) {
                            const userId = match[1];
                            localStorage.setItem('lyraClaudeUserId', userId);
                            console.log('[Lyra Page Script] ✨ Captured user ID:', userId);

                            // 触发自定义事件通知用户脚本
                            window.dispatchEvent(new CustomEvent('lyraUserIdCaptured', {
                                detail: { userId: userId }
                            }));
                            return userId;
                        }
                    }
                    return null;
                }

                const originalXHROpen = XMLHttpRequest.prototype.open;
                XMLHttpRequest.prototype.open = function(method, url) {
                    captureUserId(url);
                    return originalXHROpen.apply(this, arguments);
                };

                const originalFetch = window.fetch;
                window.fetch = function(resource, options) {
                    const url = typeof resource === 'string' ? resource : (resource.url || '');
                    if (url) {
                        captureUserId(url);
                    }
                    return originalFetch.apply(this, arguments);
                };

                console.log('[Lyra Page Script] Interceptors installed successfully');
            })();
        `;
        (document.head || document.documentElement).appendChild(interceptorScript);
        interceptorScript.remove();

        // 监听来自页面的用户ID捕获事件
        window.addEventListener('lyraUserIdCaptured', (event) => {
            const userId = event.detail.userId;
            if (userId) {
                capturedUserId = userId;
                debugLog('Received user ID from page context:', capturedUserId);
            }
        });

        // 方法2:定期检查 localStorage(作为备用)
        const checkLocalStorage = () => {
            const storedId = localStorage.getItem('lyraClaudeUserId');
            if (storedId && storedId !== capturedUserId) {
                capturedUserId = storedId;
                debugLog('Updated user ID from localStorage:', capturedUserId);
            }
        };

        setInterval(checkLocalStorage, 2000);

        // 方法3:DOM加载后尝试从页面提取
        document.addEventListener('DOMContentLoaded', () => {
            setTimeout(() => {
                if (!capturedUserId) {
                    debugLog('Attempting to extract user ID from page...');
                    checkLocalStorage();

                    const scripts = document.querySelectorAll('script');
                    scripts.forEach(script => {
                        const content = script.textContent;
                        if (content.includes('organization') || content.includes('account')) {
                            const match = content.match(/["']([a-zA-Z0-9-]{36})["']/);
                            if (match) {
                                capturedUserId = match[1];
                                localStorage.setItem('lyraClaudeUserId', capturedUserId);
                                debugLog('Captured user ID from script:', capturedUserId);
                            }
                        }
                    });
                }
            }, 2000);
        });

        // 调试:定期显示状态
        setInterval(() => {
            if (capturedUserId) {
                debugLog('User ID status: OK -', capturedUserId);
            } else {
                debugLog('User ID status: Not captured yet');
                checkLocalStorage(); // 再次尝试从 localStorage 获取
            }
        }, 5000);
    }

    // ===== 手动输入用户ID功能 =====
    function promptForUserId() {
        const currentId = capturedUserId || localStorage.getItem('lyraClaudeUserId') || '';
        const newId = prompt(i18n.t('enterUserId'), currentId);
        if (newId && newId.trim()) {
            capturedUserId = newId.trim();
            localStorage.setItem('lyraClaudeUserId', capturedUserId);
            alert(i18n.t('userIdSaved'));
            debugLog('Manual user ID set:', capturedUserId);
            const panel = document.getElementById(CONTROL_ID);
            if (panel) {
                panel.remove();
                panelInjected = false;
                createFloatingPanel();
            }
        }
    }

    // ===== 获取用户ID(带备用方案)=====
    async function ensureUserId() {
        if (capturedUserId) {
            return capturedUserId;
        }

        const savedId = localStorage.getItem('lyraClaudeUserId');
        if (savedId) {
            capturedUserId = savedId;
            return capturedUserId;
        }

        await sleep(1000);
        if (capturedUserId) {
            return capturedUserId;
        }

        const result = confirm(i18n.t('userIdNotFound') + '\n\n' + '是否手动输入组织ID?');
        if (result) {
            promptForUserId();
            return capturedUserId;
        }

        return null;
    }

    // ===== TrustedTypes处理 =====
    let trustedPolicy;
    if (typeof window.trustedTypes !== 'undefined' && window.trustedTypes.createPolicy) {
        try {
            trustedPolicy = window.trustedTypes.createPolicy('lyra-exporter-policy', {
                createHTML: (input) => input
            });
        } catch (e) {
            try {
                trustedPolicy = window.trustedTypes.defaultPolicy ||
                               window.trustedTypes.createPolicy('default', {
                                   createHTML: (input) => input
                               });
            } catch (e2) {
                debugLog('TrustedTypes not available');
            }
        }
    }

    function safeSetInnerHTML(element, html) {
        if (trustedPolicy) {
            element.innerHTML = trustedPolicy.createHTML(html);
        } else {
            element.innerHTML = html;
        }
    }

    // ===== 样式注入 =====
    function injectCustomStyle() {
        let primaryColor = '#1a73e8';
        let buttonColor = '#1a73e8';
        let buttonHoverColor = '#1765c2';

        switch(currentPlatform) {
            case 'claude':
                primaryColor = '#d97706';
                buttonColor = '#EA580C';
                buttonHoverColor = '#DC2626';
                break;
            case 'notebooklm':
                primaryColor = '#374151';
                buttonColor = '#4B5563';
                buttonHoverColor = '#1F2937';
                break;
        }

        const styleContent = `
            #${CONTROL_ID} {
                position: fixed !important; right: 12px !important; bottom: 60px !important;
                display: flex !important; flex-direction: column !important; gap: 6px !important;
                z-index: 2147483647 !important; transition: all 0.3s ease !important;
                background: white !important; border-radius: 10px !important;
                box-shadow: 0 3px 12px rgba(0,0,0,0.15) !important; padding: 8px !important;
                border: 1px solid ${currentPlatform === 'notebooklm' ? '#9CA3AF' : currentPlatform === 'claude' ? 'rgba(217, 119, 6, 0.3)' : '#e0e0e0'} !important;
                width: auto !important; min-width: 140px !important;
                font-family: 'Google Sans', Roboto, Arial, sans-serif !important; color: #3c4043 !important;
            }
            #${CONTROL_ID}.collapsed .lyra-main-controls { display: none !important; }
            #${CONTROL_ID}.collapsed {
                padding: 6px !important; width: 36px !important; height: 36px !important; min-width: 36px !important;
                justify-content: center !important; align-items: center !important; overflow: hidden !important;
            }
            #${TOGGLE_ID} {
                position: absolute !important; left: -18px !important; top: 50% !important;
                transform: translateY(-50%) !important; width: 24px !important; height: 24px !important;
                border-radius: 50% !important; display: flex !important; align-items: center !important;
                justify-content: center !important; background: #f1f3f4 !important; color: #2C84DB !important;
                cursor: pointer !important; border: 1px solid #dadce0 !important;
                transition: all 0.3s !important; box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important;
            }
            #${CONTROL_ID}.collapsed #${TOGGLE_ID} {
                position: static !important; transform: none !important; left: auto !important; top: auto !important;
            }
            #${TOGGLE_ID}:hover { background: #e8eaed !important; }
            .lyra-main-controls { display: flex !important; flex-direction: column !important; gap: 8px !important; align-items: stretch !important; width: 100% !important; }
            .lyra-button {
                display: inline-flex !important; align-items: center !important; justify-content: center !important;
                padding: 6px 12px !important; border-radius: 16px !important; cursor: pointer !important;
                font-size: 12px !important; font-weight: 500 !important; background-color: ${buttonColor} !important;
                color: white !important; border: none !important; transition: all 0.3s !important;
                box-shadow: 0 1px 2px rgba(0,0,0,0.1) !important; font-family: 'Google Sans', Roboto, Arial, sans-serif !important;
                white-space: nowrap !important; width: 100% !important; gap: 6px !important;
                min-width: 124px !important;
            }
            .lyra-button:hover { background-color: ${buttonHoverColor} !important; box-shadow: 0 2px 4px rgba(0,0,0,0.15) !important; }
            .lyra-button:disabled { opacity: 0.6 !important; cursor: not-allowed !important; }
            .lyra-button.lyra-manual-btn {
                background-color: #6B7280 !important;
                font-size: 11px !important;
                padding: 4px 8px !important;
            }
            .lyra-button.lyra-manual-btn:hover {
                background-color: #4B5563 !important;
            }
            .lyra-button svg { flex-shrink: 0 !important; width: 16px !important; height: 16px !important; }
            .lyra-title { font-size: 11px !important; font-weight: 500 !important; color: ${primaryColor} !important; margin-bottom: 6px !important; text-align: center !important; }
            .lyra-status {
                font-size: 10px !important;
                color: #6B7280 !important;
                text-align: center !important;
                padding: 2px 4px !important;
                background: #F3F4F6 !important;
                border-radius: 4px !important;
                margin-bottom: 4px !important;
            }
            .lyra-status.success {
                background: #D1FAE5 !important;
                color: #065F46 !important;
            }
            .lyra-status.error {
                background: #FEE2E2 !important;
                color: #991B1B !important;
            }
            .lyra-toggle {
                display: flex !important; align-items: center !important; justify-content: space-between !important;
                font-size: 11px !important; margin-bottom: 4px !important; gap: 4px !important; color: #5f6368 !important;
                width: 100% !important; padding: 0 2px !important;
            }
            .lyra-toggle span { flex: 1 !important; text-align: left !important; }
            .lyra-switch { position: relative !important; display: inline-block !important; width: 28px !important; height: 14px !important; flex-shrink: 0 !important; }
            .lyra-switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; }
            .lyra-slider { position: absolute !important; cursor: pointer !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background-color: #ccc !important; transition: .4s !important; border-radius: 34px !important; }
            .lyra-slider:before { position: absolute !important; content: "" !important; height: 10px !important; width: 10px !important; left: 2px !important; bottom: 2px !important; background-color: white !important; transition: .4s !important; border-radius: 50% !important; }
            input:checked + .lyra-slider { background-color: ${buttonColor} !important; }
            input:checked + .lyra-slider:before { transform: translateX(14px) !important; }
            .lyra-loading {
                display: inline-block !important; width: 16px !important; height: 16px !important;
                border: 2px solid rgba(255, 255, 255, 0.3) !important; border-radius: 50% !important;
                border-top-color: #fff !important; animation: lyra-spin 1s linear infinite !important;
            }
            @keyframes lyra-spin { to { transform: rotate(360deg); } }
            .lyra-progress { font-size: 10px !important; color: #5f6368 !important; margin-top: 4px !important; text-align: center !important; width: 100%; }
            .lyra-lang-toggle {
                display: flex !important; align-items: center !important; justify-content: center !important;
                font-size: 10px !important; margin-top: 4px !important; gap: 4px !important;
                padding: 2px 6px !important; border-radius: 12px !important;
                background: #f1f3f4 !important; cursor: pointer !important; transition: all 0.2s !important;
                width: 100% !important;
            }
            .lyra-lang-toggle:hover { background: #e8eaed !important; }
        `;

        if (typeof GM_addStyle === 'function') {
            GM_addStyle(styleContent);
        } else {
            const existingStyle = document.querySelector('style[data-lyra-styles]');
            if (!existingStyle) {
                const style = document.createElement('style');
                style.textContent = styleContent;
                style.setAttribute('data-lyra-styles', 'true');
                document.head.appendChild(style);
            }
        }
    }

    // ===== 工具函数 =====
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function blobToBase64(blob) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onloadend = () => resolve(reader.result.split(',')[1]);
            reader.onerror = reject;
            reader.readAsDataURL(blob);
        });
    }

    function toggleCollapsed() {
        const container = document.getElementById(CONTROL_ID);
        const toggleButton = document.getElementById(TOGGLE_ID);
        if (container && toggleButton) {
            isPanelCollapsed = !isPanelCollapsed;
            container.classList.toggle('collapsed', isPanelCollapsed);
            safeSetInnerHTML(toggleButton, isPanelCollapsed ?
                '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>' :
                '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>');
            localStorage.setItem('lyraExporterCollapsed', isPanelCollapsed);
        }
    }

    // ===== HTML转Markdown函数(从1.1恢复)=====
    function htmlToMarkdown(element) {
        if (!element) return '';

        let result = '';

        function processNode(node) {
            if (node.nodeType === Node.TEXT_NODE) {
                return node.textContent;
            }

            if (node.nodeType !== Node.ELEMENT_NODE) {
                return '';
            }

            const tagName = node.tagName.toLowerCase();
            const children = Array.from(node.childNodes).map(processNode).join('');

            switch(tagName) {
                case 'h1': return `\n# ${children}\n`;
                case 'h2': return `\n## ${children}\n`;
                case 'h3': return `\n### ${children}\n`;
                case 'h4': return `\n#### ${children}\n`;
                case 'h5': return `\n##### ${children}\n`;
                case 'h6': return `\n###### ${children}\n`;
                case 'strong':
                case 'b': return `**${children}**`;
                case 'em':
                case 'i': return `*${children}*`;
                case 'code':
                    if (children.includes('\n')) {
                        return `\n\`\`\`\n${children}\n\`\`\`\n`;
                    }
                    return `\`${children}\``;
                case 'pre':
                    const codeChild = node.querySelector('code');
                    if (codeChild) {
                        const lang = codeChild.className.match(/language-(\w+)/)?.[1] || '';
                        const codeContent = codeChild.textContent;
                        return `\n\`\`\`${lang}\n${codeContent}\n\`\`\`\n`;
                    }
                    return `\n\`\`\`\n${children}\n\`\`\`\n`;
                case 'hr': return '\n---\n';
                case 'br': return '\n';
                case 'p': return `\n${children}\n`;
                case 'div': return `${children}\n`;
                case 'a':
                    const href = node.getAttribute('href');
                    if (href) {
                        return `[${children}](${href})`;
                    }
                    return children;
                case 'ul':
                case 'ol':
                    return `\n${children}\n`;
                case 'li':
                    const parent = node.parentElement;
                    if (parent && parent.tagName.toLowerCase() === 'ol') {
                        const index = Array.from(parent.children).indexOf(node) + 1;
                        return `${index}. ${children}\n`;
                    }
                    return `- ${children}\n`;
                case 'blockquote': return `\n> ${children.split('\n').join('\n> ')}\n`;
                case 'table': return `\n${children}\n`;
                case 'thead': return `${children}`;
                case 'tbody': return `${children}`;
                case 'tr': return `${children}|\n`;
                case 'th': return `| **${children}** `;
                case 'td': return `| ${children} `;
                default: return children;
            }
        }

        result = processNode(element);
        result = result.replace(/\n{3,}/g, '\n\n');
        result = result.trim();

        return result;
    }

    // ===== 图片获取函数(从1.1恢复)=====
    function fetchViaGM(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                responseType: "blob",
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response.response);
                    } else {
                        reject(new Error(`GM_xmlhttpRequest 失败,状态码: ${response.status}`));
                    }
                },
                onerror: function(error) {
                    reject(new Error(`GM_xmlhttpRequest 网络错误: ${error.statusText}`));
                }
            });
        });
    }

    // ===== Gemini图片处理函数(从1.1恢复)=====
    async function processImageElement(imgElement) {
        if (!imgElement) return null;
        let imageUrlToFetch = null;
        const previewContainer = imgElement.closest('user-query-file-preview');
        if (previewContainer) {
            const lensLinkElement = previewContainer.querySelector('a[href*="lens.google.com"]');
            if (lensLinkElement && lensLinkElement.href) {
                try {
                    const urlObject = new URL(lensLinkElement.href);
                    const realImageUrl = urlObject.searchParams.get('url');
                    if (realImageUrl) {
                        imageUrlToFetch = realImageUrl;
                    }
                } catch (e) {
                    debugLog('Error parsing Lens URL:', e);
                }
            }
        }
        if (!imageUrlToFetch) {
            const fallbackSrc = imgElement.src;
            if (fallbackSrc && !fallbackSrc.startsWith('data:')) {
                imageUrlToFetch = fallbackSrc;
            }
        }
        if (!imageUrlToFetch) {
            return null;
        }
        try {
            const blob = await fetchViaGM(imageUrlToFetch);
            const base64 = await blobToBase64(blob);
            return {
                type: 'image',
                format: blob.type,
                size: blob.size,
                data: base64,
                original_src: imageUrlToFetch
            };
        } catch (error) {
            debugLog('Error processing image:', error);
            return null;
        }
    }

    // ===== Gemini数据提取函数(从1.1恢复)=====
    async function processGeminiContainer(container) {
        const userQueryElement = container.querySelector("user-query .query-text") || container.querySelector(".query-text-line");
        const modelResponseContainer = container.querySelector("model-response") || container;
        const modelResponseElement = modelResponseContainer.querySelector("message-content .markdown-main-panel");

        const questionText = userQueryElement ? userQueryElement.innerText.trim() : "";

        let answerText = "";
        if (modelResponseElement) {
            answerText = htmlToMarkdown(modelResponseElement);
        }

        const userImageElements = container.querySelectorAll("user-query img");
        const modelImageElements = modelResponseContainer.querySelectorAll("model-response img");
        const userImagesPromises = Array.from(userImageElements).map(img => processImageElement(img));
        const modelImagesPromises = Array.from(modelImageElements).map(img => processImageElement(img));
        const userImages = (await Promise.all(userImagesPromises)).filter(Boolean);
        const modelImages = (await Promise.all(modelImagesPromises)).filter(Boolean);
        if (questionText || answerText || userImages.length > 0 || modelImages.length > 0) {
            return {
                human: { text: questionText, images: userImages },
                assistant: { text: answerText, images: modelImages }
            };
        }
        return null;
    }

    async function extractGeminiConversationData() {
        const conversationTurns = document.querySelectorAll("div.conversation-turn");
        let conversationData = [];
        if (conversationTurns.length > 0) {
            for (const turn of conversationTurns) {
                const data = await processGeminiContainer(turn);
                if (data) conversationData.push(data);
            }
        } else {
            const legacyContainers = document.querySelectorAll("div.single-turn, div.conversation-container");
            for (const container of legacyContainers) {
                const data = await processGeminiContainer(container);
                if (data) conversationData.push(data);
            }
        }
        return conversationData;
    }

    // ===== NotebookLM数据提取函数(从1.1恢复)=====
    function extractNotebookLMConversationData() {
        const conversationTurns = document.querySelectorAll("div.chat-message-pair");
        let conversationData = [];
        conversationTurns.forEach((turnContainer) => {
            let questionText = "";
            const userQueryEl = turnContainer.querySelector("chat-message .from-user-container .message-text-content");
            if (userQueryEl) {
                questionText = userQueryEl.innerText.trim();
                if (questionText.startsWith('[Preamble] ')) {
                    questionText = questionText.substring('[Preamble] '.length).trim();
                }
            }
            let answerText = "";
            const modelResponseContent = turnContainer.querySelector("chat-message .to-user-container .message-text-content");
            if (modelResponseContent) {
                const answerParts = [];
                const structuralElements = modelResponseContent.querySelectorAll('labs-tailwind-structural-element-view-v2');
                structuralElements.forEach(structEl => {
                    const bulletEl = structEl.querySelector('.bullet');
                    const paragraphEl = structEl.querySelector('.paragraph');
                    let lineText = '';
                    if (bulletEl) {
                        lineText += bulletEl.innerText.trim() + ' ';
                    }
                    if (paragraphEl) {
                        let paragraphText = '';
                        paragraphEl.childNodes.forEach(node => {
                            if (node.nodeType === Node.TEXT_NODE) {
                                paragraphText += node.textContent;
                            } else if (node.nodeType === Node.ELEMENT_NODE) {
                                if (node.querySelector && node.querySelector('button.citation-marker')) {
                                    return;
                                }
                                if (node.tagName === 'SPAN' && node.classList.contains('bold')) {
                                    paragraphText += `**${node.innerText}**`;
                                } else {
                                    paragraphText += node.innerText || node.textContent || '';
                                }
                            }
                        });
                        lineText += paragraphText;
                    }
                    if (lineText.trim()) {
                        answerParts.push(lineText.trim());
                    }
                });
                answerText = answerParts.join('\n\n');
            }
            if (questionText || answerText) {
                conversationData.push({
                    human: questionText,
                    assistant: answerText
                });
            }
        });
        return conversationData;
    }

    // ===== AI Studio相关函数(从1.1恢复)=====
    function getAIStudioScroller() {
        const selectors = [
            'ms-chat-session ms-autoscroll-container',
            'mat-sidenav-content',
            '.chat-view-container'
        ];
        for (const selector of selectors) {
            const el = document.querySelector(selector);
            if (el && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) {
                return el;
            }
        }
        return document.documentElement;
    }

    function extractDataIncremental_AiStudio() {
        const turns = document.querySelectorAll('ms-chat-turn');
        turns.forEach(turn => {
            if (collectedData.has(turn)) { return; }
            const isUserTurn = turn.querySelector('.chat-turn-container.user');
            const isModelTurn = turn.querySelector('.chat-turn-container.model');
            let turnData = { type: 'unknown', text: '' };
            if (isUserTurn) {
                const userPromptNode = isUserTurn.querySelector('.user-prompt-container .turn-content');
                if (userPromptNode) {
                    const userText = userPromptNode.innerText.trim();
                    if (userText) {
                        turnData.type = 'user';
                        turnData.text = userText;
                    }
                }
            } else if (isModelTurn) {
                const responseChunks = isModelTurn.querySelectorAll('ms-prompt-chunk');
                let responseTexts = [];
                responseChunks.forEach(chunk => {
                    if (!chunk.querySelector('ms-thought-chunk')) {
                        const cmarkNode = chunk.querySelector('ms-cmark-node');
                        if (cmarkNode) {
                            const markdownText = htmlToMarkdown(cmarkNode);
                            if (markdownText) {
                                responseTexts.push(markdownText);
                            }
                        }
                    }
                });
                const responseText = responseTexts.join('\n\n').trim();
                if (responseText) {
                    turnData.type = 'model';
                    turnData.text = responseText;
                }
            }
            if (turnData.type !== 'unknown') {
                collectedData.set(turn, turnData);
            }
        });
    }

    async function autoScrollAndCaptureAIStudio(onProgress) {
        collectedData.clear();
        const scroller = getAIStudioScroller();
        onProgress(i18n.t('scrollToTop'), false);
        scroller.scrollTop = 0;
        await sleep(SCROLL_TOP_WAIT_MS);
        let lastScrollTop = -1;
        onProgress(i18n.t('startScanning'), false);
        while (true) {
            extractDataIncremental_AiStudio();
            onProgress(`${i18n.t('scanning')} ${Math.round((scroller.scrollTop + scroller.clientHeight) / scroller.scrollHeight * 100)}% (${i18n.t('foundItems')} ${collectedData.size} ${i18n.t('items')})`, false);
            if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 10) {
                break;
            }
            lastScrollTop = scroller.scrollTop;
            scroller.scrollTop += scroller.clientHeight * 0.85;
            await sleep(SCROLL_DELAY_MS);
            if (scroller.scrollTop === lastScrollTop) {
                break;
            }
        }
        onProgress(i18n.t('scanComplete'), false);
        extractDataIncremental_AiStudio();
        await sleep(500);
        const finalTurnsInDom = document.querySelectorAll('ms-chat-turn');
        let sortedData = [];
        finalTurnsInDom.forEach(turnNode => {
            if (collectedData.has(turnNode)) {
                sortedData.push(collectedData.get(turnNode));
            }
        });
        const pairedData = [];
        let lastHuman = null;
        sortedData.forEach(item => {
            if (item.type === 'user') {
                lastHuman = item.text;
            } else if (item.type === 'model' && lastHuman) {
                pairedData.push({ human: lastHuman, assistant: item.text });
                lastHuman = null;
            } else if (item.type === 'model' && !lastHuman) {
                pairedData.push({ human: "[No preceding user prompt found]", assistant: item.text });
            }
        });
        if (lastHuman) {
            pairedData.push({ human: lastHuman, assistant: "[Model response is pending]" });
        }
        return pairedData;
    }

    // ===== 获取对话标题函数(从1.1恢复)=====
    function getConversationTitle() {
        let title = '';
        try {
            switch (currentPlatform) {
                case 'gemini':
                    const defaultTitle = `Gemini ${i18n.t('defaultChatTitle')} ${new Date().toISOString().slice(0,10)}`;
                    title = prompt(i18n.t('enterTitle'), defaultTitle);
                    if (title === null) {
                        return null;
                    }
                    return title.trim() || defaultTitle;
                case 'notebooklm':
                    const nblmTitleInput = document.querySelector('input.title-input.mat-title-large') ||
                                         document.querySelector('.title-input.mat-title-large.ng-pristine.ng-valid.ng-touched') ||
                                         document.querySelector('h1.notebook-title');
                    if (nblmTitleInput) {
                        title = nblmTitleInput.value || nblmTitleInput.innerText || nblmTitleInput.textContent;
                        title = title.trim();
                    }
                    return title || `${i18n.t('untitledChat')} NotebookLM`;
                case 'aistudio':
                    const studioTitleEl = document.querySelector('div.page-title h1');
                    title = studioTitleEl ? studioTitleEl.innerText.trim() : `${i18n.t('untitledChat')} AI Studio`;
                    return title;
                default:
                    return `${i18n.t('defaultChatTitle')} ${new Date().toISOString().slice(0,10)}`;
            }
        } catch (error) {
            debugLog('Error getting title:', error);
            return `${i18n.t('untitledChat')} ${new Date().toISOString().slice(0,10)}`;
        }
    }

    // ===== Claude相关函数 =====
    function getCurrentChatUUID() {
        const url = window.location.href;
        const match = url.match(/\/chat\/([a-zA-Z0-9-]+)/);
        return match ? match[1] : null;
    }

    function checkUrlForTreeMode() {
        return window.location.href.includes('?tree=True&rendering_mode=messages&render_all_tools=true') ||
               window.location.href.includes('&tree=True&rendering_mode=messages&render_all_tools=true');
    }

    async function getAllConversations() {
        const userId = await ensureUserId();
        if (!userId) {
            debugLog('Cannot get conversations without user ID');
            return null;
        }
        try {
            const baseUrl = window.location.hostname.includes('claude.ai') ? 'https://claude.ai' : 'https://pro.easychat.top';
            const apiUrl = `${baseUrl}/api/organizations/${userId}/chat_conversations`;
            debugLog('Fetching conversations from:', apiUrl);
            const response = await fetch(apiUrl);
            if (!response.ok) {
                throw new Error(`${i18n.t('fetchFailed')}: ${response.status}`);
            }
            return await response.json();
        } catch (error) {
            debugLog('Error fetching conversations:', error);
            return null;
        }
    }

    async function processImageAttachment(imageUrl, debugInfo = '') {
        try {
            if (!imageUrl.startsWith('http')) {
                const baseUrl = window.location.hostname.includes('claude.ai') ?
                    'https://claude.ai' : 'https://pro.easychat.top';
                imageUrl = baseUrl + imageUrl;
            }
            const response = await fetch(imageUrl);
            if (!response.ok) {
                throw new Error(`Failed to fetch image: ${response.status}`);
            }
            const blob = await response.blob();
            const base64 = await blobToBase64(blob);
            return {
                type: 'image',
                format: blob.type,
                size: blob.size,
                data: base64,
                original_url: imageUrl
            };
        } catch (error) {
            debugLog('Error processing image:', error);
            return null;
        }
    }

    async function getConversationDetailsWithImages(uuid, includeImagesParam) {
        const userId = await ensureUserId();
        if (!userId) {
            return null;
        }
        try {
            const baseUrl = window.location.hostname.includes('claude.ai') ?
                'https://claude.ai' : 'https://pro.easychat.top';
            const treeMode = document.getElementById(TREE_SWITCH_ID) ?
                document.getElementById(TREE_SWITCH_ID).checked : false;
            const apiUrl = `${baseUrl}/api/organizations/${userId}/chat_conversations/${uuid}${treeMode ? '?tree=True&rendering_mode=messages&render_all_tools=true' : ''}`;

            debugLog('Fetching conversation details from:', apiUrl);
            const response = await fetch(apiUrl);
            if (!response.ok) {
                throw new Error(`${i18n.t('fetchFailed')}: ${response.status}`);
            }
            const data = await response.json();

            if (!includeImagesParam) {
                return data;
            }

            let processedImageCount = 0;
            if (data.chat_messages && Array.isArray(data.chat_messages)) {
                for (let msgIndex = 0; msgIndex < data.chat_messages.length; msgIndex++) {
                    const message = data.chat_messages[msgIndex];
                    const fileArrays = ['files', 'files_v2', 'attachments'];
                    for (const key of fileArrays) {
                        if (message[key] && Array.isArray(message[key])) {
                            for (let i = 0; i < message[key].length; i++) {
                                const file = message[key][i];
                                let imageUrl = null;
                                let isImage = false;
                                if (file.file_kind === 'image' ||
                                    (file.file_type && file.file_type.startsWith('image/'))) {
                                    isImage = true;
                                    imageUrl = file.preview_url || file.thumbnail_url || file.file_url;
                                }
                                if (isImage && imageUrl && !file.embedded_image) {
                                    const imageData = await processImageAttachment(
                                        imageUrl,
                                        `消息${msgIndex + 1}-${key}-${i + 1}`
                                    );
                                    if (imageData) {
                                        message[key][i].embedded_image = imageData;
                                        processedImageCount++;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            data._debug_info = {
                images_processed: processedImageCount,
                processing_time: new Date().toISOString()
            };

            return data;
        } catch (error) {
            debugLog('Error fetching conversation details:', error);
            return null;
        }
    }

    async function exportCurrentConversationWithImages() {
        const uuid = getCurrentChatUUID();
        if (!uuid) {
            alert(i18n.t('uuidNotFound'));
            return;
        }

        const userId = await ensureUserId();
        if (!userId) {
            return;
        }

        try {
            const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID)?.checked || false;
            const data = await getConversationDetailsWithImages(uuid, shouldIncludeImages);
            if (!data) {
                throw new Error(i18n.t('fetchFailed'));
            }
            const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `claude_${uuid.substring(0, 8)}_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        } catch (error) {
            alert(`${i18n.t('exportFailed')} ${error.message}`);
        }
    }

    async function openLyraExporterWithData(jsonData, filename) {
        try {
            const exporterWindow = window.open(LYRA_EXPORTER_URL, '_blank');
            if (!exporterWindow) {
                alert(i18n.t('cannotOpenExporter'));
                return false;
            }
            const checkInterval = setInterval(() => {
                try {
                    exporterWindow.postMessage({
                        type: 'LYRA_HANDSHAKE',
                        source: 'lyra-fetch-script'
                    }, LYRA_EXPORTER_ORIGIN);
                } catch (e) {
                    debugLog('Error sending handshake:', e);
                }
            }, 1000);

            const handleMessage = (event) => {
                if (event.origin !== LYRA_EXPORTER_ORIGIN) {
                    return;
                }
                if (event.data && event.data.type === 'LYRA_READY') {
                    clearInterval(checkInterval);
                    const dataToSend = {
                        type: 'LYRA_LOAD_DATA',
                        source: 'lyra-fetch-script',
                        data: {
                            content: jsonData,
                            filename: filename || `${currentPlatform}_export_${new Date().toISOString().slice(0,10)}.json`
                        }
                    };
                    exporterWindow.postMessage(dataToSend, LYRA_EXPORTER_ORIGIN);
                    window.removeEventListener('message', handleMessage);
                }
            };

            window.addEventListener('message', handleMessage);
            setTimeout(() => {
                clearInterval(checkInterval);
                window.removeEventListener('message', handleMessage);
                debugLog('Connection timeout');
            }, 45000);
            return true;
        } catch (error) {
            alert(`${i18n.t('cannotOpenExporter')}: ${error.message}`);
            return false;
        }
    }

    // ===== 创建浮动面板 =====
    function createFloatingPanel() {
        if (document.getElementById(CONTROL_ID) || panelInjected) {
            return false;
        }

        let savedTreeMode = false;
        let savedImageMode = includeImages;
        const existingTreeSwitch = document.getElementById(TREE_SWITCH_ID);
        const existingImageSwitch = document.getElementById(IMAGE_SWITCH_ID);
        if (existingTreeSwitch) {
            savedTreeMode = existingTreeSwitch.checked;
        }
        if (existingImageSwitch) {
            savedImageMode = existingImageSwitch.checked;
        }

        const container = document.createElement('div');
        container.id = CONTROL_ID;
        if (isPanelCollapsed) container.classList.add('collapsed');

        const toggleButton = document.createElement('div');
        toggleButton.id = TOGGLE_ID;
        safeSetInnerHTML(toggleButton, isPanelCollapsed ?
            '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>' :
            '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>');
        toggleButton.addEventListener('click', toggleCollapsed);
        container.appendChild(toggleButton);

        const controlsArea = document.createElement('div');
        controlsArea.className = 'lyra-main-controls';

        const title = document.createElement('div');
        title.className = 'lyra-title';
        switch (currentPlatform) {
            case 'claude': title.textContent = i18n.t('claudeTitle'); break;
            case 'gemini': title.textContent = i18n.t('geminiTitle'); break;
            case 'notebooklm': title.textContent = i18n.t('notebooklmTitle'); break;
            case 'aistudio': title.textContent = i18n.t('aistudioTitle'); break;
            default: title.textContent = i18n.t('defaultTitle');
        }
        controlsArea.appendChild(title);

        // Claude专用功能
        if (currentPlatform === 'claude') {
            const statusDiv = document.createElement('div');
            statusDiv.className = 'lyra-status';
            if (capturedUserId) {
                statusDiv.classList.add('success');
                statusDiv.textContent = i18n.t('detectedUserId') + capturedUserId.substring(0, 8) + '...';
            } else {
                statusDiv.classList.add('error');
                statusDiv.textContent = i18n.t('autoDetecting');
            }
            controlsArea.appendChild(statusDiv);

            const manualIdBtn = document.createElement('button');
            manualIdBtn.id = MANUAL_ID_BTN;
            manualIdBtn.className = 'lyra-button lyra-manual-btn';
            manualIdBtn.textContent = i18n.t('manualUserId');
            manualIdBtn.addEventListener('click', promptForUserId);
            controlsArea.appendChild(manualIdBtn);

            const toggleContainer = document.createElement('div');
            toggleContainer.className = 'lyra-toggle';
            const treeModeChecked = savedTreeMode || checkUrlForTreeMode();
            safeSetInnerHTML(toggleContainer, `
                <span>${i18n.t('branchMode')}</span>
                <label class="lyra-switch">
                    <input type="checkbox" id="${TREE_SWITCH_ID}" ${treeModeChecked ? 'checked' : ''}>
                    <span class="lyra-slider"></span>
                </label>
            `);
            controlsArea.appendChild(toggleContainer);

            const imageToggleContainer = document.createElement('div');
            imageToggleContainer.className = 'lyra-toggle';
            safeSetInnerHTML(imageToggleContainer, `
                <span>${i18n.t('includeImages')}</span>
                <label class="lyra-switch">
                    <input type="checkbox" id="${IMAGE_SWITCH_ID}" ${savedImageMode ? 'checked' : ''}>
                    <span class="lyra-slider"></span>
                </label>
            `);
            controlsArea.appendChild(imageToggleContainer);

            document.addEventListener('change', function(e) {
                if (e.target.id === IMAGE_SWITCH_ID) {
                    includeImages = e.target.checked;
                    localStorage.setItem('lyraIncludeImages', includeImages);
                }
            });

            const uuidButton = document.createElement('button');
            uuidButton.className = 'lyra-button';
            safeSetInnerHTML(uuidButton, `
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <circle cx="11" cy="11" r="8"></circle>
                    <path d="m21 21-4.35-4.35"></path>
                </svg>
                ${i18n.t('viewOnline')}
            `);
            uuidButton.addEventListener('click', async () => {
                const uuid = getCurrentChatUUID();
                if (!uuid) {
                    alert(i18n.t('uuidNotFound'));
                    return;
                }

                const userId = await ensureUserId();
                if (!userId) {
                    return;
                }

                const originalContent = uuidButton.innerHTML;
                safeSetInnerHTML(uuidButton, `<div class="lyra-loading"></div><span>${i18n.t('loading')}</span>`);
                uuidButton.disabled = true;

                try {
                    const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID)?.checked || false;
                    const data = await getConversationDetailsWithImages(uuid, shouldIncludeImages);
                    if (!data) {
                        throw new Error(i18n.t('fetchFailed'));
                    }
                    const jsonString = JSON.stringify(data, null, 2);
                    const filename = `claude_${uuid.substring(0, 8)}_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`;
                    await openLyraExporterWithData(jsonString, filename);
                } catch (error) {
                    alert(`${i18n.t('loadFailed')} ${error.message}`);
                } finally {
                    safeSetInnerHTML(uuidButton, originalContent);
                    uuidButton.disabled = false;
                }
            });
            controlsArea.appendChild(uuidButton);

            const exportButton = document.createElement('button');
            exportButton.className = 'lyra-button';
            safeSetInnerHTML(exportButton, `
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
                ${i18n.t('exportCurrentJSON')}
            `);
            exportButton.addEventListener('click', exportCurrentConversationWithImages);
            controlsArea.appendChild(exportButton);

            const exportAllButton = document.createElement('button');
            exportAllButton.className = 'lyra-button';
            safeSetInnerHTML(exportAllButton, `
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
                    <path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>
                </svg>
                ${i18n.t('exportAllConversations')}
            `);
            exportAllButton.addEventListener('click', async function(event) {
                const userId = await ensureUserId();
                if (!userId) {
                    return;
                }

                const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID)?.checked || false;
                const progressElem = document.createElement('div');
                progressElem.className = 'lyra-progress';
                progressElem.textContent = i18n.t('preparing');
                controlsArea.appendChild(progressElem);

                const originalContent = this.innerHTML;
                safeSetInnerHTML(this, `<div class="lyra-loading"></div><span>${i18n.t('exporting')}</span>`);
                this.disabled = true;

                try {
                    const allConversations = await getAllConversations();
                    if (!allConversations || !Array.isArray(allConversations)) {
                        throw new Error(i18n.t('fetchFailed'));
                    }

                    const result = {
                        exportedAt: new Date().toISOString(),
                        totalConversations: allConversations.length,
                        conversations: []
                    };

                    for (let i = 0; i < allConversations.length; i++) {
                        const conversation = allConversations[i];
                        progressElem.textContent = `${i18n.t('gettingConversation')} ${i + 1}/${allConversations.length}${shouldIncludeImages ? i18n.t('withImages') : ''}`;
                        if (i > 0) await sleep(500);
                        const details = await getConversationDetailsWithImages(conversation.uuid, shouldIncludeImages);
                        if (details) result.conversations.push(details);
                    }

                    const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = `claude_all_conversations_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(url);
                    alert(`${i18n.t('successExported')} ${result.conversations.length} ${i18n.t('conversations')}`);
                } catch (error) {
                    alert(`${i18n.t('exportFailed')} ${error.message}`);
                } finally {
                    safeSetInnerHTML(this, originalContent);
                    this.disabled = false;
                    if (progressElem.parentNode) progressElem.parentNode.removeChild(progressElem);
                }
            });
            controlsArea.appendChild(exportAllButton);
        } else {
            // 非Claude平台:Gemini, AI Studio, NotebookLM
            // 先添加预览按钮(除NotebookLM外)
            if (currentPlatform !== 'notebooklm') {
                const onlineViewButton = document.createElement('button');
                onlineViewButton.className = 'lyra-button';
                safeSetInnerHTML(onlineViewButton, `
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <circle cx="11" cy="11" r="8"></circle>
                        <path d="m21 21-4.35-4.35"></path>
                    </svg>
                    ${i18n.t('viewOnline')}
                `);
                onlineViewButton.addEventListener('click', async function() {
                    const title = getConversationTitle();
                    if (title === null) {
                        debugLog(i18n.t('operationCancelled'));
                        return;
                    }
                    this.disabled = true;
                    const originalContent = this.innerHTML;
                    safeSetInnerHTML(this, `<div class="lyra-loading"></div><span>${i18n.t('loading')}</span>`);
                    let progressElem = null;
                    if (currentPlatform === 'aistudio') {
                        progressElem = document.createElement('div');
                        progressElem.className = 'lyra-progress';
                        controlsArea.appendChild(progressElem);
                    }
                    try {
                        let conversationData = [];
                        if (currentPlatform === 'aistudio') {
                            conversationData = await autoScrollAndCaptureAIStudio((message) => {
                                if (progressElem) progressElem.textContent = message;
                            });
                        } else if (currentPlatform === 'gemini') {
                            conversationData = await extractGeminiConversationData();
                        }
                        if (conversationData.length > 0) {
                            const finalJson = {
                                title: title,
                                platform: currentPlatform,
                                exportedAt: new Date().toISOString(),
                                conversation: conversationData
                            };
                            const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19);
                            const sanitizedTitle = title.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_');
                            const filename = `${currentPlatform}_${sanitizedTitle}_${timestamp}.json`;
                            const jsonString = JSON.stringify(finalJson, null, 2);
                            await openLyraExporterWithData(jsonString, filename);
                        } else {
                            alert(i18n.t('noContent'));
                        }
                    } catch (error) {
                        alert(`${i18n.t('loadFailed')} ${error.message}`);
                    } finally {
                        this.disabled = false;
                        safeSetInnerHTML(this, originalContent);
                        if (progressElem && progressElem.parentNode) {
                            progressElem.parentNode.removeChild(progressElem);
                        }
                    }
                });
                controlsArea.appendChild(onlineViewButton);
            }

            // 然后添加保存按钮
            const exportButton = document.createElement('button');
            exportButton.className = 'lyra-button';
            safeSetInnerHTML(exportButton, `
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
                ${i18n.t('saveConversation')}
            `);
            exportButton.addEventListener('click', async function() {
                const title = getConversationTitle();
                if (title === null) {
                    debugLog(i18n.t('exportCancelled'));
                    return;
                }
                this.disabled = true;
                const originalContent = this.innerHTML;
                safeSetInnerHTML(this, `<div class="lyra-loading"></div><span>${i18n.t('exporting')}</span>`);
                let progressElem = null;
                if (currentPlatform === 'aistudio') {
                    progressElem = document.createElement('div');
                    progressElem.className = 'lyra-progress';
                    controlsArea.appendChild(progressElem);
                }
                try {
                    let conversationData = [];
                    if (currentPlatform === 'aistudio') {
                        conversationData = await autoScrollAndCaptureAIStudio((message) => {
                            if (progressElem) progressElem.textContent = message;
                        });
                    } else if (currentPlatform === 'gemini') {
                        conversationData = await extractGeminiConversationData();
                    } else if (currentPlatform === 'notebooklm') {
                        conversationData = extractNotebookLMConversationData();
                    }
                    if (conversationData.length > 0) {
                        const finalJson = {
                            title: title,
                            platform: currentPlatform,
                            exportedAt: new Date().toISOString(),
                            conversation: conversationData
                        };
                        const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19);
                        const sanitizedTitle = title.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_');
                        const filename = `${currentPlatform}_${sanitizedTitle}_${timestamp}.json`;
                        const jsonData = JSON.stringify(finalJson, null, 2);
                        const blob = new Blob([jsonData], { type: 'application/json;charset=utf-8' });
                        const link = document.createElement("a");
                        link.href = URL.createObjectURL(blob);
                        link.download = filename;
                        document.body.appendChild(link);
                        link.click();
                        document.body.removeChild(link);
                        URL.revokeObjectURL(link.href);
                        debugLog(i18n.t('exportSuccess'));
                    } else {
                        alert(i18n.t('noContent'));
                    }
                } catch (error) {
                    alert(`${i18n.t('exportFailed')} ${error.message}`);
                } finally {
                    this.disabled = false;
                    safeSetInnerHTML(this, originalContent);
                    if (progressElem && progressElem.parentNode) {
                        progressElem.parentNode.removeChild(progressElem);
                    }
                }
            });
            controlsArea.appendChild(exportButton);
        }

        // 语言切换按钮
        const langToggle = document.createElement('div');
        langToggle.className = 'lyra-lang-toggle';
        langToggle.id = LANG_SWITCH_ID;
        langToggle.textContent = `🌐 ${i18n.languages[i18n.getLanguage()].name}`;
        langToggle.addEventListener('click', function() {
            const currentLang = i18n.getLanguage();
            const newLang = currentLang === 'zh' ? 'en' : 'zh';
            i18n.setLanguage(newLang);
            const existingPanel = document.getElementById(CONTROL_ID);
            if (existingPanel) {
                existingPanel.remove();
                panelInjected = false;
                createFloatingPanel();
            }
        });
        controlsArea.appendChild(langToggle);

        container.appendChild(controlsArea);
        document.body.appendChild(container);
        panelInjected = true;

        // 定期更新状态
        if (currentPlatform === 'claude') {
            setInterval(() => {
                const statusDiv = container.querySelector('.lyra-status');
                if (statusDiv) {
                    if (capturedUserId) {
                        statusDiv.classList.remove('error');
                        statusDiv.classList.add('success');
                        statusDiv.textContent = i18n.t('detectedUserId') + capturedUserId.substring(0, 8) + '...';
                    } else {
                        statusDiv.classList.remove('success');
                        statusDiv.classList.add('error');
                        statusDiv.textContent = i18n.t('autoDetecting');
                    }
                }
            }, 2000);
        }

        return true;
    }

    // ===== 脚本初始化 =====
    function initScript() {
        if (!currentPlatform) {
            return;
        }

        injectCustomStyle();

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                setTimeout(() => initializePanel(), 2000);
            });
        } else {
            setTimeout(() => initializePanel(), 2000);
        }
    }

    function initializePanel() {
        if (currentPlatform === 'claude') {
            if (/\/chat\/[a-zA-Z0-9-]+/.test(window.location.href)) {
                createFloatingPanel();
            }

            let lastUrl = window.location.href;
            const observer = new MutationObserver(() => {
                if (window.location.href !== lastUrl) {
                    lastUrl = window.location.href;
                    setTimeout(() => {
                        if (/\/chat\/[a-zA-Z0-9-]+/.test(lastUrl) && !document.getElementById(CONTROL_ID)) {
                            createFloatingPanel();
                        }
                    }, 1000);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        } else {
            createFloatingPanel();
        }
    }

    initScript();

})();

QingJ © 2025

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