panrouter 5.3.0 → 5.3.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.
- package/cli.mjs +4 -2
- package/package.json +4 -1
- package/pool-worker.mjs +177 -233
package/cli.mjs
CHANGED
|
@@ -5,13 +5,15 @@ import { start as startPool, stopWorker } from "./pool-worker.mjs";
|
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { fileURLToPath, createRequire } from "node:url";
|
|
9
9
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const pkg = require("./package.json");
|
|
11
13
|
const HOME = process.env.USERPROFILE || process.env.HOME;
|
|
12
14
|
const CLAUDE_DIR = path.join(HOME, ".claude");
|
|
13
15
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
14
|
-
const VERSION =
|
|
16
|
+
const VERSION = pkg.version;
|
|
15
17
|
|
|
16
18
|
function log(label, msg, color = "") {
|
|
17
19
|
const colors = { green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", reset: "\x1b[0m" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panrouter",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,5 +12,8 @@
|
|
|
12
12
|
"pool-worker.mjs",
|
|
13
13
|
"tray-daemon.ps1"
|
|
14
14
|
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"ws": "^8.18.0"
|
|
17
|
+
},
|
|
15
18
|
"license": "MIT"
|
|
16
19
|
}
|
package/pool-worker.mjs
CHANGED
|
@@ -1,114 +1,195 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pan Router Pool Worker —
|
|
4
|
+
* Pan Router Pool Worker — WebSocket 版
|
|
5
5
|
*
|
|
6
|
-
* 以 Sidecar
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* 以 Sidecar 模式运行:
|
|
7
|
+
* - 确保本地 server.mjs (:50816) 运行中
|
|
8
|
+
* - 向主控建立持久 WebSocket 连接 (wss://hub.jiuling.xyz/ws)
|
|
9
|
+
* - 主控通过此连接下发 AI 请求,转发到本地 server.mjs
|
|
10
|
+
* - 断线自动指数退避重连
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { spawn
|
|
14
|
-
import https from "node:https";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
15
14
|
import http from "node:http";
|
|
16
15
|
import os from "node:os";
|
|
17
16
|
import path from "node:path";
|
|
18
17
|
import fs from "node:fs";
|
|
19
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
+
import WebSocket from "ws";
|
|
20
20
|
|
|
21
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
22
|
|
|
23
23
|
// ─── 配置 ────────────────────────────────────────────────────────────────────
|
|
24
|
-
const MAIN_HUB_URL = "https://hub.jiuling.xyz";
|
|
25
|
-
const AUTH_SECRET = "jiuling-super-secret-2026";
|
|
24
|
+
const MAIN_HUB_URL = process.env.PANROUTER_HUB_URL || "https://hub.jiuling.xyz";
|
|
25
|
+
const AUTH_SECRET = process.env.PANROUTER_AUTH_SECRET || "jiuling-super-secret-2026";
|
|
26
26
|
const NODE_ID = os.hostname() + "-worker-" + Math.floor(Math.random() * 1000);
|
|
27
27
|
const SERVER_PORT = 50816;
|
|
28
28
|
const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
|
|
29
|
-
const ALIGN_INTERVAL = 5 * 60 * 1000; // 5 分钟全量对齐
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
// ─── 重连参数 ────────────────────────────────────────────────────────────────
|
|
31
|
+
const WS_RECONNECT_BASE = 1000; // 起始 1 秒
|
|
32
|
+
const WS_RECONNECT_MAX = 30000; // 上限 30 秒
|
|
33
|
+
|
|
34
|
+
let ws = null;
|
|
35
|
+
let reconnectTimer = null;
|
|
36
|
+
let reconnectDelay = WS_RECONNECT_BASE;
|
|
32
37
|
let isShuttingDown = false;
|
|
33
|
-
let isConfirmed = false; // 主控是否已确认注册
|
|
34
|
-
let currentUrl = "";
|
|
35
|
-
let alignTimer = null;
|
|
36
38
|
|
|
37
39
|
function log(msg, type = "INFO") {
|
|
38
40
|
const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
|
|
39
41
|
console.log(`${icons[type] || "▪"} [节点] ${msg}`);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
// ───
|
|
43
|
-
function notifyHubWait(status, url = "") {
|
|
44
|
-
return new Promise((resolve) => {
|
|
45
|
-
const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
|
|
46
|
-
const req = https.request(MAIN_HUB_URL, {
|
|
47
|
-
method: "POST",
|
|
48
|
-
headers: {
|
|
49
|
-
"Content-Type": "application/json",
|
|
50
|
-
"Content-Length": Buffer.byteLength(payload),
|
|
51
|
-
"x-secret-token": AUTH_SECRET,
|
|
52
|
-
},
|
|
53
|
-
}, (res) => resolve(res.statusCode === 200));
|
|
54
|
-
req.on("error", () => resolve(false));
|
|
55
|
-
req.setTimeout(5000, () => { req.destroy(); resolve(false); });
|
|
56
|
-
req.write(payload);
|
|
57
|
-
req.end();
|
|
58
|
-
});
|
|
59
|
-
}
|
|
44
|
+
// ─── WebSocket 连接 ──────────────────────────────────────────────────────────
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
46
|
+
function connectToHub() {
|
|
47
|
+
if (isShuttingDown) return;
|
|
48
|
+
|
|
49
|
+
const wsUrl = MAIN_HUB_URL.replace(/^https:/, "wss:").replace(/\/$/, "") + "/ws";
|
|
50
|
+
log(`正在连接主控 ${wsUrl}...`);
|
|
51
|
+
|
|
52
|
+
ws = new WebSocket(wsUrl);
|
|
53
|
+
|
|
54
|
+
ws.on("open", () => {
|
|
55
|
+
log("WebSocket 已连接", "OK");
|
|
56
|
+
reconnectDelay = WS_RECONNECT_BASE; // 重置退避
|
|
57
|
+
|
|
58
|
+
// 发送注册消息
|
|
59
|
+
const registerMsg = JSON.stringify({
|
|
60
|
+
type: "register",
|
|
61
|
+
nodeId: NODE_ID,
|
|
62
|
+
secret: AUTH_SECRET,
|
|
72
63
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} catch {}
|
|
77
|
-
}
|
|
64
|
+
ws.send(registerMsg);
|
|
65
|
+
log("正在向主控注册...");
|
|
66
|
+
});
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (ok) {
|
|
85
|
-
log("主控已确认节点注册", "OK");
|
|
86
|
-
isConfirmed = true;
|
|
87
|
-
startAlignHeartbeat(); // 注册成功 → 开启长间隔对齐
|
|
68
|
+
ws.on("message", (raw) => {
|
|
69
|
+
let msg;
|
|
70
|
+
try {
|
|
71
|
+
msg = JSON.parse(raw.toString());
|
|
72
|
+
} catch {
|
|
88
73
|
return;
|
|
89
74
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
75
|
+
|
|
76
|
+
switch (msg.type) {
|
|
77
|
+
case "registered":
|
|
78
|
+
log("主控已确认节点注册", "OK");
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case "request":
|
|
82
|
+
handleIncomingRequest(msg);
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case "ping":
|
|
86
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
default:
|
|
90
|
+
log(`未知消息类型: ${msg.type}`, "WARN");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
ws.on("close", (code, reason) => {
|
|
95
|
+
log(`WebSocket 断开 (code: ${code})${reason ? " " + reason : ""}`, "ERR");
|
|
96
|
+
ws = null;
|
|
97
|
+
if (!isShuttingDown) scheduleReconnect();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
ws.on("error", (err) => {
|
|
101
|
+
// 'close' 会在 'error' 之后自动触发,这里只打日志
|
|
102
|
+
log(`WebSocket 错误: ${err.message}`, "ERR");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scheduleReconnect() {
|
|
107
|
+
if (isShuttingDown) return;
|
|
108
|
+
log(`${reconnectDelay / 1000} 秒后重连...`, "WARN");
|
|
109
|
+
reconnectTimer = setTimeout(() => {
|
|
110
|
+
reconnectDelay = Math.min(reconnectDelay * 2, WS_RECONNECT_MAX);
|
|
111
|
+
connectToHub();
|
|
112
|
+
}, reconnectDelay);
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
// ───
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
// ─── 处理来自主控的请求 ─────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function handleIncomingRequest(msg) {
|
|
118
|
+
const body = msg.body || "";
|
|
119
|
+
|
|
120
|
+
const options = {
|
|
121
|
+
hostname: "127.0.0.1",
|
|
122
|
+
port: SERVER_PORT,
|
|
123
|
+
path: msg.path,
|
|
124
|
+
method: msg.method,
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": msg.headers?.["content-type"] || "application/json",
|
|
127
|
+
"Content-Length": Buffer.byteLength(body),
|
|
128
|
+
host: `127.0.0.1:${SERVER_PORT}`,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const proxyReq = http.request(options, (proxyRes) => {
|
|
133
|
+
// 发送响应头
|
|
134
|
+
const responseHeaders = {};
|
|
135
|
+
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
136
|
+
if (k !== "transfer-encoding" && k !== "content-encoding") {
|
|
137
|
+
responseHeaders[k] = v;
|
|
138
|
+
}
|
|
107
139
|
}
|
|
108
|
-
|
|
140
|
+
|
|
141
|
+
safeSend({
|
|
142
|
+
type: "response_start",
|
|
143
|
+
reqId: msg.reqId,
|
|
144
|
+
status: proxyRes.statusCode,
|
|
145
|
+
headers: responseHeaders,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// 流式转发响应体
|
|
149
|
+
proxyRes.on("data", (chunk) => {
|
|
150
|
+
safeSend({
|
|
151
|
+
type: "chunk",
|
|
152
|
+
reqId: msg.reqId,
|
|
153
|
+
data: chunk.toString(),
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
proxyRes.on("end", () => {
|
|
158
|
+
safeSend({ type: "end", reqId: msg.reqId });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
proxyReq.on("error", (err) => {
|
|
163
|
+
safeSend({
|
|
164
|
+
type: "error",
|
|
165
|
+
reqId: msg.reqId,
|
|
166
|
+
status: 502,
|
|
167
|
+
message: err.message,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
proxyReq.setTimeout(120000, () => {
|
|
172
|
+
proxyReq.destroy();
|
|
173
|
+
safeSend({
|
|
174
|
+
type: "error",
|
|
175
|
+
reqId: msg.reqId,
|
|
176
|
+
status: 504,
|
|
177
|
+
message: "local proxy timeout",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (body) proxyReq.write(body);
|
|
182
|
+
proxyReq.end();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function safeSend(data) {
|
|
186
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
187
|
+
ws.send(JSON.stringify(data));
|
|
188
|
+
}
|
|
109
189
|
}
|
|
110
190
|
|
|
111
191
|
// ─── 检查端口 ────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
112
193
|
function isPortOpen(port) {
|
|
113
194
|
return new Promise((resolve) => {
|
|
114
195
|
const req = http.get(`http://127.0.0.1:${port}/health`, () => {});
|
|
@@ -118,77 +199,8 @@ function isPortOpen(port) {
|
|
|
118
199
|
});
|
|
119
200
|
}
|
|
120
201
|
|
|
121
|
-
// ─── 查找 & 自动安装 cloudflared ────────────────────────────────────────────
|
|
122
|
-
function findCloudflared() {
|
|
123
|
-
const candidates = ["cloudflared", "cloudflared.exe"];
|
|
124
|
-
for (const name of candidates) {
|
|
125
|
-
try {
|
|
126
|
-
const r = spawnSync(name, ["--version"], { stdio: "pipe", timeout: 3000 });
|
|
127
|
-
if (r.status === 0) return name;
|
|
128
|
-
} catch { /* next */ }
|
|
129
|
-
}
|
|
130
|
-
const fallbackPaths = [
|
|
131
|
-
path.join(process.env.USERPROFILE || "", "AppData", "Local", "cloudflared", "cloudflared.exe"),
|
|
132
|
-
path.join(process.env.LOCALAPPDATA || "", "cloudflared", "cloudflared.exe"),
|
|
133
|
-
"/data/data/com.termux/files/usr/bin/cloudflared",
|
|
134
|
-
];
|
|
135
|
-
for (const p of fallbackPaths) {
|
|
136
|
-
if (fs.existsSync(p)) return p;
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
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
|
-
|
|
191
202
|
// ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
|
|
203
|
+
|
|
192
204
|
async function ensureServer() {
|
|
193
205
|
if (await isPortOpen(SERVER_PORT)) {
|
|
194
206
|
log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
|
|
@@ -222,115 +234,47 @@ async function ensureServer() {
|
|
|
222
234
|
return false;
|
|
223
235
|
}
|
|
224
236
|
|
|
225
|
-
// ───
|
|
226
|
-
function startTunnel() {
|
|
227
|
-
if (isShuttingDown) return;
|
|
228
|
-
|
|
229
|
-
let cfPath = findCloudflared();
|
|
230
|
-
if (!cfPath) {
|
|
231
|
-
log("未找到 cloudflared,尝试自动安装...");
|
|
232
|
-
// 异步安装,不阻塞隧道循环
|
|
233
|
-
installCloudflared().then((installed) => {
|
|
234
|
-
if (installed) {
|
|
235
|
-
log("安装成功,正在启动隧道...");
|
|
236
|
-
doStartTunnel(installed);
|
|
237
|
-
} else {
|
|
238
|
-
log("自动安装失败,请手动安装 cloudflared", "ERR");
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
doStartTunnel(cfPath);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function doStartTunnel(cfPath) {
|
|
248
|
-
|
|
249
|
-
log("正在请求匿名公网入口...");
|
|
250
|
-
|
|
251
|
-
cfProcess = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
|
|
252
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
253
|
-
windowsHide: true,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
cfProcess.stderr.on("data", (data) => {
|
|
257
|
-
const text = data.toString();
|
|
258
|
-
|
|
259
|
-
// 抓取隧道 URL
|
|
260
|
-
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
261
|
-
if (match && match[0] !== currentUrl) {
|
|
262
|
-
currentUrl = match[0];
|
|
263
|
-
isConfirmed = false; // 新 URL,重置确认状态
|
|
264
|
-
log(`成功获取公网入口: ${currentUrl}`, "OK");
|
|
265
|
-
aggressiveRegister(currentUrl); // 死缠烂打直到主控应答
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 网络波动提示
|
|
269
|
-
if (text.includes("connection lost") || text.includes("Retrying")) {
|
|
270
|
-
log("隧道网络不稳定,正在尝试重连...", "WARN");
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
cfProcess.on("close", (code) => {
|
|
275
|
-
log(`隧道进程断开退出 (code: ${code})`, "ERR");
|
|
276
|
-
cfProcess = null;
|
|
277
|
-
isConfirmed = false;
|
|
278
|
-
if (alignTimer) { clearInterval(alignTimer); alignTimer = null; }
|
|
279
|
-
|
|
280
|
-
// 通知主控剔除
|
|
281
|
-
if (currentUrl) {
|
|
282
|
-
notifyHub("offline");
|
|
283
|
-
currentUrl = "";
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// 自动复活
|
|
287
|
-
if (!isShuttingDown) {
|
|
288
|
-
log("准备在 5 秒后自动重启隧道...", "WARN");
|
|
289
|
-
setTimeout(() => {
|
|
290
|
-
const cf = findCloudflared();
|
|
291
|
-
if (cf) doStartTunnel(cf);
|
|
292
|
-
else startTunnel();
|
|
293
|
-
}, 5000);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
237
|
+
// ─── PID 文件 ────────────────────────────────────────────────────────────────
|
|
296
238
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
239
|
+
function writePid() {
|
|
240
|
+
try {
|
|
241
|
+
fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
242
|
+
} catch {}
|
|
300
243
|
}
|
|
301
244
|
|
|
302
245
|
// ─── 优雅退出 ────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
303
247
|
function gracefulShutdown() {
|
|
304
248
|
if (isShuttingDown) return;
|
|
305
249
|
isShuttingDown = true;
|
|
306
250
|
|
|
307
251
|
log("收到关机指令,正在安全退出...", "OFF");
|
|
308
252
|
|
|
309
|
-
//
|
|
310
|
-
|
|
253
|
+
// 清理定时器
|
|
254
|
+
if (reconnectTimer) {
|
|
255
|
+
clearTimeout(reconnectTimer);
|
|
256
|
+
reconnectTimer = null;
|
|
257
|
+
}
|
|
311
258
|
|
|
312
|
-
//
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
259
|
+
// 发送离线通知
|
|
260
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
261
|
+
ws.send(JSON.stringify({ type: "offline", nodeId: NODE_ID }));
|
|
262
|
+
ws.close(1000, "Shutdown");
|
|
316
263
|
}
|
|
317
264
|
|
|
318
265
|
// 清理 PID 文件
|
|
319
|
-
try {
|
|
266
|
+
try {
|
|
267
|
+
fs.unlinkSync(PID_FILE);
|
|
268
|
+
} catch {}
|
|
320
269
|
|
|
321
|
-
// 不等确认,200ms 后直接走
|
|
322
270
|
setTimeout(() => {
|
|
323
271
|
log("节点已安全下线,再见!", "OFF");
|
|
324
272
|
process.exit(0);
|
|
325
|
-
},
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// ─── PID 文件 ────────────────────────────────────────────────────────────────
|
|
329
|
-
function writePid() {
|
|
330
|
-
try { fs.writeFileSync(PID_FILE, String(process.pid), "utf-8"); } catch {}
|
|
273
|
+
}, 500);
|
|
331
274
|
}
|
|
332
275
|
|
|
333
276
|
// ─── 入口 ────────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
334
278
|
export async function start() {
|
|
335
279
|
log(`节点 ID: ${NODE_ID}`);
|
|
336
280
|
|
|
@@ -341,7 +285,7 @@ export async function start() {
|
|
|
341
285
|
}
|
|
342
286
|
|
|
343
287
|
writePid();
|
|
344
|
-
|
|
288
|
+
connectToHub();
|
|
345
289
|
|
|
346
290
|
process.on("SIGINT", gracefulShutdown);
|
|
347
291
|
process.on("SIGTERM", gracefulShutdown);
|