fogact 1.2.5 → 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/bin/web-server.js +481 -22
- package/frontend/admin/admin-panel-v2.js +115 -7
- package/frontend/admin/index.html +3 -1
- package/frontend/user/assets/CardBind-CsCxihhP-fogact-live.js +1 -1
- package/frontend/user/assets/CardBind-CsCxihhP.js +1 -1
- package/lib/commands/test.js +23 -14
- package/lib/services/database.js +58 -0
- package/lib/services/node-service.js +123 -46
- package/package.json +1 -1
package/lib/services/database.js
CHANGED
|
@@ -7,6 +7,7 @@ const DATA_DIR = path.join(__dirname, "..", "..", "data");
|
|
|
7
7
|
const USERS_FILE = path.join(DATA_DIR, "users.json");
|
|
8
8
|
const CODES_FILE = path.join(DATA_DIR, "codes.json");
|
|
9
9
|
const USAGE_FILE = path.join(DATA_DIR, "usage.json");
|
|
10
|
+
const CARD_MERGES_FILE = path.join(DATA_DIR, "card-merges.json");
|
|
10
11
|
|
|
11
12
|
// 确保数据目录存在
|
|
12
13
|
function ensureDataDir() {
|
|
@@ -136,6 +137,11 @@ const codeDb = {
|
|
|
136
137
|
name: codeData.name || `Code-${Date.now()}`,
|
|
137
138
|
service: codeData.service || "Claude Code",
|
|
138
139
|
category: codeData.category || "标准运营",
|
|
140
|
+
subServiceType: codeData.subServiceType || codeData.category || "标准运营",
|
|
141
|
+
billingType: codeData.billingType || codeData.quota?.billingType || codeData.quota?.type || "quota",
|
|
142
|
+
cycleType: codeData.cycleType || codeData.quota?.cycleType || codeData.quota?.type || "fixed",
|
|
143
|
+
quotaUnit: codeData.quotaUnit || codeData.quota?.unit || "tokens",
|
|
144
|
+
resetTimezone: codeData.resetTimezone || codeData.quota?.resetTimezone || "Asia/Shanghai",
|
|
139
145
|
status: codeData.status || "未使用",
|
|
140
146
|
enabled: codeData.enabled !== undefined ? codeData.enabled : true,
|
|
141
147
|
usedBy: codeData.usedBy || null,
|
|
@@ -148,9 +154,14 @@ const codeDb = {
|
|
|
148
154
|
total: codeData.quota?.total || 100000,
|
|
149
155
|
used: codeData.quota?.used || 0,
|
|
150
156
|
dailyLimit: codeData.quota?.dailyLimit || 5000,
|
|
157
|
+
dailyQuota: codeData.quota?.dailyQuota || codeData.quota?.dailyLimit || 5000,
|
|
151
158
|
dailyUsed: codeData.quota?.dailyUsed || 0,
|
|
152
159
|
periodDays: codeData.quota?.periodDays || 30,
|
|
153
160
|
periodLimit: codeData.quota?.periodLimit || 50000,
|
|
161
|
+
billingType: codeData.billingType || codeData.quota?.billingType || codeData.quota?.type || "quota",
|
|
162
|
+
cycleType: codeData.cycleType || codeData.quota?.cycleType || codeData.quota?.type || "fixed",
|
|
163
|
+
unit: codeData.quotaUnit || codeData.quota?.unit || "tokens",
|
|
164
|
+
resetTimezone: codeData.resetTimezone || codeData.quota?.resetTimezone || "Asia/Shanghai",
|
|
154
165
|
},
|
|
155
166
|
// 计费设置
|
|
156
167
|
billing: {
|
|
@@ -303,6 +314,52 @@ const usageDb = {
|
|
|
303
314
|
},
|
|
304
315
|
};
|
|
305
316
|
|
|
317
|
+
// 叠卡合并记录数据库操作
|
|
318
|
+
const cardMergeDb = {
|
|
319
|
+
getAll() {
|
|
320
|
+
return readJsonFile(CARD_MERGES_FILE, []);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
create(mergeData) {
|
|
324
|
+
const items = this.getAll();
|
|
325
|
+
const now = new Date().toISOString();
|
|
326
|
+
const newItem = {
|
|
327
|
+
id: mergeData.id || `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
328
|
+
parentCodeId: mergeData.parentCodeId,
|
|
329
|
+
parentCode: mergeData.parentCode,
|
|
330
|
+
childCodeId: mergeData.childCodeId,
|
|
331
|
+
childCode: mergeData.childCode,
|
|
332
|
+
serviceType: mergeData.serviceType,
|
|
333
|
+
subServiceType: mergeData.subServiceType || "",
|
|
334
|
+
billingType: mergeData.billingType,
|
|
335
|
+
cycleType: mergeData.cycleType,
|
|
336
|
+
quotaUnit: mergeData.quotaUnit,
|
|
337
|
+
timezone: mergeData.timezone || "Asia/Shanghai",
|
|
338
|
+
mergeMode: mergeData.mergeMode,
|
|
339
|
+
parentDailyQuota: Number(mergeData.parentDailyQuota || 0),
|
|
340
|
+
childDailyQuota: Number(mergeData.childDailyQuota || 0),
|
|
341
|
+
childDays: Number(mergeData.childDays || 0),
|
|
342
|
+
childTotalValue: Number(mergeData.childTotalValue || 0),
|
|
343
|
+
addedDays: Number(mergeData.addedDays || 0),
|
|
344
|
+
addedQuota: Number(mergeData.addedQuota || 0),
|
|
345
|
+
oldExpiresAt: mergeData.oldExpiresAt || null,
|
|
346
|
+
newExpiresAt: mergeData.newExpiresAt || null,
|
|
347
|
+
createdAt: mergeData.createdAt || now,
|
|
348
|
+
};
|
|
349
|
+
items.push(newItem);
|
|
350
|
+
writeJsonFile(CARD_MERGES_FILE, items);
|
|
351
|
+
return newItem;
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
getByParent(parentCodeId) {
|
|
355
|
+
return this.getAll().filter((item) => item.parentCodeId === parentCodeId);
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
getByChild(childCodeId) {
|
|
359
|
+
return this.getAll().filter((item) => item.childCodeId === childCodeId);
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
306
363
|
// 初始化示例数据
|
|
307
364
|
function initializeSampleData() {
|
|
308
365
|
ensureDataDir();
|
|
@@ -504,5 +561,6 @@ module.exports = {
|
|
|
504
561
|
userDb,
|
|
505
562
|
codeDb,
|
|
506
563
|
usageDb,
|
|
564
|
+
cardMergeDb,
|
|
507
565
|
initializeSampleData,
|
|
508
566
|
};
|
|
@@ -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,22 +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
|
-
const service = padCell(result.serviceLabel || result.region || "Global", 9);
|
|
104
189
|
|
|
105
190
|
if (!result.available) {
|
|
106
|
-
return ` ${mark} ${name}
|
|
191
|
+
return ` ${mark} ${name} ping:${formatProbe(result.ping)} tcp:${formatProbe(result.tcp)} http:${formatProbe(result.http)} 不可达`;
|
|
107
192
|
}
|
|
108
193
|
|
|
109
|
-
const latency = padCell(`${result.avgLatency}ms`, 8);
|
|
110
|
-
const jitter = padCell(`±${result.latencyStdDev}ms`, 7);
|
|
111
|
-
const stability = padCell(stabilityLabel(result.latencyStdDev), 6);
|
|
112
|
-
const score = padCell(`${getResultScore(result)}分`, 5);
|
|
113
|
-
const level = padCell(latencyLevel(result.avgLatency), 4);
|
|
114
194
|
const bestMark = best && best === result ? " ★ 最优" : "";
|
|
115
|
-
return ` ${mark} ${name}
|
|
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}`;
|
|
116
196
|
}
|
|
117
197
|
|
|
118
198
|
function formatNodeResults(results, options = {}) {
|
|
@@ -123,21 +203,16 @@ function formatNodeResults(results, options = {}) {
|
|
|
123
203
|
const title = options.title || "节点测试结果";
|
|
124
204
|
|
|
125
205
|
lines.push(` ${title}`);
|
|
126
|
-
lines.push("
|
|
127
|
-
lines.push("
|
|
128
|
-
lines.push(" ────────────────────────────────────────────────────────────");
|
|
206
|
+
lines.push(" ───────────────────────────────────────────────────");
|
|
207
|
+
lines.push("");
|
|
129
208
|
|
|
130
209
|
for (const result of sorted) {
|
|
131
210
|
lines.push(formatNodeLine(result, best));
|
|
132
211
|
}
|
|
133
212
|
|
|
134
|
-
lines.push("
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push(" ───────────────────────────────────────────────────");
|
|
135
215
|
lines.push(` 测试完成,共 ${results.length} 个节点,${availableCount} 个可用`);
|
|
136
|
-
if (best) {
|
|
137
|
-
lines.push(` 推荐节点: ${best.name || best.url} · ${best.avgLatency}ms · ${getResultScore(best)}分`);
|
|
138
|
-
} else {
|
|
139
|
-
lines.push(" 暂无可用节点,请稍后重试或联系管理员");
|
|
140
|
-
}
|
|
141
216
|
return lines.join("\n");
|
|
142
217
|
}
|
|
143
218
|
|
|
@@ -146,4 +221,6 @@ module.exports = {
|
|
|
146
221
|
selectBestNode,
|
|
147
222
|
formatNodeResults,
|
|
148
223
|
sortNodeResults,
|
|
224
|
+
probeNode,
|
|
225
|
+
testTcp,
|
|
149
226
|
};
|