fogact 1.2.6 → 1.2.7
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/lib/services/node-service.js +119 -31
- package/package.json +1 -1
|
@@ -1,38 +1,122 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { execFile } = require("child_process");
|
|
4
|
+
const net = require("net");
|
|
3
5
|
const { testNode } = require("./fogact-api");
|
|
4
6
|
|
|
7
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
8
|
+
|
|
9
|
+
function parseNodeUrl(nodeUrl) {
|
|
10
|
+
const url = new URL(nodeUrl);
|
|
11
|
+
return {
|
|
12
|
+
hostname: url.hostname,
|
|
13
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
14
|
+
protocol: url.protocol,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function execPing(hostname) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const start = Date.now();
|
|
21
|
+
const child = execFile("ping", ["-c", "1", "-W", "2", hostname], { timeout: 3000 }, (error, stdout) => {
|
|
22
|
+
if (error) {
|
|
23
|
+
resolve({ ok: false, latency: -1, error: error.message });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const match = String(stdout).match(/time[=<]([0-9.]+)\s*ms/i);
|
|
27
|
+
resolve({ ok: true, latency: match ? Math.round(Number(match[1])) : Date.now() - start });
|
|
28
|
+
});
|
|
29
|
+
child.on("error", (error) => resolve({ ok: false, latency: -1, error: error.message }));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function testTcp(hostname, port, timeout = PROBE_TIMEOUT_MS) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
const socket = net.createConnection({ host: hostname, port });
|
|
37
|
+
let settled = false;
|
|
38
|
+
|
|
39
|
+
const done = (result) => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
socket.destroy();
|
|
43
|
+
resolve(result);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
socket.setTimeout(timeout);
|
|
47
|
+
socket.once("connect", () => done({ ok: true, latency: Date.now() - start }));
|
|
48
|
+
socket.once("timeout", () => done({ ok: false, latency: -1, error: "TCP timeout" }));
|
|
49
|
+
socket.once("error", (error) => done({ ok: false, latency: -1, error: error.message }));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function probeNode(node) {
|
|
54
|
+
const { hostname, port } = parseNodeUrl(node.url);
|
|
55
|
+
const [ping, tcp, http] = await Promise.all([
|
|
56
|
+
execPing(hostname),
|
|
57
|
+
testTcp(hostname, port),
|
|
58
|
+
testNode(node.url),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ping,
|
|
63
|
+
tcp,
|
|
64
|
+
http: {
|
|
65
|
+
ok: Boolean(http.available),
|
|
66
|
+
latency: Number(http.latency || -1),
|
|
67
|
+
error: http.error,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getSuccessfulLatencies(probes) {
|
|
73
|
+
return probes.flatMap((probe) => [probe.ping, probe.tcp, probe.http])
|
|
74
|
+
.filter((entry) => entry && entry.ok && Number(entry.latency) >= 0)
|
|
75
|
+
.map((entry) => Number(entry.latency));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function summarizeProbeResults(probes) {
|
|
79
|
+
const last = probes[probes.length - 1] || {};
|
|
80
|
+
const latencies = getSuccessfulLatencies(probes);
|
|
81
|
+
const avgLatency = latencies.length
|
|
82
|
+
? Math.round(latencies.reduce((sum, value) => sum + value, 0) / latencies.length)
|
|
83
|
+
: -1;
|
|
84
|
+
const latencyStdDev = latencies.length
|
|
85
|
+
? Math.round(Math.sqrt(latencies.reduce((sum, value) => sum + Math.pow(value - avgLatency, 2), 0) / latencies.length))
|
|
86
|
+
: 0;
|
|
87
|
+
const successRate = probes.length
|
|
88
|
+
? probes.filter((probe) => probe.tcp?.ok && probe.http?.ok).length / probes.length
|
|
89
|
+
: 0;
|
|
90
|
+
const available = probes.some((probe) => probe.tcp?.ok && probe.http?.ok);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
available,
|
|
94
|
+
reachable: available,
|
|
95
|
+
latency: avgLatency,
|
|
96
|
+
avgLatency,
|
|
97
|
+
latencyStdDev,
|
|
98
|
+
successRate,
|
|
99
|
+
ping: last.ping || { ok: false, latency: -1 },
|
|
100
|
+
tcp: last.tcp || { ok: false, latency: -1 },
|
|
101
|
+
http: last.http || { ok: false, latency: -1 },
|
|
102
|
+
error: available ? undefined : last.http?.error || last.tcp?.error || last.ping?.error || "节点不可达",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
5
106
|
async function testNodes(nodes, onProgress = null) {
|
|
6
107
|
const results = [];
|
|
7
108
|
|
|
8
109
|
for (const node of nodes) {
|
|
9
110
|
const probes = [];
|
|
10
111
|
for (let index = 0; index < 3; index += 1) {
|
|
11
|
-
|
|
12
|
-
probes.push(result);
|
|
112
|
+
probes.push(await probeNode(node));
|
|
13
113
|
}
|
|
14
114
|
|
|
15
|
-
const
|
|
16
|
-
const latencies = availableProbes.map((probe) => probe.latency);
|
|
17
|
-
const avgLatency = latencies.length
|
|
18
|
-
? Math.round(latencies.reduce((sum, value) => sum + value, 0) / latencies.length)
|
|
19
|
-
: -1;
|
|
20
|
-
const latencyStdDev = latencies.length
|
|
21
|
-
? Math.round(Math.sqrt(latencies.reduce((sum, value) => sum + Math.pow(value - avgLatency, 2), 0) / latencies.length))
|
|
22
|
-
: 0;
|
|
23
|
-
const successRate = availableProbes.length / probes.length;
|
|
24
|
-
const available = successRate > 0 && avgLatency >= 0;
|
|
25
|
-
|
|
115
|
+
const summary = summarizeProbeResults(probes);
|
|
26
116
|
results.push({
|
|
27
117
|
...node,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
latency: avgLatency,
|
|
31
|
-
avgLatency,
|
|
32
|
-
latencyStdDev,
|
|
33
|
-
successRate,
|
|
34
|
-
score: scoreNode({ available, avgLatency, latencyStdDev, successRate }),
|
|
35
|
-
error: available ? undefined : probes.find((probe) => probe.error)?.error || "节点不可达",
|
|
118
|
+
...summary,
|
|
119
|
+
score: scoreNode(summary),
|
|
36
120
|
});
|
|
37
121
|
|
|
38
122
|
if (onProgress) onProgress(node, results.length, nodes.length);
|
|
@@ -46,7 +130,9 @@ function scoreNode(result) {
|
|
|
46
130
|
const latencyScore = Math.max(0, 100 - Math.round(result.avgLatency / 4));
|
|
47
131
|
const stabilityScore = Math.max(0, 100 - result.latencyStdDev * 2);
|
|
48
132
|
const reliabilityScore = Math.round((result.successRate || 0) * 100);
|
|
49
|
-
|
|
133
|
+
const pingBonus = result.ping?.ok ? 5 : 0;
|
|
134
|
+
const tcpBonus = result.tcp?.ok ? 5 : 0;
|
|
135
|
+
return Math.min(100, Math.round(latencyScore * 0.6 + stabilityScore * 0.2 + reliabilityScore * 0.2 + pingBonus + tcpBonus));
|
|
50
136
|
}
|
|
51
137
|
|
|
52
138
|
function getResultLatency(result) {
|
|
@@ -63,6 +149,8 @@ function getResultScore(result) {
|
|
|
63
149
|
avgLatency: getResultLatency(result),
|
|
64
150
|
latencyStdDev: result.latencyStdDev || 0,
|
|
65
151
|
successRate: result.successRate || (result.available ? 1 : 0),
|
|
152
|
+
ping: result.ping,
|
|
153
|
+
tcp: result.tcp,
|
|
66
154
|
});
|
|
67
155
|
}
|
|
68
156
|
|
|
@@ -77,13 +165,6 @@ function sortNodeResults(results) {
|
|
|
77
165
|
return [...results].sort((left, right) => getResultScore(right) - getResultScore(left) || getResultLatency(left) - getResultLatency(right));
|
|
78
166
|
}
|
|
79
167
|
|
|
80
|
-
function latencyLevel(latency) {
|
|
81
|
-
if (latency <= 50) return "优秀";
|
|
82
|
-
if (latency <= 100) return "良好";
|
|
83
|
-
if (latency <= 300) return "一般";
|
|
84
|
-
return "较慢";
|
|
85
|
-
}
|
|
86
|
-
|
|
87
168
|
function stabilityLabel(stdDev) {
|
|
88
169
|
if (stdDev <= 5) return "稳定";
|
|
89
170
|
if (stdDev <= 15) return "良好";
|
|
@@ -97,16 +178,21 @@ function padCell(value, width) {
|
|
|
97
178
|
return `${text}${" ".repeat(Math.max(0, width - displayWidth))}`;
|
|
98
179
|
}
|
|
99
180
|
|
|
181
|
+
function formatProbe(entry) {
|
|
182
|
+
if (!entry?.ok) return "--";
|
|
183
|
+
return `${entry.latency}ms`;
|
|
184
|
+
}
|
|
185
|
+
|
|
100
186
|
function formatNodeLine(result, best) {
|
|
101
187
|
const mark = result.available ? "✓" : "✗";
|
|
102
188
|
const name = padCell(result.name || result.url || "FogAct", 12);
|
|
103
189
|
|
|
104
190
|
if (!result.available) {
|
|
105
|
-
return ` ${mark} ${name} 不可达`;
|
|
191
|
+
return ` ${mark} ${name} ping:${formatProbe(result.ping)} tcp:${formatProbe(result.tcp)} http:${formatProbe(result.http)} 不可达`;
|
|
106
192
|
}
|
|
107
193
|
|
|
108
194
|
const bestMark = best && best === result ? " ★ 最优" : "";
|
|
109
|
-
return ` ${mark} ${name} ${result.avgLatency}ms (±${result.latencyStdDev}ms) ${stabilityLabel(result.latencyStdDev)} ${getResultScore(result)}分${bestMark}`;
|
|
195
|
+
return ` ${mark} ${name} ping:${formatProbe(result.ping)} tcp:${formatProbe(result.tcp)} http:${formatProbe(result.http)} ${result.avgLatency}ms (±${result.latencyStdDev}ms) ${stabilityLabel(result.latencyStdDev)} ${getResultScore(result)}分${bestMark}`;
|
|
110
196
|
}
|
|
111
197
|
|
|
112
198
|
function formatNodeResults(results, options = {}) {
|
|
@@ -135,4 +221,6 @@ module.exports = {
|
|
|
135
221
|
selectBestNode,
|
|
136
222
|
formatNodeResults,
|
|
137
223
|
sortNodeResults,
|
|
224
|
+
probeNode,
|
|
225
|
+
testTcp,
|
|
138
226
|
};
|