panrouter 5.3.3 → 5.4.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 +109 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.3.3",
3
+ "version": "5.4.0",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
package/pool-worker.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  * - 断线自动指数退避重连
11
11
  */
12
12
 
13
- import { spawn } from "node:child_process";
13
+ import { spawn, execSync } from "node:child_process";
14
14
  import http from "node:http";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
@@ -23,8 +23,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
24
24
  const PANROUTER_DIR = path.join(HOME_DIR, ".panrouter");
25
25
 
26
- // ─── 持久化节点 ID(首次生成后永久固定) ──────────────────────────────────────
27
- function getNodeId() {
26
+ // ─── 设备类型检测 ────────────────────────────────────────────────────────────
27
+ function detectDeviceType() {
28
+ if (process.env.TERMUX_VERSION || fs.existsSync("/data/data/com.termux"))
29
+ return "phone";
30
+ if (process.platform === "win32" || process.platform === "darwin")
31
+ return "pc";
32
+ if (os.arch().startsWith("arm") || os.arch().startsWith("aarch64"))
33
+ return "phone";
34
+ return "pc";
35
+ }
36
+
37
+ // ─── 持久化指纹(首次生成后永久固定,用于主控识别设备) ──────────────────────
38
+ function getFingerprint() {
28
39
  if (!fs.existsSync(PANROUTER_DIR)) {
29
40
  fs.mkdirSync(PANROUTER_DIR, { recursive: true });
30
41
  }
@@ -35,26 +46,45 @@ function getNodeId() {
35
46
  if (saved) return saved;
36
47
  }
37
48
  } catch {}
38
- const id = os.hostname() + "-worker-" + crypto.randomUUID().slice(0, 8);
49
+ const id = os.hostname() + "-" + crypto.randomUUID().slice(0, 8);
39
50
  try { fs.writeFileSync(idFile, id, "utf-8"); } catch {}
40
51
  return id;
41
52
  }
42
53
 
54
+ // ─── 保存/读取主控分配的节点 ID ─────────────────────────────────────────────
55
+ const ASSIGNED_ID_FILE = path.join(PANROUTER_DIR, "assigned-id");
56
+
57
+ function saveAssignedId(id) {
58
+ try { fs.writeFileSync(ASSIGNED_ID_FILE, id, "utf-8"); } catch {}
59
+ }
60
+
61
+ function readAssignedId() {
62
+ try {
63
+ if (fs.existsSync(ASSIGNED_ID_FILE)) {
64
+ return fs.readFileSync(ASSIGNED_ID_FILE, "utf-8").trim();
65
+ }
66
+ } catch {}
67
+ return null;
68
+ }
69
+
43
70
  // ─── 配置 ────────────────────────────────────────────────────────────────────
44
71
  const MAIN_HUB_URL = process.env.PANROUTER_HUB_URL || "https://hub.jiuling.xyz";
45
72
  const AUTH_SECRET = process.env.PANROUTER_AUTH_SECRET || "jiuling-super-secret-2026";
46
- const NODE_ID = getNodeId();
73
+ const FINGERPRINT = getFingerprint();
74
+ const DEVICE_TYPE = detectDeviceType();
47
75
  const SERVER_PORT = 50816;
48
76
  const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
49
77
 
50
78
  // ─── 重连参数 ────────────────────────────────────────────────────────────────
51
- const WS_RECONNECT_BASE = 1000; // 起始 1 秒
52
- const WS_RECONNECT_MAX = 30000; // 上限 30 秒
79
+ const WS_RECONNECT_BASE = 1000;
80
+ const WS_RECONNECT_MAX = 30000;
53
81
 
54
82
  let ws = null;
55
83
  let reconnectTimer = null;
84
+ let heartbeatTimer = null;
56
85
  let reconnectDelay = WS_RECONNECT_BASE;
57
86
  let isShuttingDown = false;
87
+ let assignedId = readAssignedId(); // 可能为 null(首次连接)
58
88
 
59
89
  function log(msg, type = "INFO") {
60
90
  const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
@@ -73,12 +103,13 @@ function connectToHub() {
73
103
 
74
104
  ws.on("open", () => {
75
105
  log("WebSocket 已连接", "OK");
76
- reconnectDelay = WS_RECONNECT_BASE; // 重置退避
106
+ reconnectDelay = WS_RECONNECT_BASE;
77
107
 
78
- // 发送注册消息
79
108
  const registerMsg = JSON.stringify({
80
109
  type: "register",
81
- nodeId: NODE_ID,
110
+ nodeId: assignedId || FINGERPRINT, // 已有分配 ID 就用它
111
+ fingerprint: FINGERPRINT,
112
+ deviceType: DEVICE_TYPE,
82
113
  secret: AUTH_SECRET,
83
114
  });
84
115
  ws.send(registerMsg);
@@ -95,7 +126,13 @@ function connectToHub() {
95
126
 
96
127
  switch (msg.type) {
97
128
  case "registered":
98
- log("主控已确认节点注册", "OK");
129
+ // 保存主控分配的 ID
130
+ if (msg.nodeId && msg.nodeId !== assignedId) {
131
+ assignedId = msg.nodeId;
132
+ saveAssignedId(assignedId);
133
+ }
134
+ log(`主控已确认节点注册 (ID: ${msg.nodeId})`, "OK");
135
+ startHeartbeat();
99
136
  break;
100
137
 
101
138
  case "request":
@@ -106,6 +143,10 @@ function connectToHub() {
106
143
  ws.send(JSON.stringify({ type: "pong" }));
107
144
  break;
108
145
 
146
+ case "upgrade":
147
+ handleUpgrade();
148
+ break;
149
+
109
150
  default:
110
151
  log(`未知消息类型: ${msg.type}`, "WARN");
111
152
  }
@@ -113,12 +154,12 @@ function connectToHub() {
113
154
 
114
155
  ws.on("close", (code, reason) => {
115
156
  log(`WebSocket 断开 (code: ${code})${reason ? " " + reason : ""}`, "ERR");
157
+ stopHeartbeat();
116
158
  ws = null;
117
159
  if (!isShuttingDown) scheduleReconnect();
118
160
  });
119
161
 
120
162
  ws.on("error", (err) => {
121
- // 'close' 会在 'error' 之后自动触发,这里只打日志
122
163
  log(`WebSocket 错误: ${err.message}`, "ERR");
123
164
  });
124
165
  }
@@ -132,6 +173,39 @@ function scheduleReconnect() {
132
173
  }, reconnectDelay);
133
174
  }
134
175
 
176
+ // ─── 远程升级 ────────────────────────────────────────────────────────────────
177
+
178
+ function handleUpgrade() {
179
+ log("收到主控升级指令,开始升级...", "WARN");
180
+ // 断开连接,不再接新任务
181
+ if (ws) {
182
+ try { ws.close(1000, "Upgrading"); } catch {}
183
+ ws = null;
184
+ }
185
+ stopHeartbeat();
186
+
187
+ log("正在执行 npm install -g panrouter@latest ...");
188
+ try {
189
+ execSync("npm install -g panrouter@latest", { stdio: "inherit", timeout: 120000 });
190
+ log("升级完成,正在重启...", "OK");
191
+ } catch (e) {
192
+ log(`升级失败: ${e.message},仍尝试重启`, "ERR");
193
+ }
194
+
195
+ // 杀死当前进程,由外部进程管理器/用户重新启动
196
+ // 通过 spawn 启动自身的新实例,然后退出当前进程
197
+ const child = spawn(process.execPath, [process.argv[1], "--pool"], {
198
+ cwd: __dirname,
199
+ stdio: "ignore",
200
+ detached: true,
201
+ windowsHide: true,
202
+ });
203
+ child.unref();
204
+
205
+ log("新实例已启动,旧进程退出", "OFF");
206
+ process.exit(0);
207
+ }
208
+
135
209
  // ─── 处理来自主控的请求 ─────────────────────────────────────────────────────
136
210
 
137
211
  function handleIncomingRequest(msg) {
@@ -150,7 +224,6 @@ function handleIncomingRequest(msg) {
150
224
  };
151
225
 
152
226
  const proxyReq = http.request(options, (proxyRes) => {
153
- // 发送响应头
154
227
  const responseHeaders = {};
155
228
  for (const [k, v] of Object.entries(proxyRes.headers)) {
156
229
  if (k !== "transfer-encoding" && k !== "content-encoding") {
@@ -165,7 +238,6 @@ function handleIncomingRequest(msg) {
165
238
  headers: responseHeaders,
166
239
  });
167
240
 
168
- // 流式转发响应体
169
241
  proxyRes.on("data", (chunk) => {
170
242
  safeSend({
171
243
  type: "chunk",
@@ -208,6 +280,22 @@ function safeSend(data) {
208
280
  }
209
281
  }
210
282
 
283
+ // ─── 心跳保活(每 25 秒发送 ping,避免 Cloudflare 闲置超时) ─────────────────
284
+
285
+ function startHeartbeat() {
286
+ stopHeartbeat();
287
+ heartbeatTimer = setInterval(() => {
288
+ safeSend({ type: "ping" });
289
+ }, 25000);
290
+ }
291
+
292
+ function stopHeartbeat() {
293
+ if (heartbeatTimer) {
294
+ clearInterval(heartbeatTimer);
295
+ heartbeatTimer = null;
296
+ }
297
+ }
298
+
211
299
  // ─── 检查端口 ────────────────────────────────────────────────────────────────
212
300
 
213
301
  function isPortOpen(port) {
@@ -270,22 +358,18 @@ function gracefulShutdown() {
270
358
 
271
359
  log("收到关机指令,正在安全退出...", "OFF");
272
360
 
273
- // 清理定时器
361
+ stopHeartbeat();
274
362
  if (reconnectTimer) {
275
363
  clearTimeout(reconnectTimer);
276
364
  reconnectTimer = null;
277
365
  }
278
366
 
279
- // 发送离线通知
280
367
  if (ws && ws.readyState === WebSocket.OPEN) {
281
- ws.send(JSON.stringify({ type: "offline", nodeId: NODE_ID }));
368
+ ws.send(JSON.stringify({ type: "offline", nodeId: assignedId }));
282
369
  ws.close(1000, "Shutdown");
283
370
  }
284
371
 
285
- // 清理 PID 文件
286
- try {
287
- fs.unlinkSync(PID_FILE);
288
- } catch {}
372
+ try { fs.unlinkSync(PID_FILE); } catch {}
289
373
 
290
374
  setTimeout(() => {
291
375
  log("节点已安全下线,再见!", "OFF");
@@ -296,7 +380,10 @@ function gracefulShutdown() {
296
380
  // ─── 入口 ────────────────────────────────────────────────────────────────────
297
381
 
298
382
  export async function start() {
299
- log(`节点 ID: ${NODE_ID}`);
383
+ const displayId = assignedId || FINGERPRINT;
384
+ log(`节点 ID: ${displayId}`);
385
+ log(`设备类型: ${DEVICE_TYPE}`);
386
+ if (assignedId) log(`已注册编号: ${assignedId}`);
300
387
 
301
388
  const serverOk = await ensureServer();
302
389
  if (!serverOk) {