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`);
});