// ==UserScript== // @name Google AI Studio 聊天记录导出器 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 自动滚动 Google AI Studio 聊天界面,捕获用户消息、AI 思维链和 AI 回答,导出为 TXT 文件;或直接从 Python 代码块中提取对话并导出。 // @author qwerty // @match https://aistudio.google.com/* // @grant GM_addStyle // @grant GM_setClipboard // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwNzhmZiI+PHBhdGggZD0iTTE5LjUgMi4yNWgtMTVjLTEuMjQgMC0yLjI1IDEuMDEtMi4yNSAyLjI1djE1YzAgMS4yNCAxLjAxIDIuMjUgMi4yNSAyLjI1aDE1YzEuMjQgMCAyLjI1LTEuMDEgMi4yNS0yLjI1di0xNWMwLTEuMjQtMS4wMS0yLjI1LTIuMjUtMi4yNXptLTIuMjUgNmgtMTAuNWMtLjQxIDAtLjc1LS4zNC0uNzUtLjc1cy4zNC0uNzUuNzUtLjc1aDEwLjVjLjQxIDAgLjc1LjM0Ljc1Ljc1cy0uMzQuNzUtLjc1Ljc1em0wIDRoLTEwLjVjLS40MSAwLS43NS0uMzQtLjc1LS43NXMuMzQtLjc1Ljc1LS43NWgxMC41Yy40MSAwIC43NS4zNC43NS43NXMtLjM0Ljc1LS43NS43NXptLTMgNGgtNy41Yy0uNDEgMC0uNzUtLjM0LS43NS0uNzVzLjM0LS43NS43NS0uNzVoNy41Yy40MSAwIC43NS4zNC43NS43NXMtLjM0Ljc1LS43NS43NXoiLz48L3N2Zz4= // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/534177/Google%20AI%20Studio%20%E8%81%8A%E5%A4%A9%E8%AE%B0%E5%BD%95%E5%AF%BC%E5%87%BA%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/534177/Google%20AI%20Studio%20%E8%81%8A%E5%A4%A9%E8%AE%B0%E5%BD%95%E5%AF%BC%E5%87%BA%E5%99%A8.meta.js // ==/UserScript== (function() { // 使用立即执行函数表达式 (Immediately Invoked Function Expression, IIFE) 来封装整个脚本 // 这样做的好处是创建一个独立的作用域,避免脚本内部的变量和函数污染全局命名空间, // 同时也防止与其他可能在页面上运行的 JavaScript 代码发生冲突。 'use strict'; // 在脚本或函数的开头启用 JavaScript 的严格模式。 // 严格模式有助于开发者编写更安全、更可靠的代码,它会捕获一些常见的编码错误, // 并禁止使用一些不推荐的 JavaScript 特性。 // --- 全局配置常量 --- // 将脚本中使用的固定值或可配置参数定义为常量。这样做的好处是: // 1. 易于查找和修改:所有配置项集中在一起。 // 2. 提高可读性:使用有意义的常量名代替硬编码的值。 // 3. 避免魔法数字/字符串:代码中直接使用常量名,意图更清晰。 // --- 滚动导出按钮文本 --- const buttonTextStartScroll = "开始滚动并导出 TXT"; // “滚动导出”按钮在脚本加载完成、等待用户操作时的初始文本。 const buttonTextStopScroll = "停止滚动"; // “停止滚动”按钮上显示的文本。 const buttonTextProcessingScroll = "处理滚动数据..."; // 当脚本完成滚动、正在整理数据并准备生成下载文件时,“滚动导出”按钮临时显示的文本。 const successTextScroll = "滚动导出 TXT 成功!"; // 当滚动导出的 TXT 文件成功生成并触发下载后,“滚动导出”按钮短暂显示的成功提示文本。 const errorTextScroll = "滚动导出失败"; // 当滚动导出过程中遇到错误时,“滚动导出”按钮短暂显示的失败提示文本。 // --- 代码块导出按钮文本 --- const buttonTextStartCode = "一键导出代码块对话 (TXT)"; // 新增:“代码块导出”按钮在脚本加载完成、等待用户操作时的初始文本。 const buttonTextProcessingCode = "处理代码块数据..."; // 新增:当脚本正在解析代码块并准备生成下载文件时,“代码块导出”按钮临时显示的文本。 const successTextCode = "代码块导出 TXT 成功!"; // 新增:当代码块导出的 TXT 文件成功生成并触发下载后,“代码块导出”按钮短暂显示的成功提示文本。 const errorTextCode = "代码块导出失败"; // 新增:当代码块导出过程中遇到错误时,“代码块导出”按钮短暂显示的失败提示文本。 // --- UI 提示信息的显示时长 --- const exportTimeout = 3000; // 设置成功或失败的提示信息在按钮上显示多长时间后自动恢复为初始文本(单位:毫秒)。 // --- 导出文件相关配置 --- const EXPORT_FILENAME_PREFIX_SCROLL = 'aistudio_chat_scroll_export_'; // 定义滚动导出的 TXT 文件名的固定前缀部分。 const EXPORT_FILENAME_PREFIX_CODE = 'aistudio_code_chat_export_'; // 新增:定义代码块导出的 TXT 文件名的固定前缀部分。 // --- 自动滚动行为相关配置 --- const SCROLL_DELAY_MS = 2000; // **重要配置**:每次执行滚动操作后,脚本需要暂停多长时间(毫秒)来等待页面加载新的聊天内容。 // 这个值需要根据你的网络速度和 AI Studio 页面的响应速度进行调整。如果设置太短,可能内容还没加载出来脚本就继续滚动了; // 如果设置太长,会增加总的导出时间。这里稍微增加到 2 秒,给渲染更多时间。 const MAX_SCROLL_ATTEMPTS = 300; // 设置脚本最多尝试执行多少次滚动操作。这是一个安全限制,防止在某些异常情况下(例如,无法正确检测到页面底部)脚本陷入无限滚动。 const SCROLL_INCREMENT_FACTOR = 0.85; // 定义每次向下滚动的步长。这个值是当前浏览器窗口可见区域高度的一个比例。例如,0.85 表示每次滚动大约 85% 的窗口高度。 // 较小的值滚动更平稳,但次数更多;较大的值滚动快,但可能跳过内容加载触发点。 const SCROLL_STABILITY_CHECKS = 3; // **重要配置**:用于判断是否滚动到底部的稳定检查次数。当脚本检测到滚动容器的总高度连续这么多次都没有变化时, // 就假定已经到达了内容的底部(因为没有新内容加载进来使高度增加了)。这是比简单检查滚动条位置更可靠的方法。 // --- 代码块选择器 --- // **重要**: 这个选择器用于定位包含 Python 代码的 `` 元素。 // 初始值是基于用户提供的示例 (`code.code.gmat-body-medium`)。 // 如果 AI Studio 页面结构发生变化,导致这个选择器失效,用户需要: // 1. 在浏览器中右键点击代码块区域,选择“检查”或“检查元素”。 // 2. 在开发者工具中找到包裹代码的 `` 标签。 // 3. 观察其 class 属性或其他可用于定位的属性。 // 4. 修改下面的常量值为一个更可靠的 CSS 选择器。 const CODE_BLOCK_SELECTOR = 'code.code.gmat-body-medium'; // 新增:用于定位 Python 代码块的 CSS 选择器 // --- 脚本内部状态变量 (滚动导出) --- // 这些变量用于在脚本的整个生命周期中跟踪其当前的运行状态和收集到的数据。 let isScrolling = false; // 布尔值 (true/false),用来标记脚本当前是否正处于自动滚动的状态。这个标志用于控制滚动循环的启动和停止,并防止用户重复点击“开始”按钮。 let collectedData = new Map(); // 使用 JavaScript 的 `Map` 数据结构来存储所有收集到的聊天回合信息。 // **关键设计点**:Map 的 Key 直接使用每个聊天回合对应的 `ms-chat-turn` DOM 元素节点引用。 // 因为 DOM 节点引用是唯一的,这确保了即使在滚动过程中同一个聊天回合被多次看到,它在 Map 中也只会被记录一次。 // Map 的 Value 是一个对象,包含该回合的类型(用户/模型/思维链等)和提取到的文本内容。 // *遇到的问题*:最初尝试使用回合内元素的 ID 作为 Key,但发现一个回合可能包含多个带 ID 的元素(如思维链和回答),导致 Key 不唯一而出错。 // *解决方案*:改用 `ms-chat-turn` DOM 节点本身作为 Key,利用其引用的唯一性。 let scrollCount = 0; // 整数,用于记录从用户点击“开始”按钮后,脚本已经执行了多少次滚动操作。主要用于与 `MAX_SCROLL_ATTEMPTS` 比较,防止无限滚动。 let noChangeCounter = 0; // 整数,计数器,用于记录滚动容器的总高度 (`scrollHeight`) 连续多少次检查都没有发生变化。这是实现 `SCROLL_STABILITY_CHECKS` 的基础。 // --- UI 界面元素变量 --- // 这些变量将在 `createUI` 函数中被初始化,指向由脚本动态创建并添加到页面上的各个 HTML 元素(按钮、状态显示区)。 // 将它们声明在这里(函数外部)是为了让脚本中的其他函数(如事件处理函数、状态更新函数)也能访问到这些元素。 let captureButtonScroll = null; // 持有对“滚动导出”按钮 DOM 元素的引用。 let stopButtonScroll = null; // 持有对“停止滚动”按钮 DOM 元素的引用。 let captureButtonCode = null; // 新增:持有对“代码块导出”按钮 DOM 元素的引用。 let statusDiv = null; // 将持有对显示状态信息的 `
` DOM 元素的引用。 // --- 辅助工具函数 --- // 这些是脚本内部使用的一些通用功能函数。 /** * 返回一个 Promise,该 Promise 在指定的毫秒数后解决 (resolve)。 * 用于在异步代码中创建暂停,例如等待网络请求或页面渲染。 * @param {number} ms - 需要暂停等待的毫秒数。 * @returns {Promise} - 一个将在指定时间后完成的 Promise。 */ function delay(ms) { // `setTimeout` 会在 `ms` 毫秒后执行提供的函数(这里是 `resolve`)。 // `resolve` 被调用时,这个 `Promise` 就进入了完成状态。 return new Promise(resolve => setTimeout(resolve, ms)); } /** * 获取当前时间的格式化字符串,格式为 "YYYYMMDD_HHMMSS"。 * 主要用于生成带有时间戳的、不容易重复的文件名。 * @returns {string} - 格式化后的时间字符串。 */ function getCurrentTimestamp() { const n = new Date(); // 创建一个新的 Date 对象,表示当前的日期和时间。 const YYYY = n.getFullYear(); // 获取完整的四位数年份。 const MM = (n.getMonth() + 1).toString().padStart(2, '0'); // 获取月份(注意:月份是从 0 开始的,所以需要 +1)。`.toString()` 转为字符串,`.padStart(2, '0')` 确保月份总是两位数(例如,1 月是 '01' 而不是 '1')。 const DD = n.getDate().toString().padStart(2, '0'); // 获取月份中的日期(1-31),并补零至两位数。 const hh = n.getHours().toString().padStart(2, '0'); // 获取小时(0-23),并补零至两位数。 const mm = n.getMinutes().toString().padStart(2, '0'); // 获取分钟(0-59),并补零至两位数。 const ss = n.getSeconds().toString().padStart(2, '0'); // 获取秒钟(0-59),并补零至两位数。 // 使用模板字面量(反引号 ``)将所有部分拼接成所需的格式。 return `${YYYY}${MM}${DD}_${hh}${mm}${ss}`; } /** * 尝试在 Google AI Studio 页面上找到负责内容滚动的主要 HTML 元素 (用于滚动导出功能)。 * **这是脚本中最容易出错的部分之一,因为它依赖于目标网站的内部结构。** * 现代 Web 应用(尤其是像 AI Studio 这样基于框架构建的)的 DOM 结构可能很复杂,并且会随着更新而改变。 * 这个函数按优先级尝试了几种不同的 CSS 选择器策略来定位滚动容器。 * *遇到的问题*:很难有一个通用的选择器能完美适配所有情况和未来的更新。 * *解决方案*:提供多种查找策略,并留下警告,提示用户如果滚动不工作,最可能需要检查和修改这里。 * @returns {HTMLElement | Document} - 返回找到的滚动容器 DOM 元素。如果所有策略都失败,则返回 `document.documentElement`(代表整个文档),作为最后的尝试。 */ function getMainScrollerElement_AiStudio() { console.log("尝试查找滚动容器 (用于滚动导出)..."); // 日志记录查找操作的开始 // 策略 1: 尝试查找一个假设的、可能专门用于标识聊天滚动区域的 CSS 类名。 // 这个类名 `.chat-scrollable-container` 是基于常见的命名习惯推测的,**需要用户根据实际情况使用浏览器开发者工具(F12)检查并替换为正确的类名或选择器**。 let scroller = document.querySelector('.chat-scrollable-container'); // 检查是否找到了元素 (`scroller` 不为 null),并且该元素的内容总高度 (`scrollHeight`) 大于其在屏幕上可见的高度 (`clientHeight`)。只有当内容高度大于可见高度时,元素才真正需要滚动。 if (scroller && scroller.scrollHeight > scroller.clientHeight) { console.log("找到滚动容器 (策略 1: .chat-scrollable-container):", scroller); // 找到则打印日志并返回该元素 return scroller; } // 策略 2: 尝试查找 Angular Material 框架中常用的一个组件标签名 `mat-sidenav-content`。 // Google 的很多 Web 应用使用 Angular Material,这个标签通常包裹主要内容区域,有时它本身就是滚动容器。 scroller = document.querySelector('mat-sidenav-content'); if (scroller && scroller.scrollHeight > scroller.clientHeight) { console.log("找到滚动容器 (策略 2: mat-sidenav-content):", scroller); return scroller; } // 策略 3: 如果前两种特定策略失败,尝试一种更通用的、基于结构的方法: // a. 找到页面上第一个 `ms-chat-turn` 元素(假设聊天记录都在这个组件内)。 // b. 获取它的直接父元素。 const chatTurnsContainer = document.querySelector('ms-chat-turn')?.parentElement; // 使用可选链操作符 `?.` 防止在找不到 `ms-chat-turn` 时抛出错误。 if (chatTurnsContainer) { // 如果成功获取到父元素 let parent = chatTurnsContainer; // c. 从这个父元素开始,向上遍历 DOM 树,最多检查 5 层祖先元素(避免无限向上查找)。 for (let i = 0; i < 5 && parent; i++) { // d. 对每个祖先元素,检查它是否满足成为滚动容器的条件: // i. 内容高度大于可见高度(`parent.scrollHeight > parent.clientHeight + 10`,加 10 像素容差是为了处理可能的边框或内边距影响)。 // ii. 并且,它的 CSS `overflow-y` 属性被设置为 'auto' 或 'scroll',表示浏览器允许在该元素上出现垂直滚动条。 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; // 如果当前祖先不符合,继续检查它的父元素。 } } // 策略 4: 如果以上所有策略都失败了,打印一条警告信息到控制台, // 并返回 `document.documentElement`(代表整个 HTML 文档的根元素)。 // 这意味着脚本将尝试滚动整个浏览器窗口,这在某些单页应用中可能是正确的,但在其他情况下可能效果不佳。 // 警告信息提示用户,如果滚动不正常,最可能需要回来修改这个函数的选择器。 console.warn("警告 (滚动导出): 未能通过特定选择器精确找到 AI Studio 滚动区域,将尝试使用 document.documentElement。滚动效果可能不佳或不准确。请考虑检查并更新脚本中的 getMainScrollerElement_AiStudio 函数选择器。"); return document.documentElement; } /** * 新增:查找包含 Python 代码的 元素 (用于代码块导出功能)。 * 它使用全局常量 `CODE_BLOCK_SELECTOR` 来定位元素。 * @returns {HTMLElement | null} - 返回找到的 code 元素,如果找不到则返回 null。 */ function findCodeBlockElement() { console.log(`尝试查找代码块元素 (选择器: ${CODE_BLOCK_SELECTOR})...`); const codeElement = document.querySelector(CODE_BLOCK_SELECTOR); if (codeElement) { console.log("找到代码块元素:", codeElement); } else { // 如果找不到代码块,打印警告。这通常意味着 `CODE_BLOCK_SELECTOR` 需要更新。 console.warn(`警告 (代码块导出): 未能找到指定的代码块元素 (${CODE_BLOCK_SELECTOR})。请检查页面结构或更新脚本中的选择器。`); } return codeElement; } // --- UI 界面创建与更新 --- /** * 创建脚本所需的用户界面元素(滚动导出按钮、停止按钮、代码块导出按钮、状态显示区域), * 为它们设置样式,绑定事件监听器,并将它们添加到当前网页的 `` 中。 */ function createUI() { console.log("开始创建 UI 元素..."); // 日志记录 // --- 创建“滚动导出”按钮 --- captureButtonScroll = document.createElement('button'); // 使用标准 DOM API 创建一个新的