Download deploy/cloudflare-worker/README.md from a3216/gcli2api: direct link, hf CLI and curl.
- Browser
- Download file 17.3 kB
-
https://huggingface.co/spaces/a3216/gcli2api/resolve/main/deploy/cloudflare-worker/README.md
- Command line
-
hf download hf://spaces/a3216/gcli2api/deploy/cloudflare-worker/README.md
-
curl -L -o README.md https://huggingface.co/spaces/a3216/gcli2api/resolve/main/deploy/cloudflare-worker/README.md
WorkBuddy2API — Cloudflare Worker:反代 + 保活
把公网入口 cli.3216gemini.dpdns.org 反代到 Hugging Face Space
a3216/gcli2api(直连域名 https://a3216-gcli2api.hf.space),
并用 Cron 定时打 /healthz,防止 Space 被 HF 暂停。
这个 Worker 做什么
| 能力 | 说明 |
|---|---|
| 🔁 全量反向代理 | 方法 / 路径 / query / 请求体 / 端到端请求头原样转发,源站的状态码、响应头、响应体原样回传 |
| ⚡ 零缓冲流式 | 直接返回 originResponse.body,不调用 .text() / .json()。POST /v1/chat/completions 是 SSE 长连接,缓冲会直接毁掉流式体验 |
| ↩️ 不吞后端 302 | fetch(..., { redirect: 'manual' }),所以 GET / → /panel/ 的跳转是浏览器看到的,不会被 Worker 内部跟掉 |
| 🌐 CORS 全透传 | 后端已经返回宽松的 CORS 头,Worker 一律不自己造 Access-Control-*,避免出现重复头 |
| 🧹 逐跳头剥离 | 剥掉 host / connection / keep-alive / transfer-encoding / upgrade / proxy-* / cf-* / x-forwarded-*;Host 永远不会用公网域名转发过去 |
| 🍪 set-cookie 原样 | 不做 domain / path 改写,多条也完整保留 |
| ⏰ Cron 保活 | scheduled() 每 5 分钟 ping 一次源站 /healthz,25s 硬超时 + 失败重试 1 次 + 每次一行日志 |
| 🛡️ 不抛异常 | Cron 处理器内部吞掉所有异常,不会污染 Cron 运行记录 |
涉及的后端接口(都不需要 Worker 特殊处理,纯透传):
GET /healthz→{"healthy":2,"realm_servable":{...},"service":"workbuddy2api","total":3}(无可服务账号时 503)GET /→ 302 →/panel/GET /panel/→ 单页管理面板/panel/api/*→ 面板 JSON API,需Authorization: Bearer <api_key>POST /v1/chat/completions→ OpenAI 兼容,SSE 流式,需同一个 Bearer 密钥GET /v1/models、GET /status
为什么需要这个 Cron
Hugging Face 免费 cpu-basic Space 有一条硬规则:连续 48 小时没有任何 HTTP 流量就被暂停,
下一次访问要等 30–60 秒冷启动。
所以「反代」和「保活」其实是同一件事的两半:反代提供入口,Cron 保证入口后面那台机器不会睡着。
只要有任意 HTTP 请求打到 Space,HF 的闲置计时器就会归零——响应内容是什么完全不影响判断,
这就是为什么一次裸的 GET /healthz 就够了。
⚠️ 接管前必读:现有入口的 /healthz 是 Cloudflare 自己应答的
同一时刻实测对比:
| 目标 | /healthz 响应 |
判据 |
|---|---|---|
https://a3216-gcli2api.hf.space/healthz |
{"healthy":2,…,"service":"workbuddy2api"} |
带 x-service、x-proxied-replica,以及 HF 的 link: …/spaces/a3216/gcli2api 头 |
https://cli.3216gemini.dpdns.org/healthz |
ok(2 字节) |
只有 cf-ray;没有 x-service、没有 link |
结论:现有入口把 /healthz 短路了,请求根本没到 Space。 很可能是故意的 ——
避免探活 / 监控 / 负载均衡把 30~60s 冷启动打起来。它只短路了这一个路径:
/status、/panel/* 等仍是透传的(两边都带 HF 的 link 头,可自证)。
两个直接后果:
- 千万别拿
cli.3216gemini.dpdns.org/healthz做保活。 它会稳定返回 200、 看起来一切健康,但 Space 完全没被唤醒 —— 48 小时一到照样被暂停。 保活必须打 Space 的直连域名(见仓库根的.github/workflows/space-keepalive.yml)。 - 本 Worker 接管路由后,
/healthz的行为会变:它会把/healthz原样透传给 Space, 于是返回的是网关 JSON 而不是ok。对"真探活"来说这是更正确的 (能反映账号可用性),但如果你有监控依赖那个ok,请先调整它。 想让本 Worker 也短路/healthz,在worker.js的fetch()里加一条早返回即可。
⚠️ 免费版只有 5 个 Cron(最容易踩的坑)
Cloudflare 免费版的 cron 触发器限制是「每个账号 5 个」,不是每个 Worker 5 个。 如果你账号里别的 Worker 已经占了 5 个,再部署本 Worker 就会看到这种半成功:
Uploaded workbuddy2api-hf-proxy (1.63 sec)
Deployed workbuddy2api-hf-proxy triggers (1.62 sec)
✘ [ERROR] Trigger configuration for "workbuddy2api-hf-proxy" was only partially updated:
Cron schedules: ... failed.
- This account has reached the Workers Free limit of 5 cron triggers per account.
[code: 10072]
Successful trigger changes were not rolled back.
怎么读这段:Worker 的代码确实上传成功了,但 cron 没加上,而且
"Successful trigger changes were not rolled back" 只是说"已成功的部分不回滚"。
结果是这个 Worker 处于空转状态——没有 cron,它不会去唤醒任何人。
(* * * * * 这种多行 crons 数组里每一行都算一个触发器,别写成 5 行。)
两条出路:
出路 A:腾一个 CF cron 名额(保持 CF 原生方案)
Dashboard → Workers & Pages → 逐个 Worker 看 Settings → Triggers → Cron Triggers,
找出已经不用的那个删掉,然后重新 npx wrangler deploy。
想用 API 一次性列全账号的 cron(需要 CLOUDFLARE_API_TOKEN,权限含 Workers Scripts:Read):
ACC=a75c5f86fdd1b75eb3e860f5436a7970 # 你的 account id
for w in $(curl -s "https://api.cloudflare.com/client/v4/accounts/$ACC/workers/scripts" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | jq -r '.result[].id'); do
echo "== $w"
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACC/workers/scripts/$w/schedules" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | jq -c '.result.schedules'
done
出路 B:改用 GitHub Actions(推荐,无数量限制)
本仓库自带 .github/workflows/space-keepalive.yml,每 6 小时 ping 一次 Space,
零成本、不占 CF 名额。而且 6 小时一次完全够用——HF 的判定阈值是 48 小时,
所以 GitHub 定时任务偶发延迟几十分钟毫无影响(反过来说,本 Worker 里 */5 的
5 分钟粒度其实是严重过量的,纯粹因为免费额度够用才这么写)。
两条路不冲突:Worker 留着做反代,保活交给 Actions 也完全可以。
部署
先看上一节:如果你账号的 5 个 cron 名额已满,
wrangler deploy会报code: 10072, 这时按「出路 A/B」处理。
1. 前置条件
node -v # 需要 Node 18+
npx wrangler -v # 首次会提示安装 wrangler
npx wrangler login # 登录你的 Cloudflare 账号
2. 部署
cd D:\workbuddy2api-panel\deploy\cloudflare-worker
npx wrangler deploy
部署成功后 Worker 名称为 workbuddy2api-hf-proxy(见 wrangler.toml 的 name)。
3. 接线路由
wrangler.toml 里的 routes 默认是注释掉的,两种接法任选其一:
- A. 交给 wrangler:取消
routes注释后重新wrangler deploy,路由自动创建; - B. 在 Dashboard 手动加:Workers → 你的 Worker → Settings → Domains & Routes →
添加
cli.3216gemini.dpdns.org/*(推荐,避免误删线上路由)。
⚠️ 启用路由前确认该主机名当前没有指向别处的「已代理」DNS 记录, 否则 Worker 路由和 DNS 记录会互相打架。正确做法是该主机名由 Worker 路由接管 (DNS 里留一条指向
100::的 AAAA 占位记录)。
4. 设置密钥(可选)
只有开启「可选唤醒/签到」时才需要。密钥永远不要写进 wrangler.toml,也不要提交。
npx wrangler secret put WB2A_API_KEY
# 粘贴网关的 api_key(与面板登录用的是同一个),回车即可
未设置时 scheduled() 只做 /healthz 裸 ping,不发送任何鉴权请求。
验证
先接路由再验:下面这些 URL 打的是
cli.3216gemini.dpdns.org,只有在该主机名已经 路由到本 Worker 时才是"在验 Worker"。没接路由的话,你验到的仍是原来的链路。
*.workers.dev在部分网络下不可用(国内常见:DNS 被污染,解析到不相干的 IP、 443 不通)。所以wrangler deploy打印的https://<worker>.<sub>.workers.dev在你这里可能根本连不上——这不代表部署失败,用自定义域名的路由来验即可。
# 探活:能看到网关自己的 JSON,且带 total / service 字段
curl -i https://cli.3216gemini.dpdns.org/healthz
# 后端自带的 302 应该原样到达客户端(不要出现 -L 跟随)
curl -i https://cli.3216gemini.dpdns.org/
# 面板
curl -I https://cli.3216gemini.dpdns.org/panel/
# 流式:应逐块吐出,而不是憋到最后一次性返回
curl -N -H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-v4-flash","stream":true,"messages":[{"role":"user","content":"hi"}]}' \
https://cli.3216gemini.dpdns.org/v1/chat/completions
看到 server: cloudflare 与 cf-ray 属正常——那是 Cloudflare 边缘加的,不代表后端没响应;
后端的响应头(如 service、x-*)应当同时原样出现。
看日志
cd D:\workbuddy2api-panel\deploy\cloudflare-worker
npx wrangler tail # 实时请求日志
npx wrangler tail --format pretty # 人类可读格式
wrangler tail报Error: Unexpected server response: 400? 它走的是 WebSocket,经过本地抓包代理(DevSidecar / Reqable 之类)时经常被挡掉。 换成这几条路即可:
- Dashboard → Workers & Pages → 你的 Worker → Logs → Live / Cron Events(最省事);
- 关掉代理再
npx wrangler tail;- 或者干脆不依赖 tail:Cron 有没有在跑,看 Space 的
last_checkin_day有没有变成当天 最直接(桶里的data/state.json),或看npx wrangler deployments list的更新时间。
Cron 每次运行会打一行:
[keepalive] 2025-01-01T00:05:03.412Z /healthz -> HTTP 200 (183ms)
失败时是 warn 级别,同时带上首次失败原因,便于区分「偶发抖动」和「真的没醒」:
[keepalive] 2025-01-01T00:05:03.412Z /healthz -> FAILED status=0 (25013ms) first=ping timeout detail=ping timeout
Cron 粒度说明
[triggers] crons = ["*/5 * * * *"]= 每 5 分钟一次,这是 Cloudflare Cron Triggers 的最小粒度(不支持每分钟);- 频率换算:每 5 分钟 ≈ 288 次/天,对比 48 小时暂停阈值,余量极大;
- Cron 触发时间有秒级抖动(官方说明可能延迟数秒到数十秒),对保活完全无影响;
- 如果只是防休眠,不需要再加密;调成
*/30之类也够用,但 5 分钟最稳。
本地调试
推荐:先用 dev-server.mjs 把代理逻辑验完,再考虑 deploy。 它用 Node 起一个本地服务,
原样 import 这个 worker.js(不做任何改写),转发到真实的 HF Space——不需要 wrangler、
不需要登录:
cd deploy/cloudflare-worker
node dev-server.mjs # 监听 127.0.0.1:8787
# PORT=9000 node dev-server.mjs # 换端口
# ORIGIN=https://other.hf.space node dev-server.mjs
另开一个终端逐条验:
curl -i http://127.0.0.1:8787/ # 期望 302 → /panel/(不要加 -L)
curl -i http://127.0.0.1:8787/healthz # 期望网关自己的 JSON(带 total / service)
curl -i http://127.0.0.1:8787/status # 期望 401 invalid_api_key(说明真的打到了网关)
curl -s http://127.0.0.1:8787/__cron # 手动跑一次保活定时器,看控制台日志
/__ping 与 /__cron 是本地专用路径,永远不会被代理到源站,部署后的 Worker 里没有它们。
也可以走 wrangler(需要它自己的本地环境):
npx wrangler dev
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*" # 临时触发 Cron 处理器
.dev.vars
本地调试需要的变量(尤其是 WB2A_API_KEY)放在同目录 .dev.vars 里,不要提交:
cp .dev.vars.example .dev.vars # 然后按需填写
格式为 KEY="value",一行一个。wrangler dev 会自动读取该文件中的变量与密钥。
✅
.gitignore已经覆盖到位(根目录规则):.dev.vars/.dev.vars.*/.wrangler/全部被忽略,同时用!.dev.vars.example把模板放回版本库(模板里只有占位符)。 四个可提交文件:worker.js、wrangler.toml、.dev.vars.example、本README.md。任何 secret 都不得提交,线上一律走
wrangler secret put。
环境变量一览
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ORIGIN |
vars | https://a3216-gcli2api.hf.space |
源站地址。换 Space 只改这里 |
ROOT_REDIRECT |
vars(注释中) | /panel/ |
GET / 的跳转目标;设成空串则关闭跳转、直接透传后端的 404 |
HEALTH_PATH |
vars(注释中) | /healthz |
保活探测路径 |
CHECKIN_CRON |
vars(注释中) | 空 = 关闭 | 只在此 Cron 表达式触发时做签到 |
CHECKIN_PATH |
vars(注释中) | 空 = 关闭 | 带 Bearer 调用的路径,如 /panel/api/keepalive |
WB2A_API_KEY |
secret | 未设置 | 面板/网关 Bearer 密钥,仅签到功能需要 |
ORIGIN若填成非法 URL,Worker 会回落到默认源站而不是整体 500——配置拼错时仍能提供服务。
如果你已有自己的反代 Worker
不要替换掉你现有的反代逻辑,只把「保活」那一小块搬过去即可,改动量极小:
加
scheduled()处理器:从worker.js里复制scheduled(event, env, ctx)整个函数, 连同它依赖的DEFAULT_ORIGIN/HEALTH_PATH/PING_TIMEOUT_MS/PING_RETRY_DELAY_MS常量、resolveOrigin()/normalizePath()两个工具函数一起拿走。 (可选签到那段如果不需要,连同CHECKIN_*一起删掉即可。)**确认你的默认导出里有
scheduled**:必须是export default { fetch(request, env, ctx) {...}, scheduled(event, env, ctx) {...} }这种形状。 如果你原来是export default { async fetch(...) },直接把scheduled加进同一个对象即可, 不要改成export { scheduled }之外的写法。在
wrangler.toml加上 trigger 块:[triggers] crons = ["*/5 * * * *"]重新部署:
npx wrangler deploy。改[triggers]必须重新部署才会生效, 只改 Dashboard 里的 Cron 是临时的、下次部署会被覆盖。
反代部分如果你已经有实现,保持原样——本 Worker 的 fetch() 只是一个满足
「不缓冲 / 不跟随 302 / 剥离逐跳头 / 透传 CORS」四条硬约束的参考实现,
你的版本只要满足同样四条,效果完全等价。
常见问题
代理后 /v1/chat/completions 变成一次性返回,没有流式效果?
几乎一定是某处调用了 await res.text() / res.json() / 中间做了缓冲(包括自己包一层
TransformStream 却忘了 flush)。确认直接 return new Response(originResponse.body, ...)。
GET / 返回的是 /panel/ 的内容,看不到 302?
说明 fetch 的 redirect: 'manual' 丢了。默认的 redirect: 'follow' 会在 Worker 内部
把 302 跟掉,客户端就永远看不到那次跳转。
GET / 到底该返回 302 还是后端原始的 404?
302 → /panel/。 这是实测出来的既有行为,值得记一笔:网关只注册了 /panel/,
所以直连 Space 的 GET / 是 404;但公网入口 https://cli.3216gemini.dpdns.org/
返回的却是 302 → /panel/。同一路径两种结果,说明这层跳转是 CF 侧加的(Worker 或
Redirect Rule)。worker.js 因此也自带了这个跳转(ROOT_REDIRECT,默认 /panel/),
换用本 Worker 后直接打开域名首页的体验不变。
想对比两种行为:
curl -i https://a3216-gcli2api.hf.space/ # 404(后端真实行为)
curl -i https://cli.3216gemini.dpdns.org/ # 302 → /panel/(入口层加的)
想把跳转关掉(严格透传后端):把 ROOT_REDIRECT 设成空串。
浏览器报 CORS 重复头错误?
Worker 自己造了 Access-Control-Allow-Origin,而后端也返回了同一个头。
删掉 Worker 里的 CORS 处理,让它纯透传。
Cron 日志里一直是 503,是不是保活失败了?
不是。 503 是后端「当前没有可用账号可服务」的业务语义,但只要它回了 HTTP,
Space 就是醒着的,保活目的已经达到。只有 status=0(超时/网络失败)才需要关注——
那通常意味着正在冷启动,或者 Space 真的挂了。
想确认 Cron 到底有没有在跑?
npx wrangler tail --format pretty # 等 5 分钟就能看到 [keepalive] 行
或者 Dashboard → Workers → 你的 Worker → Logs → 筛选 Cron Events。