panrouter 5.0.3 → 5.1.1

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 +126 -107
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.0.3",
3
+ "version": "5.1.1",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
package/pool-worker.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Pan Router Pool Worker
5
- * 将当前设备自动注册为集群算力节点。
6
- * 启动 server.mjs 拉起 cloudflared 匿名隧道 → 心跳上报主控中心。
4
+ * Pan Router Pool Worker — 事件驱动版
5
+ *
6
+ * Sidecar 模式运行,守护 cloudflared 隧道进程。
7
+ * 状态改变时才通知主控(事件驱动),崩溃自动复活,关机优雅告别。
7
8
  */
8
9
 
9
10
  import { spawn, spawnSync } from "node:child_process";
@@ -23,16 +24,66 @@ const NODE_ID = os.hostname() + "-worker-" + Math.floor(Math.random() * 1000);
23
24
  const SERVER_PORT = 50816;
24
25
  const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
25
26
 
26
- let currentTunnelUrl = "";
27
- let cloudflared = null;
28
- let heartbeatTimer = null;
27
+ let cfProcess = null;
28
+ let isShuttingDown = false;
29
+ let currentUrl = "";
29
30
 
30
31
  function log(msg, type = "INFO") {
31
- const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓" };
32
+ const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
32
33
  console.log(`${icons[type] || "▪"} [节点] ${msg}`);
33
34
  }
34
35
 
35
- // ─── 检查端口是否已占用 ──────────────────────────────────────────────────────
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 — 用于首次上线确认
53
+ function notifyHubWait(status, url = "") {
54
+ return new Promise((resolve) => {
55
+ const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
56
+ const req = https.request(MAIN_HUB_URL, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ "Content-Length": Buffer.byteLength(payload),
61
+ "x-secret-token": AUTH_SECRET,
62
+ },
63
+ }, (res) => resolve(res.statusCode === 200));
64
+ req.on("error", () => resolve(false));
65
+ req.setTimeout(5000, () => { req.destroy(); resolve(false); });
66
+ req.write(payload);
67
+ req.end();
68
+ });
69
+ }
70
+
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);
82
+ }
83
+ log("注册确认已达最大重试次数", "WARN");
84
+ }
85
+
86
+ // ─── 检查端口 ────────────────────────────────────────────────────────────────
36
87
  function isPortOpen(port) {
37
88
  return new Promise((resolve) => {
38
89
  const req = http.get(`http://127.0.0.1:${port}/health`, () => {});
@@ -42,16 +93,15 @@ function isPortOpen(port) {
42
93
  });
43
94
  }
44
95
 
45
- // ─── 检查 cloudflared 是否可用 ────────────────────────────────────────────────
96
+ // ─── 查找 cloudflared ────────────────────────────────────────────────────────
46
97
  function findCloudflared() {
47
98
  const candidates = ["cloudflared", "cloudflared.exe"];
48
99
  for (const name of candidates) {
49
100
  try {
50
101
  const r = spawnSync(name, ["--version"], { stdio: "pipe", timeout: 3000 });
51
102
  if (r.status === 0) return name;
52
- } catch { /* try next */ }
103
+ } catch { /* next */ }
53
104
  }
54
- // 常见安装路径兜底
55
105
  const fallbackPaths = [
56
106
  path.join(process.env.USERPROFILE || "", "AppData", "Local", "cloudflared", "cloudflared.exe"),
57
107
  path.join(process.env.LOCALAPPDATA || "", "cloudflared", "cloudflared.exe"),
@@ -63,11 +113,10 @@ function findCloudflared() {
63
113
  return null;
64
114
  }
65
115
 
66
- // ─── 启动 server.mjs ─────────────────────────────────────────────────────────
116
+ // ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
67
117
  async function ensureServer() {
68
- const isOpen = await isPortOpen(SERVER_PORT);
69
- if (isOpen) {
70
- log(`端口 ${SERVER_PORT} 已有服务在运行,跳过启动`, "OK");
118
+ if (await isPortOpen(SERVER_PORT)) {
119
+ log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
71
120
  return true;
72
121
  }
73
122
 
@@ -86,7 +135,6 @@ async function ensureServer() {
86
135
  });
87
136
  child.unref();
88
137
 
89
- // 等待端口就绪
90
138
  for (let i = 0; i < 15; i++) {
91
139
  if (await isPortOpen(SERVER_PORT)) {
92
140
  log(`代理服务已就绪 (端口 ${SERVER_PORT})`, "OK");
@@ -99,141 +147,112 @@ async function ensureServer() {
99
147
  return false;
100
148
  }
101
149
 
102
- // ─── 启动 cloudflared 隧道 ────────────────────────────────────────────────────
150
+ // ─── 隧道守护 ────────────────────────────────────────────────────────────────
103
151
  function startTunnel() {
152
+ if (isShuttingDown) return;
153
+
104
154
  const cfPath = findCloudflared();
105
155
  if (!cfPath) {
106
- log("未找到 cloudflared,请先安装:\n Windows: scoop install cloudflared\n Termux: pkg install cloudflared\n macOS: brew install cloudflared", "ERR");
107
- return false;
156
+ log("未找到 cloudflared,请先安装", "ERR");
157
+ return;
108
158
  }
109
159
 
110
160
  log("正在请求匿名公网入口...");
111
161
 
112
- // 清理旧进程
113
- if (cloudflared) {
114
- try { cloudflared.kill(); } catch {}
115
- }
116
-
117
- cloudflared = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
162
+ cfProcess = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
118
163
  stdio: ["ignore", "pipe", "pipe"],
119
164
  windowsHide: true,
120
165
  });
121
166
 
122
- cloudflared.stderr.on("data", (data) => {
167
+ cfProcess.stderr.on("data", (data) => {
123
168
  const text = data.toString();
169
+
170
+ // 抓取隧道 URL
124
171
  const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
125
- if (match && match[0] !== currentTunnelUrl) {
126
- currentTunnelUrl = match[0];
127
- log(`成功获取公网入口: ${currentTunnelUrl}`, "OK");
128
- pushToHub();
172
+ if (match && match[0] !== currentUrl) {
173
+ currentUrl = match[0];
174
+ log(`成功获取公网入口: ${currentUrl}`, "OK");
175
+ confirmOnline(currentUrl); // 确保主控注册成功(异步重试)
129
176
  }
130
- });
131
177
 
132
- cloudflared.on("exit", (code) => {
133
- log(`cloudflared 进程退出 (code: ${code})`, code === 0 ? "INFO" : "ERR");
134
- cloudflared = null;
178
+ // 网络波动提示
179
+ if (text.includes("connection lost") || text.includes("Retrying")) {
180
+ log("隧道网络不稳定,正在尝试重连...", "WARN");
181
+ }
135
182
  });
136
183
 
137
- return true;
138
- }
139
-
140
- // ─── 心跳上报 ─────────────────────────────────────────────────────────────────
141
- function pushToHub() {
142
- if (!currentTunnelUrl) return;
143
-
144
- const payload = JSON.stringify({
145
- nodeId: NODE_ID,
146
- url: currentTunnelUrl,
147
- });
184
+ cfProcess.on("close", (code) => {
185
+ log(`隧道进程断开退出 (code: ${code})`, "ERR");
186
+ cfProcess = null;
148
187
 
149
- const req = https.request(
150
- MAIN_HUB_URL,
151
- {
152
- method: "POST",
153
- headers: {
154
- "Content-Type": "application/json",
155
- "Content-Length": Buffer.byteLength(payload),
156
- "x-secret-token": AUTH_SECRET,
157
- },
158
- },
159
- (res) => {
160
- let body = "";
161
- res.on("data", (c) => (body += c));
162
- res.on("end", () => {
163
- log(`状态已同步至主控中心 (${res.statusCode})`, "HART");
164
- });
188
+ // 通知主控剔除
189
+ if (currentUrl) {
190
+ notifyHub("offline");
191
+ currentUrl = "";
165
192
  }
166
- );
167
193
 
168
- req.on("error", (e) => log(`主控节点失联: ${e.message}`, "ERR"));
169
- req.write(payload);
170
- req.end();
171
- }
194
+ // 自动复活
195
+ if (!isShuttingDown) {
196
+ log("准备在 5 秒后自动重启隧道...", "WARN");
197
+ setTimeout(startTunnel, 5000);
198
+ }
199
+ });
172
200
 
173
- // ─── 写入 PID 文件 ────────────────────────────────────────────────────────────
174
- function writePid() {
175
- try {
176
- fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
177
- } catch {}
201
+ cfProcess.on("error", (err) => {
202
+ log(`隧道进程异常: ${err.message}`, "ERR");
203
+ });
178
204
  }
179
205
 
180
- function readPid() {
181
- try {
182
- return parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
183
- } catch {
184
- return null;
185
- }
186
- }
206
+ // ─── 优雅退出 ────────────────────────────────────────────────────────────────
207
+ function gracefulShutdown() {
208
+ if (isShuttingDown) return;
209
+ isShuttingDown = true;
187
210
 
188
- // ─── 停止 ─────────────────────────────────────────────────────────────────────
189
- function stop() {
190
- log("正在停止节点服务...");
211
+ log("收到关机指令,正在安全退出...", "OFF");
191
212
 
192
- if (heartbeatTimer) clearInterval(heartbeatTimer);
213
+ // 遗言:通知主控已离线
214
+ notifyHub("offline");
193
215
 
194
- if (cloudflared) {
195
- try { cloudflared.kill(); } catch {}
196
- cloudflared = null;
216
+ // 杀掉隧道进程
217
+ if (cfProcess) {
218
+ cfProcess.kill("SIGINT");
219
+ cfProcess = null;
197
220
  }
198
221
 
199
- try {
200
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
201
- } catch {}
222
+ // 清理 PID 文件
223
+ try { fs.unlinkSync(PID_FILE); } catch {}
202
224
 
203
- log("节点已断开集群", "OK");
225
+ // 不等确认,200ms 后直接走
226
+ setTimeout(() => {
227
+ log("节点已安全下线,再见!", "OFF");
228
+ process.exit(0);
229
+ }, 200);
204
230
  }
205
231
 
206
- // ─── 主流程 ────────────────────────────────────────────────────────────────────
232
+ // ─── PID 文件 ────────────────────────────────────────────────────────────────
233
+ function writePid() {
234
+ try { fs.writeFileSync(PID_FILE, String(process.pid), "utf-8"); } catch {}
235
+ }
236
+
237
+ // ─── 入口 ────────────────────────────────────────────────────────────────────
207
238
  export async function start() {
208
239
  log(`节点 ID: ${NODE_ID}`);
209
240
 
210
- // 1. 确保 server.mjs 在运行
211
241
  const serverOk = await ensureServer();
212
242
  if (!serverOk) {
213
243
  log("无法启动代理服务,终止接入", "ERR");
214
244
  process.exit(1);
215
245
  }
216
246
 
217
- // 2. 启动 cloudflared 隧道
218
- const tunnelOk = startTunnel();
219
- if (!tunnelOk) {
220
- log("无法启动隧道,终止接入", "ERR");
221
- process.exit(1);
222
- }
223
-
224
- // 3. 写 PID
225
247
  writePid();
248
+ startTunnel();
226
249
 
227
- // 4. 启动心跳定时器 ( 60 秒)
228
- heartbeatTimer = setInterval(pushToHub, 60000);
229
-
230
- // 5. 优雅退出
231
- process.on("SIGINT", () => { stop(); process.exit(0); });
232
- process.on("SIGTERM", () => { stop(); process.exit(0); });
250
+ process.on("SIGINT", gracefulShutdown);
251
+ process.on("SIGTERM", gracefulShutdown);
233
252
 
234
- log("节点已成功接入集群,等待主控中心调度...", "OK");
253
+ log("节点看门狗已启动,等待公网入口...", "ON");
235
254
  }
236
255
 
237
256
  export function stopWorker() {
238
- stop();
257
+ gracefulShutdown();
239
258
  }