news 2026/10/30 6:19:04

Chrome扩展开发实战:如何集成ChatGPT API实现智能交互

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Chrome扩展开发实战:如何集成ChatGPT API实现智能交互

Chrome扩展开发实战:如何集成ChatGPT API实现智能交互

在当前的浏览器生态中,用户对插件的期待早已超越了简单的页面修改或信息抓取,他们希望插件能“理解”需求并“智能”响应。然而,大多数传统浏览器插件仍停留在规则匹配或简单交互的层面,缺乏真正的智能对话能力。这种现状导致用户体验割裂,无法在浏览网页时获得即时、深度的信息辅助。将大型语言模型(如ChatGPT)的能力嵌入浏览器扩展,成为解决这一痛点的关键路径。

1. 技术选型:Browser API与第三方SDK的权衡

在决定如何集成ChatGPT API时,开发者首先面临技术栈的选择。核心考量在于通信的稳定性、流式响应的支持以及开发复杂度。

  • Browser API (fetch/XMLHttpRequest): 这是最直接的方式,通过Chrome扩展的background script或content script直接调用OpenAI的API。优势在于控制力强,无需引入额外依赖,可以精细处理请求头、错误重试和流式响应。对于需要处理Streaming Response(流式响应)的场景,使用fetchAPI配合ReadableStream是标准做法,可以逐词接收AI回复,提升用户体验。
  • 官方/第三方JavaScript SDK: 例如openainpm包。这类SDK封装了认证、请求构造和错误处理,能显著降低开发成本。但在Chrome扩展的上下文中使用需注意:MV3扩展的Service Worker环境对某些Node.js风格的模块支持有限,且SDK可能包含不必要的polyfill,增大扩展体积。更重要的是,处理流式响应时,SDK的封装可能不如直接使用fetchAPI灵活。

对于追求轻量、高性能和完全控制的扩展,建议优先使用Browser API。若扩展逻辑复杂,且对流式响应要求不高,则可考虑使用经过Tree Shaking的SDK精简版本。

2. 核心实现:从配置到会话管理

2.1 清单文件(manifest.json)关键配置

扩展的“身份证”manifest.json需要精心配置,以声明必要的权限和安全策略。

{ "manifest_version": 3, "name": "智能浏览器助手", "version": "1.0", "description": "集成ChatGPT的智能交互扩展", "permissions": [ "storage", // 用于安全存储API密钥 "activeTab" // 可选,用于与当前标签页交互 ], "host_permissions": [ "https://api.openai.com/*" // 必须声明要访问的API域名 ], "background": { "service_worker": "src/background.js", "type": "module" // 允许使用ES模块 }, "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://api.openai.com;" // 严格限制脚本来源,只允许连接到自身和OpenAI API }, "action": { "default_popup": "popup.html" } }

关键点在于host_permissions和content_security_policy。前者明确告知用户扩展将访问的第三方服务;后者是MV3扩展的安全基石,能有效防止恶意脚本注入,其中connect-src必须包含OpenAI的API端点。

2.2 实现带指数退避的重试机制

网络请求不稳定或API限流是常态。一个健壮的重试机制至关重要,指数退避能避免在服务短暂故障时加剧其压力。

// src/apiClient.js /** * 使用指数退避策略调用API * @param {string} url - API端点 * @param {RequestInit} options - 请求配置 * @param {number} maxRetries - 最大重试次数,默认3 * @param {number} baseDelay - 基础退避延迟(ms),默认1000 * @returns {Promise<Response>} - API响应 */ async function fetchWithExponentialBackoff(url, options, maxRetries = 3, baseDelay = 1000) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); // 处理特定状态码:429(过多请求)和5xx(服务器错误)应重试 if (response.status === 429 || response.status >= 500) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // 如果响应OK,或非重试错误(如4xx客户端错误),直接返回 if (response.ok || (response.status >= 400 && response.status < 500)) { return response; } } catch (error) { lastError = error; console.warn(`API调用失败 (尝试 ${attempt + 1}/${maxRetries + 1}):`, error.message); // 如果已达最大重试次数,不再等待,直接抛出错误 if (attempt === maxRetries) { break; } // 计算指数退避延迟,并加上随机抖动以避免多个客户端同步重试 const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000; console.log(`等待 ${delay.toFixed(0)}ms 后重试...`); await new Promise(resolve => setTimeout(resolve, delay)); } } // 所有重试都失败后,抛出最后的错误 throw new Error(`API请求失败,已重试${maxRetries}次。最后错误: ${lastError?.message}`); } // 使用示例:调用ChatGPT Completions API async function getChatCompletion(apiKey, messages) { const response = await fetchWithExponentialBackoff( 'https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'gpt-3.5-turbo', messages: messages, stream: true // 启用流式响应 }) } ); // 处理流式响应 if (response.body && response.ok) { return await handleStreamResponse(response.body); } return response.json(); }

此实现通过Promise链清晰地分离了请求、状态判断、错误捕获和重试逻辑。注意,对于401(未授权)或400(错误请求)等客户端错误,我们选择不重试,因为立即重试通常无法解决问题。

2.3 上下文保持的会话管理方案

为了进行连贯的多轮对话,扩展需要维护会话上下文。由于Service Worker可能被浏览器暂停或终止,持久化存储是必须的。

// src/sessionManager.js const SESSION_STORAGE_KEY = 'chatgpt_session_context'; const MAX_CONTEXT_LENGTH = 10; // 限制保存的对话轮数,以控制Token消耗 class SessionManager { constructor() { this.messages = []; } async initialize() { // 从chrome.storage中加载历史会话 const data = await chrome.storage.local.get([SESSION_STORAGE_KEY]); if (data[SESSION_STORAGE_KEY]) { this.messages = data[SESSION_STORAGE_KEY]; console.log('会话历史已加载,共', this.messages.length, '条消息'); } } /** * 添加用户消息和AI回复到会话上下文 * @param {string} userInput - 用户输入 * @param {string} aiResponse - AI回复 */ async addInteraction(userInput, aiResponse) { this.messages.push( { role: 'user', content: userInput }, { role: 'assistant', content: aiResponse } ); // 保持上下文长度不超过限制,移除最早的消息对 while (this.messages.length > MAX_CONTEXT_LENGTH * 2) { this.messages.splice(0, 2); } // 持久化到存储 await this.persist(); } /** * 获取当前用于API调用的消息数组 * 可以在此处添加系统指令(system message) * @returns {Array} - 消息数组 */ getContextForApi() { const systemMessage = { role: 'system', content: '你是一个有帮助的浏览器助手。回答应简洁明了。' }; return [systemMessage, ...this.messages]; } /** * 清空当前会话 */ async clear() { this.messages = []; await this.persist(); } async persist() { await chrome.storage.local.set({ [SESSION_STORAGE_KEY]: this.messages }); } } // 在background.js中使用 const sessionManager = new SessionManager(); await sessionManager.initialize(); // 当用户发送消息时 async function handleUserMessage(inputText) { const context = sessionManager.getContextForApi(); // ... 调用API获取回复 const aiReply = await getChatCompletion(apiKey, context); // 保存交互到会话 await sessionManager.addInteraction(inputText, aiReply); return aiReply; }

此方案将会话状态存储在chrome.storage.local中,即使扩展更新或浏览器重启,对话历史也能得以保留。通过限制保存的消息数量,有效管理了API调用的Token消耗和存储空间。

3. 性能优化策略

3.1 减少API调用次数的本地缓存策略

对于常见、确定性高的查询(例如“解释某个术语”),重复调用API是浪费的。可以实现一个简单的缓存层。

// src/cacheManager.js const RESPONSE_CACHE_KEY = 'api_response_cache'; const CACHE_TTL = 30 * 60 * 1000; // 缓存30分钟 class CacheManager { constructor() { this.cache = new Map(); } async load() { const data = await chrome.storage.local.get([RESPONSE_CACHE_KEY]); if (data[RESPONSE_CACHE_KEY]) { const entries = JSON.parse(data[RESPONSE_CACHE_KEY]); const now = Date.now(); // 过滤掉已过期的缓存项 for (const [key, { value, expiry }] of entries) { if (expiry > now) { this.cache.set(key, value); } } } } /** * 生成查询的缓存键(简单哈希) * @param {string} query - 用户查询 * @param {Array} context - 会话上下文 * @returns {string} - 缓存键 */ generateKey(query, context = []) { const contextString = context.map(m => m.content).join('|'); return `${query}|${contextString}`; } get(key) { const item = this.cache.get(key); if (item) { console.log('缓存命中:', key.substring(0, 50)); return item; } return null; } set(key, value) { this.cache.set(key, value); // 异步持久化,不阻塞主线程 this.persistLater(); } async persist() { const entries = []; const now = Date.now(); for (const [key, value] of this.cache.entries()) { entries.push([key, { value, expiry: now + CACHE_TTL }]); } await chrome.storage.local.set({ [RESPONSE_CACHE_KEY]: JSON.stringify(entries) }); } // 防抖持久化 persistLater = debounce(() => this.persist(), 5000); } // 在API调用前检查缓存 const cacheManager = new CacheManager(); await cacheManager.load(); async function getCachedOrFetch(query, context) { const cacheKey = cacheManager.generateKey(query, context); const cached = cacheManager.get(cacheKey); if (cached) { return cached; } const freshResponse = await fetchFromAPI(query, context); cacheManager.set(cacheKey, freshResponse); return freshResponse; }

3.2 使用Web Worker处理长文本的分块传输

当用户粘贴大段文本进行分析时,直接发送可能超出API Token限制。可以在前端进行预处理。

// src/worker.js (Web Worker文件) self.onmessage = function(event) { const { text, maxChunkSize } = event.data; // 简单的按句子和最大长度分块算法 const chunks = splitTextIntoChunks(text, maxChunkSize); self.postMessage({ chunks: chunks, totalChunks: chunks.length }); }; function splitTextIntoChunks(text, maxChunkSize) { const sentences = text.match(/[^.!?]+[.!?]+/g) || [text]; const chunks = []; let currentChunk = ''; for (const sentence of sentences) { if ((currentChunk + sentence).length <= maxChunkSize) { currentChunk += sentence; } else { if (currentChunk) chunks.push(currentChunk.trim()); // 如果单句就超过最大块大小,则强制按字符分割 if (sentence.length > maxChunkSize) { for (let i = 0; i < sentence.length; i += maxChunkSize) { chunks.push(sentence.substring(i, i + maxChunkSize).trim()); } currentChunk = ''; } else { currentChunk = sentence; } } } if (currentChunk) { chunks.push(currentChunk.trim()); } return chunks; } // 在主线程中调用Worker function processLongTextWithWorker(text) { return new Promise((resolve) => { const worker = new Worker('src/worker.js'); worker.postMessage({ text, maxChunkSize: 2000 }); // 假设每块最多2000字符 worker.onmessage = (event) => { const { chunks } = event.data; worker.terminate(); resolve(chunks); }; }); } // 使用示例:分块发送并汇总结果 async function summarizeLongText(text) { const chunks = await processLongTextWithWorker(text); const summaries = []; for (const chunk of chunks) { const summary = await getChatCompletion(apiKey, [ { role: 'user', content: `请总结以下文本的核心内容:${chunk}` } ]); summaries.push(summary); } // 最后,对所有的分块总结再进行一次总结 const finalSummary = await getChatCompletion(apiKey, [ { role: 'user', content: `基于以下分段总结,给出一个整体摘要:\n${summaries.join('\n')}` } ]); return finalSummary; }

Web Worker将耗时的文本分块计算移出主线程,防止页面卡顿。分块策略可以按句子、段落或固定长度进行,以保持语义完整性。

4. 安全防护措施

4.1 API密钥的安全存储方案

将API密钥硬编码在扩展代码中是严重的安全风险。应使用chrome.storage.local配合加密存储。

// src/encryption.js // 注意:前端加密不能完全替代服务器端保密,但能增加一层防护 class SimpleEncryptor { constructor() { // 使用扩展ID和用户特定信息派生一个基本密钥(实际生产环境应更复杂) this.deriveKey = async () => { const encoder = new TextEncoder(); const baseKey = encoder.encode(chrome.runtime.id + '|' + navigator.userAgent); const hashBuffer = await crypto.subtle.digest('SHA-256', baseKey); return hashBuffer.slice(0, 32); // 使用前32字节作为AES-GCM密钥 }; } async encrypt(plaintext) { const keyMaterial = await this.deriveKey(); const iv = crypto.getRandomValues(new Uint8Array(12)); // 96位IV const encoder = new TextEncoder(); const data = encoder.encode(plaintext); const cryptoKey = await crypto.subtle.importKey( 'raw', keyMaterial, { name: 'AES-GCM' }, false, ['encrypt'] ); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, cryptoKey, data ); // 将IV和密文一起存储 const combined = new Uint8Array(iv.length + encrypted.byteLength); combined.set(iv); combined.set(new Uint8Array(encrypted), iv.length); return btoa(String.fromCharCode(...combined)); // 转换为Base64 } async decrypt(ciphertextBase64) { try { const keyMaterial = await this.deriveKey(); const combined = Uint8Array.from(atob(ciphertextBase64), c => c.charCodeAt(0)); const iv = combined.slice(0, 12); const encrypted = combined.slice(12); const cryptoKey = await crypto.subtle.importKey( 'raw', keyMaterial, { name: 'AES-GCM' }, false, ['decrypt'] ); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, cryptoKey, encrypted ); const decoder = new TextDecoder(); return decoder.decode(decrypted); } catch (error) { console.error('解密失败:', error); return null; } } } // 安全存储管理器 class SecureStorage { constructor() { this.encryptor = new SimpleEncryptor(); this.API_KEY_STORAGE_KEY = 'encrypted_api_key'; } async saveApiKey(apiKey) { if (!apiKey || apiKey.trim() === '') { throw new Error('API密钥不能为空'); } const encrypted = await this.encryptor.encrypt(apiKey.trim()); await chrome.storage.local.set({ [this.API_KEY_STORAGE_KEY]: encrypted }); console.log('API密钥已加密存储'); } async getApiKey() { const result = await chrome.storage.local.get([this.API_KEY_STORAGE_KEY]); const encrypted = result[this.API_KEY_STORAGE_KEY]; if (!encrypted) { return null; } const decrypted = await this.encryptor.decrypt(encrypted); return decrypted; } async clearApiKey() { await chrome.storage.local.remove([this.API_KEY_STORAGE_KEY]); } } // 在Popup或Options页面中使用 const secureStorage = new SecureStorage(); // 用户输入API密钥后 document.getElementById('save-key-btn').addEventListener('click', async () => { const apiKeyInput = document.getElementById('api-key-input').value; try { await secureStorage.saveApiKey(apiKeyInput); alert('API密钥保存成功!'); } catch (error) { alert(`保存失败: ${error.message}`); } }); // 在background script中获取密钥 async function getStoredApiKey() { const key = await secureStorage.getApiKey(); if (!key) { // 提示用户设置密钥 await chrome.runtime.openOptionsPage(); throw new Error('请先配置API密钥'); } return key; }

此方案使用Web Crypto API进行客户端加密,密钥由运行时信息派生。虽然无法防御拥有设备完全访问权的攻击者,但能防止通过简单检查扩展存储来窃取密钥的行为。

4.2 用户输入内容的XSS过滤

扩展接收的用户输入在显示前必须进行净化,防止跨站脚本攻击。

// src/xssFilter.js /** * 简单的XSS过滤函数 * @param {string} str - 待过滤的字符串 * @returns {string} - 过滤后的安全字符串 */ function sanitizeInput(str) { if (typeof str !== 'string') return ''; const div = document.createElement('div'); div.textContent = str; // 使用textContent而非innerHTML来自动转义 let sanitized = div.innerHTML; // 额外移除可能危险的属性(如果之后允许某些HTML标签的话) // 对于纯文本显示,上面的textContent已经足够 return sanitized; } /** * 更严格的内容安全策略:只允许纯文本,完全剥离HTML标签 * @param {string} str - 输入字符串 * @returns {string} - 纯文本 */ function stripAllTags(str) { return str.replace(/<[^>]*>/g, ''); } /** * 在将用户输入插入DOM前使用 * @param {string} userInput - 原始用户输入 * @param {HTMLElement} targetElement - 目标DOM元素 */ function safelySetContent(userInput, targetElement) { const cleanInput = sanitizeInput(userInput); // 或者使用 stripAllTags(userInput) 如果确定不需要任何HTML // 使用innerText或textContent而不是innerHTML targetElement.textContent = cleanInput; // 如果确实需要渲染AI返回的简单格式化文本(如加粗*text*), // 应使用专门的、安全的Markdown解析器,并白名单化允许的标签和属性 } // 示例:安全的Markdown渲染(极简版,仅处理粗体和斜体) function safeMarkdownRender(text) { let safeText = sanitizeInput(text); // 只允许非常有限的Markdown语法,并转换为安全的HTML safeText = safeText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); safeText = safeText.replace(/\*(.*?)\*/g, '<em>$1</em>'); // 确保转换后的标签属性安全 const tempDiv = document.createElement('div'); tempDiv.innerHTML = safeText; // 移除所有非白名单的属性 const allElements = tempDiv.getElementsByTagName('*'); for (const el of allElements) { const allowedAttrs = []; for (const attr of el.attributes) { if (attr.name === 'class' || attr.name === 'style') { // 可以进一步过滤style和class的值 allowedAttrs.push({ name: attr.name, value: attr.value }); } } // 清空所有属性,然后只添加允许的 while (el.attributes.length > 0) { el.removeAttribute(el.attributes[0].name); } for (const allowed of allowedAttrs) { el.setAttribute(allowed.name, allowed.value); } } return tempDiv.innerHTML; }

永远不要信任用户输入或第三方API返回的数据。即使数据来自ChatGPT,也应进行过滤,因为理论上提示词注入可能使其返回恶意内容。

5. 避坑指南与最佳实践

5.1 免费API配额耗尽时的优雅降级

当遇到API限额或配额耗尽时,应有备用方案保证基本功能可用。

// src/fallbackManager.js class FallbackManager { constructor() { this.useFallback = false; this.fallbackCache = new Map(); this.initFallbackResponses(); } initFallbackResponses() { // 预定义一些常见查询的回复 this.fallbackCache.set('你好', '你好!我是你的浏览器助手。目前AI服务暂时不可用,但我可以帮你处理一些本地任务。'); this.fallbackCache.set('帮助', '我可以帮你:\n1. 解释页面上的术语(离线词典)\n2. 高亮文本\n3. 保存笔记到本地\n\nAI功能恢复后,我将能回答更复杂的问题。'); // ... 更多预定义回复 } /** * 检查错误类型,决定是否启用降级 * @param {Error} error - API调用错误 */ checkErrorAndSetFallback(error) { // 429: 请求过多, 402: 需要付款, 503: 服务不可用 if (error.message.includes('429') || error.message.includes('402') || error.message.includes('503')) { this.useFallback = true; console.warn('API限制触发,启用降级模式'); // 可以设置一个定时器,一段时间后尝试恢复 setTimeout(() => { this.useFallback = false; console.log('尝试退出降级模式'); }, 30 * 60 * 1000); // 30分钟后尝试恢复 return true; } return false; } /** * 获取降级回复 * @param {string} query - 用户查询 * @returns {string} - 降级回复或null */ getFallbackResponse(query) { if (!this.useFallback) return null; // 1. 检查精确匹配 if (this.fallbackCache.has(query)) { return this.fallbackCache.get(query); } // 2. 模糊匹配:检查查询是否包含关键词 const lowerQuery = query.toLowerCase(); for (const [key, response] of this.fallbackCache.entries()) { if (lowerQuery.includes(key.toLowerCase())) { return response; } } // 3. 默认回复 return '抱歉,AI服务暂时不可用。你可以尝试:\n- 刷新页面后重试\n- 检查你的API密钥和配额\n- 使用本地功能如文本高亮或笔记'; } } // 在API调用函数中集成降级逻辑 const fallbackManager = new FallbackManager(); async function getResponseWithFallback(query, context) { try { if (fallbackManager.useFallback) { const fallback = fallbackManager.getFallbackResponse(query); if (fallback) return fallback; } const apiKey = await getStoredApiKey(); const response = await getChatCompletion(apiKey, context); return response; } catch (error) { console.error('API调用失败:', error); // 检查是否应启用降级模式 const shouldFallback = fallbackManager.checkErrorAndSetFallback(error); if (shouldFallback) { const fallback = fallbackManager.getFallbackResponse(query); if (fallback) return fallback; } // 如果不应降级或没有降级回复,则抛出原始错误 throw error; } }

5.2 Chrome MV3的Service Worker生命周期管理

MV3中,background pages被Service Worker取代,它们在不活动时会被浏览器暂停或终止,需要妥善管理状态。

// src/background.js (Service Worker) // 全局状态应持久化到chrome.storage,而不是内存变量 let sessionManager; let cacheManager; // 安装/激活事件 chrome.runtime.onInstalled.addListener(() => { console.log('扩展安装/更新'); initializeServices(); }); // 启动时初始化 async function initializeServices() { sessionManager = new SessionManager(); await sessionManager.initialize(); cacheManager = new CacheManager(); await cacheManager.load(); console.log('后台服务初始化完成'); } // 监听扩展图标点击(保持Service Worker活跃的一种方式) chrome.action.onClicked.addListener((tab) => { // 重新初始化,确保状态是最新的 initializeServices().then(() => { // 打开弹出窗口或执行其他操作 chrome.action.setPopup({ popup: 'popup.html' }); }); }); // 定期唤醒Service Worker,防止被终止 const KEEP_ALIVE_INTERVAL = 4 * 60 * 1000; // 4分钟(小于5分钟终止阈值) function setupKeepAlive() { // 方法1:定期发送消息(简单但有效) setInterval(() => { // 只是一个空操作,保持SW活跃 console.log('Service Worker保持活跃心跳'); }, KEEP_ALIVE_INTERVAL); // 方法2:监听特定事件(更节能) chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { // 处理来自content script或popup的消息 if (message.type === 'ping') { sendResponse({ pong: Date.now() }); return true; // 保持消息通道开放,用于异步响应 } }); } // 启动保持活跃机制 setupKeepAlive(); // 重要:在Service Worker即将终止时保存状态 self.addEventListener('beforeterminate', async () => { console.log('Service Worker即将终止,保存状态...'); if (sessionManager) await sessionManager.persist(); if (cacheManager) await cacheManager.persist(); }); // 注意:Chrome可能不会总是触发beforeterminate,所以重要状态应实时保存

6. 完整TypeScript实现示例与测试要点

以下是一个集成了上述功能的TypeScript核心模块示例:

// src/core/ChatGPTExtension.ts import { SessionManager } from './SessionManager'; import { SecureStorage } from './SecureStorage'; import { FallbackManager } from './FallbackManager'; export interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string; timestamp?: number; } export interface ApiConfig { apiKey: string; endpoint?: string; model?: string; maxTokens?: number; } /** * ChatGPT浏览器扩展核心类 * @class ChatGPTExtension */ export class ChatGPTExtension { private sessionManager: SessionManager; private secureStorage: SecureStorage; private fallbackManager: FallbackManager; private apiConfig: ApiConfig | null = null; private isInitialized = false; constructor() { this.sessionManager = new SessionManager(); this.secureStorage = new SecureStorage(); this.fallbackManager = new FallbackManager(); } /** * 初始化扩展,加载配置和会话 * @returns {Promise<void>} */ async initialize(): Promise<void> { if (this.isInitialized) return; try { // 加载API配置 const apiKey = await this.secureStorage.getApiKey(); if (apiKey) { this.apiConfig = { apiKey, endpoint: 'https://api.openai.com/v1/chat/completions', model: 'gpt-3.5-turbo', maxTokens: 1000 }; } // 初始化会话管理器 await this.sessionManager.initialize(); this.isInitialized = true; console.log('ChatGPT扩展初始化完成'); } catch (error) { console.error('初始化失败:', error); throw error; } } /** * 处理用户消息并获取AI回复 * @param {string} userInput - 用户输入文本 * @param {boolean} useStreaming - 是否使用流式响应 * @returns {Promise<string>} AI回复内容 */ async processMessage(userInput: string, useStreaming = false): Promise<string> { if (!this.isInitialized) { await this.initialize(); } // 安全检查:过滤用户输入 const sanitizedInput = this.sanitizeInput(userInput); if (!sanitizedInput.trim()) { throw new Error('输入内容无效或为空'); } // 检查降级模式 if (this.fallbackManager.isInFallbackMode()) { const fallbackResponse = this.fallbackManager.getResponse(sanitizedInput); if (fallbackResponse) { return fallbackResponse; } } // 准备API请求 if (!this.apiConfig?.apiKey) { throw new Error('未配置API密钥,请先在扩展设置中添加'); } const context = this.sessionManager.getContext(); const messages: ChatMessage[] = [ { role: 'system', content: '你是一个有帮助的浏览器助手。回答应简洁、准确。' }, ...context, { role: 'user', content: sanitizedInput } ]; try { let aiResponse: string; if (useStreaming) { aiResponse = await this.fetchWithStreaming(messages); } else { aiResponse = await this.fetchStandard(messages); } // 保存到会话历史 await this.sessionManager.addInteraction(sanitizedInput, aiResponse); return aiResponse; } catch (error: any) { console.error('API调用失败:', error); // 检查是否应进入降级模式 this.fallbackManager.evaluateError(error); // 尝试获取降级回复 const fallback = this.fallbackManager.getResponse(sanitizedInput); if (fallback) { return fallback; } throw new Error(`处理消息失败: ${error.message}`); } } /** * 使用流式API获取响应 * @private */ private async fetchWithStreaming(messages: ChatMessage[]): Promise<string> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时 try { const response = await fetch(this.apiConfig!.endpoint!, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiConfig!.apiKey}` }, body: JSON.stringify({ model: this.apiConfig!.model, messages: messages, max_tokens: this.apiConfig!.maxTokens, stream: true, temperature: 0.7 }), signal: controller.signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('无法读取流式响应'); } const decoder = new TextDecoder(); let fullResponse = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { clearTimeout(timeoutId); return fullResponse; } try { const parsed = JSON.parse(data); const content = parsed.choices[0]?.delta?.content || ''; if (content) { fullResponse += content; // 可以在这里触发更新UI的事件 this.dispatchStreamUpdate(content); } } catch (e) { console.warn('解析流数据失败:', e); } } } } clearTimeout(timeoutId); return fullResponse; } catch (error) { clearTimeout(timeoutId); throw error; } } /** * 标准API请求 * @private */ private async fetchStandard(messages: ChatMessage[]): Promise<string> { const response = await fetch(this.apiConfig!.endpoint!, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiConfig!.apiKey}` }, body: JSON.stringify({ model: this.apiConfig!.model, messages: messages, max_tokens: this.apiConfig!.maxTokens, temperature: 0.7 }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API请求失败: ${response.status} - ${errorText}`); } const data = await response.json(); return data.choices[0]?.message?.content || ''; } /** * 输入净化 * @private */ private sanitizeInput(input: string): string { // 移除潜在的危险字符和过长的输入 let sanitized = input.trim() .substring(0, 5000) // 限制长度 .replace(/[<>]/g, ''); // 移除尖括号 return sanitized; } /** * 分发流式更新事件 * @private */ private dispatchStreamUpdate(content: string): void { const event = new CustomEvent('chatgpt-stream-update', { detail: { content } }); window.dispatchEvent(event); } /** * 清除当前会话 */ async clearSession(): Promise<void> { await this.sessionManager.clear(); } /** * 更新API配置 */ async updateApiConfig(config: Partial<ApiConfig>): Promise<void> { if (config.apiKey) { await this.secureStorage.saveApiKey(config.apiKey); } this.apiConfig = { ...this.apiConfig!, ...config }; } /** * 获取当前会话统计 */ getSessionStats(): { messageCount: number; tokenEstimate: number } { return this.sessionManager.getStats(); } } // 单元测试要点示例 (使用Jest) describe('ChatGPTExtension', () => { let extension: ChatGPTExtension; beforeEach(() => { extension = new ChatGPTExtension(); }); test('应正确初始化', async () => { await extension.initialize(); expect(extension['isInitialized']).toBe(true); }); test('应净化用户输入', () => { const maliciousInput = '<script>alert("xss")</script>正常内容'; const result = extension['sanitizeInput'](maliciousInput); expect(result).not.toContain('<script>'); expect(result).not.toContain('</script>'); }); test('处理消息时应使用会话上下文', async () => { // 模拟API调用 const mockResponse = '这是一个测试回复'; extension['fetchStandard'] = jest.fn().mockResolvedValue(mockResponse); await extension.initialize(); const response = await extension.processMessage('你好'); expect(response).toBe(mockResponse); expect(extension['sessionManager'].getContext()).toHaveLength(2); // 系统消息 + 用户/助手交互 }); test('API失败时应触发降级模式', async () => { extension['fetchStandard'] = jest.fn().mockRejectedValue(new Error('429 Too Many Requests')); // 设置降级回复 extension['fallbackManager']['fallbackCache'].set('测试', '降级回复'); const response = await extension.processMessage('测试'); expect(response).toBe('降级回复'); expect(extension['fallbackManager']['useFallback']).toBe(true); }); });

总结与展望

通过上述步骤,我们构建了一个功能完整、安全可靠的Chrome扩展,将ChatGPT的智能交互能力无缝集成到浏览器中。从技术选型、核心实现到性能优化和安全防护,每个环节都需要仔细考量。特别是MV3架构下的Service Worker生命周期管理、API密钥的安全存储以及用户输入的净化处理,是保证扩展稳定运行的关键。

在实际开发中,还可以考虑进一步优化,例如实现更智能的缓存策略(基于语义相似度而非精确匹配)、添加对更多AI模型提供商的支持、或者引入本地小型语言模型作为离线备用方案。随着浏览器扩展能力的不断增强,这类智能插件有望成为用户日常浏览的得力助手,真正实现"浏览即智能"的无缝体验。

开发此类扩展最大的收获是平衡功能与限制的艺术:在有限的浏览器权限和资源下,通过精巧的设计实现强大的功能。每一次与API的交互、每一行存储的代码、每一个安全过滤函数,都在为用户创造流畅而安全的智能体验添砖加瓦。希望这篇指南能帮助你顺利启动自己的智能浏览器扩展项目。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 10:29:22

GmSSL:国密算法的安全基石

GmSSL&#xff1a;国密算法的安全基石 【免费下载链接】GmSSL 支持国密SM2/SM3/SM4/SM9/SSL的密码工具箱 项目地址: https://gitcode.com/gh_mirrors/gm/GmSSL 价值定位能力&#xff1a;构建自主可控的密码基础设施 在数字化转型加速推进的今天&#xff0c;密码技术作为…

作者头像 李华
网站建设 2026/10/29 12:40:27

Qwen3-VL-30B作品集:从识别到推理,这些图文问答案例太实用了

Qwen3-VL-30B作品集&#xff1a;从识别到推理&#xff0c;这些图文问答案例太实用了 最近在AI圈子里&#xff0c;有个模型悄悄火起来了——Qwen3-VL-30B。你可能听说过它&#xff0c;知道这是个300亿参数的大模型&#xff0c;但你可能不知道&#xff0c;它到底能做什么&#x…

作者头像 李华
网站建设 2026/10/27 22:07:34

NSudo系统权限管理工具完全指南:从基础操作到高级应用

NSudo系统权限管理工具完全指南&#xff1a;从基础操作到高级应用 【免费下载链接】NSudo [Deprecated, work in progress alternative: https://github.com/M2Team/NanaRun] Series of System Administration Tools 项目地址: https://gitcode.com/gh_mirrors/nsu/NSudo …

作者头像 李华
网站建设 2026/10/29 13:57:13

离线阅读新纪元:fanqienovel-downloader的技术革新与实践指南

离线阅读新纪元&#xff1a;fanqienovel-downloader的技术革新与实践指南 【免费下载链接】fanqienovel-downloader 下载番茄小说 项目地址: https://gitcode.com/gh_mirrors/fa/fanqienovel-downloader 一、数字阅读的困境与破局之道 &#x1f4f1;➡️&#x1f4da; …

作者头像 李华
网站建设 2026/10/10 14:22:26

Cogito-V1-Preview-Llama-3B助力在线教育:AI助教自动批改编程作业

Cogito-V1-Preview-Llama-3B助力在线教育&#xff1a;AI助教自动批改编程作业 你有没有过这样的经历&#xff1f;作为一名在线编程课程的老师&#xff0c;深夜还在面对上百份学生提交的作业&#xff0c;一份份地检查语法、分析逻辑、写评语&#xff0c;感觉身体被掏空。或者&a…

作者头像 李华
网站建设 2026/10/10 16:43:40

OWL ADVENTURE像素风AI助手实战:上传图片与AI对话完整流程演示

OWL ADVENTURE像素风AI助手实战&#xff1a;上传图片与AI对话完整流程演示 你是不是也遇到过这样的场景&#xff1f;看到一张有趣的图片&#xff0c;想知道里面有什么故事&#xff0c;或者想了解图片里的某个细节&#xff0c;但身边没人可以问。或者&#xff0c;作为一个内容创…

作者头像 李华