1. 项目概述:一个让ChatGPT“听话”的浏览器脚本库
如果你经常和ChatGPT的网页版打交道,无论是用它来辅助写作、编程还是日常问答,你肯定遇到过一些不那么“顺手”的时刻。比如,想一键复制某个精彩的回复,却发现按钮藏得很深;或者想批量导出对话历史,却只能手动一页页复制粘贴;又或者,你希望界面能更清爽,去掉那些用不上的侧边栏元素。这些需求,官方界面往往没有提供直接的解决方案。
这就是KudoAI/chatgpt.js这个项目诞生的背景。它不是一个独立的聊天机器人,而是一个纯粹的、运行在浏览器环境中的JavaScript库。你可以把它理解为一套功能强大的“瑞士军刀”,专门用来与ChatGPT的官方网页界面进行交互。它的核心目标,是让开发者能够通过编程的方式,自动化地操作ChatGPT界面,提取数据,甚至改变其外观和行为,从而极大地提升使用效率和定制化程度。
简单来说,有了chatgpt.js,你就能让ChatGPT的网页版“听你的话”。无论是个人用户想打造一个更顺手的自动化工作流,还是开发者想基于ChatGPT的对话能力构建更复杂的应用(比如集成到自己的工具里,或者做数据分析),这个库都提供了一个稳定、可靠的底层接口。它绕过了官方可能不提供的API,直接从用户可见的网页入手,实现了高度的灵活性和控制力。
2. 核心设计思路与技术选型
2.1 为什么选择浏览器扩展/用户脚本这条路?
要理解chatgpt.js的设计,首先要明白它面对的核心挑战:ChatGPT的官方网页是一个动态生成的单页应用(SPA),其DOM结构、类名、ID都可能随着前端的更新而改变。官方没有提供用于此类自动化操作的公开API。
面对这个挑战,通常有几种思路:
- 逆向工程官方API:直接调用其内部通信接口。这种方式最直接,但风险极高,一旦官方变更接口或加密方式,脚本就会立即失效,且可能违反服务条款。
- 模拟用户操作:使用像Puppeteer、Playwright这样的无头浏览器进行自动化。这种方式稳定,但重量级,需要运行一个浏览器实例,不适合轻量级、实时的用户侧脚本。
- DOM操作与事件监听:直接与浏览器中已加载的页面DOM进行交互。这正是
chatgpt.js选择的道路。它作为一个内容脚本(Content Script)或用户脚本(User Script,如Tampermonkey、Violentmonkey),注入到ChatGPT的页面中,与页面共享同一个DOM环境。
选择DOM操作路线的核心考量:
- 轻量与实时:脚本随页面加载而注入,响应速度快,无需额外进程。
- 用户友好:最终用户只需安装一个浏览器扩展(用于管理用户脚本),即可使用基于此库开发的各种功能,门槛极低。
- 高灵活性:可以操作任何可见元素,修改样式、绑定事件、提取数据,几乎无所不能。
- 相对稳定:虽然DOM结构会变,但页面的核心功能区域(输入框、对话列表、发送按钮)的基本交互模式相对稳定。库可以通过健壮的选择器策略和错误处理来应对变化。
2.2 架构设计:模块化与可扩展性
chatgpt.js的代码库采用了清晰的模块化设计,这不是一个把所有功能堆在一起的巨型脚本。查看其源码,你会发现它通常包含以下几个核心部分:
- 核心模块 (
core.js或类似):定义最基础的、与页面版本无关的通用函数。例如,检测页面是否已加载完成、获取根容器元素、提供等待特定元素出现的工具函数等。这是库的基石。 - DOM选择器模块 (
selectors.js):这是最关键也是最需要维护的部分。它集中定义了所有用于定位页面元素的选择器字符串。例如,如何找到对话历史容器、单个消息气泡、用户头像、输入文本框、发送按钮等等。将这些选择器集中管理,是为了当ChatGPT前端更新时,开发者只需修改这个模块中的选择器,而无需改动所有业务逻辑代码。 - 功能模块 (如
conversation.js,ui.js,utils.js):基于核心模块和选择器模块,实现具体的功能。例如:conversation.js: 提供获取当前对话列表、获取最后一条消息、发送消息等方法。ui.js: 提供添加自定义按钮、修改界面样式、显示通知等方法。utils.js: 提供防抖、节流、数据格式化等工具函数。
- 入口与初始化:一个主文件负责在合适的时机(通常是页面加载完成后)初始化库,并可能对外暴露一个全局对象(如
window.chatgptJS),供其他用户脚本调用。
这种架构的好处是显而易见的:解耦和可维护性。功能开发者和脚本使用者不需要关心底层的DOM选择器是如何工作的,他们只需要调用诸如chatgpt.getLastResponse()这样的高级API。当页面变化时,维护者只需尽力更新selectors.js,理论上就能让所有依赖此库的脚本恢复工作。
注意:这种基于DOM操作的方式,其稳定性完全依赖于对ChatGPT网页结构的“猜测”和“适配”。因此,
chatgpt.js项目本身或其下游脚本,在ChatGPT前端重大更新后出现“罢工”是常态,而非异常。其价值在于提供了一个统一的、经过一定封装的适配层,降低了整个生态的维护成本。
3. 核心API与功能深度解析
让我们深入看看chatgpt.js通常会提供哪些核心能力。这些API是构建一切高级功能的基础。
3.1 对话管理
这是库最核心的功能之一,旨在让程序能“读懂”和“操作”对话。
getConversationHistory(): 此函数会扫描整个对话面板,将每条消息(包括用户的和AI的)解析成一个结构化的数组。每个消息对象可能包含:角色(user/assistant)、内容文本(纯文本或HTML)、时间戳、唯一的消息ID等。实现它需要遍历类似.group或.text-base的容器,并区分用户消息和AI消息(通常通过头像、特定类名或容器位置判断)。- 实操难点:消息内容可能包含代码块、表格等复杂格式。一个健壮的实现需要能提取出纯净的文本,或者保留必要的格式信息。有时还需要处理消息流式输出未完成的情况。
getLastMessage()/getLastResponse(): 这两个是高频使用的快捷方法。getLastMessage通常返回最后一条消息(无论谁发的),而getLastResponse特指ChatGPT的最后一条回复。实现它们需要对getConversationHistory的结果进行过滤和排序。sendMessage(text): 自动化发送消息。其实现步骤是:1) 找到文本输入框(可能是textarea或div[contenteditable])。2) 将焦点设置到该输入框。3) 模拟输入文本(直接设置value或触发input事件)。4) 找到并点击发送按钮(或模拟按下Enter键)。这里的关键是必须触发正确的DOM事件,让ChatGPT的前端框架能感知到输入变化和提交动作。- 注意事项:直接设置
input.value可能不会触发React等框架的状态更新。更可靠的做法是:input.focus(); input.value = text; const event = new Event('input', { bubbles: true }); input.dispatchEvent(event);。
- 注意事项:直接设置
3.2 界面操控与增强
让界面变得更符合个人需求。
addButton(config): 在页面的指定位置(如输入框旁、消息旁)添加一个自定义按钮。config参数需要指定按钮文本、CSS类、点击回调函数以及插入的位置选择器。这个功能极大地扩展了可能性,比如可以添加“一键润色”、“导出对话”、“翻译此条”等按钮。- 实现细节:创建
button元素,应用样式,绑定click事件,然后使用insertAdjacentElement或appendChild将其插入到目标元素附近。需要小心处理事件冒泡,避免干扰原有功能。
- 实现细节:创建
modifyUI(cssRules): 通过注入CSS规则来修改页面样式。可以用于隐藏元素(如侧边栏、水印)、改变字体、调整布局等。实现方式通常是创建一个style标签,将CSS规则写入,然后插入到文档的head中。- 心得:为了确保样式优先级足够高,可能需要使用
!important声明,或者更精细地复制原始选择器并覆盖其属性。
- 心得:为了确保样式优先级足够高,可能需要使用
showNotification(message, type): 在页面一角显示一个临时的提示框(Toast),用于反馈操作结果。这属于提供良好的用户交互体验。
3.3 状态与事件监听
感知页面的变化,是实现自动化的关键。
onMessageReceived(callback): 注册一个监听器,当页面中出现新的AI回复消息时触发回调。这个功能对于实现“自动回复”、“消息触发特定动作”等场景至关重要。- 实现原理:通常使用
MutationObserverAPI来监视对话容器DOM节点的变化。当检测到有新的、符合AI消息特征的元素被添加时,就触发回调。这是一个相对高级且消耗性能的操作,需要精心设计观察目标和过滤条件,避免过度触发。
- 实现原理:通常使用
isLoading(): 返回一个布尔值,指示ChatGPT是否正在生成回复(即是否处于“正在输入…”状态)。这可以通过检查是否存在旋转的指示器图标或特定的加载状态类来实现。getCurrentModel(): 尝试获取当前对话所使用的模型(如GPT-3.5, GPT-4)。这个信息有时会显示在输入框附近或页面标题中,但官方可能不会轻易暴露,实现此功能可能不稳定。
4. 基于chatgpt.js的典型应用场景与实操
理解了核心API,我们就可以看看如何用它们来构建实用的工具。以下是一些典型场景和实现思路。
4.1 场景一:构建对话历史导出器
需求:将一次完整的对话导出为结构化的文件,如Markdown、JSON或纯文本,方便存档或分享。
实现步骤:
- 等待与初始化:确保页面和
chatgpt.js库已加载完成。 - 获取历史:调用
chatgpt.getConversationHistory(),获得消息数组。 - 数据清洗与格式化:
- Markdown格式:将用户消息前加上
### 用户:,AI消息前加上### ChatGPT:。对于AI消息中的代码块,原样保留 ```。可以尝试将一些HTML标签(如<strong>)转换为Markdown语法(**)。 - JSON格式:直接将消息数组用
JSON.stringify序列化,结构清晰,便于程序后续处理。
- Markdown格式:将用户消息前加上
- 提供导出界面:使用
chatgpt.addButton在页面合适位置添加一个“导出对话”按钮。点击后,触发格式化逻辑,然后使用浏览器API创建下载。// 伪代码示例 chatgpt.addButton({ text: '导出为Markdown', position: 'near-input', // 假设库支持这个位置 onClick: async () => { const history = await chatgpt.getConversationHistory(); let markdown = `# 对话记录\n\n`; history.forEach(msg => { const role = msg.role === 'user' ? '用户' : 'ChatGPT'; markdown += `### ${role}:\n\n${msg.content}\n\n---\n\n`; }); // 创建Blob并下载 const blob = new Blob([markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chatgpt-conversation-${Date.now()}.md`; a.click(); URL.revokeObjectURL(url); chatgpt.showNotification('导出成功!', 'success'); } }); - 处理复杂内容:AI的回复可能包含复杂的富文本。简单的
innerText会丢失格式,而innerHTML又包含太多标签。一个折中方案是使用DOMParser将AI回复的HTML片段解析,然后递归提取文本,并对特定标签(如code,pre)进行特殊处理,转换为Markdown。
4.2 场景二:实现自动化交互脚本
需求:自动向ChatGPT发送一系列预设问题,并收集其回答,用于批量测试或数据收集。
实现步骤:
- 定义任务队列:创建一个包含多个问题的数组。
- 监听回复完成:使用
chatgpt.onMessageReceived监听新回复。当收到一个回复后,将其保存下来。 - 控制发送节奏:
- 发送第一个问题。
- 在收到回复的回调中,延迟一段时间(例如2-3秒,模拟人类阅读并避免请求过快),然后从队列中取出下一个问题,调用
chatgpt.sendMessage发送。
- 错误处理与恢复:
- 在每次发送和等待时,检查
chatgpt.isLoading()和页面状态。 - 使用
try...catch包裹关键操作。 - 可以考虑将当前进度(问题索引、已收集的答案)保存到
localStorage,这样即使页面刷新或脚本意外停止,也能从中断处恢复。
const questions = ['解释量子计算', '用Python写一个快速排序', '莎士比亚的生平']; const answers = []; let currentIndex = 0; function askNextQuestion() { if (currentIndex >= questions.length) { console.log('所有问题已完成', answers); chatgpt.showNotification('自动化任务完成!', 'info'); return; } const question = questions[currentIndex]; chatgpt.sendMessage(question).then(() => { console.log(`已发送: ${question}`); }); } // 监听回复 chatgpt.onMessageReceived((message) => { answers.push({ question: questions[currentIndex], answer: message.content }); currentIndex++; // 等待2秒后问下一个问题 setTimeout(askNextQuestion, 2000); }); // 开始 askNextQuestion();重要提示:此类自动化脚本应严格遵守ChatGPT的使用政策,避免用于制造垃圾请求、进行恶意爬取或任何可能对服务造成压力的行为。务必添加合理的延迟,并仅用于个人合法的学习和测试目的。
- 在每次发送和等待时,检查
4.3 场景三:创建个性化UI增强
需求:觉得官方界面太宽,想实现一个“聚焦模式”,隐藏侧边栏和其他干扰元素,让对话区域居中并变宽。
实现步骤:
- 分析页面结构:使用浏览器开发者工具,找到侧边栏、主对话区、顶部导航栏等元素的选择器。
- 编写CSS:编写用于隐藏和调整样式的CSS规则。
/* 隐藏侧边栏 */ nav, aside, [data-testid*="sidebar"] { display: none !important; } /* 主容器占满宽度 */ .main-container-class { max-width: 100% !important; margin: 0 auto !important; } /* 调整对话区域宽度 */ .conversation-container-class { max-width: 900px !important; /* 调整为更宽的固定值 */ } - 动态注入样式:在脚本初始化时,调用
chatgpt.modifyUI注入上述CSS。chatgpt.modifyUI(` nav, aside, [data-testid*="sidebar"] { display: none !important; } .main-container-class { max-width: 100% !important; margin: 0 auto; } .conversation-container-class { max-width: 900px !important; } `); - 添加切换按钮:为了能随时恢复原状,可以添加一个按钮来切换这个模式。这需要脚本动态维护一个状态变量,并在点击时注入或移除对应的CSS规则。
5. 开发与使用中的避坑指南
基于DOM操作和逆向工程,这条路充满了“坑”。以下是我在实际使用和开发类似工具中积累的一些关键经验。
5.1 选择器策略:稳健性高于一切
ChatGPT前端的一次小更新就可能让你的选择器全军覆没。以下策略能提升稳健性:
- 避免使用绝对定位的选择器:如
div > div > div:nth-child(3) > button。这种选择器极其脆弱。 - 优先使用属性选择器:寻找元素上稳定的
>async function findElement(selectors, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { for (const selector of selectors) { const el = document.querySelector(selector); if (el) return el; } await new Promise(resolve => setTimeout(resolve, 100)); // 等待100ms再试 } throw new Error(`找不到元素,尝试的选择器: ${selectors.join(', ')}`); } // 使用 const sendButton = await findElement([ '[data-testid="send-button"]', 'button[aria-label*="Send"]', 'button:has(svg) >> text=/发送|Send/i' ]);
5.2 处理动态加载与流式输出
ChatGPT的消息是流式输出的,这意味着一条消息的DOM元素会持续变化。
- 监听消息完成:判断一条消息是否输出完毕,不能只看元素是否存在。可以监听元素内部文本是否在一段时间内(如1.5秒)不再变化,或者寻找表示“停止生成”的图标出现。
- 获取完整内容:对于流式输出中的消息,直接取
innerText可能只能拿到已输出的部分。一个更可靠的方法是,等待消息“完成”后,再去获取其内容。chatgpt.js的onMessageReceived回调应该只在消息最终完成时才触发。
5.3 版本兼容性与更新维护
- 建立监控机制:作为脚本开发者,可以设置简单的自动化测试,定期访问ChatGPT页面并运行核心功能检查,一旦失败就发出警报。
- 社区协作:
chatgpt.js这类项目往往依赖社区。在GitHub上积极提交Issue报告失效的选择器,或提交Pull Request修复,是项目存活的关键。 - 为用户提供降级提示:在你的用户脚本中,如果检测到核心功能失效,应向用户显示友好的错误提示,例如“检测到ChatGPT界面已更新,脚本部分功能可能失效,请等待作者更新”。
5.4 安全与隐私考量
- 权限最小化:如果你在开发浏览器扩展,在
manifest.json中只申请必需的权限(如activeTab,storage)。对于用户脚本,也要明确告知用户脚本会访问哪些页面和数据。 - 谨慎处理对话数据:你的脚本可以访问所有对话内容。务必在隐私政策中说明数据如何处理(通常应承诺所有处理仅在本地浏览器进行,不上传任何数据)。代码也应开源,接受监督。
- 避免干扰正常服务:自动化脚本应设置合理的请求间隔,避免触发ChatGPT的反爬虫或滥用机制。
6. 常见问题排查实录
在实际使用基于chatgpt.js的脚本时,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 | ||
|---|---|---|---|---|
| 脚本完全不起作用,控制台无错误 | 1. 脚本管理器未正确安装或启用。 2. 脚本的 @match或@include元数据未匹配当前ChatGPT URL。3. chatgpt.js库未成功加载或初始化。 | 1. 检查Tampermonkey等扩展是否启用,脚本是否启用。 2. 检查脚本头部元数据,确保包含 https://chat.openai.com/*。3. 打开浏览器开发者工具(F12)的“控制台”(Console)和“网络”(Network)标签,查看是否有加载错误或404。 | ||
| 部分功能失效(如找不到发送按钮) | ChatGPT前端DOM结构已更新,脚本内的选择器失效。 | 1. 打开开发者工具的元素审查(Inspector),手动尝试脚本中使用的选择器,看是否能找到元素。 2. 寻找元素新的稳定属性(如 >能发送消息,但AI不回复 | 1. 发送消息后未正确触发提交事件。 2. 输入框是 contenteditable的div,直接设置value无效。3. 页面处于非活跃状态(如另一个标签页)。 | 1. 确保在设置文本后,不仅触发input事件,对于contenteditable元素,可能需要触发keydown,keyup,change等事件组合。2. 对于 div[contenteditable],尝试element.focus(); element.innerHTML = text; element.dispatchEvent(new Event('input', {bubbles: true}));。3. 检查是否有网络错误或“重试”按钮出现。 |
| 导出对话时格式错乱 | 1. 消息内容提取方式过于简单(只用innerText)。2. AI回复中的代码块、列表等富文本未被正确处理。 | 1. 尝试提取innerHTML并进行清洗,或使用专门的HTML转Markdown库(如Turndown)。2. 针对代码块(通常包裹在 pre > code标签内),在转换时保留其结构并添加Markdown代码块标记。 | ||
| 自动化脚本被中断或封禁 | 请求频率过高,行为被检测为机器人。 | 1.大幅增加请求间隔,模拟真人打字和阅读速度(例如每条消息间隔30秒以上)。 2. 避免在深夜长时间运行高频率脚本。 3. 考虑使用官方API(如果可用)进行合规的批量操作,这是最安全的方式。 |
最后,我想分享一点个人体会。像chatgpt.js这样的项目,其生命力完全在于社区。它游走在官方服务的边缘,通过巧妙的工程手段填补了用户需求与官方功能之间的鸿沟。使用它,你需要有一种“黑客精神”——乐于探索、善于调试、接受不完美,并理解工具可能会突然失效。但同时,它带来的效率提升和可能性也是巨大的。对于开发者而言,研究它的源码也是一个学习如何与复杂Web应用交互的绝佳案例。记住,保持尊重和节制地使用,让工具服务于创造,而不是滥用,才是长久之道。当你的脚本因为前端更新而失效时,别灰心,打开开发者工具,那又是一个有趣的解密游戏开始了。