Google AI Studio 聊天记录导出器(修改版)

自动滚动 Google AI Studio 聊天界面,捕获用户消息、AI 思维链和 AI 回答,导出为 TXT 文件;或直接从 Python SDK 代码块中提取对话并导出。已修复网站更新所导致的问题。按钮已移至左下角并可隐藏。

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         Google AI Studio 聊天记录导出器(修改版)
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  自动滚动 Google AI Studio 聊天界面,捕获用户消息、AI 思维链和 AI 回答,导出为 TXT 文件;或直接从 Python SDK 代码块中提取对话并导出。已修复网站更新所导致的问题。按钮已移至左下角并可隐藏。
// @author       pipdax & Gemini
// @match        https://aistudio.google.com/*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwNzhmZiI+PHBhdGggZD0iTTE5LjUgMi4yNWgtMTVjLTEuMjQgMC0yLjI1IDEuMDEtMi4yNSAyLjI1djE1YzAgMS4yNCAxLjAxIDIuMjUgMi4yNSAyLjI1aDE1YzEuMjQgMCAyLjI1LTEuMDEgMi4yNS0yLjI1di0xNWMwLTEuMjQtMS4wMS0yLjI1LTIuMjUtMi4yNXptLTIuMjUgNmgtMTAuNWMtLjQxIDAtLjc1LS4zNC0uNzUtLjc1cy4zNC0uNzUuNzUtLjc1aDEwLjVjLjQxIDAgLjc1LjM0Ljc1Ljc1cy0uMzQuNzUtLjc1Ljc1em0wIDRoLTEwLjVjLS40MSAwLS43NS0uMzQtLjc1LS43NXMuMzQtLjc1Ljc1LS43NWgxMC41Yy40MSAwIC43NS4zNC43NS43NXMtLjM0Ljc1LS4yNS43NXptLTMgNGgtNy41Yy0uNDEgMC0uNzUtLjM0LS43NS0uNzVzLjM0LS43NS43NS0uNzVoNy41Yy40MSAwIC43NS4zNC43NS43NXMtLjM0Ljc1LS43NS43NXoiLz48L3N2Zz4=
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 全局配置常量 ---
    const buttonTextStartScroll = "滚动导出TXT";
    const buttonTextStopScroll = "停止滚动";
    const buttonTextProcessingScroll = "处理滚动数据...";
    const successTextScroll = "滚动导出 TXT 成功!";
    const errorTextScroll = "滚动导出失败";

    const buttonTextStartCode = "从SDK导出对话TXT";
    const buttonTextProcessingCode = "处理SDK代码...";
    const successTextCode = "SDK代码导出成功!";
    const errorTextCode = "SDK代码导出失败";

    const exportTimeout = 3000;
    // 【修改】移除了旧的 EXPORT_FILENAME_PREFIX 常量

    const SCROLL_DELAY_MS = 1000;
    const MAX_SCROLL_ATTEMPTS = 300;
    const SCROLL_INCREMENT_FACTOR = 0.85;
    const SCROLL_STABILITY_CHECKS = 3;

    const CODE_BLOCK_SELECTOR = 'ms-get-code-dialog .code-display-body code';

    // --- 脚本内部状态变量 ---
    let isScrolling = false;
    let collectedData = new Map();
    let scrollCount = 0;
    let noChangeCounter = 0;

    // --- UI 界面元素变量 ---
    let captureButtonScroll = null;
    let stopButtonScroll = null;
    let captureButtonCode = null;
    let statusDiv = null;
    let hideButton = null;
    let buttonContainer = null;

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

    function getCurrentTimestamp() {
        const n = new Date();
        const YYYY = n.getFullYear();
        const MM = (n.getMonth() + 1).toString().padStart(2, '0');
        const DD = n.getDate().toString().padStart(2, '0');
        const hh = n.getHours().toString().padStart(2, '0');
        const mm = n.getMinutes().toString().padStart(2, '0');
        const ss = n.getSeconds().toString().padStart(2, '0');
        return `${YYYY}${MM}${DD}_${hh}${mm}${ss}`;
    }

    /**
     * 【修改】新增函数,用于从页面获取项目名称
     * @returns {string} - 清理后的项目名称,或一个默认名称
     */
    function getProjectName() {
        const xpath = "/html/body/app-root/ms-app/div/div/div/div/span/ms-prompt-switcher/ms-chunk-editor/section/ms-toolbar/div/div[1]/div/div/h1";
        const defaultName = "AI_Studio_Chat";
        try {
            const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
            const titleElement = result.singleNodeValue;
            if (titleElement && titleElement.textContent) {
                // 清理文件名中可能存在的无效字符
                const cleanName = titleElement.textContent.trim().replace(/[\\/:\*\?"<>\|]/g, '_');
                console.log("成功获取项目名称: ", cleanName);
                return cleanName || defaultName;
            } else {
                console.warn(`警告: 未能通过XPath找到项目名称元素。将使用默认名称: "${defaultName}"`);
                return defaultName;
            }
        } catch (e) {
            console.error("通过XPath获取项目名称时出错: ", e);
            return defaultName;
        }
    }


    function getMainScrollerElement_AiStudio() {
        console.log("尝试查找滚动容器 (用于滚动导出)...");
        let scroller = document.querySelector('.chat-scrollable-container');
        if (scroller && scroller.scrollHeight > scroller.clientHeight) {
            console.log("找到滚动容器 (策略 1: .chat-scrollable-container):", scroller);
            return scroller;
        }
        scroller = document.querySelector('mat-sidenav-content');
        if (scroller && scroller.scrollHeight > scroller.clientHeight) {
            console.log("找到滚动容器 (策略 2: mat-sidenav-content):", scroller);
            return scroller;
        }
        const chatTurnsContainer = document.querySelector('ms-chat-turn')?.parentElement;
        if (chatTurnsContainer) {
            let parent = chatTurnsContainer;
            for (let i = 0; i < 5 && parent; i++) {
                if (parent.scrollHeight > parent.clientHeight + 10 &&
                    (window.getComputedStyle(parent).overflowY === 'auto' || window.getComputedStyle(parent).overflowY === 'scroll')) {
                    console.log("找到滚动容器 (策略 3: 向上查找父元素):", parent);
                    return parent;
                }
                parent = parent.parentElement;
            }
        }
        console.warn("警告 (滚动导出): 未能通过特定选择器精确找到 AI Studio 滚动区域,将尝试使用 document.documentElement。如果滚动不工作,请按F12检查聊天区域的HTML结构,并更新此函数内的选择器。");
        return document.documentElement;
    }

    function findCodeBlockElement() {
        console.log(`尝试查找SDK代码块元素 (选择器: ${CODE_BLOCK_SELECTOR})...`);
        const codeElement = document.querySelector(CODE_BLOCK_SELECTOR);
        if (codeElement) {
            console.log("找到SDK代码块元素:", codeElement);
        } else {
            console.warn(`警告 (SDK代码导出): 未能找到指定的代码块元素 (${CODE_BLOCK_SELECTOR})。请检查页面结构或更新脚本中的选择器。`);
        }
        return codeElement;
    }

    /**
     * 【新增】让指定的按钮边框闪烁红色以提示用户
     */
    function flashGetCodeButton() {
        const getCodeBtn = document.querySelector("#getCodeBtn");
        if (!getCodeBtn) {
            console.warn("无法找到 #getCodeBtn 元素进行闪烁提示。");
            return;
        }
        const originalBorder = getCodeBtn.style.border;
        let flashes = 0;
        const maxFlashes = 6; // 3次亮灭
        getCodeBtn.style.transition = 'border 0.2s ease-in-out';

        const intervalId = setInterval(() => {
            if (flashes >= maxFlashes) {
                clearInterval(intervalId);
                getCodeBtn.style.border = originalBorder; // 恢复原始边框
                return;
            }
            getCodeBtn.style.border = (flashes % 2 === 0) ? '2px solid red' : originalBorder;
            flashes++;
        }, 250);
    }


    // --- UI 界面创建与更新 ---
    function createUI() {
        console.log("开始创建 UI 元素...");

        buttonContainer = document.createElement('div');
        buttonContainer.id = 'exporter-button-container';
        buttonContainer.style.cssText = `position: fixed; bottom: 30%; left: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 10px;`;
        document.body.appendChild(buttonContainer);

        captureButtonScroll = document.createElement('button');
        captureButtonScroll.textContent = buttonTextStartScroll;
        captureButtonScroll.id = 'capture-chat-scroll-button';
        captureButtonScroll.style.cssText = `padding: 10px 15px; background-color: #1a73e8; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; box-shadow: 2px 2px 5px rgba(0,0,0,0.2); transition: all 0.3s ease;`;
        captureButtonScroll.addEventListener('click', handleScrollExtraction);
        buttonContainer.appendChild(captureButtonScroll);

        stopButtonScroll = document.createElement('button');
        stopButtonScroll.textContent = buttonTextStopScroll;
        stopButtonScroll.id = 'stop-scrolling-button';
        stopButtonScroll.style.cssText = `padding: 10px 15px; background-color: #d93025; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; box-shadow: 2px 2px 5px rgba(0,0,0,0.2); display: none; transition: background-color 0.3s ease;`;
        stopButtonScroll.addEventListener('click', () => {
            if (isScrolling) {
                updateStatus('手动停止滚动信号已发送...');
                isScrolling = false;
                stopButtonScroll.disabled = true;
                stopButtonScroll.textContent = '正在停止...';
            }
        });
        buttonContainer.appendChild(stopButtonScroll);

        captureButtonCode = document.createElement('button');
        captureButtonCode.textContent = buttonTextStartCode;
        captureButtonCode.id = 'capture-chat-code-button';
        captureButtonCode.style.cssText = `padding: 10px 15px; background-color: #34a853; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; box-shadow: 2px 2px 5px rgba(0,0,0,0.2); transition: all 0.3s ease;`;
        captureButtonCode.addEventListener('click', handleCodeBlockExport);
        buttonContainer.appendChild(captureButtonCode);

        hideButton = document.createElement('button');
        hideButton.textContent = '👁️';
        hideButton.id = 'hide-exporter-buttons';
        hideButton.style.cssText = `position: fixed; bottom: calc(30% + 135px); left: 20px; z-index: 10000; padding: 5px 8px; background-color: rgba(0, 0, 0, 0.3); color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 12px;`;
        hideButton.addEventListener('click', () => {
            const isHidden = buttonContainer.style.display === 'none';
            buttonContainer.style.display = isHidden ? 'flex' : 'none';
            hideButton.textContent = isHidden ? '👁️' : '🙈';
        });
        document.body.appendChild(hideButton);


        statusDiv = document.createElement('div');
        statusDiv.id = 'extract-status-div';
        statusDiv.style.cssText = `position: fixed; bottom: 30%; left: 200px; z-index: 9998; padding: 5px 10px; background-color: rgba(0,0,0,0.7); color: white; font-size: 12px; border-radius: 3px; display: none;`;
        document.body.appendChild(statusDiv);

        GM_addStyle(`
                  #capture-chat-scroll-button:disabled, #stop-scrolling-button:disabled, #capture-chat-code-button:disabled {
                      opacity: 0.6; cursor: not-allowed; background-color: #aaa !important;
                  }
                   #capture-chat-scroll-button.success { background-color: #1e8e3e !important; }
                   #capture-chat-scroll-button.error { background-color: #d93025 !important; }
                   #capture-chat-code-button.success { background-color: #188038 !important; }
                   #capture-chat-code-button.error { background-color: #d93025 !important; }
        `);
        console.log("UI 元素创建完成。");
    }

    function updateStatus(message) {
        if (statusDiv) {
            statusDiv.textContent = message;
            statusDiv.style.display = message ? 'block' : 'none';
        }
        console.log(`[Status] ${message}`);
    }


    // --- 核心业务逻辑 (滚动导出) ---
    function extractDataIncremental_AiStudio() {
        let newlyFoundCount = 0;
        let dataUpdatedInExistingTurn = false;
        const currentTurns = document.querySelectorAll('ms-chat-turn');

        currentTurns.forEach((turn, index) => {
            const turnKey = turn;
            const turnContainer = turn.querySelector('.chat-turn-container.user, .chat-turn-container.model');
            if (!turnContainer) {
                return;
            }

            let isNewTurn = !collectedData.has(turnKey);
            let extractedInfo = collectedData.get(turnKey) || {
                domOrder: index, type: 'unknown', userText: null, thoughtText: null, responseText: null
            };
            if (isNewTurn) {
                collectedData.set(turnKey, extractedInfo);
                newlyFoundCount++;
            }

            let dataWasUpdatedThisTime = false;

            if (turnContainer.classList.contains('user')) {
                if (extractedInfo.type === 'unknown') extractedInfo.type = 'user';
                if (!extractedInfo.userText) {
                    let userNode = turn.querySelector('.turn-content ms-cmark-node');
                    let userText = userNode ? userNode.innerText.trim() : null;
                    if (userText) {
                        extractedInfo.userText = userText;
                        dataWasUpdatedThisTime = true;
                    }
                }
            } else if (turnContainer.classList.contains('model')) {
                if (extractedInfo.type === 'unknown') extractedInfo.type = 'model';

                if (!extractedInfo.thoughtText) {
                    let thoughtNode = turn.querySelector('.thought-container .mat-expansion-panel-body');
                    if (thoughtNode) {
                        let thoughtText = thoughtNode.textContent.trim();
                        if (thoughtText && thoughtText.toLowerCase() !== 'thinking process:') {
                            extractedInfo.thoughtText = thoughtText;
                            dataWasUpdatedThisTime = true;
                        }
                    }
                }

                if (!extractedInfo.responseText) {
                    const responseChunks = Array.from(turn.querySelectorAll('.turn-content > ms-prompt-chunk'));
                    const responseTexts = responseChunks
                    .filter(chunk => !chunk.querySelector('.thought-container'))
                    .map(chunk => {
                        const cmarkNode = chunk.querySelector('ms-cmark-node');
                        return cmarkNode ? cmarkNode.innerText.trim() : chunk.innerText.trim();
                    })
                    .filter(text => text);

                    if (responseTexts.length > 0) {
                        extractedInfo.responseText = responseTexts.join('\n\n');
                        dataWasUpdatedThisTime = true;
                    } else if (!extractedInfo.thoughtText) {
                        const turnContent = turn.querySelector('.turn-content');
                        if(turnContent) {
                            extractedInfo.responseText = turnContent.innerText.trim();
                            dataWasUpdatedThisTime = true;
                        }
                    }
                }

                if (dataWasUpdatedThisTime) {
                    if (extractedInfo.thoughtText && extractedInfo.responseText) extractedInfo.type = 'model_thought_reply';
                    else if (extractedInfo.responseText) extractedInfo.type = 'model_reply';
                    else if (extractedInfo.thoughtText) extractedInfo.type = 'model_thought';
                }
            }

            if (dataWasUpdatedThisTime) {
                collectedData.set(turnKey, extractedInfo);
                dataUpdatedInExistingTurn = true;
            }
        });

        if (currentTurns.length > 0 && collectedData.size === 0) {
            console.warn("警告(滚动导出): 页面上存在聊天回合 (ms-chat-turn),但未能提取任何数据。CSS选择器可能已完全失效,请按F12检查并更新 extractDataIncremental_AiStudio 函数中的选择器。");
            updateStatus(`警告: 无法从聊天记录中提取数据,请检查脚本!`);
        } else {
            updateStatus(`滚动 ${scrollCount}/${MAX_SCROLL_ATTEMPTS}... 已收集 ${collectedData.size} 条记录...`);
        }

        return newlyFoundCount > 0 || dataUpdatedInExistingTurn;
    }

    async function autoScrollDown_AiStudio() {
        console.log("启动自动滚动 (滚动导出)...");
        isScrolling = true; collectedData.clear(); scrollCount = 0; noChangeCounter = 0;
        const scroller = getMainScrollerElement_AiStudio();
        if (!scroller) {
            updateStatus('错误 (滚动): 找不到滚动区域!');
            alert('未能找到聊天记录的滚动区域,无法自动滚动。请检查脚本中的选择器。');
            isScrolling = false; return false;
        }
        console.log('使用的滚动元素 (滚动导出):', scroller);
        const isWindowScroller = (scroller === document.documentElement || scroller === document.body);
        const getScrollTop = () => isWindowScroller ? window.scrollY : scroller.scrollTop;
        const getScrollHeight = () => isWindowScroller ? document.documentElement.scrollHeight : scroller.scrollHeight;
        const getClientHeight = () => isWindowScroller ? window.innerHeight : scroller.clientHeight;
        updateStatus(`开始增量滚动 (最多 ${MAX_SCROLL_ATTEMPTS} 次)...`);
        let lastScrollHeight = -1;

        while (scrollCount < MAX_SCROLL_ATTEMPTS && isScrolling) {
            const currentScrollTop = getScrollTop(); const currentScrollHeight = getScrollHeight(); const currentClientHeight = getClientHeight();
            if (currentScrollHeight === lastScrollHeight) { noChangeCounter++; } else { noChangeCounter = 0; }
            lastScrollHeight = currentScrollHeight;
            if (noChangeCounter >= SCROLL_STABILITY_CHECKS && currentScrollTop + currentClientHeight >= currentScrollHeight - 20) {
                console.log("滚动条疑似触底 (滚动导出),停止滚动。");
                updateStatus(`滚动完成 (疑似触底)。`);
                break;
            }
            if (currentScrollTop === 0 && scrollCount > 10) {
                console.log("滚动条返回顶部 (滚动导出),停止滚动。");
                updateStatus(`滚动完成 (返回顶部)。`);
                break;
            }
            const targetScrollTop = currentScrollTop + (currentClientHeight * SCROLL_INCREMENT_FACTOR);
            if (isWindowScroller) { window.scrollTo({ top: targetScrollTop, behavior: 'smooth' }); } else { scroller.scrollTo({ top: targetScrollTop, behavior: 'smooth' }); }
            scrollCount++;
            updateStatus(`滚动 ${scrollCount}/${MAX_SCROLL_ATTEMPTS}... 等待 ${SCROLL_DELAY_MS}ms... (已收集 ${collectedData.size} 条)`);
            await delay(SCROLL_DELAY_MS);
            extractDataIncremental_AiStudio();
            if (!isScrolling) { console.log("检测到手动停止信号 (滚动导出),退出滚动循环。"); break; }
        }

        if (!isScrolling && scrollCount < MAX_SCROLL_ATTEMPTS) {
            updateStatus(`滚动已手动停止 (共 ${scrollCount} 次尝试)。`);
        } else if (scrollCount >= MAX_SCROLL_ATTEMPTS) {
            updateStatus(`滚动停止: 已达到最大尝试次数 (${MAX_SCROLL_ATTEMPTS})。`);
        }
        isScrolling = false;
        return true;
    }

    function formatAndTriggerDownloadScroll() {
        updateStatus(`处理 ${collectedData.size} 条滚动记录并生成文件...`);
        const finalTurnsInDom = document.querySelectorAll('ms-chat-turn');
        let sortedData = [];
        finalTurnsInDom.forEach(turnNode => {
            if (collectedData.has(turnNode)) {
                sortedData.push(collectedData.get(turnNode));
            }
        });

        if (sortedData.length === 0) {
            updateStatus('没有收集到任何有效滚动记录。');
            alert('滚动结束后未能收集到任何聊天记录,无法导出。请检查脚本中的CSS选择器是否与当前网站匹配。');
            captureButtonScroll.textContent = buttonTextStartScroll; captureButtonScroll.disabled = false;
            captureButtonScroll.classList.remove('success', 'error'); updateStatus('');
            return;
        }

        let fileContent = "Google AI Studio 聊天记录 (自动滚动捕获)\n=========================================\n\n";
        sortedData.forEach(item => {
            let turnContent = "";
            if (item.type === 'user' && item.userText) {
                turnContent += `--- 用户 ---\n${item.userText}\n\n`;
            } else if (item.type === 'model_thought_reply') {
                if(item.thoughtText) turnContent += `--- AI 思维链 ---\n${item.thoughtText}\n\n`;
                if(item.responseText) turnContent += `--- AI 回答 ---\n${item.responseText}\n\n`;
            } else if (item.type === 'model_thought' && item.thoughtText) {
                turnContent += `--- AI 思维链 ---\n${item.thoughtText}\n\n`;
            } else if (item.type === 'model_reply' && item.responseText) {
                turnContent += `--- AI 回答 ---\n${item.responseText}\n\n`;
            } else {
                turnContent += `--- 回合 (内容提取不完整或失败) ---\n`;
                if(item.thoughtText) turnContent += `思维链(可能不全): ${item.thoughtText}\n`;
                if(item.responseText) turnContent += `回答(可能不全): ${item.responseText}\n`;
                turnContent += '\n';
            }
            if (turnContent) {
                fileContent += turnContent.trim() + "\n\n------------------------------\n\n";
            }
        });
        fileContent = fileContent.replace(/\n\n------------------------------\n\n$/, '\n').trim();

        try {
            const blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' });
            const link = document.createElement('a');
            const url = URL.createObjectURL(blob);
            link.href = url;
            // 【修改】使用新的函数来生成文件名
            const projectName = getProjectName();
            link.download = `${projectName}_scroll_${getCurrentTimestamp()}.txt`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            captureButtonScroll.textContent = successTextScroll;
            captureButtonScroll.classList.add('success');
        } catch (e) {
            console.error("滚动导出文件失败:", e);
            captureButtonScroll.textContent = `${errorTextScroll}: 创建失败`;
            captureButtonScroll.classList.add('error');
            alert("创建滚动下载文件时出错: " + e.message);
        }

        setTimeout(() => {
            captureButtonScroll.textContent = buttonTextStartScroll;
            captureButtonScroll.disabled = false;
            captureButtonScroll.classList.remove('success', 'error');
            updateStatus('');
        }, exportTimeout);
    }

    async function handleScrollExtraction() {
        if (isScrolling) return;
        captureButtonScroll.disabled = true;
        captureButtonScroll.textContent = '滚动中...';
        stopButtonScroll.style.display = 'block';
        stopButtonScroll.disabled = false;
        stopButtonScroll.textContent = buttonTextStopScroll;

        // 【修改】在开始前先滚动到页面顶部
        const scroller = getMainScrollerElement_AiStudio();
        if (scroller) {
            updateStatus('正在滚动到顶部...');
            const isWindowScroller = (scroller === document.documentElement || scroller === document.body);
            if (isWindowScroller) {
                window.scrollTo({ top: 0, behavior: 'smooth' });
            } else {
                scroller.scrollTo({ top: 0, behavior: 'smooth' });
            }
            await delay(1500); // 等待滚动动画完成
        }

        updateStatus('初始化滚动 (滚动导出)...');

        try {
            const scrollSuccess = await autoScrollDown_AiStudio();
            if (scrollSuccess !== false) {
                captureButtonScroll.textContent = buttonTextProcessingScroll;
                updateStatus('滚动结束,准备最终处理...');
                await delay(500);
                extractDataIncremental_AiStudio();
                await delay(200);
                formatAndTriggerDownloadScroll();
            } else {
                captureButtonScroll.textContent = `${errorTextScroll}: 滚动失败`;
                captureButtonScroll.classList.add('error');
                setTimeout(() => {
                    captureButtonScroll.textContent = buttonTextStartScroll;
                    captureButtonScroll.disabled = false;
                    captureButtonScroll.classList.remove('error');
                    updateStatus('');
                }, exportTimeout);
            }
        } catch (error) {
            console.error('滚动处理过程中发生错误:', error);
            updateStatus(`错误 (滚动导出): ${error.message}`);
            alert(`滚动处理过程中发生错误: ${error.message}`);
            captureButtonScroll.textContent = `${errorTextScroll}: 处理出错`;
            captureButtonScroll.classList.add('error');
            setTimeout(() => {
                captureButtonScroll.textContent = buttonTextStartScroll;
                captureButtonScroll.disabled = false;
                captureButtonScroll.classList.remove('error');
                updateStatus('');
            }, exportTimeout);
            isScrolling = false;
        } finally {
            stopButtonScroll.style.display = 'none';
            isScrolling = false;
        }
    }


    // --- 核心业务逻辑 (SDK代码导出) ---
    function extractConversationFromCodeBlock(codeText) {
        console.log("--- 开始从SDK代码中提取对话 (行状态机) ---");
        const conversation = [];
        const lines = codeText.split('\n');
        let state = '寻找 contents';
        let current_content_block = null;
        let current_part_lines = [];
        const roleExtractRegex = /role\s*=\s*["'](user|model)["']/;

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            const trimmedLine = line.trim();

            switch (state) {
                case '寻找 contents':
                    if (trimmedLine.includes('contents = [')) {
                        state = '寻找 Content';
                    }
                    break;
                case '寻找 Content':
                    if (trimmedLine.startsWith('types.Content(')) {
                        if (current_content_block) {
                            processCollectedContent(current_content_block, conversation);
                        }
                        current_content_block = null;
                        state = '寻找 Role';
                    } else if (trimmedLine === ']') {
                        if (current_content_block) {
                            processCollectedContent(current_content_block, conversation);
                        }
                        state = '结束';
                    }
                    break;
                case '寻找 Role':
                    let role = 'unknown';
                    const roleMatch = roleExtractRegex.exec(line);
                    if (roleMatch && roleMatch[1]) {
                        role = roleMatch[1];
                    }
                    current_content_block = { role: role, parts: [] };
                    state = '寻找 Part 开始';
                    // Fallthrough to check the same line for a Part
                case '寻找 Part 开始':
                    if (!current_content_block) break;
                    const partStartIndex = line.indexOf('types.Part.from_text');
                    if (partStartIndex !== -1) {
                        state = '读取 Part 文本';
                        current_part_lines = [];
                        const tripleQuoteIndex = line.indexOf('"""');
                        if (tripleQuoteIndex > -1) {
                            const textAfterTripleQuote = line.substring(tripleQuoteIndex + 3);
                            const endTripleQuoteIndexSameLine = textAfterTripleQuote.indexOf('"""');
                            if (endTripleQuoteIndexSameLine !== -1) {
                                const singleLineText = textAfterTripleQuote.substring(0, endTripleQuoteIndexSameLine);
                                current_content_block.parts.push(singleLineText.trim());
                                state = '寻找 Part 开始';
                            } else {
                                current_part_lines.push(textAfterTripleQuote);
                            }
                        }
                    } else if (trimmedLine.startsWith('],') || trimmedLine === '],') {
                        state = '寻找 Content';
                    } else if (trimmedLine === ']') {
                        if (current_content_block) {
                            processCollectedContent(current_content_block, conversation);
                        }
                        state = '结束';
                    }
                    break;
                case '读取 Part 文本':
                    const endTripleQuoteIndex = line.indexOf('"""');
                    if (endTripleQuoteIndex !== -1) {
                        const textBeforeEnd = line.substring(0, endTripleQuoteIndex);
                        current_part_lines.push(textBeforeEnd);
                        const fullPartText = current_part_lines.join('\n').trim();
                        if (current_content_block) {
                            current_content_block.parts.push(fullPartText);
                        }
                        current_part_lines = [];
                        state = '寻找 Part 开始';
                    } else {
                        current_part_lines.push(line);
                    }
                    break;
            }
            if (state === '结束') break;
        }

        if (state !== '结束' && current_content_block) {
            processCollectedContent(current_content_block, conversation);
        }
        if (conversation.length === 0) {
            console.warn("SDK代码提取完成: 未能提取到任何有效的对话部分。");
        }
        return conversation;
    }

    function processCollectedContent(contentBlock, conversation) {
        if (!contentBlock || !contentBlock.parts || contentBlock.parts.length === 0) {
            return;
        }
        if (contentBlock.role === 'user') {
            conversation.push({ type: 'user', text: contentBlock.parts[0] || "" });
        } else if (contentBlock.role === 'model') {
            const numParts = contentBlock.parts.length;
            if (numParts === 1) {
                conversation.push({ type: 'response', text: contentBlock.parts[0] });
            } else if (numParts > 1) {
                const thoughtParts = contentBlock.parts.slice(0, numParts - 1);
                const thoughtText = thoughtParts.join('\n\n---\n\n');
                conversation.push({ type: 'thought', text: thoughtText });
                const responsePart = contentBlock.parts[numParts - 1];
                conversation.push({ type: 'response', text: responsePart });
            }
        } else {
            contentBlock.parts.forEach((partText) => {
                conversation.push({ type: 'unknown_part', text: partText });
            });
        }
    }


    function formatAndDownloadCodeChat(conversationData) {
        updateStatus(buttonTextProcessingCode);
        if (!conversationData || conversationData.length === 0) {
            updateStatus('错误 (SDK代码): 未提取到有效对话内容。');
            alert('未能从SDK代码中提取到任何有效的对话内容,无法导出。');
            captureButtonCode.textContent = buttonTextStartCode;
            captureButtonCode.disabled = false;
            updateStatus('');
            return;
        }

        let fileContent = "对话记录 (提取自 Python SDK 代码)\n=========================================\n\n";
        conversationData.forEach(item => {
            switch (item.type) {
                case 'user':
                    fileContent += `--- 用户输出 ---\n${item.text}\n\n`;
                    break;
                case 'thought':
                    fileContent += `--- AI 思维链 ---\n${item.text}\n\n`;
                    break;
                case 'response':
                    fileContent += `--- AI 输出 ---\n${item.text}\n\n`;
                    break;
                default:
                    fileContent += `--- 未知部分 (${item.type}) ---\n${item.text}\n\n`;
            }
            fileContent += "-----------------------------------------\n\n";
        });
        fileContent = fileContent.replace(/\n\n-----------------------------------------\n\n$/, '\n').trim();

        try {
            const blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' });
            const link = document.createElement('a');
            const url = URL.createObjectURL(blob);
            link.href = url;
            // 【修改】使用新的函数来生成文件名
            const projectName = getProjectName();
            link.download = `${projectName}_sdk_${getCurrentTimestamp()}.txt`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            captureButtonCode.textContent = successTextCode;
            captureButtonCode.classList.add('success');
        } catch (e) {
            console.error("SDK代码导出文件失败:", e);
            captureButtonCode.textContent = `${errorTextCode}: 创建失败`;
            captureButtonCode.classList.add('error');
            alert("创建SDK代码下载文件时出错: " + e.message);
        }

        setTimeout(() => {
            captureButtonCode.textContent = buttonTextStartCode;
            captureButtonCode.disabled = false;
            captureButtonCode.classList.remove('success', 'error');
            updateStatus('');
        }, exportTimeout);
    }

    function handleCodeBlockExport() {
        console.log("“从SDK代码导出对话”按钮被点击。");
        updateStatus('正在查找SDK代码块...');
        captureButtonCode.disabled = true;
        captureButtonCode.textContent = '查找SDK代码...';

        setTimeout(() => {
            const codeElement = findCodeBlockElement();
            if (!codeElement) {
                // 【修改】当找不到代码块时,闪烁按钮并给出更友好的提示
                flashGetCodeButton();
                alert(`导出失败:未找到SDK代码块。\n\n请先点击页面上闪烁的 "Get code" 按钮,等待代码出现后,再点击“从SDK导出对话TXT”按钮。`);

                updateStatus(`错误: 未找到SDK代码块`);
                captureButtonCode.textContent = errorTextCode;
                captureButtonCode.classList.add('error');
                setTimeout(() => {
                    captureButtonCode.textContent = buttonTextStartCode;
                    captureButtonCode.disabled = false;
                    captureButtonCode.classList.remove('error');
                    updateStatus('');
                }, exportTimeout);
                return;
            }

            const codeText = codeElement.textContent;
            if (!codeText || codeText.trim().length === 0) {
                updateStatus('错误: SDK代码块为空。');
                alert('找到的SDK代码块元素内容为空,无法提取对话。');
                captureButtonCode.textContent = errorTextCode;
                captureButtonCode.classList.add('error');
                setTimeout(() => {
                    captureButtonCode.textContent = buttonTextStartCode;
                    captureButtonCode.disabled = false;
                    captureButtonCode.classList.remove('error');
                    updateStatus('');
                }, exportTimeout);
                return;
            }
            updateStatus('正在从SDK代码中提取对话...');
            captureButtonCode.textContent = '提取中...';
            setTimeout(() => {
                try {
                    const conversationData = extractConversationFromCodeBlock(codeText);
                    formatAndDownloadCodeChat(conversationData);
                } catch (error) {
                    console.error('SDK代码导出处理过程中发生错误:', error);
                    updateStatus(`错误 (SDK代码导出): ${error.message}`);
                    alert(`SDK代码导出处理过程中发生错误: ${error.message}`);
                    captureButtonCode.textContent = `${errorTextCode}: 处理出错`;
                    captureButtonCode.classList.add('error');
                    setTimeout(() => {
                        captureButtonCode.textContent = buttonTextStartCode;
                        captureButtonCode.disabled = false;
                        captureButtonCode.classList.remove('error');
                        updateStatus('');
                    }, exportTimeout);
                }
            }, 50);

        }, 50);
    }


    // --- 脚本初始化入口 ---
    console.log("Google AI Studio 导出脚本 (v1.4): 等待页面加载 (2.5秒)...");
    setTimeout(createUI, 2500);

})();