#!/usr/bin/env node /** * worker.js 本地开发/验证服务器。 * * 为什么需要它:Worker 只有在部署到 Cloudflare 之后才会真正接管 * cli.3216gemini.dpdns.org —— 万一代理逻辑写错,你的域名当场就废了。 * 这个脚本用 Node 起一个本地 HTTP 服务,**原样执行同一个 worker.js** * (直接 import,不做任何改写),把它转发到真实的 HF Space, * 于是你可以在本地先把每一条路径验完,再决定要不要 deploy。 * * 用法: * node dev-server.mjs # 监听 127.0.0.1:8787 * PORT=9000 node dev-server.mjs * ORIGIN=https://other-space.hf.space node dev-server.mjs * * 然后另开一个终端: * curl -i http://127.0.0.1:8787/healthz # 应返回网关的 JSON * curl -i http://127.0.0.1:8787/ # 应是 302 → /panel/(不被跟随) * curl -s http://127.0.0.1:8787/__cron # 手动跑一次保活定时器(看日志) * * 注意: * - 本脚本会真实请求 HF Space。若 Space 正在休眠,第一次 /healthz 会等 * 30~60s 冷启动,这是正常现象(也正是 cron 保活要解决的问题)。 * - /__cron 是本地专用路径,永远不会被代理到源站,部署后的 Worker 里没有它。 * - 绝不把 .dev.vars 提交进版本库(见同目录 README)。 */ import http from "node:http"; import { Readable } from "node:stream"; import worker from "./worker.js"; const PORT = Number(process.env.PORT || 8787); const HOST = process.env.HOST || "127.0.0.1"; const CRON = process.env.CRON || "*/5 * * * *"; /** 把 Node 的 IncomingMessage 头转成 Web Headers。 */ function toWebHeaders(nodeHeaders) { const h = new Headers(); for (const [name, value] of Object.entries(nodeHeaders)) { if (value === undefined) continue; if (Array.isArray(value)) { for (const v of value) h.append(name, v); } else { h.set(name, value); } } return h; } const server = http.createServer(async (req, res) => { try { exphost: { // 本地专用:手动触发一次 scheduled(),用来验证保活/重试/日志逻辑。 if (req.url === "/__cron") { console.log(`[dev] 手动触发 scheduled() cron="${CRON}"`); await worker.scheduled({ cron: CRON }, process.env, {}); res.writeHead(200, { "content-type": "application/json; charset=utf-8" }); res.end(JSON.stringify({ ok: true, triggered: CRON }, null, 2) + "\n"); break exphost; } // 本地专用:探活,不会打到源站。 if (req.url === "/__ping") { res.writeHead(200, { "content-type": "text/plain" }); res.end("dev-server alive\n"); break exphost; } const url = new URL(req.url, `http://${HOST}:${PORT}`); const hasBody = req.method !== "GET" && req.method !== "HEAD"; // Node 要求流式请求体显式声明 duplex:"half";Cloudflare Workers 不需要, // 所以这个参数只在这里出现,worker.js 本身保持平台无关。 const request = new Request(url, { method: req.method, headers: toWebHeaders(req.headers), body: hasBody ? req : undefined, duplex: hasBody ? "half" : undefined, redirect: "manual", }); const response = await worker.fetch(request, process.env, {}); const outHeaders = {}; for (const [k, v] of response.headers) outHeaders[k] = v; res.writeHead(response.status, outHeaders); if (response.body) { // 关键:流式回传,不缓冲 —— SSE 必须这样,否则流式体验当场报废。 await new Promise((resolve, reject) => { Readable.fromWeb(response.body).pipe(res).on("finish", resolve).on("error", reject); }); } else { res.end(); } } } catch (err) { console.error("[dev] 处理请求失败:", err); if (!res.headersSent) { res.writeHead(502, { "content-type": "application/json; charset=utf-8" }); } res.end(JSON.stringify({ error: "dev_server_error", detail: String(err && err.message) }) + "\n"); } }); server.listen(PORT, HOST, () => { console.log(`[dev] worker.js 本地验证服务: http://${HOST}:${PORT}`); console.log(`[dev] 源站 ORIGIN = ${process.env.ORIGIN || "(默认 https://a3216-gcli2api.hf.space)"}`); console.log("[dev] 试试:"); console.log(`[dev] curl -i http://${HOST}:${PORT}/healthz`); console.log(`[dev] curl -i http://${HOST}:${PORT}/`); console.log(`[dev] curl -s http://${HOST}:${PORT}/__cron`); });