Thriller

M

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Thriller
// @namespace    http://tampermonkey.net/merged_tools
// @version      1.0.1
// @description  M
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @match        https://notebooklm.google.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// @connect      notebooklm.google.com
// @connect      accounts.google.com
// @license PolyForm-Noncommercial-1.0.0  https://polyformproject.org/licenses/noncommercial/1.0.0/
// ==/UserScript==


(function () {
    'use strict';

    const SINGLETON_KEY = '__CHATGPT_EXPORTER_MARKDOWN_SINGLETON__';
    if (window[SINGLETON_KEY]) {
        console.warn('[ChatGPT Exporter] 检测到重复实例,当前实例已跳过启动。');
        return;
    }
    window[SINGLETON_KEY] = true;

    // --- 配置与全局变量 ---
    const BASE_DELAY = 600;
    const JITTER = 400;
    const PAGE_LIMIT = 100;
    const PROJECT_SIDEBAR_PREVIEW = 5;
    const AUTO_EXPORT_STORAGE_KEY = 'chatgpt_exporter_auto_export_schedule_v1';
    const EXPORT_FILENAME_SETTINGS_STORAGE_KEY = 'chatgpt_exporter_filename_settings_v1';
    const EXPORT_DOWNLOAD_SETTINGS_STORAGE_KEY = 'chatgpt_exporter_download_settings_v1';
    const FILE_HANDLE_DB_NAME = 'chatgpt_exporter_file_handles_v1';
    const FILE_HANDLE_STORE_NAME = 'handles';
    const FILE_HANDLE_KEY_ZIP = 'zip_export_target';
    const AUTO_EXPORT_MIN_MINUTES = 1;
    const DEFAULT_NOTIFY_MODE = 'toast'; // 可改为 'silent' 以完全静默
    const SCHEDULE_NOTIFY_MODE = 'silent';
    const TEAM_ONLY_MODE = true; // 仅保留团队空间导出
    let accessToken = null;
    let capturedWorkspaceIds = new Set(); // 使用Set存储网络拦截到的ID,确保唯一性
    let exportInProgress = false;
    let autoExportTimer = null;
    let autoExportTaskRunning = false;
    let autoExportState = loadAutoExportState();
    let filenameSettings = loadFilenameSettings();
    let downloadSettings = loadDownloadSettings();
    let cachedZipFileHandle = null;
    let fileHandleDbPromise = null;
    // --- 核心:网络拦截与信息捕获 ---
    (function interceptNetwork() {
        const rawFetch = window.fetch;
        window.fetch = async function (resource, options) {
            tryCaptureToken(options?.headers);
            if (options?.headers?.['ChatGPT-Account-Id']) {
                const id = options.headers['ChatGPT-Account-Id'];
                if (id && !capturedWorkspaceIds.has(id)) {
                    console.log('🎯 [Fetch] 捕获到 Workspace ID:', id);
                    capturedWorkspaceIds.add(id);
                }
            }
            return rawFetch.apply(this, arguments);
        };

        const rawOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function () {
            this.addEventListener('readystatechange', () => {
                if (this.readyState === 4) {
                    try {
                        tryCaptureToken(this.getRequestHeader('Authorization'));
                        const id = this.getRequestHeader('ChatGPT-Account-Id');
                        if (id && !capturedWorkspaceIds.has(id)) {
                            console.log('🎯 [XHR] 捕获到 Workspace ID:', id);
                            capturedWorkspaceIds.add(id);
                        }
                    } catch (_) {}
                }
            });
            return rawOpen.apply(this, arguments);
        };
    })();

    function tryCaptureToken(header) {
        if (!header) return;
        const h = typeof header === 'string' ? header : header instanceof Headers ? header.get('Authorization') : header.Authorization || header.authorization;
        if (h?.startsWith('Bearer ')) {
        const token = h.slice(7);
        // [v8.2.0 修复] 在捕获源头增加验证,拒绝已知的无效占位符Token
        if (token && token.toLowerCase() !== 'dummy') {
            accessToken = token;
        }
        }
    }

    async function ensureAccessToken(options = {}) {
        const { notifyMode = DEFAULT_NOTIFY_MODE } = options;
        if (accessToken) return accessToken;
        try {
            const session = await (await fetch('/api/auth/session?unstable_client=true')).json();
            if (session.accessToken) {
                accessToken = session.accessToken;
                return accessToken;
            }
        } catch (_) {}
        notifyUser('无法获取 Access Token。请刷新页面或打开任意一个对话后再试。', {
            mode: notifyMode,
            level: 'error',
            duration: 3200
        });
        return null;
    }

    // --- 辅助函数 ---
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    const jitter = () => BASE_DELAY + Math.random() * JITTER;
    const sanitizeFilename = (name) => name.replace(/[\/\\?%*:|"<>]/g, '-').trim();
    const normalizeEpochSeconds = (value) => {
        if (!value) return 0;
        if (typeof value === 'number' && Number.isFinite(value)) {
            return value > 1e12 ? Math.floor(value / 1000) : value;
        }
        if (typeof value === 'string') {
            const parsed = Date.parse(value);
            if (!Number.isNaN(parsed)) {
                return Math.floor(parsed / 1000);
            }
        }
        return 0;
    };
    const formatTimestamp = (value) => {
        const seconds = normalizeEpochSeconds(value);
        if (!seconds) return '';
        const date = new Date(seconds * 1000);
        return Number.isNaN(date.getTime()) ? '' : date.toLocaleString();
    };
    const parseDateInputToEpoch = (value, isEnd = false) => {
        if (!value) return null;
        const parts = value.split('-').map(Number);
        if (parts.length !== 3 || parts.some(Number.isNaN)) return null;
        const [year, month, day] = parts;
        const date = isEnd
            ? new Date(year, month - 1, day, 23, 59, 59, 999)
            : new Date(year, month - 1, day, 0, 0, 0, 0);
        const epochMs = date.getTime();
        return Number.isNaN(epochMs) ? null : Math.floor(epochMs / 1000);
    };

    function ensureToastStyles() {
        if (document.getElementById('chatgpt-exporter-toast-style')) return;
        const style = document.createElement('style');
        style.id = 'chatgpt-exporter-toast-style';
        style.textContent = `
            #cge-toast-container {
                position: fixed;
                right: 16px;
                bottom: 16px;
                z-index: 100001;
                display: flex;
                flex-direction: column;
                gap: 8px;
                pointer-events: none;
                max-width: min(78vw, 360px);
            }
            .cge-toast {
                padding: 10px 12px;
                border-radius: 10px;
                color: #fff;
                font-size: 13px;
                line-height: 1.35;
                box-shadow: 0 6px 20px rgba(0, 0, 0, 0.26);
                opacity: 0;
                transform: translateY(6px);
                transition: opacity 0.2s ease, transform 0.2s ease;
                pointer-events: none;
                word-break: break-word;
            }
            .cge-toast[data-level="info"] { background: rgba(15, 66, 116, 0.95); }
            .cge-toast[data-level="success"] { background: rgba(24, 122, 84, 0.95); }
            .cge-toast[data-level="error"] { background: rgba(164, 52, 66, 0.96); }
            .cge-toast.cge-toast-show {
                opacity: 1;
                transform: translateY(0);
            }
        `;
        document.head.appendChild(style);
    }

    function showToast(message, level = 'info', duration = 2600) {
        if (!message) return;
        ensureToastStyles();
        let container = document.getElementById('cge-toast-container');
        if (!container) {
            container = document.createElement('div');
            container.id = 'cge-toast-container';
            document.body.appendChild(container);
        }
        const toast = document.createElement('div');
        toast.className = 'cge-toast';
        toast.dataset.level = level;
        toast.textContent = message;
        container.appendChild(toast);
        requestAnimationFrame(() => toast.classList.add('cge-toast-show'));
        setTimeout(() => {
            toast.classList.remove('cge-toast-show');
            setTimeout(() => {
                if (toast.parentNode) toast.parentNode.removeChild(toast);
                if (container && container.childElementCount === 0 && container.parentNode) {
                    container.parentNode.removeChild(container);
                }
            }, 220);
        }, Math.max(1200, duration));
    }

    function notifyUser(message, options = {}) {
        const { mode = DEFAULT_NOTIFY_MODE, level = 'info', duration = 2600 } = options;
        if (!message || mode === 'silent') return;
        if (mode === 'modal') {
            alert(message);
            return;
        }
        showToast(message, level, duration);
    }

    function escapeHtml(value) {
        return String(value ?? '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    function normalizeFilenameSettings(input = {}) {
        const template = typeof input.template === 'string' ? input.template.trim() : '';
        const appendDate = input.appendDate === undefined ? true : Boolean(input.appendDate);
        return { template, appendDate };
    }

    function loadFilenameSettings() {
        try {
            const raw = localStorage.getItem(EXPORT_FILENAME_SETTINGS_STORAGE_KEY);
            if (!raw) return normalizeFilenameSettings();
            const parsed = JSON.parse(raw);
            return normalizeFilenameSettings(parsed);
        } catch (_) {
            return normalizeFilenameSettings();
        }
    }

    function saveFilenameSettings() {
        try {
            localStorage.setItem(EXPORT_FILENAME_SETTINGS_STORAGE_KEY, JSON.stringify(filenameSettings));
        } catch (_) {}
    }

    function normalizeDownloadSettings(input = {}) {
        const mode = input.mode === 'file-handle' ? 'file-handle' : 'browser';
        const handleName = typeof input.handleName === 'string' ? input.handleName.trim() : '';
        return { mode, handleName };
    }

    function loadDownloadSettings() {
        try {
            const raw = localStorage.getItem(EXPORT_DOWNLOAD_SETTINGS_STORAGE_KEY);
            if (!raw) return normalizeDownloadSettings();
            const parsed = JSON.parse(raw);
            return normalizeDownloadSettings(parsed);
        } catch (_) {
            return normalizeDownloadSettings();
        }
    }

    function saveDownloadSettings() {
        try {
            localStorage.setItem(EXPORT_DOWNLOAD_SETTINGS_STORAGE_KEY, JSON.stringify(downloadSettings));
        } catch (_) {}
    }

    function supportsFixedFileOverwrite() {
        return typeof window.showSaveFilePicker === 'function' && typeof indexedDB !== 'undefined';
    }

    function idbRequestToPromise(request) {
        return new Promise((resolve, reject) => {
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error || new Error('IndexedDB 请求失败'));
        });
    }

    function idbTransactionToPromise(tx) {
        return new Promise((resolve, reject) => {
            tx.oncomplete = () => resolve();
            tx.onerror = () => reject(tx.error || new Error('IndexedDB 事务失败'));
            tx.onabort = () => reject(tx.error || new Error('IndexedDB 事务中止'));
        });
    }

    function getFileHandleDb() {
        if (!fileHandleDbPromise) {
            fileHandleDbPromise = new Promise((resolve, reject) => {
                const request = indexedDB.open(FILE_HANDLE_DB_NAME, 1);
                request.onupgradeneeded = () => {
                    const db = request.result;
                    if (!db.objectStoreNames.contains(FILE_HANDLE_STORE_NAME)) {
                        db.createObjectStore(FILE_HANDLE_STORE_NAME);
                    }
                };
                request.onsuccess = () => resolve(request.result);
                request.onerror = () => reject(request.error || new Error('打开 IndexedDB 失败'));
            });
        }
        return fileHandleDbPromise;
    }

    async function getStoredZipFileHandle() {
        if (cachedZipFileHandle) return cachedZipFileHandle;
        if (!supportsFixedFileOverwrite()) return null;
        try {
            const db = await getFileHandleDb();
            const tx = db.transaction(FILE_HANDLE_STORE_NAME, 'readonly');
            const store = tx.objectStore(FILE_HANDLE_STORE_NAME);
            const handle = await idbRequestToPromise(store.get(FILE_HANDLE_KEY_ZIP));
            await idbTransactionToPromise(tx);
            cachedZipFileHandle = handle || null;
            return cachedZipFileHandle;
        } catch (_) {
            return null;
        }
    }

    async function setStoredZipFileHandle(handle) {
        if (!supportsFixedFileOverwrite() || !handle) return;
        const db = await getFileHandleDb();
        const tx = db.transaction(FILE_HANDLE_STORE_NAME, 'readwrite');
        tx.objectStore(FILE_HANDLE_STORE_NAME).put(handle, FILE_HANDLE_KEY_ZIP);
        await idbTransactionToPromise(tx);
        cachedZipFileHandle = handle;
    }

    async function clearStoredZipFileHandle() {
        cachedZipFileHandle = null;
        if (!supportsFixedFileOverwrite()) return;
        try {
            const db = await getFileHandleDb();
            const tx = db.transaction(FILE_HANDLE_STORE_NAME, 'readwrite');
            tx.objectStore(FILE_HANDLE_STORE_NAME).delete(FILE_HANDLE_KEY_ZIP);
            await idbTransactionToPromise(tx);
        } catch (_) {}
    }

    async function ensureFileHandlePermission(handle, requestWrite = false) {
        if (!handle) return false;
        const options = { mode: 'readwrite' };
        try {
            let state = await handle.queryPermission(options);
            if (state === 'granted') return true;
            if (!requestWrite) return false;
            state = await handle.requestPermission(options);
            return state === 'granted';
        } catch (_) {
            return false;
        }
    }

    async function pickAndRememberZipFileHandle(suggestedName = 'chatgpt_backup.zip') {
        if (!supportsFixedFileOverwrite()) {
            throw new Error('当前浏览器不支持固定文件覆盖模式。');
        }

        const handle = await window.showSaveFilePicker({
            suggestedName,
            types: [{
                description: 'ZIP 文件',
                accept: { 'application/zip': ['.zip'] }
            }]
        });
        const granted = await ensureFileHandlePermission(handle, true);
        if (!granted) {
            throw new Error('未授予固定文件写入权限。');
        }

        await setStoredZipFileHandle(handle);
        downloadSettings.handleName = handle.name || '';
        saveDownloadSettings();
        return handle;
    }

    function buildDefaultExportZipFilename(mode, selectionType, workspaceId, date, appendDate = true) {
        if (selectionType === 'selected') {
            return mode === 'team'
                ? `chatgpt_team_selected_${workspaceId}${appendDate ? `_${date}` : ''}.zip`
                : mode === 'project'
                    ? `chatgpt_project_selected${appendDate ? `_${date}` : ''}.zip`
                    : `chatgpt_personal_selected${appendDate ? `_${date}` : ''}.zip`;
        }
        return mode === 'team'
            ? `chatgpt_team_backup_${workspaceId}${appendDate ? `_${date}` : ''}.zip`
            : mode === 'project'
                ? `chatgpt_project_backup${appendDate ? `_${date}` : ''}.zip`
                : `chatgpt_personal_backup${appendDate ? `_${date}` : ''}.zip`;
    }

    function buildExportZipFilename(mode, selectionType, workspaceId, overrideSettings = null) {
        const normalizedMode = normalizeExportMode(mode);
        const normalizedSelectionType = selectionType === 'selected' ? 'selected' : 'full';
        const date = new Date().toISOString().slice(0, 10);
        const settings = overrideSettings ? normalizeFilenameSettings(overrideSettings) : filenameSettings;

        if (!settings.template) {
            return buildDefaultExportZipFilename(
                normalizedMode,
                normalizedSelectionType,
                workspaceId,
                date,
                settings.appendDate
            );
        }

        const typeValue = normalizedSelectionType === 'selected' ? 'selected' : 'backup';
        const workspaceValue = typeof workspaceId === 'string' ? workspaceId.trim() : '';
        const hasDatePlaceholder = /\{date\}/i.test(settings.template);
        let baseName = settings.template
            .replace(/\{mode\}/gi, normalizedMode)
            .replace(/\{type\}/gi, typeValue)
            .replace(/\{workspace\}/gi, workspaceValue)
            .replace(/\{date\}/gi, date)
            .trim();

        if (settings.appendDate && !hasDatePlaceholder) {
            baseName = `${baseName}_${date}`;
        }

        baseName = sanitizeFilename(baseName)
            .replace(/\.zip$/i, '')
            .replace(/_+/g, '_')
            .replace(/^[-_. ]+|[-_. ]+$/g, '');

        if (!baseName) {
            baseName = `chatgpt_export_${date}`;
        }

        return `${baseName}.zip`;
    }

    function normalizeScheduleEntries(entries) {
        if (!Array.isArray(entries)) return [];
        const map = new Map();
        entries.forEach(item => {
            const id = typeof item?.id === 'string' ? item.id.trim() : '';
            if (!id || map.has(id)) return;
            map.set(id, {
                id,
                title: typeof item?.title === 'string' && item.title.trim() ? item.title.trim() : 'Untitled Conversation',
                projectId: typeof item?.projectId === 'string' && item.projectId.trim() ? item.projectId.trim() : null,
                projectTitle: typeof item?.projectTitle === 'string' && item.projectTitle.trim() ? item.projectTitle.trim() : null,
                is_archived: Boolean(item?.is_archived),
                create_time: normalizeEpochSeconds(item?.create_time || 0),
                update_time: normalizeEpochSeconds(item?.update_time || item?.create_time || 0)
            });
        });
        return Array.from(map.values());
    }

    function normalizeExportMode(mode) {
        if (TEAM_ONLY_MODE) return 'team';
        return ['personal', 'project', 'team'].includes(mode) ? mode : 'personal';
    }

    function getModeLabel(mode) {
        return mode === 'team' ? '团队空间' : mode === 'project' ? '项目空间' : '个人空间';
    }

    function normalizeAutoExportState(input = {}) {
        const mode = normalizeExportMode(input.mode);
        const intervalRaw = Number(input.intervalMinutes);
        const intervalMinutes = Number.isFinite(intervalRaw)
            ? Math.max(AUTO_EXPORT_MIN_MINUTES, Math.floor(intervalRaw))
            : 60;
        const workspaceId = typeof input.workspaceId === 'string' ? input.workspaceId.trim() : '';
        const nextRunAt = Number(input.nextRunAt);
        const lastRunAt = Number(input.lastRunAt);
        const lastError = typeof input.lastError === 'string' ? input.lastError.trim() : '';
        const selectionType = input.selectionType === 'selected' ? 'selected' : 'all';
        const selectedModeRaw = ['personal', 'project', 'team'].includes(input.selectedMode) ? input.selectedMode : '';
        let selectedEntries = normalizeScheduleEntries(input.selectedEntries);
        const selectedMode = TEAM_ONLY_MODE ? 'team' : selectedModeRaw;
        const selectedWorkspaceId = typeof input.selectedWorkspaceId === 'string' ? input.selectedWorkspaceId.trim() : '';
        if (TEAM_ONLY_MODE && selectedModeRaw && selectedModeRaw !== 'team') {
            selectedEntries = [];
        }
        return {
            enabled: Boolean(input.enabled),
            mode,
            intervalMinutes,
            workspaceId,
            nextRunAt: Number.isFinite(nextRunAt) && nextRunAt > 0 ? nextRunAt : null,
            lastRunAt: Number.isFinite(lastRunAt) && lastRunAt > 0 ? lastRunAt : null,
            lastError,
            selectionType,
            selectedEntries,
            selectedMode,
            selectedWorkspaceId
        };
    }

    function loadAutoExportState() {
        try {
            const raw = localStorage.getItem(AUTO_EXPORT_STORAGE_KEY);
            if (!raw) {
                return normalizeAutoExportState();
            }
            const parsed = JSON.parse(raw);
            return normalizeAutoExportState(parsed);
        } catch (_) {
            return normalizeAutoExportState();
        }
    }

    function saveAutoExportState() {
        try {
            localStorage.setItem(AUTO_EXPORT_STORAGE_KEY, JSON.stringify(autoExportState));
        } catch (_) {}
    }

    function getIntervalExportState() {
        return {
            ...autoExportState,
            timerArmed: Boolean(autoExportTimer)
        };
    }

    function getAutoExportStatusText() {
        const errorHint = autoExportState.lastError
            ? ` 最近错误:${autoExportState.lastError}`
            : '';
        if (!autoExportState.enabled) {
            return `未启动定时导出。${errorHint}`;
        }
        const modeLabel = getModeLabel(autoExportState.mode);
        const selectionHint = autoExportState.selectionType === 'selected'
            ? `仅已选 ${autoExportState.selectedEntries.length} 条`
            : '全部对话';
        const workspaceHint = autoExportState.mode === 'team'
            ? `,Workspace: ${autoExportState.workspaceId || '未配置'}`
            : '';
        const nextRun = autoExportState.nextRunAt
            ? new Date(autoExportState.nextRunAt).toLocaleString()
            : '未知';
        const lastRun = autoExportState.lastRunAt
            ? new Date(autoExportState.lastRunAt).toLocaleString()
            : '暂无';
        return `运行中:每 ${autoExportState.intervalMinutes} 分钟导出一次(${modeLabel}${workspaceHint},${selectionHint})。下次:${nextRun},上次:${lastRun}。${errorHint}`;
    }

    function clearAutoExportTimer() {
        if (autoExportTimer) {
            clearTimeout(autoExportTimer);
            autoExportTimer = null;
        }
    }

    function scheduleNextAutoExport(delayMs = null) {
        clearAutoExportTimer();
        if (!autoExportState.enabled) return;
        const intervalMs = autoExportState.intervalMinutes * 60 * 1000;
        const waitMs = delayMs == null ? intervalMs : Math.max(1000, Math.floor(delayMs));
        autoExportState.nextRunAt = Date.now() + waitMs;
        saveAutoExportState();
        autoExportTimer = setTimeout(runAutoExportTick, waitMs);
    }

    async function runAutoExportTick(source = 'interval') {
        if (!autoExportState.enabled) return;

        if (autoExportTaskRunning || exportInProgress) {
            scheduleNextAutoExport(autoExportState.intervalMinutes * 60 * 1000);
            return;
        }

        autoExportTaskRunning = true;
        try {
            let workspaceId = null;
            if (autoExportState.mode === 'team') {
                workspaceId = autoExportState.workspaceId || resolveWorkspaceId(null) || '';
                if (!workspaceId) {
                    throw new Error('团队空间定时导出失败:缺少 Workspace ID。');
                }
                if (!autoExportState.workspaceId) {
                    autoExportState.workspaceId = workspaceId;
                }
            }

            let exported = false;
            if (autoExportState.selectionType === 'selected') {
                const selectedEntries = normalizeScheduleEntries(autoExportState.selectedEntries);
                if (selectedEntries.length === 0) {
                    throw new Error('定时导出已设置为“仅已选对话”,但未配置对话列表。');
                }
                exported = await exportConversations({
                    mode: autoExportState.mode,
                    workspaceId,
                    conversationEntries: selectedEntries,
                    exportType: 'selected',
                    notifyMode: SCHEDULE_NOTIFY_MODE
                });
            } else {
                exported = await startScheduledExport({
                    mode: autoExportState.mode,
                    workspaceId,
                    autoConfirm: true,
                    source,
                    notifyMode: SCHEDULE_NOTIFY_MODE
                });
            }
            if (exported) {
                autoExportState.lastRunAt = Date.now();
                autoExportState.lastError = '';
            } else {
                autoExportState.lastError = '任务未执行(可能被取消或并发导出跳过)。';
            }
        } catch (err) {
            autoExportState.lastError = err?.message || String(err);
            console.error('[ChatGPT Exporter] 定时导出执行失败:', err);
        } finally {
            autoExportTaskRunning = false;
            if (autoExportState.enabled) {
                scheduleNextAutoExport(autoExportState.intervalMinutes * 60 * 1000);
            }
            saveAutoExportState();
        }
    }

    function startIntervalExport(options = {}) {
        const runNow = Boolean(options.runNow);
        const merged = normalizeAutoExportState({
            ...autoExportState,
            ...options,
            enabled: true
        });

        if (merged.mode === 'team' && !merged.workspaceId) {
            const detectedIds = detectAllWorkspaceIds();
            if (detectedIds.length === 1) {
                merged.workspaceId = detectedIds[0];
            }
        }

        if (merged.mode === 'team' && !merged.workspaceId) {
            throw new Error('团队空间定时导出需要 Team Workspace ID。');
        }

        if (merged.selectionType === 'selected') {
            if (!Array.isArray(merged.selectedEntries) || merged.selectedEntries.length === 0) {
                throw new Error('定时导出选择了“仅已选对话”,但尚未选择任何对话。');
            }
            const expectedWorkspaceId = merged.mode === 'team' ? merged.workspaceId : '';
            const selectedWorkspaceId = merged.mode === 'team' ? (merged.selectedWorkspaceId || '') : '';
            if (merged.selectedMode !== merged.mode || selectedWorkspaceId !== expectedWorkspaceId) {
                throw new Error('已选定时对话与当前空间设置不匹配,请重新选择定时对话。');
            }
        }

        autoExportState = merged;
        autoExportState.lastError = '';
        scheduleNextAutoExport();
        if (runNow) {
            setTimeout(() => {
                runAutoExportTick('interval-start');
            }, 50);
        }
        console.log('[ChatGPT Exporter] 已启动定时导出:', getIntervalExportState());
        return getIntervalExportState();
    }

    function stopIntervalExport() {
        clearAutoExportTimer();
        autoExportState.enabled = false;
        autoExportState.nextRunAt = null;
        saveAutoExportState();
        console.log('[ChatGPT Exporter] 已停止定时导出。');
        return getIntervalExportState();
    }

    function resumeIntervalExportIfNeeded() {
        if (!autoExportState.enabled) return;
        const intervalMs = autoExportState.intervalMinutes * 60 * 1000;
        const remaining = autoExportState.nextRunAt
            ? autoExportState.nextRunAt - Date.now()
            : intervalMs;
        scheduleNextAutoExport(Math.max(1000, remaining));
    }

    function ensureGreatBallThemeStyles() {
        if (document.getElementById('chatgpt-exporter-greatball-theme')) return;
        const style = document.createElement('style');
        style.id = 'chatgpt-exporter-greatball-theme';
        style.textContent = `
            #export-dialog-overlay.cge-theme-greatball-overlay {
                backdrop-filter: blur(3px);
                background:
                    radial-gradient(circle at 12% 12%, rgba(112, 173, 255, 0.18), transparent 44%),
                    radial-gradient(circle at 88% 88%, rgba(105, 163, 255, 0.12), transparent 42%),
                    linear-gradient(180deg, rgba(5, 12, 24, 0.86), rgba(5, 12, 24, 0.74)) !important;
            }

            #export-dialog.cge-theme-greatball-dialog {
                position: relative;
                overflow: hidden;
                isolation: isolate;
                border: 4px solid #214b7e !important;
                border-radius: 16px !important;
                background: linear-gradient(180deg, #3f6ba3 0%, #325b90 48%, #2a4f80 100%) !important;
                box-shadow:
                    0 20px 42px rgba(0, 0, 0, 0.45),
                    inset 0 0 0 2px rgba(160, 204, 255, 0.72),
                    inset 0 1px 0 rgba(230, 243, 255, 0.2) !important;
                color: #edf6ff !important;
                font-family: "Trebuchet MS", "Verdana", "Noto Sans SC", sans-serif !important;
            }

            #export-dialog.cge-theme-greatball-dialog::before {
                content: "";
                position: absolute;
                inset: 10px;
                border: 1px solid rgba(169, 210, 255, 0.3);
                border-radius: 12px;
                pointer-events: none;
                z-index: 0;
            }

            #export-dialog.cge-theme-greatball-dialog > * {
                position: relative;
                z-index: 1;
            }

            #export-dialog.cge-theme-greatball-dialog h2 {
                margin: 0 0 16px 0 !important;
                padding: 10px 14px !important;
                border: 2px solid #7eb1ea !important;
                border-radius: 11px !important;
                background: linear-gradient(180deg, #496fa3 0%, #3a6093 100%) !important;
                color: #f7fbff !important;
                font-size: 18px !important;
                font-weight: 900 !important;
                text-shadow: 0 1px 0 rgba(10, 24, 42, 0.42);
                box-shadow: inset 0 1px 0 rgba(215, 235, 255, 0.28);
            }

            #export-dialog.cge-theme-greatball-dialog strong,
            #export-dialog.cge-theme-greatball-dialog code {
                color: #f5fbff !important;
            }

            #export-dialog.cge-theme-greatball-dialog [style*="color: #666"] {
                color: #cfe3ff !important;
            }

            #export-dialog.cge-theme-greatball-dialog code {
                background: rgba(17, 37, 64, 0.58) !important;
                border: 1px solid rgba(141, 181, 228, 0.42);
                border-radius: 6px;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-space-grid {
                display: grid;
                gap: 16px;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-space-card {
                border: 2px solid #5486be !important;
                border-radius: 14px !important;
                background: linear-gradient(180deg, rgba(21, 46, 80, 0.92), rgba(18, 40, 69, 0.96)) !important;
                box-shadow:
                    inset 0 0 0 2px rgba(12, 31, 54, 0.86),
                    inset 0 1px 0 rgba(179, 211, 250, 0.16) !important;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-space-card p {
                color: #d2e5ff !important;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-space-actions {
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-team-footer {
                display: flex;
                justify-content: space-between;
                align-items: center;
                gap: 10px;
                flex-wrap: wrap;
            }

            #export-dialog.cge-theme-greatball-dialog button {
                border: 2px solid #5180b3 !important;
                border-radius: 11px !important;
                background: linear-gradient(180deg, #f4fbff 0%, #d9e9fb 100%) !important;
                color: #16385d !important;
                font-weight: 900 !important;
                box-shadow:
                    0 2px 0 #2f517a,
                    inset 0 1px 0 rgba(255, 255, 255, 0.75) !important;
                transition: transform 0.12s ease, filter 0.12s ease, box-shadow 0.12s ease;
            }

            #export-dialog.cge-theme-greatball-dialog button:hover:not(:disabled) {
                transform: translateY(-1px);
                filter: saturate(1.06);
            }

            #export-dialog.cge-theme-greatball-dialog button:active:not(:disabled) {
                transform: translateY(1px);
            }

            #export-dialog.cge-theme-greatball-dialog button:disabled {
                opacity: 0.62;
                cursor: not-allowed;
            }

            #export-dialog.cge-theme-greatball-dialog #select-team-btn,
            #export-dialog.cge-theme-greatball-dialog #start-team-export-btn,
            #export-dialog.cge-theme-greatball-dialog #schedule-start-btn,
            #export-dialog.cge-theme-greatball-dialog #schedule-pick-btn,
            #export-dialog.cge-theme-greatball-dialog #schedule-run-now-btn,
            #export-dialog.cge-theme-greatball-dialog #export-selected-btn {
                border-color: #1a4f80 !important;
                background: linear-gradient(180deg, #6cdaff 0%, #308fd5 100%) !important;
                color: #f7fcff !important;
            }

            #export-dialog.cge-theme-greatball-dialog input,
            #export-dialog.cge-theme-greatball-dialog select {
                border: 2px solid #6e9acd !important;
                border-radius: 10px !important;
                background: linear-gradient(180deg, #f7fcff, #e2efff) !important;
                color: #123252 !important;
                font-weight: 700;
            }

            #export-dialog.cge-theme-greatball-dialog input:focus,
            #export-dialog.cge-theme-greatball-dialog select:focus {
                outline: none;
                border-color: #8db8eb !important;
                box-shadow: 0 0 0 3px rgba(120, 170, 230, 0.25) !important;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-picker-row {
                display: flex;
                gap: 8px;
                margin-bottom: 8px;
                align-items: center;
                flex-wrap: wrap;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-picker-row-search input {
                flex: 1 1 260px;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-picker-row-search select {
                flex: 0 1 auto;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-picker-row-filter select,
            #export-dialog.cge-theme-greatball-dialog .cge-picker-row-filter input,
            #export-dialog.cge-theme-greatball-dialog .cge-picker-row-filter button {
                flex: 1 1 140px;
                min-width: 120px;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-picker-footer {
                display: flex;
                justify-content: space-between;
                align-items: center;
                gap: 10px;
                flex-wrap: wrap;
                margin-top: 16px;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-picker-footer-left,
            #export-dialog.cge-theme-greatball-dialog .cge-picker-footer-right {
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
            }

            #export-dialog.cge-theme-greatball-dialog #conv-list {
                border: 2px solid #5d8abd !important;
                border-radius: 10px !important;
                background: linear-gradient(180deg, rgba(225, 238, 255, 0.95), rgba(204, 224, 249, 0.92)) !important;
            }

            #export-dialog.cge-theme-greatball-dialog #conv-list label {
                border: 1px solid rgba(56, 98, 146, 0.38) !important;
                border-radius: 10px !important;
                background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(231, 242, 255, 0.92)) !important;
            }

            #export-dialog.cge-theme-greatball-dialog .cge-tag-project {
                background: #d8ecff !important;
                color: #0f4c84 !important;
                border: 1px solid rgba(15, 76, 132, 0.3);
            }

            #export-dialog.cge-theme-greatball-dialog .cge-tag-archived {
                background: #ffe6cf !important;
                color: #8f520f !important;
                border: 1px solid rgba(143, 82, 15, 0.3);
            }

            #export-dialog.cge-theme-greatball-dialog[data-cge-dialog="picker"] #conv-list {
                max-height: min(48vh, 420px) !important;
            }

            .cge-ball-btn {
                position: relative !important;
                border: none !important;
                border-radius: 50% !important;
                overflow: hidden !important;
                cursor: pointer !important;
                user-select: none !important;
                transition: transform 0.12s ease, filter 0.12s ease, box-shadow 0.12s ease;
            }

            .cge-ball-btn::before {
                content: "";
                position: absolute;
                left: 0;
                right: 0;
                top: 50%;
                height: 4px;
                margin-top: -2px;
                background: #0f1f35;
                z-index: 2;
            }

            .cge-ball-btn::after {
                content: "";
                position: absolute;
                width: 16px;
                height: 16px;
                left: 50%;
                top: 50%;
                margin: -8px 0 0 -8px;
                border-radius: 50%;
                background: radial-gradient(circle at 32% 28%, #fff, #d8e1ee 62%, #9aa8bb 100%);
                border: 2px solid #1b2940;
                z-index: 3;
            }

            .cge-ball-btn .cge-ball-label {
                position: absolute;
                left: 50%;
                top: 73%;
                transform: translate(-50%, -50%);
                z-index: 4;
                color: #0d2a46;
                font-weight: 900;
                font-size: 13px;
                line-height: 1;
                text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
                pointer-events: none;
                white-space: nowrap;
            }

            #gpt-rescue-btn.cge-theme-greatball-fab,
            #gpt-rescue-btn.cge-ball-great {
                position: fixed !important;
                right: 18px !important;
                bottom: 122px !important;
                width: 56px !important;
                height: 56px !important;
                padding: 0 !important;
                border: 2px solid #11406e !important;
                background:
                    radial-gradient(circle at 24% 24%, #ff6a70 0 8px, transparent 9px),
                    radial-gradient(circle at 76% 24%, #ff6a70 0 8px, transparent 9px),
                    radial-gradient(circle at 30% 14%, rgba(255, 255, 255, 0.34) 0 10px, transparent 11px),
                    linear-gradient(180deg, #4cb9ff 0 50%, #f6faff 50% 100%) !important;
                box-shadow:
                    0 8px 16px rgba(0, 0, 0, 0.32),
                    inset 0 1px 0 rgba(255, 255, 255, 0.7) !important;
                z-index: 99997 !important;
            }

            #gpt-rescue-btn.cge-theme-greatball-fab[data-export-state="busy"] {
                filter: saturate(1.12) brightness(1.02);
            }

            #gpt-rescue-btn.cge-theme-greatball-fab[data-export-state="done"] {
                background: linear-gradient(180deg, #5dd9ad, #2f9d76) !important;
            }

            #gpt-rescue-btn.cge-theme-greatball-fab[data-export-state="error"] {
                background: linear-gradient(180deg, #ff8f98, #dc4a57) !important;
            }

            @media (max-width: 900px) {
                #export-dialog.cge-theme-greatball-dialog {
                    padding: 16px !important;
                }

                #gpt-rescue-btn.cge-theme-greatball-fab,
                #gpt-rescue-btn.cge-ball-great {
                    right: 10px !important;
                    bottom: 108px !important;
                    width: 52px !important;
                    height: 52px !important;
                }

                .cge-ball-btn .cge-ball-label {
                    font-size: 12px;
                }
            }
        `;
        document.head.appendChild(style);
    }

    /**
     * [新增] 从Cookie中获取 oai-device-id
     * @returns {string|null} - 返回设备ID或null
     */
    function getOaiDeviceId() {
        const cookieString = document.cookie;
        const match = cookieString.match(/oai-did=([^;]+)/);
        return match ? match[1] : null;
    }

    function generateUniqueFilename(convData) {
        const convId = convData.conversation_id || '';
        const shortId = convId.includes('-') ? convId.split('-').pop() : (convId || Date.now().toString(36));
        let baseName = convData.title;
        if (!baseName || baseName.trim().toLowerCase() === 'new chat') {
            baseName = 'Untitled Conversation';
        }
        return `${sanitizeFilename(baseName)}_${shortId}.json`;
    }

    function generateMarkdownFilename(convData) {
        const jsonName = generateUniqueFilename(convData);
        return jsonName.endsWith('.json')
            ? `${jsonName.slice(0, -5)}.md`
            : `${jsonName}.md`;
    }

    function cleanMessageContent(text) {
        if (!text) return '';
        return text
            .replace(/\uE200cite(?:\uE202turn\d+(?:search|view)\d+)+\uE201/gi, '')
            .replace(/cite(?:turn\d+(?:search|view)\d+)+/gi, '')
            .trim();
    }

    function processContentReferences(text, contentReferences) {
        if (!text || !Array.isArray(contentReferences) || contentReferences.length === 0) {
            return { text, footnotes: [] };
        }

        const references = contentReferences.filter(ref => ref && typeof ref.matched_text === 'string' && ref.matched_text.length > 0);
        if (references.length === 0) {
            return { text, footnotes: [] };
        }

        const getReferenceInfo = (ref) => {
            const item = Array.isArray(ref.items) ? ref.items[0] : null;
            const url = item?.url || (Array.isArray(ref.safe_urls) ? ref.safe_urls[0] : '') || '';
            const title = item?.title || '';
            let label = item?.attribution || '';
            if (!label && typeof ref.alt === 'string') {
                const match = ref.alt.match(/\[([^\]]+)\]\([^)]+\)/);
                if (match) label = match[1];
            }
            if (!label) label = title || url;
            return { url, title, label };
        };

        const footnotes = [];
        const footnoteIndexByKey = new Map();
        const citationRefs = references
            .filter(ref => ref.type === 'grouped_webpages')
            .sort((a, b) => {
                const aIdx = Number.isFinite(a.start_idx) ? a.start_idx : Number.MAX_SAFE_INTEGER;
                const bIdx = Number.isFinite(b.start_idx) ? b.start_idx : Number.MAX_SAFE_INTEGER;
                return aIdx - bIdx;
            });

        citationRefs.forEach(ref => {
            const info = getReferenceInfo(ref);
            if (!info.url) return;
            const key = `${info.url}|${info.title}`;
            if (footnoteIndexByKey.has(key)) return;
            const index = footnotes.length + 1;
            footnoteIndexByKey.set(key, index);
            footnotes.push({ index, url: info.url, title: info.title, label: info.label });
        });

        const sortedByReplacement = references
            .slice()
            .sort((a, b) => {
                const aIdx = Number.isFinite(a.start_idx) ? a.start_idx : -1;
                const bIdx = Number.isFinite(b.start_idx) ? b.start_idx : -1;
                if (aIdx !== -1 || bIdx !== -1) {
                    return bIdx - aIdx;
                }
                return (b.matched_text?.length || 0) - (a.matched_text?.length || 0);
            });

        let output = text;
        sortedByReplacement.forEach(ref => {
            if (!ref?.matched_text || ref.type === 'sources_footnote') return;
            let replacement = '';
            if (ref.type === 'grouped_webpages') {
                const info = getReferenceInfo(ref);
                if (info.url) {
                    const key = `${info.url}|${info.title}`;
                    const index = footnoteIndexByKey.get(key);
                    replacement = index ? `([${info.label}][${index}])` : (ref.alt || '');
                } else {
                    replacement = ref.alt || '';
                }
            } else {
                replacement = ref.alt || '';
            }

            if (Number.isFinite(ref.start_idx) && Number.isFinite(ref.end_idx)) {
                if (output.slice(ref.start_idx, ref.end_idx) === ref.matched_text) {
                    output = output.slice(0, ref.start_idx) + replacement + output.slice(ref.end_idx);
                    return;
                }
            }
            output = output.split(ref.matched_text).join(replacement);
        });

        return { text: output, footnotes };
    }

    function extractConversationMessages(convData) {
        const mapping = convData?.mapping;
        if (!mapping) return [];

        const messages = [];
        const mappingKeys = Object.keys(mapping);
        const rootId = mapping['client-created-root']
            ? 'client-created-root'
            : mappingKeys.find(id => !mapping[id]?.parent) || mappingKeys[0];
        const visited = new Set();

        const traverse = (nodeId) => {
            if (!nodeId || visited.has(nodeId)) return;
            visited.add(nodeId);
            const node = mapping[nodeId];
            if (!node) return;

            const msg = node.message;
            if (msg) {
                const author = msg.author?.role;
                const isHidden = msg.metadata?.is_visually_hidden_from_conversation ||
                    msg.metadata?.is_contextual_answers_system_message;
                if (author && author !== 'system' && !isHidden) {
                    const content = msg.content;
                    if (content?.content_type === 'text' && Array.isArray(content.parts)) {
                        const rawText = content.parts
                            .map(part => typeof part === 'string' ? part : (part?.text ?? ''))
                            .filter(Boolean)
                            .join('\n');
                        const contentReferences = msg.metadata?.content_references || [];
                        let processedText = rawText;
                        let footnotes = [];
                        if (Array.isArray(contentReferences) && contentReferences.length > 0) {
                            const processed = processContentReferences(rawText, contentReferences);
                            processedText = processed.text;
                            footnotes = processed.footnotes;
                        }
                        const cleaned = cleanMessageContent(processedText);
                        if (cleaned) {
                            messages.push({
                                role: author,
                                content: cleaned,
                                create_time: msg.create_time || null,
                                footnotes
                            });
                        }
                    }
                }
            }

            if (Array.isArray(node.children)) {
                node.children.forEach(childId => traverse(childId));
            }
        };

        if (rootId) {
            traverse(rootId);
        } else {
            mappingKeys.forEach(traverse);
        }

        return messages;
    }

    function convertConversationToMarkdown(convData) {
        const messages = extractConversationMessages(convData);
        if (messages.length === 0) {
            return '# Conversation\nNo visible user or assistant messages were exported.\n';
        }

        const mdLines = [];
        messages.forEach(msg => {
            const roleLabel = msg.role === 'user' ? '# User' : '# Assistant';
            mdLines.push(roleLabel);
            mdLines.push(msg.content);
            if (Array.isArray(msg.footnotes) && msg.footnotes.length > 0) {
                mdLines.push('');
                msg.footnotes
                    .slice()
                    .sort((a, b) => a.index - b.index)
                    .forEach(note => {
                        if (!note.url) return;
                        const title = note.title ? ` "${note.title}"` : '';
                        mdLines.push(`[${note.index}]: ${note.url}${title}`);
                    });
            }
            mdLines.push('');
        });

        return mdLines.join('\n').trim() + '\n';
    }

    function downloadFile(blob, filename) {
        const a = document.createElement('a');
        const blobUrl = URL.createObjectURL(blob);
        a.href = blobUrl;
        a.download = filename;
        a.rel = 'noopener';
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        setTimeout(() => {
            if (a.parentNode) {
                a.parentNode.removeChild(a);
            }
            URL.revokeObjectURL(blobUrl);
        }, 60000);
    }

    async function saveExportArchive(blob, filename, options = {}) {
        const { notifyMode = DEFAULT_NOTIFY_MODE } = options;
        const fallbackToBrowser = (message, switchMode = false) => {
            if (switchMode) {
                downloadSettings.mode = 'browser';
                saveDownloadSettings();
            }
            if (message) {
                notifyUser(message, { mode: notifyMode, level: 'info', duration: 3200 });
            }
            downloadFile(blob, filename);
        };

        if (downloadSettings.mode !== 'file-handle') {
            downloadFile(blob, filename);
            return;
        }

        if (!supportsFixedFileOverwrite()) {
            fallbackToBrowser('固定覆盖模式不可用,已回退为普通下载。', true);
            return;
        }

        const handle = await getStoredZipFileHandle();
        if (!handle) {
            fallbackToBrowser('未找到固定保存文件,已回退为普通下载。请在“文件命名”里重新选择。', true);
            return;
        }

        const granted = await ensureFileHandlePermission(handle, true);
        if (!granted) {
            fallbackToBrowser('固定文件写入权限不可用,本次已回退为普通下载。');
            return;
        }

        try {
            const writable = await handle.createWritable();
            await writable.write(blob);
            await writable.close();
            if (handle.name && downloadSettings.handleName !== handle.name) {
                downloadSettings.handleName = handle.name;
                saveDownloadSettings();
            }
        } catch (err) {
            const errName = err?.name || '';
            if (errName === 'NotFoundError' || errName === 'InvalidStateError') {
                await clearStoredZipFileHandle();
                downloadSettings.handleName = '';
                saveDownloadSettings();
                fallbackToBrowser('固定保存文件已失效,已回退为普通下载。请重新选择固定文件。', true);
                return;
            }
            fallbackToBrowser(
                `固定覆盖写入失败(${err?.message || errName || '未知错误'}),已回退为普通下载。`,
                false
            );
            return;
        }
    }

    // --- 导出流程核心逻辑 ---
    function getExportButton() {
        let btn = document.getElementById('gpt-rescue-btn');
        if (!btn) {
            addBtn();
            btn = document.getElementById('gpt-rescue-btn');
        }
        return btn;
    }

    function setExportButtonState(btn, options = {}) {
        if (!btn) return;
        const {
            label = '导出',
            title = '打开导出面板',
            state = 'idle'
        } = options;
        const labelEl = btn.querySelector('.cge-ball-label');
        if (labelEl) {
            labelEl.textContent = label;
        } else {
            btn.textContent = label;
        }
        btn.title = title;
        btn.setAttribute('aria-label', title);
        btn.dataset.exportState = state;
    }

    async function exportConversations(options = {}) {
        const {
            mode = 'personal',
            workspaceId = null,
            conversationEntries = null,
            exportType = null,
            notifyMode = DEFAULT_NOTIFY_MODE
        } = options;
        if (exportInProgress) {
            console.warn('[ChatGPT Exporter] 已有导出任务进行中,本次请求已跳过。');
            notifyUser('已有导出任务进行中,本次请求已跳过。', { mode: notifyMode, level: 'info' });
            return false;
        }
        exportInProgress = true;
        let exportSucceeded = false;

        const btn = getExportButton();
        btn.disabled = true;
        setExportButtonState(btn, { label: '准备', title: '正在准备导出...', state: 'busy' });

        try {
            if (!await ensureAccessToken({ notifyMode })) {
                btn.disabled = false;
                setExportButtonState(btn, { label: '导出', title: '打开导出面板', state: 'idle' });
                return false;
            }

            const zip = new JSZip();
            if (Array.isArray(conversationEntries) && conversationEntries.length > 0) {
                for (let i = 0; i < conversationEntries.length; i++) {
                    const entry = conversationEntries[i];
                    const label = entry?.title ? entry.title.slice(0, 12) : '对话';
                    setExportButtonState(btn, {
                        label: '抓取',
                        title: `正在导出: ${label} (${i + 1}/${conversationEntries.length})`,
                        state: 'busy'
                    });
                    const convData = await getConversation(entry.id, workspaceId);
                    const target = entry?.projectTitle
                        ? zip.folder(sanitizeFilename(entry.projectTitle))
                        : zip;
                    target.file(generateUniqueFilename(convData), JSON.stringify(convData, null, 2));
                    target.file(generateMarkdownFilename(convData), convertConversationToMarkdown(convData));
                    await sleep(jitter());
                }
            } else {
                setExportButtonState(btn, {
                    label: '扫描',
                    title: '正在获取项目外对话...',
                    state: 'busy'
                });
                const orphanIds = await collectIds(btn, workspaceId, null);
                for (let i = 0; i < orphanIds.length; i++) {
                    setExportButtonState(btn, {
                        label: '抓取',
                        title: `根目录对话 (${i + 1}/${orphanIds.length})`,
                        state: 'busy'
                    });
                    const convData = await getConversation(orphanIds[i], workspaceId);
                    zip.file(generateUniqueFilename(convData), JSON.stringify(convData, null, 2));
                    zip.file(generateMarkdownFilename(convData), convertConversationToMarkdown(convData));
                    await sleep(jitter());
                }

                setExportButtonState(btn, {
                    label: '项目',
                    title: '正在获取项目列表...',
                    state: 'busy'
                });
                const projects = await getProjects(workspaceId);
                for (const project of projects) {
                    const projectFolder = zip.folder(sanitizeFilename(project.title));
                    setExportButtonState(btn, {
                        label: '项目',
                        title: `项目: ${project.title}`,
                        state: 'busy'
                    });
                    const projectConvIds = await collectIds(btn, workspaceId, project.id);
                    if (projectConvIds.length === 0) continue;

                    for (let i = 0; i < projectConvIds.length; i++) {
                        setExportButtonState(btn, {
                            label: '抓取',
                            title: `${project.title.substring(0, 12)} (${i + 1}/${projectConvIds.length})`,
                            state: 'busy'
                        });
                        const convData = await getConversation(projectConvIds[i], workspaceId);
                        projectFolder.file(generateUniqueFilename(convData), JSON.stringify(convData, null, 2));
                        projectFolder.file(generateMarkdownFilename(convData), convertConversationToMarkdown(convData));
                        await sleep(jitter());
                    }
                }
            }

            setExportButtonState(btn, {
                label: '打包',
                title: '正在生成 ZIP 文件...',
                state: 'busy'
            });
            const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE" });
            const selectionType = exportType || ((Array.isArray(conversationEntries) && conversationEntries.length > 0) ? 'selected' : 'full');
            const filename = buildExportZipFilename(mode, selectionType, workspaceId);
            await saveExportArchive(blob, filename, { notifyMode });
            notifyUser('导出完成。', { mode: notifyMode, level: 'success' });
            setExportButtonState(btn, { label: '完成', title: '导出完成', state: 'done' });
            exportSucceeded = true;

        } catch (e) {
            console.error('导出过程中发生严重错误:', e);
            notifyUser(`导出失败: ${e.message}。详情请查看控制台(F12 -> Console)。`, {
                mode: notifyMode,
                level: 'error',
                duration: 3800
            });
            setExportButtonState(btn, { label: '错误', title: `导出失败: ${e.message}`, state: 'error' });
        } finally {
            exportInProgress = false;
            setTimeout(() => {
                btn.disabled = false;
                setExportButtonState(btn, { label: '导出', title: '打开导出面板', state: 'idle' });
            }, 2500);
        }
        return exportSucceeded;
    }

    async function startExportProcess(mode, workspaceId, options = {}) {
        const normalizedMode = normalizeExportMode(mode);
        const resolvedWorkspaceId = normalizedMode === 'team'
            ? (workspaceId || resolveWorkspaceId(null) || '')
            : null;
        if (normalizedMode === 'team' && !resolvedWorkspaceId) {
            throw new Error('团队空间导出需要 Team Workspace ID。');
        }
        return await exportConversations({
            mode: normalizedMode,
            workspaceId: resolvedWorkspaceId,
            ...options
        });
    }

    async function startProjectSpaceExportProcess(workspaceId = null, options = {}) {
        const { notifyMode = DEFAULT_NOTIFY_MODE } = options;
        try {
            const projectEntries = await listProjectSpaceConversations(workspaceId);
            if (projectEntries.length === 0) {
                notifyUser('未找到项目空间对话。', { mode: notifyMode, level: 'info' });
                return false;
            }
            return await exportConversations({
                mode: 'project',
                workspaceId,
                conversationEntries: projectEntries,
                exportType: 'full',
                notifyMode
            });
        } catch (err) {
            console.error('导出项目空间失败:', err);
            notifyUser(`导出项目空间失败: ${err.message}`, {
                mode: notifyMode,
                level: 'error',
                duration: 3800
            });
            return false;
        }
    }

    async function startSelectiveExportProcess(mode, workspaceId, conversationEntries, options = {}) {
        return await exportConversations({ mode, workspaceId, conversationEntries, ...options });
    }

    function startScheduledExport(options = {}) {
        const {
            mode = 'personal',
            workspaceId = null,
            autoConfirm = false,
            source = 'schedule',
            notifyMode = autoConfirm ? SCHEDULE_NOTIFY_MODE : DEFAULT_NOTIFY_MODE
        } = options;
        const normalizedMode = normalizeExportMode(mode);
        const proceed = async () => {
            if (normalizedMode === 'project') {
                return await startProjectSpaceExportProcess(workspaceId, { notifyMode });
            }
            return await startExportProcess(normalizedMode, workspaceId, { notifyMode });
        };

        if (autoConfirm) {
            return proceed();
        }

        const modeLabel = normalizedMode === 'team' ? '团队空间' : normalizedMode === 'project' ? '项目空间' : '个人空间';
        if (confirm(`Chrome 扩展请求导出 ${modeLabel} 对话(来源: ${source})。是否开始?`)) {
            return proceed();
        }
        return Promise.resolve(false);
    }

    // --- API 调用函数 ---
    async function getProjects(workspaceId) {
        if (!workspaceId) return [];
        const deviceId = getOaiDeviceId();
        if (!deviceId) {
            throw new Error('无法获取 oai-device-id,请确保已登录并刷新页面。');
        }
        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            'ChatGPT-Account-Id': workspaceId,
            'oai-device-id': deviceId
        };
        const r = await fetch(`/backend-api/gizmos/snorlax/sidebar`, { headers });
        if (!r.ok) {
            console.warn(`获取项目(Gizmo)列表失败 (${r.status})`);
            return [];
        }
        const data = await r.json();
        const projects = [];
        data.items?.forEach(item => {
            if (item?.gizmo?.id && item?.gizmo?.display?.name) {
                projects.push({ id: item.gizmo.id, title: item.gizmo.display.name });
            }
        });
        return projects;
    }

    function resolveWorkspaceId(workspaceId) {
        if (workspaceId) return workspaceId;
        const match = document.cookie.match(/(?:^|; )_account=([^;]+)/);
        if (match?.[1]) return match[1];
        const detectedIds = detectAllWorkspaceIds();
        return detectedIds.length > 0 ? detectedIds[0] : null;
    }

    async function getProjectSpaces(workspaceId, options = {}) {
        const deviceId = getOaiDeviceId();
        if (!deviceId) {
            throw new Error('无法获取 oai-device-id,请确保已登录并刷新页面。');
        }
        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            'oai-device-id': deviceId
        };
        const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
        if (resolvedWorkspaceId) { headers['ChatGPT-Account-Id'] = resolvedWorkspaceId; }

        const query = new URLSearchParams();
        if (options.conversationsPerGizmo !== undefined) {
            query.set('conversations_per_gizmo', String(options.conversationsPerGizmo));
        }
        if (options.ownedOnly !== undefined) {
            query.set('owned_only', options.ownedOnly ? 'true' : 'false');
        }
        const url = query.toString()
            ? `/backend-api/gizmos/snorlax/sidebar?${query.toString()}`
            : '/backend-api/gizmos/snorlax/sidebar';

        const r = await fetch(url, { headers });
        if (!r.ok) {
            throw new Error(`获取项目空间列表失败 (${r.status})`);
        }
        const data = await r.json();
        const projects = [];
        data.items?.forEach(item => {
            const rawGizmo = item?.gizmo?.gizmo || item?.gizmo || item;
            const display = rawGizmo?.display || item?.gizmo?.display || item?.display;
            const id = rawGizmo?.id || item?.gizmo?.id || item?.id;
            const title = display?.name || rawGizmo?.name || 'Untitled Project';
            if (!id) return;
            projects.push({
                id,
                title,
                conversations: item?.conversations?.items || []
            });
        });
        return projects;
    }

    async function collectIds(btn, workspaceId, gizmoId) {
        const all = new Set();
        const deviceId = getOaiDeviceId();
        if (!deviceId) {
            throw new Error('无法获取 oai-device-id,请确保已登录并刷新页面。');
        }
        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            'oai-device-id': deviceId
        };
        if (workspaceId) { headers['ChatGPT-Account-Id'] = workspaceId; }

        if (gizmoId) {
            let cursor = '0';
            do {
                const r = await fetch(`/backend-api/gizmos/${gizmoId}/conversations?cursor=${cursor}`, { headers });
                if (!r.ok) throw new Error(`列举项目对话列表失败 (${r.status})`);
                const j = await r.json();
                j.items?.forEach(it => all.add(it.id));
                cursor = j.cursor;
                await sleep(jitter());
            } while (cursor);
        } else {
            for (const is_archived of [false, true]) {
                let offset = 0, has_more = true, page = 0;
                do {
                    setExportButtonState(btn, {
                        label: '扫描',
                        title: `项目外对话 (${is_archived ? 'Archived' : 'Active'} p${++page})`,
                        state: 'busy'
                    });
                    const r = await fetch(`/backend-api/conversations?offset=${offset}&limit=${PAGE_LIMIT}&order=updated${is_archived ? '&is_archived=true' : ''}`, { headers });
                    if (!r.ok) throw new Error(`列举项目外对话列表失败 (${r.status})`);
                    const j = await r.json();
                    if (j.items && j.items.length > 0) {
                        j.items.forEach(it => all.add(it.id));
                        has_more = j.items.length === PAGE_LIMIT;
                        offset += j.items.length;
                    } else {
                        has_more = false;
                    }
                    await sleep(jitter());
                } while (has_more);
            }
        }
        return Array.from(all);
    }

    function upsertConversationEntry(map, item, extra = {}) {
        if (!item?.id) return;
        const create_time = normalizeEpochSeconds(item.create_time || 0);
        const update_time = normalizeEpochSeconds(item.update_time || item.create_time || 0);
        const entry = {
            id: item.id,
            title: item.title || 'Untitled Conversation',
            create_time,
            update_time,
            is_archived: item.is_archived ?? extra.is_archived ?? false,
            projectId: extra.projectId || null,
            projectTitle: extra.projectTitle || null
        };
        const existing = map.get(entry.id);
        if (!existing) {
            map.set(entry.id, entry);
            return;
        }
        if (!existing.projectTitle && entry.projectTitle) {
            existing.projectTitle = entry.projectTitle;
            existing.projectId = entry.projectId;
        }
        if (!existing.create_time && entry.create_time) {
            existing.create_time = entry.create_time;
        }
        existing.is_archived = existing.is_archived || entry.is_archived;
        if ((entry.update_time || 0) > (existing.update_time || 0)) {
            existing.update_time = entry.update_time;
        }
        if (existing.title === 'Untitled Conversation' && entry.title) {
            existing.title = entry.title;
        }
    }

    async function listConversations(workspaceId) {
        if (!await ensureAccessToken()) {
            throw new Error('无法获取 Access Token,请刷新页面或打开任意一个对话后再试。');
        }

        const deviceId = getOaiDeviceId();
        if (!deviceId) {
            throw new Error('无法获取 oai-device-id,请确保已登录并刷新页面。');
        }

        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            'oai-device-id': deviceId
        };
        if (workspaceId) { headers['ChatGPT-Account-Id'] = workspaceId; }

        const map = new Map();
        const addEntry = (item, extra = {}) => upsertConversationEntry(map, item, extra);

        for (const is_archived of [false, true]) {
            let offset = 0;
            let has_more = true;
            do {
                const r = await fetch(`/backend-api/conversations?offset=${offset}&limit=${PAGE_LIMIT}&order=updated${is_archived ? '&is_archived=true' : ''}`, { headers });
                if (!r.ok) throw new Error(`列举对话列表失败 (${r.status})`);
                const j = await r.json();
                if (j.items && j.items.length > 0) {
                    j.items.forEach(it => addEntry(it, { is_archived }));
                    has_more = j.items.length === PAGE_LIMIT;
                    offset += j.items.length;
                } else {
                    has_more = false;
                }
                await sleep(jitter());
            } while (has_more);
        }

        if (workspaceId) {
            const projects = await getProjects(workspaceId);
            for (const project of projects) {
                let cursor = '0';
                do {
                    const r = await fetch(`/backend-api/gizmos/${project.id}/conversations?cursor=${cursor}`, { headers });
                    if (!r.ok) throw new Error(`列举项目对话列表失败 (${r.status})`);
                    const j = await r.json();
                    j.items?.forEach(it => addEntry(it, { projectId: project.id, projectTitle: project.title }));
                    cursor = j.cursor;
                    await sleep(jitter());
                } while (cursor);
            }
        }

        return Array.from(map.values())
            .sort((a, b) => (b.update_time || 0) - (a.update_time || 0));
    }

    async function listProjectSpaceConversations(workspaceId) {
        if (!await ensureAccessToken()) {
            throw new Error('无法获取 Access Token,请刷新页面或打开任意一个对话后再试。');
        }

        const deviceId = getOaiDeviceId();
        if (!deviceId) {
            throw new Error('无法获取 oai-device-id,请确保已登录并刷新页面。');
        }

        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            'oai-device-id': deviceId
        };
        const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
        if (resolvedWorkspaceId) { headers['ChatGPT-Account-Id'] = resolvedWorkspaceId; }

        const map = new Map();
        const projects = await getProjectSpaces(resolvedWorkspaceId, { conversationsPerGizmo: PROJECT_SIDEBAR_PREVIEW, ownedOnly: true });

        for (const project of projects) {
            let cursor = '0';
            let fetched = false;
            do {
                const r = await fetch(`/backend-api/gizmos/${project.id}/conversations?cursor=${cursor}`, { headers });
                if (!r.ok) {
                    if (!fetched && Array.isArray(project.conversations) && project.conversations.length > 0) {
                        console.warn(`项目空间对话列表请求失败 (${r.status}),使用侧边栏返回的预览对话。`);
                        project.conversations.forEach(item => upsertConversationEntry(map, item, {
                            projectId: project.id,
                            projectTitle: project.title
                        }));
                        cursor = null;
                        break;
                    }
                    throw new Error(`列举项目空间对话列表失败 (${r.status})`);
                }
                const j = await r.json();
                j.items?.forEach(item => upsertConversationEntry(map, item, {
                    projectId: project.id,
                    projectTitle: project.title
                }));
                cursor = j.cursor;
                fetched = true;
                await sleep(jitter());
            } while (cursor);
        }

        return Array.from(map.values())
            .sort((a, b) => (b.update_time || 0) - (a.update_time || 0));
    }

    async function getConversation(id, workspaceId) {
        const deviceId = getOaiDeviceId();
        if (!deviceId) {
            throw new Error('无法获取 oai-device-id,请确保已登录并刷新页面。');
        }
        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            'oai-device-id': deviceId
        };
        if (workspaceId) { headers['ChatGPT-Account-Id'] = workspaceId; }
        const r = await fetch(`/backend-api/conversation/${id}`, { headers });
        if (!r.ok) throw new Error(`获取对话详情失败 conv ${id} (${r.status})`);
        const j = await r.json();
        j.__fetched_at = new Date().toISOString();
        return j;
    }

    // --- UI 相关函数 ---
    // (UI部分无变动,此处省略以保持简洁)
    /**
     * [新增] 全面检测函数,返回所有找到的ID
     * @returns {string[]} - 返回包含所有唯一Workspace ID的数组
     */
    function detectAllWorkspaceIds() {
        const foundIds = new Set(capturedWorkspaceIds); // 从网络拦截的结果开始
        // 扫描 __NEXT_DATA__
        try {
            const data = JSON.parse(document.getElementById('__NEXT_DATA__').textContent);
            // 遍历所有账户信息
            const accounts = data?.props?.pageProps?.user?.accounts;
            if (accounts) {
                Object.values(accounts).forEach(acc => {
                    if (acc?.account?.id) {
                        foundIds.add(acc.account.id);
                    }
                });
            }
        } catch (e) {}

        // 扫描 localStorage
        try {
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                if (key && (key.includes('account') || key.includes('workspace'))) {
                    const value = localStorage.getItem(key);
                    if (value && /^[a-z0-9]{2,}-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value.replace(/"/g, ''))) {
                         const extractedId = value.match(/ws-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i);
                         if(extractedId) foundIds.add(extractedId[0]);
                    } else if (value && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value.replace(/"/g, ''))) {
                         foundIds.add(value.replace(/"/g, ''));
                    }
                }
            }
        } catch(e) {}

        console.log('🔍 检测到以下 Workspace IDs:', Array.from(foundIds));
        return Array.from(foundIds);
    }

    function showConversationPicker(options = {}) {
        const {
            mode: requestedMode = 'personal',
            workspaceId = null,
            action = 'export',
            initialSelectedIds = [],
            onConfirm = null,
            onBack = null
        } = options;
        const mode = normalizeExportMode(requestedMode);
        const isScheduleAction = action === 'schedule';
        const selectedActionLabel = isScheduleAction ? '保存定时选择' : '导出选中';
        ensureGreatBallThemeStyles();
        const existing = document.getElementById('export-dialog-overlay');
        if (existing) existing.remove();

        const overlay = document.createElement('div');
        overlay.id = 'export-dialog-overlay';
        overlay.classList.add('cge-theme-greatball-overlay');
        Object.assign(overlay.style, {
            position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
            backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: '99998',
            display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
            padding: '5vh 16px 3vh', boxSizing: 'border-box', overflowY: 'auto'
        });

        const dialog = document.createElement('div');
        dialog.id = 'export-dialog';
        dialog.classList.add('cge-theme-greatball-dialog');
        dialog.setAttribute('data-cge-dialog', 'picker');
        Object.assign(dialog.style, {
            background: '#fff', padding: '24px', borderRadius: '12px',
            boxShadow: '0 5px 15px rgba(0,0,0,.3)', width: 'min(92vw, 880px)',
            fontFamily: 'sans-serif', color: '#333', boxSizing: 'border-box'
        });

        const closeDialog = () => {
            if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
        };
        const state = {
            list: [],
            filtered: [],
            selected: new Set(Array.isArray(initialSelectedIds) ? initialSelectedIds : []),
            query: '',
            scope: mode === 'project' ? 'project' : 'all',
            scopeLocked: mode === 'project',
            archived: 'all',
            timeField: 'update',
            loading: true,
            pageSize: 100,
            visibleCount: 100,
            startDate: '',
            endDate: ''
        };

        const renderBase = () => {
            const modeLabel = mode === 'team' ? '团队空间' : mode === 'project' ? '项目空间' : '个人空间';
            const workspaceLabel = workspaceId ? `(${workspaceId})` : '';
            dialog.innerHTML = `
                <h2 style="margin-top:0; margin-bottom: 12px; font-size: 18px;">${isScheduleAction ? '选择定时导出对话' : '选择要导出的对话'}</h2>
                <div style="margin-bottom: 12px; color: #666; font-size: 12px;">空间:${modeLabel}${workspaceLabel}</div>
                <div class="cge-picker-row cge-picker-row-search" style="display: flex; gap: 8px; margin-bottom: 8px;">
                    <input id="conv-search" type="text" placeholder="搜索标题/项目名/ID"
                        style="flex: 1; padding: 8px; border-radius: 6px; border: 1px solid #ccc; box-sizing: border-box;">
                    <select id="filter-scope" style="padding: 8px 28px 8px 8px; border-radius: 6px; border: 1px solid #ccc;">
                        <option value="all">全部范围</option>
                        <option value="project">仅项目</option>
                        <option value="root">仅项目外</option>
                    </select>
                    <select id="filter-archived" style="padding: 8px 28px 8px 8px; border-radius: 6px; border: 1px solid #ccc;">
                        <option value="all">全部状态</option>
                        <option value="active">仅未归档</option>
                        <option value="archived">仅已归档</option>
                    </select>
                </div>
                <div class="cge-picker-row cge-picker-row-filter" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
                    <select id="filter-time-field" style="padding: 8px 28px 8px 8px; border-radius: 6px; border: 1px solid #ccc;">
                        <option value="update">按更新时间</option>
                        <option value="create">按创建时间</option>
                    </select>
                    <input id="filter-start-date" type="date" style="padding: 8px; border-radius: 6px; border: 1px solid #ccc;">
                    <span style="color: #666; font-size: 12px;">至</span>
                    <input id="filter-end-date" type="date" style="padding: 8px; border-radius: 6px; border: 1px solid #ccc;">
                    <button id="clear-date-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">清空日期</button>
                </div>
                <div id="conv-status" style="margin-bottom: 8px; font-size: 12px; color: #666;">正在加载列表...</div>
                <div id="conv-list" style="max-height: 360px; overflow: auto; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px; background: #fff;"></div>
                <div class="cge-picker-footer" style="display: flex; justify-content: space-between; align-items: center; margin-top: 16px;">
                    <div class="cge-picker-footer-left" style="display: flex; gap: 8px;">
                        <button id="select-all-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">全选</button>
                        <button id="clear-all-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">清空</button>
                    </div>
                    <div class="cge-picker-footer-right" style="display: flex; gap: 8px;">
                        <button id="back-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">返回</button>
                        <button id="export-selected-btn" style="padding: 8px 12px; border: none; border-radius: 6px; background: #10a37f; color: #fff; cursor: pointer; font-weight: bold;" disabled>${selectedActionLabel} (0)</button>
                    </div>
                </div>
            `;

            const searchInput = dialog.querySelector('#conv-search');
            const scopeSelect = dialog.querySelector('#filter-scope');
            const archivedSelect = dialog.querySelector('#filter-archived');
            const timeFieldSelect = dialog.querySelector('#filter-time-field');
            const startDateInput = dialog.querySelector('#filter-start-date');
            const endDateInput = dialog.querySelector('#filter-end-date');
            const clearDateBtn = dialog.querySelector('#clear-date-btn');
            const selectAllBtn = dialog.querySelector('#select-all-btn');
            const clearAllBtn = dialog.querySelector('#clear-all-btn');
            const backBtn = dialog.querySelector('#back-btn');
            const exportBtn = dialog.querySelector('#export-selected-btn');

            if (state.scopeLocked && scopeSelect) {
                scopeSelect.value = 'project';
                scopeSelect.disabled = true;
                scopeSelect.style.opacity = '0.7';
                scopeSelect.style.cursor = 'not-allowed';
                scopeSelect.title = '项目空间仅包含项目对话';
            }

            searchInput.oninput = (e) => {
                state.query = e.target.value || '';
                applyFilters();
                renderList();
            };
            scopeSelect.onchange = (e) => {
                state.scope = e.target.value;
                applyFilters();
                renderList();
            };
            archivedSelect.onchange = (e) => {
                state.archived = e.target.value;
                applyFilters();
                renderList();
            };
            timeFieldSelect.onchange = (e) => {
                state.timeField = e.target.value;
                applyFilters();
                renderList();
            };
            startDateInput.onchange = (e) => {
                state.startDate = e.target.value || '';
                applyFilters();
                renderList();
            };
            endDateInput.onchange = (e) => {
                state.endDate = e.target.value || '';
                applyFilters();
                renderList();
            };
            clearDateBtn.onclick = () => {
                state.startDate = '';
                state.endDate = '';
                startDateInput.value = '';
                endDateInput.value = '';
                applyFilters();
                renderList();
            };
            selectAllBtn.onclick = () => {
                state.filtered.forEach(item => state.selected.add(item.id));
                renderList();
            };
            clearAllBtn.onclick = () => {
                state.selected.clear();
                renderList();
            };
            backBtn.onclick = () => {
                closeDialog();
                if (isScheduleAction) {
                    if (typeof onBack === 'function') {
                        onBack();
                    } else {
                        showExportDialog();
                    }
                    return;
                }
                showExportDialog();
            };
            exportBtn.onclick = async () => {
                if (state.selected.size === 0) return;
                const selectedList = state.list.filter(item => state.selected.has(item.id));
                closeDialog();
                if (isScheduleAction) {
                    if (typeof onConfirm === 'function') {
                        await onConfirm(selectedList);
                    } else {
                        showExportDialog();
                    }
                    return;
                }
                await startSelectiveExportProcess(mode, workspaceId, selectedList);
            };
        };

        const applyFilters = () => {
            const query = state.query.trim().toLowerCase();
            const startBound = parseDateInputToEpoch(state.startDate, false);
            const endBound = parseDateInputToEpoch(state.endDate, true);
            state.filtered = state.list.filter(item => {
                const text = `${item.title || ''} ${item.projectTitle || ''} ${item.id || ''}`.toLowerCase();
                if (query && !text.includes(query)) return false;
                if (state.scope === 'project' && !item.projectTitle) return false;
                if (state.scope === 'root' && item.projectTitle) return false;
                if (state.archived === 'active' && item.is_archived) return false;
                if (state.archived === 'archived' && !item.is_archived) return false;
                if (startBound || endBound) {
                    const sourceTime = state.timeField === 'create'
                        ? item.create_time
                        : item.update_time;
                    const ts = normalizeEpochSeconds(sourceTime || 0);
                    if (!ts) return false;
                    if (startBound && ts < startBound) return false;
                    if (endBound && ts > endBound) return false;
                }
                return true;
            });
            state.visibleCount = state.pageSize;
        };

        const renderList = () => {
            const statusEl = dialog.querySelector('#conv-status');
            const listEl = dialog.querySelector('#conv-list');
            const exportBtn = dialog.querySelector('#export-selected-btn');
            const selectAllBtn = dialog.querySelector('#select-all-btn');
            const clearAllBtn = dialog.querySelector('#clear-all-btn');
            const controlsDisabled = state.loading;

            if (selectAllBtn) selectAllBtn.disabled = controlsDisabled;
            if (clearAllBtn) clearAllBtn.disabled = controlsDisabled;
            if (exportBtn) exportBtn.disabled = controlsDisabled || state.selected.size === 0;

            listEl.innerHTML = '';
            if (state.loading) {
                statusEl.textContent = '正在加载列表...';
                return;
            }

            const visibleCount = Math.min(state.visibleCount, state.filtered.length);
            statusEl.textContent = `共 ${state.list.length} 条,当前筛选 ${state.filtered.length} 条,显示 ${visibleCount} 条,已选 ${state.selected.size} 条`;
            exportBtn.textContent = `${selectedActionLabel} (${state.selected.size})`;

            if (state.filtered.length === 0) {
                const empty = document.createElement('div');
                empty.textContent = '没有匹配的对话。';
                empty.style.color = '#999';
                empty.style.padding = '8px 4px';
                listEl.appendChild(empty);
                return;
            }

            const visibleItems = state.filtered.slice(0, state.visibleCount);
            visibleItems.forEach(item => {
                const label = document.createElement('label');
                Object.assign(label.style, {
                    display: 'flex', gap: '8px', padding: '8px',
                    border: '1px solid #e5e7eb', borderRadius: '6px',
                    marginBottom: '8px', cursor: 'pointer', alignItems: 'flex-start'
                });

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.checked = state.selected.has(item.id);
                checkbox.onchange = (e) => {
                    if (e.target.checked) {
                        state.selected.add(item.id);
                    } else {
                        state.selected.delete(item.id);
                    }
                    renderList();
                };

                const content = document.createElement('div');
                content.style.flex = '1';

                const title = document.createElement('div');
                title.textContent = item.title || 'Untitled Conversation';
                title.style.fontWeight = 'bold';
                title.style.fontSize = '14px';

                const meta = document.createElement('div');
                meta.style.fontSize = '12px';
                meta.style.color = '#666';
                const timeLabelPrefix = state.timeField === 'create' ? '创建' : '更新';
                const timeValue = state.timeField === 'create' ? item.create_time : item.update_time;
                const timeLabel = formatTimestamp(timeValue) || '未知';
                meta.textContent = `${timeLabelPrefix}: ${timeLabel}`;

                const tags = document.createElement('div');
                tags.style.marginTop = '6px';
                tags.style.display = 'flex';
                tags.style.gap = '6px';
                tags.style.flexWrap = 'wrap';

                if (item.projectTitle) {
                    const projectTag = document.createElement('span');
                    projectTag.className = 'cge-tag-project';
                    projectTag.textContent = `项目: ${item.projectTitle}`;
                    Object.assign(projectTag.style, {
                        background: '#eef2ff', color: '#4338ca',
                        padding: '2px 6px', borderRadius: '999px', fontSize: '11px'
                    });
                    tags.appendChild(projectTag);
                }

                if (item.is_archived) {
                    const archivedTag = document.createElement('span');
                    archivedTag.className = 'cge-tag-archived';
                    archivedTag.textContent = '已归档';
                    Object.assign(archivedTag.style, {
                        background: '#fef3c7', color: '#92400e',
                        padding: '2px 6px', borderRadius: '999px', fontSize: '11px'
                    });
                    tags.appendChild(archivedTag);
                }

                content.appendChild(title);
                content.appendChild(meta);
                if (tags.childNodes.length > 0) content.appendChild(tags);

                label.appendChild(checkbox);
                label.appendChild(content);
                listEl.appendChild(label);
            });

            if (state.filtered.length > state.visibleCount) {
                const loadMore = document.createElement('button');
                loadMore.textContent = `加载更多(剩余 ${state.filtered.length - state.visibleCount} 条)`;
                Object.assign(loadMore.style, {
                    width: '100%', padding: '8px 12px', border: '1px solid #ccc',
                    borderRadius: '6px', background: '#fff', cursor: 'pointer'
                });
                loadMore.onclick = () => {
                    state.visibleCount = Math.min(state.visibleCount + state.pageSize, state.filtered.length);
                    renderList();
                };
                listEl.appendChild(loadMore);
            }
        };

        renderBase();
        overlay.appendChild(dialog);
        document.body.appendChild(overlay);
        overlay.onclick = (e) => {
            if (e.target !== overlay) return;
            closeDialog();
            if (isScheduleAction) {
                if (typeof onBack === 'function') {
                    onBack();
                } else {
                    showExportDialog();
                }
            }
        };

        const listPromise = mode === 'project'
            ? listProjectSpaceConversations(workspaceId)
            : listConversations(workspaceId);
        listPromise
            .then(list => {
                state.list = list;
                const listIds = new Set(list.map(item => item.id));
                state.selected = new Set(Array.from(state.selected).filter(id => listIds.has(id)));
                state.loading = false;
                applyFilters();
                renderList();
            })
            .catch(err => {
                const statusEl = dialog.querySelector('#conv-status');
                state.loading = false;
                state.list = [];
                state.filtered = [];
                statusEl.textContent = `加载失败: ${err.message}`;
                renderList();
            });
    }

    /**
     * [重构] 多步骤、用户主导的导出对话框
     */
    function showExportDialog() {
        ensureGreatBallThemeStyles();
        if (document.getElementById('export-dialog-overlay')) return;

        const overlay = document.createElement('div');
        overlay.id = 'export-dialog-overlay';
        overlay.classList.add('cge-theme-greatball-overlay');
        Object.assign(overlay.style, {
            position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
            backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: '99998',
            display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
            padding: '5vh 16px 3vh', boxSizing: 'border-box', overflowY: 'auto'
        });

        const dialog = document.createElement('div');
        dialog.id = 'export-dialog';
        dialog.classList.add('cge-theme-greatball-dialog');
        dialog.setAttribute('data-cge-dialog', 'main');
        Object.assign(dialog.style, {
            background: '#fff', padding: '24px', borderRadius: '12px',
            boxShadow: '0 5px 15px rgba(0,0,0,.3)', width: 'min(92vw, 560px)',
            fontFamily: 'sans-serif', color: '#333', boxSizing: 'border-box'
        });

        const closeDialog = () => document.body.removeChild(overlay);

        let pendingTeamAction = null;
        const renderStep = (step, action = null) => {
            pendingTeamAction = action;
            let html = '';
            switch (step) {
                case 'team': {
                    const detectedIds = detectAllWorkspaceIds();
                    html = `<h2 style="margin-top:0; margin-bottom: 20px; font-size: 18px;">导出团队空间</h2>`;

                    if (detectedIds.length > 1) {
                        html += `<div style="background: #eef2ff; border: 1px solid #818cf8; border-radius: 8px; padding: 12px; margin-bottom: 20px;">
                                     <p style="margin: 0 0 12px 0; font-weight: bold; color: #4338ca;">🔎 检测到多个 Workspace,请选择一个:</p>
                                     <div id="workspace-id-list">`;
                        detectedIds.forEach((id, index) => {
                            html += `<label style="display: block; margin-bottom: 8px; padding: 8px; border-radius: 6px; cursor: pointer; border: 1px solid #ddd; background: #fff;">
                                         <input type="radio" name="workspace_id" value="${id}" ${index === 0 ? 'checked' : ''}>
                                         <code style="margin-left: 8px; font-family: monospace; color: #555;">${id}</code>
                                      </label>`;
                        });
                        html += `</div></div>`;
                    } else if (detectedIds.length === 1) {
                        html += `<div style="background: #f0fdf4; border: 1px solid #4ade80; border-radius: 8px; padding: 12px; margin-bottom: 20px;">
                                     <p style="margin: 0 0 8px 0; font-weight: bold; color: #166534;">✅ 已自动检测到 Workspace ID:</p>
                                     <code id="workspace-id-code" style="background: #e0e7ff; padding: 4px 8px; border-radius: 4px; font-family: monospace; color: #4338ca; word-break: break-all;">${detectedIds[0]}</code>
                                   </div>`;
                    } else {
                        html += `<div style="background: #fffbeb; border: 1px solid #facc15; border-radius: 8px; padding: 12px; margin-bottom: 20px;">
                                     <p style="margin: 0; color: #92400e;">⚠️ 未能自动检测到 Workspace ID。</p>
                                     <p style="margin: 8px 0 0 0; font-size: 12px; color: #92400e;">请尝试刷新页面或打开一个团队对话,或在下方手动输入。</p>
                                   </div>
                                   <label for="team-id-input" style="display: block; margin-bottom: 8px; font-weight: bold;">手动输入 Team Workspace ID:</label>
                                   <input type="text" id="team-id-input" placeholder="粘贴您的 Workspace ID (ws-...)" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #ccc; box-sizing: border-box;">`;
                    }

                    let actionButtons = '';
                    if (pendingTeamAction === 'all') {
                        actionButtons = `<button id="start-team-export-btn" style="padding: 10px 16px; border: none; border-radius: 8px; background: #10a37f; color: #fff; cursor: pointer; font-weight: bold;">导出全部 (ZIP)</button>`;
                    } else if (pendingTeamAction === 'select') {
                        actionButtons = `<button id="start-team-picker-btn" style="padding: 10px 16px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer;">选择对话导出</button>`;
                    } else {
                        actionButtons = `<button id="start-team-export-btn" style="padding: 10px 16px; border: none; border-radius: 8px; background: #10a37f; color: #fff; cursor: pointer; font-weight: bold;">导出全部 (ZIP)</button>
                                     <button id="start-team-picker-btn" style="padding: 10px 16px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer;">选择对话导出</button>`;
                    }

                    html += `<div class="cge-team-footer" style="display: flex; justify-content: space-between; align-items: center; margin-top: 24px;">
                                 <button id="back-btn" style="padding: 10px 16px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer;">返回</button>
                                 <div class="cge-space-actions" style="display: flex; gap: 8px;">
                                     ${actionButtons}
                                 </div>
                               </div>`;
                    break;
                }

                case 'initial':
                default:
                    const scheduleWorkspaceValue = (autoExportState.workspaceId || '').replace(/"/g, '&quot;');
                    const scheduleWorkspaceDisabled = '';
                    const scheduleSelectionType = autoExportState.selectionType === 'selected' ? 'selected' : 'all';
                    const filenameTemplateValue = escapeHtml(filenameSettings.template || '');
                    const filenameAppendDateChecked = filenameSettings.appendDate ? 'checked' : '';
                    const fixedOverwriteSupported = supportsFixedFileOverwrite();
                    const fixedOverwriteChecked = downloadSettings.mode === 'file-handle' ? 'checked' : '';
                    const fixedOverwriteDisabled = fixedOverwriteSupported ? '' : 'disabled';
                    const fixedFileName = escapeHtml(downloadSettings.handleName || '未设置');
                    const fixedOverwriteHint = fixedOverwriteSupported
                        ? '开启后将直接写入同一个 ZIP 文件,避免浏览器自动改名为 (1)。'
                        : '当前浏览器不支持固定文件覆盖模式,将使用普通下载。';
                    const filenamePreviewValue = escapeHtml(buildExportZipFilename(
                        normalizeExportMode(autoExportState.mode),
                        autoExportState.selectionType === 'selected' ? 'selected' : 'full',
                        autoExportState.workspaceId || ''
                    ));
                    html = `<h2 style="margin-top:0; margin-bottom: 20px; font-size: 18px;">团队空间导出</h2>
                                <div class="cge-space-grid" style="display: flex; flex-direction: column; gap: 16px;">
                                    <div class="cge-space-card" style="padding: 16px; border: 1px solid #ccc; border-radius: 8px; background: #f9fafb;">
                                        <strong style="font-size: 16px;">团队空间</strong>
                                        <p style="margin: 4px 0 12px 0; color: #666;">导出团队空间下的对话,将自动检测ID。</p>
                                        <div class="cge-space-actions" style="display: flex; gap: 8px;">
                                            <button id="select-team-btn" style="padding: 8px 12px; border: none; border-radius: 6px; background: #10a37f; color: #fff; cursor: pointer; font-weight: bold;">导出全部</button>
                                            <button id="select-team-picker-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">选择对话导出</button>
                                        </div>
                                    </div>
                                    <div class="cge-space-card" style="padding: 16px; border: 1px solid #ccc; border-radius: 8px; background: #f9fafb;">
                                        <strong style="font-size: 16px;">文件命名</strong>
                                        <p style="margin: 4px 0 12px 0; color: #666;">自定义 ZIP 文件名。若要尽量保持同名文件,请关闭“自动追加日期”(部分浏览器仍可能自动改名为 (1))。</p>
                                        <div class="cge-picker-row" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap;">
                                            <input id="filename-template-input" type="text" value="${filenameTemplateValue}" placeholder="例如 latest_chat_backup 或 backup_{mode}_{type}" style="flex: 1 1 320px; min-width: 220px; padding: 8px; border-radius: 6px; border: 1px solid #ccc;" />
                                        </div>
                                        <div class="cge-picker-row" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap;">
                                            <label style="display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: #666;">
                                                <input id="filename-append-date-checkbox" type="checkbox" ${filenameAppendDateChecked} />
                                                自动追加日期
                                            </label>
                                            <button id="filename-save-btn" style="padding: 8px 12px; border: none; border-radius: 6px; background: #10a37f; color: #fff; cursor: pointer; font-weight: bold;">保存命名设置</button>
                                            <button id="filename-reset-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">恢复默认</button>
                                        </div>
                                        <div style="font-size: 12px; color: #666; margin-bottom: 6px;">支持变量:<code>{mode}</code> <code>{type}</code> <code>{workspace}</code> <code>{date}</code></div>
                                        <div style="font-size: 12px; color: #666; margin-bottom: 8px;">预览:<code id="filename-preview">${filenamePreviewValue}</code></div>
                                        <div class="cge-picker-row" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap;">
                                            <label style="display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: #666;">
                                                <input id="download-overwrite-checkbox" type="checkbox" ${fixedOverwriteChecked} ${fixedOverwriteDisabled} />
                                                固定文件覆盖模式(避免 (1))
                                            </label>
                                            <button id="download-pick-file-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;" ${fixedOverwriteDisabled}>选择固定保存文件</button>
                                            <button id="download-clear-file-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;" ${fixedOverwriteDisabled}>清除固定文件</button>
                                        </div>
                                        <div id="download-fixed-file-status" style="font-size: 12px; color: #666; margin-bottom: 4px;">当前固定文件:<code>${fixedFileName}</code></div>
                                        <div style="font-size: 12px; color: #666;">${fixedOverwriteHint}</div>
                                    </div>
                                    <div class="cge-space-card" style="padding: 16px; border: 1px solid #ccc; border-radius: 8px; background: #f9fafb;">
                                        <strong style="font-size: 16px;">定时导出</strong>
                                        <p style="margin: 4px 0 12px 0; color: #666;">每隔固定时间自动导出,启动后会立即执行一次,刷新页面后会自动恢复。</p>
                                        <div class="cge-picker-row" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap;">
                                            <input id="schedule-interval-input" type="number" min="${AUTO_EXPORT_MIN_MINUTES}" value="${autoExportState.intervalMinutes}" style="flex: 1 1 120px; min-width: 120px; padding: 8px; border-radius: 6px; border: 1px solid #ccc;" />
                                            <select id="schedule-mode-select" disabled style="flex: 1 1 140px; min-width: 140px; padding: 8px; border-radius: 6px; border: 1px solid #ccc; opacity: 0.75; cursor: not-allowed;">
                                                <option value="team" selected>团队空间</option>
                                            </select>
                                            <input id="schedule-workspace-input" type="text" placeholder="团队 Workspace ID (ws-...)" value="${scheduleWorkspaceValue}" ${scheduleWorkspaceDisabled} style="flex: 2 1 200px; min-width: 180px; padding: 8px; border-radius: 6px; border: 1px solid #ccc;" />
                                        </div>
                                        <div class="cge-picker-row" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap;">
                                            <select id="schedule-selection-type" style="flex: 1 1 180px; min-width: 180px; padding: 8px; border-radius: 6px; border: 1px solid #ccc;">
                                                <option value="all" ${scheduleSelectionType === 'all' ? 'selected' : ''}>导出全部对话</option>
                                                <option value="selected" ${scheduleSelectionType === 'selected' ? 'selected' : ''}>仅导出已选对话</option>
                                            </select>
                                            <button id="schedule-pick-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">选择定时对话</button>
                                        </div>
                                        <div id="schedule-selection-summary" style="font-size: 12px; color: #666; margin-bottom: 8px;"></div>
                                        <div class="cge-space-actions" style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px;">
                                            <button id="schedule-start-btn" style="padding: 8px 12px; border: none; border-radius: 6px; background: #10a37f; color: #fff; cursor: pointer; font-weight: bold;">启动定时</button>
                                            <button id="schedule-stop-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">停止定时</button>
                                            <button id="schedule-run-now-btn" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer;">立即执行一次</button>
                                        </div>
                                        <div id="schedule-status" style="font-size: 12px; color: #666;">${getAutoExportStatusText()}</div>
                                    </div>
                                </div>
                                <div class="cge-team-footer" style="display: flex; justify-content: flex-end; margin-top: 24px;">
                                    <button id="cancel-btn" style="padding: 10px 16px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer;">取消</button>
                                </div>`;
                    break;
            }
            dialog.innerHTML = html;
            attachListeners(step);
        };

        const attachListeners = (step) => {
            if (step === 'initial') {
                const startTeamFlow = (action) => {
                    const detectedIds = detectAllWorkspaceIds();
                    if (detectedIds.length === 1) {
                        const workspaceId = detectedIds[0];
                        closeDialog();
                        if (action === 'all') {
                            startExportProcess('team', workspaceId);
                        } else {
                            showConversationPicker({ mode: 'team', workspaceId });
                        }
                        return;
                    }
                    renderStep('team', action);
                };
                document.getElementById('select-team-btn').onclick = () => startTeamFlow('all');
                document.getElementById('select-team-picker-btn').onclick = () => startTeamFlow('select');
                document.getElementById('cancel-btn').onclick = closeDialog;

                const scheduleModeSelect = document.getElementById('schedule-mode-select');
                const scheduleIntervalInput = document.getElementById('schedule-interval-input');
                const scheduleWorkspaceInput = document.getElementById('schedule-workspace-input');
                const scheduleSelectionTypeSelect = document.getElementById('schedule-selection-type');
                const schedulePickBtn = document.getElementById('schedule-pick-btn');
                const scheduleSelectionSummaryEl = document.getElementById('schedule-selection-summary');
                const scheduleStatusEl = document.getElementById('schedule-status');
                const filenameTemplateInput = document.getElementById('filename-template-input');
                const filenameAppendDateCheckbox = document.getElementById('filename-append-date-checkbox');
                const filenamePreviewEl = document.getElementById('filename-preview');
                const filenameSaveBtn = document.getElementById('filename-save-btn');
                const filenameResetBtn = document.getElementById('filename-reset-btn');
                const downloadOverwriteCheckbox = document.getElementById('download-overwrite-checkbox');
                const downloadPickFileBtn = document.getElementById('download-pick-file-btn');
                const downloadClearFileBtn = document.getElementById('download-clear-file-btn');
                const downloadFixedFileStatusEl = document.getElementById('download-fixed-file-status');
                if (autoExportState.mode !== 'team') {
                    autoExportState.mode = 'team';
                    autoExportState.workspaceId = (autoExportState.workspaceId || '').trim();
                    saveAutoExportState();
                }
                const resolveTeamWorkspaceIdFromInput = (rawWorkspaceId, showAlert = false) => {
                    const directWorkspaceId = (rawWorkspaceId || '').trim();
                    if (directWorkspaceId) return directWorkspaceId;
                    const detectedIds = detectAllWorkspaceIds();
                    if (detectedIds.length === 1) return detectedIds[0];
                    if (showAlert) {
                        alert('团队空间需要 Team Workspace ID(可手动填写,或先打开一个团队对话后再试)。');
                    }
                    return '';
                };
                const isSelectionContextMatched = (mode, workspaceId) => {
                    const normalizedWorkspaceId = mode === 'team' ? (workspaceId || '') : '';
                    return autoExportState.selectedMode === mode &&
                        (autoExportState.selectedWorkspaceId || '') === normalizedWorkspaceId;
                };
                const updateScheduleWorkspaceState = () => {
                    const teamMode = scheduleModeSelect.value === 'team';
                    scheduleWorkspaceInput.disabled = !teamMode;
                    scheduleWorkspaceInput.style.opacity = teamMode ? '1' : '0.7';
                    if (!teamMode) return;
                    if ((scheduleWorkspaceInput.value || '').trim()) return;
                    const detectedIds = detectAllWorkspaceIds();
                    if (detectedIds.length === 1) {
                        scheduleWorkspaceInput.value = detectedIds[0];
                    }
                };
                const getDraftFilenameSettings = () => normalizeFilenameSettings({
                    template: filenameTemplateInput?.value || '',
                    appendDate: Boolean(filenameAppendDateCheckbox?.checked)
                });
                const updateFilenamePreview = () => {
                    if (!filenamePreviewEl) return;
                    const mode = scheduleModeSelect?.value || normalizeExportMode(autoExportState.mode);
                    const selectionType = scheduleSelectionTypeSelect?.value === 'selected' ? 'selected' : 'full';
                    const workspaceId = mode === 'team'
                        ? (scheduleWorkspaceInput?.value || '').trim()
                        : '';
                    filenamePreviewEl.textContent = buildExportZipFilename(
                        mode,
                        selectionType,
                        workspaceId,
                        getDraftFilenameSettings()
                    );
                };
                const updateFixedFileStatus = () => {
                    if (downloadFixedFileStatusEl) {
                        const name = downloadSettings.handleName || '未设置';
                        downloadFixedFileStatusEl.innerHTML = `当前固定文件:<code>${escapeHtml(name)}</code>`;
                    }
                };
                const updateScheduleSelectionSummary = () => {
                    const mode = scheduleModeSelect.value;
                    const workspaceId = mode === 'team' ? scheduleWorkspaceInput.value.trim() : '';
                    const selectionType = scheduleSelectionTypeSelect.value === 'selected' ? 'selected' : 'all';
                    schedulePickBtn.disabled = false;
                    schedulePickBtn.style.opacity = '1';
                    schedulePickBtn.style.cursor = 'pointer';
                    schedulePickBtn.textContent = selectionType === 'selected'
                        ? '选择定时对话'
                        : '切换到已选并选择';

                    if (selectionType === 'all') {
                        scheduleSelectionSummaryEl.textContent = '当前策略:导出全部对话。';
                        return;
                    }

                    const selectedCount = autoExportState.selectedEntries.length;
                    if (selectedCount === 0) {
                        scheduleSelectionSummaryEl.textContent = '当前策略:仅导出已选对话(未选择)。';
                        return;
                    }

                    if (!isSelectionContextMatched(mode, workspaceId)) {
                        scheduleSelectionSummaryEl.textContent = `已保存 ${selectedCount} 条定时对话,但与当前空间设置不匹配,请重新选择。`;
                        return;
                    }
                    scheduleSelectionSummaryEl.textContent = `已保存 ${selectedCount} 条定时对话。`;
                };
                const updateScheduleStatus = () => {
                    scheduleStatusEl.textContent = getAutoExportStatusText();
                };
                scheduleModeSelect.onchange = () => {
                    updateScheduleWorkspaceState();
                    updateScheduleSelectionSummary();
                    updateFilenamePreview();
                };
                scheduleWorkspaceInput.oninput = () => {
                    updateScheduleSelectionSummary();
                    updateFilenamePreview();
                };
                scheduleSelectionTypeSelect.onchange = () => {
                    autoExportState.selectionType = scheduleSelectionTypeSelect.value === 'selected' ? 'selected' : 'all';
                    saveAutoExportState();
                    updateScheduleSelectionSummary();
                    updateFilenamePreview();
                };
                if (filenameTemplateInput) {
                    filenameTemplateInput.oninput = () => updateFilenamePreview();
                }
                if (filenameAppendDateCheckbox) {
                    filenameAppendDateCheckbox.onchange = () => updateFilenamePreview();
                }
                if (filenameSaveBtn) {
                    filenameSaveBtn.onclick = () => {
                        filenameSettings = getDraftFilenameSettings();
                        saveFilenameSettings();
                        if (downloadOverwriteCheckbox) {
                            downloadSettings.mode = downloadOverwriteCheckbox.checked ? 'file-handle' : 'browser';
                            saveDownloadSettings();
                        }
                        updateFilenamePreview();
                        updateFixedFileStatus();
                        notifyUser('命名设置已保存。', { mode: DEFAULT_NOTIFY_MODE, level: 'success' });
                    };
                }
                if (filenameResetBtn) {
                    filenameResetBtn.onclick = () => {
                        filenameSettings = normalizeFilenameSettings();
                        saveFilenameSettings();
                        if (filenameTemplateInput) filenameTemplateInput.value = filenameSettings.template;
                        if (filenameAppendDateCheckbox) filenameAppendDateCheckbox.checked = filenameSettings.appendDate;
                        updateFilenamePreview();
                        notifyUser('已恢复默认命名规则。', { mode: DEFAULT_NOTIFY_MODE, level: 'info' });
                    };
                }
                if (downloadOverwriteCheckbox) {
                    downloadOverwriteCheckbox.onchange = () => {
                        if (downloadOverwriteCheckbox.checked && !supportsFixedFileOverwrite()) {
                            downloadOverwriteCheckbox.checked = false;
                            notifyUser('当前浏览器不支持固定文件覆盖模式。', {
                                mode: DEFAULT_NOTIFY_MODE,
                                level: 'error'
                            });
                            return;
                        }
                        downloadSettings.mode = downloadOverwriteCheckbox.checked ? 'file-handle' : 'browser';
                        saveDownloadSettings();
                        updateFixedFileStatus();
                    };
                }
                if (downloadPickFileBtn) {
                    downloadPickFileBtn.onclick = async () => {
                        try {
                            const mode = scheduleModeSelect?.value || normalizeExportMode(autoExportState.mode);
                            const selectionType = scheduleSelectionTypeSelect?.value === 'selected' ? 'selected' : 'full';
                            const workspaceId = mode === 'team' ? (scheduleWorkspaceInput?.value || '').trim() : '';
                            const suggestedName = buildExportZipFilename(
                                mode,
                                selectionType,
                                workspaceId,
                                getDraftFilenameSettings()
                            );
                            await pickAndRememberZipFileHandle(suggestedName);
                            downloadSettings.mode = 'file-handle';
                            saveDownloadSettings();
                            if (downloadOverwriteCheckbox) downloadOverwriteCheckbox.checked = true;
                            updateFixedFileStatus();
                            notifyUser('固定保存文件已设置,后续将直接覆盖该文件。', {
                                mode: DEFAULT_NOTIFY_MODE,
                                level: 'success'
                            });
                        } catch (err) {
                            if (err?.name === 'AbortError') return;
                            notifyUser(`选择固定文件失败: ${err?.message || err}`, {
                                mode: DEFAULT_NOTIFY_MODE,
                                level: 'error',
                                duration: 3800
                            });
                        }
                    };
                }
                if (downloadClearFileBtn) {
                    downloadClearFileBtn.onclick = async () => {
                        await clearStoredZipFileHandle();
                        downloadSettings.handleName = '';
                        if (downloadSettings.mode === 'file-handle') {
                            downloadSettings.mode = 'browser';
                            if (downloadOverwriteCheckbox) downloadOverwriteCheckbox.checked = false;
                        }
                        saveDownloadSettings();
                        updateFixedFileStatus();
                        notifyUser('已清除固定保存文件。', {
                            mode: DEFAULT_NOTIFY_MODE,
                            level: 'info'
                        });
                    };
                }
                updateScheduleWorkspaceState();
                updateScheduleSelectionSummary();
                updateScheduleStatus();
                updateFilenamePreview();
                updateFixedFileStatus();

                schedulePickBtn.onclick = () => {
                    if (scheduleSelectionTypeSelect.value !== 'selected') {
                        scheduleSelectionTypeSelect.value = 'selected';
                        autoExportState.selectionType = 'selected';
                        saveAutoExportState();
                    }
                    const mode = scheduleModeSelect.value;
                    let workspaceId = mode === 'team' ? resolveTeamWorkspaceIdFromInput(scheduleWorkspaceInput.value, true) : '';
                    if (mode === 'team' && !workspaceId) return;
                    if (mode === 'team') {
                        scheduleWorkspaceInput.value = workspaceId;
                    }
                    const initialSelectedIds = isSelectionContextMatched(mode, workspaceId)
                        ? autoExportState.selectedEntries.map(item => item.id)
                        : [];
                    closeDialog();
                    showConversationPicker({
                        mode,
                        workspaceId: mode === 'team' ? workspaceId : null,
                        action: 'schedule',
                        initialSelectedIds,
                        onConfirm: async (selectedList) => {
                            autoExportState.selectionType = 'selected';
                            autoExportState.selectedEntries = normalizeScheduleEntries(selectedList);
                            autoExportState.selectedMode = mode;
                            autoExportState.selectedWorkspaceId = mode === 'team' ? workspaceId : '';
                            autoExportState.lastError = '';
                            saveAutoExportState();
                            showExportDialog();
                        },
                        onBack: () => showExportDialog()
                    });
                };

                document.getElementById('schedule-start-btn').onclick = () => {
                    const intervalMinutes = Math.max(
                        AUTO_EXPORT_MIN_MINUTES,
                        Math.floor(Number(scheduleIntervalInput.value || 0))
                    );
                    if (!Number.isFinite(intervalMinutes) || intervalMinutes < AUTO_EXPORT_MIN_MINUTES) {
                        alert(`请输入有效的间隔分钟数(>= ${AUTO_EXPORT_MIN_MINUTES})。`);
                        return;
                    }

                    const mode = scheduleModeSelect.value;
                    const selectionType = scheduleSelectionTypeSelect.value === 'selected' ? 'selected' : 'all';
                    const workspaceId = mode === 'team'
                        ? resolveTeamWorkspaceIdFromInput(scheduleWorkspaceInput.value, true)
                        : '';
                    if (mode === 'team' && !workspaceId) return;
                    if (mode === 'team') {
                        scheduleWorkspaceInput.value = workspaceId;
                    }

                    const contextMatched = isSelectionContextMatched(mode, workspaceId);
                    const selectedEntries = contextMatched
                        ? normalizeScheduleEntries(autoExportState.selectedEntries)
                        : [];
                    if (selectionType === 'selected' && selectedEntries.length === 0) {
                        alert('当前“仅导出已选对话”没有有效选择,请先点击“选择定时对话”。');
                        updateScheduleSelectionSummary();
                        return;
                    }

                    try {
                        const state = startIntervalExport({
                            mode,
                            workspaceId,
                            intervalMinutes,
                            runNow: true,
                            selectionType,
                            selectedEntries: selectionType === 'selected' ? selectedEntries : autoExportState.selectedEntries,
                            selectedMode: selectionType === 'selected' ? mode : autoExportState.selectedMode,
                            selectedWorkspaceId: selectionType === 'selected'
                                ? (mode === 'team' ? workspaceId : '')
                                : autoExportState.selectedWorkspaceId
                        });
                        scheduleIntervalInput.value = String(state.intervalMinutes);
                        scheduleModeSelect.value = state.mode;
                        scheduleWorkspaceInput.value = state.workspaceId || '';
                        scheduleSelectionTypeSelect.value = state.selectionType || 'all';
                        updateScheduleWorkspaceState();
                        updateScheduleSelectionSummary();
                        updateScheduleStatus();
                    } catch (err) {
                        alert(err?.message || '启动定时导出失败。');
                    }
                };

                document.getElementById('schedule-stop-btn').onclick = () => {
                    stopIntervalExport();
                    updateScheduleSelectionSummary();
                    updateScheduleStatus();
                };

                document.getElementById('schedule-run-now-btn').onclick = async () => {
                    const mode = scheduleModeSelect.value;
                    const selectionType = scheduleSelectionTypeSelect.value === 'selected' ? 'selected' : 'all';
                    let workspaceId = mode === 'team'
                        ? resolveTeamWorkspaceIdFromInput(scheduleWorkspaceInput.value, true)
                        : null;
                    if (mode === 'team' && !workspaceId) return;
                    if (mode === 'team') {
                        scheduleWorkspaceInput.value = workspaceId;
                    }

                    if (selectionType === 'selected') {
                        const contextMatched = isSelectionContextMatched(mode, workspaceId || '');
                        const selectedEntries = contextMatched
                            ? normalizeScheduleEntries(autoExportState.selectedEntries)
                            : [];
                        if (selectedEntries.length === 0) {
                            alert('当前“仅导出已选对话”没有有效选择,请先点击“选择定时对话”。');
                            updateScheduleSelectionSummary();
                            return;
                        }
                        closeDialog();
                        const exported = await exportConversations({
                            mode,
                            workspaceId,
                            conversationEntries: selectedEntries,
                            exportType: 'selected',
                            notifyMode: SCHEDULE_NOTIFY_MODE
                        });
                        if (exported) {
                            autoExportState.lastRunAt = Date.now();
                            autoExportState.lastError = '';
                        } else {
                            autoExportState.lastError = '立即执行未完成导出。';
                        }
                        saveAutoExportState();
                        return;
                    }

                    closeDialog();
                    await startScheduledExport({
                        mode,
                        workspaceId,
                        autoConfirm: true,
                        source: 'manual-interval'
                    });
                };
            } else if (step === 'team') {
                document.getElementById('back-btn').onclick = () => renderStep('initial');
                const resolveWorkspaceId = () => {
                    let workspaceId = '';
                    const radioChecked = document.querySelector('input[name="workspace_id"]:checked');
                    const codeEl = document.getElementById('workspace-id-code');
                    const inputEl = document.getElementById('team-id-input');

                    if (radioChecked) {
                        workspaceId = radioChecked.value;
                    } else if (codeEl) {
                        workspaceId = codeEl.textContent;
                    } else if (inputEl) {
                        workspaceId = inputEl.value.trim();
                    }

                    if (!workspaceId) {
                        alert('请选择或输入一个有效的 Team Workspace ID!');
                        return;
                    }
                    return workspaceId;
                };
                const exportAllBtn = document.getElementById('start-team-export-btn');
                const pickerBtn = document.getElementById('start-team-picker-btn');
                if (exportAllBtn) exportAllBtn.onclick = () => {
                    const workspaceId = resolveWorkspaceId();
                    if (!workspaceId) return;
                    closeDialog();
                    startExportProcess('team', workspaceId);
                };
                if (pickerBtn) pickerBtn.onclick = () => {
                    const workspaceId = resolveWorkspaceId();
                    if (!workspaceId) return;
                    closeDialog();
                    showConversationPicker({ mode: 'team', workspaceId });
                };
            }
        };

        overlay.appendChild(dialog);
        document.body.appendChild(overlay);
        overlay.onclick = (e) => { if (e.target === overlay) closeDialog(); };
        renderStep('initial');
    }

    function addBtn() {
        ensureGreatBallThemeStyles();
        const oldRail = document.getElementById('cge-poke-rail');
        if (oldRail) oldRail.remove();
        const oldMasterBtn = document.getElementById('cge-master-btn');
        if (oldMasterBtn) oldMasterBtn.remove();
        const oldPokeBtn = document.getElementById('cge-poke-btn');
        if (oldPokeBtn) oldPokeBtn.remove();

        let b = document.getElementById('gpt-rescue-btn');
        if (!b) {
            b = document.createElement('button');
            b.id = 'gpt-rescue-btn';
            b.type = 'button';
            b.className = 'cge-ball-btn cge-ball-great cge-theme-greatball-fab';
            b.innerHTML = '<span class="cge-ball-label">导出</span>';
        } else {
            if (!b.querySelector('.cge-ball-label')) {
                b.innerHTML = '<span class="cge-ball-label">导出</span>';
            }
            b.classList.remove('cge-ball-master', 'cge-ball-ultra', 'cge-ball-poke');
            b.classList.add('cge-ball-btn', 'cge-ball-great', 'cge-theme-greatball-fab');
        }
        if (!b.parentElement || b.parentElement !== document.body) {
            document.body.appendChild(b);
        }

        b.style.display = '';
        b.disabled = false;
        setExportButtonState(b, { label: '导出', title: '打开导出面板', state: 'idle' });
        b.onclick = showExportDialog;
    }

    // --- 脚本启动 ---
    setTimeout(addBtn, 2000);
    resumeIntervalExportIfNeeded();

    window.ChatGPTExporter = window.ChatGPTExporter || {};
    Object.assign(window.ChatGPTExporter, {
        showDialog: showExportDialog,
        startManualExport: (mode = 'team', workspaceId = null) => {
            const normalizedMode = normalizeExportMode(mode);
            if (normalizedMode === 'project') {
                return startProjectSpaceExportProcess(workspaceId);
            }
            return startExportProcess(normalizedMode, workspaceId);
        },
        startScheduledExport,
        startIntervalExport,
        stopIntervalExport,
        getIntervalExportState
    });

    document.documentElement.setAttribute('data-chatgpt-exporter-ready', '1');
    window.dispatchEvent(new CustomEvent('CHATGPT_EXPORTER_READY'));

    window.addEventListener('message', (event) => {
        if (event.source !== window) return;
        const data = event.data || {};
        if (data?.type !== 'CHATGPT_EXPORTER_COMMAND') return;
        const api = window.ChatGPTExporter;
        if (!api) return;
        try {
            switch (data.action) {
                case 'START_SCHEDULED_EXPORT':
                    api.startScheduledExport(data.payload || {});
                    break;
                case 'START_INTERVAL_EXPORT':
                    api.startIntervalExport(data.payload || {});
                    break;
                case 'STOP_INTERVAL_EXPORT':
                    api.stopIntervalExport();
                    break;
                case 'OPEN_DIALOG':
                    api.showDialog();
                    break;
                case 'START_MANUAL_EXPORT':
                    api.startManualExport(data.payload?.mode, data.payload?.workspaceId);
                    break;
                default:
                    console.warn('[ChatGPT Exporter] 未知命令:', data.action);
            }
        } catch (err) {
            console.error('[ChatGPT Exporter] 处理命令失败:', err);
        }
    });

})();





// ===== END 1.js =====

// ===== BEGIN 2.js =====

// ==UserScript==
// @name         NotebookLM Chat
// @namespace    local.notebooklm
// @version      0.4.4
// @description  在 ChatGPT 页面调用 NotebookLM 聊天,支持背景框与发送时自动拼接
// @author       you
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @match        https://notebooklm.google.com/*
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// @connect      notebooklm.google.com
// @connect      accounts.google.com
// ==/UserScript==

(function () {
  "use strict";

  const NOTEBOOKLM_HOME_URL = "https://notebooklm.google.com/";
  const BATCHEXECUTE_URL =
    "https://notebooklm.google.com/_/LabsTailwindUi/data/batchexecute";
  const QUERY_URL =
    "https://notebooklm.google.com/_/LabsTailwindUi/data/google.internal.labs.tailwind.orchestration.v1.LabsTailwindOrchestrationService/GenerateFreeFormStreamed";

  const RPC = {
    LIST_NOTEBOOKS: "wXbhsf",
    GET_NOTEBOOK: "rLM1Ne",
  };

  const DEFAULT_BL = "boq_labs-tailwind-frontend_20251221.14_p0";
  const STORAGE_KEY = "tm_notebooklm_chat_clipboard_state_v1";
  const TOKEN_CACHE_TTL_MS = 60 * 1000;
  const CONTEXT_BLOCK_TITLE = "【NotebookLM 背景信息】";
  const QUESTION_BLOCK_TITLE = "【用户问题】";
  const ASK_SEND_BUTTON_TITLE = "先查询 NotebookLM 背景,再自动拼接并发送";
  const DEFAULT_NOTEBOOK_SYSTEM_PROMPT = "";
  const DEFAULT_MERGE_INSTRUCTION_PROMPT = [
    "以下是 NotebookLM 返回的背景资料,请把它作为上下文参考:",
    "笔记本:{{notebook}}",
    "NotebookLM 提问:{{question}}",
    "NotebookLM 返回:",
    "{{answer}}",
    "",
    "请基于以上背景继续回答我的问题。",
  ].join("\n");

  const state = {
    panelOpen: false,
    reqid: 100000,
    notebooks: [],
    sources: [],
    selectedNotebookId: "",
    sourceSelectionByNotebook: {},
    conversationByNotebook: {},
    historyByConversation: {},
    lastResult: null,
    notebookSystemPrompt: DEFAULT_NOTEBOOK_SYSTEM_PROMPT,
    mergeInstructionPrompt: DEFAULT_MERGE_INSTRUCTION_PROMPT,
    chatContextText: "",
    chatContextLayout: null,
    chatContextMinimized: false,
    chatContextFabPos: null,
  };

  const tokenCache = {
    csrfToken: "",
    sessionId: "",
    bl: DEFAULT_BL,
    fetchedAt: 0,
  };

  const ui = {
    root: null,
    toggle: null,
    notebookSelect: null,
    reloadBtn: null,
    sourceList: null,
    sourceCount: null,
    selectAllBtn: null,
    clearAllBtn: null,
    notebookSystemPromptInput: null,
    mergeInstructionPromptInput: null,
    newConversationCheckbox: null,
    askBtn: null,
    chatContextWrap: null,
    chatContextFab: null,
    askSendFloatingBtn: null,
    chatContextTextarea: null,
    status: null,
  };

  let chatContextObserver = null;
  let chatContextResizeObserver = null;
  let chatContextResizeObservedEl = null;
  let chatContextResizePersistTimer = 0;
  let chatContextApplyingLayout = false;
  let chatContextWindowResizeBound = false;
  let chatContextFabJustDragged = false;
  let chatAutoAskInFlight = false;
  let askSendFloatingPositionBound = false;

  function loadPersistedState() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return;
      const saved = JSON.parse(raw);
      if (saved && typeof saved === "object") {
        state.panelOpen = saved.panelOpen !== false;
        state.notebookSystemPrompt =
          typeof saved.notebookSystemPrompt === "string"
            ? saved.notebookSystemPrompt
            : DEFAULT_NOTEBOOK_SYSTEM_PROMPT;
        state.mergeInstructionPrompt = (() => {
          if (typeof saved.mergeInstructionPrompt !== "string") {
            return DEFAULT_MERGE_INSTRUCTION_PROMPT;
          }
          const legacyValue = "请结合以下背景信息回答用户问题。";
          if (saved.mergeInstructionPrompt.trim() === legacyValue) {
            return DEFAULT_MERGE_INSTRUCTION_PROMPT;
          }
          return saved.mergeInstructionPrompt;
        })();
        state.chatContextText = saved.chatContextText || "";
        state.chatContextLayout =
          saved.chatContextLayout && typeof saved.chatContextLayout === "object"
            ? saved.chatContextLayout
            : null;
        state.chatContextMinimized = saved.chatContextMinimized === true;
        state.chatContextFabPos =
          saved.chatContextFabPos && typeof saved.chatContextFabPos === "object"
            ? saved.chatContextFabPos
            : null;
        state.selectedNotebookId = saved.selectedNotebookId || "";
        state.sourceSelectionByNotebook = saved.sourceSelectionByNotebook || {};
        // On ChatGPT, keep panel collapsed after refresh. Open only when user clicks.
        if (isChatGPTPage()) {
          state.panelOpen = false;
        }
      }
    } catch (_) {
      // ignore storage errors
    }
  }

  function persistState() {
    try {
      localStorage.setItem(
        STORAGE_KEY,
        JSON.stringify({
          panelOpen: state.panelOpen,
          notebookSystemPrompt: state.notebookSystemPrompt,
          mergeInstructionPrompt: state.mergeInstructionPrompt,
          chatContextText: state.chatContextText,
          chatContextLayout: state.chatContextLayout,
          chatContextMinimized: state.chatContextMinimized,
          chatContextFabPos: state.chatContextFabPos,
          selectedNotebookId: state.selectedNotebookId,
          sourceSelectionByNotebook: state.sourceSelectionByNotebook,
        })
      );
    } catch (_) {
      // ignore storage errors
    }
  }

  function injectStyles() {
    const style = document.createElement("style");
    style.textContent = `
#tm-nblm-toggle{
  position:fixed; right:20px; bottom:20px; z-index:12030;
  width:60px; height:60px; border:3px solid #111827; border-radius:999px; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:linear-gradient(180deg,#ef4444 0 47%, #111827 47% 53%, #f8fafc 53% 100%);
  color:transparent; font-size:0; cursor:pointer; user-select:none; overflow:hidden;
  box-shadow:0 12px 28px rgba(17,24,39,.35);
  transition:transform .2s ease, box-shadow .2s ease, filter .2s ease;
}
#tm-nblm-toggle::before{
  content:"";
  position:absolute;
  left:50%;
  top:50%;
  width:20px;
  height:20px;
  transform:translate(-50%,-50%);
  border-radius:999px;
  border:3px solid #111827;
  background:#ffffff;
}
#tm-nblm-toggle::after{
  content:"";
  position:absolute;
  left:50%;
  top:50%;
  width:8px;
  height:8px;
  transform:translate(-50%,-50%);
  border-radius:999px;
  background:#ef4444;
}
#tm-nblm-toggle:hover{
  transform:translateY(-1px);
  filter:saturate(108%);
  box-shadow:0 16px 32px rgba(17,24,39,.44);
}
#tm-nblm-toggle:active{
  transform:translateY(0) scale(.97);
}
#tm-nblm-root{
  position:fixed; right:20px; bottom:90px; z-index:12020;
  width:min(430px, calc(100vw - 24px)); max-height:min(78vh, 760px);
  display:flex; flex-direction:column; gap:10px; overflow:hidden;
  border:3px solid #111827; border-radius:20px;
  background:linear-gradient(165deg,#ef4444 0%,#dc2626 68%,#b91c1c 100%);
  box-shadow:0 20px 44px rgba(127,29,29,.42), inset 0 1px 0 rgba(255,255,255,.25);
  transform-origin:right bottom;
  transition:opacity .18s ease, transform .18s ease;
  font:13px/1.45 "Trebuchet MS","Noto Sans SC","Microsoft YaHei",sans-serif; color:#111827;
}
#tm-nblm-root.tm-hide{
  opacity:0;
  transform:translateY(10px) scale(.97);
  pointer-events:none;
}
#tm-nblm-root, #tm-nblm-root *{
  box-sizing:border-box;
}
#tm-nblm-root button,
#tm-nblm-root select,
#tm-nblm-root textarea,
#tm-nblm-root input{
  color:#101623;
}
#tm-nblm-head{
  display:flex; align-items:center; justify-content:space-between;
  padding:10px 12px;
  border-bottom:3px solid #111827;
  background:linear-gradient(180deg,#991b1b,#7f1d1d);
}
#tm-nblm-title{
  color:#fde047;
  font-weight:800;
  font-size:14px;
  letter-spacing:.4px;
  text-shadow:0 1px 0 rgba(0,0,0,.35);
}
#tm-nblm-close{
  border:2px solid #111827;
  border-radius:999px;
  background:#fef3c7;
  color:#7c2d12;
  padding:5px 11px;
  cursor:pointer;
  font-weight:700;
}
#tm-nblm-body{
  padding:10px 12px 12px;
  display:flex;
  flex-direction:column;
  gap:10px;
  overflow:auto;
  background-image:
    radial-gradient(rgba(15,23,42,.08) 1px, transparent 1px),
    linear-gradient(180deg,#fee2e2 0%,#fef3c7 52%,#dbeafe 100%);
  background-size:8px 8px, auto;
}
#tm-nblm-row{ display:flex; gap:8px; align-items:center; }
#tm-nblm-row select, #tm-nblm-row button, #tm-nblm-body textarea, #tm-nblm-body input[type="text"]{
  font:inherit;
}
#tm-nblm-notebook{
  flex:1; min-width:0;
  border:2px solid #111827;
  border-radius:10px;
  padding:8px;
  background:#ecfccb;
  color:#1f2937;
  appearance:auto;
}
#tm-nblm-reload{
  border:2px solid #111827;
  border-radius:10px;
  background:#e0f2fe;
  color:#0c4a6e;
  font-weight:700;
  padding:8px 10px;
  cursor:pointer;
}
#tm-nblm-source-wrap{
  border:2px solid #111827;
  border-radius:12px;
  padding:8px;
  background:#d9f99d;
  display:flex; flex-direction:column; gap:6px;
}
#tm-nblm-source-top{
  display:flex; align-items:center; justify-content:space-between; gap:8px;
}
#tm-nblm-source-count{ color:#14532d; font-size:12px; font-weight:700; }
#tm-nblm-source-actions{ display:flex; gap:6px; }
#tm-nblm-source-actions button{
  border:2px solid #111827;
  border-radius:999px;
  background:#fef3c7;
  color:#7c2d12;
  font-weight:700;
  padding:4px 8px; cursor:pointer;
}
#tm-nblm-source-list{
  max-height:160px; overflow:auto; display:flex; flex-direction:column; gap:4px;
}
.tm-nblm-source-item{
  display:grid; grid-template-columns:auto 1fr; gap:8px; align-items:center;
  border-radius:10px;
  padding:6px;
  background:#f7fee7;
  border:2px solid #a3e635;
}
.tm-nblm-source-item:hover{ background:#ecfccb; }
.tm-nblm-source-text{ min-width:0; display:flex; flex-direction:column; }
.tm-nblm-source-title{
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  color:#14532d;
}
.tm-nblm-source-id{
  color:#4d7c0f; font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#tm-nblm-config-wrap{
  border:2px solid #111827;
  border-radius:12px;
  padding:8px;
  background:#dbeafe;
  display:flex; flex-direction:column; gap:8px;
}
.tm-nblm-config-item{
  display:flex; flex-direction:column; gap:6px;
}
.tm-nblm-config-item label{
  color:#1e3a8a;
  font-size:12px;
  font-weight:700;
}
.tm-nblm-config-item textarea{
  width:100%; min-height:58px; max-height:140px; resize:vertical;
  border:2px solid #111827;
  border-radius:10px;
  padding:8px;
  background:#eff6ff;
  color:#111827;
}
.tm-nblm-config-item textarea::placeholder{
  color:#6b7280; opacity:1;
}
#tm-nblm-options{
  display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;
}
#tm-nblm-options label{ display:flex; align-items:center; gap:6px; color:#7f1d1d; font-weight:700; }
#tm-nblm-actions{ display:flex; gap:8px; flex-wrap:wrap; }
#tm-nblm-actions button{
  border:2px solid #111827;
  border-radius:10px;
  padding:8px 10px;
  cursor:pointer;
  color:#fff;
}
#tm-nblm-ask{
  background:linear-gradient(180deg,#fde047,#f59e0b);
  color:#3f2400;
  font-weight:800;
  box-shadow:0 6px 14px rgba(146,64,14,.3);
}
#tm-nblm-ask:hover:not(:disabled){
  filter:brightness(1.03);
}
#tm-nblm-ask:active:not(:disabled){
  transform:translateY(1px);
}
#tm-nblm-actions button:disabled{
  opacity:.55; cursor:not-allowed;
}
#tm-nblm-status{
  font-size:12px;
  color:#7f1d1d;
  font-weight:700;
}
#tm-nblm-chat-context-wrap,
#tm-nblm-chat-context-fab{
  display:none !important;
}
#tm-nblm-ask-send-float{
  position:fixed;
  display:none;
  align-items:center;
  justify-content:center;
  width:var(--tm-nblm-ask-size, 40px);
  height:var(--tm-nblm-ask-size, 40px);
  border:2px solid #111827;
  border-radius:999px;
  padding:0;
  background:
    linear-gradient(180deg,#ef4444 0 47%, #111827 47% 53%, #f8fafc 53% 100%);
  color:#f8fafc;
  text-shadow:0 1px 0 rgba(0,0,0,.45);
  font:800 12px/1 "Trebuchet MS","Noto Sans SC","Microsoft YaHei",sans-serif;
  letter-spacing:.1px;
  box-shadow:0 8px 20px rgba(17,24,39,.36);
  white-space:nowrap;
  backdrop-filter:saturate(120%) blur(4px);
  -webkit-backdrop-filter:saturate(120%) blur(4px);
  z-index:12010;
  transform:translateZ(0);
  pointer-events:auto;
  transition:box-shadow .2s ease, transform .2s ease, opacity .2s ease;
  cursor:pointer;
  overflow:hidden;
}
#tm-nblm-ask-send-float.tm-show{
  display:inline-flex;
}
body.tm-nblm-panel-open #tm-nblm-ask-send-float{
  opacity:0;
  pointer-events:none;
  transform:translateY(8px) scale(.94);
}
#tm-nblm-ask-send-float::after{
  content:"";
  position:absolute;
  inset:-2px;
  border-radius:inherit;
  border:1px solid rgba(255,255,255,.45);
  opacity:0;
  transform:scale(.93);
  transition:opacity .2s ease, transform .24s ease;
  pointer-events:none;
}
#tm-nblm-ask-send-float:hover:not(:disabled):not(.tm-loading){
  transform:translateY(-1px);
  box-shadow:
    0 14px 28px rgba(17,24,39,.4),
    inset 0 1px 0 rgba(255,255,255,.42),
    inset 0 -12px 20px rgba(0,0,0,.25);
  filter:saturate(108%);
}
#tm-nblm-ask-send-float:hover:not(:disabled):not(.tm-loading)::after{
  opacity:1;
  transform:scale(1);
}
#tm-nblm-ask-send-float:active:not(:disabled):not(.tm-loading){
  transform:translateY(0) scale(.97);
}
#tm-nblm-ask-send-float.tm-loading{
  font-size:0;
  color:transparent;
  pointer-events:none;
  background:
    linear-gradient(180deg,#f87171 0 47%, #111827 47% 53%, #ffffff 53% 100%);
  background-size:180% 180%;
  animation:tm-nblm-ask-pulse 1.1s ease-in-out infinite, tm-nblm-ask-sheen 1.8s ease-in-out infinite;
}
#tm-nblm-ask-send-float.tm-loading::before{
  content:"";
  width:var(--tm-nblm-spinner-size, 16px);
  height:var(--tm-nblm-spinner-size, 16px);
  border:var(--tm-nblm-spinner-border, 2px) solid transparent;
  border-top-color:#fff;
  border-right-color:rgba(255,255,255,.92);
  border-radius:999px;
  animation:tm-nblm-ask-spin .75s linear infinite;
  filter:drop-shadow(0 0 4px rgba(255,255,255,.35));
}
#tm-nblm-ask-send-float.tm-loading::after{
  opacity:1;
  transform:scale(1);
  animation:tm-nblm-ask-ring 1.3s ease-out infinite;
}
#tm-nblm-ask-send-float:disabled{
  opacity:.6;
  cursor:not-allowed;
}
@keyframes tm-nblm-ask-spin{
  from{ transform:rotate(0deg); }
  to{ transform:rotate(360deg); }
}
@keyframes tm-nblm-ask-pulse{
  0%,100%{
    box-shadow:
      0 10px 22px rgba(17,24,39,.34),
      inset 0 1px 0 rgba(255,255,255,.34),
      inset 0 -10px 16px rgba(0,0,0,.28);
  }
  50%{
    box-shadow:
      0 14px 28px rgba(127,29,29,.46),
      inset 0 1px 0 rgba(255,255,255,.45),
      inset 0 -12px 18px rgba(0,0,0,.34);
  }
}
@keyframes tm-nblm-ask-sheen{
  0%{ background-position:0% 50%; }
  50%{ background-position:100% 50%; }
  100%{ background-position:0% 50%; }
}
@keyframes tm-nblm-ask-ring{
  0%{ opacity:.45; transform:scale(.88); }
  70%{ opacity:0; transform:scale(1.22); }
  100%{ opacity:0; transform:scale(1.22); }
}
#tm-nblm-chat-context{
  width:100%;
  flex:1 1 auto;
  min-height:74px;
  height:100%;
  resize:none;
  border:1px solid #cfd4dc;
  border-radius:8px;
  padding:8px;
  box-sizing:border-box;
  font:12px/1.45 "Noto Sans SC","Microsoft YaHei",sans-serif;
  background:#fff;
  color:#111827;
  cursor:text;
}
#tm-nblm-chat-context::placeholder{
  color:#6b7280;
}
`;
    document.head.appendChild(style);
  }

  function createUI() {
    ui.toggle = document.createElement("button");
    ui.toggle.id = "tm-nblm-toggle";
    ui.toggle.textContent = "NotebookLM";
    ui.toggle.setAttribute("aria-label", "NotebookLM 控制面板");
    ui.toggle.title = "NotebookLM 控制面板";

    ui.root = document.createElement("div");
    ui.root.id = "tm-nblm-root";

    const head = document.createElement("div");
    head.id = "tm-nblm-head";
    const title = document.createElement("div");
    title.id = "tm-nblm-title";
    title.textContent = "控制面板";
    const close = document.createElement("button");
    close.id = "tm-nblm-close";
    close.textContent = "收起";
    head.appendChild(title);
    head.appendChild(close);

    const body = document.createElement("div");
    body.id = "tm-nblm-body";

    const row = document.createElement("div");
    row.id = "tm-nblm-row";
    ui.notebookSelect = document.createElement("select");
    ui.notebookSelect.id = "tm-nblm-notebook";
    ui.reloadBtn = document.createElement("button");
    ui.reloadBtn.id = "tm-nblm-reload";
    ui.reloadBtn.textContent = "刷新";
    row.appendChild(ui.notebookSelect);
    row.appendChild(ui.reloadBtn);

    const sourceWrap = document.createElement("div");
    sourceWrap.id = "tm-nblm-source-wrap";
    const sourceTop = document.createElement("div");
    sourceTop.id = "tm-nblm-source-top";
    ui.sourceCount = document.createElement("div");
    ui.sourceCount.id = "tm-nblm-source-count";
    ui.sourceCount.textContent = "来源: 0";
    const sourceActions = document.createElement("div");
    sourceActions.id = "tm-nblm-source-actions";
    ui.selectAllBtn = document.createElement("button");
    ui.selectAllBtn.textContent = "全选";
    ui.clearAllBtn = document.createElement("button");
    ui.clearAllBtn.textContent = "全不选";
    sourceActions.appendChild(ui.selectAllBtn);
    sourceActions.appendChild(ui.clearAllBtn);
    sourceTop.appendChild(ui.sourceCount);
    sourceTop.appendChild(sourceActions);
    ui.sourceList = document.createElement("div");
    ui.sourceList.id = "tm-nblm-source-list";
    sourceWrap.appendChild(sourceTop);
    sourceWrap.appendChild(ui.sourceList);

    const configWrap = document.createElement("div");
    configWrap.id = "tm-nblm-config-wrap";

    const notebookPromptItem = document.createElement("div");
    notebookPromptItem.className = "tm-nblm-config-item";
    const notebookPromptLabel = document.createElement("label");
    notebookPromptLabel.htmlFor = "tm-nblm-notebook-system-prompt";
    notebookPromptLabel.textContent = "NotebookLM 系统提示词(可选)";
    ui.notebookSystemPromptInput = document.createElement("textarea");
    ui.notebookSystemPromptInput.id = "tm-nblm-notebook-system-prompt";
    ui.notebookSystemPromptInput.placeholder =
      "例如:请只基于已选来源回答,不确定时明确说明。";
    ui.notebookSystemPromptInput.value = state.notebookSystemPrompt || "";
    notebookPromptItem.appendChild(notebookPromptLabel);
    notebookPromptItem.appendChild(ui.notebookSystemPromptInput);

    const mergePromptItem = document.createElement("div");
    mergePromptItem.className = "tm-nblm-config-item";
    const mergePromptLabel = document.createElement("label");
    mergePromptLabel.htmlFor = "tm-nblm-merge-instruction-prompt";
    mergePromptLabel.textContent = "拼接提示词模板(用于背景内容)";
    ui.mergeInstructionPromptInput = document.createElement("textarea");
    ui.mergeInstructionPromptInput.id = "tm-nblm-merge-instruction-prompt";
    ui.mergeInstructionPromptInput.placeholder =
      "支持占位符:{{notebook}} {{question}} {{answer}}";
    ui.mergeInstructionPromptInput.value = state.mergeInstructionPrompt || "";
    mergePromptItem.appendChild(mergePromptLabel);
    mergePromptItem.appendChild(ui.mergeInstructionPromptInput);

    configWrap.appendChild(notebookPromptItem);
    configWrap.appendChild(mergePromptItem);

    const options = document.createElement("div");
    options.id = "tm-nblm-options";
    const left = document.createElement("div");
    left.style.display = "flex";
    left.style.gap = "12px";
    left.style.flexWrap = "wrap";
    const newConversationLabel = document.createElement("label");
    ui.newConversationCheckbox = document.createElement("input");
    ui.newConversationCheckbox.type = "checkbox";
    newConversationLabel.appendChild(ui.newConversationCheckbox);
    newConversationLabel.appendChild(document.createTextNode("每次新会话"));
    left.appendChild(newConversationLabel);
    options.appendChild(left);

    ui.status = document.createElement("div");
    ui.status.id = "tm-nblm-status";
    ui.status.textContent = "准备就绪";

    body.appendChild(row);
    body.appendChild(sourceWrap);
    body.appendChild(configWrap);
    body.appendChild(options);
    body.appendChild(ui.status);

    ui.root.appendChild(head);
    ui.root.appendChild(body);

    document.body.appendChild(ui.toggle);
    document.body.appendChild(ui.root);

    close.addEventListener("click", () => {
      state.panelOpen = false;
      syncPanelVisibility();
      persistState();
    });

    ui.toggle.addEventListener("click", () => {
      state.panelOpen = !state.panelOpen;
      syncPanelVisibility();
      persistState();
    });

    ui.reloadBtn.addEventListener("click", () => {
      void initializeData(true);
    });

    ui.notebookSelect.addEventListener("change", () => {
      state.selectedNotebookId = ui.notebookSelect.value;
      persistState();
      void onNotebookChanged();
    });

    ui.selectAllBtn.addEventListener("click", () => {
      setAllSourceSelection(true);
    });

    ui.clearAllBtn.addEventListener("click", () => {
      setAllSourceSelection(false);
    });

    ui.notebookSystemPromptInput.addEventListener("input", () => {
      state.notebookSystemPrompt = ui.notebookSystemPromptInput.value;
      persistState();
    });

    ui.mergeInstructionPromptInput.addEventListener("input", () => {
      state.mergeInstructionPrompt = ui.mergeInstructionPromptInput.value;
      persistState();
    });

  }

  function syncPanelVisibility() {
    if (!ui.root) return;
    const open = !!state.panelOpen;
    ui.root.classList.toggle("tm-hide", !open);
    document.body?.classList.toggle("tm-nblm-panel-open", open);
  }

  function ensurePanelUI() {
    const root = document.getElementById("tm-nblm-root");
    const toggle = document.getElementById("tm-nblm-toggle");
    const panelReady =
      root &&
      toggle &&
      root.querySelector("#tm-nblm-notebook") &&
      root.querySelector("#tm-nblm-status");

    if (panelReady) {
      ui.root = root;
      ui.toggle = toggle;
      ui.notebookSelect = root.querySelector("#tm-nblm-notebook");
      ui.reloadBtn = root.querySelector("#tm-nblm-reload");
      ui.status = root.querySelector("#tm-nblm-status");
      return;
    }

    root?.remove();
    toggle?.remove();
    createUI();
    syncPanelVisibility();
  }

  function setStatus(text) {
    ui.status.textContent = text;
  }

  function findChatSendButtonInForm(formEl) {
    if (!(formEl instanceof HTMLFormElement)) return null;
    const buttons = formEl.querySelectorAll("button");
    for (const btn of buttons) {
      if (!isLikelySendButton(btn)) continue;
      if (!isElementVisible(btn)) continue;
      return btn;
    }
    return null;
  }

  function findChatActionAnchorButtonInForm(formEl) {
    if (!(formEl instanceof HTMLFormElement)) return null;

    const sendBtn = findChatSendButtonInForm(formEl);
    if (sendBtn) return sendBtn;

    const buttons = [...formEl.querySelectorAll("button")].filter(
      (btn) => btn.id !== "tm-nblm-ask-send-float" && isElementVisible(btn)
    );
    for (const btn of buttons) {
      const marker = [
        btn.getAttribute("data-testid") || "",
        btn.getAttribute("aria-label") || "",
        btn.getAttribute("title") || "",
      ]
        .join(" ")
        .toLowerCase();
      if (
        marker.includes("voice") ||
        marker.includes("speech") ||
        marker.includes("microphone") ||
        marker.includes("语音") ||
        marker.includes("麦克风")
      ) {
        return btn;
      }
    }

    return buttons.length ? buttons[buttons.length - 1] : null;
  }

  function hideAskSendFloatingButton(btn) {
    if (!(btn instanceof HTMLButtonElement)) return;
    btn.classList.remove("tm-show");
  }

  function getRightActionClusterRect(formEl, anchorRect) {
    if (!(formEl instanceof HTMLFormElement)) return null;
    if (!anchorRect) return null;

    const anchorCenterY = anchorRect.top + anchorRect.height / 2;
    const rowTolerance = Math.max(20, Math.round(anchorRect.height * 1.25));
    const maxHorizontalSpan = 220;
    const clusterRects = [];

    for (const el of formEl.querySelectorAll("button")) {
      if (!(el instanceof HTMLButtonElement)) continue;
      if (el.id === "tm-nblm-ask-send-float") continue;
      if (!isElementVisible(el)) continue;
      const rect = el.getBoundingClientRect();
      if (rect.width <= 0 || rect.height <= 0) continue;

      const centerY = rect.top + rect.height / 2;
      if (Math.abs(centerY - anchorCenterY) > rowTolerance) continue;
      if (rect.left < anchorRect.left - maxHorizontalSpan) continue;
      if (rect.right > anchorRect.right + 24) continue;

      clusterRects.push(rect);
    }

    if (!clusterRects.length) return null;
    let left = Infinity;
    let right = -Infinity;
    for (const rect of clusterRects) {
      if (rect.left < left) left = rect.left;
      if (rect.right > right) right = rect.right;
    }
    if (!Number.isFinite(left) || !Number.isFinite(right)) return null;
    return { left, right };
  }

  function positionAskSendFloatingButton(btn, anchorBtn, formEl) {
    if (!(btn instanceof HTMLButtonElement)) return;
    if (!(anchorBtn instanceof HTMLButtonElement)) {
      hideAskSendFloatingButton(btn);
      return;
    }

    const rect = anchorBtn.getBoundingClientRect();
    const btnSize = Math.max(34, Math.min(44, Math.round(rect.height)));
    const gap = 8;
    const viewportPad = 8;

    if (rect.width <= 0 || rect.height <= 0) {
      hideAskSendFloatingButton(btn);
      return;
    }

    btn.style.setProperty("--tm-nblm-ask-size", `${btnSize}px`);
    btn.style.setProperty(
      "--tm-nblm-spinner-size",
      `${Math.max(12, Math.round(btnSize * 0.42))}px`
    );
    btn.style.setProperty(
      "--tm-nblm-spinner-border",
      `${Math.max(2, Math.round(btnSize * 0.08))}px`
    );

    const cluster = getRightActionClusterRect(formEl, rect);
    let left = (cluster ? cluster.left : rect.left) - btnSize - gap;
    let top = rect.top + (rect.height - btnSize) / 2;

    if (left < viewportPad) {
      left = (cluster ? cluster.right : rect.right) + gap;
    }
    left = Math.max(viewportPad, Math.min(left, window.innerWidth - btnSize - viewportPad));
    top = Math.max(viewportPad, Math.min(top, window.innerHeight - btnSize - viewportPad));

    btn.style.left = `${Math.round(left)}px`;
    btn.style.top = `${Math.round(top)}px`;
    btn.classList.add("tm-show");
  }

  function ensureAskSendFloatingButton() {
    if (!isChatGPTPage()) return null;

    let btn = document.getElementById("tm-nblm-ask-send-float");
    if (!btn) {
      btn = document.createElement("button");
      btn.id = "tm-nblm-ask-send-float";
      btn.type = "button";
      btn.textContent = "查";
      btn.setAttribute("aria-label", "查背景并发送");
      btn.title = ASK_SEND_BUTTON_TITLE;
      btn.addEventListener("click", () => {
        if (chatAutoAskInFlight) return;
        const inputEl = findChatInputElement();
        if (!inputEl) {
          setStatus("未找到 ChatGPT 输入框");
          return;
        }
        const formEl = inputEl.closest("form");
        if (!(formEl instanceof HTMLFormElement)) {
          setStatus("未找到发送表单");
          return;
        }
        const questionText = extractQuestionTextFromChatInput(readInputText(inputEl));
        if (!questionText) {
          setStatus("请先在 ChatGPT 输入框中输入问题");
          return;
        }
        void autoAskNotebookForChatSubmit(formEl, inputEl, questionText);
      });
    }

    if (btn.parentElement !== document.body) {
      document.body.appendChild(btn);
    }

    const inputEl = findChatInputElement();
    const formEl = inputEl?.closest("form") || document.querySelector("form");
    if (!(formEl instanceof HTMLFormElement)) {
      hideAskSendFloatingButton(btn);
      return null;
    }

    const anchorBtn = findChatActionAnchorButtonInForm(formEl);
    if (!(anchorBtn instanceof HTMLButtonElement)) {
      hideAskSendFloatingButton(btn);
      return null;
    }

    positionAskSendFloatingButton(btn, anchorBtn, formEl);
    ui.askSendFloatingBtn = btn;
    return btn;
  }

  function setAskSendFloatingLoading(loading) {
    const btn =
      ui.askSendFloatingBtn ||
      document.getElementById("tm-nblm-ask-send-float");
    if (!(btn instanceof HTMLButtonElement)) return;
    const on = !!loading;
    btn.classList.toggle("tm-loading", on);
    btn.setAttribute("aria-busy", on ? "true" : "false");
    btn.title = on ? "NotebookLM 查询中..." : ASK_SEND_BUTTON_TITLE;
  }

  function setBusy(busy) {
    if (ui.askBtn) ui.askBtn.disabled = busy;
    ui.reloadBtn.disabled = busy;
    ui.notebookSelect.disabled = busy;
    if (ui.askSendFloatingBtn) {
      ui.askSendFloatingBtn.disabled = busy;
    }
  }

  function bindAskSendFloatingButtonPosition() {
    if (askSendFloatingPositionBound) return;
    askSendFloatingPositionBound = true;

    let timer = 0;
    const schedule = () => {
      if (timer) return;
      timer = window.setTimeout(() => {
        timer = 0;
        ensureAskSendFloatingButton();
      }, 80);
    };

    window.addEventListener("resize", schedule);
    window.addEventListener("scroll", schedule, true);
  }

  function getDefaultChatContextLayout() {
    const width = Math.max(360, Math.min(920, Math.floor(window.innerWidth * 0.62)));
    const height = 220;
    const x = Math.max(12, Math.floor((window.innerWidth - width) / 2));
    const y = Math.max(12, window.innerHeight - height - 24);
    return { x, y, width, height, borderRadius: 10 };
  }

  function normalizeChatContextLayout(rawLayout) {
    const defaults = getDefaultChatContextLayout();
    const base = rawLayout && typeof rawLayout === "object" ? rawLayout : defaults;
    const minWidth = 320;
    const minHeight = 140;
    const maxWidth = Math.max(minWidth, window.innerWidth - 12);
    const maxHeight = Math.max(minHeight, window.innerHeight - 12);
    const widthRaw = Number(base.width);
    const heightRaw = Number(base.height);
    const xRaw = Number(base.x);
    const yRaw = Number(base.y);
    const borderRadiusRaw = Number(base.borderRadius);
    const width = Math.max(
      minWidth,
      Math.min(maxWidth, Number.isFinite(widthRaw) ? widthRaw : defaults.width)
    );
    const height = Math.max(
      minHeight,
      Math.min(maxHeight, Number.isFinite(heightRaw) ? heightRaw : defaults.height)
    );
    const maxX = Math.max(0, window.innerWidth - width);
    const maxY = Math.max(0, window.innerHeight - height);
    const x = Math.max(0, Math.min(maxX, Number.isFinite(xRaw) ? xRaw : defaults.x));
    const y = Math.max(0, Math.min(maxY, Number.isFinite(yRaw) ? yRaw : defaults.y));
    const borderRadius = Math.max(
      0,
      Math.min(28, Number.isFinite(borderRadiusRaw) ? borderRadiusRaw : defaults.borderRadius)
    );
    return { x, y, width, height, borderRadius };
  }

  function applyChatContextLayout(wrapEl, layout) {
    if (!wrapEl || !layout) return;
    chatContextApplyingLayout = true;
    wrapEl.style.left = `${Math.round(layout.x)}px`;
    wrapEl.style.top = `${Math.round(layout.y)}px`;
    wrapEl.style.width = `${Math.round(layout.width)}px`;
    wrapEl.style.height = `${Math.round(layout.height)}px`;
    wrapEl.style.borderRadius = `${Math.round(layout.borderRadius)}px`;
    chatContextApplyingLayout = false;
  }

  function setChatContextLayout(nextLayout, persist = false) {
    state.chatContextLayout = normalizeChatContextLayout(nextLayout);
    if (ui.chatContextWrap) {
      applyChatContextLayout(ui.chatContextWrap, state.chatContextLayout);
    }
    updateChatContextFabPosition();
    if (persist) persistState();
  }

  function updateChatContextFabPosition() {
    if (!ui.chatContextFab) return;
    const anchor =
      state.chatContextFabPos && typeof state.chatContextFabPos === "object"
        ? state.chatContextFabPos
        : state.chatContextLayout || getDefaultChatContextLayout();
    const left = Number(anchor?.x);
    const top = Number(anchor?.y);
    if (Number.isFinite(left)) {
      ui.chatContextFab.style.left = `${Math.round(left)}px`;
    }
    if (Number.isFinite(top)) {
      ui.chatContextFab.style.top = `${Math.round(top)}px`;
    }
  }

  function setChatContextMinimized(minimized, persist = false) {
    state.chatContextMinimized = !!minimized;
    if (state.chatContextMinimized && !state.chatContextFabPos) {
      const rect = ui.chatContextWrap?.getBoundingClientRect();
      const base = state.chatContextLayout || getDefaultChatContextLayout();
      state.chatContextFabPos = {
        x: Math.round(rect?.left ?? base.x),
        y: Math.round(rect?.top ?? base.y),
      };
    }
    if (ui.chatContextWrap) {
      ui.chatContextWrap.classList.toggle("tm-minimized", state.chatContextMinimized);
    }
    if (ui.chatContextFab) {
      updateChatContextFabPosition();
      ui.chatContextFab.classList.toggle("tm-show", state.chatContextMinimized);
    }
    if (persist) persistState();
  }

  function bindChatContextFabDrag(fabEl) {
    if (!fabEl || fabEl.dataset.tmFabDragBound) return;
    fabEl.dataset.tmFabDragBound = "1";
    fabEl.addEventListener("mousedown", (event) => {
      if (event.button !== 0) return;
      event.preventDefault();

      const rect = fabEl.getBoundingClientRect();
      const startX = event.clientX;
      const startY = event.clientY;
      const startLeft = rect.left;
      const startTop = rect.top;
      let moved = false;
      const prevUserSelect = document.body.style.userSelect;
      document.body.style.userSelect = "none";

      const onMove = (moveEvent) => {
        const dx = moveEvent.clientX - startX;
        const dy = moveEvent.clientY - startY;
        if (!moved && (Math.abs(dx) > 2 || Math.abs(dy) > 2)) {
          moved = true;
        }
        const left = Math.round(startLeft + dx);
        const top = Math.round(startTop + dy);
        fabEl.style.left = `${left}px`;
        fabEl.style.top = `${top}px`;
      };

      const onUp = () => {
        document.body.style.userSelect = prevUserSelect;
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        if (!moved) return;

        chatContextFabJustDragged = true;
        window.setTimeout(() => {
          chatContextFabJustDragged = false;
        }, 90);

        const left = Number.parseInt(fabEl.style.left, 10);
        const top = Number.parseInt(fabEl.style.top, 10);
        if (Number.isFinite(left) && Number.isFinite(top)) {
          state.chatContextFabPos = { x: left, y: top };
          persistState();
        }
      };

      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
    });
  }

  function isPointNearBorder(clientX, clientY, rect, edge = 16) {
    const nearLeft = clientX - rect.left <= edge;
    const nearRight = rect.right - clientX <= edge;
    const nearTop = clientY - rect.top <= edge;
    const nearBottom = rect.bottom - clientY <= edge;
    return nearLeft || nearRight || nearTop || nearBottom;
  }

  function bindChatContextDrag(wrapEl) {
    if (!wrapEl || wrapEl.dataset.tmDragBound) return;
    wrapEl.dataset.tmDragBound = "1";
    const dragEdge = 24;
    const resizeEdge = 6;

    wrapEl.addEventListener("dblclick", (event) => {
      const rect = wrapEl.getBoundingClientRect();
      if (!isPointNearBorder(event.clientX, event.clientY, rect, dragEdge)) return;
      setChatContextMinimized(true, true);
      event.preventDefault();
    });

    wrapEl.addEventListener("mousedown", (event) => {
      if (event.button !== 0) return;
      const rect = wrapEl.getBoundingClientRect();
      if (!isPointNearBorder(event.clientX, event.clientY, rect, dragEdge)) return;

      const resizeDirection = getChatContextResizeDirection(
        event.clientX,
        event.clientY,
        rect,
        resizeEdge
      );
      if (resizeDirection) return;

      const startX = event.clientX;
      const startY = event.clientY;
      const startLeft = rect.left;
      const startTop = rect.top;
      const prevUserSelect = document.body.style.userSelect;
      document.body.style.userSelect = "none";

      const onMove = (moveEvent) => {
        const next = {
          ...(state.chatContextLayout || getDefaultChatContextLayout()),
          x: startLeft + (moveEvent.clientX - startX),
          y: startTop + (moveEvent.clientY - startY),
        };
        setChatContextLayout(next, false);
      };

      const onUp = () => {
        document.body.style.userSelect = prevUserSelect;
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        persistState();
      };

      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
      event.preventDefault();
      event.stopPropagation();
    }, true);
  }

  function getChatContextResizeDirection(clientX, clientY, rect, edge = 16) {
    const nearLeft = clientX - rect.left <= edge;
    const nearRight = rect.right - clientX <= edge;
    const nearTop = clientY - rect.top <= edge;
    const nearBottom = rect.bottom - clientY <= edge;

    if (nearTop && nearLeft) return "nw";
    if (nearTop && nearRight) return "ne";
    if (nearBottom && nearLeft) return "sw";
    if (nearBottom && nearRight) return "se";
    if (nearTop) return "n";
    if (nearBottom) return "s";
    if (nearLeft) return "w";
    if (nearRight) return "e";
    return "";
  }

  function resizeCursorForDirection(direction) {
    if (!direction) return "default";
    if (direction === "n" || direction === "s") return "ns-resize";
    if (direction === "e" || direction === "w") return "ew-resize";
    if (direction === "ne" || direction === "sw") return "nesw-resize";
    if (direction === "nw" || direction === "se") return "nwse-resize";
    return "default";
  }

  function setChatContextResizeCursor(wrapEl, cursor) {
    if (wrapEl) wrapEl.style.cursor = cursor;
    if (ui.chatContextTextarea) {
      ui.chatContextTextarea.style.cursor = cursor === "default" ? "text" : cursor;
    }
  }

  function bindChatContextWindowLikeResize(wrapEl) {
    if (!wrapEl || wrapEl.dataset.tmWindowResizeBound) return;
    wrapEl.dataset.tmWindowResizeBound = "1";

    const resizeEdge = 6;
    const dragEdge = 24;
    let resizing = false;

    wrapEl.addEventListener("mousemove", (event) => {
      if (resizing) return;
      const rect = wrapEl.getBoundingClientRect();
      const direction = getChatContextResizeDirection(
        event.clientX,
        event.clientY,
        rect,
        resizeEdge
      );
      if (direction) {
        setChatContextResizeCursor(wrapEl, resizeCursorForDirection(direction));
      } else if (isPointNearBorder(event.clientX, event.clientY, rect, dragEdge)) {
        setChatContextResizeCursor(wrapEl, "move");
      } else {
        setChatContextResizeCursor(wrapEl, "default");
      }
    });

    wrapEl.addEventListener("mouseleave", () => {
      if (!resizing) setChatContextResizeCursor(wrapEl, "default");
    });

    wrapEl.addEventListener(
      "mousedown",
      (event) => {
        if (event.button !== 0) return;

        const rect = wrapEl.getBoundingClientRect();
        const direction = getChatContextResizeDirection(
          event.clientX,
          event.clientY,
          rect,
          resizeEdge
        );
        if (!direction) return;

        event.preventDefault();
        event.stopImmediatePropagation();
        resizing = true;
        setChatContextResizeCursor(wrapEl, resizeCursorForDirection(direction));
        const startX = event.clientX;
        const startY = event.clientY;
        const startLayout = state.chatContextLayout || getDefaultChatContextLayout();
        const base = {
          x: startLayout.x,
          y: startLayout.y,
          width: startLayout.width,
          height: startLayout.height,
          borderRadius: startLayout.borderRadius,
        };
        const prevUserSelect = document.body.style.userSelect;
        document.body.style.userSelect = "none";

        const onMove = (moveEvent) => {
          const dx = moveEvent.clientX - startX;
          const dy = moveEvent.clientY - startY;
          const next = { ...base };

          if (direction.includes("e")) next.width = base.width + dx;
          if (direction.includes("s")) next.height = base.height + dy;
          if (direction.includes("w")) {
            next.width = base.width - dx;
            next.x = base.x + dx;
          }
          if (direction.includes("n")) {
            next.height = base.height - dy;
            next.y = base.y + dy;
          }

          setChatContextLayout(next, false);
        };

        const onUp = () => {
          resizing = false;
          document.body.style.userSelect = prevUserSelect;
          window.removeEventListener("mousemove", onMove);
          window.removeEventListener("mouseup", onUp);
          persistState();
          setChatContextResizeCursor(wrapEl, "default");
        };

        window.addEventListener("mousemove", onMove);
        window.addEventListener("mouseup", onUp);
        event.stopPropagation();
      },
      true
    );
  }

  function bindChatContextResizeObserver(wrapEl) {
    if (!wrapEl) return;
    if (!chatContextResizeObserver) {
      chatContextResizeObserver = new ResizeObserver((entries) => {
        if (chatContextApplyingLayout) return;
        const entry = entries[0];
        if (!entry) return;
        const rect = entry.contentRect;
        const next = {
          ...(state.chatContextLayout || getDefaultChatContextLayout()),
          width: Math.round(rect.width),
          height: Math.round(rect.height),
        };
        setChatContextLayout(next, false);
        if (chatContextResizePersistTimer) {
          clearTimeout(chatContextResizePersistTimer);
        }
        chatContextResizePersistTimer = window.setTimeout(() => {
          persistState();
        }, 180);
      });
    }
    if (chatContextResizeObservedEl === wrapEl) return;
    if (chatContextResizeObservedEl) {
      chatContextResizeObserver.unobserve(chatContextResizeObservedEl);
    }
    chatContextResizeObserver.observe(wrapEl);
    chatContextResizeObservedEl = wrapEl;
  }

  function bindChatContextWindowResize() {
    if (chatContextWindowResizeBound) return;
    chatContextWindowResizeBound = true;
    window.addEventListener("resize", () => {
      if (!ui.chatContextWrap) return;
      setChatContextLayout(state.chatContextLayout || getDefaultChatContextLayout(), true);
    });
  }

  function nextReqId() {
    state.reqid += 100000;
    return state.reqid;
  }

  function extractByRegex(text, regex) {
    const m = text.match(regex);
    return m ? m[1] : "";
  }

  function isHttpOk(status) {
    return status >= 200 && status < 300;
  }

  function makeHttpError(message, status = 0, body = "") {
    const err = new Error(message);
    err.status = status;
    err.body = body;
    return err;
  }

  function httpRequest({ method, url, headers = {}, data = "", timeoutMs = 30000 }) {
    if (typeof GM_xmlhttpRequest === "function") {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method,
          url,
          headers,
          data,
          timeout: timeoutMs,
          responseType: "text",
          withCredentials: true,
          anonymous: false,
          onload: (resp) => resolve(resp),
          ontimeout: () => reject(makeHttpError(`请求超时: ${url}`)),
          onerror: () => reject(makeHttpError(`请求失败: ${url}`)),
          onabort: () => reject(makeHttpError(`请求中断: ${url}`)),
        });
      });
    }

    return fetch(url, {
      method,
      credentials: "include",
      headers,
      body: data || undefined,
    }).then(async (resp) => ({
      status: resp.status,
      responseText: await resp.text(),
      finalUrl: resp.url,
    }));
  }

  function parsePageTokensFromHtml(html) {
    const csrfToken = extractByRegex(html, /"SNlM0e"\s*:\s*"([^"]+)"/);
    const sessionId =
      extractByRegex(html, /"FdrFJe"\s*:\s*"([^"]+)"/) ||
      extractByRegex(html, /"f\.sid"\s*:\s*"([^"]+)"/);
    const bl =
      extractByRegex(html, /(boq_labs-tailwind-frontend_[0-9.]+_p\d+)/) || DEFAULT_BL;
    return { csrfToken, sessionId, bl };
  }

  async function getPageTokens(forceRefresh = false) {
    const freshEnough = Date.now() - tokenCache.fetchedAt < TOKEN_CACHE_TTL_MS;
    if (!forceRefresh && tokenCache.sessionId && freshEnough) {
      return {
        csrfToken: tokenCache.csrfToken,
        sessionId: tokenCache.sessionId,
        bl: tokenCache.bl || DEFAULT_BL,
      };
    }

    if (location.hostname === "notebooklm.google.com") {
      const localHtml =
        document.documentElement?.outerHTML ||
        document.documentElement?.innerHTML ||
        "";
      const localTokens = parsePageTokensFromHtml(localHtml);
      if (localTokens.sessionId) {
        tokenCache.csrfToken = localTokens.csrfToken || "";
        tokenCache.sessionId = localTokens.sessionId;
        tokenCache.bl = localTokens.bl || DEFAULT_BL;
        tokenCache.fetchedAt = Date.now();
        return {
          csrfToken: tokenCache.csrfToken,
          sessionId: tokenCache.sessionId,
          bl: tokenCache.bl || DEFAULT_BL,
        };
      }
    }

    const resp = await httpRequest({
      method: "GET",
      url: NOTEBOOKLM_HOME_URL,
    });
    const status = Number(resp?.status || 0);
    const html = String(resp?.responseText || "");
    const finalUrl = String(resp?.finalUrl || "").toLowerCase();

    if (!isHttpOk(status)) {
      throw makeHttpError(`加载 NotebookLM 页面失败: HTTP ${status}`, status, html);
    }

    const tokens = parsePageTokensFromHtml(html);
    if (tokens.sessionId) {
      tokenCache.csrfToken = tokens.csrfToken || "";
      tokenCache.sessionId = tokens.sessionId;
      tokenCache.bl = tokens.bl || DEFAULT_BL;
      tokenCache.fetchedAt = Date.now();
      return {
        csrfToken: tokenCache.csrfToken,
        sessionId: tokenCache.sessionId,
        bl: tokenCache.bl || DEFAULT_BL,
      };
    }

    const looksLikeGoogleLoginPage =
      finalUrl.includes("accounts.google.com") ||
      /ServiceLogin|signin\/v2\/identifier|identifierId/i.test(html);
    if (looksLikeGoogleLoginPage) {
      throw makeHttpError(
        "NotebookLM 未登录。请先在浏览器打开 https://notebooklm.google.com/ 完成登录。",
        status,
        html
      );
    }
    throw makeHttpError(
      "未读取到 NotebookLM 会话参数(f.sid)。请确认已登录,且浏览器未拦截跨站 Cookie。",
      status,
      html
    );
  }

  function stripAntiXssi(text) {
    return text.replace(/^\)\]\}'\r?\n/, "");
  }

  function parseChunkedResponse(raw) {
    const cleaned = stripAntiXssi(raw).trim();
    if (!cleaned) return [];
    const lines = cleaned.split("\n");
    const chunks = [];
    let i = 0;
    while (i < lines.length) {
      const line = lines[i].trim();
      if (!line) {
        i += 1;
        continue;
      }
      if (/^\d+$/.test(line)) {
        i += 1;
        if (i < lines.length) {
          try {
            chunks.push(JSON.parse(lines[i]));
          } catch (_) {
            // ignore malformed chunk
          }
        }
        i += 1;
      } else {
        try {
          chunks.push(JSON.parse(line));
        } catch (_) {
          // ignore malformed line
        }
        i += 1;
      }
    }
    return chunks;
  }

  function extractRpcResult(chunks, rpcId) {
    for (const chunk of chunks) {
      if (!Array.isArray(chunk)) continue;
      const items = chunk.length > 0 && Array.isArray(chunk[0]) ? chunk : [chunk];
      for (const item of items) {
        if (!Array.isArray(item) || item.length < 3) continue;
        if (item[0] === "er" && item[1] === rpcId) {
          throw new Error(`RPC 错误: ${item[2] ?? "unknown"}`);
        }
        if (item[0] === "wrb.fr" && item[1] === rpcId) {
          const payload = item[2];
          if (typeof payload === "string") {
            try {
              return JSON.parse(payload);
            } catch (_) {
              return payload;
            }
          }
          return payload;
        }
      }
    }
    return null;
  }

  async function rpcCall(rpcId, params, sourcePath = "/") {
    async function runWithTokens(forceRefresh) {
      const { csrfToken, sessionId } = await getPageTokens(forceRefresh);
      const query = new URLSearchParams({
        rpcids: rpcId,
        "source-path": sourcePath,
        "f.sid": sessionId,
        rt: "c",
      });

      const rpcRequest = [[[rpcId, JSON.stringify(params), null, "generic"]]];
      const bodyParts = [`f.req=${encodeURIComponent(JSON.stringify(rpcRequest))}`];
      if (csrfToken) bodyParts.push(`at=${encodeURIComponent(csrfToken)}`);
      const body = `${bodyParts.join("&")}&`;

      const resp = await httpRequest({
        method: "POST",
        url: `${BATCHEXECUTE_URL}?${query.toString()}`,
        headers: {
          "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
          "x-same-domain": "1",
        },
        data: body,
      });

      const status = Number(resp?.status || 0);
      const text = String(resp?.responseText || "");
      if (!isHttpOk(status)) {
        throw makeHttpError(`RPC HTTP ${status}`, status, text);
      }

      const chunks = parseChunkedResponse(text);
      const result = extractRpcResult(chunks, rpcId);
      if (result === null || result === undefined) {
        throw makeHttpError(`RPC ${rpcId} 返回空数据`, status, text);
      }
      return result;
    }

    try {
      return await runWithTokens(false);
    } catch (err) {
      const status = Number(err?.status || 0);
      if (![400, 401, 403].includes(status)) throw err;
      return await runWithTokens(true);
    }
  }

  function parseNotebook(raw) {
    if (!Array.isArray(raw)) return null;
    const id = typeof raw[2] === "string" ? raw[2] : "";
    if (!id) return null;
    const rawTitle = typeof raw[0] === "string" ? raw[0] : "";
    const title = rawTitle.replace("thought\n", "").trim() || "(未命名笔记本)";
    return { id, title };
  }

  function parseSource(raw) {
    if (!Array.isArray(raw) || raw.length === 0) return null;
    const sourceId = Array.isArray(raw[0]) ? raw[0][0] : raw[0];
    if (typeof sourceId !== "string" || !sourceId) return null;
    const title = typeof raw[1] === "string" && raw[1].trim() ? raw[1] : "未命名来源";
    return { id: sourceId, title };
  }

  async function fetchSourcesForNotebook(notebookId) {
    const result = await rpcCall(
      RPC.GET_NOTEBOOK,
      [notebookId, null, [2], null, 0],
      `/notebook/${notebookId}`
    );
    let rawSources = [];
    if (
      Array.isArray(result) &&
      Array.isArray(result[0]) &&
      Array.isArray(result[0][1])
    ) {
      rawSources = result[0][1];
    }
    return rawSources.map(parseSource).filter(Boolean);
  }

  async function loadNotebooks() {
    const result = await rpcCall(RPC.LIST_NOTEBOOKS, [null, 1, null, [2]], "/");
    const rawList = Array.isArray(result) && Array.isArray(result[0]) ? result[0] : result;
    const notebooks = Array.isArray(rawList) ? rawList.map(parseNotebook).filter(Boolean) : [];
    state.notebooks = notebooks;
    renderNotebookOptions();
  }

  async function loadSources(notebookId) {
    state.sources = await fetchSourcesForNotebook(notebookId);

    const selected = new Set(state.sourceSelectionByNotebook[notebookId] || []);
    const availableIds = new Set(state.sources.map((s) => s.id));
    const filtered = [...selected].filter((id) => availableIds.has(id));
    const effective = filtered.length > 0 ? filtered : state.sources.map((s) => s.id);
    state.sourceSelectionByNotebook[notebookId] = effective;
    persistState();
    renderSources();
  }

  async function validateSelectedSourceIds(notebookId, selectedSourceIds) {
    const liveSources = await fetchSourcesForNotebook(notebookId);
    state.sources = liveSources;

    const availableIds = new Set(liveSources.map((s) => s.id));
    const filtered = (selectedSourceIds || []).filter((id) => availableIds.has(id));
    state.sourceSelectionByNotebook[notebookId] = filtered;
    persistState();
    renderSources();
    return filtered;
  }

  function renderNotebookOptions() {
    ui.notebookSelect.replaceChildren();
    if (!state.notebooks.length) {
      const option = document.createElement("option");
      option.value = "";
      option.textContent = "没有可用笔记本";
      ui.notebookSelect.appendChild(option);
      ui.notebookSelect.value = "";
      state.selectedNotebookId = "";
      return;
    }

    for (const nb of state.notebooks) {
      const option = document.createElement("option");
      option.value = nb.id;
      option.textContent = nb.title;
      ui.notebookSelect.appendChild(option);
    }

    const exists = state.notebooks.some((nb) => nb.id === state.selectedNotebookId);
    if (!exists) state.selectedNotebookId = state.notebooks[0].id;
    ui.notebookSelect.value = state.selectedNotebookId;
  }

  function renderSources() {
    const notebookId = state.selectedNotebookId;
    const selected = new Set(state.sourceSelectionByNotebook[notebookId] || []);
    ui.sourceList.replaceChildren();

    for (const src of state.sources) {
      const item = document.createElement("label");
      item.className = "tm-nblm-source-item";

      const cb = document.createElement("input");
      cb.type = "checkbox";
      cb.value = src.id;
      cb.checked = selected.has(src.id);
      cb.addEventListener("change", () => {
        const set = new Set(state.sourceSelectionByNotebook[notebookId] || []);
        if (cb.checked) set.add(src.id);
        else set.delete(src.id);
        state.sourceSelectionByNotebook[notebookId] = [...set];
        persistState();
        updateSourceCount();
      });

      const text = document.createElement("span");
      text.className = "tm-nblm-source-text";
      const title = document.createElement("span");
      title.className = "tm-nblm-source-title";
      title.textContent = src.title;
      const sid = document.createElement("span");
      sid.className = "tm-nblm-source-id";
      sid.textContent = src.id;
      text.appendChild(title);
      text.appendChild(sid);

      item.appendChild(cb);
      item.appendChild(text);
      ui.sourceList.appendChild(item);
    }

    updateSourceCount();
  }

  function updateSourceCount() {
    const notebookId = state.selectedNotebookId;
    const selected = state.sourceSelectionByNotebook[notebookId] || [];
    ui.sourceCount.textContent = `来源: 已选 ${selected.length} / 总计 ${state.sources.length}`;
  }

  function setAllSourceSelection(selectAll) {
    const notebookId = state.selectedNotebookId;
    if (!notebookId) return;
    state.sourceSelectionByNotebook[notebookId] = selectAll
      ? state.sources.map((s) => s.id)
      : [];
    persistState();
    renderSources();
  }

  function buildConversationHistory(conversationId) {
    const turns = state.historyByConversation[conversationId];
    if (!Array.isArray(turns) || turns.length === 0) return null;
    const history = [];
    for (const turn of turns) {
      history.push([turn.answer, null, 2]);
      history.push([turn.query, null, 1]);
    }
    return history;
  }

  async function askNotebook({ notebookId, question, sourceIds, forceNewConversation }) {
    const sourceArray = sourceIds.map((id) => [[id]]);

    let conversationId = null;
    if (!forceNewConversation) {
      conversationId = state.conversationByNotebook[notebookId] || null;
    }
    if (!conversationId) conversationId = crypto.randomUUID();

    const conversationHistory =
      forceNewConversation || !state.historyByConversation[conversationId]
        ? null
        : buildConversationHistory(conversationId);

    async function runWithTokens(forceRefresh) {
      const { csrfToken, sessionId, bl } = await getPageTokens(forceRefresh);
      const params = [
        sourceArray,
        question,
        conversationHistory,
        [2, null, [1]],
        conversationId,
      ];
      const fReq = JSON.stringify([null, JSON.stringify(params)]);
      const bodyParts = [`f.req=${encodeURIComponent(fReq)}`];
      if (csrfToken) bodyParts.push(`at=${encodeURIComponent(csrfToken)}`);
      const body = `${bodyParts.join("&")}&`;

      const query = new URLSearchParams({
        hl: "en",
        _reqid: String(nextReqId()),
        rt: "c",
        "f.sid": sessionId,
        bl: bl || DEFAULT_BL,
      });

      const resp = await httpRequest({
        method: "POST",
        url: `${QUERY_URL}?${query.toString()}`,
        headers: {
          "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
          "x-same-domain": "1",
        },
        data: body,
      });

      const status = Number(resp?.status || 0);
      const rawText = String(resp?.responseText || "");
      if (!isHttpOk(status)) {
        throw makeHttpError(`提问请求失败: HTTP ${status}`, status, rawText);
      }

      const answer = parseAskResponse(rawText);
      return { answer, conversationId, rawText };
    }

    try {
      return await runWithTokens(false);
    } catch (err) {
      const status = Number(err?.status || 0);
      if (![400, 401, 403].includes(status)) throw err;
      return await runWithTokens(true);
    }
  }

  function isLikelyNotebookThinkingText(text) {
    const normalized = String(text || "").trim().toLowerCase();
    if (!normalized) return false;
    if (
      /^\*\*(synthesizing|analyzing|integrating|reviewing|planning|drafting)\b/.test(
        normalized
      )
    ) {
      return true;
    }
    if (
      /^i'?m now (integrating|synthesizing|analyzing|reviewing|combining)\b/.test(
        normalized
      ) ||
      /^let me (integrate|synthesize|analyze|review|combine)\b/.test(normalized)
    ) {
      return true;
    }
    return false;
  }

  function parseAskResponse(rawText) {
    const cleaned = stripAntiXssi(rawText);
    const lines = cleaned.trim().split("\n");
    let lastAnswerAnyText = "";
    let lastAnswerNonThinkingText = "";

    function processChunk(jsonStr) {
      let data;
      try {
        data = JSON.parse(jsonStr);
      } catch (_) {
        return;
      }
      if (!Array.isArray(data)) return;

      for (const item of data) {
        if (!Array.isArray(item) || item.length < 3 || item[0] !== "wrb.fr") continue;
        if (typeof item[2] !== "string") continue;

        let inner;
        try {
          inner = JSON.parse(item[2]);
        } catch (_) {
          continue;
        }
        if (!Array.isArray(inner) || !Array.isArray(inner[0])) continue;

        const first = inner[0];
        const text = typeof first[0] === "string" ? first[0] : "";
        if (!text) continue;

        let isAnswer = false;
        if (text.length > 20 && Array.isArray(first[4])) {
          const tail = first[4][first[4].length - 1];
          if (tail === 1) isAnswer = true;
        }
        if (!isAnswer) continue;

        const isThinking = isLikelyNotebookThinkingText(text);
        lastAnswerAnyText = text;
        if (!isThinking) {
          lastAnswerNonThinkingText = text;
        }
      }
    }

    let i = 0;
    while (i < lines.length) {
      const line = lines[i].trim();
      if (!line) {
        i += 1;
        continue;
      }
      if (/^\d+$/.test(line)) {
        i += 1;
        if (i < lines.length) processChunk(lines[i]);
        i += 1;
      } else {
        processChunk(line);
        i += 1;
      }
    }
    return lastAnswerNonThinkingText || lastAnswerAnyText || "(no answer text extracted)";
  }

  function isChatGPTPage() {
    const host = location.hostname;
    return host === "chat.openai.com" || host === "chatgpt.com" || host.endsWith(".chatgpt.com");
  }

  function isElementVisible(el) {
    if (!el) return false;
    const style = window.getComputedStyle(el);
    return style.display !== "none" && style.visibility !== "hidden";
  }

  function readInputText(inputEl) {
    if (
      inputEl instanceof HTMLTextAreaElement ||
      inputEl instanceof HTMLInputElement
    ) {
      return inputEl.value || "";
    }
    return inputEl.innerText || inputEl.textContent || "";
  }

  function findChatInputElement() {
    const selectors = [
      "#prompt-textarea",
      'textarea[data-testid="prompt-textarea"]',
      'div[data-testid="prompt-textarea"]',
      'div[contenteditable="true"][id="prompt-textarea"]',
      'div[contenteditable="true"]',
    ];

    for (const selector of selectors) {
      const nodes = document.querySelectorAll(selector);
      for (const node of nodes) {
        if (!isElementVisible(node)) continue;
        if (selector === 'div[contenteditable="true"]' && !node.closest("form")) continue;
        return node;
      }
    }
    return null;
  }

  function findChatInputElementInForm(formEl) {
    if (!(formEl instanceof HTMLFormElement)) return null;

    const selectors = [
      "#prompt-textarea",
      'textarea[data-testid="prompt-textarea"]',
      'div[data-testid="prompt-textarea"]',
      'div[contenteditable="true"][id="prompt-textarea"]',
      'div[contenteditable="true"]',
    ];

    for (const selector of selectors) {
      const nodes = formEl.querySelectorAll(selector);
      for (const node of nodes) {
        if (!isElementVisible(node)) continue;
        return node;
      }
    }
    return null;
  }

  function ensureChatContextDock() {
    if (!isChatGPTPage()) return null;
    let wrap = document.getElementById("tm-nblm-chat-context-wrap");
    let textarea = wrap?.querySelector("#tm-nblm-chat-context") || null;
    let fab = document.getElementById("tm-nblm-chat-context-fab");

    if (!wrap) {
      const inputEl = findChatInputElement();
      if (!inputEl) return null;

      wrap = document.createElement("div");
      wrap.id = "tm-nblm-chat-context-wrap";
    }
    if (!textarea) {
      textarea = document.createElement("textarea");
      textarea.id = "tm-nblm-chat-context";
      textarea.readOnly = true;
      textarea.placeholder = "NotebookLM 查询结果将显示在这里";
      wrap.appendChild(textarea);
    }

    const oldDragBar = wrap.querySelector("#tm-nblm-chat-context-drag");
    if (oldDragBar) {
      oldDragBar.remove();
    }

    if (!fab) {
      fab = document.createElement("button");
      fab.id = "tm-nblm-chat-context-fab";
      fab.type = "button";
      fab.textContent = "N";
      fab.title = "打开 NotebookLM 背景框";
      fab.addEventListener("click", (event) => {
        if (chatContextFabJustDragged) {
          event.preventDefault();
          event.stopPropagation();
          return;
        }
        if (state.chatContextFabPos) {
          const next = { ...(state.chatContextLayout || getDefaultChatContextLayout()) };
          const x = Number(state.chatContextFabPos.x);
          const y = Number(state.chatContextFabPos.y);
          if (Number.isFinite(x)) next.x = x;
          if (Number.isFinite(y)) next.y = y;
          setChatContextLayout(next, false);
        }
        setChatContextMinimized(false, true);
      });
    }

    if (wrap.parentElement !== document.body) {
      document.body.appendChild(wrap);
    }
    if (fab.parentElement !== document.body) {
      document.body.appendChild(fab);
    }

    ui.chatContextWrap = wrap;
    ui.chatContextFab = fab;
    bindChatContextFabDrag(fab);
    if (!state.chatContextLayout) {
      state.chatContextLayout = getDefaultChatContextLayout();
    }
    setChatContextLayout(state.chatContextLayout, false);

    if (textarea) {
      if (textarea.value !== state.chatContextText) {
        textarea.value = state.chatContextText || "";
      }
    }

    bindChatContextWindowLikeResize(wrap);
    bindChatContextDrag(wrap);
    bindChatContextWindowResize();
    setChatContextMinimized(state.chatContextMinimized, false);

    ui.chatContextTextarea = textarea;
    return wrap;
  }

  function setChatContextText(text) {
    state.chatContextText = text || "";
    const wrap = ensureChatContextDock();
    if (!wrap || !ui.chatContextTextarea) return false;
    if (ui.chatContextTextarea.value !== state.chatContextText) {
      ui.chatContextTextarea.value = state.chatContextText;
    }
    persistState();
    return true;
  }

  function getChatContextText() {
    return (ui.chatContextTextarea?.value ?? state.chatContextText ?? "").trim();
  }

  function setInputText(inputEl, text) {
    if (
      inputEl instanceof HTMLTextAreaElement ||
      inputEl instanceof HTMLInputElement
    ) {
      const proto = Object.getPrototypeOf(inputEl);
      const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
      if (descriptor?.set) descriptor.set.call(inputEl, text);
      else inputEl.value = text;
      inputEl.dispatchEvent(new Event("input", { bubbles: true }));
      inputEl.dispatchEvent(new Event("change", { bubbles: true }));
      inputEl.focus();
      inputEl.setSelectionRange?.(text.length, text.length);
      return;
    }

    inputEl.focus();
    const selection = window.getSelection();
    if (selection) {
      const range = document.createRange();
      range.selectNodeContents(inputEl);
      selection.removeAllRanges();
      selection.addRange(range);
    }

    let inserted = false;
    try {
      inserted = document.execCommand("insertText", false, text);
    } catch (_) {
      inserted = false;
    }
    if (!inserted) {
      inputEl.textContent = text;
    }
    inputEl.dispatchEvent(
      new InputEvent("input", {
        bubbles: true,
        cancelable: true,
        data: text,
        inputType: "insertText",
      })
    );
    inputEl.dispatchEvent(new Event("change", { bubbles: true }));
  }

  function buildNotebookQuestion(questionText) {
    const finalQuestion = String(questionText || "").trim();
    if (!finalQuestion) return "";

    const systemPrompt = String(state.notebookSystemPrompt || "").trim();
    if (!systemPrompt) return finalQuestion;

    return [
      "请优先遵循以下系统提示词,再回答用户问题:",
      "",
      "【系统提示词】",
      systemPrompt,
      "",
      "【用户问题】",
      finalQuestion,
    ].join("\n");
  }

  function renderMergePromptTemplate(template, data) {
    const rawTemplate = String(template || "").trim();
    if (!rawTemplate) {
      return String(data?.answer || "").trim();
    }
    return rawTemplate.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
      const value = data && Object.prototype.hasOwnProperty.call(data, key) ? data[key] : "";
      return String(value ?? "");
    });
  }

  function buildMergedPrompt(contextText, questionText) {
    return [
      CONTEXT_BLOCK_TITLE,
      contextText,
      "",
      QUESTION_BLOCK_TITLE,
      questionText
    ].join("\n");
  }

  function isMergedPrompt(text) {
    if (!text) return false;
    return text.includes(CONTEXT_BLOCK_TITLE) && text.includes(QUESTION_BLOCK_TITLE);
  }

  function extractQuestionTextFromChatInput(rawText) {
    const text = (rawText || "").trim();
    if (!text) return "";
    if (!isMergedPrompt(text)) return text;

    const idx = text.indexOf(QUESTION_BLOCK_TITLE);
    if (idx < 0) return text;
    return text.slice(idx + QUESTION_BLOCK_TITLE.length).trim();
  }

  function getQuestionFromChatInput() {
    if (!isChatGPTPage()) return "";
    const inputEl = findChatInputElement();
    if (!inputEl) return "";
    return extractQuestionTextFromChatInput(readInputText(inputEl));
  }

  function sleep(ms) {
    return new Promise((resolve) => {
      window.setTimeout(resolve, ms);
    });
  }

  function pickCurrentChatInput(formEl, fallbackInputEl) {
    const candidates = [
      findChatInputElementInForm(formEl),
      findChatInputElement(),
      fallbackInputEl,
    ];
    for (const el of candidates) {
      if (!(el instanceof Element)) continue;
      if (!el.isConnected) continue;
      if (!isElementVisible(el)) continue;
      return el;
    }
    return null;
  }

  function isMergedPromptAppliedOnInput(inputEl, expectedQuestionText = "") {
    if (!(inputEl instanceof Element)) return false;
    const raw = readInputText(inputEl);
    if (!isMergedPrompt(raw)) return false;

    const expected = String(expectedQuestionText || "").trim();
    if (!expected) return true;
    const extracted = extractQuestionTextFromChatInput(raw);
    return extracted === expected;
  }

  async function applyMergedPromptWithRetry(
    formEl,
    fallbackInputEl,
    mergedText,
    expectedQuestionText
  ) {
    const totalAttempts = 4;
    for (let attempt = 0; attempt < totalAttempts; attempt += 1) {
      const inputEl = pickCurrentChatInput(formEl, fallbackInputEl);
      if (!inputEl) {
        await sleep(80);
        continue;
      }

      setInputText(inputEl, mergedText);
      await sleep(attempt === 0 ? 80 : 140);

      const latestInput = pickCurrentChatInput(formEl, inputEl) || inputEl;
      if (isMergedPromptAppliedOnInput(latestInput, expectedQuestionText)) {
        return { ok: true, inputEl: latestInput };
      }
    }

    return {
      ok: false,
      inputEl: pickCurrentChatInput(formEl, fallbackInputEl),
    };
  }

  function rememberAskResult({
    notebookId,
    sourceIds,
    question,
    answer,
    conversationId,
    rawResponse,
  }) {
    const notebook = state.notebooks.find((n) => n.id === notebookId);

    state.conversationByNotebook[notebookId] = conversationId;
    if (!state.historyByConversation[conversationId]) {
      state.historyByConversation[conversationId] = [];
    }
    state.historyByConversation[conversationId].push({
      query: question,
      answer,
    });

    state.lastResult = {
      timestamp: new Date().toISOString(),
      notebookId,
      notebookTitle: notebook?.title || "",
      sourceIds,
      question,
      answer,
      conversationId,
      rawResponse,
    };
    state.lastResult.chatgptPrompt = buildChatGPTContextPrompt(state.lastResult);
    return state.lastResult;
  }

  function submitChatForm(formEl) {
    if (!(formEl instanceof HTMLFormElement)) return;
    if (typeof formEl.requestSubmit === "function") {
      formEl.requestSubmit();
      return;
    }
    const submitBtn = formEl.querySelector('button[type="submit"]');
    if (submitBtn instanceof HTMLButtonElement) {
      submitBtn.click();
      return;
    }
    formEl.submit();
  }

  async function autoAskNotebookForChatSubmit(formEl, inputEl, questionText) {
    const notebookId = state.selectedNotebookId;
    let sourceIds = notebookId ? state.sourceSelectionByNotebook[notebookId] || [] : [];

    if (!notebookId || !questionText) {
      if (!notebookId) setStatus("请先选择笔记本");
      else if (!questionText) setStatus("请先在 ChatGPT 输入框中输入问题");
      return;
    }

    chatAutoAskInFlight = true;
    setAskSendFloatingLoading(true);
    setBusy(true);
    setStatus("正在自动向 NotebookLM 提问...");

    try {
      sourceIds = await validateSelectedSourceIds(notebookId, sourceIds);
      if (!sourceIds.length) {
        setStatus("已移除失效来源,请至少选择一个来源");
        return;
      }

      const notebookQuestion = buildNotebookQuestion(questionText);
      const result = await askNotebook({
        notebookId,
        question: notebookQuestion,
        sourceIds,
        forceNewConversation: ui.newConversationCheckbox.checked,
      });

      rememberAskResult({
        notebookId,
        sourceIds,
        question: questionText,
        answer: result.answer,
        conversationId: result.conversationId,
        rawResponse: result.rawText,
      });

      const injected = injectPromptToChatInput(state.lastResult.chatgptPrompt);
      const finalContext = getChatContextText() || state.lastResult.chatgptPrompt || "";
      const merged = buildMergedPrompt(finalContext, questionText);
      const mergeResult = await applyMergedPromptWithRetry(
        formEl,
        inputEl,
        merged,
        questionText
      );

      if (!mergeResult.ok) {
        setStatus("自动提问完成,但拼接未生效,已取消发送");
        return;
      }

      await sleep(120);
      const submitForm = mergeResult.inputEl?.closest("form") || formEl;
      submitChatForm(submitForm);
      setStatus(injected.ok ? "自动提问完成,已拼接背景并发送" : `自动提问完成,但${injected.message}`);
    } catch (err) {
      console.error("[TM NotebookLM]", err);
      setStatus(`自动提问失败: ${err.message || err}`);
    } finally {
      chatAutoAskInFlight = false;
      setAskSendFloatingLoading(false);
      setBusy(false);
    }
  }

  function isLikelySendButton(btn) {
    if (!(btn instanceof HTMLButtonElement)) return false;
    if ((btn.getAttribute("type") || "").toLowerCase() === "submit") return true;
    const testid = (btn.getAttribute("data-testid") || "").toLowerCase();
    const aria = (btn.getAttribute("aria-label") || "").toLowerCase();
    const title = (btn.getAttribute("title") || "").toLowerCase();
    if (testid.includes("send")) return true;
    if (aria.includes("send") || aria.includes("发送")) return true;
    if (title.includes("send") || title.includes("发送")) return true;
    return false;
  }

  function startChatContextObserver() {
    if (!isChatGPTPage() || chatContextObserver) return;
    let scheduled = false;
    chatContextObserver = new MutationObserver(() => {
      if (scheduled) return;
      scheduled = true;
      window.setTimeout(() => {
        scheduled = false;

        ensurePanelUI();
        const dockMissing = !ui.chatContextWrap || !ui.chatContextWrap.isConnected;
        const fabMissing = !ui.chatContextFab || !ui.chatContextFab.isConnected;

        ensureAskSendFloatingButton();
        if (dockMissing || fabMissing) {
          ensureChatContextDock();
        }
      }, 250);
    });
    chatContextObserver.observe(document.body, { childList: true, subtree: true });
  }

  function buildChatGPTContextPrompt(result) {
    const template = String(state.mergeInstructionPrompt || "");
    const notebookName = result.notebookTitle || result.notebookId || "";
    let rendered = renderMergePromptTemplate(template, {
      notebook: notebookName,
      notebookTitle: result.notebookTitle || "",
      notebookId: result.notebookId || "",
      question: result.question || "",
      answer: result.answer || "",
      context: result.answer || "",
    }).trim();

    const hasAnswerPlaceholder = /{{\s*(answer|context)\s*}}/.test(template);
    if (!hasAnswerPlaceholder && result.answer) {
      rendered = [rendered, result.answer].filter(Boolean).join("\n\n").trim();
    }

    if (!rendered) return result.answer || "";
    if (rendered.includes("{{") || rendered.includes("}}")) {
      return result.answer || "";
    }
    return rendered;
  }

  function injectPromptToChatInput(promptText) {
    if (!isChatGPTPage()) {
      return { ok: false, message: "当前页面不是 ChatGPT,未写入背景框" };
    }

    const ok = setChatContextText(promptText);
    if (!ok) {
      return { ok: false, message: "未找到背景框挂载位置" };
    }
    return { ok: true, message: "已写入背景框,发送时会自动拼接到问题" };
  }

  async function handleAsk() {
    const notebookId = state.selectedNotebookId;
    const question = getQuestionFromChatInput();
    const notebookQuestion = buildNotebookQuestion(question);
    if (!notebookId) {
      setStatus("请先选择笔记本");
      return;
    }
    if (!question) {
      setStatus("请先在 ChatGPT 输入框中输入问题");
      return;
    }

    let sourceIds = state.sourceSelectionByNotebook[notebookId] || [];

    setBusy(true);
    setStatus("正在提问...");

    try {
      sourceIds = await validateSelectedSourceIds(notebookId, sourceIds);
      if (!sourceIds.length) {
        setStatus("已移除失效来源,请至少选择一个来源");
        return;
      }

      const result = await askNotebook({
        notebookId,
        question: notebookQuestion,
        sourceIds,
        forceNewConversation: ui.newConversationCheckbox.checked,
      });

      rememberAskResult({
        notebookId,
        sourceIds,
        question,
        answer: result.answer,
        conversationId: result.conversationId,
        rawResponse: result.rawText,
      });
      const statusParts = ["提问完成"];
      const injected = injectPromptToChatInput(state.lastResult.chatgptPrompt);
      if (injected.ok) statusParts.push("已写入背景框");
      else statusParts.push(injected.message);

      setStatus(statusParts.join(","));
    } catch (err) {
      console.error("[TM NotebookLM]", err);
      setStatus(`提问失败: ${err.message || err}`);
    } finally {
      setBusy(false);
    }
  }

  async function onNotebookChanged() {
    const notebookId = state.selectedNotebookId;
    if (!notebookId) {
      state.sources = [];
      renderSources();
      return;
    }
    setStatus("加载来源...");
    try {
      await loadSources(notebookId);
      setStatus("来源已加载");
    } catch (err) {
      console.error("[TM NotebookLM]", err);
      state.sources = [];
      renderSources();
      setStatus(`加载来源失败: ${err.message || err}`);
    }
  }

  async function initializeData(forceRefresh = false) {
    if (forceRefresh) {
      state.notebooks = [];
      state.sources = [];
      renderNotebookOptions();
      renderSources();
    }
    setBusy(true);
    setStatus("加载笔记本...");
    try {
      await loadNotebooks();
      if (!state.selectedNotebookId && state.notebooks.length > 0) {
        state.selectedNotebookId = state.notebooks[0].id;
      }
      ui.notebookSelect.value = state.selectedNotebookId;
      await onNotebookChanged();
      setStatus("准备就绪");
    } catch (err) {
      console.error("[TM NotebookLM]", err);
      setStatus(`初始化失败: ${err.message || err}`);
    } finally {
      setBusy(false);
      persistState();
    }
  }

  function mount() {
    loadPersistedState();
    injectStyles();
    ensurePanelUI();
    if (isChatGPTPage()) {
      ensureAskSendFloatingButton();
      bindAskSendFloatingButtonPosition();
      ensureChatContextDock();
      startChatContextObserver();
    }
    syncPanelVisibility();
    void initializeData(false);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", mount, { once: true });
  } else {
    mount();
  }
})();


// ===== END 2.js =====

// ===== BEGIN 3.js =====

// ==UserScript==
// @name         Thriller
// @namespace    http://tampermonkey.net/merged_tools
// @version      4.5.2_1.2.0_Merged
// @description  Thriller
// @author       Moe
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
// @run-at       document-idle
// @grant        none
// ==/UserScript==

// ===================== 0. 闂備礁鎲$敮鐐存櫠濡も偓閿曘垽鍩勯崘銊х獮闁哄鐗勯崝宥呪枍韫囨洍鍋撳▓鍨灀闁稿鎹囬弻娑滅疀閺囥劌濮曠紓浣介哺閻熲晛鐣峰Ο琛℃瀻闁逛即娼ф慨銈夋⒑閸濆嫬鏆旈柛搴㈠絻閿曘垽鏁撻悩铏珫?=====================
const THRILLER_EXPIRES_AT = '3036-01-31T00:00:00+09:00';

// 失效后是否弹窗提示(true/false)
const THRILLER_EXPIRE_ALERT = false;

(function thrillerExpiryGuard() {
    function parseExpires(s) {
        if (!s) return NaN;

        // 支持只写日期:'YYYY-MM-DD' -> 默认当天 00:00:00 失效
        // 如果你想“到当天结束才失效”,把 00:00:00 改成 23:59:59.999
        if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
            return new Date(`${s}T00:00:00+09:00`).getTime();
            // return new Date(`${s}T23:59:59.999+09:00`).getTime(); // ← 当天结束才失效
        }

        const t = new Date(s).getTime();
        return t;
    }

    const expiresMs = parseExpires(THRILLER_EXPIRES_AT);
    const expired = Number.isFinite(expiresMs) && Date.now() >= expiresMs;

    if (expired) {
        window.__THRILLER_DISABLED__ = true;

        const msg = `[Thriller] 已到期,脚本已禁用(expiresAt=${THRILLER_EXPIRES_AT})`;
        console.warn(msg);

        if (THRILLER_EXPIRE_ALERT) {
            if (!window.__THRILLER_EXPIRE_ALERTED__) {
                window.__THRILLER_EXPIRE_ALERTED__ = true;
                alert('Thriller 脚本已到期,已自动禁用。');
            }
        }
    } else {
        window.__THRILLER_DISABLED__ = false;
    }
})();











/**
 * ==============================================================================
 * 婵犵妲呴崹顏堝焵椤掑啯鐝柛瀣ㄥ劚閳藉骞橀姘闂備焦瀵х粙鎺楊敄瀹稿獘tGPT 濠电偞鍨堕幐鎾磻閹剧粯鈷戞繛鍡樺劤閺嬫稒銇勯敂钘夌祷閻?(v4.5.2 闂備礁鎲¢崹鐔煎磻閹剧粯鐓曢柡宥冨妿婢а囨煙娓氬灝濡界€垫澘瀚濂稿炊瑜滈弳?
 * 闂備礁鎲″濠氬窗閺囥垹绀傛慨妞诲亾闁轰礁绉撮~婵嬵敆婢跺﹥顔撻梻?JSON闂備焦瀵х粙鎴︽嚐椤栫倛鍥箵閹规娅g槐鎺懳熸导娆戠闂佽娴烽弫鎼併€佹繝鍥ㄥ剨濞寸姴顑嗛弲顒勬倵濞戞凹娓?濠电偞鍨堕幐鎼佸箹椤愩儯浜归柛娑橈功閻熷湱鎲告惔銊ョ;闁规崘绉ぐ鎺濇晩闁芥ê顦弸娆撴煟? * ==============================================================================
 */
(() => {
    'use strict';
    if (window.__THRILLER_DISABLED__) return;
    console.log('>>> [Module 1] Loading: ChatGPT Revive...');

    // ===================== 1. 闂備礁鎼粔鍫曗€﹂崼銏㈢处濡わ絽鍟惌妤呮煕鐏炲墽鈯曟繛?=====================
    // [设置] 是否默认开启遮罩 (true: 开启, false: 关闭) - 代码内开关
    const ENABLE_OVERLAY_DEFAULT = true;
    const SKIPPED_CONTENT_TYPES = new Set(['thoughts', 'reasoning_recap', 'model_editable_context']);

    // ===================== 2. 闂備胶顭堢换鍫ュ礉瀹€鍕剳妞ゆ帒瀚崑鎰版煠閸濄儺鏆柛?=====================
    let loadedMessages = [];
    let lastThreadTitle = '';
    let lastThreadId = 'chat_history';

    // ===================== 3. IndexedDB 闂備浇妗ㄩ懗鑸垫櫠濡も偓閻e灚鎷呯憴鍕妳?=====================
    const IDB_NAME = 'resu_immortal_db';
    const IDB_VER = 2;
    const STORE_THREADS = 'threads';
    const STORE_MSG = 'messages';
    const STORE_SETTINGS = 'settings';
    const KEY_URL_MAP = 'resu_url_mappings';
    const KEY_SESSION_MAP = 'resu_session_mappings';
    let idb = null;
    let dbQueue = Promise.resolve();

    function withDbLock(fn) { dbQueue = dbQueue.then(fn, fn); return dbQueue; }

    function idbReq(req) {
        return new Promise((resolve, reject) => {
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }
    async function idbOpen() {
        if (idb) return idb;
        idb = await new Promise((resolve, reject) => {
            const req = indexedDB.open(IDB_NAME, IDB_VER);
            req.onerror = () => reject(req.error);
            req.onupgradeneeded = () => {
                const db = req.result;
                if (!db.objectStoreNames.contains(STORE_THREADS)) {
                    const st = db.createObjectStore(STORE_THREADS, { keyPath: 'threadId' });
                    st.createIndex('updatedAt', 'updatedAt', { unique: false });
                }
                if (!db.objectStoreNames.contains(STORE_MSG)) {
                    const sm = db.createObjectStore(STORE_MSG, { keyPath: 'id', autoIncrement: true });
                    sm.createIndex('threadId', 'threadId', { unique: false });
                    sm.createIndex('thread_seq', ['threadId', 'seq'], { unique: true });
                }
                if (!db.objectStoreNames.contains(STORE_SETTINGS)) {
                    db.createObjectStore(STORE_SETTINGS, { keyPath: 'key' });
                }
            };
            req.onsuccess = () => resolve(req.result);
        });
        return idb;
    }
    async function settingsGet(key) {
        const db = await idbOpen();
        const tx = db.transaction([STORE_SETTINGS], 'readonly');
        const st = tx.objectStore(STORE_SETTINGS);
        const row = await idbReq(st.get(key));
        await new Promise((resolve) => { tx.oncomplete = resolve; });
        return row ? row.value : null;
    }
    async function settingsSet(key, value) {
        const db = await idbOpen();
        const tx = db.transaction([STORE_SETTINGS], 'readwrite');
        const st = tx.objectStore(STORE_SETTINGS);
        st.put({ key, value });
        await new Promise((resolve, reject) => {
            tx.oncomplete = resolve;
            tx.onerror = () => reject(tx.error);
            tx.onabort = () => reject(tx.error);
        });
    }
    async function ensureThread(threadId, titleFromImport) {
        const db = await idbOpen();
        const tx = db.transaction([STORE_THREADS], 'readwrite');
        const st = tx.objectStore(STORE_THREADS);
        const now = Date.now();
        const cur = await idbReq(st.get(threadId));
        let next;
        if (cur) {
            const hasTitle = cur.title && String(cur.title).trim();
            next = { ...cur, title: hasTitle ? cur.title : (titleFromImport || threadId), updatedAt: now };
        } else {
            next = { threadId, title: titleFromImport || threadId, createdAt: now, updatedAt: now, nextSeq: 0 };
        }
        st.put(next);
        await new Promise((resolve, reject) => {
            tx.oncomplete = resolve;
            tx.onerror = () => reject(tx.error);
            tx.onabort = () => reject(tx.error);
        });
        return next;
    }
    async function bumpSeq(threadId) {
        const db = await idbOpen();
        const tx = db.transaction([STORE_THREADS], 'readwrite');
        const st = tx.objectStore(STORE_THREADS);
        const thr = await idbReq(st.get(threadId));
        if (!thr) throw new Error('thread not found: ' + threadId);
        const seq = thr.nextSeq || 0;
        thr.nextSeq = seq + 1;
        thr.updatedAt = Date.now();
        st.put(thr);
        await new Promise((resolve, reject) => {
            tx.oncomplete = resolve;
            tx.onerror = () => reject(tx.error);
            tx.onabort = () => reject(tx.error);
        });
        return seq;
    }
    async function addMsg(threadId, role, content, ts, source, extra = null) {
        return withDbLock(async () => {
            const db = await idbOpen();
            await ensureThread(threadId);
            const seq = await bumpSeq(threadId);
            const tx = db.transaction([STORE_MSG], 'readwrite');
            const sm = tx.objectStore(STORE_MSG);
            const payload = {
                threadId, seq, role, content,
                ts: ts || Date.now(),
                source: source || 'live',
            };
            if (extra && typeof extra === 'object') {
                if (typeof extra.sender === 'string' && extra.sender.trim()) payload.sender = extra.sender.trim();
                if (typeof extra.thinking === 'string' && extra.thinking.trim()) payload.thinking = extra.thinking.trim();
            }
            const id = await idbReq(sm.add(payload));
            await new Promise((resolve, reject) => {
                tx.oncomplete = resolve;
                tx.onerror = () => reject(tx.error);
                tx.onabort = () => reject(tx.error);
            });
            return id;
        });
    }
    async function clearThread(threadId) {
        return withDbLock(async () => {
            const db = await idbOpen();
            const tx = db.transaction([STORE_MSG], 'readwrite');
            const sm = tx.objectStore(STORE_MSG);
            const idx = sm.index('threadId');
            await new Promise((resolve, reject) => {
                const range = IDBKeyRange.only(threadId);
                const curReq = idx.openCursor(range);
                curReq.onsuccess = () => {
                    const c = curReq.result;
                    if (!c) return resolve();
                    c.delete();
                    c.continue();
                };
                curReq.onerror = () => reject(curReq.error);
            });
            await new Promise((resolve, reject) => {
                tx.oncomplete = resolve;
                tx.onerror = () => reject(tx.error);
                tx.onabort = () => reject(tx.error);
            });
            const tx2 = db.transaction([STORE_THREADS], 'readwrite');
            const st = tx2.objectStore(STORE_THREADS);
            const thr = await idbReq(st.get(threadId));
            if (thr) {
                thr.nextSeq = 0;
                thr.updatedAt = Date.now();
                st.put(thr);
            }
            await new Promise((resolve, reject) => {
                tx2.oncomplete = resolve;
                tx2.onerror = () => reject(tx2.error);
                tx2.onabort = () => reject(tx2.error);
            });
        });
    }
    async function getFullThreadMessages(threadId) {
        const db = await idbOpen();
        const tx = db.transaction([STORE_MSG], 'readonly');
        const sm = tx.objectStore(STORE_MSG);
        const idx = sm.index('thread_seq');
        const range = IDBKeyRange.bound([threadId, 0], [threadId, Infinity]);
        const req = idx.getAll(range);
        const msgs = await idbReq(req);
        await new Promise((resolve) => { tx.oncomplete = resolve; });
        msgs.sort((a, b) => a.seq - b.seq);
        return msgs;
    }

    // ===================== 4. 闂佽娴烽弫鎼佸储瑜斿畷鐢割敇閵忥紕鍔甸梺鍝勫缁绘帞鏁? =====================
    function scrapeNewMessagesFromPage() {
        const scraped = [];
        const articles = document.querySelectorAll('article:not([data-resu-injected="true"])');
        for (const art of articles) {
            const userEl = art.querySelector('[data-message-author-role="user"]');
            const assistEl = art.querySelector('[data-message-author-role="assistant"]');
            let role = '';
            if (userEl) role = 'user';
            else if (assistEl) role = 'assistant';
            else continue;
            let content = '';
            if (role === 'user') {
                const textDiv = art.querySelector('.whitespace-pre-wrap');
                content = textDiv ? textDiv.innerText : art.innerText;
            } else {
                const mdDiv = art.querySelector('.markdown');
                if (mdDiv) {
                    const clone = mdDiv.cloneNode(true);
                    const details = clone.querySelectorAll('details');
                    details.forEach(d => d.remove());
                    content = clone.innerText;
                } else {
                    if (art.querySelector('.text-red-500')) continue;
                    content = art.innerText;
                }
            }
            content = content.trim();
            content = content.replace(/^(?:闁诲骸婀遍…鍫濐嚕閸洖鐒垫い鎺嶈兌缁犳岸鏌嶈閸撴瑩宕滈悜绯穙ught for|Thinking)\s*\d+\s*(?:s|缂傚倷绀侀ˇ鐗堢箾婵垁conds?)\s*(\n+)?/i, '');
            const pollutionPatterns = [
                /Is this conversation helpful so far\?/i,
                /这个回答有帮助吗?/i,
                /Was this response better or worse/i
            ];
            let isTrash = false;
            for (const p of pollutionPatterns) {
                if (p.test(content)) {
                    if (content.length < 100) isTrash = true;
                    else content = content.replace(p, '');
                }
            }
            if (isTrash) continue;
            content = content.replace(/Copy code|复制代码/gi, '');
            if (role === 'assistant') {
                content = content.replace(/^(ChatGPT|Assistant)([::]|\\s)+/i, '');
            }
            if (role === 'user') {
                content = content.replace(/^(You|你说|你)([::]|\\s)+/i, '');
            }
            content = content.replace(/\[\d+\]/g, '');
            content = content.trim();
            if (content) {
                scraped.push({ role, content });
            }
        }
        return scraped;
    }
    async function exportThread(threadId) {
        const db = await idbOpen();
        await ensureThread(threadId);
        const txT = db.transaction([STORE_THREADS], 'readonly');
        const st = txT.objectStore(STORE_THREADS);
        const thr = await idbReq(st.get(threadId));
        await new Promise((resolve) => { txT.oncomplete = resolve; });
        setStatus('⏳ 正在抓取网页新对话…');
        const newMessages = scrapeNewMessagesFromPage();
        const finalMessages = [...loadedMessages, ...newMessages];
        return {
            title: thr?.title || threadId,
            exported_at: new Date().toISOString(),
            total_messages: finalMessages.length,
            messages: finalMessages.map(m => ({ role: m.role, content: m.content }))
        };
    }

    // ===================== 5. 闂備礁鎼崐绋棵洪敐鍛瀻闁靛繆鈧磭绐為梺鑽ゅ枑婢瑰棗顫?=====================
    const FS = { supported: typeof window.showDirectoryPicker === 'function', dirHandle: null, fileList: [] };
    async function ensureDirPermission(dirHandle, mode = 'readwrite') {
        try {
            const q = await dirHandle.queryPermission({ mode });
            if (q === 'granted') return true;
            const r = await dirHandle.requestPermission({ mode });
            return r === 'granted';
        } catch { return false; }
    }
    async function pickLibraryFolder() {
        if (!FS.supported) { setStatus('⚠️ 当前浏览器不支持文件夹访问'); return; }
        try {
            const dir = await window.showDirectoryPicker();
            const ok = await ensureDirPermission(dir, 'readwrite');
            if (!ok) { setStatus('⚠️ 权限未授权'); return; }
            FS.dirHandle = dir;
            await settingsSet('libraryDir', dir);
            setStatus('✅ 已选择库文件夹');
            await refreshLibraryList();
        } catch { setStatus('(已取消选择)'); }
    }
    async function loadSavedFolderIfAny() {
        if (!FS.supported) return;
        const saved = await settingsGet('libraryDir');
        if (!saved) return;
        FS.dirHandle = saved;
        const ok = await ensureDirPermission(saved, 'readwrite');
        if (!ok) { setStatus('⚠️ 需重新授权文件夹'); return; }
        await refreshLibraryList();
    }
    async function refreshLibraryList() {
        if (!FS.dirHandle) { setStatus('⚠️ 未选择文件夹'); return; }
        const ok = await ensureDirPermission(FS.dirHandle, 'read');
        if (!ok) { setStatus('⚠️ 无读取权限'); return; }
        const list = [];
        try {
            for await (const [name, handle] of FS.dirHandle.entries()) {
                if (handle.kind === 'file' && name.toLowerCase().endsWith('.json')) list.push({ name, handle });
            }
        } catch { setStatus('❌ 遍历文件夹失败'); return; }
        list.sort((a, b) => a.name.localeCompare(b.name));
        FS.fileList = list;
        updateLibraryMenu();
        setStatus(`✅ 库文件夹:${list.length} 个 JSON`);
    }
    function updateLibraryMenu() {
        const sel = $p('#resu-lib-select');
        sel.innerHTML = '';
        if (!FS.fileList.length) {
            const opt = document.createElement('option');
            opt.value = '';
            opt.textContent = '(闂備礁鎼崐绋棵洪敐鍛瀻闁靛繆鈧磭绐為梻渚囧亝缁嬫牜绮堢€n剛纾?';
            sel.appendChild(opt);
            return;
        }
        for (const f of FS.fileList) {
            const opt = document.createElement('option');
            opt.value = f.name;
            opt.textContent = f.name;
            sel.appendChild(opt);
        }
    }
    async function writeJsonToFolderIfPossible(filename, jsonString) {
        if (!FS.supported || !FS.dirHandle) return false;
        const ok = await ensureDirPermission(FS.dirHandle, 'readwrite');
        if (!ok) return false;
        try {
            const fh = await FS.dirHandle.getFileHandle(filename, { create: true });
            const w = await fh.createWritable();
            await w.write(jsonString);
            await w.close();
            return true;
        } catch { return false; }
    }
    function downloadJson(obj, filename) {
        const blob = new Blob([JSON.stringify(obj, null, 2)], { type: "application/json" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        a.remove();
        setTimeout(() => URL.revokeObjectURL(url), 1000);
    }

    // ===================== 6. JSON 闂佽崵鍠愰悷杈╁緤妤e啯鍊甸柦妯侯樈閸熷懘鏌曟径鍫濆姎鐎?=====================
    function extractTitleFromJson(raw) {
        try {
            if (!raw || typeof raw !== 'object') return '';
            const candidates = [
                raw.title, raw.conversation_title, raw.thread_title, raw.name,
                raw?.thread?.title, raw?.meta?.title, raw?.metadata?.title,
            ];
            for (const v of candidates) if (typeof v === 'string' && v.trim()) return v.trim();
            return '';
        } catch { return ''; }
    }
    function safeFileName(name) {
        let s = (name || '').toString().trim();
        if (!s) return 'chat_history';
        s = s.replace(/[\\/:\*?"<>|]/g, ' ');
        s = s.replace(/[\u0000-\u001F\u007F]/g, ' ');
        s = s.replace(/\s+/g, ' ').trim();
        s = s.replace(/[. ]+$/g, '');
        if (!s) s = 'chat_history';
        if (s.length > 120) s = s.slice(0, 120).trim();
        return s || 'chat_history';
    }
    function fnv1a32(str) {
        let h = 0x811c9dc5;
        for (let i = 0; i < str.length; i++) {
            h ^= str.charCodeAt(i);
            h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
        }
        return ('0000000' + h.toString(16)).slice(-8);
    }
    function parseJsonToMessages(raw) {
        if (!raw) return [];
        if (Array.isArray(raw) && raw.length > 0 && raw[0] && raw[0].mapping) {
            return parseChatGptConversation(raw[0]);
        }
        if (raw && typeof raw === 'object' && raw.mapping) {
            return parseChatGptConversation(raw);
        }
        const arr = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.messages) ? raw.messages : []);
        const out = [];
        for (const item of arr) {
            const normalized = normalizeGenericMessage(item);
            if (normalized) out.push(normalized);
        }
        return out;
    }

    function normalizeGenericMessage(item) {
        if (!item || typeof item !== 'object') return null;
        const roleRaw = typeof item.author === 'string' ? item.author : item.author?.role;
        const role = normalizeMessageRole(item.role || roleRaw || '');
        if (!role) return null;
        const sender = deriveSenderForGeneric(item, role);
        const content = extractTextForGeneric(item, role).trim();
        if (!content) return null;
        if (role === 'assistant' && isNonAnswerAssistantContent(content)) return null;
        return { role, sender, content };
    }

    function deriveSenderForGeneric(item, role) {
        if (typeof item.sender === 'string' && item.sender.trim()) return item.sender.trim();
        if (typeof item.model === 'string' && item.model.trim() && role === 'assistant') {
            const model = item.model.trim();
            const gptMatch = model.match(/gpt-(.+)/i);
            if (gptMatch) return `GPT-${gptMatch[1]}`;
            return model;
        }
        if (role === 'assistant') return 'assistant';
        return 'user';
    }

    function extractThinkingForGeneric(item) {
        if (typeof item.thinking === 'string' && item.thinking.trim()) return item.thinking.trim();
        if (!Array.isArray(item.content)) return '';
        const chunks = [];
        for (const part of item.content) {
            if (!part || typeof part !== 'object') continue;
            if (part.type === 'think' && typeof part.think === 'string' && part.think.trim()) {
                chunks.push(part.think.trim());
            }
        }
        return chunks.join('\n\n').trim();
    }

    function extractTextForGeneric(item, role) {
        if (typeof item.content === 'string') return item.content;
        if (typeof item.text === 'string') return item.text;
        if (Array.isArray(item.content)) {
            const chunks = [];
            for (const part of item.content) {
                if (typeof part === 'string') {
                    if (part.trim()) chunks.push(part.trim());
                    continue;
                }
                if (!part || typeof part !== 'object') continue;
                if (part.type === 'text' && typeof part.text === 'string') chunks.push(part.text);
                else if (typeof part.text === 'string') chunks.push(part.text);
                else if (typeof part.value === 'string') chunks.push(part.value);
            }
            return chunks.join('\n\n');
        }
        if (item.content && typeof item.content === 'object') {
            if (item.content.content_type) {
                return formatMessageText({
                    author: { role },
                    content: item.content,
                    metadata: item.metadata || {},
                });
            }
            if (typeof item.content.text === 'string') return item.content.text;
            if (Array.isArray(item.content.parts)) return extractTextFromParts(item.content.parts);
        }
        if (Array.isArray(item.parts)) return extractTextFromParts(item.parts);
        return '';
    }

    function parseChatGptConversation(conv) {
        const mapping = conv?.mapping || {};
        const path = resolveConversationPath(conv, mapping);
        const out = [];

        for (const id of path) {
            const node = mapping[id];
            const message = node?.message;
            if (!message) continue;
            if (shouldSkipMessage(message)) continue;
            if (!hasRenderablePayload(message)) continue;

            const role = normalizeMessageRole(message.author?.role || '');
            if (!role) continue;
            if (role === 'assistant' && shouldSkipAssistantInternalMessage(message)) continue;
            const model = message.metadata?.model_slug || '';
            let sender = role;
            if (role === 'assistant') {
                const gptMatch = model.match(/gpt-(.+)/i);
                sender = gptMatch ? `GPT-${gptMatch[1]}` : (model || 'assistant');
            }

            const content = formatMessageText(message).trim();
            if (!content) continue;
            if (role === 'assistant' && isNonAnswerAssistantContent(content)) continue;
            const ts = parseChatGptTimestamp(message.create_time || conv.create_time) || Date.now();
            out.push({ role, sender, content, ts });
        }

        out.sort((a, b) => a.ts - b.ts);
        return out.map(({ ts, ...rest }) => rest);
    }

    function resolveConversationPath(conv, mapping) {
        const current = conv?.current_node;
        const path = [];
        const seen = new Set();
        let nodeId = current;
        while (nodeId && mapping[nodeId] && !seen.has(nodeId)) {
            seen.add(nodeId);
            path.push(nodeId);
            nodeId = mapping[nodeId].parent;
        }
        if (path.length > 0) {
            path.reverse();
            return path;
        }
        const ids = Object.keys(mapping);
        ids.sort((a, b) => {
            const ta = Number(mapping[a]?.message?.create_time || 0);
            const tb = Number(mapping[b]?.message?.create_time || 0);
            return ta - tb;
        });
        return ids;
    }

    function normalizeMessageRole(role) {
        const value = String(role || '').toLowerCase().trim();
        if (value === 'user') return 'user';
        if (value === 'assistant') return 'assistant';
        return '';
    }

    function shouldSkipAssistantInternalMessage(messageData) {
        if (!messageData || messageData.author?.role !== 'assistant') return false;
        const recipient = String(messageData.recipient || '').trim().toLowerCase();
        if (recipient && recipient !== 'all') return true;
        const channel = String(messageData.channel || '').trim().toLowerCase();
        if (channel === 'commentary') return true;
        const contentType = String(messageData.content?.content_type || '').toLowerCase();
        if (contentType === 'execution_output') return true;
        if (contentType === 'code' && (recipient && recipient !== 'all')) return true;
        const reasoningStatus = String(messageData.metadata?.reasoning_status || '').toLowerCase();
        if (reasoningStatus === 'is_reasoning' && contentType === 'code') return true;
        return false;
    }

    function isNonAnswerAssistantContent(messageText) {
        const text = String(messageText || '').trim();
        if (!text) return false;
        if (/^Execution Output:/i.test(text)) return true;
        if (looksLikeToolCallJson(text)) return true;
        if (looksLikeSandboxPythonScript(text)) return true;
        if (looksLikeSandboxExecutionTuple(text)) return true;
        return false;
    }

    function stripOuterCodeFence(text) {
        const source = String(text || '').trim();
        const m = source.match(/^```[a-z0-9_-]*\n([\s\S]*?)\n```$/i);
        return m ? m[1].trim() : source;
    }

    function looksLikeToolCallJson(text) {
        const source = stripOuterCodeFence(text);
        if (!(source.startsWith('{') && source.endsWith('}'))) return false;
        let parsed = null;
        try {
            parsed = JSON.parse(source);
        } catch {
            return false;
        }
        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
        const keys = Object.keys(parsed).map((k) => String(k || '').toLowerCase());
        const toolKeys = new Set([
            'image_query',
            'search_query',
            'open',
            'click',
            'find',
            'screenshot',
            'sports',
            'finance',
            'weather',
            'time',
            'response_length',
            'tool_uses',
            'shell_command',
            'parallel',
        ]);
        return keys.some((k) => toolKeys.has(k) || k.endsWith('_query') || k.endsWith('_queries'));
    }

    function looksLikeSandboxPythonScript(text) {
        const source = stripOuterCodeFence(text);
        if (!/\/mnt\/data/i.test(source)) return false;
        if (/^from pathlib import Path\b/m.test(source)) return true;
        if (/\b(base|path)\s*=\s*Path\(\s*['"]\/mnt\/data['"]\s*\)/i.test(source)) return true;
        if (/\bwrite_text\(/i.test(source) && /\bPath\(/.test(source)) return true;
        return false;
    }

    function looksLikeSandboxExecutionTuple(text) {
        const source = stripOuterCodeFence(text);
        return /^\(\s*['"]\/mnt\/data\/[\s\S]*\)\s*$/i.test(source);
    }

    function parseChatGptTimestamp(value) {
        if (typeof value !== 'number' || Number.isNaN(value)) return null;
        return value * 1000;
    }

    function shouldSkipMessage(messageData) {
        if (!messageData) return true;
        if (messageData.author?.role === 'system') return true;
        if (messageData.metadata?.is_visually_hidden_from_conversation === true) return true;
        const contentType = messageData.content?.content_type;
        return SKIPPED_CONTENT_TYPES.has(contentType);
    }

    function hasRenderablePayload(messageData) {
        const content = messageData?.content;
        if (!content) return false;
        const contentType = content.content_type;
        if (contentType === 'code' || contentType === 'execution_output') {
            return typeof content.text === 'string' && content.text.trim().length > 0;
        }
        if (Array.isArray(content.parts)) {
            return extractTextFromParts(content.parts).trim().length > 0;
        }
        if (typeof content.text === 'string' && content.text.trim().length > 0) {
            return true;
        }
        if (Array.isArray(messageData.metadata?.content_references)) {
            return messageData.metadata.content_references.some((ref) => {
                return buildReferenceReplacement(ref).trim().length > 0;
            });
        }
        return false;
    }

    function formatMessageText(messageData) {
        const content = messageData.content || {};
        const contentType = content.content_type;
        const isText = contentType === 'text' || contentType === 'multimodal_text';
        let messageText = '';

        if (isText && Array.isArray(content.parts)) {
            messageText = extractTextFromParts(content.parts);
        } else if (contentType === 'code') {
            messageText = `\`\`\`${content.language || ''}\n${content.text || ''}\n\`\`\``;
        } else if (contentType === 'execution_output') {
            messageText = `Execution Output:\n> ${content.text || ''}`;
        } else if (Array.isArray(content.parts)) {
            messageText = extractTextFromParts(content.parts);
        } else if (typeof content.text === 'string') {
            messageText = content.text;
        } else if (Array.isArray(messageData.metadata?.content_references)) {
            messageText = messageData.metadata.content_references
                .map((reference) => buildReferenceReplacement(reference))
                .filter(Boolean)
                .join('\n');
        }

        if (messageData.author?.role === 'assistant') {
            messageText = processAssistantMessage(messageData, messageText);
        }
        return String(messageText || '').trim();
    }

    function processAssistantMessage(messageData, messageText) {
        let result = typeof messageText === 'string' ? messageText : '';
        result = processContentReferences(messageData, result);
        return result;
    }

    function extractTextFromParts(parts) {
        const segments = [];
        for (const part of parts) {
            if (typeof part === 'string') {
                segments.push(part);
                continue;
            }
            if (!part || typeof part !== 'object') continue;
            const objectPartText = extractObjectPartText(part);
            if (objectPartText) segments.push(objectPartText);
        }
        return joinTextSegments(segments).trim();
    }

    function joinTextSegments(segments) {
        let out = '';
        for (const seg of segments) {
            const text = String(seg ?? '');
            if (!text) continue;
            if (!out) {
                out = text;
                continue;
            }
            if (/[ \t\n]$/.test(out) || /^[ \t\n]/.test(text)) {
                out += text;
            } else {
                out += '\n' + text;
            }
        }
        return out;
    }

    function extractObjectPartText(part) {
        if (typeof part.text === 'string') return part.text;
        if (typeof part.value === 'string') return part.value;
        if (Array.isArray(part.parts)) return extractTextFromParts(part.parts);

        const contentType = String(part.content_type || part.type || '').toLowerCase();
        const imageUrl = extractFirstUrl([part.url, part.image_url, part.thumbnail_url, part.content_url, part.href]);
        if (contentType.includes('image')) {
            if (imageUrl) return `![image](${imageUrl})`;
            const pointer = part.asset_pointer || part.file_id || part.id || '';
            return pointer ? `[Image: ${String(pointer)}]` : '[Image]';
        }
        if (contentType.includes('audio')) return '[Audio]';
        if (contentType.includes('video')) return '[Video]';
        if (contentType.includes('file')) {
            const fileLabel = part.filename || part.name || part.file_id || part.id || 'file';
            return `[File: ${String(fileLabel)}]`;
        }
        if (imageUrl) return imageUrl;
        return '';
    }

    function processContentReferences(messageData, messageText) {
        const references = messageData.metadata?.content_references;
        if (!Array.isArray(references) || references.length === 0) return messageText;

        let result = messageText;
        const sortedReferences = [...references].sort((a, b) => {
            const aStart = typeof a?.start_idx === 'number' ? a.start_idx : -1;
            const bStart = typeof b?.start_idx === 'number' ? b.start_idx : -1;
            return bStart - aStart;
        });

        for (const reference of sortedReferences) {
            const matchedText = typeof reference?.matched_text === 'string' ? reference.matched_text : '';
            const start = reference?.start_idx;
            const end = reference?.end_idx;
            const replacement = buildReferenceReplacement(reference);
            if (!replacement) {
                if (matchedText && matchedText.trim().length > 0 && result.includes(matchedText)) {
                    result = result.replace(matchedText, '');
                    continue;
                }
                if (
                    typeof start === 'number' &&
                    typeof end === 'number' &&
                    start >= 0 &&
                    end >= start &&
                    start <= result.length &&
                    end <= result.length
                ) {
                    result = result.slice(0, start) + result.slice(end);
                }
                continue;
            }
            if (matchedText && matchedText.trim().length > 0 && result.includes(matchedText)) {
                result = result.replace(matchedText, replacement);
                continue;
            }

            if (
                typeof start === 'number' &&
                typeof end === 'number' &&
                start >= 0 &&
                end >= start &&
                start <= result.length &&
                end <= result.length
            ) {
                result = result.slice(0, start) + replacement + result.slice(end);
                continue;
            }

            if (matchedText && result.includes(matchedText)) {
                result = result.replace(matchedText, replacement);
            }
        }
        return result;
    }

    function buildReferenceReplacement(reference) {
        if (!reference || typeof reference !== 'object') return '';
        const type = String(reference.type || '').toLowerCase();
        const hasImageRef =
            type === 'image_v2' ||
            (Array.isArray(reference.refs) && reference.refs.some((item) => item?.ref_type === 'image'));
        if (hasImageRef) return buildImageReferenceReplacement(reference);
        if (type === 'grouped_webpages') return '';
        if (type === 'sources_footnote') return '';
        return '';
    }

    function buildImageReferenceReplacement(reference) {
        const markdownImages = [];
        const images = Array.isArray(reference.images) ? reference.images : [];
        for (const image of images.slice(0, 8)) {
            const src = image?.thumbnail_url || image?.content_url || image?.url;
            if (!src) continue;
            const title = sanitizeMarkdownLabel(image?.title || 'image');
            const link = extractFirstUrl([image?.url, image?.content_url]);
            if (link && link !== src) markdownImages.push(`[![${title}](${src})](${link})`);
            else markdownImages.push(`![${title}](${src})`);
        }
        if (markdownImages.length > 0) return `\n${markdownImages.join('\n')}\n`;
        if (typeof reference.alt === 'string' && reference.alt.trim()) return reference.alt.trim();
        if (typeof reference.prompt_text === 'string' && reference.prompt_text.trim()) {
            const promptText = reference.prompt_text.replace(/^(\s*##\s*Images:\s*)/i, '').trim();
            if (promptText) return `\n${promptText}\n`;
        }
        return '';
    }

    function buildGroupedWebpagesReplacement(reference) {
        const links = [];
        const items = Array.isArray(reference.items) ? reference.items : [];
        const fallbackItems = Array.isArray(reference.fallback_items) ? reference.fallback_items : [];
        for (const item of [...items, ...fallbackItems]) {
            const url = extractFirstUrl([item?.url]);
            if (!url) continue;
            const title = sanitizeMarkdownLabel(item?.title || item?.attribution || 'source');
            links.push(`[${title}](${url})`);
            if (links.length >= 5) break;
        }
        if (links.length === 1) return ` (${links[0]})`;
        if (links.length > 1) return ` (${links.join(' | ')})`;
        if (typeof reference.alt === 'string' && reference.alt.trim()) return reference.alt.trim();
        return '';
    }

    function buildSourcesFootnoteReplacement(reference) {
        const sources = Array.isArray(reference.sources) ? reference.sources : [];
        if (sources.length === 0) return '';
        const lines = [];
        for (const source of sources.slice(0, 8)) {
            const url = extractFirstUrl([source?.url]);
            if (!url) continue;
            const title = sanitizeMarkdownLabel(source?.title || source?.attribution || 'source');
            lines.push(`- [${title}](${url})`);
        }
        if (lines.length === 0) return '';
        return `\n\nSources:\n${lines.join('\n')}\n`;
    }

    function sanitizeMarkdownLabel(value) {
        return String(value || '')
            .replace(/[\[\]\r\n]+/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
    }

    function extractFirstUrl(candidates) {
        for (const candidate of candidates) {
            if (typeof candidate !== 'string') continue;
            const url = candidate.trim();
            if (/^(https?:\/\/|sandbox:|blob:|data:image\/)/i.test(url)) return url;
        }
        return '';
    }

    // ===================== 7. DOM 闂備胶鎳撻悘婵堢矓瀹曞洨绀婇柡鍐ㄧ墕閸愨偓缂備礁顑堝▔鏇熶繆?=====================
    const sleep = (ms) => new Promise(r => setTimeout(r, ms));

    async function renderLoadedMessagesOnPage(messages, timeoutMs = 10000) {
        const start = Date.now();
        let lastErr = null;
        while (Date.now() - start < timeoutMs) {
            if (canRenderVisualHistory()) {
                try {
                    renderVisualHistory(messages);
                    return;
                } catch (err) {
                    lastErr = err;
                }
            }
            await sleep(120);
        }
        if (lastErr) throw lastErr;
        throw new Error('未找到可渲染区域');
    }

    // ===================== 8. 婵犵數鍋為幐绋款嚕閸洘鍋傞悗锝庝憾閸ゆ洟鏌¢崶鈺佹瀾闁?=====================
    function findHistoryHostNode() {
        const firstArticle = document.querySelector('main article[data-turn-id], main article');
        if (firstArticle?.parentElement) return firstArticle.parentElement;
        return (
            document.querySelector('main #thread') ||
            document.querySelector('main') ||
            document.body
        );
    }

    function ensureVisualHistoryCanvas() {
        const host = findHistoryHostNode();
        if (!host) return null;
        let canvas = document.getElementById('resu-lc-thread');
        if (!canvas) {
            canvas = document.createElement('section');
            canvas.id = 'resu-lc-thread';
            canvas.className = 'resu-lc-thread';
            canvas.setAttribute('data-resu-injected', 'true');
            if (host.firstChild) host.insertBefore(canvas, host.firstChild);
            else host.appendChild(canvas);
        }
        return canvas;
    }

    function canRenderVisualHistory() {
        return !!findHistoryHostNode();
    }

    function ensureKatexStylesheet() {
        if (document.getElementById('resu-katex-css')) return;
        const style = document.createElement('style');
        style.id = 'resu-katex-css';
        style.textContent = `
          .resu-lc-math-block math {
            display: block;
            overflow-x: auto;
          }
          .resu-lc-markdown math {
            font-size: 1.04em;
          }
        `;
        document.head.appendChild(style);
    }

    function renderVisualHistory(messages) {
        ensureKatexStylesheet();
        const canvas = ensureVisualHistoryCanvas();
        if (!canvas) {
            console.warn('[Resu] render failed: mount not found');
            return;
        }
        const normalized = Array.isArray(messages) ? messages : [];
        canvas.innerHTML = '';
        const frag = document.createDocumentFragment();

        for (const raw of normalized) {
            const role = normalizeMessageRole(raw?.role || raw?.author || '');
            if (!role) continue;
            const content = String(raw?.content || '').trim();
            const sender = (typeof raw?.sender === 'string' && raw.sender.trim())
                ? raw.sender.trim()
                : (role === 'assistant' ? 'assistant' : 'anonymous');
            if (!content) continue;
            if (role === 'assistant' && isNonAnswerAssistantContent(content)) continue;

            if (role === 'user') {
                frag.appendChild(createUserTurnNode(content));
                continue;
            }
            frag.appendChild(createAssistantTurnNode({ role, sender, content }));
        }

        canvas.appendChild(frag);
    }

    function createUserTurnNode(content) {
        const turn = document.createElement('article');
        turn.className = 'resu-lc-turn resu-lc-turn-user group';
        turn.setAttribute('data-resu-injected', 'true');
        turn.innerHTML = `
          <div class="resu-lc-msg-shell">
            <div class="resu-lc-msg-inner">
              <div class="resu-lc-msg-main">
                <div class="resu-lc-text-message" dir="auto">
                  <div class="resu-lc-markdown">${renderMarkdownMessage(content || '')}</div>
                </div>
              </div>
            </div>
          </div>
        `;
        bindTurnActions(turn);
        return turn;
    }

    function createAssistantTurnNode(msg) {
        const turn = document.createElement('article');
        turn.className = 'resu-lc-turn resu-lc-turn-assistant group';
        turn.setAttribute('data-resu-injected', 'true');

        turn.innerHTML = `
          <div class="resu-lc-msg-shell">
            <div class="resu-lc-msg-inner">
              <div class="resu-lc-msg-main resu-lc-agent-turn">
                <div class="resu-lc-text-message" dir="auto">
                  <div class="resu-lc-markdown">${renderMarkdownMessage(msg.content || '')}</div>
                </div>
              </div>
            </div>
          </div>
        `;
        bindTurnActions(turn);
        return turn;
    }

    function renderCopyIconSvg() {
        return `
          <svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="none" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="icon-md-heavy" aria-hidden="true">
            <path fill="currentColor" fill-rule="evenodd" d="M7 5a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-2v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h2zm2 2h5a3 3 0 0 1 3 3v5h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1zM5 9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1z" clip-rule="evenodd"></path>
          </svg>
        `;
    }

    function bindTurnActions(root) {
        const codeButtons = root.querySelectorAll('.resu-copy-code');
        codeButtons.forEach((btn) => {
            btn.addEventListener('click', async () => {
                const encoded = btn.getAttribute('data-resu-code') || '';
                let decoded = '';
                try {
                    decoded = decodeURIComponent(encoded);
                } catch {
                    decoded = encoded;
                }
                await copyTextToClipboard(decoded);
                btn.classList.add('is-copied');
                const label = btn.querySelector('.resu-copy-code-live');
                if (label) label.textContent = 'Copied';
                setTimeout(() => {
                    btn.classList.remove('is-copied');
                    const resetLabel = btn.querySelector('.resu-copy-code-live');
                    if (resetLabel) resetLabel.textContent = 'Copy code';
                }, 1300);
            });
        });
    }

    async function copyTextToClipboard(text) {
        const value = String(text || '');
        if (!value) return;
        try {
            if (navigator?.clipboard?.writeText) {
                await navigator.clipboard.writeText(value);
                return;
            }
        } catch {}
        try {
            const ta = document.createElement('textarea');
            ta.value = value;
            ta.setAttribute('readonly', 'readonly');
            ta.style.position = 'fixed';
            ta.style.opacity = '0';
            ta.style.left = '-9999px';
            document.body.appendChild(ta);
            ta.focus();
            ta.select();
            document.execCommand('copy');
            ta.remove();
        } catch {}
    }

    function renderMarkdownMessage(markdownText) {
        const text = String(markdownText || '').replace(/\r\n?/g, '\n');
        if (!text.trim()) return '';

        const codeBlocks = [];
        const withCodePlaceholders = text.replace(/```([^\n`]*)\n([\s\S]*?)```/g, (_m, lang, code) => {
            const token = `@@RESU_CODE_${codeBlocks.length}@@`;
            codeBlocks.push({ lang: String(lang || '').trim(), code: String(code || '') });
            return `\n${token}\n`;
        });

        const lines = withCodePlaceholders.split('\n');
        const html = [];
        let i = 0;

        while (i < lines.length) {
            const line = lines[i];
            const trimmed = line.trim();
            if (!trimmed) {
                i++;
                continue;
            }

            const codeToken = trimmed.match(/^@@RESU_CODE_(\d+)@@$/);
            if (codeToken) {
                const idx = Number(codeToken[1]);
                const block = codeBlocks[idx];
                const langRaw = String(block?.lang || 'unknown').trim() || 'unknown';
                const lang = escapeHtml(langRaw);
                const code = escapeHtml(block?.code || '');
                const encoded = escapeAttribute(encodeURIComponent(block?.code || ''));
                html.push(`
                  <div class="resu-code-root relative w-full rounded-md bg-gray-900 text-xs text-white/80">
                    <div class="relative flex items-center justify-between rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
                      <span class="">${lang}</span>
                      <div class="flex items-center justify-center gap-4">
                        <button type="button" class="resu-copy-code ml-auto flex gap-2 rounded-sm focus:outline focus:outline-white" data-resu-code="${encoded}" aria-label="Copy code">
                          ${renderCopyIconSvg()}
                          <span class="relative">
                            <span class="invisible">Copy code</span>
                            <span class="resu-copy-code-live absolute inset-0 flex items-center">Copy code</span>
                          </span>
                        </button>
                      </div>
                    </div>
                    <div class="overflow-y-auto p-4">
                      <code class="hljs language-${lang} !whitespace-pre resu-code-content">${code}</code>
                    </div>
                    <div class="resu-code-float absolute bottom-2 right-2 flex items-center gap-2 font-sans text-xs text-gray-200 transition-opacity duration-150 pointer-events-none opacity-0">
                      <button type="button" class="resu-copy-code cursor-pointer flex items-center justify-center rounded p-1.5 hover:bg-gray-700 focus:bg-gray-700 focus:outline focus:outline-white" data-resu-code="${encoded}" aria-label="Copy code">
                        ${renderCopyIconSvg()}
                      </button>
                    </div>
                  </div>
                `);
                i++;
                continue;
            }

            const gallery = [];
            let j = i;
            while (j < lines.length) {
                const parsed = parseImageMarkdownLine(lines[j].trim());
                if (!parsed) break;
                gallery.push(parsed);
                j++;
            }
            if (gallery.length > 0) {
                html.push(renderImageGallery(gallery));
                i = j;
                continue;
            }

            if (trimmed === '\\[') {
                const mathLines = [];
                let k = i + 1;
                while (k < lines.length && lines[k].trim() !== '\\]') {
                    mathLines.push(lines[k]);
                    k++;
                }
                if (k < lines.length && lines[k].trim() === '\\]') {
                    html.push(renderMathBlock(mathLines.join('\n')));
                    i = k + 1;
                    continue;
                }
            }
            if (trimmed === '$$') {
                const mathLines = [];
                let k = i + 1;
                while (k < lines.length && lines[k].trim() !== '$$') {
                    mathLines.push(lines[k]);
                    k++;
                }
                if (k < lines.length && lines[k].trim() === '$$') {
                    html.push(renderMathBlock(mathLines.join('\n')));
                    i = k + 1;
                    continue;
                }
            }
            const singleBlockMath = trimmed.match(/^\\\[(.+)\\\]$/);
            if (singleBlockMath) {
                html.push(renderMathBlock(singleBlockMath[1]));
                i++;
                continue;
            }

            const heading = trimmed.match(/^(#{1,6})\s+(.+)$/);
            if (heading) {
                const level = Math.max(1, Math.min(6, heading[1].length));
                html.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
                i++;
                continue;
            }

            if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
                html.push('<hr />');
                i++;
                continue;
            }

            if (trimmed.startsWith('>')) {
                const quoteLines = [];
                while (i < lines.length && lines[i].trim().startsWith('>')) {
                    quoteLines.push(lines[i].replace(/^\s*>\s?/, ''));
                    i++;
                }
                html.push(`<blockquote>${quoteLines.map((q) => renderInlineMarkdown(q)).join('<br />')}</blockquote>`);
                continue;
            }

            if (isTableHeader(lines, i)) {
                const { tableHtml, nextIndex } = consumeTable(lines, i);
                html.push(tableHtml);
                i = nextIndex;
                continue;
            }

            if (/^[-*+]\s+/.test(trimmed)) {
                const { listHtml, nextIndex } = consumeList(lines, i, 'ul');
                html.push(listHtml);
                i = nextIndex;
                continue;
            }
            if (/^\d+\.\s+/.test(trimmed)) {
                const { listHtml, nextIndex } = consumeList(lines, i, 'ol');
                html.push(listHtml);
                i = nextIndex;
                continue;
            }

            const paraLines = [];
            while (i < lines.length && !isBlockStart(lines[i])) {
                if (lines[i].trim()) paraLines.push(lines[i]);
                i++;
            }
            html.push(`<p>${paraLines.map((p) => renderInlineMarkdown(p)).join('<br />')}</p>`);
        }

        return html.join('\n');
    }

    function renderMathBlock(expression) {
        const mathHtml = renderMathExpression(expression, true);
        return `<div class="resu-lc-math-block">${mathHtml}</div>`;
    }

    function renderMathExpression(expression, displayMode = false) {
        const expr = String(expression || '').trim();
        if (!expr) return '';
        const katexLib = window?.katex;
        if (katexLib && typeof katexLib.renderToString === 'function') {
            try {
                return katexLib.renderToString(expr, {
                    throwOnError: false,
                    strict: 'ignore',
                    displayMode,
                    // Use MathML to avoid relying on external KaTeX CSS blocked by CSP.
                    output: 'mathml',
                });
            } catch {}
        }
        return `<code class="resu-lc-math-raw">${escapeHtml(expr)}</code>`;
    }

    function parseImageMarkdownLine(line) {
        if (!line) return null;
        const linked = line.match(/^\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)$/);
        if (linked) {
            const alt = linked[1] || 'image';
            const src = safeUrl(linked[2]);
            const href = safeUrl(linked[3]);
            if (!src) return null;
            return { alt, src, href: href || src };
        }
        const plain = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
        if (plain) {
            const alt = plain[1] || 'image';
            const src = safeUrl(plain[2]);
            if (!src) return null;
            return { alt, src, href: src };
        }
        return null;
    }

    function renderImageGallery(images) {
        const items = images.map((img) => {
            const alt = escapeHtml(img.alt || 'image');
            const src = escapeAttribute(img.src);
            const href = escapeAttribute(img.href || img.src);
            return `<a href="${href}" target="_blank" rel="noopener noreferrer"><img src="${src}" alt="${alt}" loading="lazy" /></a>`;
        });
        return `<div class="resu-lc-gallery">${items.join('')}</div>`;
    }

    function consumeList(lines, startIndex, listType) {
        const isOrdered = listType === 'ol';
        const chunks = [];
        let i = startIndex;
        while (i < lines.length) {
            const trimmed = lines[i].trim();
            if (!trimmed) break;
            const marker = isOrdered ? /^\d+\.\s+(.+)$/ : /^[-*+]\s+(.+)$/;
            const m = trimmed.match(marker);
            if (!m) break;
            chunks.push(`<li>${renderInlineMarkdown(m[1])}</li>`);
            i++;
        }
        return { listHtml: `<${listType}>${chunks.join('')}</${listType}>`, nextIndex: i };
    }

    function isTableHeader(lines, index) {
        if (index + 1 >= lines.length) return false;
        const header = lines[index].trim();
        const sep = lines[index + 1].trim();
        if (!header.includes('|')) return false;
        return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(sep);
    }

    function splitTableRow(line) {
        const raw = line.trim().replace(/^\|/, '').replace(/\|$/, '');
        return raw.split('|').map((v) => v.trim());
    }

    function consumeTable(lines, startIndex) {
        const headCells = splitTableRow(lines[startIndex]);
        let i = startIndex + 2;
        const bodyRows = [];
        while (i < lines.length) {
            const line = lines[i].trim();
            if (!line || !line.includes('|')) break;
            bodyRows.push(splitTableRow(line));
            i++;
        }

        const headHtml = `<tr>${headCells.map((cell) => `<th>${renderInlineMarkdown(cell)}</th>`).join('')}</tr>`;
        const bodyHtml = bodyRows
            .map((row) => `<tr>${row.map((cell) => `<td>${renderInlineMarkdown(cell)}</td>`).join('')}</tr>`)
            .join('');
        return {
            tableHtml: `<div class="resu-lc-table-wrap"><table><thead>${headHtml}</thead><tbody>${bodyHtml}</tbody></table></div>`,
            nextIndex: i,
        };
    }

    function isBlockStart(line) {
        const trimmed = line.trim();
        if (!trimmed) return true;
        if (/^@@RESU_CODE_(\d+)@@$/.test(trimmed)) return true;
        if (/^(#{1,6})\s+/.test(trimmed)) return true;
        if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) return true;
        if (trimmed === '\\[' || trimmed === '$$' || /^\\\[(.+)\\\]$/.test(trimmed)) return true;
        if (trimmed.startsWith('>')) return true;
        if (/^[-*+]\s+/.test(trimmed)) return true;
        if (/^\d+\.\s+/.test(trimmed)) return true;
        if (parseImageMarkdownLine(trimmed)) return true;
        return false;
    }

    function renderInlineMarkdown(text) {
        const source = String(text || '');
        const mathTokens = [];
        let prepared = source.replace(/\\\(([\s\S]+?)\\\)/g, (_m, expr) => {
            const token = `@@RESU_MATH_${mathTokens.length}@@`;
            mathTokens.push(String(expr || '').trim());
            return token;
        });
        prepared = prepared.replace(/(^|[^\\])\$([^\n$]+?)\$/g, (_m, prefix, expr) => {
            const token = `@@RESU_MATH_${mathTokens.length}@@`;
            mathTokens.push(String(expr || '').trim());
            return `${prefix}${token}`;
        });

        let out = escapeHtml(prepared);
        out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_m, alt, url) => {
            const src = safeUrl(url);
            if (!src) return escapeHtml(_m);
            return `<img src="${escapeAttribute(src)}" alt="${alt || 'image'}" loading="lazy" />`;
        });
        out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => {
            const href = safeUrl(url);
            if (!href) return escapeHtml(_m);
            return `<a href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
        });
        out = out.replace(/`([^`]+)`/g, (_m, code) => `<code>${code}</code>`);
        out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
        out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>');
        out = out.replace(/~~([^~]+)~~/g, '<del>$1</del>');
        out = out.replace(/@@RESU_MATH_(\d+)@@/g, (_m, idxText) => {
            const idx = Number(idxText);
            if (!Number.isFinite(idx) || idx < 0 || idx >= mathTokens.length) return '';
            return renderMathExpression(mathTokens[idx], false);
        });
        return out;
    }

    function safeUrl(value) {
        const url = String(value || '').trim();
        if (/^(https?:\/\/|sandbox:|blob:|data:image\/)/i.test(url)) return url;
        if (url.startsWith('/')) return url;
        return '';
    }

    function escapeHtml(value) {
        return String(value || '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    function escapeAttribute(value) {
        return escapeHtml(value).replace(/`/g, '&#96;');
    }


    // ===================== 9. 闂備礁缍婇弲鎻掝渻閹烘梻涓嶆繛鍡樻尭缁€宀勬煛瀹ュ繐顩柣鐔村灪缁绘盯骞橀搹顐㈡闂?=====================
    function getConversationIdFromPath(pathname = window.location.pathname) {
        const text = String(pathname || '');
        const m = text.match(/\/c\/([^/?#]+)/i);
        return m ? m[1] : '';
    }

    function normalizeConversationPath(pathname = window.location.pathname) {
        const conversationId = getConversationIdFromPath(pathname);
        return conversationId ? `/c/${conversationId}` : '';
    }

    async function bindUrlToThread(threadId) {
        const currentPath = normalizeConversationPath(window.location.pathname);
        const conversationId = getConversationIdFromPath(currentPath);
        if (!conversationId) {
            setStatus('⚠️ 当前页面没有会话ID,无法绑定');
            return;
        }

        const savedByPath = await settingsGet(KEY_URL_MAP) || {};
        savedByPath[currentPath] = threadId;
        await settingsSet(KEY_URL_MAP, savedByPath);

        const savedBySession = await settingsGet(KEY_SESSION_MAP) || {};
        savedBySession[conversationId] = threadId;
        await settingsSet(KEY_SESSION_MAP, savedBySession);

        console.log(`[Resu] Bound Session ${conversationId} (${currentPath}) to Thread ${threadId}`);
        setStatus('✅ 已绑定会话ID,刷新可自动恢复');
    }
    async function checkAndAutoRender() {
        const currentPath = normalizeConversationPath(window.location.pathname);
        const conversationId = getConversationIdFromPath(currentPath);
        if (!conversationId) return;

        const savedByPath = await settingsGet(KEY_URL_MAP) || {};
        const savedBySession = await settingsGet(KEY_SESSION_MAP) || {};
        const threadId = savedByPath[currentPath] || savedBySession[conversationId];
        if (!threadId) return;

        // 兼容旧数据:如果只命中了 session 映射,补写路径映射,便于后续快速恢复
        if (!savedByPath[currentPath]) {
            savedByPath[currentPath] = threadId;
            await settingsSet(KEY_URL_MAP, savedByPath);
        }

        if (document.getElementById('resu-lc-thread')) return;
        console.log(`[Resu] Found auto-restore for session ${conversationId} -> ${threadId}`);
        setStatus('⏳ 正在自动恢复历史...');
        try {
            const msgs = await getFullThreadMessages(threadId);
            if (msgs && msgs.length > 0) {
                loadedMessages = msgs;
                lastThreadId = threadId;
                let retries = 0;
                const autoTimer = setInterval(() => {
                    retries++;
                    if (canRenderVisualHistory()) {
                        clearInterval(autoTimer);
                        renderVisualHistory(msgs);
                        setStatus(`✅ 自动恢复成功 (${msgs.length}条)`);
                    } else if (retries > 60) {
                        clearInterval(autoTimer);
                        setStatus('⚠️ 自动恢复等待中 (未找到模板)');
                    }
                }, 250);
            }
        } catch (e) {
            console.error(e);
            setStatus('❌ 自动恢复出错');
        }
    }
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            setTimeout(checkAndAutoRender, 1500);
        }
    }).observe(document, { subtree: true, childList: true });

    // ===================== 10. 本地渲染主流程 =====================
    async function renderOnlyFlow(messages) {
        showOverlay('正在渲染…', '正在将存档渲染到当前页面...');
        await renderLoadedMessagesOnPage(messages, 10000);
        try {
            await bindUrlToThread(lastThreadId);
        } catch (e) {
            console.warn('[Resu] 会话ID绑定失败(不影响渲染):', e);
        }
        return { totalParts: 1 };
    }

    // ===================== 11. 闂備礁婀遍搹搴ㄥ储妤e喛缍栭柟鐑樺焾濞尖晠鏌涘┑鍕姕闁哄懏鎮傞獮?=====================
    async function seedFromLoadedJson(threadId, msgs, titleFromImport) {
        await idbOpen();
        await ensureThread(threadId, titleFromImport);
        await clearThread(threadId);
        for (const m of msgs) {
            await addMsg(threadId, m.role, m.content, Date.now(), 'base', {
                sender: m.sender || '',
            });
        }
    }
    async function loadJsonObjectToState(raw, inferredName) {
        const msgs = parseJsonToMessages(raw);
        if (!msgs.length) {
            setStatus('❌ 解析失败(无消息)');
            loadedMessages = [];
            $p('#resu-preview').value = '';
            disableActions();
            return false;
        }
        const importedTitle = extractTitleFromJson(raw) || inferredName || 'chat_history';
        const base = safeFileName(importedTitle);
        const hint = `${inferredName || ''}|${raw?.exported_at || ''}|${msgs.length}|${JSON.stringify(raw)?.length || 0}`;
        const threadId = `${base}__${fnv1a32(hint)}`;
        loadedMessages = msgs;
        lastThreadTitle = importedTitle;
        lastThreadId = threadId;
        const jsonStr = JSON.stringify(msgs);
        $p('#resu-preview').value =
            `已载入:${loadedMessages.length} 条消息\n` +
            `标题:${importedTitle}\n` +
            `模式:本地渲染(不发送消息) + 自动恢复\n` +
            `压缩后体积:${jsonStr.length} 字符\n`;
        setStatus('⏳ 写入库…');
        await seedFromLoadedJson(threadId, loadedMessages, importedTitle);
        setStatus(`✅ 已载入:${loadedMessages.length} 条`);
        enableActions();
        return true;
    }
    // ===================== 12. UI 构建 (重构版) =====================
    // 注入样式
    const styleEl = document.createElement('style');
    styleEl.textContent = `
    .resu-wrapper {
      --resu-poke-red: #6942d6;
      --resu-poke-red-dark: #3e277f;
      --resu-poke-yellow: #ff63d4;
      --resu-poke-yellow-dark: #ca43aa;
      --resu-poke-blue: #88d9ff;
      --resu-poke-blue-dark: #3f77c7;
      --resu-poke-ink: #130f24;
      --resu-poke-cream: #edf7ff;
      --resu-poke-border: #090b14;
      font-family: "Trebuchet MS", "Segoe UI", Tahoma, sans-serif;
    }
    .resu-toggle-btn {
        position: fixed;
        top: 50%;
        right: 14px;
        transform: translateY(-50%);
        width: 60px;
        height: 60px;
        border: 4px solid var(--resu-poke-border);
        border-radius: 999px;
        cursor: pointer;
        z-index: 999990;
        display: flex;
        align-items: center;
        justify-content: center;
        background:
          radial-gradient(circle at 27% 21%, #ff63d4 0 12px, transparent 13px),
          radial-gradient(circle at 73% 21%, #ff63d4 0 12px, transparent 13px),
          linear-gradient(180deg, var(--resu-poke-red) 0 46%, var(--resu-poke-border) 46% 53%, var(--resu-poke-cream) 53% 100%);
        box-shadow: 0 10px 20px rgba(0,0,0,0.38), inset 0 0 0 2px rgba(255,255,255,0.22);
        transition: transform 0.18s ease, filter 0.2s ease;
        overflow: hidden;
    }
    .resu-toggle-btn::before {
        content: '';
        position: absolute;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        border: 3px solid var(--resu-poke-border);
        background: #dff4ff;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        box-shadow: inset 0 0 0 3px #a8d8ef;
    }
    .resu-toggle-btn::after {
        content: 'M';
        position: absolute;
        top: 8px;
        left: 50%;
        transform: translateX(-50%);
        color: #dee3ff;
        font-size: 15px;
        font-weight: 900;
        letter-spacing: 0.5px;
        text-shadow: 0 1px 0 rgba(0,0,0,0.45);
    }
    .resu-toggle-btn svg { display: none; }
    .resu-toggle-btn:hover {
        transform: translateY(-50%) scale(1.06);
        filter: saturate(1.08);
    }
    .resu-panel {
        position: fixed;
        top: 10px;
        right: 10px;
        width: min(356px, calc(100vw - 20px));
        color: var(--resu-poke-ink);
        z-index: 999999;
        border: 4px solid var(--resu-poke-border);
        border-radius: 10px;
        background:
          linear-gradient(180deg, var(--resu-poke-red) 0 56px, #160f31 56px 63px, var(--resu-poke-cream) 63px 100%);
        box-shadow: 0 16px 44px rgba(0,0,0,0.4), inset 0 0 0 2px rgba(255,255,255,0.24);
        display: none;
        flex-direction: column;
        padding: 10px 12px 12px;
        animation: resuSlideIn 0.28s cubic-bezier(0.22, 1, 0.36, 1);
        overflow: hidden;
        isolation: isolate;
        gap: 6px;
    }
    .resu-panel::before {
      content: '';
      position: absolute;
      inset: 5px;
      border: 2px solid rgba(255,255,255,0.58);
      border-radius: 6px;
      pointer-events: none;
      opacity: 0.45;
    }
    .resu-panel::after {
      content: '';
      position: absolute;
      left: 12px;
      right: 12px;
      top: 61px;
      height: 1px;
      background: rgba(255,255,255,0.35);
      pointer-events: none;
    }
    @keyframes resuSlideIn { from { opacity: 0; transform: translateX(20px) scale(0.97); } to { opacity: 1; transform: translateX(0) scale(1); } }
    .resu-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin: -10px -12px 4px -12px;
      padding: 7px 10px 8px;
      border-bottom: 2px solid rgba(255,255,255,0.2);
      background: linear-gradient(180deg, rgba(255,255,255,0.09) 0%, rgba(255,255,255,0) 100%);
      color: #fff;
    }
    .resu-title {
      display: inline-flex;
      align-items: center;
      gap: 7px;
      font-weight: 900;
      font-size: 14px;
      color: #f6f8ff;
      letter-spacing: 0.25px;
      text-transform: none;
      text-shadow: 0 2px 0 rgba(0,0,0,0.3);
    }
    .resu-title::before {
      content: '';
      width: 14px;
      height: 14px;
      border-radius: 50%;
      border: 2px solid #1d103b;
      background: linear-gradient(180deg, #a78cff 0 48%, #1d103b 48% 58%, #dcecff 58% 100%);
      box-shadow: inset 0 0 0 1px rgba(255,255,255,0.35);
      flex: 0 0 14px;
    }
    .resu-close {
      border: 3px solid var(--resu-poke-border);
      background: #dff2ff;
      color: var(--resu-poke-red-dark);
      cursor: pointer;
      width: 30px;
      height: 30px;
      border-radius: 999px;
      font-weight: 900;
      line-height: 1;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 2px 0 rgba(0,0,0,0.28);
    }
    .resu-close:hover { background: #cce7fb; }
    .resu-section-label {
      color: #f7f1ff;
      font-size: 10px;
      margin: 4px 0 4px 0;
      font-weight: 900;
      text-transform: uppercase;
      letter-spacing: 0.9px;
      background: linear-gradient(180deg, #7f62e3 0%, #4f36a8 100%);
      border: 2px solid #261453;
      border-radius: 6px;
      padding: 4px 8px;
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.3);
    }
    .resu-btn {
      border: 3px solid #120d26;
      border-radius: 8px;
      cursor: pointer;
      font-size: 12px;
      font-weight: 900;
      padding: 9px 12px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      transition: transform 0.14s ease, box-shadow 0.14s ease, filter 0.2s ease;
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), 0 3px 0 rgba(0,0,0,0.25);
      text-shadow: 0 1px 0 rgba(0,0,0,0.26);
    }
    .resu-btn:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
    .resu-btn:hover:not(:disabled) { transform: translateY(-1px); filter: saturate(1.08) brightness(1.02); }
    .resu-btn:active:not(:disabled) { transform: translateY(1px); box-shadow: inset 0 1px 0 rgba(255,255,255,0.25), 0 1px 0 rgba(0,0,0,0.28); }
    .resu-btn-primary {
      background: linear-gradient(180deg, var(--resu-poke-yellow) 0%, var(--resu-poke-yellow-dark) 100%);
      color: #fff;
      width: 100%;
      margin-top: 0;
      padding: 10px;
      font-size: 13px;
    }
    .resu-btn-secondary {
      background: linear-gradient(180deg, #7a59e3 0%, var(--resu-poke-red-dark) 100%);
      color: #fff;
    }
    .resu-btn-danger {
      background: linear-gradient(180deg, #9245d6 0%, #6c2fa8 100%);
      color: #fff;
    }
    .resu-btn-export {
      background: linear-gradient(180deg, var(--resu-poke-blue) 0%, var(--resu-poke-blue-dark) 100%);
      color: #fff;
    }
    .resu-row {
      display: flex;
      gap: 8px;
      align-items: stretch;
    }
    .resu-row-tight { margin-top: 10px; }
    .resu-card .resu-row:first-child { margin-bottom: 8px; }
    .resu-row > .resu-btn {
      min-height: 44px;
    }
    .resu-btn-fill { flex: 1; }
    .resu-btn-block { width: 100%; }
    .resu-btn-icon {
      width: 48px;
      flex: 0 0 48px;
      padding: 0;
      font-size: 15px;
    }
    .resu-panel-body {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-top: 2px;
    }
    .resu-card {
      border: 2px solid #2b1c59;
      border-radius: 8px;
      background: linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0.18) 100%);
      padding: 9px;
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.3);
    }
    .resu-card-status {
      padding: 6px 8px 8px;
    }
    .resu-divider {
      height: 2px;
      border-radius: 2px;
      background: linear-gradient(90deg, rgba(80,55,170,0.1) 0%, rgba(80,55,170,0.85) 18%, rgba(80,55,170,0.85) 82%, rgba(80,55,170,0.1) 100%);
      margin: 1px 4px 0;
    }
    .resu-select {
      width: 100%;
      background: linear-gradient(180deg, #ffffff 0%, #e9f4ff 100%);
      color: #151734;
      border: 3px solid #2e205e;
      padding: 8px 10px;
      border-radius: 8px;
      outline: none;
      margin: 8px 0;
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.92), 0 2px 0 rgba(0,0,0,0.15);
      font-weight: 700;
      min-height: 44px;
    }
    #resu-status {
      font-size: 11px;
      color: #2f2b66;
      text-align: left;
      margin-top: 0;
      height: 20px;
      line-height: 18px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-weight: 700;
      border: 2px solid #2b1c59;
      border-radius: 6px;
      background: linear-gradient(180deg, #f7fcff 0%, #dcecff 100%);
      padding: 0 8px;
    }
    .resu-debug { margin-top: 6px; }
    #resu-preview {
      width: 100%;
      height: 100px;
      background: #1a1431;
      border: 3px solid #2f2259;
      color: #c5eeff;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
      font-size: 10px;
      padding: 8px;
      border-radius: 8px;
      resize: none;
      outline: none;
      box-shadow: inset 0 0 0 1px rgba(168,216,239,0.25);
    }
    details > summary {
      color: #2f2b66;
      font-size: 11px;
      cursor: pointer;
      margin-top: 4px;
      list-style: none;
      font-weight: 800;
      border: 2px solid #2b1c59;
      border-radius: 6px;
      background: linear-gradient(180deg, #f7fbff 0%, #ddeeff 100%);
      padding: 4px 8px;
      display: inline-block;
    }
    .resu-debug > summary { margin-top: 0; }
    .resu-panel > * {
      position: relative;
      z-index: 1;
    }
    details > summary:hover {
      color: #fff;
      background: linear-gradient(180deg, #7e63e3 0%, #5037aa 100%);
    }
    .resu-lc-thread {
      --resu-lc-text: #ececf1;
      --resu-lc-text-secondary: #a1a1aa;
      --resu-lc-border: #3f3f46;
      --resu-lc-surface: #171717;
      --resu-lc-surface-hover: #262626;
      padding: 6px 0 26px;
      color: var(--resu-lc-text);
    }
    .resu-lc-turn {
      width: 100%;
      margin: 0;
      color: inherit;
      padding: 0.2rem 0;
    }
    .resu-lc-msg-shell {
      width: 100%;
      border: 0;
      background: transparent;
    }
    .resu-lc-msg-inner {
      position: relative;
      margin: 0 auto;
      display: flex;
      width: 100%;
      padding: 0.08rem 1rem;
      justify-content: center;
    }
    @media (min-width: 640px) {
      .resu-lc-msg-inner { padding: 0.08rem 1.5rem; }
    }
    @media (min-width: 1024px) {
      .resu-lc-msg-inner { padding: 0.08rem 4rem; }
    }
    .resu-lc-msg-main {
      position: relative;
      display: flex;
      width: 100%;
      max-width: 48rem;
      min-width: 0;
      flex-direction: column;
      gap: 0.15rem;
    }
    .resu-lc-turn-user .resu-lc-msg-main {
      width: 100%;
      align-items: flex-end;
    }
    .resu-lc-turn-assistant .resu-lc-msg-main {
      width: 100%;
      align-items: flex-start;
    }
    .resu-lc-sender { display: none; }
    .resu-lc-text-message {
      display: flex;
      min-height: 18px;
      flex-direction: column;
      align-items: stretch;
      width: 100%;
      gap: 0.4rem;
      overflow: visible;
    }
    .resu-lc-turn-user .resu-lc-text-message {
      align-items: flex-end;
    }
    .resu-lc-turn-assistant .resu-lc-text-message {
      align-items: flex-start;
    }
    .resu-lc-markdown {
      width: 100%;
      max-width: 100%;
      word-break: break-word;
      line-height: 1.62;
      font-size: 0.99rem;
      color: var(--resu-lc-text);
      border: 0;
      border-radius: 0;
      padding: 0;
      box-shadow: none;
      background: transparent;
    }
    .resu-lc-turn-assistant .resu-lc-markdown {
      width: 100%;
      background: transparent;
      border: 0;
      border-radius: 0;
      padding: 0;
      box-shadow: none;
      margin-right: auto;
    }
    .resu-lc-turn-user .resu-lc-markdown {
      width: fit-content;
      max-width: min(100%, var(--resu-user-chat-width, 70%));
      background: #323232d9;
      color: #ececec;
      border: 0;
      border-radius: 18px;
      padding: 0.62rem 1rem;
      box-shadow: none;
      margin-left: auto;
    }
    .resu-lc-markdown p { margin: 0 0 0.5rem 0; white-space: pre-wrap; }
    .resu-lc-markdown p:last-child { margin-bottom: 0; }
    .resu-lc-markdown h1,
    .resu-lc-markdown h2,
    .resu-lc-markdown h3,
    .resu-lc-markdown h4,
    .resu-lc-markdown h5,
    .resu-lc-markdown h6 {
      margin: 1rem 0 0.5rem 0;
      color: #f8fafc;
      line-height: 1.28;
      font-weight: 700;
    }
    .resu-lc-markdown h1 { font-size: 1.35rem; }
    .resu-lc-markdown h2 { font-size: 1.2rem; }
    .resu-lc-markdown h3 { font-size: 1.08rem; }
    .resu-lc-markdown ul, .resu-lc-markdown ol { margin: 0 0 0.65rem 1.2rem; }
    .resu-lc-markdown li { margin: 0.2rem 0; }
    .resu-lc-markdown img {
      display: block;
      max-width: min(420px, 100%);
      height: auto;
      border-radius: 0.75rem;
      border: 1px solid #3f3f46;
      margin: 0.2rem 0;
    }
    .resu-lc-markdown a {
      color: #8ab4ff;
      text-decoration: underline;
      text-underline-offset: 2px;
      text-decoration-color: rgba(138,180,255,0.5);
    }
    .resu-lc-markdown a:hover { color: #b6d1ff; }
    .resu-lc-markdown code:not(.resu-code-content) {
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
      background: #202123;
      border: 1px solid #3f3f46;
      border-radius: 0.38rem;
      padding: 0.06rem 0.3rem;
      font-size: 0.9em;
      color: #e5e7eb;
    }
    .resu-lc-markdown pre {
      width: 100%;
      margin: 0 0 0.75rem 0;
      padding: 0;
      border: 0;
      background: transparent;
      overflow: visible;
    }
    .resu-code-root { margin: 0 0 0.75rem 0; }
    .resu-code-root .icon-md-heavy {
      stroke-width: 2;
      width: 1.125rem;
      height: 1.125rem;
    }
    .resu-code-content {
      display: block;
      white-space: pre !important;
      font-size: calc(0.85 * var(--markdown-font-size, 1rem));
      line-height: 1.5;
      color: inherit;
      background: transparent !important;
      border: 0 !important;
      padding: 0 !important;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
    }
    .resu-code-root:hover .resu-code-float,
    .resu-code-root:focus-within .resu-code-float {
      pointer-events: auto;
      opacity: 1;
    }
    .resu-copy-code.is-copied .resu-copy-code-live { color: #d1fae5; }
    .resu-lc-math-block {
      width: 100%;
      overflow-x: auto;
      margin: 0 0 0.75rem 0;
      padding: 0.2rem 0;
    }
    .resu-lc-math-block .katex-display {
      margin: 0;
      padding: 0;
    }
    .resu-lc-markdown .katex {
      color: #ececf1;
      font-size: 1.04em;
    }
    .resu-lc-math-raw {
      display: inline-block;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
      background: #202123;
      border: 1px solid #3f3f46;
      border-radius: 0.3rem;
      padding: 0.12rem 0.3rem;
      color: #e5e7eb;
    }
    .resu-lc-markdown blockquote {
      margin: 0 0 0.7rem 0;
      border-left: 2px solid #4b5563;
      padding: 0.15rem 0 0.15rem 0.75rem;
      color: #d4d4d8;
      background: rgba(24,24,27,0.55);
      border-radius: 0 0.45rem 0.45rem 0;
    }
    .resu-lc-markdown hr { border: 0; border-top: 1px solid #3f3f46; margin: 0.8rem 0; }
    .resu-lc-table-wrap {
      width: 100%;
      overflow-x: auto;
      margin: 0 0 0.75rem 0;
      border: 1px solid #3f3f46;
      border-radius: 0.6rem;
    }
    .resu-lc-table-wrap table { width: 100%; border-collapse: collapse; font-size: 0.83rem; }
    .resu-lc-table-wrap th,
    .resu-lc-table-wrap td {
      border-bottom: 1px solid #3f3f46;
      padding: 0.45rem 0.6rem;
      text-align: left;
      vertical-align: top;
    }
    .resu-lc-table-wrap th { background: #23252b; color: #f3f4f6; font-weight: 700; }
    .resu-lc-table-wrap tr:last-child td { border-bottom: 0; }
    .resu-lc-gallery {
      display: flex;
      flex-wrap: wrap;
      gap: 0.55rem;
      margin: 0 0 0.75rem 0;
    }
    .resu-lc-gallery a {
      display: block;
      width: min(320px, 100%);
      border-radius: 0.75rem;
      overflow: hidden;
      border: 1px solid #3f3f46;
      background: #18181b;
      line-height: 0;
    }
    .resu-lc-gallery img {
      display: block;
      width: 100%;
      height: auto;
      max-height: 320px;
      object-fit: cover;
      margin: 0;
    }
    @media (max-width: 560px) {
      .resu-panel {
        top: 8px;
        right: 8px;
        left: 8px;
        width: auto;
      }
      .resu-toggle-btn {
        right: 8px;
        width: 56px;
        height: 56px;
      }
    }
  `;
    document.head.appendChild(styleEl);
    const wrapper = document.createElement('div');
    wrapper.className = 'resu-wrapper';
    wrapper.innerHTML = `
    <!-- 悬浮开关 -->
    <div id="resu-toggle" class="resu-toggle-btn" title="打开控制面板">
       <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
    </div>
    <!-- 主面板 -->
    <div id="resu-panel" class="resu-panel">
      <div class="resu-header">
        <div class="resu-title">控制面板</div>
        <button id="resu-close" class="resu-close" title="收起">✕</button>
      </div>
      <div class="resu-panel-body">
        <div class="resu-card">
          <div class="resu-row">
            <button id="resu-lib-pick" class="resu-btn resu-btn-secondary resu-btn-fill">📁 绑定库目录</button>
            <button id="resu-lib-refresh" class="resu-btn resu-btn-secondary resu-btn-icon" title="刷新文件列表">🔄</button>
          </div>
          <select id="resu-lib-select" class="resu-select"><option value="">(未绑定或文件夹为空)</option></select>
          <button id="resu-lib-load" class="resu-btn resu-btn-secondary resu-btn-block">📥 读取选中存档</button>
        </div>
        <div class="resu-divider"></div>
        <div class="resu-card">
          <button id="resu-send" disabled class="resu-btn resu-btn-primary resu-btn-block">🧩 渲染聊天记录</button>
          <div class="resu-row resu-row-tight">
            <button id="resu-export" disabled class="resu-btn resu-btn-export resu-btn-fill">💾 备份当前对话</button>
            <button id="resu-clear" disabled class="resu-btn resu-btn-danger resu-btn-fill">🗑️ 清除缓存</button>
          </div>
        </div>
        <div class="resu-card resu-card-status">
          <div id="resu-status">等待操作...</div>
          <details class="resu-debug">
            <summary>调试控制台</summary>
            <textarea id="resu-preview" readonly></textarea>
          </details>
        </div>
      </div>
    </div>
  `;
    document.body.appendChild(wrapper);
    const $p = (sel) => wrapper.querySelector(sel);
    const setStatus = (msg) => {
        const el = $p('#resu-status');
        if (!el) return;
        el.textContent = msg;
        el.title = msg;
    };

    // UI 交互逻辑
    const toggleBtn = $p('#resu-toggle');
    const mainPanel = $p('#resu-panel');
    const closeBtn = $p('#resu-close');
    toggleBtn.addEventListener('click', () => {
        mainPanel.style.display = 'flex';
        toggleBtn.style.display = 'none';
    });
    closeBtn.addEventListener('click', () => {
        mainPanel.style.display = 'none';
        toggleBtn.style.display = 'flex';
    });
    function enableActions() {
        $p('#resu-send').disabled = false;
        $p('#resu-export').disabled = false;
        $p('#resu-clear').disabled = false;
    }
    function disableActions() {
        $p('#resu-send').disabled = true;
        $p('#resu-export').disabled = true;
        $p('#resu-clear').disabled = true;
    }

    // ===================== 13. 闂傚倷绶¢崜锕傚窗濮樿泛纾?=====================
    const overlayState = { prevOverflow: null, enabled: true, runtime: null };
    function overlayOn() { return (overlayState.runtime ?? overlayState.enabled) === true; }
    function ensureOverlay() {
        let ov = document.getElementById('resu-overlay');
        const hasValidNodes = ov &&
            ov.querySelector('#resu-ov-title') &&
            ov.querySelector('#resu-ov-sub') &&
            ov.querySelector('#resu-spin');
        if (hasValidNodes) return ov;
        if (ov) ov.remove();
        ov = document.createElement('div');
        ov.id = 'resu-overlay';
        ov.style.cssText = `position:fixed; inset:0; background:rgba(0,0,0,0.95); z-index:1000000; display:none; align-items:center; justify-content:center; backdrop-filter: blur(4px);`;
        ov.innerHTML = `
      <div style="width:min(500px,90vw); background:#161227; border:1px solid #4a3398; border-radius:16px; padding:24px; box-shadow:0 20px 50px rgba(0,0,0,0.8); color:#fff; text-align:center;">
        <div id="resu-spin" style="margin:0 auto 16px; width:32px; height:32px; border:4px solid rgba(255,255,255,.14); border-top-color:#7b55e8; border-radius:50%; animation:resuSpin 1s linear infinite;"></div>
        <div id="resu-ov-title" style="font-weight:700; font-size:16px; margin-bottom:8px;">正在处理</div>
        <div id="resu-ov-sub" style="color:#b7a7ef; font-size:13px;">请稍候...</div>
      </div>
    `;
        const style = document.createElement('style');
        style.textContent = `@keyframes resuSpin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`;
        ov.appendChild(style);
        document.body.appendChild(ov);
        return ov;
    }
    function getOverlayParts() {
        try {
            let ov = ensureOverlay();
            if (!ov) return null;
            let titleEl = ov.querySelector('#resu-ov-title');
            let subEl = ov.querySelector('#resu-ov-sub');
            let spinEl = ov.querySelector('#resu-spin');
            if (!titleEl || !subEl || !spinEl) {
                ov.remove();
                ov = ensureOverlay();
                if (!ov) return null;
                titleEl = ov.querySelector('#resu-ov-title');
                subEl = ov.querySelector('#resu-ov-sub');
                spinEl = ov.querySelector('#resu-spin');
                if (!titleEl || !subEl || !spinEl) return null;
            }
            return { ov, titleEl, subEl, spinEl };
        } catch (e) {
            console.warn('[Resu] overlay init failed:', e);
            return null;
        }
    }
    function lockScroll() {
        if (overlayState.prevOverflow == null) {
            overlayState.prevOverflow = document.documentElement.style.overflow;
            document.documentElement.style.overflow = 'hidden';
        }
    }
    function unlockScroll() {
        if (overlayState.prevOverflow != null) {
            document.documentElement.style.overflow = overlayState.prevOverflow;
            overlayState.prevOverflow = null;
        }
    }
    function showOverlay(title, sub) {
        if (!overlayOn()) return;
        const parts = getOverlayParts();
        if (!parts) return;
        const { ov, titleEl, subEl, spinEl } = parts;
        ov.style.display = 'flex';
        lockScroll();
        titleEl.textContent = title || '正在渲染…';
        subEl.textContent = sub || '请稍候';
        spinEl.style.display = 'block';
        spinEl.style.animation = 'resuSpin 1s linear infinite';
        spinEl.style.borderColor = 'rgba(255,255,255,.1)';
        spinEl.style.borderTopColor = '#7b55e8';
    }
    function showOverlaySuccess(title, sub) {
        if (!overlayOn()) return;
        const parts = getOverlayParts();
        if (!parts) return;
        const { ov, titleEl, subEl, spinEl } = parts;
        ov.style.display = 'flex';
        lockScroll();
        titleEl.textContent = title || '✅ 操作成功';
        subEl.textContent = sub || '已完成';
        spinEl.style.display = 'block';
        spinEl.style.animation = 'none';
        spinEl.style.borderColor = 'rgba(136,217,255,.35)';
        spinEl.style.borderTopColor = '#88d9ff';
    }
    function showOverlayFail(title, sub) {
        if (!overlayOn()) return;
        const parts = getOverlayParts();
        if (!parts) return;
        const { ov, titleEl, subEl, spinEl } = parts;
        ov.style.display = 'flex';
        lockScroll();
        titleEl.textContent = title || '⚠️ 操作失败';
        subEl.textContent = sub || '已取消';
        spinEl.style.display = 'none';
    }
    function hideOverlay() {
        const ov = document.getElementById('resu-overlay');
        if (ov) ov.style.display = 'none';
        unlockScroll();
    }

    // ===================== 14. 闂備礁鎲$敮妤冩崲閸岀儑缍栭柟鐗堟緲缁€宀勬煛瀹ュ繐顩柣鐔村灩闇夋繝濠傚枤閸庡孩绻涢崱鎰伄缂佽鲸甯¢獮瀣攽閸喐鐦?=====================
    async function loadJsonFromLibrarySelected() {
        if (!FS.dirHandle || !FS.fileList.length) { setStatus('⚠️ 请先点击刷新按钮'); return; }
        const name = $p('#resu-lib-select').value;
        const item = FS.fileList.find(x => x.name === name);
        if (!item) { setStatus('⚠️ 请选择一个有效文件'); return; }
        try {
            const file = await item.handle.getFile();
            const text = await file.text();
            const raw = JSON.parse(text);
            const inferredName = file.name.replace(/\.json$/i, '');
            await loadJsonObjectToState(raw, inferredName);
        } catch (e) {
            console.error(e);
            setStatus('❌ 载入失败,文件损坏?');
        }
    }
    $p('#resu-lib-pick').addEventListener('click', async () => { await pickLibraryFolder(); });
    $p('#resu-lib-refresh').addEventListener('click', async () => { await refreshLibraryList(); });
    $p('#resu-lib-load').addEventListener('click', async () => { await loadJsonFromLibrarySelected(); });
    $p('#resu-export').addEventListener('click', async () => {
        try {
            if (!lastThreadId) return;
            const data = await exportThread(lastThreadId);
            const filename = `${safeFileName(lastThreadTitle)}.json`;
            const jsonString = JSON.stringify(data, null, 2);
            const wrote = await writeJsonToFolderIfPossible(filename, jsonString);
            if (wrote) setStatus(`✅ 已导出到库:${filename}`);
            else { downloadJson(data, filename); setStatus('✅ 已下载文件'); }
        } catch (e) { setStatus('❌ 导出失败'); }
    });
    $p('#resu-clear').addEventListener('click', async () => {
        try {
            if (!lastThreadId) return;
            if (!confirm('确定要清空当前线程的缓存吗?')) return;
            await clearThread(lastThreadId);
            setStatus('🧹 缓存已清空');
        } catch (e) { setStatus('❌ 清空失败'); }
    });
    $p('#resu-send').addEventListener('click', async () => {
        if (!loadedMessages.length) {
            setStatus('⚠️ 请先读取一个存档');
            return;
        }
        try {
            await renderOnlyFlow(loadedMessages);
            showOverlaySuccess('✅ 渲染成功', '已绑定会话ID,刷新可自动恢复');
            setTimeout(() => { hideOverlay(); setStatus('✅ 渲染完成(仅本地)'); }, 1200);
        } catch (e) {
            console.error(e);
            const msg = String(e?.message || e || '未知错误');
            showOverlayFail('❌ 渲染失败', msg);
            setTimeout(() => { hideOverlay(); setStatus(`❌ 渲染失败:${msg}`); }, 2200);
        }
    });

    // ===================== 15. 初始化 =====================
    (async () => {
        disableActions();
        // [设置] 强制默认值
        overlayState.enabled = ENABLE_OVERLAY_DEFAULT;
        if (FS.supported) await loadSavedFolderIfAny();
        else setStatus('就绪 (无文件夹支持)');
        setTimeout(checkAndAutoRender, 1000);
    })().catch(() => setStatus('⚠️ 初始化出错'));
})();


// ===== END 3.js =====

// ===== MERGED UI OVERRIDES (STYLE ONLY) =====
(() => {
    'use strict';
    const STYLE_ID = 'merged-unified-pokeball-ui';
    function injectStyle() {
        if (document.getElementById(STYLE_ID)) return;
        const style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = `
            :root {
                --merged-ball-right: 12px;
                --merged-ball-bottom: 14px;
                --merged-ball-size: 48px;
                --merged-ball-gap: 2px;
                --merged-ball-border: #111827;
                --merged-ui-bg1: #fff8e7;
                --merged-ui-bg2: #f1e3c2;
                --merged-ui-card: #fffdf7;
                --merged-ui-border: #262626;
                --merged-ui-text: #1c1c1c;
                --merged-ui-muted: #4b4b4b;
                --merged-ui-input-bg: #ffffff;
                --merged-ui-input-text: #1a1a1a;
                --merged-ui-btn-main: #ee1515;
                --merged-ui-btn-main-2: #b30f0f;
                --merged-ui-btn-text: #ffffff;
                --merged-ui-red: #ee1515;
                --merged-ui-red-2: #b30f0f;
                --merged-ui-focus: rgba(238, 21, 21, 0.24);
                --merged-ui-radius-sm: 10px;
                --merged-ui-radius-md: 14px;
            }

            html body #tm-nblm-toggle,
            html body #resu-toggle.resu-toggle-btn,
            html body #gpt-rescue-btn.cge-theme-greatball-fab,
            html body #gpt-rescue-btn.cge-ball-great {
                position: fixed !important;
                left: auto !important;
                top: auto !important;
                right: var(--merged-ball-right) !important;
                width: var(--merged-ball-size) !important;
                height: var(--merged-ball-size) !important;
                margin: 0 !important;
                padding: 0 !important;
                box-sizing: border-box !important;
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
                border: 2px solid var(--merged-ball-border) !important;
                border-radius: 999px !important;
                box-shadow: 0 5px 12px rgba(17, 24, 39, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.62) !important;
            }

            html body #tm-nblm-toggle {
                bottom: var(--merged-ball-bottom) !important;
                z-index: 12030 !important;
                background: linear-gradient(180deg, #ef4444 0 47%, #111827 47% 53%, #f8fafc 53% 100%) !important;
            }
            html body #tm-nblm-toggle::before {
                width: 16px !important;
                height: 16px !important;
                border-width: 2px !important;
            }
            html body #tm-nblm-toggle::after {
                width: 6px !important;
                height: 6px !important;
            }

            html body #resu-toggle.resu-toggle-btn {
                transform: none !important;
                bottom: calc(var(--merged-ball-bottom) + var(--merged-ball-size) + var(--merged-ball-gap)) !important;
                z-index: 12020 !important;
                background:
                    radial-gradient(circle at 27% 21%, #ffd84a 0 8px, transparent 9px),
                    radial-gradient(circle at 73% 21%, #ffd84a 0 8px, transparent 9px),
                    linear-gradient(180deg, #2f3442 0 47%, #111827 47% 53%, #f8fafc 53% 100%) !important;
            }
            html body #resu-toggle.resu-toggle-btn::before {
                width: 16px !important;
                height: 16px !important;
                border-width: 2px !important;
            }
            html body #resu-toggle.resu-toggle-btn::after {
                top: 6px !important;
                font-size: 10px !important;
                color: #ffe89a !important;
            }
            html body #resu-toggle.resu-toggle-btn:hover {
                transform: none !important;
            }

            html body #gpt-rescue-btn.cge-theme-greatball-fab,
            html body #gpt-rescue-btn.cge-ball-great {
                bottom: calc(var(--merged-ball-bottom) + var(--merged-ball-size) + var(--merged-ball-gap) + var(--merged-ball-size) + var(--merged-ball-gap)) !important;
                z-index: 12010 !important;
            }
            html body #gpt-rescue-btn .cge-ball-label {
                font-size: 10px !important;
                top: 75% !important;
            }

            html body #tm-nblm-toggle:hover,
            html body #gpt-rescue-btn.cge-theme-greatball-fab:hover,
            html body #gpt-rescue-btn.cge-ball-great:hover {
                transform: none !important;
            }

            /* Unified non-pokeball UI theme */
            html body #export-dialog.cge-theme-greatball-dialog,
            html body #tm-nblm-root,
            html body .resu-panel {
                border-color: var(--merged-ui-border) !important;
                color: var(--merged-ui-text) !important;
                font-family: "Trebuchet MS", "Verdana", "Noto Sans SC", sans-serif !important;
            }

            html body #export-dialog.cge-theme-greatball-dialog {
                background: linear-gradient(180deg, var(--merged-ui-bg1) 0%, var(--merged-ui-bg2) 100%) !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog h2 {
                background: linear-gradient(180deg, var(--merged-ui-red), var(--merged-ui-red-2)) !important;
                border-color: var(--merged-ui-border) !important;
                color: #fffdf8 !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog .cge-space-card {
                background: var(--merged-ui-card) !important;
                border-color: var(--merged-ui-border) !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog p,
            html body #export-dialog.cge-theme-greatball-dialog [style*="color: #666"] {
                color: var(--merged-ui-muted) !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog input,
            html body #export-dialog.cge-theme-greatball-dialog select {
                background: var(--merged-ui-input-bg) !important;
                color: var(--merged-ui-input-text) !important;
                border-color: var(--merged-ui-border) !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #select-team-btn,
            html body #export-dialog.cge-theme-greatball-dialog #start-team-export-btn,
            html body #export-dialog.cge-theme-greatball-dialog #schedule-start-btn,
            html body #export-dialog.cge-theme-greatball-dialog #schedule-pick-btn,
            html body #export-dialog.cge-theme-greatball-dialog #schedule-run-now-btn,
            html body #export-dialog.cge-theme-greatball-dialog #export-selected-btn {
                background: linear-gradient(180deg, var(--merged-ui-btn-main), var(--merged-ui-btn-main-2)) !important;
                border-color: #6f0d0d !important;
                color: var(--merged-ui-btn-text) !important;
                text-shadow: none !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-list label,
            html body #export-dialog.cge-theme-greatball-dialog #conv-list [style*="color: #666"] {
                color: var(--merged-ui-input-text) !important;
            }

            html body #tm-nblm-root {
                background: linear-gradient(180deg, var(--merged-ui-bg1) 0%, var(--merged-ui-bg2) 100%) !important;
                box-shadow: 0 14px 30px rgba(30, 24, 14, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.8) !important;
            }
            html body #tm-nblm-head {
                background: linear-gradient(180deg, var(--merged-ui-red), var(--merged-ui-red-2)) !important;
                border-bottom-color: var(--merged-ui-border) !important;
            }
            html body #tm-nblm-title {
                color: #fffdf8 !important;
                text-shadow: none !important;
            }
            html body #tm-nblm-body {
                background-image:
                    radial-gradient(rgba(32, 32, 32, 0.05) 1px, transparent 1px),
                    linear-gradient(180deg, #fff8e7 0%, #f3e6c8 100%) !important;
                background-size: 8px 8px, auto !important;
            }
            html body #tm-nblm-source-wrap,
            html body #tm-nblm-config-wrap,
            html body .tm-nblm-source-item {
                background: var(--merged-ui-card) !important;
                border-color: var(--merged-ui-border) !important;
            }
            html body #tm-nblm-source-count,
            html body .tm-nblm-source-title,
            html body .tm-nblm-source-id,
            html body .tm-nblm-config-item label,
            html body #tm-nblm-options label,
            html body #tm-nblm-status {
                color: var(--merged-ui-muted) !important;
            }
            html body #tm-nblm-root select,
            html body #tm-nblm-root textarea,
            html body #tm-nblm-root input[type="text"],
            html body #tm-nblm-root button:not(#tm-nblm-toggle):not(#tm-nblm-ask-send-float) {
                border-color: var(--merged-ui-border) !important;
            }
            html body #tm-nblm-notebook,
            html body #tm-nblm-reload,
            html body #tm-nblm-body textarea,
            html body #tm-nblm-body input[type="text"] {
                background: var(--merged-ui-input-bg) !important;
                color: var(--merged-ui-input-text) !important;
            }
            html body #tm-nblm-ask {
                background: linear-gradient(180deg, var(--merged-ui-btn-main), var(--merged-ui-btn-main-2)) !important;
                color: var(--merged-ui-btn-text) !important;
                border-color: #6f0d0d !important;
                text-shadow: none !important;
            }

            html body .resu-panel {
                background: linear-gradient(180deg, var(--merged-ui-red) 0 56px, #202020 56px 63px, var(--merged-ui-bg1) 63px 100%) !important;
                box-shadow: 0 16px 32px rgba(30, 24, 14, 0.32), inset 0 0 0 2px rgba(255, 255, 255, 0.38) !important;
            }
            html body .resu-header {
                background: linear-gradient(180deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0) 100%) !important;
                border-bottom-color: rgba(255, 255, 255, 0.38) !important;
            }
            html body .resu-title {
                color: #fffdf8 !important;
            }
            html body .resu-card {
                background: var(--merged-ui-card) !important;
                border-color: var(--merged-ui-border) !important;
            }
            html body .resu-select,
            html body .resu-preview,
            html body #resu-preview {
                background: var(--merged-ui-input-bg) !important;
                color: var(--merged-ui-input-text) !important;
                border-color: var(--merged-ui-border) !important;
            }
            html body .resu-btn-primary,
            html body .resu-btn-secondary,
            html body .resu-btn-export,
            html body .resu-btn-danger {
                background: linear-gradient(180deg, var(--merged-ui-btn-main), var(--merged-ui-btn-main-2)) !important;
                border-color: #6f0d0d !important;
                color: var(--merged-ui-btn-text) !important;
                text-shadow: none !important;
            }
            html body #resu-status,
            html body .resu-debug > summary {
                color: var(--merged-ui-input-text) !important;
                background: var(--merged-ui-input-bg) !important;
                border-color: var(--merged-ui-border) !important;
            }

            /* UI polish: spacing, feedback, readability */
            html body #export-dialog.cge-theme-greatball-dialog {
                border-radius: var(--merged-ui-radius-md) !important;
                box-shadow: 0 22px 44px rgba(20, 16, 10, 0.28) !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog .cge-space-card {
                border-radius: var(--merged-ui-radius-sm) !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog button {
                min-height: 36px !important;
                border-radius: 9px !important;
                font-weight: 800 !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-list {
                scrollbar-width: thin;
                scrollbar-color: #8c8c8c #f6f0df;
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-list label {
                border-radius: 9px !important;
                transition: background-color 0.14s ease, border-color 0.14s ease !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-list label:hover {
                background: #fff4d8 !important;
                border-color: #b79a63 !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog input[type="checkbox"] {
                accent-color: var(--merged-ui-red);
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-status,
            html body #export-dialog.cge-theme-greatball-dialog #schedule-status,
            html body #export-dialog.cge-theme-greatball-dialog #schedule-selection-summary,
            html body #export-dialog.cge-theme-greatball-dialog #filename-preview,
            html body #export-dialog.cge-theme-greatball-dialog #download-fixed-file-status {
                color: var(--merged-ui-muted) !important;
            }

            html body #tm-nblm-root {
                border-radius: 18px !important;
            }
            html body #tm-nblm-body {
                scrollbar-width: thin;
                scrollbar-color: #8c8c8c #f6f0df;
            }
            html body #tm-nblm-root button:not(#tm-nblm-toggle):not(#tm-nblm-ask-send-float) {
                min-height: 34px !important;
                border-radius: 9px !important;
                font-weight: 800 !important;
            }
            html body #tm-nblm-root input[type="checkbox"] {
                accent-color: var(--merged-ui-red);
            }
            html body #tm-nblm-source-list {
                scrollbar-width: thin;
                scrollbar-color: #8c8c8c #f6f0df;
            }

            html body .resu-panel {
                border-radius: 12px !important;
            }
            html body .resu-panel .resu-btn {
                min-height: 40px !important;
                border-width: 2px !important;
            }
            html body .resu-panel .resu-panel-body {
                scrollbar-width: thin;
                scrollbar-color: #8c8c8c #f6f0df;
            }

            /* Alignment audit: component size + text baseline + row alignment */
            html body #export-dialog.cge-theme-greatball-dialog {
                text-align: left !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog h2 {
                line-height: 1.2 !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog button,
            html body #export-dialog.cge-theme-greatball-dialog select,
            html body #export-dialog.cge-theme-greatball-dialog input:not([type="checkbox"]):not([type="radio"]) {
                box-sizing: border-box !important;
                min-height: 36px !important;
                line-height: 1.2 !important;
                padding-top: 7px !important;
                padding-bottom: 7px !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog button {
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
                text-align: center !important;
                vertical-align: middle !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog .cge-picker-row,
            html body #export-dialog.cge-theme-greatball-dialog .cge-picker-footer,
            html body #export-dialog.cge-theme-greatball-dialog .cge-space-actions,
            html body #export-dialog.cge-theme-greatball-dialog .cge-team-footer {
                align-items: center !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog .cge-picker-row span {
                line-height: 1.2 !important;
                align-self: center !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog .cge-picker-row label {
                display: inline-flex !important;
                align-items: center !important;
                gap: 6px !important;
                line-height: 1.2 !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #workspace-id-list label {
                display: flex !important;
                align-items: center !important;
                min-height: 38px !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #workspace-id-list input[type="radio"] {
                margin: 0 !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-list label {
                align-items: flex-start !important;
                gap: 10px !important;
                padding: 9px 10px !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-list label input[type="checkbox"] {
                width: 16px !important;
                height: 16px !important;
                margin-top: 2px !important;
                flex: 0 0 16px !important;
            }
            html body #export-dialog.cge-theme-greatball-dialog #conv-list label > div {
                min-width: 0 !important;
            }

            html body #tm-nblm-root {
                text-align: left !important;
            }
            html body #tm-nblm-row,
            html body #tm-nblm-source-top,
            html body #tm-nblm-source-actions,
            html body #tm-nblm-options {
                align-items: center !important;
            }
            html body #tm-nblm-root button:not(#tm-nblm-toggle):not(#tm-nblm-ask-send-float) {
                box-sizing: border-box !important;
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
                line-height: 1.2 !important;
                text-align: center !important;
            }
            html body #tm-nblm-notebook,
            html body #tm-nblm-reload,
            html body #tm-nblm-root textarea,
            html body #tm-nblm-root input[type="text"] {
                box-sizing: border-box !important;
                line-height: 1.25 !important;
            }
            html body #tm-nblm-notebook,
            html body #tm-nblm-reload {
                min-height: 34px !important;
            }
            html body #tm-nblm-source-actions button {
                min-height: 30px !important;
                padding: 4px 10px !important;
            }
            html body .tm-nblm-source-item {
                align-items: flex-start !important;
                padding: 7px 8px !important;
            }
            html body .tm-nblm-source-item input[type="checkbox"] {
                width: 16px !important;
                height: 16px !important;
                margin-top: 2px !important;
                flex: 0 0 16px !important;
            }
            html body #tm-nblm-options label {
                line-height: 1.2 !important;
            }

            html body .resu-panel {
                text-align: left !important;
            }
            html body .resu-panel .resu-header {
                min-height: 42px !important;
            }
            html body .resu-panel .resu-btn {
                box-sizing: border-box !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                text-align: center !important;
                line-height: 1.2 !important;
            }
            html body .resu-panel .resu-title,
            html body .resu-panel .resu-close {
                line-height: 1 !important;
            }
            html body .resu-panel .resu-select,
            html body .resu-panel #resu-preview {
                box-sizing: border-box !important;
                line-height: 1.3 !important;
            }
            html body .resu-panel #resu-status {
                min-height: 24px !important;
                height: auto !important;
                display: flex !important;
                align-items: center !important;
                line-height: 1.2 !important;
                padding-top: 2px !important;
                padding-bottom: 2px !important;
            }
            html body .resu-panel details > summary {
                display: inline-flex !important;
                align-items: center !important;
                min-height: 30px !important;
                line-height: 1.2 !important;
            }

            html body #export-dialog.cge-theme-greatball-dialog button:focus-visible,
            html body #export-dialog.cge-theme-greatball-dialog input:focus-visible,
            html body #export-dialog.cge-theme-greatball-dialog select:focus-visible,
            html body #tm-nblm-root button:focus-visible,
            html body #tm-nblm-root input:focus-visible,
            html body #tm-nblm-root textarea:focus-visible,
            html body #tm-nblm-root select:focus-visible,
            html body .resu-panel button:focus-visible,
            html body .resu-panel select:focus-visible,
            html body .resu-panel textarea:focus-visible {
                outline: none !important;
                box-shadow: 0 0 0 3px var(--merged-ui-focus) !important;
            }

            @media (prefers-reduced-motion: reduce) {
                html body #export-dialog.cge-theme-greatball-dialog *,
                html body #tm-nblm-root *,
                html body .resu-panel *,
                html body #tm-nblm-toggle,
                html body #resu-toggle.resu-toggle-btn,
                html body #gpt-rescue-btn.cge-theme-greatball-fab,
                html body #gpt-rescue-btn.cge-ball-great {
                    animation: none !important;
                    transition: none !important;
                }
            }

            @media (max-width: 900px) {
                :root {
                    --merged-ball-right: 8px;
                    --merged-ball-bottom: 10px;
                    --merged-ball-size: 44px;
                    --merged-ball-gap: 2px;
                }
                html body #tm-nblm-toggle::before,
                html body #resu-toggle.resu-toggle-btn::before {
                    width: 14px !important;
                    height: 14px !important;
                }
                html body #gpt-rescue-btn .cge-ball-label {
                    font-size: 9px !important;
                }
            }
        `;
        document.head.appendChild(style);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', injectStyle, { once: true });
        return;
    }
    injectStyle();
})();