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 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
- lines.push(`${index === cursor ? "❯" : " "} ${choice.title}`);
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
- const result = await testNode(node.url);
12
- probes.push(result);
135
+ probes.push(await probeNode(node));
13
136
  }
14
137
 
15
- const availableProbes = probes.filter((probe) => probe.available);
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
- available,
29
- reachable: available,
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
- return Math.round(latencyScore * 0.7 + stabilityScore * 0.2 + reliabilityScore * 0.1);
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 formatNodeLine(result, best) {
101
- const mark = result.available ? "" : "";
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 bestMark = best && best === result ? " ★ 最优" : "";
109
- return ` ${mark} ${name} ${result.avgLatency}ms (±${result.latencyStdDev}ms) ${stabilityLabel(result.latencyStdDev)} ${getResultScore(result)}分${bestMark}`;
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fogact",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "FogAct activation helper for Claude Code and Codex",
5
5
  "keywords": [
6
6
  "fogact",