panrouter 5.1.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/pool-worker.mjs +60 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
package/pool-worker.mjs CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Pan Router Pool Worker — 事件驱动版
4
+ * Pan Router Pool Worker — 懒汉心跳版
5
5
  *
6
6
  * 以 Sidecar 模式运行,守护 cloudflared 隧道进程。
7
- * 状态改变时才通知主控(事件驱动),崩溃自动复活,关机优雅告别。
7
+ * - 没注册上:死缠烂打,每 10 秒重试直到主控应答
8
+ * - 注册上了:绝对静默,既不轮询也不发心跳
9
+ * - 每 5 分钟发一次全量对齐(防止主控重启丢了节点)
10
+ * - 隧道崩了自动复活,关机优雅告别
8
11
  */
9
12
 
10
13
  import { spawn, spawnSync } from "node:child_process";
@@ -23,33 +26,20 @@ const AUTH_SECRET = "jiuling-super-secret-2026";
23
26
  const NODE_ID = os.hostname() + "-worker-" + Math.floor(Math.random() * 1000);
24
27
  const SERVER_PORT = 50816;
25
28
  const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
29
+ const ALIGN_INTERVAL = 5 * 60 * 1000; // 5 分钟全量对齐
26
30
 
27
31
  let cfProcess = null;
28
32
  let isShuttingDown = false;
33
+ let isConfirmed = false; // 主控是否已确认注册
29
34
  let currentUrl = "";
35
+ let alignTimer = null;
30
36
 
31
37
  function log(msg, type = "INFO") {
32
38
  const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
33
39
  console.log(`${icons[type] || "▪"} [节点] ${msg}`);
34
40
  }
35
41
 
36
- // ─── 通知主控 ────────────────────────────────────────────────────────────────
37
- function notifyHub(status, url = "") {
38
- const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
39
- const req = https.request(MAIN_HUB_URL, {
40
- method: "POST",
41
- headers: {
42
- "Content-Type": "application/json",
43
- "Content-Length": Buffer.byteLength(payload),
44
- "x-secret-token": AUTH_SECRET,
45
- },
46
- });
47
- req.on("error", () => {});
48
- req.write(payload);
49
- req.end();
50
- }
51
-
52
- // 可等待的 notify — 用于首次上线确认
42
+ // ─── 可等待的通知(返回 true=200 应答) ─────────────────────────────────────
53
43
  function notifyHubWait(status, url = "") {
54
44
  return new Promise((resolve) => {
55
45
  const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
@@ -68,19 +58,54 @@ function notifyHubWait(status, url = "") {
68
58
  });
69
59
  }
70
60
 
71
- // 首次上线确认 失败则重试,最多 5 次
72
- async function confirmOnline(url, retries = 5) {
73
- const ok = await notifyHubWait("online", url);
74
- if (ok) {
75
- log("主控已确认节点注册", "OK");
76
- return;
77
- }
78
- if (retries > 0) {
79
- log(`注册尚未确认,3 秒后重试 (剩余 ${retries} 次)`, "INFO");
80
- await new Promise((r) => setTimeout(r, 3000));
81
- return confirmOnline(url, retries - 1);
61
+ // ─── 发后即忘的通知(离线遗言用,不等回执) ────────────────────────────────
62
+ function notifyHub(status, url = "") {
63
+ try {
64
+ const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
65
+ const req = https.request(MAIN_HUB_URL, {
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ "Content-Length": Buffer.byteLength(payload),
70
+ "x-secret-token": AUTH_SECRET,
71
+ },
72
+ });
73
+ req.on("error", () => {});
74
+ req.write(payload);
75
+ req.end();
76
+ } catch {}
77
+ }
78
+
79
+ // ─── 懒汉注册:死缠烂打直到主控应答 ─────────────────────────────────────────
80
+ async function aggressiveRegister(url) {
81
+ log("正在向主控注册...");
82
+ while (!isShuttingDown) {
83
+ const ok = await notifyHubWait("online", url);
84
+ if (ok) {
85
+ log("主控已确认节点注册", "OK");
86
+ isConfirmed = true;
87
+ startAlignHeartbeat(); // 注册成功 → 开启长间隔对齐
88
+ return;
89
+ }
90
+ log(`注册失败,10 秒后重试...`, "WARN");
91
+ await new Promise((r) => setTimeout(r, 10000));
82
92
  }
83
- log("注册确认已达最大重试次数", "WARN");
93
+ }
94
+
95
+ // ─── 5 分钟全量对齐心跳(主控重启后自动恢复) ──────────────────────────────
96
+ function startAlignHeartbeat() {
97
+ if (alignTimer) clearInterval(alignTimer);
98
+ alignTimer = setInterval(async () => {
99
+ if (isShuttingDown || !currentUrl) return;
100
+ const ok = await notifyHubWait("online", currentUrl);
101
+ if (ok) {
102
+ log("全量对齐完成", "HART");
103
+ } else {
104
+ log("全量对齐失败,主控可能已重启,重新注册...", "WARN");
105
+ isConfirmed = false;
106
+ aggressiveRegister(currentUrl); // 异步重入注册流程
107
+ }
108
+ }, ALIGN_INTERVAL);
84
109
  }
85
110
 
86
111
  // ─── 检查端口 ────────────────────────────────────────────────────────────────
@@ -171,8 +196,9 @@ function startTunnel() {
171
196
  const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
172
197
  if (match && match[0] !== currentUrl) {
173
198
  currentUrl = match[0];
199
+ isConfirmed = false; // 新 URL,重置确认状态
174
200
  log(`成功获取公网入口: ${currentUrl}`, "OK");
175
- confirmOnline(currentUrl); // 确保主控注册成功(异步重试)
201
+ aggressiveRegister(currentUrl); // 死缠烂打直到主控应答
176
202
  }
177
203
 
178
204
  // 网络波动提示
@@ -184,6 +210,8 @@ function startTunnel() {
184
210
  cfProcess.on("close", (code) => {
185
211
  log(`隧道进程断开退出 (code: ${code})`, "ERR");
186
212
  cfProcess = null;
213
+ isConfirmed = false;
214
+ if (alignTimer) { clearInterval(alignTimer); alignTimer = null; }
187
215
 
188
216
  // 通知主控剔除
189
217
  if (currentUrl) {