Greasy Fork

Greasy Fork is available in English.

Lyra's Fetch

Claude、Gemini、NotebookLM、Google AI Studio对话管理工具的配套脚本:一键获取对话UUID和完整JSON数据,支持树形分支模式导出。轻松管理数百个对话窗口,导出完整时间线、思考过程、工具调用和Artifacts。配合Lyra's Exporter使用,让每次AI对话都成为您的数字资产!

当前为 2025-10-30 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Lyra's Fetch
// @namespace    userscript://lyra-universal-ai-exporter
// @version      2.0
// @description  Claude、Gemini、NotebookLM、Google AI Studio对话管理工具的配套脚本:一键获取对话UUID和完整JSON数据,支持树形分支模式导出。轻松管理数百个对话窗口,导出完整时间线、思考过程、工具调用和Artifacts。配合Lyra's Exporter使用,让每次AI对话都成为您的数字资产!
// @description:en Claude, Gemini, NotebookLM, Google AI Studio conversation management companion script: One-click UUID extraction and complete JSON export with tree-branch mode support. Designed for power users to easily manage hundreds of conversation windows, export complete timelines, thinking processes, tool calls and Artifacts. Works with Lyra's Exporter management app to turn every AI chat into your digital asset!
// @homepage     https://yalums.github.io/lyra-exporter/
// @supportURL   https://github.com/Yalums/lyra-exporter/issues
// @author       Yalums
// @match        https://claude.easychat.top/*
// @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        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @license      GNU General Public License v3.0
// ==/UserScript==

    /* global JSZip */

    (function() {
        'use strict';
        if (window.lyraFetchInitialized) return;
        window.lyraFetchInitialized = true;

        // ===== 配置 =====
        const Config = {
            CONTROL_ID: 'lyra-controls',
            TOGGLE_ID: 'lyra-toggle-button',
            LANG_SWITCH_ID: 'lyra-lang-switch',
            TREE_SWITCH_ID: 'lyra-tree-mode-switch',
            IMAGE_SWITCH_ID: 'lyra-image-switch',
            MANUAL_ID_BTN: 'lyra-manual-id-btn',
            // 使用 old.js 的 URL 和 Origin 以支持 postMessage 预览功能
            EXPORTER_URL: 'https://yalums.github.io/lyra-exporter/',
            EXPORTER_ORIGIN: 'https://yalums.github.io'
        };

        // ===== 状态管理 =====
        const State = {
            currentPlatform: (() => {
                const host = window.location.hostname;
                if (host.includes('claude') || host.includes('easychat.top')) return 'claude';
                if (host.includes('gemini')) return 'gemini';
                if (host.includes('notebooklm')) return 'notebooklm';
                if (host.includes('aistudio')) return 'aistudio';
                return null;
            })(),
            isPanelCollapsed: localStorage.getItem('lyraExporterCollapsed') === 'true',
            includeImages: localStorage.getItem('lyraIncludeImages') === 'true',
            capturedUserId: localStorage.getItem('lyraClaudeUserId') || '',
            panelInjected: false
        };

        let collectedData = new Map();
        // ai studio的滚动常量
        const SCROLL_DELAY_MS = 250;
        const SCROLL_TOP_WAIT_MS = 1000;

        // ===== i18n 国际化 (REVERTED to simple v8.1 strings) =====
        const i18n = {
            languages: {
                zh: {
                    loading: '加载中...', exporting: '导出中...', compressing: '压缩中...', preparing: '准备中...',
                    exportSuccess: '导出成功!', noContent: '没有可导出的对话内容。',
                    exportCurrentJSON: '导出当前', exportAllConversations: '导出全部',
                    branchMode: '多分支', includeImages: '含图像',
                    enterFilename: '请输入文件名(不含扩展名):', untitledChat: '未命名对话',
                    uuidNotFound: '未找到对话UUID!', fetchFailed: '获取对话数据失败',
                    exportFailed: '导出失败: ', gettingConversation: '获取对话',
                    withImages: ' (处理图片中...)', successExported: '成功导出', conversations: '个对话!',
                    manualUserId: '手动设置ID', enterUserId: '请输入您的组织ID (settings/account):',
                    userIdSaved: '用户ID已保存!',
                    // 从 old.js 添加 (保留预览和加载失败)
                    viewOnline: '预览对话',
                    loadFailed: '加载失败: ',
                    cannotOpenExporter: '无法打开 Lyra Exporter,请检查弹窗拦截',
                },
                en: {
                    loading: 'Loading...', exporting: 'Exporting...', compressing: 'Compressing...', preparing: 'Preparing...',
                    exportSuccess: 'Export successful!', noContent: 'No conversation content to export.',
                    exportCurrentJSON: 'Export', exportAllConversations: 'Save All',
                    branchMode: 'Branch', includeImages: 'Images',
                    enterFilename: 'Enter filename (without extension):', untitledChat: 'Untitled Chat',
                    uuidNotFound: 'UUID not found!', fetchFailed: 'Failed to fetch conversation data',
                    exportFailed: 'Export failed: ', gettingConversation: 'Getting conversation',
                    withImages: ' (processing images...)', successExported: 'Successfully exported', conversations: 'conversations!',
                    manualUserId: 'Customize UUID', enterUserId: 'Organization ID (settings/account)',
                    userIdSaved: 'User ID saved!',
                    // 从 old.js 添加 (保留预览和加载失败)
                    viewOnline: 'Preview',
                    loadFailed: 'Load failed: ',
                    cannotOpenExporter: 'Cannot open Lyra Exporter, please check popup blocker',
                }
            },
            currentLang: localStorage.getItem('lyraExporterLanguage') || (navigator.language.startsWith('zh') ? 'zh' : 'en'),
            // Reverted to simple t() function
            t: (key) => i18n.languages[i18n.currentLang]?.[key] || key,
            setLanguage: (lang) => {
                i18n.currentLang = lang;
                localStorage.setItem('lyraExporterLanguage', lang);
            },
            // Kept from old.js for UI
            getLanguageShort() {
                return this.currentLang === 'zh' ? '简体中文' : 'English';
            }
        };

        const previewIcon = '<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="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
        const collapseIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"></polyline></svg>';
        const expandIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg>';
        const exportIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>';
        const zipIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 11V9a7 7 0 0 0-7-7a7 7 0 0 0-7 7v2"></path><rect x="3" y="11" width="18" height="10" rx="2" ry="2"></rect></svg>';


        // ===== 工具函数 =====
        const Utils = {
            sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),

            sanitizeFilename: (name) => name.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_').substring(0, 100),

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

            downloadJSON: (jsonString, filename) => {
                const blob = new Blob([jsonString], { type: 'application/json' });
                Utils.downloadFile(blob, filename);
            },

            downloadFile: (blob, filename) => {
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = filename;
                a.click();
                URL.revokeObjectURL(url);
            },

            setButtonLoading: (btn, text) => {
                btn.disabled = true;
                btn.innerHTML = `<div class="lyra-loading"></div> <span>${text}</span>`;
            },

            restoreButton: (btn, originalContent) => {
                btn.disabled = false;
                btn.innerHTML = originalContent;
            },

            createButton: (innerHTML, onClick) => {
                const btn = document.createElement('button');
                btn.className = 'lyra-button';
                btn.innerHTML = innerHTML;
                btn.addEventListener('click', () => onClick(btn));
                return btn;
            },

            createToggle: (label, id, checked = false) => {
                const container = document.createElement('div');
                container.className = 'lyra-toggle';
                container.innerHTML = `
                    <span>${label}</span>
                    <label class="lyra-switch">
                        <input type="checkbox" id="${id}" ${checked ? 'checked' : ''}>
                        <span class="lyra-slider"></span>
                    </label>
                `;
                return container;
            },

            createProgressElem: (parent) => {
                const elem = document.createElement('div');
                elem.className = 'lyra-progress';
                parent.appendChild(elem);
                return elem;
            }
        };

        // ===== Lyra Communicator (移植自 old.js 的 openLyraExporterWithData) =====
        const LyraCommunicator = {
            open: async (jsonData, filename) => {
                try {
                    const exporterWindow = window.open(Config.EXPORTER_URL, '_blank');
                    if (!exporterWindow) {
                        alert(i18n.t('cannotOpenExporter'));
                        return false;
                    }

                    const checkInterval = setInterval(() => {
                        try {
                            exporterWindow.postMessage({
                                type: 'LYRA_HANDSHAKE',
                                source: 'lyra-fetch-script'
                            }, Config.EXPORTER_ORIGIN);
                        } catch (e) {
                            // Error sending handshake
                        }
                    }, 1000);

                    const handleMessage = (event) => {
                        if (event.origin !== Config.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 || `${State.currentPlatform}_export_${new Date().toISOString().slice(0,10)}.json`
                                }
                            };
                            exporterWindow.postMessage(dataToSend, Config.EXPORTER_ORIGIN);
                            window.removeEventListener('message', handleMessage);
                        }
                    };

                    window.addEventListener('message', handleMessage);

                    setTimeout(() => {
                        clearInterval(checkInterval);
                        window.removeEventListener('message', handleMessage);
                    }, 45000); // 45s 超时

                    return true;
                } catch (error) {
                    alert(`${i18n.t('cannotOpenExporter')}: ${error.message}`);
                    return false;
                }
            }
        };

        // ===== 平台处理器:Claude =====
        const ClaudeHandler = {
        init: () => {
            // 拦截请求以捕获用户ID
            const script = document.createElement('script');
            script.textContent = `
                (function() {
                    function captureUserId(url) {
                        const match = url.match(/\\/api\\/organizations\\/([a-f0-9-]+)\\//);
                        if (match && match[1]) {
                            localStorage.setItem('lyraClaudeUserId', match[1]);
                            window.dispatchEvent(new CustomEvent('lyraUserIdCaptured', { detail: { userId: match[1] } }));
                        }
                    }
                    const originalXHROpen = XMLHttpRequest.prototype.open;
                    XMLHttpRequest.prototype.open = function() {
                        if (arguments[1]) captureUserId(arguments[1]);
                        return originalXHROpen.apply(this, arguments);
                    };
                    const originalFetch = window.fetch;
                    window.fetch = function(resource) {
                        const url = typeof resource === 'string' ? resource : (resource.url || '');
                        if (url) captureUserId(url);
                        return originalFetch.apply(this, arguments);
                    };
                })();
            `;
            (document.head || document.documentElement).appendChild(script);
            script.remove();
            window.addEventListener('lyraUserIdCaptured', (e) => {
                if (e.detail.userId) State.capturedUserId = e.detail.userId;
            });
            // ✅ 移除了定时更新 status UI 的代码,因为已经不显示 UUID 了
        },
        addUI: (controlsArea) => {
            // ✅ 只添加两个toggle,手动输入按钮移到 createPanel 中
            
            // ✅ 保留:分支模式开关
            const treeMode = window.location.search.includes('tree=true');
            controlsArea.appendChild(Utils.createToggle(i18n.t('branchMode'), Config.TREE_SWITCH_ID, treeMode));
            
            // ✅ 保留:图片开关
            controlsArea.appendChild(Utils.createToggle(i18n.t('includeImages'), Config.IMAGE_SWITCH_ID, State.includeImages));
            document.addEventListener('change', (e) => {
                if (e.target.id === Config.IMAGE_SWITCH_ID) {
                    State.includeImages = e.target.checked;
                    localStorage.setItem('lyraIncludeImages', State.includeImages);
                }
            });
        },
        addButtons: (controlsArea) => {
            controlsArea.appendChild(Utils.createButton(
                `${previewIcon} ${i18n.t('viewOnline')}`,
                async (btn) => {
                    const uuid = ClaudeHandler.getCurrentUUID();
                    if (!uuid) { alert(i18n.t('uuidNotFound')); return; }
                    if (!await ClaudeHandler.ensureUserId()) return;
                    const original = btn.innerHTML;
                    Utils.setButtonLoading(btn, i18n.t('loading'));
                    try {
                        const includeImages = document.getElementById(Config.IMAGE_SWITCH_ID)?.checked || false;
                        const data = await ClaudeHandler.getConversation(uuid, includeImages);
                        if (!data) throw new Error(i18n.t('fetchFailed'));
                        const jsonString = JSON.stringify(data, null, 2);
                        const filename = `claude_${data.name || 'conversation'}_${uuid.substring(0, 8)}.json`;
                        await LyraCommunicator.open(jsonString, filename);
                    } catch (error) {
                        alert(`${i18n.t('loadFailed')} ${error.message}`);
                    } finally {
                        Utils.restoreButton(btn, original);
                    }
                }
            ));
            controlsArea.appendChild(Utils.createButton(
                `${exportIcon} ${i18n.t('exportCurrentJSON')}`,
                async (btn) => {
                    const uuid = ClaudeHandler.getCurrentUUID();
                    if (!uuid) { alert(i18n.t('uuidNotFound')); return; }
                    if (!await ClaudeHandler.ensureUserId()) return;
                    const filename = prompt(i18n.t('enterFilename'), Utils.sanitizeFilename(`claude_${uuid.substring(0, 8)}`));
                    if (!filename?.trim()) return;
                    const original = btn.innerHTML;
                    Utils.setButtonLoading(btn, i18n.t('exporting'));
                    try {
                        const includeImages = document.getElementById(Config.IMAGE_SWITCH_ID)?.checked || false;
                        const data = await ClaudeHandler.getConversation(uuid, includeImages);
                        if (!data) throw new Error(i18n.t('fetchFailed'));
                        Utils.downloadJSON(JSON.stringify(data, null, 2), `${filename.trim()}.json`);
                    } catch (error) {
                        alert(`${i18n.t('exportFailed')} ${error.message}`);
                    } finally {
                        Utils.restoreButton(btn, original);
                    }
                }
            ));
            controlsArea.appendChild(Utils.createButton(
                `${zipIcon} ${i18n.t('exportAllConversations')}`,
                (btn) => ClaudeHandler.exportAll(btn, controlsArea)
            ));
        },
        getCurrentUUID: () => window.location.pathname.match(/\/chat\/([a-zA-Z0-9-]+)/)?.[1],
        ensureUserId: async () => {
            if (State.capturedUserId) return State.capturedUserId;
            const saved = localStorage.getItem('lyraClaudeUserId');
            if (saved) {
                State.capturedUserId = saved;
                return saved;
            }
            alert('未能检测到用户ID / User ID not detected');
            return null;
        },
        getBaseUrl: () => {
            if (window.location.hostname.includes('claude.ai')) {
                return 'https://claude.ai';
            } else if (window.location.hostname.includes('easychat.top')) {
                return `https://${window.location.hostname}`;
            }
            return window.location.origin;
        },
        getAllConversations: async () => {
            const userId = await ClaudeHandler.ensureUserId();
            if (!userId) return null;
            try {
                const response = await fetch(`${ClaudeHandler.getBaseUrl()}/api/organizations/${userId}/chat_conversations`);
                if (!response.ok) throw new Error('Fetch failed');
                return await response.json();
            } catch (error) {
                console.error('Get all conversations error:', error);
                return null;
            }
        },
        getConversation: async (uuid, includeImages = false) => {
            const userId = await ClaudeHandler.ensureUserId();
            if (!userId) return null;
            try {
                const treeMode = document.getElementById(Config.TREE_SWITCH_ID)?.checked || false;
                const endpoint = treeMode ?
                    `/api/organizations/${userId}/chat_conversations/${uuid}?tree=True&rendering_mode=messages&render_all_tools=true` :
                    `/api/organizations/${userId}/chat_conversations/${uuid}`;
                const apiUrl = `${ClaudeHandler.getBaseUrl()}${endpoint}`;
                const response = await fetch(apiUrl);
                if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
                const data = await response.json();
                if (includeImages && data.chat_messages) {
                    for (const msg of data.chat_messages) {
                        const fileArrays = ['files', 'files_v2', 'attachments'];
                        for (const key of fileArrays) {
                            if (Array.isArray(msg[key])) {
                                for (const file of msg[key]) {
                                    const isImage = file.file_kind === 'image' || file.file_type?.startsWith('image/');
                                    const imageUrl = file.preview_url || file.thumbnail_url || file.file_url;
                                    if (isImage && imageUrl && !file.embedded_image) {
                                        try {
                                            const fullUrl = imageUrl.startsWith('http') ? imageUrl : ClaudeHandler.getBaseUrl() + imageUrl;
                                            const imgResp = await fetch(fullUrl);
                                            if (imgResp.ok) {
                                                const blob = await imgResp.blob();
                                                const base64 = await Utils.blobToBase64(blob);
                                                file.embedded_image = { type: 'image', format: blob.type, size: blob.size, data: base64, original_url: imageUrl };
                                            }
                                        } catch (err) {
                                            console.error('Process image error:', err);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                return data;
            } catch (error) {
                console.error('Get conversation error:', error);
                return null;
            }
        },
        exportAll: async (btn, controlsArea) => {
            if (typeof JSZip === 'undefined') {
                alert('Error: JSZip library not loaded.');
                return;
            }
            if (!await ClaudeHandler.ensureUserId()) return;
            const progress = Utils.createProgressElem(controlsArea);
            progress.textContent = i18n.t('preparing');
            const original = btn.innerHTML;
            Utils.setButtonLoading(btn, i18n.t('exporting'));
            try {
                const allConvs = await ClaudeHandler.getAllConversations();
                if (!allConvs || !Array.isArray(allConvs)) throw new Error(i18n.t('fetchFailed'));
                const zip = new JSZip();
                const includeImages = document.getElementById(Config.IMAGE_SWITCH_ID)?.checked || false;
                let exported = 0;
                console.log(`Starting export of ${allConvs.length} conversations`);
                for (let i = 0; i < allConvs.length; i++) {
                    const conv = allConvs[i];
                    progress.textContent = `${i18n.t('gettingConversation')} ${i + 1}/${allConvs.length}${includeImages ? i18n.t('withImages') : ''}`;
                    if (i > 0 && i % 5 === 0) {
                        await new Promise(resolve => setTimeout(resolve, 0));
                    } else if (i > 0) {
                        await Utils.sleep(300);
                    }
                    try {
                        const data = await ClaudeHandler.getConversation(conv.uuid, includeImages);
                        if (data) {
                            const title = Utils.sanitizeFilename(data.name || conv.uuid);
                            const filename = `claude_${conv.uuid.substring(0, 8)}_${title}.json`;
                            zip.file(filename, JSON.stringify(data, null, 2));
                            exported++;
                        }
                    } catch (error) {
                        console.error(`Failed to process ${conv.uuid}:`, error);
                    }
                }
                console.log(`Export complete: ${exported} files. Compressing...`);
                progress.textContent = `${i18n.t('compressing')} 0%`;
                const zipBlob = await zip.generateAsync({
                    type: "blob",
                    compression: "DEFLATE",
                    compressionOptions: { level: 1 }
                }, (metadata) => {
                    const percent = metadata.percent.toFixed(0);
                    progress.textContent = `${i18n.t('compressing')} ${percent}%`;
                });
                const zipFilename = `claude_export_all_${new Date().toISOString().slice(0, 10)}.zip`;
                Utils.downloadFile(zipBlob, zipFilename);
                alert(`${i18n.t('successExported')} ${exported} ${i18n.t('conversations')}`);
            } catch (error) {
                console.error('Export all error:', error);
                alert(`${i18n.t('exportFailed')} ${error.message}`);
            } finally {
                Utils.restoreButton(btn, original);
                if (progress.parentNode) progress.parentNode.removeChild(progress);
            }
        }
    };

        // ===== 平台处理器:Gemini/NotebookLM/AIStudio =====

        // ===== 图片抓取辅助函数 (移植自 old.js) =====
        function fetchViaGM(url) {
            return new Promise((resolve, reject) => {
                // 检查 GM_xmlhttpRequest 是否可用
                if (typeof GM_xmlhttpRequest === 'undefined') {
                    console.error('GM_xmlhttpRequest is not defined. Make sure @grant GM_xmlhttpRequest is in the script header.');
                    // 备用方案:尝试使用 fetch (可能会因CORS失败)
                    fetch(url).then(response => {
                        if (response.ok) return response.blob();
                        throw new Error(`Fetch failed with status: ${response.status}`);
                    }).then(resolve).catch(reject);
                    return;
                }

                // 优先使用 GM_xmlhttpRequest
                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 failed with status: ${response.status}`));
                        }
                    },
                    onerror: function(error) {
                        reject(new Error(`GM_xmlhttpRequest network error: ${error.statusText || 'Unknown error'}`));
                    }
                });
            });
        }

        // REVERTED: Removed data:image logic
        async function processImageElement(imgElement) {
            if (!imgElement) return null;
            let imageUrlToFetch = null;

            // 尝试从 Gemini 的 Lens 链接中获取原始 URL
            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) {
                        console.error('Error parsing Lens URL:', e);
                    }
                }
            }

            // 备用方案:直接使用 src (REVERTED to skip data: URLs)
            if (!imageUrlToFetch) {
                const fallbackSrc = imgElement.src;
                if (fallbackSrc && !fallbackSrc.startsWith('data:')) {
                    imageUrlToFetch = fallbackSrc;
                }
            }

            if (!imageUrlToFetch) {
                // Do not log here, it's normal for data: URLs to be skipped
                return null;
            }

            try {
                // (关键) 使用 fetchViaGM 抓取图片
                const blob = await fetchViaGM(imageUrlToFetch);
                const base64 = await Utils.blobToBase64(blob); // Utils.blobToBase64 存在于 new.js
                return {
                    type: 'image',
                    format: blob.type,
                    size: blob.size,
                    data: base64,
                    original_src: imageUrlToFetch
                };
            } catch (error) {
                console.error('Failed to process image:', imageUrlToFetch, error);
                return null;
            }
        }


        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':
                        // 检查子节点是否包含换行符,或者父节点是否是 'pre'
                        const parentIsPre = node.parentElement?.tagName.toLowerCase() === 'pre';
                        if (children.includes('\n') || parentIsPre) {
                            // 如果父是pre,我们假设pre-code结构,这个逻辑会被pre处理
                            if (parentIsPre) return children;
                            // 单独的code块
                            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; // 使用 textContent 获取原始代码
                            return `\n\`\`\`${lang}\n${codeContent}\n\`\`\`\n`;
                        }
                        // 备用:如果pre里没有code
                        return `\n\`\`\`\n${children}\n\`\`\`\n`;
                    case 'hr': return '\n---\n';
                    case 'br': return '\n';
                    case 'p': return `\n${children}\n`;
                    // div 默认不添加额外换行,除非它是内容的主要容器
                    case 'div': return `${children}`; // 相比 old.js 做了调整,避免过多换行
                    case 'a':
                        const href = node.getAttribute('href');
                        if (href) {
                            return `[${children}](${href})`;
                        }
                        return children;
                    case 'ul':
                        return `\n${Array.from(node.children).map(li => `- ${processNode(li)}`).join('\n')}\n`;
                    case 'ol':
                        return `\n${Array.from(node.children).map((li, i) => `${i + 1}. ${processNode(li)}`).join('\n')}\n`;
                    case 'li':
                        // 'ul' 和 'ol' 的逻辑已经处理了 'li',这里直接返回子内容
                        return children;
                    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(/^\s+/, '');
            result = result.replace(/\n{3,}/g, '\n\n');
            result = result.trim();
            return result;
        }

        function getAIStudioScroller() {
            const selectors = [
                'ms-chat-session ms-autoscroll-container', // new.js 似乎没这个
                'mat-sidenav-content', // old.js 有
                '.chat-view-container' // old.js 有
            ];
            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;
        }

        // (修改) 恢复 AI Studio 增量数据提取
        // (注意:此函数依赖于上面添加的全局 'collectedData' 和 'htmlToMarkdown')
        async function extractDataIncremental_AiStudio() {
            const turns = document.querySelectorAll('ms-chat-turn');

            // (关键) 必须使用 for...of 来支持 await
            for (const turn of turns) {
                if (collectedData.has(turn)) { continue; }

                const isUserTurn = turn.querySelector('.chat-turn-container.user');
                const isModelTurn = turn.querySelector('.chat-turn-container.model');
                // (关键) 改变数据结构以包含图片
                let turnData = { type: 'unknown', text: '', images: [] };

                if (isUserTurn) {
                    const userPromptNode = isUserTurn.querySelector('.user-prompt-container .turn-content');
                    if (userPromptNode) {
                        const userText = userPromptNode.innerText.trim();
                        if (userText.match(/^User\s*[\n:]?/i)) {
                        // remove extra "User"
                        userText = userText.replace(/^User\s*[\n:]?/i, '').trim();
                        }
                        if (userText) {
                            turnData.type = 'user';
                            turnData.text = userText;
                        }
                    }
                    // (新增) 抓取用户图片
                    const imgNodes = isUserTurn.querySelectorAll('.user-prompt-container img');
                    const imgPromises = Array.from(imgNodes).map(processImageElement);
                    turnData.images = (await Promise.all(imgPromises)).filter(Boolean);

                } else if (isModelTurn) {
                    const responseChunks = isModelTurn.querySelectorAll('ms-prompt-chunk');
                    let responseTexts = [];
                    const imgPromises = []; // (新增) 收集模型图片

                    responseChunks.forEach(chunk => {
                        // 过滤掉 'thought' 块
                        if (!chunk.querySelector('ms-thought-chunk')) {
                            // 使用 old.js 的选择器
                            const cmarkNode = chunk.querySelector('ms-cmark-node');
                            if (cmarkNode) {
                                // (关键修复) 调用 htmlToMarkdown
                                const markdownText = htmlToMarkdown(cmarkNode);
                                if (markdownText) {
                                    responseTexts.push(markdownText);
                                }
                                // (新增) 在 cmark 节点内查找图片
                                const imgNodes = cmarkNode.querySelectorAll('img');
                                imgNodes.forEach(img => imgPromises.push(processImageElement(img)));
                            }
                        }
                    });
                    const responseText = responseTexts.join('\n\n').trim();
                    if (responseText) {
                        turnData.type = 'model';
                        turnData.text = responseText;
                    }
                    // (新增) 处理模型图片
                    turnData.images = (await Promise.all(imgPromises)).filter(Boolean);
                }

                // (关键) 只有在有内容时才添加
                if (turnData.type !== 'unknown' && (turnData.text || turnData.images.length > 0)) {
                    collectedData.set(turn, turnData);
                }
            }
        }


        const ScraperHandler = {
            handlers: {
                gemini: {
                    // REVERTED: Simple prompt logic from v8.1
                    getTitle: () => prompt('请输入对话标题 / Enter title:', '对话') || i18n.t('untitledChat'),
                    extractData: async () => {
                        const conversationData = [];
                        const turns = document.querySelectorAll("div.conversation-turn, div.single-turn, div.conversation-container"); // 合并选择器

                        // (关键) 改造为异步函数以处理图片
                        const processContainer = async (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 humanText = userQueryElement ? userQueryElement.innerText.trim() : "";
                            let assistantText = "";

                            if (modelResponseElement) {
                                assistantText = htmlToMarkdown(modelResponseElement);
                            } else {
                                // 备用方案(针对旧版或简单结构)
                                const fallbackEl = modelResponseContainer.querySelector("model-response, .response-container");
                                if (fallbackEl) assistantText = fallbackEl.innerText.trim(); // 简单文本
                            }

                            // (新增) 抓取图片
                            const userImageElements = container.querySelectorAll("user-query img");
                            const modelImageElements = modelResponseContainer.querySelectorAll("model-response img");

                            // (新增) 并行处理图片
                            const userImagesPromises = Array.from(userImageElements).map(processImageElement);
                            const modelImagesPromises = Array.from(modelImageElements).map(processImageElement);

                            const userImages = (await Promise.all(userImagesPromises)).filter(Boolean);
                            const modelImages = (await Promise.all(modelImagesPromises)).filter(Boolean);

                            if (humanText || assistantText || userImages.length > 0 || modelImages.length > 0) {
                                conversationData.push({
                                    human: { text: humanText, images: userImages },
                                    assistant: { text: assistantText, images: modelImages }
                                });
                            }
                        };

                        // (关键) 使用 for...of 循环来支持 await
                        for (const turn of turns) {
                            await processContainer(turn);
                        }

                        return conversationData;
                    }
                },
                notebooklm: {
                    // REVERTED: Simple date logic from v8.1
                    getTitle: () => 'NotebookLM_' + new Date().toISOString().slice(0, 10),
                    extractData: async () => {
                        const data = [];
                        const turns = document.querySelectorAll("div.chat-message-pair");

                        // (关键) 使用 for...of 循环来支持 await
                        for (const turn of turns) {
                            let question = turn.querySelector("chat-message .from-user-container .message-text-content")?.innerText.trim() || "";
                            if (question.startsWith('[Preamble] ')) question = question.substring('[Preamble] '.length).trim();

                            let answer = "";
                            const answerEl = turn.querySelector("chat-message .to-user-container .message-text-content");
                            if (answerEl) {
                                const parts = [];
                                answerEl.querySelectorAll('labs-tailwind-structural-element-view-v2').forEach(el => {
                                    let line = el.querySelector('.bullet')?.innerText.trim() + ' ' || '';
                                    const para = el.querySelector('.paragraph');
                                    if (para) {
                                        let text = '';
                                        para.childNodes.forEach(node => {
                                            if (node.nodeType === Node.TEXT_NODE) text += node.textContent;
                                            else if (node.nodeType === Node.ELEMENT_NODE && !node.querySelector?.('.citation-marker')) {
                                                text += node.classList?.contains('bold') ? `**${node.innerText}**` : (node.innerText || node.textContent || '');
                                            }
                                        });
                                        line += text;
                                    }
                                    if (line.trim()) parts.push(line.trim());
                                });
                                answer = parts.join('\n\n');
                            }

                            // (新增) 抓取图片
                            const userImageElements = turn.querySelectorAll("chat-message .from-user-container img");
                            const modelImageElements = turn.querySelectorAll("chat-message .to-user-container img");

                            const userImagesPromises = Array.from(userImageElements).map(processImageElement);
                            const modelImagesPromises = Array.from(modelImageElements).map(processImageElement);

                            const userImages = (await Promise.all(userImagesPromises)).filter(Boolean);
                            const modelImages = (await Promise.all(modelImagesPromises)).filter(Boolean);

                            if (question || answer || userImages.length > 0 || modelImages.length > 0) {
                                data.push({
                                    human: { text: question, images: userImages },
                                    assistant: { text: answer, images: modelImages }
                                });
                            }
                        }
                        return data;
                    }
                },
                aistudio: {
                    // REVERTED: Simple prompt logic from v8.1
                    getTitle: () => prompt('请输入对话标题 / Enter title:', 'AI_Studio_Chat') || 'AI_Studio_Chat',
                    extractData: async () => {
                        // 清空上次抓取的数据
                        collectedData.clear();
                        const scroller = getAIStudioScroller();
                        scroller.scrollTop = 0;
                        await Utils.sleep(SCROLL_TOP_WAIT_MS);

                        let lastScrollTop = -1;

                        while (true) {
                            await extractDataIncremental_AiStudio(); // (关键) 变为 await

                            // 检查是否到达底部
                            if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 10) {
                                break;
                            }

                            lastScrollTop = scroller.scrollTop;
                            scroller.scrollTop += scroller.clientHeight * 0.85; // 向下滚动
                            await Utils.sleep(SCROLL_DELAY_MS); // 等待内容加载

                            // 如果滚动位置没有变化,说明已到底部
                            if (scroller.scrollTop === lastScrollTop) {
                                break;
                            }
                        }

                        // 扫描完成,最后抓取一次
                        await extractDataIncremental_AiStudio(); // (关键) 变为 await
                        await Utils.sleep(500);

                        // 整理数据 (来自 old.js)
                        const finalTurnsInDom = document.querySelectorAll('ms-chat-turn');
                        let sortedData = [];
                        finalTurnsInDom.forEach(turnNode => {
                            if (collectedData.has(turnNode)) {
                                sortedData.push(collectedData.get(turnNode));
                            }
                        });

                        const pairedData = [];
                        // (关键) lastHuman 现在是一个对象
                        let lastHuman = null;
                        sortedData.forEach(item => {
                            if (item.type === 'user') {
                                // 如果连续出现 user,合并
                                if (!lastHuman) lastHuman = { text: '', images: [] };
                                lastHuman.text = (lastHuman.text ? lastHuman.text + '\n' : '') + item.text;
                                lastHuman.images.push(...item.images);
                            } else if (item.type === 'model' && lastHuman) {
                                // 发现 model,与之前的 user 配对
                                pairedData.push({ human: lastHuman, assistant: { text: item.text, images: item.images } });
                                lastHuman = null; // 重置
                            } else if (item.type === 'model' && !lastHuman) {
                                // 发现一个没有对应 user 的 model
                                pairedData.push({ human: { text: "[No preceding user prompt found]", images: [] }, assistant: { text: item.text, images: item.images } });
                            }
                        });

                        // 如果最后有 user 提问但没有 model 回答
                        if (lastHuman) {
                            pairedData.push({ human: lastHuman, assistant: { text: "[Model response is pending]", images: [] } });
                        }

                        return pairedData;
                    }
                }
            },

            addButtons: (controlsArea, platform) => {
                const handler = ScraperHandler.handlers[platform];
                if (!handler) return;

                // (新增) 预览按钮
                    controlsArea.appendChild(Utils.createButton(
                    `${previewIcon} ${i18n.t('viewOnline')}`,
                    async (btn) => {
                        const title = handler.getTitle();
                        if (!title) return; // 用户取消了 prompt

                        const original = btn.innerHTML;
                        Utils.setButtonLoading(btn, i18n.t('loading'));

                        let progressElem = null;
                        if (platform === 'aistudio') {
                            progressElem = Utils.createProgressElem(controlsArea);
                            progressElem.textContent = i18n.t('loading'); // Use 'loading' for consistency
                        }

                        try {
                            const conversationData = await handler.extractData();
                            if (!conversationData || conversationData.length === 0) {
                                alert(i18n.t('noContent'));
                                Utils.restoreButton(btn, original);
                                if (progressElem) progressElem.remove();
                                return;
                            }

                            const finalJson = {
                                title: title,
                                platform: platform,
                                exportedAt: new Date().toISOString(),
                                conversation: conversationData
                            };

                            const filename = `${platform}_${Utils.sanitizeFilename(title)}_${new Date().toISOString().slice(0, 10)}.json`;
                            await LyraCommunicator.open(JSON.stringify(finalJson, null, 2), filename);
                        } catch (error) {
                            alert(`${i18n.t('loadFailed')} ${error.message}`);
                        } finally {
                            Utils.restoreButton(btn, original);
                            if (progressElem) progressElem.remove();
                        }
                    }
                ));

                controlsArea.appendChild(Utils.createButton(
                    `${exportIcon} ${i18n.t('exportCurrentJSON')}`,
                    async (btn) => {
                        const title = handler.getTitle();
                        if (!title) return; // 用户取消了 prompt

                        const original = btn.innerHTML;
                        Utils.setButtonLoading(btn, i18n.t('exporting'));

                        let progressElem = null;
                        if (platform === 'aistudio') {
                            progressElem = Utils.createProgressElem(controlsArea);
                            progressElem.textContent = i18n.t('exporting'); // Use 'exporting' for consistency
                        }

                        try {
                            const conversationData = await handler.extractData();
                            if (!conversationData || conversationData.length === 0) {
                                alert(i18n.t('noContent'));
                                Utils.restoreButton(btn, original);
                                if (progressElem) progressElem.remove();
                                return;
                            }

                            const finalJson = {
                                title: title,
                                platform: platform,
                                exportedAt: new Date().toISOString(),
                                conversation: conversationData
                            };

                            const filename = `${platform}_${Utils.sanitizeFilename(title)}_${new Date().toISOString().slice(0, 10)}.json`;
                            Utils.downloadJSON(JSON.stringify(finalJson, null, 2), filename);
                        } catch (error) {
                            alert(`${i18n.t('exportFailed')} ${error.message}`);
                        } finally {
                            Utils.restoreButton(btn, original);
                            if (progressElem) progressElem.remove();
                        }
                    }
                ));
            }
        };

    const UI = {
        injectStyle: () => {
            const buttonColor = { claude: '#141413', gemini: '#1a73e8', notebooklm: '#000000', aistudio: '#777779' }[State.currentPlatform] || '#4285f4';

            const style = `
                #${Config.CONTROL_ID} {
                    position: fixed !important;
                    top: 50% !important;
                    right: 0 !important;
                    transform: translateY(-50%) !important;
                    background: white !important;
                    border: 1px solid #dadce0 !important;
                    border-radius: 8px !important; 
                    padding: 16px 16px 8px 16px !important;
                    width: 136px !important;
                    z-index: 999999 !important;
                    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif !important;
                    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
                }

                #${Config.CONTROL_ID}.collapsed {
                    transform: translateY(-50%) translateX(calc(100% - 35px)) !important;
                    opacity: 0.8 !important;
                    background: white !important;
                    border-color: #dadce0 !important;
                    border-radius: 8px 0 0 8px !important;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
                    pointer-events: all !important;
                }
                #${Config.CONTROL_ID}.collapsed .lyra-main-controls {
                    opacity: 0 !important;
                    pointer-events: none !important;
                }

                #${Config.CONTROL_ID}:hover {
                    opacity: 1 !important;
                }
                
                #${Config.TOGGLE_ID} {
                    position: absolute !important;
                    left: 0 !important;
                    top: 50% !important;
                    transform: translateY(-50%) translateX(-50%) !important;
                    cursor: pointer !important;
                    width: 32px !important;
                    height: 32px !important;
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    background: #ffffff !important;
                    color: ${buttonColor} !important;
                    border-radius: 50% !important;
                    box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important;
                    border: 1px solid #dadce0 !important;
                    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
                    z-index: 1000 !important;
                    pointer-events: all !important;
                }

                #${Config.CONTROL_ID}.collapsed #${Config.TOGGLE_ID} {
                    z-index: 2 !important;
                    left: 17.5px !important;
                    transform: translateY(-50%) translateX(-50%) !important;
                    width: 24px !important;
                    height: 24px !important;
                    background: ${buttonColor} !important;
                    color: white !important;
                }

                #${Config.CONTROL_ID}.collapsed #${Config.TOGGLE_ID}:hover {
                    box-shadow: 
                        0 4px 12px rgba(0,0,0,0.25),
                        0 0 0 3px rgba(255,255,255,0.9) !important;
                    transform: translateY(-50%) translateX(-50%) scale(1.15) !important;
                    opacity: 0.9 !important;
                }

                .lyra-main-controls {
                    margin-left: 0px !important; 
                    padding: 0 3px !important;
                    transition: opacity 0.3s !important;
                }
                
                .lyra-title {
                    font-size: 16px !important;
                    font-weight: 700 !important;
                    color: #202124 !important;
                    text-align: center;
                    margin-bottom: 12px !important;
                    padding-bottom: 0px !important;
                    letter-spacing: 0.3px !important;
                }

                .lyra-input-trigger {
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    gap: 3px !important;
                    font-size: 10px !important;
                    margin: 10px auto 0 auto !important;
                    padding: 2px 6px !important;
                    border-radius: 3px !important;
                    background: transparent !important;
                    cursor: pointer !important;
                    transition: all 0.15s !important;
                    white-space: nowrap !important;
                    color: #5f6368 !important;
                    border: none !important;
                    font-weight: 500 !important;
                    width: fit-content !important;
                }

                .lyra-input-trigger:hover {
                    background: #f1f3f4 !important;
                    color: #202124 !important;
                }

                .lyra-button {
                    display: flex !important;
                    align-items: center !important;
                    justify-content: flex-start !important;
                    gap: 8px !important;
                    width: 100% !important;
                    padding: 8px 12px !important;
                    margin: 8px 0 !important;
                    border: none !important;
                    border-radius: 6px !important;
                    background: ${buttonColor} !important;
                    color: white !important;
                    font-size: 11px !important;
                    font-weight: 500 !important;
                    cursor: pointer !important;
                    letter-spacing: 0.3px !important;
                    height: 32px !important;  // ← 新增:固定按钮高度
                    box-sizing: border-box !important;  // ← 新增:确保padding计入总高度
                }
                .lyra-button svg {
                    width: 16px !important;
                    height: 16px !important;
                    flex-shrink: 0 !important;
                }
                .lyra-button:disabled {
                    opacity: 0.6 !important;
                    cursor: not-allowed !important;
                }

                .lyra-status {
                    font-size: 10px !important;
                    padding: 6px 8px !important;
                    border-radius: 4px !important;
                    margin: 4px 0 !important;
                    text-align: center !important;
                }
                .lyra-status.success {
                    background: #e8f5e9 !important;
                    color: #2e7d32 !important;
                    border: 1px solid #c8e6c9 !important;
                }
                .lyra-status.error {
                    background: #ffebee !important;
                    color: #c62828 !important;
                    border: 1px solid #ffcdd2 !important;
                }

                .lyra-toggle {
                    display: flex !important;
                    align-items: center !important;
                    justify-content: space-between !important;
                    font-size: 11px !important;
                    font-weight: 500 !important;
                    color: #5f6368 !important;
                    margin: 3px 0 !important;
                    gap: 8px !important;
                    padding: 4px 8px !important;
                }

                .lyra-toggle:last-of-type {
                    margin-bottom: 14px !important;
                }

                .lyra-switch {
                    position: relative !important;
                    display: inline-block !important;
                    width: 32px !important;
                    height: 16px !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: .3s !important;
                    border-radius: 34px !important;
                }
                .lyra-slider:before {
                    position: absolute !important;
                    content: "" !important;
                    height: 12px !important;
                    width: 12px !important;
                    left: 2px !important;
                    bottom: 2px !important;
                    background-color: white !important;
                    transition: .3s !important;
                    border-radius: 50% !important;
                }
                input:checked + .lyra-slider {
                    background-color: ${buttonColor} !important;
                }
                input:checked + .lyra-slider:before {
                    transform: translateX(16px) !important;
                }

                .lyra-loading {
                    display: inline-block !important;
                    width: 14px !important;
                    height: 14px !important;
                    border: 2px solid rgba(255, 255, 255, 0.3) !important;
                    border-radius: 50% !important;
                    border-top-color: #fff !important;
                    animation: lyra-spin 0.8s 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;
                    padding: 4px !important;
                    background: #f8f9fa !important;
                    border-radius: 4px !important;
                }

                .lyra-lang-toggle {
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    gap: 3px !important;
                    font-size: 10px !important;
                    margin: 4px auto 0 auto !important;
                    padding: 2px 6px !important;
                    border-radius: 3px !important;
                    background: transparent !important;
                    cursor: pointer !important;
                    transition: all 0.15s !important;
                    white-space: nowrap !important;
                    color: #5f6368 !important;
                    border: none !important;
                    font-weight: 500 !important;
                    width: fit-content !important;
                }
                .lyra-lang-toggle:hover {
                    background: #f1f3f4 !important;
                    color: #202124 !important;
                }
            `;

            if (typeof GM_addStyle !== 'undefined') {
                GM_addStyle(style);
            } else {
                const styleEl = document.createElement('style');
                styleEl.textContent = style;
                (document.head || document.documentElement).appendChild(styleEl);
            }
        },

        toggleCollapsed: () => {
            State.isPanelCollapsed = !State.isPanelCollapsed;
            localStorage.setItem('lyraExporterCollapsed', State.isPanelCollapsed);
            const panel = document.getElementById(Config.CONTROL_ID);
            const toggle = document.getElementById(Config.TOGGLE_ID);
            if (!panel || !toggle) return;
            if (State.isPanelCollapsed) {
                panel.classList.add('collapsed');
                toggle.innerHTML = collapseIcon;
            } else {
                panel.classList.remove('collapsed');
                toggle.innerHTML = expandIcon;
            }
        },

        recreatePanel: () => {
            document.getElementById(Config.CONTROL_ID)?.remove();
            State.panelInjected = false;
            UI.createPanel();
        },

        createPanel: () => {
            if (document.getElementById(Config.CONTROL_ID) || State.panelInjected) return false;

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

            const toggle = document.createElement('div');
            toggle.id = Config.TOGGLE_ID;
            toggle.innerHTML = State.isPanelCollapsed ? collapseIcon : expandIcon;
            toggle.addEventListener('click', UI.toggleCollapsed);
            container.appendChild(toggle);

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

            const title = document.createElement('div');
            title.className = 'lyra-title';
            const titles = { claude: 'Claude', gemini: 'Gemini', notebooklm: 'Note LM', aistudio: 'AI Studio' };
            title.textContent = titles[State.currentPlatform] || 'Exporter';
            controls.appendChild(title);

            // 添加平台特定UI和按钮
            if (State.currentPlatform === 'claude') {
                ClaudeHandler.addUI(controls);
                ClaudeHandler.addButtons(controls);
                
                // 手动输入ID标签(放在语言切换之前)
                const inputLabel = document.createElement('div');
                inputLabel.className = 'lyra-input-trigger';
                inputLabel.textContent = `${i18n.t('manualUserId')}`;
                inputLabel.style.cssText = 'color: #141413 !important';
                inputLabel.addEventListener('click', () => {
                    const newId = prompt(i18n.t('enterUserId'), State.capturedUserId);
                    if (newId?.trim()) {
                        State.capturedUserId = newId.trim();
                        localStorage.setItem('lyraClaudeUserId', State.capturedUserId);
                        alert(i18n.t('userIdSaved'));
                        UI.recreatePanel();
                    }
                });
                controls.appendChild(inputLabel);
            } else {
                ScraperHandler.addButtons(controls, State.currentPlatform);
            }

            // 语言切换
            const langToggle = document.createElement('div');
            langToggle.className = 'lyra-lang-toggle';
            langToggle.textContent = `🌐 ${i18n.getLanguageShort()}`;
            langToggle.addEventListener('click', () => {
                i18n.setLanguage(i18n.currentLang === 'zh' ? 'en' : 'zh');
                UI.recreatePanel();
            });
            controls.appendChild(langToggle);

            container.appendChild(controls);
            document.body.appendChild(container);
            State.panelInjected = true;

            const panel = document.getElementById(Config.CONTROL_ID);
            if (State.isPanelCollapsed) {
                panel.classList.add('collapsed');
                toggle.innerHTML = collapseIcon;
            } else {
                panel.classList.remove('collapsed');
                toggle.innerHTML = expandIcon;
            }
            
            return true;
        }
    };

    const init = () => {
        if (!State.currentPlatform) return;

        if (State.currentPlatform === 'claude') ClaudeHandler.init();

        UI.injectStyle();

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

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

        init();
    })();