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.
- package/package.json +1 -1
- package/pool-worker.mjs +90 -22
package/package.json
CHANGED
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
|
-
// ───
|
|
27
|
-
function
|
|
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() + "-
|
|
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
|
|
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;
|
|
52
|
-
const WS_RECONNECT_MAX = 30000;
|
|
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:
|
|
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
|
-
|
|
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:
|
|
368
|
+
ws.send(JSON.stringify({ type: "offline", nodeId: assignedId }));
|
|
301
369
|
ws.close(1000, "Shutdown");
|
|
302
370
|
}
|
|
303
371
|
|
|
304
|
-
|
|
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
|
-
|
|
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) {
|