尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零开发浏览器插件GaryPrompt:实现AI提示词高效管理

从零开发浏览器插件GaryPrompt:实现AI提示词高效管理 在日常使用豆包、ChatGPT、文心一言等AI助手时你是否也遇到过这样的烦恼精心调试好一个复杂的提示词Prompt想要在另一个对话窗口或新标签页中复用却只能手动复制粘贴不仅效率低下还容易出错。或者你有一套自己总结的“技能库”Skill每次使用都要从笔记里翻找体验非常割裂。为了解决这个痛点我开发了一款名为GaryPrompt的浏览器插件。它就像一个贴身的AI助手管家让你可以一键保存、管理和调用任何网页上的提示词彻底告别繁琐的复制粘贴。无论是豆包的对话、ChatGPT的指令还是任何网页上的文本片段都能轻松收纳随用随取。本文将手把手带你了解GaryPrompt的核心功能并详细拆解其开发过程。无论你是想直接使用这款提升效率的神器还是对浏览器插件开发感兴趣希望学习如何从零构建一个实用的工具这篇文章都能为你提供完整的指南。我们将从插件设计思路讲起涵盖环境搭建、核心代码实现、API集成、打包发布以及常见问题排查最终你将获得一个可运行、可扩展的完整项目。1. GaryPrompt 插件核心价值与设计思路在深入代码之前我们首先要明确这个插件解决了什么问题以及它是如何设计的。理解这些背景有助于我们更好地使用或二次开发它。1.1 痛点分析与解决方案核心痛点提示词管理碎片化优质的Prompt可能散落在各个AI平台的聊天记录、本地文档、收藏夹中难以统一管理和快速检索。复用操作低效在不同会话或平台间复用Prompt需要多次“选中-复制-切换标签页-粘贴”的操作。缺乏上下文关联简单的文本复制无法携带Prompt的元信息如适用模型、创建时间、分类标签等。GaryPrompt的解决方案一键抓取通过浏览器右键菜单或插件图标按钮快速将当前选中的网页文本特别是AI对话中的Prompt保存至插件库。集中管理提供独立的插件弹出页面Popup对保存的Prompt进行增删改查、分类打标。一键填充在目标网页的输入框中通过右键菜单或快捷键快速将选中的Prompt插入光标处实现秒级复用。数据同步利用浏览器的存储API如chrome.storage保存数据并可考虑扩展至云端同步。1.2 技术架构与核心模块GaryPrompt是一个典型的浏览器扩展Browser Extension遵循Manifest V3规范。其主要由以下几部分组成manifest.json扩展的配置文件声明权限、资源、后台脚本、弹出页面等。后台服务脚本Service Worker处理扩展的核心逻辑如右键菜单创建、存储操作、消息中转等。在MV3中替代了传统的背景页Background Page。弹出页面Popup用户点击扩展图标时出现的界面用于管理Prompt库。内容脚本Content Script注入到用户访问的网页中用于从页面抓取文本或向页面注入文本。选项页面Options Page用于进行更复杂的插件设置当前版本可能简化。数据流大致如下用户选中文本 → 右键菜单点击“保存到GaryPrompt” → 内容脚本获取文本 → 发送消息给后台脚本 → 后台脚本将数据存入chrome.storage→ 弹出页面从存储中读取并展示数据。2. 开发环境准备与项目初始化工欲善其事必先利其器。开始编码前我们需要准备好开发环境。2.1 环境与工具浏览器Google Chrome 88 或 Microsoft Edge 88支持Manifest V3的版本。推荐使用最新稳定版。代码编辑器Visual Studio Code、WebStorm、Sublime Text等任选VS Code有丰富的插件支持。核心技能HTML、CSS、JavaScript (ES6)。不需要任何后端或复杂的构建工具浏览器扩展开发前端知识足够。2.2 创建项目结构首先创建一个名为garyprompt-extension的文件夹并建立如下初始目录结构garyprompt-extension/ ├── manifest.json # 扩展配置文件 ├── background.js # 后台服务脚本 ├── content.js # 内容脚本 ├── popup.html # 弹出页面HTML结构 ├── popup.js # 弹出页面逻辑 ├── popup.css # 弹出页面样式 ├── options.html # 选项页面可选本期简化 ├── icons/ # 扩展图标 │ ├── icon16.png │ ├── icon48.png │ └── icon128.png └── _locales/ # 国际化文件夹可选 └── en/ └── messages.json你可以先准备几个简单的图标文件16x16, 48x48, 128x128像素放在icons文件夹或者后期用占位图替代。3. 核心配置文件Manifest V3详解manifest.json是扩展的“身份证”和“说明书”它定义了扩展的基本信息、权限、资源以及运行方式。3.1 基础配置创建manifest.json文件填入以下内容{ manifest_version: 3, name: GaryPrompt - Prompt Manager, version: 1.0.0, description: 一键保存和管理网页提示词助力高效AI对话。, author: Your Name, action: { default_popup: popup.html, default_title: GaryPrompt, default_icon: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png } }, icons: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png }, permissions: [ storage, contextMenus, activeTab ], host_permissions: [ all_urls ], background: { service_worker: background.js }, content_scripts: [ { matches: [all_urls], js: [content.js], run_at: document_end } ], web_accessible_resources: [{ resources: [], matches: [all_urls] }] }关键字段解释manifest_version: 必须为3表示使用Manifest V3。action: 定义了扩展图标点击后的行为default_popup指定弹出页面。permissions: 申请扩展需要的权限。storage: 使用chrome.storageAPI保存插件数据。contextMenus: 创建浏览器右键菜单。activeTab: 获取当前活动标签页的信息交互时。host_permissions: 声明扩展可以访问的网站all_urls表示所有网址谨慎使用实际发布时可细化。background: 声明后台服务脚本MV3中必须是service_worker。content_scripts: 定义注入到哪些页面的脚本及其注入时机。4. 功能模块实现从右键菜单到数据存储接下来我们将分模块实现核心功能。4.1 后台脚本初始化与右键菜单创建background.js文件。后台脚本在扩展安装后一直存在事件驱动负责管理右键菜单和核心数据逻辑。// background.js // 安装或更新扩展时初始化 chrome.runtime.onInstalled.addListener(() { console.log(GaryPrompt Extension Installed.); // 创建右键菜单项 chrome.contextMenus.create({ id: saveToGaryPrompt, title: 保存到 GaryPrompt, contexts: [selection] // 仅在选中文本时显示 }); chrome.contextMenus.create({ id: insertFromGaryPrompt, title: 从 GaryPrompt 插入, contexts: [editable] // 在可编辑区域输入框显示 }); // 初始化存储空间如果为空 chrome.storage.local.get([prompts], (result) { if (!result.prompts) { chrome.storage.local.set({ prompts: [] }); } }); }); // 监听右键菜单点击事件 chrome.contextMenus.onClicked.addListener((info, tab) { if (info.menuItemId saveToGaryPrompt) { // 用户选中文本并点击了“保存” const selectedText info.selectionText.trim(); if (selectedText) { // 发送消息给内容脚本获取更多上下文如网页标题 chrome.tabs.sendMessage(tab.id, { action: getPageContextForSave, selectedText: selectedText }, (response) { // 收到内容脚本的回复后保存Prompt const pageTitle response?.pageTitle || tab.title || 未知页面; const url tab.url || ; savePromptToStorage(selectedText, pageTitle, url); }); } } else if (info.menuItemId insertFromGaryPrompt) { // 用户想在输入框插入Prompt通知弹出页面打开或直接处理 // 更复杂的逻辑可能需要与popup通信这里先简单通知 chrome.action.openPopup(); // 尝试打开弹出页注意此API有限制 // 实际项目中可能会通过消息让popup显示一个选择列表 } }); // 保存Prompt到本地存储的函数 function savePromptToStorage(text, source, url) { chrome.storage.local.get([prompts], (result) { const prompts result.prompts || []; const newPrompt { id: Date.now(), // 简单的时间戳作为ID content: text, source: source, url: url, tags: [], createdAt: new Date().toISOString() }; prompts.unshift(newPrompt); // 新加的放在前面 chrome.storage.local.set({ prompts: prompts }, () { console.log(Prompt saved:, newPrompt); // 可以发送一个通知给用户 chrome.notifications.create({ type: basic, iconUrl: icons/icon48.png, title: GaryPrompt, message: 提示词已保存 }); }); }); } // 监听来自弹出页面或内容脚本的消息 chrome.runtime.onMessage.addListener((request, sender, sendResponse) { console.log(Background received message:, request); // 例如处理获取所有Prompt的请求 if (request.action getAllPrompts) { chrome.storage.local.get([prompts], (result) { sendResponse({ prompts: result.prompts || [] }); }); return true; // 表示会异步发送响应 } // 处理插入Prompt的请求 if (request.action insertPrompt) { const { promptId, tabId } request; // 这里需要先获取Prompt内容然后发送给指定标签页的内容脚本 chrome.storage.local.get([prompts], (result) { const targetPrompt (result.prompts || []).find(p p.id promptId); if (targetPrompt tabId) { chrome.tabs.sendMessage(tabId, { action: insertText, text: targetPrompt.content }); } }); } });4.2 内容脚本与网页内容交互创建content.js文件。内容脚本可以访问和操作DOM是扩展与用户浏览的网页之间的桥梁。// content.js // 监听来自后台脚本的消息 chrome.runtime.onMessage.addListener((request, sender, sendResponse) { console.log(Content script received:, request); if (request.action getPageContextForSave) { // 当后台请求页面上下文时返回页面标题和选中文本 const pageTitle document.title; sendResponse({ pageTitle: pageTitle, selectedText: request.selectedText // 实际上后台已传这里可再处理 }); } if (request.action insertText) { // 接收要插入的文本并插入到当前焦点元素或活动元素 insertTextAtCursor(request.text); sendResponse({ status: success }); } // 如果需要异步响应return true }); // 在光标处插入文本的通用函数 function insertTextAtCursor(text) { const activeElement document.activeElement; const isEditable activeElement ( activeElement.tagName TEXTAREA || activeElement.tagName INPUT || activeElement.isContentEditable ); if (!isEditable) { console.warn(当前焦点元素不可编辑。); return; } // 对于可编辑元素如div和input/textarea处理方式不同 if (activeElement.isContentEditable) { // 可编辑DIV document.execCommand(insertText, false, text); } else if (activeElement.tagName TEXTAREA || activeElement.tagName INPUT) { // Input或Textarea const start activeElement.selectionStart; const end activeElement.selectionEnd; const value activeElement.value; activeElement.value value.substring(0, start) text value.substring(end); activeElement.selectionStart activeElement.selectionEnd start text.length; // 触发input事件让React/Vue等框架能捕获到变化 activeElement.dispatchEvent(new Event(input, { bubbles: true })); } } // 也可以直接暴露一个全局函数供后台调用另一种方式 // 但通过消息传递是更安全、标准的方式。4.3 弹出页面Prompt管理界面弹出页面是用户管理已保存Prompt的主要界面。我们先创建简单的HTML和CSS。popup.html:!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleGaryPrompt/title link relstylesheet hrefpopup.css /head body div classcontainer header h1 GaryPrompt/h1 p classsubtitle你的提示词管理库/p /header div classsearch-box input typetext idsearchInput placeholder搜索提示词内容或来源... button idclearSearch×/button /div div classprompt-list idpromptList !-- Prompt列表将通过JS动态加载 -- div classempty-state idemptyState p还没有保存任何提示词。/p p在网页上选中文本右键选择“保存到 GaryPrompt”即可添加。/p /div /div div classactions button idrefreshBtn刷新列表/button button idoptionsBtn classsecondary设置/button /div /div script srcpopup.js/script /body /htmlpopup.css:body { width: 400px; height: 500px; margin: 0; font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; font-size: 14px; color: #333; background-color: #f9f9f9; } .container { padding: 16px; display: flex; flex-direction: column; height: calc(100% - 32px); } header { border-bottom: 1px solid #eee; padding-bottom: 12px; margin-bottom: 16px; } header h1 { margin: 0; font-size: 20px; color: #2c3e50; } .subtitle { margin: 4px 0 0; font-size: 12px; color: #7f8c8d; } .search-box { position: relative; margin-bottom: 16px; } #searchInput { width: 100%; padding: 8px 12px; padding-right: 30px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none; transition: border 0.2s; } #searchInput:focus { border-color: #3498db; } #clearSearch { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: none; border: none; font-size: 18px; color: #aaa; cursor: pointer; padding: 0; width: 20px; height: 20px; display: none; } #clearSearch:hover { color: #777; } .prompt-list { flex: 1; overflow-y: auto; border: 1px solid #eee; border-radius: 6px; background-color: #fff; padding: 8px; } .prompt-item { padding: 12px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background-color 0.1s; } .prompt-item:hover { background-color: #f8fafc; } .prompt-item:last-child { border-bottom: none; } .prompt-content { font-size: 13px; line-height: 1.5; color: #2d3748; margin-bottom: 8px; white-space: pre-wrap; word-break: break-word; max-height: 80px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; } .prompt-meta { font-size: 11px; color: #718096; display: flex; justify-content: space-between; } .prompt-source { font-weight: 500; } .empty-state { text-align: center; padding: 40px 20px; color: #a0aec0; } .empty-state p { margin: 8px 0; } .actions { display: flex; justify-content: space-between; padding-top: 16px; border-top: 1px solid #eee; } button { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background-color 0.2s; } #refreshBtn { background-color: #3498db; color: white; } #refreshBtn:hover { background-color: #2980b9; } .secondary { background-color: #e2e8f0; color: #4a5568; } .secondary:hover { background-color: #cbd5e0; }popup.js:// popup.js document.addEventListener(DOMContentLoaded, function() { const promptListEl document.getElementById(promptList); const emptyStateEl document.getElementById(emptyState); const searchInput document.getElementById(searchInput); const clearSearchBtn document.getElementById(clearSearch); const refreshBtn document.getElementById(refreshBtn); const optionsBtn document.getElementById(optionsBtn); let allPrompts []; // 初始化加载Prompt列表 loadPrompts(); // 搜索框事件 searchInput.addEventListener(input, function(e) { filterPrompts(e.target.value); toggleClearButton(); }); clearSearchBtn.addEventListener(click, function() { searchInput.value ; filterPrompts(); toggleClearButton(); searchInput.focus(); }); // 刷新按钮 refreshBtn.addEventListener(click, loadPrompts); // 设置按钮示例可链接到options页面 optionsBtn.addEventListener(click, function() { // chrome.runtime.openOptionsPage(); // 如果配置了options页面 console.log(打开设置); }); // 加载Prompt列表 function loadPrompts() { // 向后台脚本发送消息获取所有Prompt chrome.runtime.sendMessage({ action: getAllPrompts }, (response) { if (chrome.runtime.lastError) { console.error(Error fetching prompts:, chrome.runtime.lastError); return; } allPrompts response.prompts || []; renderPromptList(allPrompts); toggleEmptyState(); }); } // 渲染Prompt列表 function renderPromptList(prompts) { promptListEl.innerHTML ; if (prompts.length 0) { promptListEl.appendChild(emptyStateEl); emptyStateEl.style.display block; return; } emptyStateEl.style.display none; prompts.forEach(prompt { const itemEl createPromptItem(prompt); promptListEl.appendChild(itemEl); }); } // 创建单个Prompt项 function createPromptItem(prompt) { const div document.createElement(div); div.className prompt-item; div.dataset.id prompt.id; // 限制显示内容长度 const displayContent prompt.content.length 200 ? prompt.content.substring(0, 200) ... : prompt.content; const sourceName prompt.source || 未知来源; const shortSource sourceName.length 30 ? sourceName.substring(0, 30) ... : sourceName; const date new Date(prompt.createdAt).toLocaleDateString(); div.innerHTML div classprompt-content${escapeHtml(displayContent)}/div div classprompt-meta span classprompt-source title${escapeHtml(sourceName)}${escapeHtml(shortSource)}/span span classprompt-date${date}/span /div ; // 点击项将其内容插入到当前活动页面的输入框 div.addEventListener(click, function() { insertPromptIntoActiveTab(prompt.id, prompt.content); }); return div; } // 插入Prompt到当前标签页 function insertPromptIntoActiveTab(promptId, promptContent) { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) { if (tabs.length 0) return; const activeTab tabs[0]; // 发送消息给后台由后台转发给内容脚本执行插入 chrome.runtime.sendMessage({ action: insertPrompt, tabId: activeTab.id, promptId: promptId }, (response) { // 插入成功后可以关闭弹出页面或给出提示 window.close(); // 关闭弹出页 }); }); } // 过滤Prompt function filterPrompts(keyword) { if (!keyword.trim()) { renderPromptList(allPrompts); return; } const lowerKeyword keyword.toLowerCase(); const filtered allPrompts.filter(p p.content.toLowerCase().includes(lowerKeyword) || (p.source p.source.toLowerCase().includes(lowerKeyword)) ); renderPromptList(filtered); } // 切换清空搜索按钮的显示 function toggleClearButton() { clearSearchBtn.style.display searchInput.value ? block : none; } // 切换空状态显示 function toggleEmptyState() { // 逻辑已在renderPromptList中处理 } // 简单的HTML转义防止XSS function escapeHtml(text) { const div document.createElement(div); div.textContent text; return div.innerHTML; } });5. 加载、测试与调试扩展代码编写完成后我们需要在浏览器中加载未打包的扩展进行测试。5.1 在 Chrome/Edge 中加载扩展打开浏览器进入扩展管理页面Chrome: 在地址栏输入chrome://extensions/Edge: 在地址栏输入edge://extensions/打开右上角的“开发者模式”开关。点击左上角的“加载已解压的扩展程序”按钮。在弹出的文件选择器中定位到你的garyprompt-extension项目文件夹点击选择文件夹。扩展列表中应该会出现GaryPrompt - Prompt Manager并显示图标。5.2 功能测试流程测试右键菜单保存打开任意网页例如 CSDN 的一篇技术文章。选中一段文本可以是一段代码或描述。右键点击你应该能看到“保存到 GaryPrompt”的菜单项。点击它浏览器右上角应该会出现一个简单的通知提示“提示词已保存”。测试弹出页面管理点击浏览器工具栏中的 GaryPrompt 图标。弹出页面应该打开并显示你刚刚保存的提示词卡片包含内容预览、来源和日期。测试搜索功能在弹出页面的搜索框中输入关键词列表应该能实时过滤。测试插入功能打开一个可以输入文字的页面例如豆包、ChatGPT网页版、一个记事本网页。将光标聚焦到输入框。点击 GaryPrompt 图标在弹出页面中点击一个已保存的提示词。弹出页面关闭同时你刚才点击的提示词内容应该被插入到输入框中。5.3 调试技巧后台脚本调试在chrome://extensions/页面找到你的扩展点击“service worker”链接即可打开后台脚本的控制台。弹出页面调试右键点击扩展图标选择“审查弹出内容”即可打开弹出页面的开发者工具。内容脚本调试在普通网页的开发者工具中切换到Console面板其上下文Context选择器通常会有一个[Extension]或你的扩展ID的选项选择它就可以看到内容脚本的日志。或者内容脚本的日志默认会打印在网页的控制台里。6. 常见问题与排查思路在开发和使用过程中你可能会遇到以下问题问题现象可能原因解决思路右键菜单不显示1.contexts配置错误。2. 后台脚本onInstalled事件未触发或报错。3. 扩展未正确加载或已禁用。1. 检查background.js中contexts是否为[selection]或[editable]。2. 打开后台脚本的控制台查看是否有报错。3. 去扩展管理页面确认扩展已启用并尝试重新加载。点击保存没反应1. 内容脚本未注入到当前页面。2. 消息传递失败。3. 存储API操作失败。1. 检查manifest.json中content_scripts的matches是否包含当前页面URL。2. 在后台脚本和内容脚本中增加console.log查看消息流。3. 检查后台脚本控制台是否有chrome.storage的错误。弹出页面无法加载/空白1. 文件路径错误。2. HTML/JS/CSS 存在语法错误。3. 资源加载被CSP策略阻止。1. 检查manifest.json中default_popup路径是否正确。2. 打开弹出页面的开发者工具查看Console和Network面板报错。3. 检查是否有跨域资源加载问题本例中应无此问题。提示词插入位置不对1.insertTextAtCursor函数未正确识别可编辑元素。2. 目标页面是富文本编辑器如CKEditor。3. 框架React/Vue未触发更新。1. 在content.js中调试activeElement和isEditable的判断逻辑。2. 对于复杂编辑器可能需要调用其特定的API。3. 确保在修改value后正确触发了input或change事件。数据保存后刷新页面丢失使用了错误的存储API或作用域。确保使用的是chrome.storage.local而非localStorage。localStorage是页面作用域而chrome.storage是扩展作用域。在部分网站如Chrome网上应用店无效受浏览器安全策略限制。浏览器限制扩展在特定特权页面如chrome://、edge://开头的页面以及网上应用店运行内容脚本。这是正常行为。7. 进阶优化与工程实践一个基础版本完成后我们可以从工程化和用户体验角度进行优化。7.1 数据持久化与同步当前数据存储在chrome.storage.local中仅限于本地。可以考虑使用chrome.storage.sync在用户登录同一Chrome账号的不同设备间同步数据有容量限制。集成后端API如果你有自己的服务器可以增加选项让用户将数据备份到云端。这需要处理OAuth授权、加密传输等。7.2 功能增强编辑与分类在弹出页面中为每个Prompt项添加“编辑”和“删除”按钮并支持添加标签Tags进行分类。快捷键支持在manifest.json中声明commands实现通过快捷键如CtrlShiftS快速保存选中文本。导入/导出提供将Prompt库导出为JSON文件以及从JSON文件导入的功能便于备份和迁移。高级搜索支持按标签、日期范围进行筛选。模板变量在Prompt中支持{{date}}、{{topic}}等变量插入时进行替换。7.3 代码结构与模块化随着功能增多代码需要更好的组织将background.js中的不同功能拆分为模块如contextMenu.js、storage.js、notification.js。使用ES6 Modules进行导入导出需在manifest.json的background字段中设置type: module。考虑使用类似Vue或React的小型框架来构建更复杂的弹出页面和选项页面但需注意扩展包体积。7.4 发布到商店代码压缩与混淆使用Webpack、Parcel等打包工具压缩HTML、CSS、JS文件减少扩展体积。准备商店素材制作宣传图1280x800或640x400、详细描述、分类、截图等。隐私政策如果涉及任何数据收集必须提供隐私政策链接。提交审核前往 Chrome网上应用店开发者控制台 或 Microsoft Edge Add-ons 开发者门户 打包上传zip文件填写信息并提交审核。7.5 安全注意事项最小权限原则在manifest.json中host_permissions不要滥用all_urls。如果可能精确指定需要操作的网站如[https://chat.openai.com/*, https://www.doubao.com/*]。净化用户输入从网页抓取或用户输入的内容在插入到DOM前一定要进行转义如使用textContent或escapeHtml函数防止XSS攻击。谨慎处理消息在消息监听器中验证消息来源 (sender)并对接收到的数据做合法性检查。通过以上步骤你不仅拥有了一个功能完整的浏览器插件也掌握了从构思、开发、测试到考虑发布和安全的完整流程。GaryPrompt的核心思路——通过浏览器扩展桥接网页内容与本地管理——可以复用到很多场景比如代码片段管理、文章收藏、表单填充等。希望这个项目能成为你提升工作效率的得力工具也成为你学习浏览器扩展开发的一个扎实起点。如果在实现过程中遇到任何问题欢迎在评论区交流探讨。
返回列表