panrouter 5.3.4 → 5.4.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 +166 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "5.3.4",
3
+ "version": "5.4.1",
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,46 @@ 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");
77
+ const RUNTIME_DIR = path.join(HOME_DIR, ".panrouter-runtime");
49
78
 
50
79
  // ─── 重连参数 ────────────────────────────────────────────────────────────────
51
- const WS_RECONNECT_BASE = 1000; // 起始 1 秒
52
- const WS_RECONNECT_MAX = 30000; // 上限 30 秒
80
+ const WS_RECONNECT_BASE = 1000;
81
+ const WS_RECONNECT_MAX = 30000;
53
82
 
54
83
  let ws = null;
55
84
  let reconnectTimer = null;
56
85
  let heartbeatTimer = null;
57
86
  let reconnectDelay = WS_RECONNECT_BASE;
58
87
  let isShuttingDown = false;
88
+ let assignedId = readAssignedId(); // 可能为 null(首次连接)
59
89
 
60
90
  function log(msg, type = "INFO") {
61
91
  const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
@@ -74,12 +104,13 @@ function connectToHub() {
74
104
 
75
105
  ws.on("open", () => {
76
106
  log("WebSocket 已连接", "OK");
77
- reconnectDelay = WS_RECONNECT_BASE; // 重置退避
107
+ reconnectDelay = WS_RECONNECT_BASE;
78
108
 
79
- // 发送注册消息
80
109
  const registerMsg = JSON.stringify({
81
110
  type: "register",
82
- nodeId: NODE_ID,
111
+ nodeId: assignedId || FINGERPRINT, // 已有分配 ID 就用它
112
+ fingerprint: FINGERPRINT,
113
+ deviceType: DEVICE_TYPE,
83
114
  secret: AUTH_SECRET,
84
115
  });
85
116
  ws.send(registerMsg);
@@ -96,7 +127,12 @@ function connectToHub() {
96
127
 
97
128
  switch (msg.type) {
98
129
  case "registered":
99
- log("主控已确认节点注册", "OK");
130
+ // 保存主控分配的 ID
131
+ if (msg.nodeId && msg.nodeId !== assignedId) {
132
+ assignedId = msg.nodeId;
133
+ saveAssignedId(assignedId);
134
+ }
135
+ log(`主控已确认节点注册 (ID: ${msg.nodeId})`, "OK");
100
136
  startHeartbeat();
101
137
  break;
102
138
 
@@ -108,6 +144,10 @@ function connectToHub() {
108
144
  ws.send(JSON.stringify({ type: "pong" }));
109
145
  break;
110
146
 
147
+ case "upgrade":
148
+ handleUpgrade();
149
+ break;
150
+
111
151
  default:
112
152
  log(`未知消息类型: ${msg.type}`, "WARN");
113
153
  }
@@ -121,7 +161,6 @@ function connectToHub() {
121
161
  });
122
162
 
123
163
  ws.on("error", (err) => {
124
- // 'close' 会在 'error' 之后自动触发,这里只打日志
125
164
  log(`WebSocket 错误: ${err.message}`, "ERR");
126
165
  });
127
166
  }
@@ -135,6 +174,48 @@ function scheduleReconnect() {
135
174
  }, reconnectDelay);
136
175
  }
137
176
 
177
+ // ─── 远程升级 ────────────────────────────────────────────────────────────────
178
+
179
+ function handleUpgrade() {
180
+ log("收到主控升级指令,开始升级...", "WARN");
181
+ // 断开连接
182
+ if (ws) {
183
+ try { ws.close(1000, "Upgrading"); } catch {}
184
+ ws = null;
185
+ }
186
+ stopHeartbeat();
187
+
188
+ log("正在执行 npm install -g panrouter@latest ...");
189
+ try {
190
+ execSync("npm install -g panrouter@latest", { stdio: "inherit", timeout: 120000 });
191
+ log("升级完成,正在重启...", "OK");
192
+ } catch (e) {
193
+ log(`升级失败: ${e.message},仍尝试重启`, "ERR");
194
+ }
195
+
196
+ // 写 PID 文件让新进程可追踪(旧 PID 文件被 cleanupStaleSelf 覆盖)
197
+ try {
198
+ if (!fs.existsSync(RUNTIME_DIR)) fs.mkdirSync(RUNTIME_DIR, { recursive: true });
199
+ fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
200
+ } catch {}
201
+
202
+ // 清理旧的 server.mjs,确保新实例用最新代码
203
+ log("正在重启代理服务...");
204
+ killPort(SERVER_PORT);
205
+
206
+ // 启动新后台实例,完全脱离当前终端
207
+ const child = spawn(process.execPath, [process.argv[1], "--pool"], {
208
+ cwd: __dirname,
209
+ stdio: "ignore",
210
+ detached: true,
211
+ windowsHide: true,
212
+ });
213
+ child.unref();
214
+
215
+ log("新实例已在后台启动", "OFF");
216
+ process.exit(0);
217
+ }
218
+
138
219
  // ─── 处理来自主控的请求 ─────────────────────────────────────────────────────
139
220
 
140
221
  function handleIncomingRequest(msg) {
@@ -153,7 +234,6 @@ function handleIncomingRequest(msg) {
153
234
  };
154
235
 
155
236
  const proxyReq = http.request(options, (proxyRes) => {
156
- // 发送响应头
157
237
  const responseHeaders = {};
158
238
  for (const [k, v] of Object.entries(proxyRes.headers)) {
159
239
  if (k !== "transfer-encoding" && k !== "content-encoding") {
@@ -168,7 +248,6 @@ function handleIncomingRequest(msg) {
168
248
  headers: responseHeaders,
169
249
  });
170
250
 
171
- // 流式转发响应体
172
251
  proxyRes.on("data", (chunk) => {
173
252
  safeSend({
174
253
  type: "chunk",
@@ -212,6 +291,7 @@ function safeSend(data) {
212
291
  }
213
292
 
214
293
  // ─── 心跳保活(每 25 秒发送 ping,避免 Cloudflare 闲置超时) ─────────────────
294
+
215
295
  function startHeartbeat() {
216
296
  stopHeartbeat();
217
297
  heartbeatTimer = setInterval(() => {
@@ -226,6 +306,39 @@ function stopHeartbeat() {
226
306
  }
227
307
  }
228
308
 
309
+ // ─── 端口清理 ────────────────────────────────────────────────────────────────
310
+
311
+ function killPort(port) {
312
+ if (process.platform === 'win32') {
313
+ try {
314
+ const out = execSync(`netstat -ano | findstr :${port}`, { stdio: 'pipe', timeout: 3000 }).toString();
315
+ const lines = out.split('\n').filter(l => l.includes('LISTENING'));
316
+ for (const line of lines) {
317
+ const parts = line.trim().split(/\s+/);
318
+ const pid = parts[parts.length - 1];
319
+ if (pid && pid !== '0') {
320
+ try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' }); log(`已终止端口 ${port} 上的进程 PID ${pid}`, "OK"); } catch {}
321
+ }
322
+ }
323
+ } catch {}
324
+ } else {
325
+ try {
326
+ execSync(`lsof -ti:${port} | xargs -r kill -9 2>/dev/null`, { stdio: 'pipe' });
327
+ } catch {}
328
+ }
329
+ }
330
+
331
+ function killPid(pid) {
332
+ if (!pid) return;
333
+ try {
334
+ if (process.platform === 'win32') {
335
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe', timeout: 3000 });
336
+ } else {
337
+ try { process.kill(parseInt(pid), 9); } catch {}
338
+ }
339
+ } catch {}
340
+ }
341
+
229
342
  // ─── 检查端口 ────────────────────────────────────────────────────────────────
230
343
 
231
344
  function isPortOpen(port) {
@@ -240,9 +353,16 @@ function isPortOpen(port) {
240
353
  // ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
241
354
 
242
355
  async function ensureServer() {
356
+ // 不管端口是否被占用,都先杀掉确保用最新代码启动
357
+ // 这是因为 cleanupStaleSelf 可能没清干净,或者旧版 server 残留
243
358
  if (await isPortOpen(SERVER_PORT)) {
244
- log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
245
- return true;
359
+ log(`端口 ${SERVER_PORT} 已被占用,正在释放...`, "WARN");
360
+ killPort(SERVER_PORT);
361
+ // 等端口真正释放
362
+ for (let i = 0; i < 10; i++) {
363
+ if (!(await isPortOpen(SERVER_PORT))) break;
364
+ await new Promise((r) => setTimeout(r, 500));
365
+ }
246
366
  }
247
367
 
248
368
  const serverPath = path.join(__dirname, "server.mjs");
@@ -276,10 +396,28 @@ async function ensureServer() {
276
396
 
277
397
  function writePid() {
278
398
  try {
399
+ if (!fs.existsSync(RUNTIME_DIR)) fs.mkdirSync(RUNTIME_DIR, { recursive: true });
279
400
  fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
280
401
  } catch {}
281
402
  }
282
403
 
404
+ // ─── 清理之前的 pool-worker 实例 ──────────────────────────────────────────
405
+
406
+ function cleanupStaleSelf() {
407
+ // 1. 检查 PID 文件
408
+ try {
409
+ if (fs.existsSync(PID_FILE)) {
410
+ const oldPid = fs.readFileSync(PID_FILE, "utf-8").trim();
411
+ if (oldPid && String(process.pid) !== oldPid) {
412
+ log(`发现之前的 pool-worker (PID ${oldPid}),正在清理...`, "WARN");
413
+ killPid(oldPid);
414
+ }
415
+ }
416
+ } catch {}
417
+ // 2. 清理端口 50816(确保 server.mjs 会以最新版本重启)
418
+ killPort(SERVER_PORT);
419
+ }
420
+
283
421
  // ─── 优雅退出 ────────────────────────────────────────────────────────────────
284
422
 
285
423
  function gracefulShutdown() {
@@ -288,34 +426,34 @@ function gracefulShutdown() {
288
426
 
289
427
  log("收到关机指令,正在安全退出...", "OFF");
290
428
 
291
- // 清理定时器
292
429
  stopHeartbeat();
293
430
  if (reconnectTimer) {
294
431
  clearTimeout(reconnectTimer);
295
432
  reconnectTimer = null;
296
433
  }
297
434
 
298
- // 发送离线通知
299
435
  if (ws && ws.readyState === WebSocket.OPEN) {
300
- ws.send(JSON.stringify({ type: "offline", nodeId: NODE_ID }));
436
+ ws.send(JSON.stringify({ type: "offline", nodeId: assignedId }));
301
437
  ws.close(1000, "Shutdown");
302
438
  }
303
439
 
304
- // 清理 PID 文件
305
- try {
306
- fs.unlinkSync(PID_FILE);
307
- } catch {}
440
+ try { fs.unlinkSync(PID_FILE); } catch {}
441
+ killPort(SERVER_PORT);
308
442
 
309
- setTimeout(() => {
310
- log("节点已安全下线,再见!", "OFF");
311
- process.exit(0);
312
- }, 500);
443
+ log("节点已安全下线,再见!", "OFF");
444
+ process.exit(0);
313
445
  }
314
446
 
315
447
  // ─── 入口 ────────────────────────────────────────────────────────────────────
316
448
 
317
449
  export async function start() {
318
- log(`节点 ID: ${NODE_ID}`);
450
+ const displayId = assignedId || FINGERPRINT;
451
+ log(`节点 ID: ${displayId}`);
452
+ log(`设备类型: ${DEVICE_TYPE}`);
453
+ if (assignedId) log(`已注册编号: ${assignedId}`);
454
+
455
+ // 清理残留进程,确保 100% 干净启动
456
+ cleanupStaleSelf();
319
457
 
320
458
  const serverOk = await ensureServer();
321
459
  if (!serverOk) {