Greasy Fork

Greasy Fork is available in English.

Lyra Exporter Fetch

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

当前为 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 Exporter Fetch
// @name:en      Lyra Exporter Fetch
// @namespace    userscript://lyra-conversation-exporter
// @version      7.0
// @description  Lyra's Exporter - AI对话导出器的配套脚本,专业的AI对话导出器 - 支持Claude、ChatGPT、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, ChatGPT, Gemini, NotebookLM, and more.
// @homepage     https://github.com/Yalums/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://chatgpt.com/*
// @match        https://chat.openai.com/*
// @match        https://share.tu-zi.com/*
// @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
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/index.js
// @license      GNU General Public License v3.0
// ==/UserScript==

    (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',
            WORKSPACE_TYPE_ID: 'lyra-workspace-type',
            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('chatgpt') || host.includes('openai') || host.includes('tu-zi.com')) return 'chatgpt';
                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') || '',
            // ChatGPT 相关状态
            chatgptAccessToken: null,
            chatgptWorkspaceId: localStorage.getItem('lyraChatGPTWorkspaceId') || '',
            chatgptWorkspaceType: localStorage.getItem('lyraChatGPTWorkspaceType') || 'user', // 'user' or 'team'
            chatgptCapturedWorkspaces: new Set(),
            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已保存!',
                    workspaceType: '团队空间', userWorkspace: '个人区', teamWorkspace: '工作区',
                    manualWorkspaceId: '手动设置工作区ID', enterWorkspaceId: '请输入工作区ID:',
                    workspaceIdSaved: '工作区ID已保存!', tokenNotFound: '未找到访问令牌!',
                    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!',
                    workspaceType: 'Workspace', userWorkspace: 'Personal', teamWorkspace: 'Team',
                    manualWorkspaceId: 'Set Workspace ID', enterWorkspaceId: 'Enter Workspace ID:',
                    workspaceIdSaved: 'Workspace ID saved!', tokenNotFound: 'Access token not found!',
                    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, useInlineStyles = false) => {
                const btn = document.createElement('button');
                btn.className = 'lyra-button';
                btn.innerHTML = innerHTML;
                btn.addEventListener('click', () => onClick(btn));

                // 为 notebooklm 和 gemini 使用内联样式(优先级最高)
                if (useInlineStyles) {
                    Object.assign(btn.style, {
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'flex-start',
                        gap: '8px',
                        width: '100%',
                        maxWidth: '100%',
                        padding: '8px 12px',
                        margin: '8px 0',
                        border: 'none',
                        borderRadius: '6px',
                        fontSize: '11px',
                        fontWeight: '500',
                        cursor: 'pointer',
                        letterSpacing: '0.3px',
                        height: '32px',
                        boxSizing: 'border-box',
                        whiteSpace: 'nowrap'
                    });
                }

                return btn;
            },

            createToggle: (label, id, checked = false) => {
                const container = document.createElement('div');
                container.className = 'lyra-toggle';
                const labelSpan = document.createElement('span');
                labelSpan.className = 'lyra-toggle-label';
                labelSpan.textContent = label;
                
                const switchLabel = document.createElement('label');
                switchLabel.className = 'lyra-switch';
                
                const input = document.createElement('input');
                input.type = 'checkbox';
                input.id = id;
                input.checked = checked;
                
                const slider = document.createElement('span');
                slider.className = 'lyra-slider';
                
                switchLabel.appendChild(input);
                switchLabel.appendChild(slider);
                container.appendChild(labelSpan);
                container.appendChild(switchLabel);
                
                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) => {
            // 使用 fflate 替代 JSZip 以解决沙盒环境中的 Web Workers 限制
            if (typeof fflate === 'undefined' || typeof fflate.zipSync !== 'function' || typeof fflate.strToU8 !== 'function') {
                alert('Error: fflate 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 includeImages = document.getElementById(Config.IMAGE_SWITCH_ID)?.checked || false;
                let exported = 0;
                console.log(`Starting export of ${allConvs.length} conversations`);

                // 收集所有对话数据并准备 ZIP 条目
                const zipEntries = {};
                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`;
                            // 使用 fflate.strToU8 将 JSON 字符串转换为 Uint8Array
                            zipEntries[filename] = fflate.strToU8(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')}…`;

                // 使用 fflate.zipSync 同步生成 ZIP(避免 Web Workers 问题)
                // 压缩级别 1 = 快速压缩,适合大量数据
                const zipUint8 = fflate.zipSync(zipEntries, { level: 1 });
                const zipBlob = new Blob([zipUint8], { type: 'application/zip' });

                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);
            }
        }
    };

    // ===== ChatGPT 处理器 =====
    const ChatGPTHandler = {
        init: () => {
            // 拦截网络请求以捕获 access token 和 workspace ID
            const rawFetch = window.fetch;
            window.fetch = async function(resource, options) {
                // 捕获 Authorization header
                const headers = options?.headers;
                if (headers) {
                    let authHeader = null;
                    if (typeof headers === 'string') {
                        authHeader = headers;
                    } else if (headers instanceof Headers) {
                        authHeader = headers.get('Authorization');
                    } else {
                        authHeader = headers.Authorization || headers.authorization;
                    }

                    if (authHeader?.startsWith('Bearer ')) {
                        const token = authHeader.slice(7);
                        if (token && token.toLowerCase() !== 'dummy') {
                            State.chatgptAccessToken = token;
                        }
                    }

                    // 捕获 workspace ID
                    let workspaceId = null;
                    if (headers instanceof Headers) {
                        workspaceId = headers.get('ChatGPT-Account-Id');
                    } else if (typeof headers === 'object') {
                        workspaceId = headers['ChatGPT-Account-Id'];
                    }

                    if (workspaceId && !State.chatgptCapturedWorkspaces.has(workspaceId)) {
                        State.chatgptCapturedWorkspaces.add(workspaceId);
                    }
                }

                return rawFetch.apply(this, arguments);
            };
        },

        ensureAccessToken: async () => {
            if (State.chatgptAccessToken) return State.chatgptAccessToken;

            try {
                const response = await fetch('/api/auth/session?unstable_client=true');
                const session = await response.json();
                if (session.accessToken) {
                    State.chatgptAccessToken = session.accessToken;
                    return session.accessToken;
                }
            } catch (error) {
                console.error('Failed to get access token:', error);
            }

            return null;
        },

        getOaiDeviceId: () => {
            const cookieString = document.cookie;
            const match = cookieString.match(/oai-did=([^;]+)/);
            return match ? match[1] : null;
        },

        ensureWorkspaceId: () => {
            if (State.chatgptWorkspaceId) return State.chatgptWorkspaceId;

            // 尝试从捕获的workspace中获取
            const captured = Array.from(State.chatgptCapturedWorkspaces);
            if (captured.length > 0) {
                State.chatgptWorkspaceId = captured[0];
                localStorage.setItem('lyraChatGPTWorkspaceId', captured[0]);
                return captured[0];
            }

            return null;
        },

        getCurrentConversationId: () => {
            const match = window.location.pathname.match(/\/c\/([a-zA-Z0-9-]+)/);
            return match ? match[1] : null;
        },

        getAllConversations: async (workspaceId) => {
            const token = await ChatGPTHandler.ensureAccessToken();
            if (!token) throw new Error(i18n.t('tokenNotFound'));

            const deviceId = ChatGPTHandler.getOaiDeviceId();
            if (!deviceId) throw new Error('Cannot get device ID');

            const headers = {
                'Authorization': `Bearer ${token}`,
                'oai-device-id': deviceId
            };
            
            // 只在团队空间且有workspace ID时才添加
            if (State.chatgptWorkspaceType === 'team' && workspaceId) {
                headers['ChatGPT-Account-Id'] = workspaceId;
            }

            const allConversations = [];
            let offset = 0;
            let hasMore = true;

            while (hasMore) {
                const response = await fetch(`/backend-api/conversations?offset=${offset}&limit=28&order=updated`, { headers });
                if (!response.ok) throw new Error('Failed to fetch conversation list');

                const data = await response.json();
                if (data.items && data.items.length > 0) {
                    allConversations.push(...data.items);
                    hasMore = data.items.length === 28;
                    offset += data.items.length;
                } else {
                    hasMore = false;
                }
            }

            return allConversations;
        },

        getConversation: async (conversationId, workspaceId) => {
            const token = await ChatGPTHandler.ensureAccessToken();
            if (!token) {
                console.error('[ChatGPT] Token not found');
                throw new Error(i18n.t('tokenNotFound'));
            }
            
            const deviceId = ChatGPTHandler.getOaiDeviceId();
            if (!deviceId) {
                console.error('[ChatGPT] Device ID not found in cookies');
                throw new Error('Cannot get device ID');
            }

            const headers = {
                'Authorization': `Bearer ${token}`,
                'oai-device-id': deviceId
            };
            
            // 关键修复:只在团队空间模式时才添加workspace ID header
            // 用户空间不需要这个header
            if (State.chatgptWorkspaceType === 'team' && workspaceId) {
                headers['ChatGPT-Account-Id'] = workspaceId;
            }

            console.log('[ChatGPT] Fetching conversation:', {
                conversationId,
                workspaceId,
                workspaceType: State.chatgptWorkspaceType,
                willAddWorkspaceHeader: State.chatgptWorkspaceType === 'team' && !!workspaceId,
                hasToken: !!token,
                hasDeviceId: !!deviceId,
                tokenPrefix: token.substring(0, 10) + '...',
                headers: { ...headers, 'Authorization': 'Bearer ***' }
            });

            const response = await fetch(`/backend-api/conversation/${conversationId}`, { headers });
            
            console.log('[ChatGPT] Response status:', response.status);
            
            if (!response.ok) {
                const errorText = await response.text();
                console.error('[ChatGPT] Fetch failed:', {
                    status: response.status,
                    statusText: response.statusText,
                    error: errorText,
                    conversationId,
                    workspaceType: State.chatgptWorkspaceType
                });
                
                // 如果是 404 错误,提示用户切换工作区类型
                let errorMessage = `Failed to fetch conversation (${response.status}): ${errorText || response.statusText}`;
                if (response.status === 404) {
                    const currentMode = State.chatgptWorkspaceType === 'team' ? i18n.t('teamWorkspace') : i18n.t('userWorkspace');
                    const suggestMode = State.chatgptWorkspaceType === 'team' ? i18n.t('userWorkspace') : i18n.t('teamWorkspace');
                    errorMessage += `\n\n当前模式: ${currentMode}\n建议尝试切换到: ${suggestMode}`;
                }
                
                throw new Error(errorMessage);
            }

            return await response.json();
        },

        previewConversation: async () => {
            const conversationId = ChatGPTHandler.getCurrentConversationId();
            if (!conversationId) {
                alert(i18n.t('uuidNotFound'));
                return;
            }

            try {
                // 只在团队空间模式时获取workspace ID,用户空间传空字符串
                const workspaceId = State.chatgptWorkspaceType === 'team' ? ChatGPTHandler.ensureWorkspaceId() : '';
                const data = await ChatGPTHandler.getConversation(conversationId, workspaceId);

                const win = window.open(Config.EXPORTER_URL, '_blank');
                if (!win) {
                    alert(i18n.t('cannotOpenExporter'));
                    return;
                }

                const checkReady = setInterval(() => {
                    try {
                        win.postMessage({
                            type: 'lyra-preview',
                            data: data,
                            platform: 'chatgpt'
                        }, Config.EXPORTER_ORIGIN);
                        clearInterval(checkReady);
                    } catch (e) {}
                }, 100);

                setTimeout(() => clearInterval(checkReady), 5000);
            } catch (error) {
                console.error('Preview error:', error);
                // 提示用户切换工作区类型
                alert(`${i18n.t('loadFailed')} ${error.message}`);
            }
        },

        exportCurrent: async (btn) => {
            const conversationId = ChatGPTHandler.getCurrentConversationId();
            if (!conversationId) {
                alert(i18n.t('uuidNotFound'));
                return;
            }

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

            try {
                const workspaceId = State.chatgptWorkspaceType === 'team' ? ChatGPTHandler.ensureWorkspaceId() : '';
                const data = await ChatGPTHandler.getConversation(conversationId, workspaceId);

                const filename = prompt(i18n.t('enterFilename'), data.title || i18n.t('untitledChat'));
                if (!filename) {
                    Utils.restoreButton(btn, original);
                    return;
                }

                Utils.downloadJSON(JSON.stringify(data, null, 2), `${Utils.sanitizeFilename(filename)}.json`);
            } catch (error) {
                console.error('Export error:', error);
                alert(`${i18n.t('exportFailed')} ${error.message}`);
            } finally {
                Utils.restoreButton(btn, original);
            }
        },

        exportAll: async (btn, controlsArea) => {
            if (typeof fflate === 'undefined' || typeof fflate.zipSync !== 'function' || typeof fflate.strToU8 !== 'function') {
                alert('Error: fflate library not loaded.');
                return;
            }

            const progress = Utils.createProgressElem(controlsArea);
            progress.textContent = i18n.t('preparing');
            const original = btn.innerHTML;
            Utils.setButtonLoading(btn, i18n.t('exporting'));

            try {
                const workspaceId = State.chatgptWorkspaceType === 'team' ? ChatGPTHandler.ensureWorkspaceId() : '';
                const allConvs = await ChatGPTHandler.getAllConversations(workspaceId);
                if (!allConvs || !Array.isArray(allConvs)) throw new Error(i18n.t('fetchFailed'));

                let exported = 0;
                const zipEntries = {};

                for (let i = 0; i < allConvs.length; i++) {
                    const conv = allConvs[i];
                    progress.textContent = `${i18n.t('gettingConversation')} ${i + 1}/${allConvs.length}`;

                    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 ChatGPTHandler.getConversation(conv.id, workspaceId);
                        if (data) {
                            const title = Utils.sanitizeFilename(data.title || conv.id);
                            const filename = `chatgpt_${conv.id.substring(0, 8)}_${title}.json`;
                            zipEntries[filename] = fflate.strToU8(JSON.stringify(data, null, 2));
                            exported++;
                        }
                    } catch (error) {
                        console.error(`Failed to process ${conv.id}:`, error);
                    }
                }

                progress.textContent = `${i18n.t('compressing')}…`;
                const zipUint8 = fflate.zipSync(zipEntries, { level: 1 });
                const zipBlob = new Blob([zipUint8], { type: 'application/zip' });

                const zipFilename = `chatgpt_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);
            }
        },

        addUI: (controls) => {
            // 工作区类型切换:默认用户空间(unchecked),切换到团队空间(checked)
            const initialLabel = State.chatgptWorkspaceType === 'team' ? i18n.t('teamWorkspace') : i18n.t('userWorkspace');
            const workspaceToggle = Utils.createToggle(
                initialLabel,
                Config.WORKSPACE_TYPE_ID,
                State.chatgptWorkspaceType === 'team'
            );
            
            // 添加change事件监听
            const toggleInput = workspaceToggle.querySelector('input');
            const toggleLabel = workspaceToggle.querySelector('.lyra-toggle-label');
            
            toggleInput.addEventListener('change', (e) => {
                State.chatgptWorkspaceType = e.target.checked ? 'team' : 'user';
                localStorage.setItem('lyraChatGPTWorkspaceType', State.chatgptWorkspaceType);
                toggleLabel.textContent = e.target.checked ? i18n.t('teamWorkspace') : i18n.t('userWorkspace');
                console.log('[ChatGPT] Workspace type changed to:', State.chatgptWorkspaceType);
            });
            
            controls.appendChild(workspaceToggle);
        },

        addButtons: (controls) => {
            // 预览按钮
            controls.appendChild(Utils.createButton(
                `${previewIcon} ${i18n.t('viewOnline')}`,
                () => ChatGPTHandler.previewConversation()
            ));

            // 导出当前按钮
            controls.appendChild(Utils.createButton(
                `${exportIcon} ${i18n.t('exportCurrentJSON')}`,
                (btn) => ChatGPTHandler.exportCurrent(btn)
            ));

            // 导出全部按钮
            controls.appendChild(Utils.createButton(
                `${zipIcon} ${i18n.t('exportAllConversations')}`,
                (btn) => ChatGPTHandler.exportAll(btn, controls)
            ));

            // 手动设置Workspace ID
            const workspaceIdLabel = document.createElement('div');
            workspaceIdLabel.className = 'lyra-input-trigger';
            workspaceIdLabel.textContent = `${i18n.t('manualWorkspaceId')}`;
            workspaceIdLabel.addEventListener('click', () => {
                const newId = prompt(i18n.t('enterWorkspaceId'), State.chatgptWorkspaceId);
                if (newId?.trim()) {
                    State.chatgptWorkspaceId = newId.trim();
                    localStorage.setItem('lyraChatGPTWorkspaceId', State.chatgptWorkspaceId);
                    alert(i18n.t('workspaceIdSaved'));
                }
            });
            controls.appendChild(workspaceIdLabel);
        }
    };

        // ===== 平台处理器: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(includeImages = true) {
            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) {
                        let 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;
                        }
                    }
                    // (新增) 抓取用户图片 - 根据 includeImages 参数决定是否处理
                    if (includeImages) {
                        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 节点内查找图片 - 根据 includeImages 参数决定是否处理
                                if (includeImages) {
                                    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;
                    }
                    // (新增) 处理模型图片
                    if (includeImages) {
                        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 (includeImages = true) => {
                        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(); // 简单文本
                            }

                            // (新增) 抓取图片 - 根据 includeImages 参数决定是否处理
                            let userImages = [];
                            let modelImages = [];

                            if (includeImages) {
                                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);

                                userImages = (await Promise.all(userImagesPromises)).filter(Boolean);
                                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 (includeImages = true) => {
                        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');
                            }

                            // (新增) 抓取图片 - 根据 includeImages 参数决定是否处理
                            let userImages = [];
                            let modelImages = [];

                            if (includeImages) {
                                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);

                                userImages = (await Promise.all(userImagesPromises)).filter(Boolean);
                                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 (includeImages = true) => {
                        // 清空上次抓取的数据
                        collectedData.clear();
                        const scroller = getAIStudioScroller();
                        scroller.scrollTop = 0;
                        await Utils.sleep(SCROLL_TOP_WAIT_MS);

                        let lastScrollTop = -1;

                        while (true) {
                            await extractDataIncremental_AiStudio(includeImages); // (关键) 传递 includeImages 参数

                            // 检查是否到达底部
                            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(includeImages); // (关键) 传递 includeImages 参数
                        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;

                // 为 gemini 和 aistudio 添加 includeImages toggle
                if (platform === 'gemini' || platform === 'aistudio') {
                    const imageToggle = Utils.createToggle(i18n.t('includeImages'), Config.IMAGE_SWITCH_ID, State.includeImages);
                    controlsArea.appendChild(imageToggle);

                    // 为 toggle 添加主题色
                    const themeColors = { gemini: '#1a73e8', aistudio: '#777779' };
                    const toggleSwitch = imageToggle.querySelector('.lyra-switch input');
                    if (toggleSwitch) {
                        toggleSwitch.addEventListener('change', (e) => {
                            State.includeImages = e.target.checked;
                            localStorage.setItem('lyraIncludeImages', State.includeImages);
                        });

                        // 应用主题色到 slider
                        const slider = imageToggle.querySelector('.lyra-slider');
                        if (slider) {
                            const color = themeColors[platform];
                            slider.style.setProperty('--theme-color', color);
                        }
                    }
                }

                // 判断是否使用内联样式(notebooklm 和 gemini)
                const useInlineStyles = (platform === 'notebooklm' || platform === 'gemini');

                // 获取按钮主题色
                const buttonColor = { gemini: '#1a73e8', notebooklm: '#000000', aistudio: '#777779' }[platform] || '#4285f4';

                // (新增) 预览按钮
                const previewBtn = 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 {
                            // 获取 includeImages 状态
                            const includeImages = (platform === 'gemini' || platform === 'aistudio') ?
                                (document.getElementById(Config.IMAGE_SWITCH_ID)?.checked || false) : true;

                            const conversationData = await handler.extractData(includeImages);
                            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();
                        }
                    },
                    useInlineStyles
                );

                // 应用按钮颜色
                if (useInlineStyles) {
                    Object.assign(previewBtn.style, {
                        backgroundColor: buttonColor,
                        color: 'white'
                    });
                }
                controlsArea.appendChild(previewBtn);

                const exportBtn = 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 {
                            // 获取 includeImages 状态
                            const includeImages = (platform === 'gemini' || platform === 'aistudio') ?
                                (document.getElementById(Config.IMAGE_SWITCH_ID)?.checked || false) : true;

                            const conversationData = await handler.extractData(includeImages);
                            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();
                        }
                    },
                    useInlineStyles
                );

                // 应用按钮颜色
                if (useInlineStyles) {
                    Object.assign(exportBtn.style, {
                        backgroundColor: buttonColor,
                        color: 'white'
                    });
                }
                controlsArea.appendChild(exportBtn);
            }
        };

    const UI = {
        injectStyle: () => {
            const buttonColor = { claude: '#141413', chatgpt: '#10A37F', 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.68 !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;
                    --theme-color: ${buttonColor};
                }
                .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: var(--theme-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');

            // 为 notebooklm 和 gemini 添加内联样式固定面板宽高
            if (State.currentPlatform === 'notebooklm' || State.currentPlatform === 'gemini') {
                Object.assign(container.style, {
                    position: 'fixed',
                    top: '50%',
                    right: '0',
                    transform: 'translataeY(-50%)',
                    background: 'white',
                    border: '1px solid #dadce0',
                    borderRadius: '8px',
                    padding: '16px 16px 8px 16px',
                    width: '136px',
                    zIndex: '999999',
                    fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif",
                    transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
                    boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
                    boxSizing: 'border-box'
                });
            }

            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';

            // 为 notebooklm 和 gemini 的 controls 添加内联样式
            if (State.currentPlatform === 'notebooklm' || State.currentPlatform === 'gemini') {
                Object.assign(controls.style, {
                    marginLeft: '0px',
                    padding: '0 3px',
                    transition: 'opacity 0.3s'
                });
            }

            const title = document.createElement('div');
            title.className = 'lyra-title';
            const titles = { claude: 'Claude', chatgpt: 'ChatGPT', 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.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 if (State.currentPlatform === 'chatgpt') {
                ChatGPTHandler.addUI(controls);
                ChatGPTHandler.addButtons(controls);
            } 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();
        if (State.currentPlatform === 'chatgpt') ChatGPTHandler.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 if (State.currentPlatform === 'chatgpt') {
                if (/\/c\/[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 (/\/c\/[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();
    })();