ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

我的浏览器新标签页,终于接上 TaoToken 了!

我的浏览器新标签页,终于接上 TaoToken 了! 1. 从新标签页扩展的密钥困境说起做 Chrome 新标签页扩展的人大多会经历一个相似的阶段功能越加越多密钥越管越乱。我自己的 DailyViewTab 从快捷启动、天气、壁纸一路加到每日一词和番茄钟最开始每个功能模块都单独写了一段请求逻辑天气用一套 Key每日一词用另一套后来想加个 AI 摘要卡片又得再塞一个 endpoint 进去。结果就是background.js里散落着三四个不同的apiKey常量改一次配置要翻五六个文件本地调试和生产环境还经常对不上。这个问题的本质不是代码写得差而是扩展程序天然是一个多入口、多生命周期的运行环境。新标签页chrome_url_overrides.newtab是一个页面popup 是另一个页面service worker 又是独立的后台线程它们各自发请求、各自读配置。如果没有一个统一的出口密钥就会像野草一样到处长。更麻烦的是Chrome 扩展的存储分chrome.storage.local、chrome.storage.sync和内存变量三种同步策略不一致时你在设置页改了 Key新标签页那边可能还拿着旧的。我试过的最笨的办法是把 Key 硬编码在config.js里打包前手动替换。这个方案在只有一两个 Key 的时候勉强能用但只要涉及多模型、多环境就会立刻崩掉。后来我把所有请求收敛到一个request.js封装层endpoint 和 Key 全部从统一配置读取情况才好转。而真正让这套结构稳定下来的是把 endpoint 统一指向 TaoToken 的 API 地址这样无论扩展里有多少个功能模块它们面对的都是同一个 Base URL 和同一套鉴权方式。TaoToken 在这里扮演的角色是一个兼容 OpenAI 接口规范的统一入口。它的 API 地址是https://taotoken.net/api你拿到的 Key 可以同时用于对话、补全等不同模型调用不需要为每个模型单独申请一套凭证。对于浏览器扩展这种功能模块多、但每个模块请求量都不大的场景来说这种统一性带来的维护收益非常明显。你可以在官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content了解它的整体能力但本文的重点不是介绍它是什么而是怎么把它接进一个真实的 Chrome 扩展里并且让 401 错误彻底消失。适合读这篇的人正在写或维护 Chrome 扩展、新标签页、popup 类工具需要在扩展端发起 AI 请求并且已经被多工具各自维护密钥折磨过的开发者。如果你只是想在网页里调一次 API那用 fetch 就够了但扩展程序的权限模型、跨域策略、存储机制都和普通网页不同这才是需要专门处理的地方。接下来的内容会按真实开发顺序走先讲清楚扩展端请求为什么会 401再给出 manifest 权限配置和请求封装的可复制片段然后用一次真实调用验证 401 是否消失最后把常见的报错逐个排查一遍。全程代码都可以直接拿去改。2. TaoToken 前置准备扩展端请求的鉴权基础在把 TaoToken 接进扩展之前有几个前置概念必须先理清楚否则后面配 manifest 和写封装时会一头雾水。这一节不涉及注册流程的注水内容只讲和扩展开发直接相关的部分。首先是Base URL 和路径拼接规则。TaoToken 的 API 根地址是https://taotoken.net/api兼容 OpenAI 的接口风格。也就是说如果你要调对话补全完整地址是https://taotoken.net/api/v1/chat/completions如果要调模型列表是https://taotoken.net/api/v1/models。这里有个容易踩的坑很多人在封装时把 Base URL 写成https://taotoken.net/api/v1然后在拼接时又加了一次/v1结果变成/api/v1/v1/chat/completions直接 404。我的建议是Base URL 只写到/api版本号在具体请求路径里补这样最不容易出错。其次是Key 的存放位置。Chrome 扩展里绝对不要把 Key 写进manifest.json或任何会被打包进content script的文件因为 content script 运行在网页上下文里用户打开 DevTools 就能看到。正确的做法是把 Key 存在chrome.storage.local里由 service worker 或扩展页面新标签页、popup读取后再发请求。chrome.storage.local的数据不会同步到其他设备也不会暴露给网页是扩展端存密钥的合理位置。如果你需要跨设备同步配置可以用chrome.storage.sync但要注意它有 8KB 的单条限制而且同步的是明文敏感 Key 还是放 local 更稳妥。第三是扩展的请求发起位置。Chrome 扩展有三类上下文可以发网络请求service worker后台、扩展页面新标签页/popup/options、content script。其中 content script 受网页的 CORS 策略约束而 service worker 和扩展页面在host_permissions声明了目标域名后可以跨域请求。所以AI 请求应该放在 service worker 或扩展页面里发不要放在 content script 里。新标签页扩展的天然优势是新标签页本身就是一个扩展页面它可以直接发请求不需要绕道 content script。第四是manifest 版本的选择。现在 Chrome 已经全面推行 Manifest V3service worker 取代了原来的 background page。V3 的 service worker 是事件驱动的随时可能被浏览器休眠所以不要把 Key 或请求状态存在 service worker 的全局变量里每次请求前从chrome.storage读一次或者用chrome.storage.onChanged监听变化。这一点在调试 401 时特别重要因为 service worker 重启后全局变量会丢失如果你把 Key 存在变量里重启后就会变成 undefined请求自然 401。最后是模型 ID 的确认。TaoToken 支持多种模型具体可用的 Model ID 需要以你账号下的模型列表为准。你可以在扩展的设置页里做一个拉取模型列表的按钮请求https://taotoken.net/api/v1/models把返回的data[].id渲染成下拉框让用户自己选。这样比硬编码模型名更灵活也避免了模型下线后请求失败。如果你只是想快速验证先用一个确定可用的 Model ID 跑通链路再去做下拉框。把这四点理清楚之后扩展端的结构就清晰了Key 存 storage请求在扩展页面或 service worker 发Base URL 统一指向 TaoTokenModel ID 从模型列表动态获取。下一节就按这个结构给出可复制的配置。3. 可复制配置manifest 权限与请求封装片段这一节是全文最核心的部分所有片段都可以直接复制到你的扩展项目里。我会按manifest 权限 → 配置存储 → 请求封装 → 调用示例的顺序给出每一步都说明为什么这么写。3.1 manifest.json 的权限声明Manifest V3 里跨域请求需要在host_permissions里声明目标域名。如果你只请求 TaoToken声明它的域名即可{ manifest_version: 3, name: DailyViewTab, version: 1.0.0, permissions: [storage], host_permissions: [https://taotoken.net/*], background: { service_worker: background.js, type: module }, chrome_url_overrides: { newtab: newtab.html }, action: { default_popup: popup.html } }这里有几个关键点。permissions里必须有storage否则chrome.storage.local用不了。host_permissions里写https://taotoken.net/*注意是https且带/*这样 service worker 和新标签页发请求时才不会被 CORS 拦截。如果你还想请求其他域名比如天气 API一并加进去用逗号分隔。background.type设为module是为了能用 ES module 的import语法组织请求封装如果你的项目不用模块化可以去掉这行。3.2 统一配置的存储结构在chrome.storage.local里我用一个taotoken_config对象存所有相关配置结构如下{ taotoken_config: { baseUrl: https://taotoken.net/api, apiKey: sk-你的Key, modelId: 你的模型ID, timeout: 30000 } }写入配置的代码放在 options 页面或设置弹窗里// options.js async function saveConfig(config) { await chrome.storage.local.set({ taotoken_config: config }); console.log(配置已保存, config); } // 读取配置 async function loadConfig() { const result await chrome.storage.local.get(taotoken_config); return result.taotoken_config || null; }注意baseUrl只写到/api不带/v1。apiKey就是你在 TaoToken 控制台创建的 Key创建入口在https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。modelId建议从模型列表动态获取后填入不要硬编码。3.3 请求封装request.js这是整个扩展的请求出口所有 AI 调用都走这里。它负责读配置、拼 URL、加鉴权头、处理超时和错误// request.js const DEFAULT_CONFIG { baseUrl: https://taotoken.net/api, timeout: 30000 }; async function getConfig() { const result await chrome.storage.local.get(taotoken_config); return { ...DEFAULT_CONFIG, ...(result.taotoken_config || {}) }; } export async function chatCompletion(messages, options {}) { const config await getConfig(); if (!config.apiKey) { throw new Error(未配置 API Key请先在设置页填写); } if (!config.modelId) { throw new Error(未配置 Model ID请先从模型列表选择); } const url ${config.baseUrl}/v1/chat/completions; const controller new AbortController(); const timer setTimeout(() controller.abort(), config.timeout); try { const response await fetch(url, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${config.apiKey} }, body: JSON.stringify({ model: config.modelId, messages, ...options }), signal: controller.signal }); if (!response.ok) { const errText await response.text(); throw new Error(请求失败 ${response.status}: ${errText}); } const data await response.json(); return data.choices?.[0]?.message?.content ?? ; } finally { clearTimeout(timer); } } export async function listModels() { const config await getConfig(); const url ${config.baseUrl}/v1/models; const response await fetch(url, { headers: { Authorization: Bearer ${config.apiKey} } }); if (!response.ok) { throw new Error(拉取模型失败 ${response.status}); } const data await response.json(); return data.data?.map(m m.id) ?? []; }这段封装里有几个设计决策值得说明。第一getConfig每次请求前都从 storage 读而不是缓存到模块变量这样 service worker 休眠重启后不会拿到旧值。第二用AbortController做超时控制避免请求卡死。第三错误信息里带上response.status和响应体这样 401 和 404 能一眼区分。第四listModels单独封装方便设置页调用。3.4 在新标签页里调用新标签页是扩展页面可以直接import这个封装// newtab.js import { chatCompletion } from ./request.js; async function summarize(text) { try { const reply await chatCompletion([ { role: system, content: 你是一个简洁的摘要助手。 }, { role: user, content: 请用一句话总结${text} } ]); document.getElementById(summary).textContent reply; } catch (err) { console.error(摘要失败, err); document.getElementById(summary).textContent 摘要生成失败 err.message; } }如果你把请求放在 service worker 里新标签页通过chrome.runtime.sendMessage转发也可以但新标签页直接调用更简单少一层消息传递。唯一要注意的是新标签页的newtab.html里引入newtab.js时要用typemodule否则import会报错script typemodule srcnewtab.js/script到这里配置和封装就齐了。下一节用一次真实调用验证 401 是否消失。4. 验证请求一次真实调用确认 401 消失配置写完之后最怕的就是看起来都对但一跑就 401。这一节给出一个可复现的验证流程从最小请求开始逐步确认链路通畅。4.1 先用 curl 验证 Key 本身有效在把 Key 填进扩展之前先用命令行确认这个 Key 在 TaoToken 侧是有效的。这一步能排除掉Key 本身有问题的可能curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: 你的模型ID, messages: [{role: user, content: 说一句你好}] }如果返回里有choices[0].message.content说明 Key 和模型 ID 都没问题问题一定出在扩展端。如果这里就返回 401那说明 Key 填错了或者已失效去控制台重新创建一个。如果返回 404多半是模型 ID 不对用curl https://taotoken.net/api/v1/models -H Authorization: Bearer sk-你的Key拉一下可用列表。4.2 在扩展里打日志确认配置读取正确在request.js的getConfig后面加一行临时日志async function getConfig() { const result await chrome.storage.local.get(taotoken_config); const config { ...DEFAULT_CONFIG, ...(result.taotoken_config || {}) }; console.log([TaoToken] 当前配置, { baseUrl: config.baseUrl, hasKey: !!config.apiKey, keyPrefix: config.apiKey ? config.apiKey.slice(0, 6) : null, modelId: config.modelId }); return config; }打开新标签页按 F12 看 Console。如果hasKey是 false说明设置页没保存成功或者你保存的 key 名不是taotoken_config。如果keyPrefix显示的不是你 Key 的开头说明读到了旧配置。这一步能快速定位配置没读到类问题。4.3 触发一次真实请求并观察结果在新标签页的控制台里手动调一次import(./request.js).then(async ({ chatCompletion }) { const reply await chatCompletion([ { role: user, content: 用一句话介绍你自己 } ]); console.log(回复, reply); });如果一切正常你会看到模型返回的一句话同时 Network 面板里那条chat/completions请求的状态码是 200。如果还是 401看 Network 面板里请求头的Authorization字段是否真的带上了Bearer sk-...。常见情况是请求发出去了但Authorization头是空的说明config.apiKey在拼接时是 undefined。4.4 确认 401 消失的判断标准判断 401 是否真正消失不能只看这次没报错要看三个信号同时成立Network 面板里请求状态码是 200Response 里有合法的choices数组Console 里没有请求失败 401的报错。三个都满足才算链路通了。如果状态码是 200 但choices为空那是模型返回格式问题不是鉴权问题要单独排查。我实测下来401 最常见的三个原因是Key 没存进 storage、请求头拼写错误比如写成Authentication而不是Authorization、Base URL 多拼了一层/v1导致请求打到了错误路径。这三个都在下一节的排查清单里。5. 本篇常见错误排查401、local proxy failed 与 reading choices这一节把扩展端接 TaoToken 时最常撞到的报错逐个拆开给出定位方法和修复动作。每个报错都对应真实的控制台输出你可以直接对照。5.1 401 Unauthorized鉴权头缺失或 Key 无效控制台典型输出请求失败 401: {error:{message:Invalid API key,type:invalid_request_error}}排查顺序先在 Network 面板点开那条请求看 Request Headers 里有没有Authorization: Bearer sk-xxx。如果没有说明封装里没带上检查headers对象是否写对注意是Authorization不是AuthenticationBearer和 Key 之间有一个空格。如果有这个头但还是 401把同一个 Key 拿到 curl 里试curl 也 401 就是 Key 本身失效去控制台重新创建。如果 curl 能通但扩展 401检查是不是读到了旧配置清一下chrome.storage.local再存一次。还有一种隐蔽情况Key 是从网页复制时带了首尾空格或换行。在保存前做一次trim()config.apiKey config.apiKey.trim();5.2 local proxy failed请求根本没发出去控制台典型输出Failed to fetch net::ERR_FAILED或者在某些封装库里显示local proxy failed。这个报错的意思是请求在到达 TaoToken 之前就失败了通常有三个原因。第一host_permissions没声明https://taotoken.net/*Manifest V3 会直接拦截跨域请求。第二请求发在了 content script 里受网页 CORS 限制。第三扩展的 service worker 被浏览器休眠后全局变量丢失导致 URL 拼成了undefined/v1/...。修复动作确认 manifest 权限、把请求移到扩展页面或 service worker、每次请求前重新读配置。5.3 reading choices响应结构不符合预期控制台典型输出TypeError: Cannot read properties of undefined (reading choices)这个报错说明response.json()返回的对象里没有choices字段。原因通常是请求返回了错误响应比如 401 或 429但代码没检查response.ok就直接取choices。修复方法是在封装里先判断状态码if (!response.ok) { const errText await response.text(); throw new Error(请求失败 ${response.status}: ${errText}); } const data await response.json(); return data.choices?.[0]?.message?.content ?? ;用可选链?.兜底即使结构异常也不会抛 TypeError而是返回空字符串方便上层处理。5.4 OAuth 相关报错误用了网页登录态控制台典型输出OAuth token expired或者请求被重定向到登录页。这个报错一般出现在你误把网页端的登录态当成了 API Key。TaoToken 的 API 调用用的是Authorization: Bearer sk-xxx这种 Key 鉴权不是 OAuth 流程。如果你在扩展里用了chrome.identity拿到的 token 去请求就会撞上这个。修复动作去控制台创建 API Key用 Key 而不是 OAuth token。创建入口在https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。5.5 三件套检查清单如果你用的是 CC Switch、Cline MCP 或 Codex 这类工具配置里必须同时写全三件套缺一个都会失败配置项值说明Base URLhttps://taotoken.net/api不带/v1API Keysk-...从控制台创建Model ID从/v1/models获取不要硬编码在扩展的settings.json或auth.json里这三项要一一对应。如果只填了 Key 没填 Base URL请求会打到默认地址如果只填了 Base URL 没填 Model ID请求会因缺少 model 字段被拒。三件套齐全是链路通的最低要求。6. 把统一出口留在扩展里写到这里扩展端的接入链路已经完整了manifest 声明权限storage 存配置request.js 做统一封装新标签页直接调用401 用 curl 和 Network 面板两步定位。这套结构最大的好处不是能调通而是以后加任何新功能都只需要在 request.js 里复用同一个出口不用再为每个模块单独配 Key。如果你后续要做更复杂的编码类功能比如让扩展里的 AI 帮你生成代码片段、做多轮对话可以考虑用 Coding Plan 这类长期方案入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。如果只是想快速验证某个模型的效果直接在模型对话页试就行https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content里面有各语言的调用示例。最后留一个我踩过的坑扩展更新后chrome.storage.local里的旧配置不会自动清空。如果你改了配置结构比如把apiKey改名成key旧数据还在新代码读不到就会一直 401。解决办法是在onInstalled事件里做一次版本迁移或者干脆在设置页加一个重置配置按钮。这个坑不常遇到但遇到时很难查因为代码看起来完全正确。
返回列表