
1. 从零做一个 Chrome 插件为什么我卡在 AI 接入这一步先说清楚这篇要解决什么用 Codex 从零生成一个 Manifest V3 的 Chrome 插件插件里带 AI 能力比如把知乎问题页整理成文档视图、一键总结正文然后走完 Edge 本地加载、Chrome Web Store 提交审核的完整流程。适合谁看会一点前端、想用 AI 编程把一个小想法推到上架、但一碰到「插件里怎么调大模型」就卡住的人。我这次做的插件叫「知乎文档阅读器」核心功能很朴素打开知乎问题页把信息密度很杂的页面重排成左侧目录、右侧正文的文档式阅读界面支持隐藏图片、复制正文、快捷键切换。第一版让 Codex 直接生成目录结构和代码跑起来很快真正让我停下来的是下一步——我想给插件加一个「AI 总结当前回答」的按钮。问题就出在这里。Chrome 插件是纯前端环境Manifest V3 的 background service worker 里没有 Node 环境你不可能把某个厂商的 API Key 硬编码进 content.js那等于把密钥公开挂在商店里。而如果每个模型厂商都单独接一遍OpenAI 一套、Anthropic 一套、国内模型又一套请求格式、鉴权头、返回结构全不一样插件里会堆满 if-else。我试过的最笨办法是让 Codex 直接写死一个 Key 在 background.js 里本地测试能跑但一想到要提交审核就删了——商店审核会检查 remote code 和密钥泄露风险这种写法基本等于自己给自己埋雷。所以这篇的重点不是「插件怎么写」而是插件里的 AI 能力怎么用一个统一 Key、一条 API 通道接进去让 background service worker 只认一个 Base URL、一个 Key、一个 Model ID。这样插件代码干净审核时权限说明也好写后面换模型只改一个字符串。下面我会按真实顺序拆先给可复制的 manifest.json 和 background service worker再讲请求封装然后本地加载验证最后是审核前自检和提交。中间会穿插我踩过的坑尤其是 401、local proxy failed、reading choices 这几类报错。2. TaoToken 统一 Key 接入插件 AI 能力的前置准备在写插件代码之前得先把「AI 通道」这件事定下来。我的选择是用 TaoToken 做统一入口原因是它把多家模型的调用收敛成一套 OpenAI 兼容格式插件里只需要维护一个 Base URL 和一个 Key不用为每个模型写不同的请求体。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 这个不加 UTM。注意这两个地址的区别官网是给你看文档、进控制台、拿 Key 的API 是代码里真正请求的 Base URL别混。具体要准备三样东西我把它叫「三件套」后面插件配置里会反复出现第一是 Base URL。代码里请求的根地址是https://taotoken.net/api注意很多 OpenAI 兼容客户端会在后面自动拼/v1/chat/completions所以你在配置里填的 Base URL 通常就是到/api这一层具体拼法看你用的封装。我这次在 background.js 里是手动拼完整路径避免歧义。第二是 API Key。进控制台创建地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。创建完复制出来形如sk-开头的一串。这个 Key 在插件里绝对不能写进前端代码正确做法是让用户自己在插件的 options 页面填存到chrome.storage.localbackground 请求时再读出来。第三是 Model ID。这个必须和你账号里可用的模型对上填错会直接报 model not found。你可以在模型对话页面先验证一下模型能不能正常回话地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。我习惯先在对话页发一句「你好」确认通道通再写进插件省得在插件里 debug 半天发现是模型名写错了。如果你后面要做的是长期编码类、Agent 类的插件比如自动改代码、批量处理可以看 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。但这次做的是轻量总结类插件用普通 API Key 就够。这里有个关键点要强调插件是前端环境Key 一旦打包进 zip 就等于公开。所以我的设计是「用户自填 Key」——插件本身不带任何密钥用户装完后在设置页填自己的 Key存本地。这样审核时你可以在权限说明里写清楚「不收集用户数据、Key 仅存本地」合规风险低很多。拿 Key 的完整动作打开控制台 → 创建 API Key → 复制 → 在模型对话页发一条测试消息确认可用 → 记下你要用的 Model ID。这三步做完再进下一节写代码。3. 可复制配置manifest.json 与 background service worker这一节是全文最核心的部分直接给可复制的文件。目录结构我按 Codex 生成的第一版整理成这样zhihu-doc-reader/ manifest.json popup.html options.html icons/ icon128.png src/ background.js content.js content.css popup.js options.js先看 manifest.json。Manifest V3 和 V2 最大的区别是 background 从 page 变成了 service worker权限声明也更严格。下面这份是我实际用的注意host_permissions只申请了知乎域名和 TaoToken 的 API 域名申请越少审核越顺{ manifest_version: 3, name: 知乎文档阅读器, version: 1.0.0, description: 把知乎问题页整理成文档式阅读界面支持 AI 总结正文。, permissions: [storage, activeTab, scripting], host_permissions: [ https://www.zhihu.com/*, https://taotoken.net/* ], background: { service_worker: src/background.js }, action: { default_popup: popup.html, default_icon: { 128: icons/icon128.png } }, options_page: options.html, content_scripts: [ { matches: [https://www.zhihu.com/question/*], js: [src/content.js], css: [src/content.css], run_at: document_start } ], icons: { 128: icons/icon128.png } }几个容易踩的点run_at我设成document_start因为知乎页面动态加载多注入晚了会反复重绘host_permissions里必须显式加上https://taotoken.net/*否则 background 发请求会被 CORS 拦掉报错长得像网络错误其实是权限没给。接下来是 background.js也就是 service worker。它负责接收 content script 发来的「总结这段正文」消息读本地存的 Key调 TaoToken 的 API把结果回传。这是插件 AI 能力的核心// src/background.js const API_BASE https://taotoken.net/api; const CHAT_PATH /v1/chat/completions; async function getConfig() { const { apiKey, modelId } await chrome.storage.local.get([ apiKey, modelId, ]); return { apiKey, modelId }; } async function summarize(text) { const { apiKey, modelId } await getConfig(); if (!apiKey) { throw new Error(NO_API_KEY); } if (!modelId) { throw new Error(NO_MODEL_ID); } const resp await fetch(API_BASE CHAT_PATH, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer apiKey, }, body: JSON.stringify({ model: modelId, messages: [ { role: system, content: 你是一个阅读助手请用简洁的中文总结用户提供的正文控制在 200 字以内。, }, { role: user, content: text.slice(0, 6000) }, ], temperature: 0.3, }), }); if (!resp.ok) { const errText await resp.text(); throw new Error(API_ERROR_ resp.status _ errText); } const data await resp.json(); const choice data.choices data.choices[0]; if (!choice || !choice.message) { throw new Error(BAD_RESPONSE); } return choice.message.content; } chrome.runtime.onMessage.addListener((msg, sender, sendResponse) { if (msg.type SUMMARIZE) { summarize(msg.text) .then((result) sendResponse({ ok: true, result })) .catch((err) sendResponse({ ok: false, error: err.message })); return true; // 保持消息通道打开异步返回 } });这里有个 Manifest V3 的坑必须说service worker 里return true不能省否则 sendResponse 是异步的通道会提前关闭content script 收到 undefined。我第一次就栽在这报错表现为「总结按钮点了没反应」控制台里 background 没报错其实是消息没回。再看 options.js负责让用户填 Key 和 Model ID存本地// src/options.js const $ (id) document.getElementById(id); chrome.storage.local.get([apiKey, modelId]).then((cfg) { $(apiKey).value cfg.apiKey || ; $(modelId).value cfg.modelId || ; }); $(save).addEventListener(click, async () { const apiKey $(apiKey).value.trim(); const modelId $(modelId).value.trim(); await chrome.storage.local.set({ apiKey, modelId }); $(status).textContent 已保存; });对应的 options.html 很简单两个 input 加一个按钮这里不展开。关键是「三件套」在插件里的落点Base URL 写死在 background.js 的API_BASEKey 和 Model ID 存在chrome.storage.local由用户在 options 页填。这样插件包里没有任何密钥审核时权限说明可以写得很干净。content.js 负责在知乎页面注入文档视图并在用户点「AI 总结」时把正文通过chrome.runtime.sendMessage发给 background。核心片段// src/content.js节选 function getArticleText() { const nodes document.querySelectorAll(.RichContent-inner); return Array.from(nodes) .map((n) n.innerText) .join(\n\n); } async function onSummarizeClick() { const text getArticleText(); if (!text) return; const resp await chrome.runtime.sendMessage({ type: SUMMARIZE, text, }); if (resp resp.ok) { renderSummary(resp.result); } else { renderError(resp ? resp.error : UNKNOWN); } }到这一步插件的 AI 通道就通了content 抓正文 → background 读本地 Key → 请求 TaoToken → 回传结果渲染。整套只认一个 Base URL、一个 Key、一个 Model ID换模型只改 options 里的 Model ID。4. 本地加载与验证请求Edge 和 Chrome 都能跑代码写完先别急着打包上架本地加载验证是必须的。Edge 和 Chrome 都能加载解压扩展路径分别是edge://extensions/和chrome://extensions/。具体动作打开扩展页 → 打开右上角「开发人员模式」→ 点「加载解压缩的扩展」→ 选中你的zhihu-doc-reader目录。加载成功后扩展列表里会出现你的插件图标是灰的说明没报错图标变红或者列表里出现「错误」按钮点进去看 service worker 的报错。加载后先验证三件事。第一popup 能不能打开点插件图标如果 popup.html 有语法错误弹窗会是空白。第二content script 有没有注入打开一个知乎问题页看文档视图有没有出现没出现就按 F12 看 Console 有没有报错。第三AI 总结能不能通先在 options 页填好 Key 和 Model ID再点总结按钮。验证请求这一步我建议先在 background 的 service worker 控制台里手动跑一次 fetch确认通道本身是通的。在扩展页找到你的插件点「service worker」链接会打开一个 DevTools在 Console 里贴fetch(https://taotoken.net/api/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer 你的Key, }, body: JSON.stringify({ model: 你的ModelID, messages: [{ role: user, content: 你好 }], }), }) .then((r) r.json()) .then((d) console.log(d.choices[0].message.content)) .catch((e) console.error(e));如果返回一段中文说明 Base URL、Key、Model ID 三件套都对。如果报 401是 Key 问题如果报 model not found是 Model ID 问题如果报 Failed to fetch多半是host_permissions没加https://taotoken.net/*。实测下来最容易忽略的是 service worker 的生命周期。Manifest V3 的 service worker 空闲几十秒会被浏览器挂起下次消息来了再唤醒。所以你在 Console 里手动跑的 fetch 和插件实际请求可能不在同一个生命周期别用「Console 里能跑」就断定插件没问题一定要点真实按钮走一遍完整链路。本地验证通过后再打包。打包前把manifest.json里的 version 确认一下Chrome Web Store 不允许重复版本号。打包命令很简单在插件目录外执行cd zhihu-doc-reader zip -r ../zhihu-doc-reader-1.0.0.zip . -x *.DS_Store注意 zip 的根目录必须是 manifest.json 所在层不能多套一层文件夹否则上传后商店识别不到 manifest。5. 常见报错排查401、local proxy failed、reading choices这一节按我实际遇到的报错来每个都给现象、原因、动作。401 Unauthorized。现象是 background 返回API_ERROR_401总结按钮显示错误。原因通常是 Key 没填、填错、或者 Key 前后带了空格。动作进 options 页重新粘贴 Key注意别带换行在 service worker Console 里打印chrome.storage.local.get([apiKey])确认存进去的值和你在控制台复制的一致。还有一种情况是 Key 被禁用或额度用完去控制台确认状态。local proxy failed / Failed to fetch。现象是请求根本没发出去报错像网络层错误。原因有两个一是host_permissions没加https://taotoken.net/*Manifest V3 下跨域请求必须显式声明二是请求地址拼错比如 Base URL 写成https://taotoken.net少了/api或者路径拼成/v1/chat/completions但 Base 里已经带了/v1变成/v1/v1/...。动作检查 manifest 的 host_permissions检查API_BASE CHAT_PATH拼出来的完整 URL在 Console 里console.log出来看一眼。reading choices 相关报错。现象是返回 200 但解析失败报BAD_RESPONSE或者Cannot read properties of undefined (reading choices)。原因是返回结构和你预期的不一样可能是模型返回了错误对象但 HTTP 状态是 200也可能是流式返回被当成非流式解析。动作在 background 里把data整个console.log出来确认data.choices存在如果用了stream: true要么改成 false要么按 SSE 逐行解析。我这次没开流式直接非流式拿完整结果简单可靠。OAuth / 鉴权头错误。现象是 403 或者提示鉴权方式不对。原因是有些客户端默认走 OAuth 或者把 Key 放错 header。TaoToken 走的是标准 Bearer 鉴权header 必须是Authorization: Bearer sk-xxx别写成x-api-key或者api-key。动作检查 header 拼写确认没有多余空格。service worker 消息无响应。现象是点总结按钮没反应background 也没报错。原因就是前面说的return true没写异步 sendResponse 通道提前关闭。动作在chrome.runtime.onMessage.addListener里确认异步分支返回了 true。content script 注入时机问题。现象是页面刚打开时文档视图没出现刷新一下才有。原因是run_at设成了document_idle知乎的动态内容还没渲染完。动作改成document_start并在 content.js 里用 MutationObserver 监听内容变化加防抖避免频繁重绘。如果你用的是 Claude Code 这类工具做插件开发配置里同样要写全三件套Base URL 填https://taotoken.net/apiKey 填你的 KeyModel ID 填你要用的模型。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 里面有具体的 settings 配置示例。Cline、CC Switch 这类工具也是同样的三件套逻辑Base URL、Key、Model ID 一个都不能少缺一个就会报鉴权或模型找不到。6. 审核前自检与提交从 Draft 到 Pending review插件本地跑通、AI 通道验证过接下来是上架。Chrome Web Store 的坑比开发多我按提交顺序列一遍。先准备素材。必填的是 128x128 的商店图标至少一张 1280x800 或 640x400 的截图。小宣传图 440x280 不是必填但建议准备。尺寸不对后台会直接卡住别在这浪费时间。然后进开发者后台上传 zip。上传后填描述、分类、语言。描述里别用「伪装」「破解」这类词我第一版叫「知乎飞书伪装器」后来改成「知乎文档阅读器」同一个产品审核风险完全不同。平台审核看的是用途是否清晰、是否合规不是名字够不够炸。最容易卡的是 Privacy 页面。点提交时如果提示Unable to publish对照下面这几项逐条补activeTab justification写清楚为什么需要 activeTab比如「用于在用户点击插件时读取当前知乎页面内容」。host permission justification写清楚为什么需要https://www.zhihu.com/*和https://taotoken.net/*前者是注入阅读视图后者是调用 AI 总结接口。remote code use justification明确写「本插件不使用远程代码所有逻辑打包在扩展内」。storage justification写「仅用于保存用户的 API Key 和模型偏好存储在本地」。single purpose description一句话说清插件只做一件事比如「把知乎问题页整理成文档式阅读界面」。data usage certification勾选不收集用户数据。publisher contact email验证邮箱。privacy policy URL即使不收集数据也要提供一个公开链接。隐私政策我写了个模板核心就几句本扩展不收集、不传输、不售卖任何用户数据所有页面处理在用户浏览器本地完成storage 仅保存本地偏好仅申请知乎域名权限用于运行不使用远程代码。这段直接复用改改插件名就行。提交后状态从 Draft 变成 Pending review就说明跑通了。审核期间别频繁改草稿被拒了按拒绝原因改改完重新提交。最后说一个我踩过的坑插件里如果让用户自填 Key审核时要在权限说明里写清楚「Key 仅存本地、不上传」否则容易被判定为收集敏感信息。我的做法是在 options 页加一行提示文字同时在隐私政策里明确写出来两边对齐审核基本不会卡这一点。到这里从 Codex 生成插件、TaoToken 统一 Key 接入、本地验证、到提交审核的完整链路就走完了。插件本身不复杂真正值钱的是这套「前端插件 统一 API 通道 用户自填 Key」的结构换成公众号排版、素材整理、本地自动化逻辑都能复用。