在实际开发中,我们经常需要将大型语言模型的能力集成到自己的应用中,而不仅仅是使用官方网页。ChatGPT 等模型通过 API 提供了强大的文本生成能力,但如何让用户在使用浏览器时,能便捷地调用这些能力,比如智能分析当前网页、总结内容或基于网页内容进行对话,是一个常见的工程需求。这通常涉及到浏览器扩展程序的开发。
本文将带你从零开始,开发一个能够与 ChatGPT API 交互的 Chrome 扩展程序。这个扩展的核心功能是:在用户浏览任意网页时,可以通过点击扩展图标或快捷键,将当前页面的网址或选中的文本发送给 ChatGPT,并获取智能回复。我们将重点解决扩展程序与外部 API 的通信、内容脚本注入、权限声明以及处理 API 密钥安全等实际问题。通过本文,你将掌握开发一个功能完整、可投入使用的 AI 浏览器扩展的核心技术栈和工程实践。
1. 理解 Chrome 扩展程序与 AI 集成的架构
在动手写代码之前,需要理清几个核心概念和它们之间的协作关系,这是避免后续开发混乱的关键。
1.1 Chrome 扩展程序的基本构成
一个典型的 Chrome 扩展由以下几部分组成,它们运行在各自独立的上下文中:
- 清单文件 (manifest.json):扩展的“身份证”和“说明书”,定义了扩展的名称、版本、权限、后台脚本、内容脚本、浏览器动作等核心信息。没有它,Chrome 无法识别你的扩展。
- 后台脚本 (Background Script):一个长期运行在浏览器后台的 JavaScript 环境。它没有用户界面,但可以监听浏览器事件(如安装、标签页更新)、管理扩展状态、并与内容脚本或弹出页面进行通信。它是扩展的“大脑”和“调度中心”。
- 内容脚本 (Content Script):注入到用户正在浏览的网页中的 JavaScript 文件。它可以读取和修改页面的 DOM,获取页面内容(如文本、网址),但不能直接使用 Chrome扩展API(除了少数几个,如
chrome.runtime用于通信)。它充当了网页与扩展后台之间的“信使”。 - 弹出页面 (Popup):当用户点击工具栏上的扩展图标时弹出的一个小窗口。它本质上是一个独立的 HTML 页面,可以包含自己的样式和逻辑,常用于提供快捷操作界面。
- 选项页面 (Options Page):一个更复杂的配置页面,用户可以通过右键点击扩展图标选择“选项”来打开。常用于设置 API 密钥等敏感信息。
1.2 与 ChatGPT API 集成的数据流
我们的目标是让用户在当前网页触发动作,最终获得 AI 的回复。数据流如下:
- 用户触发:用户在网页上选中文本后右键选择扩展菜单项,或直接点击扩展图标。
- 内容脚本采集:内容脚本被激活,获取当前页面的 URL 和用户选中的文本。
- 内部通信:内容脚本通过
chrome.runtime.sendMessage将采集到的数据发送给后台脚本。 - 外部 API 调用:后台脚本接收到数据后,构造符合 OpenAI API 格式的请求,附上你的 API 密钥,发送到
https://api.openai.com/v1/chat/completions。 - 处理响应:后台脚本收到 OpenAI 的 JSON 响应后,解析出 AI 生成的文本内容。
- 结果展示:后台脚本将结果发送回内容脚本(或弹出页面),由内容脚本将结果以某种形式(如侧边栏、弹窗、直接修改页面)展示给用户。
这个流程中,API 密钥的存储和调用安全是重中之重。绝对不能将密钥硬编码在内容脚本或前端页面中,因为它们很容易被他人查看。密钥应存储在后台脚本可以安全访问的地方,例如 Chrome 的本地存储 (chrome.storage.local) 中,并由后台脚本负责所有外网请求。
2. 环境准备与项目初始化
2.1 开发环境与账号准备
你需要准备以下环境:
- Chrome 浏览器:版本 88 或更高(支持 Manifest V3)。确保可以从
chrome://extensions/页面加载已解压的扩展程序。 - 代码编辑器:如 VS Code。
- OpenAI API 密钥:访问 OpenAI Platform 注册账号并创建 API Key。请注意,调用 API 会产生费用。
- 一个空的项目目录:例如
chatgpt-browser-extension。
2.2 创建核心项目文件
在你的项目目录下,创建以下文件和文件夹结构:
chatgpt-browser-extension/ ├── manifest.json # 扩展清单文件 ├── background.js # 后台脚本 ├── content.js # 内容脚本 ├── popup.html # 弹出页面 HTML ├── popup.js # 弹出页面逻辑 ├── options.html # 选项页面 HTML ├── options.js # 选项页面逻辑 └── icons/ # 扩展图标文件夹 ├── icon16.png ├── icon48.png └── icon128.png你可以先准备几个简单的图标文件(16x16, 48x48, 128x128 像素),或者用占位图片。
3. 编写清单文件与配置权限
manifest.json是扩展的蓝图。我们使用 Manifest V3 版本,它更安全、性能更好。
{ "manifest_version": 3, "name": "ChatGPT 网页助手", "version": "1.0", "description": "使用 ChatGPT 分析当前网页内容或选中的文本。", "permissions": [ "activeTab", "scripting", "storage" ], "host_permissions": [ "https://api.openai.com/*" ], "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"] } ], "action": { "default_popup": "popup.html", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }, "options_page": "options.html", "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }关键配置解释:
"manifest_version": 3:声明使用 Manifest V3。"permissions":"activeTab":允许扩展临时访问当前激活标签页的 URL 和内容。"scripting":允许以编程方式注入脚本(虽然我们通过content_scripts静态注入,但此权限为未来动态注入留有余地)。"storage":允许使用chrome.storageAPI 来安全地存储用户的 API 密钥等数据。
"host_permissions": ["https://api.openai.com/*"]:这是最关键的权限之一。它允许扩展的后台脚本向api.openai.com域名发起网络请求。没有这个权限,调用会因 CORS 或权限错误而失败。"background": { "service_worker": "background.js" }:指定后台脚本文件。在 V3 中,后台脚本以 Service Worker 形式运行。"content_scripts":指定要注入到所有网页 ("<all_urls>") 的内容脚本文件。"action":定义了浏览器工具栏图标的行为,这里指定点击后弹出popup.html。
4. 实现选项页面以安全配置 API 密钥
首先实现选项页面,让用户能够安全地输入和保存他们的 OpenAI API 密钥。
options.html (简化版):
<!DOCTYPE html> <html> <head> <title>ChatGPT 助手设置</title> <style> body { width: 400px; padding: 20px; font-family: sans-serif; } .input-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input[type="password"] { width: 100%; padding: 8px; box-sizing: border-box; } button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer; } #status { margin-top: 10px; color: green; } </style> </head> <body> <h2>API 密钥设置</h2> <div class="input-group"> <label for="apiKey">OpenAI API Key:</label> <input type="password" id="apiKey" placeholder="sk-..."> <p><small>你的密钥仅保存在本地浏览器中,用于向 OpenAI 发起请求。</small></p> </div> <button id="saveBtn">保存设置</button> <div id="status"></div> <script src="options.js"></script> </body> </html>options.js:
document.addEventListener('DOMContentLoaded', function() { const apiKeyInput = document.getElementById('apiKey'); const saveButton = document.getElementById('saveBtn'); const statusDiv = document.getElementById('status'); // 页面加载时,从存储中读取并填充已有的 API Key chrome.storage.local.get(['openaiApiKey'], function(result) { if (result.openaiApiKey) { apiKeyInput.value = result.openaiApiKey; } }); // 保存按钮点击事件 saveButton.addEventListener('click', function() { const apiKey = apiKeyInput.value.trim(); if (!apiKey) { showStatus('请输入有效的 API Key。', 'red'); return; } // 简单验证格式(以 sk- 开头) if (!apiKey.startsWith('sk-')) { showStatus('API Key 格式似乎不正确,请检查。', 'orange'); // 不阻止保存,因为格式未来可能变化 } // 使用 chrome.storage.local 保存 chrome.storage.local.set({ openaiApiKey: apiKey }, function() { if (chrome.runtime.lastError) { showStatus('保存失败:' + chrome.runtime.lastError.message, 'red'); } else { showStatus('设置已保存!', 'green'); } }); }); function showStatus(message, color) { statusDiv.textContent = message; statusDiv.style.color = color; setTimeout(() => { statusDiv.textContent = ''; }, 3000); } });这个页面通过chrome.storage.localAPI 将密钥保存在用户的本地浏览器存储中。后台脚本可以读取这里存储的密钥,而密钥不会暴露在网页的源代码里。
5. 构建后台脚本:处理通信与 API 调用
后台脚本 (background.js) 是扩展的核心,负责与 OpenAI 通信。
// 监听来自内容脚本或弹出页面的消息 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // 判断消息类型 if (request.action === 'callChatGPT') { const { prompt, context } = request.data; callOpenAIChatAPI(prompt, context).then(sendResponse).catch(error => { console.error('API调用失败:', error); sendResponse({ success: false, error: error.message }); }); // 返回 true 表示我们将异步调用 sendResponse return true; } // 可以添加其他 action 的处理逻辑 }); /** * 调用 OpenAI Chat Completions API * @param {string} prompt - 用户的主要指令 * @param {string} context - 上下文信息(如网页内容) * @returns {Promise<Object>} - 解析为包含AI回复的对象 */ async function callOpenAIChatAPI(prompt, context = '') { // 1. 从本地存储获取 API 密钥 const result = await chrome.storage.local.get(['openaiApiKey']); const apiKey = result.openaiApiKey; if (!apiKey) { throw new Error('未设置 OpenAI API 密钥。请右键点击扩展图标,进入“选项”进行设置。'); } // 2. 构造请求消息 const messages = []; if (context) { // 可以将上下文作为系统消息或用户消息的一部分传入 messages.push({ role: 'user', content: `请基于以下内容回答问题:\n\n${context}\n\n问题:${prompt}` }); } else { messages.push({ role: 'user', content: prompt }); } // 3. 准备请求参数 const requestBody = { model: 'gpt-3.5-turbo', // 可根据需要改为 gpt-4 等 messages: messages, max_tokens: 1000, // 控制回复长度 temperature: 0.7, // 控制创造性 }; // 4. 发起 fetch 请求 const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`API 请求失败 (${response.status}): ${errorData.error?.message || response.statusText}`); } const data = await response.json(); // 5. 提取并返回 AI 回复 const aiReply = data.choices[0]?.message?.content?.trim(); if (!aiReply) { throw new Error('API 返回了空回复。'); } return { success: true, reply: aiReply }; }关键点解析:
- 消息监听:
chrome.runtime.onMessage.addListener是扩展内部组件通信的枢纽。内容脚本或弹出页面通过chrome.runtime.sendMessage发送消息,后台脚本在这里接收并处理。 - 异步处理与
return true:因为callOpenAIChatAPI是异步函数,我们需要return true来告诉 Chrome 我们将异步调用sendResponse函数。否则,消息通道会在监听函数返回后立即关闭。 - 密钥安全获取:通过
chrome.storage.local.get从本地存储读取密钥,避免了在前端代码中暴露。 - API 请求构造:严格遵循 OpenAI Chat Completions API 的格式,设置
model、messages、max_tokens等参数。 - 错误处理:对网络错误、API 返回错误、空回复等情况都进行了处理,并将错误信息通过
sendResponse传回调用方,便于前端展示。
6. 开发内容脚本与用户交互界面
内容脚本需要获取页面信息,并提供用户交互的入口。这里我们实现两种方式:右键上下文菜单和与弹出页面配合。
6.1 创建内容脚本获取页面信息
content.js:
// 监听来自弹出页面或后台脚本的消息 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'getPageContent') { // 获取当前页面基本信息 const pageInfo = { url: window.location.href, title: document.title, selectedText: window.getSelection().toString().trim(), // 可以尝试获取更简洁的页面正文,这里是一个简单示例 bodyText: document.body.innerText.substring(0, 5000) // 限制长度 }; sendResponse({ success: true, data: pageInfo }); } // 监听其他 action... }); // 可以主动向后台脚本发送消息(例如,当页面加载完成时) // window.addEventListener('load', () => { // chrome.runtime.sendMessage({action: 'pageLoaded'}); // });这个脚本主要充当数据提供者。当弹出页面需要当前网页信息时,会向内容脚本发送getPageContent消息。
6.2 实现弹出页面作为主要操作界面
popup.html:
<!DOCTYPE html> <html> <head> <title>ChatGPT 助手</title> <style> body { width: 350px; padding: 15px; font-family: sans-serif; } textarea, input { width: 100%; box-sizing: border-box; margin-bottom: 10px; padding: 8px; } button { width: 100%; padding: 10px; margin-bottom: 5px; background-color: #007bff; color: white; border: none; cursor: pointer; } button:disabled { background-color: #ccc; } #result { margin-top: 15px; padding: 10px; border: 1px solid #ddd; background-color: #f9f9f9; white-space: pre-wrap; max-height: 300px; overflow-y: auto; } .status { font-size: 0.9em; color: #666; margin-bottom: 10px; } .error { color: #d9534f; } .success { color: #5cb85c; } </style> </head> <body> <h3>分析当前网页</h3> <div class="status" id="pageStatus">正在获取页面信息...</div> <textarea id="customPrompt" rows="3" placeholder="请输入你想问的问题(例如:总结这篇文章)">请总结这个网页的主要内容。</textarea> <button id="analyzeBtn">发送到 ChatGPT</button> <div id="result"></div> <p><small><a href="#" id="optionsLink">设置 API 密钥</a></small></p> <script src="popup.js"></script> </body> </html>popup.js:
document.addEventListener('DOMContentLoaded', async function() { const pageStatus = document.getElementById('pageStatus'); const customPrompt = document.getElementById('customPrompt'); const analyzeBtn = document.getElementById('analyzeBtn'); const resultDiv = document.getElementById('result'); const optionsLink = document.getElementById('optionsLink'); let currentPageInfo = null; // 1. 弹出页面打开时,立即获取当前标签页的信息 try { // 获取当前活跃的标签页 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); // 向该标签页的内容脚本发送消息,获取页面内容 const response = await chrome.tabs.sendMessage(tab.id, { action: 'getPageContent' }); if (response && response.success) { currentPageInfo = response.data; pageStatus.textContent = `已就绪:${currentPageInfo.title}`; pageStatus.className = 'status success'; } else { throw new Error('无法从页面获取内容。'); } } catch (error) { console.error('获取页面信息失败:', error); pageStatus.textContent = '无法获取页面信息。请刷新页面或确保扩展有权访问此页面。'; pageStatus.className = 'status error'; analyzeBtn.disabled = true; } // 2. 发送分析请求 analyzeBtn.addEventListener('click', async () => { if (!currentPageInfo) { showResult('错误:无页面信息。', true); return; } const prompt = customPrompt.value.trim() || '请总结这个网页的主要内容。'; analyzeBtn.disabled = true; analyzeBtn.textContent = '思考中...'; resultDiv.textContent = ''; try { // 将用户指令和页面上下文发送给后台脚本 const response = await chrome.runtime.sendMessage({ action: 'callChatGPT', data: { prompt: prompt, context: `网页标题:${currentPageInfo.title}\n网页URL:${currentPageInfo.url}\n网页正文(部分):${currentPageInfo.bodyText}` } }); if (response.success) { showResult(response.reply, false); } else { showResult(`错误:${response.error}`, true); } } catch (error) { console.error('通信失败:', error); showResult(`请求失败:${error.message}`, true); } finally { analyzeBtn.disabled = false; analyzeBtn.textContent = '发送到 ChatGPT'; } }); // 3. 打开选项页面 optionsLink.addEventListener('click', (e) => { e.preventDefault(); chrome.runtime.openOptionsPage(); }); function showResult(text, isError) { resultDiv.textContent = text; resultDiv.style.color = isError ? '#d9534f' : '#333'; resultDiv.style.fontWeight = isError ? 'bold' : 'normal'; } });交互流程详解:
popup.js加载:当用户点击扩展图标,popup.html被打开,popup.js执行。- 获取当前标签页:
chrome.tabs.query用于获取当前窗口下激活的标签页对象。 - 与内容脚本通信:
chrome.tabs.sendMessage向特定标签页(通过tab.id指定)的内容脚本发送消息。内容脚本 (content.js) 中的监听器收到getPageContent消息后,收集页面信息并返回。 - 与后台脚本通信:当用户点击“发送”按钮,
popup.js使用chrome.runtime.sendMessage将用户指令和页面上下文发送给后台脚本 (background.js)。 - 处理响应:后台脚本调用 OpenAI API 并返回结果,
popup.js将结果显示在弹出窗口中。
7. 加载、测试与调试
7.1 加载扩展程序
- 打开 Chrome 浏览器,进入
chrome://extensions/。 - 打开右上角的“开发者模式”开关。
- 点击“加载已解压的扩展程序”按钮。
- 选择你创建的
chatgpt-browser-extension项目文件夹。 - 扩展程序应该会出现在列表中,并显示在浏览器工具栏。
7.2 测试流程
- 设置 API 密钥:右键点击工具栏上的扩展图标,选择“选项”。在打开的选项页面中输入你的 OpenAI API 密钥并保存。
- 打开一个网页:例如,一篇新闻文章。
- 点击扩展图标:弹出窗口应显示“已就绪:[网页标题]”。
- 发送请求:在文本框中输入问题(或使用默认的总结问题),点击“发送到 ChatGPT”。
- 查看结果:等待几秒后,AI 的回复应该会显示在结果框中。
7.3 常见问题排查
在开发过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 检查与解决步骤 |
|---|---|---|
| 扩展图标不显示或无法点击 | manifest.json格式错误或关键文件缺失。 | 1. 检查chrome://extensions/页面,扩展列表下是否有错误信息。2. 检查 manifest.json的 JSON 格式是否正确(可使用 JSON 验证工具)。3. 确认 popup.html、background.js等文件路径与manifest.json中声明的一致。 |
| 弹出页面显示“无法获取页面信息” | 内容脚本未成功注入或通信失败。 | 1. 在目标网页上右键 -> “检查”,打开开发者工具,切换到 Console 标签页,查看是否有来自内容脚本的错误。 2. 在 Console 中输入 chrome.runtime,看是否可用,以确认内容脚本已注入。3. 检查 manifest.json中content_scripts的matches字段是否包含了当前网页的 URL 模式。 |
| 点击“发送”后无反应或报错“未设置 API 密钥” | API 密钥未保存或后台脚本读取失败。 | 1. 确认已在选项页面成功保存密钥(保存后应有成功提示)。 2. 在 chrome://extensions/页面找到你的扩展,点击“service worker”链接进入后台脚本的控制台,查看console.error输出。3. 在后台脚本的 callOpenAIChatAPI函数开始处添加console.log('API Key:', apiKey),检查是否成功读取。 |
| 请求失败,控制台显示网络错误或 CORS 错误 | 缺少host_permissions或 API 密钥无效。 | 1.首要检查:确认manifest.json中已正确声明"host_permissions": ["https://api.openai.com/*"]。2. 在后台脚本的 Service Worker 控制台,查看 fetch请求的详细错误信息。如果是 401,通常是 API 密钥错误;如果是 429,可能是达到速率限制。3. 前往 OpenAI API 使用情况页面 检查额度。 |
| 弹出页面在点击按钮后卡住,然后按钮恢复但无结果 | 后台脚本的异步消息处理未正确返回true。 | 确保background.js中的chrome.runtime.onMessage监听器在发起异步操作(如callOpenAIChatAPI)时,最后一行有return true;。 |
调试技巧:
- 后台脚本:在
chrome://extensions/页面,找到你的扩展,点击“service worker”旁边的链接,会打开一个独立的开发者工具窗口。 - 弹出页面:右键点击扩展图标弹出的窗口,选择“检查”。
- 内容脚本:在目标网页上按 F12 打开开发者工具,其 Console 和 Sources 面板中可以看到内容脚本的日志和代码。
8. 生产环境注意事项与扩展方向
一个能在学习环境运行的原型,与一个健壮、可投入实际使用的扩展之间,还有不少差距。
8.1 安全与隐私最佳实践
- 永远不要硬编码 API 密钥:本文的方案(存储在
chrome.storage.local)是基础做法。对于团队或分发,应考虑更安全的方案,如通过你的后端服务器中转请求,由服务器持有密钥,扩展只与你的服务器通信。 - 最小权限原则:
manifest.json中的permissions和host_permissions只声明真正需要的。例如,如果功能不需要修改页面,就不要申请activeTab或scripting。 - 内容脚本的谨慎操作:内容脚本能访问页面 DOM,要避免执行可能破坏页面功能或引发安全风险的代码。获取的页面内容应仅限于功能所需,并告知用户。
- 隐私政策:如果你的扩展会收集或发送用户数据(即使是发送到 OpenAI),应考虑提供隐私政策说明。
8.2 功能增强与优化
- 添加上下文菜单:让用户可以直接在网页上选中文本,右键调用 ChatGPT。
// 在 background.js 中 chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: "chatgptAnalyze", title: "使用 ChatGPT 分析", contexts: ["selection"] // 仅在选中文本时显示 }); }); chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "chatgptAnalyze") { // 处理选中的文本 chrome.tabs.sendMessage(tab.id, {action: 'analyzeSelection', text: info.selectionText}); } }); - 支持流式响应:OpenAI API 支持 Server-Sent Events (SSE) 流式传输。你可以修改后台脚本和前端,实现打字机效果,提升用户体验。
- 模型与参数配置:在选项页面中,允许用户选择模型(如 gpt-3.5-turbo, gpt-4)、设置
temperature、max_tokens等。 - 对话历史与持久化:使用
chrome.storage.local或chrome.storage.session保存对话历史,实现多轮对话。 - 错误处理与用户反馈:提供更友好的错误提示,如网络超时、额度不足、内容过长等。
- 国际化:使用
chrome.i18nAPI 支持多语言。
8.3 发布前检查清单
在考虑将扩展发布到 Chrome 网上应用店前,请完成以下检查:
- [ ] 图标齐全且符合尺寸要求(16, 48, 128像素)。
- [ ]
manifest.json中的name、description、version准确无误。 - [ ] 所有声明的权限都是功能必需的,并在描述中说明用途。
- [ ] 已移除所有调试用的
console.log语句(或至少移除敏感信息)。 - [ ] 选项页面清晰说明了 API 密钥的用途和存储方式。
- [ ] 在多种类型网页(简单页、复杂SPA、PDF查看器等)上进行了基本功能测试。
- [ ] 阅读并遵守 Chrome 网上应用店开发者计划政策 。
通过以上步骤,你不仅构建了一个可用的 ChatGPT 浏览器扩展,更掌握了 Chrome 扩展开发的核心模式:清单配置、权限管理、后台脚本与内容脚本的通信、安全存储以及与外部的 API 集成。这个模式可以复用于集成其他 AI 服务或任何需要与网页内容交互的浏览器自动化工具。