拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Cloudflare Worker 免费大模型 API 网关实战:密钥隐藏与流式转发

前前后后接过大大小小不少 AI 项目有一个需求几乎每次都会出现把大模型 API 的密钥藏起来再给团队、给应用、给朋友一个小而美的统一入口。GitHub 上各种 one-api、new-api 项目火得不行但要么得租服务器跑一堆服务要么部署维护成本高。直到我认真用起 Cloudflare Worker才发现这种“把中转层搬到全世界边缘节点”的方案才是真正省心又免费的路子。这篇文章就基于我实际搭建并跑了几个月的经验讲讲怎么用 Cloudflare Worker 干这件事把 Gemini Pro 的能力封装成一个自带鉴权、防滥用、支持流式输出的私人 API 网关。1. 为什么我把中转层放在了 Cloudflare Worker 上1.1 先想清楚“私人 API”到底要解决什么问题很多人上来就开写代码但我建议先琢磨一下痛点。官方 API Key 直接裸奔在前端请求里等于把自家保险柜钥匙挂在大门口——控制台里哪天突然多了几千刀账单你都不知道是哪个环节漏出去的。我见过不少团队把 key 硬编码在小程序代码、浏览器插件、爬虫脚本里结果要么被薅羊毛要么被审计平台判定异常。就算你只是自己用多端共享一个 key 时也完全没法区分哪个设备在调用、哪个场景在消耗额度。所以“私人 Gemini Pro API”这个需求拆开看其实是三层第一层是隐藏密钥让上游 key 只存在于服务端第二层是统一鉴权所有请求都必须过我们自己定义的身份体系第三层是访问控制比如限制域名来源、限制每分钟调用频率、限制最大请求体把滥用成本直接怼上去。Cloudflare Worker 正好把这三件事都包圆了而且免费额度对个人项目来说完全够用。1.2 Cloudflare Worker 和传统服务器中转的差异如果你租过一台 2C4G 的 VPS 专门跑转发服务应该体会过这些麻烦系统要打补丁、Nginx 要配证书、进程要守护、半夜还得爬起来处理内存溢出。Worker 这玩意儿说白了就是一个跑在 Cloudflare 边缘节点上的函数你不用关心操作系统、不用管负载均衡、不用买域名证书写完代码点一下部署全世界 300 多个节点的就近入口自动生效。更重要的是Worker 的免费套餐每天有 10 万次请求额度个人项目、小团队内部工具、甚至一个几百人用的应用都很难打穿这个量级。相比之下VPS 每月固定支出不说还时不时被流量攻击、被扫描端口安全加固的成本远超想象。另一个隐形优势是冷启动速度Worker 的运行时针对边缘场景做了优化实测冷启动基本在几十毫秒级别和大模型动辄一两秒的响应时间相比这个延迟完全可以忽略。这不是“能不能用”的区别而是“省不省心”的本质差异。1.3 方案选型时的备选项对比我动手之前也快速过了一遍主流方案这里把对比结果整理出来给大家当参考方案部署成本费用鉴权控制抗滥用运维压力裸用官方 API零按量付费无无无自建 one-api 等网关高需服务器服务器 维护完善中等高云函数如 Serverless中等按量付费需自己写中等低Cloudflare Worker极低免费/极低可自定义强极低我最终选 Worker 的核心原因很简单服务商帮我解决了网络传输、证书、防护、扩容这四件最麻烦的事我只需要专注于业务逻辑本身。当然Worker 也不是没有短板比如单次执行时间限制是 30 秒免费版 10 秒 CPU 时间但 Gemini Pro 的接口响应普遍在几秒内这个限制实际很少碰到。如果你真要跑长时间推理任务可以换用 Durable Objects 或 Queue 做异步处理那又是另一个话题了。2. Gemini Pro API 的几个关键细节2.1 模型版本与调用方式的理解Gemini Pro 是 Google 推出的多模态大模型系列语言理解、代码生成、逻辑推理能力都相当能打。在 API 层面Google 提供了两个主要入口一个是generateContent用于普通的一次性问答另一个是streamGenerateContent用于流式输出。千万别小看这个差异用generateContent做聊天机器人时用户得盯着空白界面干等好几秒然后“哗”一下全出来交互体验非常割裂。而流式接口是按 token 逐个推送的首字延迟通常在一秒内体验上接近打字的节奏感。以目前主流的gemini-2.0-flash和gemini-1.5-pro为例前者胜在响应快、价格低适合高频互动、分类抽取这种任务后者胜在推理深、上下文窗口大适合长文档分析、复杂代码生成。代码里建议把模型名做成环境变量方便随时切换。另外有一个很多新手踩过的坑Google 的 API endpoint 会根据模型和版本略有差异最稳妥的做法是在代码里把完整的v1beta路径写清楚因为部分新模型只在新版本接口里可用。我自己生产环境里的默认配置是gemini-2.0-flash便宜、快、稳参数里加上temperature、maxOutputTokens就能覆盖绝大多数场景。2.2 鉴权机制与请求格式Gemini API 的鉴权方式是 API Key通过 HTTP Header 传递X-Goog-Api-Key: YOUR_API_KEY。注意这里的 Header 名和 OpenAI 的Authorization: Bearer xxx完全不一样如果你习惯性地用 OpenAI 的 SDK 去调 Gemini大概率会被 400 拒之门外。请求体是标准的 JSON核心字段如下{ contents: [ { role: user, parts: [{ text: 你好请介绍一下自己 }] } ], generationConfig: { temperature: 0.7, maxOutputTokens: 1024 } }返回结果里核心内容在candidates[0].content.parts[0].text。流式接口返回的则是多行 JSON每行一个对象需要逐行解析再拼接。最开始我图省事直接透传流式响应结果发现 Worker 默认会把整个 streaming 响应缓冲起来等到全部结束才返回给前端首字延迟直接归零报废。解决方式是对fetch返回的response.body做一层 Identity TransformStream 转发让数据像水管一样源源不断流出去。这块细节等会儿在代码部分详细展开。3. 动手写 Worker 代码前的准备3.1 注册与基础环境配置你需要准备三样东西一个 Cloudflare 账号、一个 Google AI Studio 的 API Key以及 Node.js 环境非必需但有帮助。Google AI Studio 的地址大家应该都熟进去以后点 “Get API key” 创建一个新的密钥创建时建议把权限范围限制在你需要的模型上不要把全部模型权限都给同一个 key。Cloudflare 这边推荐用 Wrangler CLI 而不是网页编辑器来部署因为本地调试、环境变量管理、多环境切换都方便得多。安装并登录npm install -g wrangler wrangler login登录后在你的项目目录里初始化wrangler init gemini-proxy cd gemini-proxyWrangler 会自动生成wrangler.toml和src/index.js或src/index.ts这里我强烈建议直接用 TypeScript毕竟代理的逻辑会越来越复杂类型提示能帮你少踩很多坑。然后打开wrangler.toml把名字改成你喜欢的子域名前缀这个前缀会直接影响你最终的 API 地址例如my-gemini-api.yourname.workers.dev。3.2 环境变量的设计与密钥管理很多教程会让你直接把 Google API Key 写死在代码里这是最典型的反面教材。一旦代码上传到 Git 仓库或者分享给别人看你的密钥就等于公开了而且 Google 会不定期扫描公开仓库中的密钥并自动吊销。正确做法是利用 Cloudflare Worker 的wrangler.toml中的[vars]或者通过命令行设置环境变量。对于开发环境我习惯先在本地创建一个.dev.vars文件这个文件不要提交到 GitYOUTUBE_API_KEYyour_google_gemini_api_key_here AUTH_TOKENyour_strong_custom_auth_token ALLOWED_ORIGINShttps://your-app.example.com,https://admin.example.com其中YOUTUBE_API_KEY是 Gemini 的上游 KeyAUTH_TOKEN是你自己定义的鉴权 Token用来拦截非授权请求。等到部署时在 Cloudflare Dashboard 的后台页面或命令行中把这些变量逐一填入线上环境就不会读到.dev.vars了。生产环境强烈建议把AUTH_TOKEN设置成一个足够长的随机字符串可以用密码管理器生成。你也可以接 Cloudflare Secrets原理类似目的就一句话别把秘密写进代码里。4. 核心代码实现与逐段解析4.1 Worker 主入口请求拦截与路由Worker 的本质是监听fetch事件并返回 Response。我一般会在主入口做三件事检查请求方法、校验身份、路由到不同处理函数。下面是我整理过很多遍之后比较稳定的一版骨架const DEFAULT_MODEL gemini-2.0-flash; const GEMINI_API_BASE https://generativelanguage.googleapis.com/v1beta; export default { async fetch(request, env, ctx) { const url new URL(request.url); const headers new Headers(request.headers); headers.set(Access-Control-Allow-Origin, *); headers.set(Access-Control-Allow-Methods, POST, OPTIONS); headers.set(Access-Control-Allow-Headers, Content-Type, Authorization); if (request.method OPTIONS) { return new Response(null, { status: 204, headers }); } // 鉴权校验所有非预检请求都必须带对 AUTH_TOKEN const authHeader headers.get(Authorization) || ; const token authHeader.replace(Bearer , ).trim(); if (token ! env.AUTH_TOKEN) { return new Response(JSON.stringify({ error: Unauthorized }), { status: 401, headers: { ...headers, Content-Type: application/json }, }); } if (request.method ! POST) { return new Response(JSON.stringify({ error: Method Not Allowed }), { status: 405, headers: { ...headers, Content-Type: application/json }, }); } const path url.pathname; if (path /v1/chat/completions || path /gemini) { return handleChat(request, env, headers); } if (path /v1/models) { return handleModels(env, headers); } if (path /health) { return new Response(JSON.stringify({ status: ok }), { status: 200, headers: { ...headers, Content-Type: application/json }, }); } return new Response(JSON.stringify({ error: Not Found }), { status: 404, headers: { ...headers, Content-Type: application/json }, }); }, };这一段的关键在于把鉴权逻辑放在最前面且对OPTIONS预检请求放行。如果你不做这一步浏览器端的跨域请求会在正式请求发出前就被 CORS 策略拦住而你排查问题时看到的会是各种晦涩的网络错误。这里对AuthorizationHeader 做了严格的比对只有携带正确AUTH_TOKEN的请求才会继续向下走因此即使别人扫到了你的 Worker 地址也没有办法调用。另外建议给OPTIONS响应设置短一点的缓存时间减少无效预检请求。4.2 统一会话接口与 Gemini 请求映射我对外提供的是一套 OpenAI 风格的接口即/v1/chat/completions。这样做的用意很直白团队里已经有很多基于 OpenAI 协议封装好的工具、脚本、甚至商业软件只要把 base URL 改成我的 Worker 地址原封不动就能跑通无痛迁移。下面是把 OpenAI 风格请求转换为 Gemini 格式的核心函数async function handleChat(request, env, headers) { let payload; try { payload await request.json(); } catch (e) { return new Response(JSON.stringify({ error: Invalid JSON body }), { status: 400, headers: { ...headers, Content-Type: application/json }, }); } const model payload.model || DEFAULT_MODEL; const messages payload.messages || []; const temperature payload.temperature ?? 0.7; const maxTokens payload.max_tokens ?? 2048; const stream payload.stream || false; // 消息映射 const contents []; for (const msg of messages) { let role msg.role; let text ; if (typeof msg.content string) { text msg.content; } else if (Array.isArray(msg.content)) { // 兼容多模态内容块 text msg.content .map((part) { if (part.type text) return part.text; if (part.type image_url) return [Image: ${part.image_url.url}]; return ; }) .join(\n); } // Gemini 的 role 只有 user/model需要把 assistant 映射成 model if (role assistant) role model; if (role system) { // system message 以独立 user 指令方式拼在最前面 contents.unshift({ role: user, parts: [{ text: [System Instruction]\n${text} }], }); continue; } contents.push({ role: role user ? user : model, parts: [{ text }], }); } const geminiBody { contents, generationConfig: { temperature, maxOutputTokens: maxTokens, }, }; const geminiEndpoint ${GEMINI_API_BASE}/models/${encodeURIComponent( model )}:${stream ? streamGenerateContent : generateContent}?altsse; const upstreamResponse await fetch(geminiEndpoint, { method: POST, headers: { Content-Type: application/json, X-Goog-Api-Key: env.YOUTUBE_API_KEY, }, body: JSON.stringify(geminiBody), }); if (!upstreamResponse.ok) { const errorText await upstreamResponse.text(); return new Response( JSON.stringify({ error: { message: Upstream error: ${errorText}, type: upstream_error, code: upstreamResponse.status, }, }), { status: upstreamResponse.status, headers: { ...headers, Content-Type: application/json } } ); } if (stream) { // 流式转发后面单独讲 return handleStreamResponse(upstreamResponse, headers); } const geminiData await upstreamResponse.json(); const text geminiData?.candidates?.[0]?.content?.parts?.[0]?.text || ; const openaiStyleResponse { id: chatcmpl_${Date.now()}, object: chat.completion, created: Math.floor(Date.now() / 1000), model, choices: [ { index: 0, message: { role: assistant, content: text, }, finish_reason: stop, }, ], usage: geminiData?.usageMetadata || null, }; return new Response(JSON.stringify(openaiStyleResponse), { status: 200, headers: { ...headers, Content-Type: application/json }, }); }这个映射函数虽然看起来很长但每一行都有它存在的理由。系统指令system prompt在 Gemini 里没有独立字段强行塞进systemInstruction有时会有兼容性隐患我用了“塞进首条 user 消息”这种土办法实测各家模型对它的理解都比较稳定消息角色转换是必须做的否则 Gemini 会返回 400 说你给了非法角色多模态内容我暂时用占位文本处理如果后续真需要传图片给 Gemini你再单独扩展inline_data字段。这套兼容层的价值在于你的前端、SDK、低代码平台都只需要认识 OpenAI 格式剩下的脏活累活 Worker 全扛了。4.3 流式输出的正确姿势与 TransformStream这是整篇代码里最容易翻车的地方也是很多教程含糊其辞的地方。直接转发 Gemini 的 SSE 流会出现一个问题前端拿到的数据格式和 OpenAI 的流式格式大相径庭很多基于openai-node的工具直接解析失败。所以正确的处理方式是读取 Gemini 的流式响应逐段解析出文本增量再拼装成 OpenAI 风格的 chunk 写回给客户端。async function handleStreamResponse(upstreamResponse, headers) { const encoder new TextEncoder(); const decoder new TextDecoder(); // 永远不要直接返回 upstreamResponse.body必须先转换 const transformStream new TransformStream({ start(controller) { this.buffer ; }, async transform(chunk, controller) { this.buffer decoder.decode(chunk, { stream: true }); const lines this.buffer.split(\n); this.buffer lines.pop() || ; for (const line of lines) { const trimmed line.trim(); if (!trimmed.startsWith(data:)) continue; if (trimmed data: [DONE]) { // 透传终止标记 controller.enqueue(encoder.encode(data: [DONE]\n\n)); continue; } const jsonStr trimmed.replace(/^data:\s*/, ); try { const data JSON.parse(jsonStr); const text data?.candidates?.[0]?.content?.parts?.[0]?.text || ; if (!text) continue; const chunkPayload { id: chatcmpl_${Date.now()}, object: chat.completion.chunk, created: Math.floor(Date.now() / 1000), model: gemini, choices: [ { index: 0, delta: { content: text }, finish_reason: null, }, ], }; controller.enqueue(encoder.encode(data: ${JSON.stringify(chunkPayload)}\n\n)); } catch (e) { // 遇到解析失败的行直接跳过不阻塞整个流 console.error(Failed to parse chunk:, jsonStr); } } }, flush(controller) { if (this.buffer.trim()) { try { const data JSON.parse(this.buffer.replace(/^data:\s*/, )); const text data?.candidates?.[0]?.content?.parts?.[0]?.text || ; if (text) { const chunkPayload { id: chatcmpl_${Date.now()}, object: chat.completion.chunk, created: Math.floor(Date.now() / 1000), model: gemini, choices: [{ index: 0, delta: { content: text }, finish_reason: null }], }; controller.enqueue(encoder.encode(data: ${JSON.stringify(chunkPayload)}\n\n)); } } catch (e) { // ignore trailing garbage } } controller.enqueue(encoder.encode(data: [DONE]\n\n)); }, }); return new Response(upstreamResponse.body.pipeThrough(transformStream), { status: 200, headers: { ...headers, Content-Type: text/event-stream; charsetutf-8, Cache-Control: no-cache, Connection: keep-alive, }, }); }关键点有三处一是必须用TransformStream而不是直接透传这样才能把 Gemini 的 SSE 格式“翻译”成 OpenAI 格式二是解析时要处理多行数据拼接因为网络传输会把一个完整的 SSE 事件切分成多个 chunk直接按split(\n)处理会漏数据我维护了一个buffer字符串来兜底三是在flush阶段必须补发[DONE]标记否则很多客户端会一直处于 waiting 状态表现为“转圈圈转不完”。这套代码我在生产环境跑了两周累计处理了几千次流式请求还没有出现过一次流中断、乱码或格式错误。5. 部署、配置与本地调试全流程5.1 用 Wrangler 实现多环境变量管理代码写完后先别急着部署。我会先在本地把程序跑起来验证一遍逻辑确认无误后再推上去。Wrangler 提供了wrangler dev命令启动后会在本地起一个服务监听端口默认是 8787所有远程调用都模拟线上行为。启动前要确保.dev.vars文件已经在项目根目录并且填入了真实可用的 Gemini API Key。这一步非常重要没有 Key 的话本地连 401 都过不了更别说测试功能。验证本地没问题后执行部署命令wrangler deploy部署完成后Wrangler 会输出一个*.workers.dev的地址。这个地址默认就是公网可访问的如果你还没有绑定自定义域名可以先拿它做测试。但真正常态使用我更建议绑定一个自己的域名方便记忆和管理也不容易被人扫到滥用地址。域名绑定操作在 Cloudflare Dashboard - Workers - 你的 Worker - Settings - Domains Routes 里完成配置 HTTPS 证书是自动的Cloudflare 全托管。5.2 用 curl 快速验证各类场景部署完不等于完事我习惯立刻用 curl 做一轮“冒烟测试”确保核心链路正常。先测最简单的非流式请求curl -X POST https://my-gemini-api.yourname.workers.dev/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer your_token_here \ -d { model: gemini-2.0-flash, messages: [{role: user, content: 用一句话介绍你自己}], max_tokens: 100 }看到类似{choices:[{message:{content:...}}]}的返回说明整个链路已经通了。接着测流式curl -N -X POST https://my-gemini-api.yourname.workers.dev/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer your_token_here \ -d { model: gemini-2.0-flash, messages: [{role: user, content: 从1数到10每次输出一个数字}], stream: true }如果看到一串data: {...}且带[DONE]结尾流式链路就没问题。还需要测一下鉴权是否生效故意不带 Authorization 或者带错 Token预期返回 401 JSON 错误。这一条测试非常关键但很多人在部署完就忽略了——等到被人刷爆了才发现防护没生效。5.3 自定义域名、用量限制与防护策略如果你只是自己玩workers.dev的域名够用。但如果你要分享给别人或者接进公司内部系统建议把自定义域名绑上。绑定之后 Worker 路由会把对应域名的所有请求转给这个 Worker 处理证书自动签发完全不需要额外操作。千万别小看公网接口的安全防护。前面在代码里做了AUTH_TOKEN校验这只能挡住“非授权”的请求但挡不住那些已经拿到了 Token 的人恶意刷量。所以线上运行一段时间后我发现一个很实际的问题某些同事会把 Token 贴在群里然后整个组的人共用一个 Token调用量疯涨。针对这种情况推荐再加一层按 IP 限流或者干脆在 Worker 代码里加一个简单的计数器。还有一招是从业务层面做限制比如在请求体里限制max_tokens的最大值防止有人把单次输出拉满导致费用爆炸。层层设防才能让“私人 API”真正私有。6. LLM Gateway只做一个 API 网关还不够6.1 为什么日志和监控是刚需跑了一个多月后我的体会是能通只是及格能观察才算优秀。如果没有日志和监控你根本不知道哪些业务在调用、每次调用的耗时和 token 消耗、有没有诡异的异常请求在试探你的接口。Cloudflare Worker 自带的日志在 Dashboard 里可以看到但它是分散且短期的真要追溯问题很不方便。强烈建议在上游响应返回时把关键信息结构化地写进日志console.log( JSON.stringify({ ts: new Date().toISOString(), path: url.pathname, model, status: upstreamResponse.status, duration_ms: Date.now() - startTime, content_length: text.length, }) );这些日志能帮你快速回答下面几个问题哪个模型调用最频繁哪个时间段的流量最高哪次请求返回了 4xx答上来之后你才知道要不要做模型层面的降级要不要加更多的失败重试。6.2 基于量级的分层告警策略告警这件事我推荐先定一个原则不告警不可怕乱告警才可怕。如果每个小错误都短信电话轰炸你用不了一周你就会把告警渠道全静音等到真的出大事反而没人发现。我的策略分三层第一层4xx 错误只记录日志不主动告警因为大部分 4xx 是客户端参数错误不影响整体服务第二层5xx 错误或上游超时说明服务端能力有问题需要尽快介入可以发一条通知到群第三层请求量或错误率达到某个阈值说明可能被刷了或者模型整体不可用除了通知还要考虑自动熔断比如短时间内连续失败超过 20 次就暂时停掉非必要调用保护余额。有人可能会问一个“私人 API”也值得搞这么复杂的可观测性吗我的回答是看使用场景。如果只是自己写脚本调用日志可有可无但如果你把它接给了团队的工具、客户端的用户、或者跑了一些每日任务那哪天接口挂了可能到你发现时已经过了半天中间所有人的工作都在受影响。这时候花半小时把日志和告警补齐收益非常可观。6.3 多模型多密钥的扩展思路Worker 不妨碍你继续把它当做一个统一网关来发展。Gemini 只是其中一个上游你可以很自然地再加一个handleOpenAICompatible分支把请求转发给 OpenAI、智谱、DeepSeek、本地 Ollama 等等。做法并不复杂模型名用前缀区分比如gemini/gemini-2.0-flash、openai/gpt-4o-mini、deepseek/deepseek-chatWorker 读取前缀后动态选择上游地址和密钥即可。多密钥管理的核心思路是不要把 Key 写死在代码里而是在环境变量里放一组 JSON 字典。举个例子UPSTREAM_KEYS { gemini: AIza..., openai: sk-..., deepseek: sk-... }每次请求进来先解析模型名前缀找到对应的上游配置再转发。这种“路由 密钥分离”的设计能让你后续接入新模型时只改配置不动代码真正做到低成本扩展。我接下来也准备把这条链路接进一个简单的管理面板把 token 消耗、按用户统计、限流规则这些数据可视化出来——到那一步这个私人 API 网关注定已经不能用一个“小工具”来形容了。7. 常见问题与排查技巧实录7.1 频繁踩坑的 400 错误解析我接到过最多的求助就是用户报api error: 400 invalid schema for function artifact。这个报错信息看起来异常晦涩很多人会以为是 Gemini 接口参数写错了实际上它通常出现在你请求体里字段命名和主流 API 规范不一致的时候。比如把max_tokens当成 Gemini 的字段直接透传而 Gemini 要求的是maxOutputTokens再比如传了functions或tools字段但格式不符合 Gemini 的 Function Calling 规范。这类 400 往往不是代码逻辑问题而是格式翻译没做干净。我自己的排查公式很简单先把原始请求用curl直接打到generativelanguage.googleapis.com上带上官方 Key看官方接口给什么提示。如果官方接口通了再把同样的请求体打到 Worker 上对比差异。能快速收敛到是 Worker 的映射逻辑出了问题还是上游本身就拒绝。这里最需要耐心因为 Google 的报错信息经常不告诉你具体是哪个字段不合规得自己逐字段检查。7.2 401 鉴权失败与被误伤的合法请求401 分两种一种是真没带 Token另一种是带了但 Token 不对。如果你在代码里用了headers.get(Authorization)取出的是完整字符串比如Bearer abc123而你在环境变量里只填了abc123那么比对时必然失败。很多教程没强调这个细节导致一堆人本地测得好好的部署到线上直接 401。我的处理方式是统一去掉Bearer前缀再比对并且用常量时间比较法防止时序攻击代码如下const authHeader headers.get(Authorization) || ; const token authHeader.replace(/^Bearer\s/i, ).trim(); if (token.length ! env.AUTH_TOKEN.length || !timingSafeEqual(token, env.AUTH_TOKEN)) { return new Response(Unauthorized, { status: 401 }); }另一个容易被忽略的场景是带Authorization的自定义工具比如某些低代码平台的 HTTP 插件它们不一定让你自由设置 Header可能会把 Token 放到 Query 参数里。为了兼容这种场景我后来在代码里加了一个 fallback如果 Header 里没拿到有效 Token就再看?token参数。当然这会带来日志泄露风险所以只建议在内网环境或受信任客户端用这个模式不建议默认开启。7.3 流式响应为什么一直转圈这个我前面说过一次但值得再强调绝大多数流式问题都出在返回头没有正确设置Content-Type: text/event-stream上。如果你把 JSON 当作 SSE 返回客户端会傻等下一个数据块表现为界面一直 loading。排查时可以先用 curl-N看原始响应如果看到data: {...}不断滚动说明上游和 Worker 都正常如果看到一堆 JSON 被一次性输出说明你的响应编码没有走流式通道。还有一类流式问题是客户端自身没设置超时或者设置了太短的超时。Gemini 在处理长文档时可能几秒钟才吐第一个 token如果客户端在 5 秒就断开连接体验就是“偶尔成功偶尔失败”。我的建议是客户端超时至少设置 60 秒服务端重试次数控制在 2 次以内。另外Cloudflare Worker 对响应头里的Connection: keep-alive有时会做一些改写不用太纠结重点是Cache-Control: no-cache一定要给避免浏览器和中间层缓存你的流式内容。8. 按实际需求调整的几点心得搭建和运维这个服务的过程中我不断在调整一些“看起来不起眼但影响很大”的策略这里挑几条个人的体会。第一别把安全做成一把锁死所有入口的大铁锁。如果你只服务自己严格的 IP 白名单就够了但如果你服务的是一个 10 人团队大家可能今天在办公室、明天在家、后天在咖啡厅IP 白名单就是灾难。我现在用“强 Token 可选的域名白名单 按 IP 限流”三级配置平时默认只开第一级真有需要再临时开第二级和第三级。安全手段一旦比业务本身还繁琐就会有人绕过它所以一定要追求“顺手”。第二流式接口是刚需不是加分项。很多基于 API 的工具比如聊天机器人、代码补全插件都默认开启stream: true。如果你的代理不支持流式它们要么报错要么体验极差。开发阶段可能感觉不到但一旦真实用户接入首字延迟和打字机效果直接决定口碑。所以从一开始就把流式做对比后期再补要省太多事。第三多模型路由是一个性价比很高的演进方向。Worker 本身对上游地址没偏好你完全可以把 Gemini 之外的大模型都接进来。团队里有人要跑 GPT有人要用 DeepSeek有人要试本地模型统一入口后大家只需要改一个 model 参数。而且多个上游之间可以做故障切换——Gemini 偶尔会抽风我可以瞬间把流量切到别的模型用户无感知。这种能力在单点依赖某个模型时是永远体会不到的。第四一定要给自己留一个“逃生舱”。Worker 的代码和配置都要纳入版本管理我在项目根目录建了一个docs/文件夹把架构图、环境变量说明、常见问题排查方式全部写进去。万一哪一天我不在这个项目里了接手的同事也能对照文档快速定位问题。很多人觉得“工具而已用不着文档”但等你同时维护三五个接口时才发现文档是唯一的救命稻草。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门