panrouter 5.0.3 → 5.1.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 +92 -107
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.0.3",
3
+ "version": "5.1.0",
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,32 @@ 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
+ // ─── 检查端口 ────────────────────────────────────────────────────────────────
36
53
  function isPortOpen(port) {
37
54
  return new Promise((resolve) => {
38
55
  const req = http.get(`http://127.0.0.1:${port}/health`, () => {});
@@ -42,16 +59,15 @@ function isPortOpen(port) {
42
59
  });
43
60
  }
44
61
 
45
- // ─── 检查 cloudflared 是否可用 ────────────────────────────────────────────────
62
+ // ─── 查找 cloudflared ────────────────────────────────────────────────────────
46
63
  function findCloudflared() {
47
64
  const candidates = ["cloudflared", "cloudflared.exe"];
48
65
  for (const name of candidates) {
49
66
  try {
50
67
  const r = spawnSync(name, ["--version"], { stdio: "pipe", timeout: 3000 });
51
68
  if (r.status === 0) return name;
52
- } catch { /* try next */ }
69
+ } catch { /* next */ }
53
70
  }
54
- // 常见安装路径兜底
55
71
  const fallbackPaths = [
56
72
  path.join(process.env.USERPROFILE || "", "AppData", "Local", "cloudflared", "cloudflared.exe"),
57
73
  path.join(process.env.LOCALAPPDATA || "", "cloudflared", "cloudflared.exe"),
@@ -63,11 +79,10 @@ function findCloudflared() {
63
79
  return null;
64
80
  }
65
81
 
66
- // ─── 启动 server.mjs ─────────────────────────────────────────────────────────
82
+ // ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
67
83
  async function ensureServer() {
68
- const isOpen = await isPortOpen(SERVER_PORT);
69
- if (isOpen) {
70
- log(`端口 ${SERVER_PORT} 已有服务在运行,跳过启动`, "OK");
84
+ if (await isPortOpen(SERVER_PORT)) {
85
+ log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
71
86
  return true;
72
87
  }
73
88
 
@@ -86,7 +101,6 @@ async function ensureServer() {
86
101
  });
87
102
  child.unref();
88
103
 
89
- // 等待端口就绪
90
104
  for (let i = 0; i < 15; i++) {
91
105
  if (await isPortOpen(SERVER_PORT)) {
92
106
  log(`代理服务已就绪 (端口 ${SERVER_PORT})`, "OK");
@@ -99,141 +113,112 @@ async function ensureServer() {
99
113
  return false;
100
114
  }
101
115
 
102
- // ─── 启动 cloudflared 隧道 ────────────────────────────────────────────────────
116
+ // ─── 隧道守护 ────────────────────────────────────────────────────────────────
103
117
  function startTunnel() {
118
+ if (isShuttingDown) return;
119
+
104
120
  const cfPath = findCloudflared();
105
121
  if (!cfPath) {
106
- log("未找到 cloudflared,请先安装:\n Windows: scoop install cloudflared\n Termux: pkg install cloudflared\n macOS: brew install cloudflared", "ERR");
107
- return false;
122
+ log("未找到 cloudflared,请先安装", "ERR");
123
+ return;
108
124
  }
109
125
 
110
126
  log("正在请求匿名公网入口...");
111
127
 
112
- // 清理旧进程
113
- if (cloudflared) {
114
- try { cloudflared.kill(); } catch {}
115
- }
116
-
117
- cloudflared = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
128
+ cfProcess = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
118
129
  stdio: ["ignore", "pipe", "pipe"],
119
130
  windowsHide: true,
120
131
  });
121
132
 
122
- cloudflared.stderr.on("data", (data) => {
133
+ cfProcess.stderr.on("data", (data) => {
123
134
  const text = data.toString();
135
+
136
+ // 抓取隧道 URL
124
137
  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();
138
+ if (match && match[0] !== currentUrl) {
139
+ currentUrl = match[0];
140
+ log(`成功获取公网入口: ${currentUrl}`, "OK");
141
+ notifyHub("online", currentUrl);
129
142
  }
130
- });
131
143
 
132
- cloudflared.on("exit", (code) => {
133
- log(`cloudflared 进程退出 (code: ${code})`, code === 0 ? "INFO" : "ERR");
134
- cloudflared = null;
144
+ // 网络波动提示
145
+ if (text.includes("connection lost") || text.includes("Retrying")) {
146
+ log("隧道网络不稳定,正在尝试重连...", "WARN");
147
+ }
135
148
  });
136
149
 
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
- });
150
+ cfProcess.on("close", (code) => {
151
+ log(`隧道进程断开退出 (code: ${code})`, "ERR");
152
+ cfProcess = null;
148
153
 
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
- });
154
+ // 通知主控剔除
155
+ if (currentUrl) {
156
+ notifyHub("offline");
157
+ currentUrl = "";
165
158
  }
166
- );
167
159
 
168
- req.on("error", (e) => log(`主控节点失联: ${e.message}`, "ERR"));
169
- req.write(payload);
170
- req.end();
171
- }
160
+ // 自动复活
161
+ if (!isShuttingDown) {
162
+ log("准备在 5 秒后自动重启隧道...", "WARN");
163
+ setTimeout(startTunnel, 5000);
164
+ }
165
+ });
172
166
 
173
- // ─── 写入 PID 文件 ────────────────────────────────────────────────────────────
174
- function writePid() {
175
- try {
176
- fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
177
- } catch {}
167
+ cfProcess.on("error", (err) => {
168
+ log(`隧道进程异常: ${err.message}`, "ERR");
169
+ });
178
170
  }
179
171
 
180
- function readPid() {
181
- try {
182
- return parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
183
- } catch {
184
- return null;
185
- }
186
- }
172
+ // ─── 优雅退出 ────────────────────────────────────────────────────────────────
173
+ function gracefulShutdown() {
174
+ if (isShuttingDown) return;
175
+ isShuttingDown = true;
187
176
 
188
- // ─── 停止 ─────────────────────────────────────────────────────────────────────
189
- function stop() {
190
- log("正在停止节点服务...");
177
+ log("收到关机指令,正在安全退出...", "OFF");
191
178
 
192
- if (heartbeatTimer) clearInterval(heartbeatTimer);
179
+ // 遗言:通知主控已离线
180
+ notifyHub("offline");
193
181
 
194
- if (cloudflared) {
195
- try { cloudflared.kill(); } catch {}
196
- cloudflared = null;
182
+ // 杀掉隧道进程
183
+ if (cfProcess) {
184
+ cfProcess.kill("SIGINT");
185
+ cfProcess = null;
197
186
  }
198
187
 
199
- try {
200
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
201
- } catch {}
188
+ // 清理 PID 文件
189
+ try { fs.unlinkSync(PID_FILE); } catch {}
190
+
191
+ // 不等确认,200ms 后直接走
192
+ setTimeout(() => {
193
+ log("节点已安全下线,再见!", "OFF");
194
+ process.exit(0);
195
+ }, 200);
196
+ }
202
197
 
203
- log("节点已断开集群", "OK");
198
+ // ─── PID 文件 ────────────────────────────────────────────────────────────────
199
+ function writePid() {
200
+ try { fs.writeFileSync(PID_FILE, String(process.pid), "utf-8"); } catch {}
204
201
  }
205
202
 
206
- // ─── 主流程 ────────────────────────────────────────────────────────────────────
203
+ // ─── 入口 ────────────────────────────────────────────────────────────────────
207
204
  export async function start() {
208
205
  log(`节点 ID: ${NODE_ID}`);
209
206
 
210
- // 1. 确保 server.mjs 在运行
211
207
  const serverOk = await ensureServer();
212
208
  if (!serverOk) {
213
209
  log("无法启动代理服务,终止接入", "ERR");
214
210
  process.exit(1);
215
211
  }
216
212
 
217
- // 2. 启动 cloudflared 隧道
218
- const tunnelOk = startTunnel();
219
- if (!tunnelOk) {
220
- log("无法启动隧道,终止接入", "ERR");
221
- process.exit(1);
222
- }
223
-
224
- // 3. 写 PID
225
213
  writePid();
214
+ startTunnel();
226
215
 
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); });
216
+ process.on("SIGINT", gracefulShutdown);
217
+ process.on("SIGTERM", gracefulShutdown);
233
218
 
234
- log("节点已成功接入集群,等待主控中心调度...", "OK");
219
+ log("节点看门狗已启动,等待公网入口...", "ON");
235
220
  }
236
221
 
237
222
  export function stopWorker() {
238
- stop();
223
+ gracefulShutdown();
239
224
  }