fogact 1.2.6 → 1.2.8
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/index.js +9 -1
- package/lib/services/node-service.js +153 -35
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -17,6 +17,12 @@ const MENU_CHOICES = [
|
|
|
17
17
|
{ title: "4. 退出", value: "exit" },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
+
const MENU_COLORS = {
|
|
21
|
+
test: "\x1b[34m",
|
|
22
|
+
exit: "\x1b[90m",
|
|
23
|
+
};
|
|
24
|
+
const ANSI_RESET = "\x1b[0m";
|
|
25
|
+
|
|
20
26
|
const UPDATE_TIMEOUT_MS = 2500;
|
|
21
27
|
|
|
22
28
|
function parseVersion(version) {
|
|
@@ -158,7 +164,9 @@ function renderMenu(cursor = 0) {
|
|
|
158
164
|
];
|
|
159
165
|
|
|
160
166
|
MENU_CHOICES.forEach((choice, index) => {
|
|
161
|
-
|
|
167
|
+
const color = MENU_COLORS[choice.value] || "";
|
|
168
|
+
const title = color ? `${color}${choice.title}${ANSI_RESET}` : choice.title;
|
|
169
|
+
lines.push(`${index === cursor ? "❯" : " "} ${title}`);
|
|
162
170
|
});
|
|
163
171
|
|
|
164
172
|
lines.push("");
|
|
@@ -1,38 +1,145 @@
|
|
|
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
|
+
const ANSI = {
|
|
9
|
+
reset: "\x1b[0m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
yellow: "\x1b[33m",
|
|
12
|
+
red: "\x1b[31m",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function shouldColorize(options = {}) {
|
|
16
|
+
if (options.color === true || process.env.FORCE_COLOR) return true;
|
|
17
|
+
if (options.color === false || process.env.NO_COLOR) return false;
|
|
18
|
+
return Boolean(process.stdout.isTTY);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function colorize(value, color, enabled) {
|
|
22
|
+
if (!enabled || !color) return String(value);
|
|
23
|
+
return `${ANSI[color]}${value}${ANSI.reset}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function latencyColor(latency) {
|
|
27
|
+
if (latency < 60) return "green";
|
|
28
|
+
if (latency < 150) return "yellow";
|
|
29
|
+
return "red";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseNodeUrl(nodeUrl) {
|
|
33
|
+
const url = new URL(nodeUrl);
|
|
34
|
+
return {
|
|
35
|
+
hostname: url.hostname,
|
|
36
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
37
|
+
protocol: url.protocol,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function execPing(hostname) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
const child = execFile("ping", ["-c", "1", "-W", "2", hostname], { timeout: 3000 }, (error, stdout) => {
|
|
45
|
+
if (error) {
|
|
46
|
+
resolve({ ok: false, latency: -1, error: error.message });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const match = String(stdout).match(/time[=<]([0-9.]+)\s*ms/i);
|
|
50
|
+
resolve({ ok: true, latency: match ? Math.round(Number(match[1])) : Date.now() - start });
|
|
51
|
+
});
|
|
52
|
+
child.on("error", (error) => resolve({ ok: false, latency: -1, error: error.message }));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function testTcp(hostname, port, timeout = PROBE_TIMEOUT_MS) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
const socket = net.createConnection({ host: hostname, port });
|
|
60
|
+
let settled = false;
|
|
61
|
+
|
|
62
|
+
const done = (result) => {
|
|
63
|
+
if (settled) return;
|
|
64
|
+
settled = true;
|
|
65
|
+
socket.destroy();
|
|
66
|
+
resolve(result);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
socket.setTimeout(timeout);
|
|
70
|
+
socket.once("connect", () => done({ ok: true, latency: Date.now() - start }));
|
|
71
|
+
socket.once("timeout", () => done({ ok: false, latency: -1, error: "TCP timeout" }));
|
|
72
|
+
socket.once("error", (error) => done({ ok: false, latency: -1, error: error.message }));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function probeNode(node) {
|
|
77
|
+
const { hostname, port } = parseNodeUrl(node.url);
|
|
78
|
+
const [ping, tcp, http] = await Promise.all([
|
|
79
|
+
execPing(hostname),
|
|
80
|
+
testTcp(hostname, port),
|
|
81
|
+
testNode(node.url),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ping,
|
|
86
|
+
tcp,
|
|
87
|
+
http: {
|
|
88
|
+
ok: Boolean(http.available),
|
|
89
|
+
latency: Number(http.latency || -1),
|
|
90
|
+
error: http.error,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getSuccessfulLatencies(probes) {
|
|
96
|
+
return probes.flatMap((probe) => [probe.ping, probe.tcp, probe.http])
|
|
97
|
+
.filter((entry) => entry && entry.ok && Number(entry.latency) >= 0)
|
|
98
|
+
.map((entry) => Number(entry.latency));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function summarizeProbeResults(probes) {
|
|
102
|
+
const last = probes[probes.length - 1] || {};
|
|
103
|
+
const latencies = getSuccessfulLatencies(probes);
|
|
104
|
+
const avgLatency = latencies.length
|
|
105
|
+
? Math.round(latencies.reduce((sum, value) => sum + value, 0) / latencies.length)
|
|
106
|
+
: -1;
|
|
107
|
+
const latencyStdDev = latencies.length
|
|
108
|
+
? Math.round(Math.sqrt(latencies.reduce((sum, value) => sum + Math.pow(value - avgLatency, 2), 0) / latencies.length))
|
|
109
|
+
: 0;
|
|
110
|
+
const successRate = probes.length
|
|
111
|
+
? probes.filter((probe) => probe.tcp?.ok && probe.http?.ok).length / probes.length
|
|
112
|
+
: 0;
|
|
113
|
+
const available = probes.some((probe) => probe.tcp?.ok && probe.http?.ok);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
available,
|
|
117
|
+
reachable: available,
|
|
118
|
+
latency: avgLatency,
|
|
119
|
+
avgLatency,
|
|
120
|
+
latencyStdDev,
|
|
121
|
+
successRate,
|
|
122
|
+
ping: last.ping || { ok: false, latency: -1 },
|
|
123
|
+
tcp: last.tcp || { ok: false, latency: -1 },
|
|
124
|
+
http: last.http || { ok: false, latency: -1 },
|
|
125
|
+
error: available ? undefined : last.http?.error || last.tcp?.error || last.ping?.error || "节点不可达",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
5
129
|
async function testNodes(nodes, onProgress = null) {
|
|
6
130
|
const results = [];
|
|
7
131
|
|
|
8
132
|
for (const node of nodes) {
|
|
9
133
|
const probes = [];
|
|
10
134
|
for (let index = 0; index < 3; index += 1) {
|
|
11
|
-
|
|
12
|
-
probes.push(result);
|
|
135
|
+
probes.push(await probeNode(node));
|
|
13
136
|
}
|
|
14
137
|
|
|
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
|
-
|
|
138
|
+
const summary = summarizeProbeResults(probes);
|
|
26
139
|
results.push({
|
|
27
140
|
...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 || "节点不可达",
|
|
141
|
+
...summary,
|
|
142
|
+
score: scoreNode(summary),
|
|
36
143
|
});
|
|
37
144
|
|
|
38
145
|
if (onProgress) onProgress(node, results.length, nodes.length);
|
|
@@ -46,7 +153,9 @@ function scoreNode(result) {
|
|
|
46
153
|
const latencyScore = Math.max(0, 100 - Math.round(result.avgLatency / 4));
|
|
47
154
|
const stabilityScore = Math.max(0, 100 - result.latencyStdDev * 2);
|
|
48
155
|
const reliabilityScore = Math.round((result.successRate || 0) * 100);
|
|
49
|
-
|
|
156
|
+
const pingBonus = result.ping?.ok ? 5 : 0;
|
|
157
|
+
const tcpBonus = result.tcp?.ok ? 5 : 0;
|
|
158
|
+
return Math.min(100, Math.round(latencyScore * 0.6 + stabilityScore * 0.2 + reliabilityScore * 0.2 + pingBonus + tcpBonus));
|
|
50
159
|
}
|
|
51
160
|
|
|
52
161
|
function getResultLatency(result) {
|
|
@@ -63,6 +172,8 @@ function getResultScore(result) {
|
|
|
63
172
|
avgLatency: getResultLatency(result),
|
|
64
173
|
latencyStdDev: result.latencyStdDev || 0,
|
|
65
174
|
successRate: result.successRate || (result.available ? 1 : 0),
|
|
175
|
+
ping: result.ping,
|
|
176
|
+
tcp: result.tcp,
|
|
66
177
|
});
|
|
67
178
|
}
|
|
68
179
|
|
|
@@ -77,13 +188,6 @@ function sortNodeResults(results) {
|
|
|
77
188
|
return [...results].sort((left, right) => getResultScore(right) - getResultScore(left) || getResultLatency(left) - getResultLatency(right));
|
|
78
189
|
}
|
|
79
190
|
|
|
80
|
-
function latencyLevel(latency) {
|
|
81
|
-
if (latency <= 50) return "优秀";
|
|
82
|
-
if (latency <= 100) return "良好";
|
|
83
|
-
if (latency <= 300) return "一般";
|
|
84
|
-
return "较慢";
|
|
85
|
-
}
|
|
86
|
-
|
|
87
191
|
function stabilityLabel(stdDev) {
|
|
88
192
|
if (stdDev <= 5) return "稳定";
|
|
89
193
|
if (stdDev <= 15) return "良好";
|
|
@@ -97,16 +201,26 @@ function padCell(value, width) {
|
|
|
97
201
|
return `${text}${" ".repeat(Math.max(0, width - displayWidth))}`;
|
|
98
202
|
}
|
|
99
203
|
|
|
100
|
-
function
|
|
101
|
-
|
|
204
|
+
function formatProbe(entry, colorEnabled) {
|
|
205
|
+
if (!entry?.ok) return colorize("--", "red", colorEnabled);
|
|
206
|
+
const value = `${entry.latency}ms`;
|
|
207
|
+
return colorize(value, latencyColor(entry.latency), colorEnabled);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function formatNodeLine(result, best, colorEnabled) {
|
|
211
|
+
const mark = colorize(result.available ? "✓" : "✗", result.available ? "green" : "red", colorEnabled);
|
|
102
212
|
const name = padCell(result.name || result.url || "FogAct", 12);
|
|
213
|
+
const ping = `ping:${formatProbe(result.ping, colorEnabled)}`;
|
|
214
|
+
const tcp = `tcp:${formatProbe(result.tcp, colorEnabled)}`;
|
|
215
|
+
const http = `http:${formatProbe(result.http, colorEnabled)}`;
|
|
103
216
|
|
|
104
217
|
if (!result.available) {
|
|
105
|
-
return ` ${mark} ${name}
|
|
218
|
+
return ` ${mark} ${name} ${ping} ${tcp} ${http} ${colorize("不可达", "red", colorEnabled)}`;
|
|
106
219
|
}
|
|
107
220
|
|
|
108
|
-
const
|
|
109
|
-
|
|
221
|
+
const latency = colorize(`${result.avgLatency}ms`, latencyColor(result.avgLatency), colorEnabled);
|
|
222
|
+
const bestMark = best && best === result ? ` ${colorize("★ 最优", "yellow", colorEnabled)}` : "";
|
|
223
|
+
return ` ${mark} ${name} ${ping} ${tcp} ${http} ${latency} (±${result.latencyStdDev}ms) ${stabilityLabel(result.latencyStdDev)} ${getResultScore(result)}分${bestMark}`;
|
|
110
224
|
}
|
|
111
225
|
|
|
112
226
|
function formatNodeResults(results, options = {}) {
|
|
@@ -115,13 +229,14 @@ function formatNodeResults(results, options = {}) {
|
|
|
115
229
|
const availableCount = results.filter((result) => result.available).length;
|
|
116
230
|
const lines = [];
|
|
117
231
|
const title = options.title || "节点测试结果";
|
|
232
|
+
const colorEnabled = shouldColorize(options);
|
|
118
233
|
|
|
119
234
|
lines.push(` ${title}`);
|
|
120
235
|
lines.push(" ───────────────────────────────────────────────────");
|
|
121
236
|
lines.push("");
|
|
122
237
|
|
|
123
238
|
for (const result of sorted) {
|
|
124
|
-
lines.push(formatNodeLine(result, best));
|
|
239
|
+
lines.push(formatNodeLine(result, best, colorEnabled));
|
|
125
240
|
}
|
|
126
241
|
|
|
127
242
|
lines.push("");
|
|
@@ -135,4 +250,7 @@ module.exports = {
|
|
|
135
250
|
selectBestNode,
|
|
136
251
|
formatNodeResults,
|
|
137
252
|
sortNodeResults,
|
|
253
|
+
probeNode,
|
|
254
|
+
testTcp,
|
|
255
|
+
shouldColorize,
|
|
138
256
|
};
|