File size: 4,596 Bytes
6d60378 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | #!/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`);
});
|