panrouter 5.1.1 → 5.3.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 +132 -36
package/package.json
CHANGED
package/pool-worker.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pan Router Pool Worker —
|
|
4
|
+
* Pan Router Pool Worker — 懒汉心跳版
|
|
5
5
|
*
|
|
6
6
|
* 以 Sidecar 模式运行,守护 cloudflared 隧道进程。
|
|
7
|
-
*
|
|
7
|
+
* - 没注册上:死缠烂打,每 10 秒重试直到主控应答
|
|
8
|
+
* - 注册上了:绝对静默,既不轮询也不发心跳
|
|
9
|
+
* - 每 5 分钟发一次全量对齐(防止主控重启丢了节点)
|
|
10
|
+
* - 隧道崩了自动复活,关机优雅告别
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -23,33 +26,20 @@ const AUTH_SECRET = "jiuling-super-secret-2026";
|
|
|
23
26
|
const NODE_ID = os.hostname() + "-worker-" + Math.floor(Math.random() * 1000);
|
|
24
27
|
const SERVER_PORT = 50816;
|
|
25
28
|
const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
|
|
29
|
+
const ALIGN_INTERVAL = 5 * 60 * 1000; // 5 分钟全量对齐
|
|
26
30
|
|
|
27
31
|
let cfProcess = null;
|
|
28
32
|
let isShuttingDown = false;
|
|
33
|
+
let isConfirmed = false; // 主控是否已确认注册
|
|
29
34
|
let currentUrl = "";
|
|
35
|
+
let alignTimer = null;
|
|
30
36
|
|
|
31
37
|
function log(msg, type = "INFO") {
|
|
32
38
|
const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
|
|
33
39
|
console.log(`${icons[type] || "▪"} [节点] ${msg}`);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
// ───
|
|
37
|
-
function notifyHub(status, url = "") {
|
|
38
|
-
const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
|
|
39
|
-
const req = https.request(MAIN_HUB_URL, {
|
|
40
|
-
method: "POST",
|
|
41
|
-
headers: {
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
"Content-Length": Buffer.byteLength(payload),
|
|
44
|
-
"x-secret-token": AUTH_SECRET,
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
req.on("error", () => {});
|
|
48
|
-
req.write(payload);
|
|
49
|
-
req.end();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 可等待的 notify — 用于首次上线确认
|
|
42
|
+
// ─── 可等待的通知(返回 true=200 应答) ─────────────────────────────────────
|
|
53
43
|
function notifyHubWait(status, url = "") {
|
|
54
44
|
return new Promise((resolve) => {
|
|
55
45
|
const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
|
|
@@ -68,19 +58,54 @@ function notifyHubWait(status, url = "") {
|
|
|
68
58
|
});
|
|
69
59
|
}
|
|
70
60
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
// ─── 发后即忘的通知(离线遗言用,不等回执) ────────────────────────────────
|
|
62
|
+
function notifyHub(status, url = "") {
|
|
63
|
+
try {
|
|
64
|
+
const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
|
|
65
|
+
const req = https.request(MAIN_HUB_URL, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
70
|
+
"x-secret-token": AUTH_SECRET,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
req.on("error", () => {});
|
|
74
|
+
req.write(payload);
|
|
75
|
+
req.end();
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── 懒汉注册:死缠烂打直到主控应答 ─────────────────────────────────────────
|
|
80
|
+
async function aggressiveRegister(url) {
|
|
81
|
+
log("正在向主控注册...");
|
|
82
|
+
while (!isShuttingDown) {
|
|
83
|
+
const ok = await notifyHubWait("online", url);
|
|
84
|
+
if (ok) {
|
|
85
|
+
log("主控已确认节点注册", "OK");
|
|
86
|
+
isConfirmed = true;
|
|
87
|
+
startAlignHeartbeat(); // 注册成功 → 开启长间隔对齐
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
log(`注册失败,10 秒后重试...`, "WARN");
|
|
91
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
82
92
|
}
|
|
83
|
-
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── 5 分钟全量对齐心跳(主控重启后自动恢复) ──────────────────────────────
|
|
96
|
+
function startAlignHeartbeat() {
|
|
97
|
+
if (alignTimer) clearInterval(alignTimer);
|
|
98
|
+
alignTimer = setInterval(async () => {
|
|
99
|
+
if (isShuttingDown || !currentUrl) return;
|
|
100
|
+
const ok = await notifyHubWait("online", currentUrl);
|
|
101
|
+
if (ok) {
|
|
102
|
+
log("全量对齐完成", "HART");
|
|
103
|
+
} else {
|
|
104
|
+
log("全量对齐失败,主控可能已重启,重新注册...", "WARN");
|
|
105
|
+
isConfirmed = false;
|
|
106
|
+
aggressiveRegister(currentUrl); // 异步重入注册流程
|
|
107
|
+
}
|
|
108
|
+
}, ALIGN_INTERVAL);
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
// ─── 检查端口 ────────────────────────────────────────────────────────────────
|
|
@@ -93,7 +118,7 @@ function isPortOpen(port) {
|
|
|
93
118
|
});
|
|
94
119
|
}
|
|
95
120
|
|
|
96
|
-
// ─── 查找 cloudflared
|
|
121
|
+
// ─── 查找 & 自动安装 cloudflared ────────────────────────────────────────────
|
|
97
122
|
function findCloudflared() {
|
|
98
123
|
const candidates = ["cloudflared", "cloudflared.exe"];
|
|
99
124
|
for (const name of candidates) {
|
|
@@ -113,6 +138,56 @@ function findCloudflared() {
|
|
|
113
138
|
return null;
|
|
114
139
|
}
|
|
115
140
|
|
|
141
|
+
async function installCloudflared() {
|
|
142
|
+
log("正在自动安装 cloudflared...");
|
|
143
|
+
const platform = process.platform;
|
|
144
|
+
try {
|
|
145
|
+
if (platform === "win32") {
|
|
146
|
+
const dir = path.join(process.env.USERPROFILE || "C:/Users/default", "AppData", "Local", "cloudflared");
|
|
147
|
+
const exe = path.join(dir, "cloudflared.exe");
|
|
148
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
149
|
+
log("正在下载 cloudflared (Windows)...");
|
|
150
|
+
const url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe";
|
|
151
|
+
spawnSync("curl.exe", ["-L", "-o", exe, url], { stdio: "pipe", timeout: 120000 });
|
|
152
|
+
if (fs.existsSync(exe)) {
|
|
153
|
+
// 添加至用户 PATH(下次终端生效)
|
|
154
|
+
const currentPath = process.env.PATH || "";
|
|
155
|
+
if (!currentPath.includes(dir)) {
|
|
156
|
+
try {
|
|
157
|
+
spawnSync("powershell.exe", [
|
|
158
|
+
"-Command", `[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path","User") + ";${dir}", "User")`
|
|
159
|
+
], { stdio: "pipe", timeout: 10000 });
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
log("cloudflared 安装完成", "OK");
|
|
163
|
+
return exe;
|
|
164
|
+
}
|
|
165
|
+
} else if (platform === "darwin") {
|
|
166
|
+
log("正在通过 Homebrew 安装 cloudflared...");
|
|
167
|
+
spawnSync("brew", ["install", "cloudflare/cloudflare/cloudflared"], { stdio: "inherit", timeout: 120000 });
|
|
168
|
+
const r = spawnSync("which", ["cloudflared"], { stdio: "pipe", timeout: 5000 });
|
|
169
|
+
if (r.status === 0) { log("cloudflared 安装完成", "OK"); return "cloudflared"; }
|
|
170
|
+
} else {
|
|
171
|
+
// Linux / Termux
|
|
172
|
+
const isTermux = fs.existsSync("/data/data/com.termux");
|
|
173
|
+
if (isTermux) {
|
|
174
|
+
log("正在通过 pkg 安装 cloudflared (Termux)...");
|
|
175
|
+
spawnSync("pkg", ["install", "cloudflared", "-y"], { stdio: "inherit", timeout: 120000 });
|
|
176
|
+
} else {
|
|
177
|
+
log("正在下载 cloudflared (Linux)...");
|
|
178
|
+
const url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64";
|
|
179
|
+
spawnSync("curl", ["-L", "-o", "/usr/local/bin/cloudflared", url], { stdio: "pipe", timeout: 120000 });
|
|
180
|
+
spawnSync("chmod", ["+x", "/usr/local/bin/cloudflared"], { stdio: "pipe", timeout: 5000 });
|
|
181
|
+
}
|
|
182
|
+
const r = spawnSync("which", ["cloudflared"], { stdio: "pipe", timeout: 5000 });
|
|
183
|
+
if (r.status === 0) { log("cloudflared 安装完成", "OK"); return "cloudflared"; }
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
log(`自动安装失败: ${e.message}`, "ERR");
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
116
191
|
// ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
|
|
117
192
|
async function ensureServer() {
|
|
118
193
|
if (await isPortOpen(SERVER_PORT)) {
|
|
@@ -151,12 +226,26 @@ async function ensureServer() {
|
|
|
151
226
|
function startTunnel() {
|
|
152
227
|
if (isShuttingDown) return;
|
|
153
228
|
|
|
154
|
-
|
|
229
|
+
let cfPath = findCloudflared();
|
|
155
230
|
if (!cfPath) {
|
|
156
|
-
log("未找到 cloudflared
|
|
231
|
+
log("未找到 cloudflared,尝试自动安装...");
|
|
232
|
+
// 异步安装,不阻塞隧道循环
|
|
233
|
+
installCloudflared().then((installed) => {
|
|
234
|
+
if (installed) {
|
|
235
|
+
log("安装成功,正在启动隧道...");
|
|
236
|
+
doStartTunnel(installed);
|
|
237
|
+
} else {
|
|
238
|
+
log("自动安装失败,请手动安装 cloudflared", "ERR");
|
|
239
|
+
}
|
|
240
|
+
});
|
|
157
241
|
return;
|
|
158
242
|
}
|
|
159
243
|
|
|
244
|
+
doStartTunnel(cfPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function doStartTunnel(cfPath) {
|
|
248
|
+
|
|
160
249
|
log("正在请求匿名公网入口...");
|
|
161
250
|
|
|
162
251
|
cfProcess = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
|
|
@@ -171,8 +260,9 @@ function startTunnel() {
|
|
|
171
260
|
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
172
261
|
if (match && match[0] !== currentUrl) {
|
|
173
262
|
currentUrl = match[0];
|
|
263
|
+
isConfirmed = false; // 新 URL,重置确认状态
|
|
174
264
|
log(`成功获取公网入口: ${currentUrl}`, "OK");
|
|
175
|
-
|
|
265
|
+
aggressiveRegister(currentUrl); // 死缠烂打直到主控应答
|
|
176
266
|
}
|
|
177
267
|
|
|
178
268
|
// 网络波动提示
|
|
@@ -184,6 +274,8 @@ function startTunnel() {
|
|
|
184
274
|
cfProcess.on("close", (code) => {
|
|
185
275
|
log(`隧道进程断开退出 (code: ${code})`, "ERR");
|
|
186
276
|
cfProcess = null;
|
|
277
|
+
isConfirmed = false;
|
|
278
|
+
if (alignTimer) { clearInterval(alignTimer); alignTimer = null; }
|
|
187
279
|
|
|
188
280
|
// 通知主控剔除
|
|
189
281
|
if (currentUrl) {
|
|
@@ -194,7 +286,11 @@ function startTunnel() {
|
|
|
194
286
|
// 自动复活
|
|
195
287
|
if (!isShuttingDown) {
|
|
196
288
|
log("准备在 5 秒后自动重启隧道...", "WARN");
|
|
197
|
-
setTimeout(
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
const cf = findCloudflared();
|
|
291
|
+
if (cf) doStartTunnel(cf);
|
|
292
|
+
else startTunnel();
|
|
293
|
+
}, 5000);
|
|
198
294
|
}
|
|
199
295
|
});
|
|
200
296
|
|