☰
我用 Codex 从 0 做了一个 Chrome 插件,还真的提交审核了:TaoToken 统一 Key 接入实录
2026/10/9 17:16:13 网站建设 项目流程

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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。创建完复制出来,形如sk-开头的一串。这个 Key 在插件里绝对不能写进前端代码,正确做法是让用户自己在插件的 options 页面填,存到chrome.storage.local,background 请求时再读出来。

第三是 Model ID。这个必须和你账号里可用的模型对上,填错会直接报 model not found。你可以在模型对话页面先验证一下模型能不能正常回话,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。我习惯先在对话页发一句「你好」确认通道通,再写进插件,省得在插件里 debug 半天发现是模型名写错了。

如果你后面要做的是长期编码类、Agent 类的插件(比如自动改代码、批量处理),可以看 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。但这次做的是轻量总结类插件,用普通 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_BASE,Key 和 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/api,Key 填你的 Key,Model ID 填你要用的模型。Claude Code 的接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,里面有具体的 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」的结构,换成公众号排版、素材整理、本地自动化,逻辑都能复用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询