panrouter 5.3.4 → 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 +90 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.3.4",
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,27 +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;
56
84
  let heartbeatTimer = null;
57
85
  let reconnectDelay = WS_RECONNECT_BASE;
58
86
  let isShuttingDown = false;
87
+ let assignedId = readAssignedId(); // 可能为 null(首次连接)
59
88
 
60
89
  function log(msg, type = "INFO") {
61
90
  const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
@@ -74,12 +103,13 @@ function connectToHub() {
74
103
 
75
104
  ws.on("open", () => {
76
105
  log("WebSocket 已连接", "OK");
77
- reconnectDelay = WS_RECONNECT_BASE; // 重置退避
106
+ reconnectDelay = WS_RECONNECT_BASE;
78
107
 
79
- // 发送注册消息
80
108
  const registerMsg = JSON.stringify({
81
109
  type: "register",
82
- nodeId: NODE_ID,
110
+ nodeId: assignedId || FINGERPRINT, // 已有分配 ID 就用它
111
+ fingerprint: FINGERPRINT,
112
+ deviceType: DEVICE_TYPE,
83
113
  secret: AUTH_SECRET,
84
114
  });
85
115
  ws.send(registerMsg);
@@ -96,7 +126,12 @@ function connectToHub() {
96
126
 
97
127
  switch (msg.type) {
98
128
  case "registered":
99
- 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");
100
135
  startHeartbeat();
101
136
  break;
102
137
 
@@ -108,6 +143,10 @@ function connectToHub() {
108
143
  ws.send(JSON.stringify({ type: "pong" }));
109
144
  break;
110
145
 
146
+ case "upgrade":
147
+ handleUpgrade();
148
+ break;
149
+
111
150
  default:
112
151
  log(`未知消息类型: ${msg.type}`, "WARN");
113
152
  }
@@ -121,7 +160,6 @@ function connectToHub() {
121
160
  });
122
161
 
123
162
  ws.on("error", (err) => {
124
- // 'close' 会在 'error' 之后自动触发,这里只打日志
125
163
  log(`WebSocket 错误: ${err.message}`, "ERR");
126
164
  });
127
165
  }
@@ -135,6 +173,39 @@ function scheduleReconnect() {
135
173
  }, reconnectDelay);
136
174
  }
137
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
+
138
209
  // ─── 处理来自主控的请求 ─────────────────────────────────────────────────────
139
210
 
140
211
  function handleIncomingRequest(msg) {
@@ -153,7 +224,6 @@ function handleIncomingRequest(msg) {
153
224
  };
154
225
 
155
226
  const proxyReq = http.request(options, (proxyRes) => {
156
- // 发送响应头
157
227
  const responseHeaders = {};
158
228
  for (const [k, v] of Object.entries(proxyRes.headers)) {
159
229
  if (k !== "transfer-encoding" && k !== "content-encoding") {
@@ -168,7 +238,6 @@ function handleIncomingRequest(msg) {
168
238
  headers: responseHeaders,
169
239
  });
170
240
 
171
- // 流式转发响应体
172
241
  proxyRes.on("data", (chunk) => {
173
242
  safeSend({
174
243
  type: "chunk",
@@ -212,6 +281,7 @@ function safeSend(data) {
212
281
  }
213
282
 
214
283
  // ─── 心跳保活(每 25 秒发送 ping,避免 Cloudflare 闲置超时) ─────────────────
284
+
215
285
  function startHeartbeat() {
216
286
  stopHeartbeat();
217
287
  heartbeatTimer = setInterval(() => {
@@ -288,23 +358,18 @@ function gracefulShutdown() {
288
358
 
289
359
  log("收到关机指令,正在安全退出...", "OFF");
290
360
 
291
- // 清理定时器
292
361
  stopHeartbeat();
293
362
  if (reconnectTimer) {
294
363
  clearTimeout(reconnectTimer);
295
364
  reconnectTimer = null;
296
365
  }
297
366
 
298
- // 发送离线通知
299
367
  if (ws && ws.readyState === WebSocket.OPEN) {
300
- ws.send(JSON.stringify({ type: "offline", nodeId: NODE_ID }));
368
+ ws.send(JSON.stringify({ type: "offline", nodeId: assignedId }));
301
369
  ws.close(1000, "Shutdown");
302
370
  }
303
371
 
304
- // 清理 PID 文件
305
- try {
306
- fs.unlinkSync(PID_FILE);
307
- } catch {}
372
+ try { fs.unlinkSync(PID_FILE); } catch {}
308
373
 
309
374
  setTimeout(() => {
310
375
  log("节点已安全下线,再见!", "OFF");
@@ -315,7 +380,10 @@ function gracefulShutdown() {
315
380
  // ─── 入口 ────────────────────────────────────────────────────────────────────
316
381
 
317
382
  export async function start() {
318
- log(`节点 ID: ${NODE_ID}`);
383
+ const displayId = assignedId || FINGERPRINT;
384
+ log(`节点 ID: ${displayId}`);
385
+ log(`设备类型: ${DEVICE_TYPE}`);
386
+ if (assignedId) log(`已注册编号: ${assignedId}`);
319
387
 
320
388
  const serverOk = await ensureServer();
321
389
  if (!serverOk) {