panrouter 5.0.2 → 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 +97 -111
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.0.2",
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,12 +1,13 @@
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
- import { spawn } from "node:child_process";
10
+ import { spawn, spawnSync } from "node:child_process";
10
11
  import https from "node:https";
11
12
  import http from "node:http";
12
13
  import os from "node:os";
@@ -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,31 +59,30 @@ 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
- const r = spawn.sync?.(name, ["--version"], { stdio: "pipe" }) ?? { status: 1 };
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
- // Windows 常见安装路径兜底
55
- const winPaths = [
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"),
74
+ "/data/data/com.termux/files/usr/bin/cloudflared",
58
75
  ];
59
- for (const p of winPaths) {
76
+ for (const p of fallbackPaths) {
60
77
  if (fs.existsSync(p)) return p;
61
78
  }
62
79
  return null;
63
80
  }
64
81
 
65
- // ─── 启动 server.mjs ─────────────────────────────────────────────────────────
82
+ // ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
66
83
  async function ensureServer() {
67
- const isOpen = await isPortOpen(SERVER_PORT);
68
- if (isOpen) {
69
- log(`端口 ${SERVER_PORT} 已有服务在运行,跳过启动`, "OK");
84
+ if (await isPortOpen(SERVER_PORT)) {
85
+ log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
70
86
  return true;
71
87
  }
72
88
 
@@ -85,7 +101,6 @@ async function ensureServer() {
85
101
  });
86
102
  child.unref();
87
103
 
88
- // 等待端口就绪
89
104
  for (let i = 0; i < 15; i++) {
90
105
  if (await isPortOpen(SERVER_PORT)) {
91
106
  log(`代理服务已就绪 (端口 ${SERVER_PORT})`, "OK");
@@ -98,141 +113,112 @@ async function ensureServer() {
98
113
  return false;
99
114
  }
100
115
 
101
- // ─── 启动 cloudflared 隧道 ────────────────────────────────────────────────────
116
+ // ─── 隧道守护 ────────────────────────────────────────────────────────────────
102
117
  function startTunnel() {
118
+ if (isShuttingDown) return;
119
+
103
120
  const cfPath = findCloudflared();
104
121
  if (!cfPath) {
105
- log("未找到 cloudflared,请先安装:\n Windows: scoop install cloudflared\n Termux: pkg install cloudflared\n macOS: brew install cloudflared", "ERR");
106
- return false;
122
+ log("未找到 cloudflared,请先安装", "ERR");
123
+ return;
107
124
  }
108
125
 
109
126
  log("正在请求匿名公网入口...");
110
127
 
111
- // 清理旧进程
112
- if (cloudflared) {
113
- try { cloudflared.kill(); } catch {}
114
- }
115
-
116
- 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}`], {
117
129
  stdio: ["ignore", "pipe", "pipe"],
118
130
  windowsHide: true,
119
131
  });
120
132
 
121
- cloudflared.stderr.on("data", (data) => {
133
+ cfProcess.stderr.on("data", (data) => {
122
134
  const text = data.toString();
135
+
136
+ // 抓取隧道 URL
123
137
  const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
124
- if (match && match[0] !== currentTunnelUrl) {
125
- currentTunnelUrl = match[0];
126
- log(`成功获取公网入口: ${currentTunnelUrl}`, "OK");
127
- pushToHub();
138
+ if (match && match[0] !== currentUrl) {
139
+ currentUrl = match[0];
140
+ log(`成功获取公网入口: ${currentUrl}`, "OK");
141
+ notifyHub("online", currentUrl);
128
142
  }
129
- });
130
143
 
131
- cloudflared.on("exit", (code) => {
132
- log(`cloudflared 进程退出 (code: ${code})`, code === 0 ? "INFO" : "ERR");
133
- cloudflared = null;
144
+ // 网络波动提示
145
+ if (text.includes("connection lost") || text.includes("Retrying")) {
146
+ log("隧道网络不稳定,正在尝试重连...", "WARN");
147
+ }
134
148
  });
135
149
 
136
- return true;
137
- }
138
-
139
- // ─── 心跳上报 ─────────────────────────────────────────────────────────────────
140
- function pushToHub() {
141
- if (!currentTunnelUrl) return;
142
-
143
- const payload = JSON.stringify({
144
- nodeId: NODE_ID,
145
- url: currentTunnelUrl,
146
- });
150
+ cfProcess.on("close", (code) => {
151
+ log(`隧道进程断开退出 (code: ${code})`, "ERR");
152
+ cfProcess = null;
147
153
 
148
- const req = https.request(
149
- MAIN_HUB_URL,
150
- {
151
- method: "POST",
152
- headers: {
153
- "Content-Type": "application/json",
154
- "Content-Length": Buffer.byteLength(payload),
155
- "x-secret-token": AUTH_SECRET,
156
- },
157
- },
158
- (res) => {
159
- let body = "";
160
- res.on("data", (c) => (body += c));
161
- res.on("end", () => {
162
- log(`状态已同步至主控中心 (${res.statusCode})`, "HART");
163
- });
154
+ // 通知主控剔除
155
+ if (currentUrl) {
156
+ notifyHub("offline");
157
+ currentUrl = "";
164
158
  }
165
- );
166
159
 
167
- req.on("error", (e) => log(`主控节点失联: ${e.message}`, "ERR"));
168
- req.write(payload);
169
- req.end();
170
- }
160
+ // 自动复活
161
+ if (!isShuttingDown) {
162
+ log("准备在 5 秒后自动重启隧道...", "WARN");
163
+ setTimeout(startTunnel, 5000);
164
+ }
165
+ });
171
166
 
172
- // ─── 写入 PID 文件 ────────────────────────────────────────────────────────────
173
- function writePid() {
174
- try {
175
- fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
176
- } catch {}
167
+ cfProcess.on("error", (err) => {
168
+ log(`隧道进程异常: ${err.message}`, "ERR");
169
+ });
177
170
  }
178
171
 
179
- function readPid() {
180
- try {
181
- return parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
182
- } catch {
183
- return null;
184
- }
185
- }
172
+ // ─── 优雅退出 ────────────────────────────────────────────────────────────────
173
+ function gracefulShutdown() {
174
+ if (isShuttingDown) return;
175
+ isShuttingDown = true;
186
176
 
187
- // ─── 停止 ─────────────────────────────────────────────────────────────────────
188
- function stop() {
189
- log("正在停止节点服务...");
177
+ log("收到关机指令,正在安全退出...", "OFF");
190
178
 
191
- if (heartbeatTimer) clearInterval(heartbeatTimer);
179
+ // 遗言:通知主控已离线
180
+ notifyHub("offline");
192
181
 
193
- if (cloudflared) {
194
- try { cloudflared.kill(); } catch {}
195
- cloudflared = null;
182
+ // 杀掉隧道进程
183
+ if (cfProcess) {
184
+ cfProcess.kill("SIGINT");
185
+ cfProcess = null;
196
186
  }
197
187
 
198
- try {
199
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
200
- } 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
+ }
201
197
 
202
- log("节点已断开集群", "OK");
198
+ // ─── PID 文件 ────────────────────────────────────────────────────────────────
199
+ function writePid() {
200
+ try { fs.writeFileSync(PID_FILE, String(process.pid), "utf-8"); } catch {}
203
201
  }
204
202
 
205
- // ─── 主流程 ────────────────────────────────────────────────────────────────────
203
+ // ─── 入口 ────────────────────────────────────────────────────────────────────
206
204
  export async function start() {
207
205
  log(`节点 ID: ${NODE_ID}`);
208
206
 
209
- // 1. 确保 server.mjs 在运行
210
207
  const serverOk = await ensureServer();
211
208
  if (!serverOk) {
212
209
  log("无法启动代理服务,终止接入", "ERR");
213
210
  process.exit(1);
214
211
  }
215
212
 
216
- // 2. 启动 cloudflared 隧道
217
- const tunnelOk = startTunnel();
218
- if (!tunnelOk) {
219
- log("无法启动隧道,终止接入", "ERR");
220
- process.exit(1);
221
- }
222
-
223
- // 3. 写 PID
224
213
  writePid();
214
+ startTunnel();
225
215
 
226
- // 4. 启动心跳定时器 ( 60 秒)
227
- heartbeatTimer = setInterval(pushToHub, 60000);
228
-
229
- // 5. 优雅退出
230
- process.on("SIGINT", () => { stop(); process.exit(0); });
231
- process.on("SIGTERM", () => { stop(); process.exit(0); });
216
+ process.on("SIGINT", gracefulShutdown);
217
+ process.on("SIGTERM", gracefulShutdown);
232
218
 
233
- log("节点已成功接入集群,等待主控中心调度...", "OK");
219
+ log("节点看门狗已启动,等待公网入口...", "ON");
234
220
  }
235
221
 
236
222
  export function stopWorker() {
237
- stop();
223
+ gracefulShutdown();
238
224
  }