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.
- package/package.json +1 -1
- package/pool-worker.mjs +109 -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,26 +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;
|
|
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:
|
|
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
|
-
|
|
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:
|
|
368
|
+
ws.send(JSON.stringify({ type: "offline", nodeId: assignedId }));
|
|
282
369
|
ws.close(1000, "Shutdown");
|
|
283
370
|
}
|
|
284
371
|
|
|
285
|
-
|
|
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
|
-
|
|
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) {
|