1. 为什么 Chrome 插件许可证服务必须独立部署
做 Chrome 插件的人迟早会碰到同一个问题:Pro 版本怎么校验。我见过太多插件把支付平台的 API Key 直接写进background.js或者content.js,然后打包上传到商店。这种做法在技术上能跑通,但安全模型是崩的。扩展代码最终会落到用户浏览器里,压缩和混淆只是提高阅读成本,不是加密。任何人打开 DevTools 的 Sources 面板,或者把 crx 解包,都能看到里面的接口地址、请求参数,甚至明文密钥。
所以正确的结构是三层:Chrome 插件只负责收集用户输入的 License Key,然后请求你自己的服务端;服务端持有真正的支付平台密钥,去和许可证提供商校验;校验结果再返回给插件。插件永远不接触支付平台的 Key,它只知道一个 Worker 地址。
这个服务端用 Cloudflare Workers 非常合适。它免费额度够个人产品用,冷启动几乎无感,全球边缘节点,而且天然支持环境变量注入密钥。你不需要维护一台服务器,也不需要担心证书和扩容。
但光有 Worker 还不够。许可证服务涉及 API Key、环境变量、部署版本、回滚。如果每次改代码都手动wrangler deploy,时间一长必然出现「本地代码和线上版本不一致」的问题。这时候就需要 GitOps:Git 仓库是部署状态的唯一来源,代码合并到主分支就自动部署,每次线上变更都能回溯到某个 commit。
这篇内容要解决的就是这条链路:用 Cursor 辅助写 Worker 代码和配置文件,用 GitHub Actions 做自动部署,用 TaoToken 统一管理模型调用凭据,最后用 curl 验证许可证接口真的返回了正确结果。适合正在做 Chrome 插件商业化、或者想把小型服务纳入自动化部署的独立开发者。
2. TaoToken 统一 Key 接入:把模型调用凭据从代码里拿出来
在讲 Worker 部署之前,先解决一个容易被忽略的问题:许可证服务本身可能也要调用大模型。比如你想在激活时做风控判断、或者给用户生成一段个性化的欢迎文案、或者用模型解析支付平台的异常返回。这些调用都需要 API Key。
如果每个服务各自维护一套 Key,很快就会乱:有的写在.env,有的塞进 GitHub Secrets,有的直接硬编码。更麻烦的是,当你同时用多个模型供应商时,Key 的格式、Base URL、模型 ID 都不一样,切换成本很高。
TaoToken 在这里的作用是提供一个统一的 API 通道。你只需要在 TaoToken 控制台创建一个 Key,然后所有模型调用都走同一个 Base URL 和同一个 Key,模型 ID 在请求体里指定。这样 Worker 的环境变量里只需要放一个TAOTOKEN_API_KEY,而不是五六个不同平台的密钥。
具体操作路径是这样的:先到官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册账号,然后进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 创建 API Key。创建完成后,在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 可以看到完整的 Key 字符串,复制保存好,它只会完整显示一次。
如果你只是想先验证模型能不能通,可以直接用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 发一条消息测试。这个页面不需要写代码,适合确认 Key 有效、模型可用。
对于长期做编码和 Agent 的场景,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 会更划算,它针对高频调用做了额度优化。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各语言的调用示例。
API 的基础地址是https://taotoken.net/api,注意这个地址不带 UTM 参数,是给代码里用的。在 Worker 里调用时,请求结构大致是这样:
const resp = await fetch("https://taotoken.net/api/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${env.TAOTOKEN_API_KEY}` }, body: JSON.stringify({ model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "ping" }] }) });这里的关键点是:TAOTOKEN_API_KEY只存在于 Worker 的环境变量里,不出现在代码仓库,也不出现在 Chrome 插件里。插件端永远只请求你自己的 Worker 地址。
如果你用的是 Claude Code 这类工具,TaoToken 也提供了对应的接入方式,参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。核心还是三件套:Base URL 填https://taotoken.net/api,Key 填你创建的 Key,Model ID 按文档里的可用列表填。
3. 可复制配置:wrangler.toml、GitHub Actions 与 Worker 代码
这一节给出可以直接复制粘贴的配置。整个项目结构建议这样组织:
scraper-ai-license-server/ ├── src/ │ └── index.js ├── wrangler.toml ├── package.json └── .github/ └── workflows/ └── deploy.yml先看wrangler.toml。这个文件定义 Worker 的名称、入口、兼容日期,以及非敏感的环境变量。敏感密钥不写在这里,通过 Cloudflare 后台或wrangler secret设置。
name = "scraper-ai-license-server" main = "src/index.js" compatibility_date = "2026-06-01" [vars] LICENSE_PROVIDER = "creem" TAOTOKEN_BASE_URL = "https://taotoken.net/api" # 敏感变量不写在这里,用以下命令设置: # npx wrangler secret put CREEM_API_KEY # npx wrangler secret put TAOTOKEN_API_KEY注意compatibility_date要填一个已经发布的日期,不要填未来日期,否则 wrangler 会报错。[vars]里只放非敏感配置,比如供应商名称和 Base URL。
接下来是 Worker 主代码src/index.js。它处理/activate和/verify两个接口,统一 CORS,统一 JSON 响应,密钥全部从env读取。
function corsHeaders() { return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type" }; } function jsonResponse(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json", ...corsHeaders() } }); } async function activateLicense(request, env) { const { licenseKey, instanceId } = await request.json(); if (!licenseKey || !instanceId) { return jsonResponse({ valid: false, message: "missing licenseKey or instanceId" }, 400); } const providerResp = await fetch("https://api.creem.io/v1/licenses/activate", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": env.CREEM_API_KEY }, body: JSON.stringify({ license_key: licenseKey, instance_id: instanceId }) }); const providerData = await providerResp.json(); if (!providerResp.ok) { return jsonResponse({ valid: false, message: providerData.message || "provider error" }, 502); } return jsonResponse({ valid: true, activatedAt: Date.now(), plan: providerData.plan || "pro" }); } async function verifyLicense(request, env) { const { licenseKey, instanceId } = await request.json(); const providerResp = await fetch("https://api.creem.io/v1/licenses/validate", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": env.CREEM_API_KEY }, body: JSON.stringify({ license_key: licenseKey, instance_id: instanceId }) }); const providerData = await providerResp.json(); return jsonResponse({ valid: providerResp.ok && providerData.valid === true }); } export default { async fetch(request, env) { const url = new URL(request.url); if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders() }); } if (url.pathname === "/activate" && request.method === "POST") { return activateLicense(request, env); } if (url.pathname === "/verify" && request.method === "POST") { return verifyLicense(request, env); } return jsonResponse({ error: "Not found" }, 404); } };这段代码里,env.CREEM_API_KEY和env.TAOTOKEN_API_KEY都来自 Cloudflare 的加密环境变量,不会出现在 Git 仓库里。如果你要在激活流程里加模型调用,就在activateLicense里用env.TAOTOKEN_API_KEY去请求env.TAOTOKEN_BASE_URL。
然后是 GitHub Actions 工作流.github/workflows/deploy.yml。它监听 main 分支的 push,安装依赖后执行wrangler deploy。
name: Deploy License Worker on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: "20" - name: Install dependencies run: npm install - name: Deploy to Cloudflare Workers run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}CLOUDFLARE_API_TOKEN和CLOUDFLARE_ACCOUNT_ID配置在 GitHub 仓库的 Settings → Secrets and variables → Actions 里。API Token 在 Cloudflare 后台的 My Profile → API Tokens 创建,权限选「Edit Cloudflare Workers」即可。
package.json里只需要声明 wrangler 依赖:
{ "name": "scraper-ai-license-server", "version": "1.0.0", "private": true, "devDependencies": { "wrangler": "^3.80.0" } }这套配置的核心逻辑是:代码里没有任何明文密钥,所有敏感信息通过 Cloudflare Secrets 和 GitHub Secrets 注入。Git 仓库可以公开,也不会泄露。
4. 验证请求:push 触发部署与 curl 测试许可证接口
配置写完之后,验证分两步:先确认 push 能触发自动部署,再确认接口真的返回正确结果。
第一步,把代码提交到 main 分支:
git add . git commit -m "feat: add license worker with gitops deploy" git push origin mainpush 之后打开 GitHub 仓库的 Actions 标签页,应该能看到一个名为「Deploy License Worker」的工作流正在运行。点进去可以看到每一步的日志。如果wrangler deploy成功,最后会输出类似这样的内容:
Uploaded scraper-ai-license-server (1.23 sec) Deployed scraper-ai-license-server triggers (0.45 sec) https://scraper-ai-license-server.<your-subdomain>.workers.dev这个 URL 就是你的许可证服务地址。把它填到 Chrome 插件的LICENSE_API_BASE里。
第二步,用 curl 验证/activate接口。先测一个明显无效的 Key,确认错误处理正常:
curl -X POST https://scraper-ai-license-server.<your-subdomain>.workers.dev/activate \ -H "Content-Type: application/json" \ -d '{"licenseKey":"invalid-key-123","instanceId":"test-instance-001"}'预期返回:
{"valid":false,"message":"provider error"}状态码应该是 502 或 400,说明 Worker 正确地把错误透传出来了,而不是崩溃或者返回 200。
再用一个真实的测试 License Key 验证成功路径:
curl -X POST https://scraper-ai-license-server.<your-subdomain>.workers.dev/activate \ -H "Content-Type: application/json" \ -d '{"licenseKey":"YOUR_REAL_TEST_KEY","instanceId":"test-instance-001"}'预期返回:
{"valid":true,"activatedAt":1735689600000,"plan":"pro"}如果看到valid: true,说明整条链路是通的:curl → Cloudflare Worker → 支付平台许可证接口 → 返回结果。这时候再去 Chrome 插件里调用同一个接口,应该也能拿到相同结果。
再验证一下 CORS 预检请求:
curl -X OPTIONS https://scraper-ai-license-server.<your-subdomain>.workers.dev/activate \ -H "Origin: chrome-extension://abcdefg" \ -H "Access-Control-Request-Method: POST" \ -i响应头里应该包含Access-Control-Allow-Origin: *和Access-Control-Allow-Methods: POST, OPTIONS。如果没有这两个头,Chrome 插件里的 fetch 会被浏览器拦截。
最后验证/verify接口:
curl -X POST https://scraper-ai-license-server.<your-subdomain>.workers.dev/verify \ -H "Content-Type: application/json" \ -d '{"licenseKey":"YOUR_REAL_TEST_KEY","instanceId":"test-instance-001"}'返回{"valid":true}就说明校验接口也正常。到这里,一次完整的 push 触发部署 + curl 验证就完成了。
5. 本篇常见错排查:401、CORS、wrangler 报错与 OAuth 问题
这一节列出实际部署中最容易撞到的几个错误,以及对应的排查方向。
错误一:401 Unauthorized来自支付平台
现象是 curl 返回{"valid":false,"message":"provider error"},但你去 Cloudflare 后台看日志,发现请求确实发出去了,只是支付平台返回了 401。这通常说明CREEM_API_KEY没有正确设置,或者设置后没有重新部署。
排查步骤:先在 Cloudflare 后台进入 Workers & Pages → 你的 Worker → Settings → Variables and Secrets,确认CREEM_API_KEY存在且类型是 Secret。如果刚添加,需要重新触发一次部署才能生效。可以用npx wrangler secret list查看当前已设置的 secret 名称。
注意,wrangler secret put设置的变量不会出现在wrangler.toml里,这是正常的。如果你在本地用wrangler dev调试,需要在项目根目录建一个.dev.vars文件,里面写CREEM_API_KEY=xxx,但这个文件必须加入.gitignore。
错误二:local proxy failed或reading 'choices'报错
如果你在 Worker 里调用了 TaoToken 的模型接口,可能会遇到Cannot read properties of undefined (reading 'choices')。这通常是因为响应结构和你预期的不一样。先打印原始响应:
const raw = await resp.text(); console.log("raw response:", raw);常见原因是 Base URL 写错了。TaoToken 的 API 地址是https://taotoken.net/api,完整的 chat completions 路径是https://taotoken.net/api/v1/chat/completions。如果你只写了https://taotoken.net,就会 404,返回的就不是 JSON 结构,解析choices自然报错。
另一个原因是Authorization头格式不对。必须是Bearer <你的Key>,中间有一个空格。少了空格或者用了Token前缀都会 401。
错误三:CORS 报错No 'Access-Control-Allow-Origin' header
Chrome 插件里 fetch 报这个错,说明 Worker 没有正确处理 OPTIONS 预检。检查你的 Worker 代码里是否有这一段:
if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders() }); }这段必须放在路由判断的最前面。如果你把它放在/activate判断之后,OPTIONS 请求会先命中 404 分支,就不会返回 CORS 头。
另外,Access-Control-Allow-Headers要包含Content-Type,因为插件发的是 JSON 请求。如果你还传了自定义头,比如X-Instance-Id,也要加进去。
错误四:wrangler deploy报OAuth error或Authentication error
在 GitHub Actions 里跑wrangler deploy时,如果报 OAuth 相关错误,通常是因为CLOUDFLARE_API_TOKEN没有设置,或者 Token 权限不够。wrangler 在 CI 环境里不会走 OAuth 浏览器登录,它只认CLOUDFLARE_API_TOKEN环境变量。
排查:确认 GitHub Secrets 里有CLOUDFLARE_API_TOKEN和CLOUDFLARE_ACCOUNT_ID。API Token 的权限要包含Workers Scripts:Edit。如果 Token 创建时只给了读权限,部署会失败。
还有一个容易忽略的点:CLOUDFLARE_ACCOUNT_ID不是你的登录邮箱,而是 Cloudflare 后台右侧栏显示的 Account ID,一串十六进制字符。填错了会报account not found。
错误五:部署成功但接口 404
Actions 显示部署成功,但 curl 访问返回 404。先确认你访问的 URL 和部署日志里输出的 URL 完全一致。Workers 的默认域名是<worker-name>.<subdomain>.workers.dev,subdomain是你在 Cloudflare 注册时设置的,不是账号 ID。
如果 URL 没错,检查wrangler.toml里的main字段是否指向了正确的入口文件。如果main = "src/index.js"但实际文件在src/worker/index.js,wrangler 会部署一个空 Worker,所有请求都 404。
6. 语义一致 CTA:把 Key 管理和部署流程固定下来
整套流程跑通之后,你会发现真正花时间的不是写 Worker 代码,而是把密钥管理和部署流程固定成可重复的动作。Cursor 在这里的价值是帮你快速生成wrangler.toml、GitHub Actions YAML 和 Worker 路由代码的初稿,但密钥放哪里、怎么注入、怎么验证,这些决策还是得自己做。
我的建议是把三件事固定下来:第一,所有模型调用统一走 TaoToken 的 Base URL 和 Key,Worker 环境变量里只保留一个TAOTOKEN_API_KEY;第二,所有敏感信息通过 Cloudflare Secrets 和 GitHub Secrets 注入,代码仓库里不出现任何明文密钥;第三,每次部署后用 curl 跑一遍/activate和/verify,确认返回结构没变。
如果你还没创建 TaoToken 的 Key,可以从 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 开始。想先确认模型调用能不能通,用模型对话 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 发一条消息最快。接入细节和参数说明在文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 里。长期做编码和 Agent 的话,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 的额度更适合高频场景。
最后一步,把 Chrome 插件里的LICENSE_API_BASE改成你的 Worker 地址,然后在插件里调用/activate,确认chrome.storage.local里写入了licenseStatus: "pro"。到这一步,从 Cursor 写代码到 GitOps 自动部署再到插件端激活,整条链路就闭环了。