fogact 1.1.10 → 1.2.2
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/web-server.js +303 -97
- package/frontend/admin/admin-panel-v2.js +8 -5
- package/frontend/assets/market-ui.css +11 -84
- package/frontend/user/assets/Dashboard-rPsmltm5.js +1 -1
- package/frontend/user/assets/DashboardLayout-DDkxHYFj.js +2 -2
- package/frontend/user/assets/Welcome-Dtfp6oER.js +1 -1
- package/frontend/user/assets/announcement-35mOnjRL.js +1 -1
- package/frontend/user/assets/index-Da98HOxL.js +3 -3
- package/frontend/user/index.html +5 -5
- package/lib/commands/activate.js +1 -8
- package/lib/commands/restore.js +41 -38
- package/lib/commands/test.js +15 -21
- package/lib/config/claude.js +1 -0
- package/lib/config/codex.js +1 -1
- package/lib/config/upstream.js +3 -0
- package/lib/index.js +77 -35
- package/lib/platforms/openclaw.js +4 -4
- package/lib/platforms/opencode.js +3 -3
- package/lib/services/activation-orchestrator.js +170 -246
- package/lib/services/backup-service.js +65 -13
- package/lib/services/fogact-api.js +40 -26
- package/lib/services/node-service.js +85 -14
- package/package.json +1 -1
|
@@ -92,30 +92,74 @@ function createActivationBackup(service, filePaths, metadata = {}) {
|
|
|
92
92
|
return backupRoot;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function restoreManifestBackup(backupRoot) {
|
|
96
|
+
const manifestPath = path.join(backupRoot, "manifest.json");
|
|
97
|
+
if (!fs.existsSync(manifestPath)) {
|
|
98
|
+
throw new Error("Backup manifest not found");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
102
|
+
const restored = [];
|
|
103
|
+
for (const file of manifest.files || []) {
|
|
104
|
+
const backupPath = path.join(backupRoot, file.backupName);
|
|
105
|
+
if (!fs.existsSync(backupPath)) continue;
|
|
106
|
+
fs.mkdirSync(path.dirname(file.originalPath), { recursive: true });
|
|
107
|
+
if (file.isDirectory) {
|
|
108
|
+
if (fs.existsSync(file.originalPath)) {
|
|
109
|
+
fs.rmSync(file.originalPath, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
copyRecursive(backupPath, file.originalPath);
|
|
112
|
+
} else {
|
|
113
|
+
fs.copyFileSync(backupPath, file.originalPath);
|
|
114
|
+
}
|
|
115
|
+
restored.push(file.originalPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return restored;
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
function listBackups(service = null) {
|
|
96
122
|
ensureBackupDir();
|
|
97
123
|
|
|
98
|
-
const
|
|
124
|
+
const entries = fs.readdirSync(BACKUP_DIR, { withFileTypes: true });
|
|
99
125
|
const backups = [];
|
|
100
126
|
|
|
101
|
-
for (const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const entryPath = path.join(BACKUP_DIR, entry.name);
|
|
129
|
+
|
|
130
|
+
if (entry.isDirectory()) {
|
|
131
|
+
const manifestPath = path.join(entryPath, "manifest.json");
|
|
132
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
133
|
+
try {
|
|
134
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
135
|
+
if (!service || manifest.service === service) {
|
|
136
|
+
backups.push({
|
|
137
|
+
file: entry.name,
|
|
138
|
+
path: entryPath,
|
|
139
|
+
kind: "manifest",
|
|
140
|
+
originalPath: (manifest.files || []).map((file) => file.originalPath).join(", "),
|
|
141
|
+
...manifest,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// Skip invalid backup folders.
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
105
149
|
|
|
150
|
+
if (!entry.name.endsWith(".json")) continue;
|
|
106
151
|
try {
|
|
107
|
-
const
|
|
108
|
-
const backup = JSON.parse(content);
|
|
109
|
-
|
|
152
|
+
const backup = JSON.parse(fs.readFileSync(entryPath, "utf8"));
|
|
110
153
|
if (!service || backup.service === service) {
|
|
111
154
|
backups.push({
|
|
112
|
-
file,
|
|
113
|
-
path:
|
|
155
|
+
file: entry.name,
|
|
156
|
+
path: entryPath,
|
|
157
|
+
kind: "single",
|
|
114
158
|
...backup,
|
|
115
159
|
});
|
|
116
160
|
}
|
|
117
161
|
} catch (err) {
|
|
118
|
-
// Skip invalid backup files
|
|
162
|
+
// Skip invalid backup files.
|
|
119
163
|
}
|
|
120
164
|
}
|
|
121
165
|
|
|
@@ -129,6 +173,10 @@ function restoreBackup(backupPath) {
|
|
|
129
173
|
throw new Error("Backup file not found");
|
|
130
174
|
}
|
|
131
175
|
|
|
176
|
+
if (fs.statSync(backupPath).isDirectory()) {
|
|
177
|
+
return restoreManifestBackup(backupPath);
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
const content = fs.readFileSync(backupPath, "utf8");
|
|
133
181
|
const backup = JSON.parse(content);
|
|
134
182
|
|
|
@@ -139,14 +187,18 @@ function restoreBackup(backupPath) {
|
|
|
139
187
|
|
|
140
188
|
fs.writeFileSync(backup.originalPath, backup.content);
|
|
141
189
|
|
|
142
|
-
return backup.originalPath;
|
|
190
|
+
return [backup.originalPath];
|
|
143
191
|
}
|
|
144
192
|
|
|
145
193
|
function clearBackups(service = null) {
|
|
146
194
|
const backups = listBackups(service);
|
|
147
195
|
|
|
148
196
|
for (const backup of backups) {
|
|
149
|
-
fs.
|
|
197
|
+
if (fs.existsSync(backup.path) && fs.statSync(backup.path).isDirectory()) {
|
|
198
|
+
fs.rmSync(backup.path, { recursive: true, force: true });
|
|
199
|
+
} else if (fs.existsSync(backup.path)) {
|
|
200
|
+
fs.unlinkSync(backup.path);
|
|
201
|
+
}
|
|
150
202
|
}
|
|
151
203
|
|
|
152
204
|
return backups.length;
|
|
@@ -6,7 +6,7 @@ const http = require("http");
|
|
|
6
6
|
|
|
7
7
|
// 支持环境变量覆盖 API 地址;默认连接公网 FogAct 面板。
|
|
8
8
|
const DEFAULT_API_BASE = "https://cliproxy.fogidc.com";
|
|
9
|
-
const API_BASE = process.env.FOGACT_API_BASE ||
|
|
9
|
+
const API_BASE = process.env.FOGACT_API_BASE || DEFAULT_API_BASE;
|
|
10
10
|
|
|
11
11
|
function makeRequest(path, options = {}) {
|
|
12
12
|
return new Promise((resolve, reject) => {
|
|
@@ -57,7 +57,6 @@ function makeRequest(path, options = {}) {
|
|
|
57
57
|
|
|
58
58
|
async function verifyActivationCode(code, service) {
|
|
59
59
|
try {
|
|
60
|
-
// 调用本地激活 API
|
|
61
60
|
const response = await makeRequest("/api/activate", {
|
|
62
61
|
method: "POST",
|
|
63
62
|
body: {
|
|
@@ -67,17 +66,18 @@ async function verifyActivationCode(code, service) {
|
|
|
67
66
|
},
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
const normalized = normalizeActivationResponse(response);
|
|
70
|
+
|
|
71
|
+
if (response.status >= 200 && response.status < 300 && normalized.valid) {
|
|
71
72
|
return {
|
|
72
73
|
valid: true,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
nodes: response.data.data.nodes || [],
|
|
74
|
+
...normalized.data,
|
|
75
|
+
data: normalized.data,
|
|
76
|
+
nodes: normalized.data.nodes || [],
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
return { valid: false, error:
|
|
80
|
+
return { valid: false, error: normalized.message || "Invalid code" };
|
|
81
81
|
} catch (err) {
|
|
82
82
|
return { valid: false, error: err.message };
|
|
83
83
|
}
|
|
@@ -116,45 +116,59 @@ async function inspectActivationCode(code) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
async function redeemActivationCode(code, service) {
|
|
119
|
-
|
|
119
|
+
const result = await inspectActivationCode(code);
|
|
120
|
+
return result.valid
|
|
121
|
+
? { valid: true, service: result.service, expiresAt: result.expiresAt, quota: result.quota, nodes: result.nodes || [] }
|
|
122
|
+
: result;
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
async function getNodes(service) {
|
|
123
126
|
try {
|
|
124
|
-
const response = await makeRequest(`/api/nodes?service=${service}`);
|
|
127
|
+
const response = await makeRequest(`/api/nodes?service=${encodeURIComponent(service || "")}`);
|
|
125
128
|
|
|
126
129
|
if (response.status === 200 && Array.isArray(response.data.nodes)) {
|
|
127
130
|
return response.data.nodes;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
return [
|
|
132
|
-
{ name: "FogAct Local Node", url: "http://localhost:34020", region: "Global" }
|
|
133
|
-
];
|
|
133
|
+
return [{ name: "FogAct", url: API_BASE, region: "Global" }];
|
|
134
134
|
} catch (err) {
|
|
135
|
-
|
|
136
|
-
// 返回默认节点
|
|
137
|
-
return [
|
|
138
|
-
{ name: "FogAct Local Node", url: "http://localhost:34020", region: "Global" }
|
|
139
|
-
];
|
|
135
|
+
return [{ name: "FogAct", url: API_BASE, region: "Global" }];
|
|
140
136
|
}
|
|
141
137
|
}
|
|
142
138
|
|
|
139
|
+
function requestUrl(urlString, options = {}) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const url = new URL(urlString);
|
|
142
|
+
const isHttps = url.protocol === "https:";
|
|
143
|
+
const client = isHttps ? https : http;
|
|
144
|
+
const req = client.request({
|
|
145
|
+
hostname: url.hostname,
|
|
146
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
147
|
+
path: url.pathname + url.search,
|
|
148
|
+
method: options.method || "GET",
|
|
149
|
+
headers: { "User-Agent": `fogact/${packageJson.version}`, ...options.headers },
|
|
150
|
+
timeout: options.timeout || 8000,
|
|
151
|
+
}, (res) => {
|
|
152
|
+
res.resume();
|
|
153
|
+
res.on("end", () => resolve({ status: res.statusCode }));
|
|
154
|
+
});
|
|
155
|
+
req.on("timeout", () => req.destroy(new Error("Request timed out")));
|
|
156
|
+
req.on("error", reject);
|
|
157
|
+
req.end();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
async function testNode(nodeUrl) {
|
|
144
162
|
const start = Date.now();
|
|
145
163
|
|
|
146
164
|
try {
|
|
147
|
-
const
|
|
148
|
-
const response = await
|
|
149
|
-
method: "GET",
|
|
150
|
-
headers: { Host: url.hostname },
|
|
151
|
-
});
|
|
152
|
-
|
|
165
|
+
const healthUrl = new URL("/health", nodeUrl).toString();
|
|
166
|
+
const response = await requestUrl(healthUrl);
|
|
153
167
|
const latency = Date.now() - start;
|
|
154
168
|
|
|
155
169
|
return {
|
|
156
170
|
url: nodeUrl,
|
|
157
|
-
available: response.status
|
|
171
|
+
available: response.status >= 200 && response.status < 500,
|
|
158
172
|
latency,
|
|
159
173
|
};
|
|
160
174
|
} catch (err) {
|
|
@@ -2,43 +2,114 @@
|
|
|
2
2
|
|
|
3
3
|
const { testNode } = require("./fogact-api");
|
|
4
4
|
|
|
5
|
-
async function testNodes(nodes) {
|
|
5
|
+
async function testNodes(nodes, onProgress = null) {
|
|
6
6
|
const results = [];
|
|
7
7
|
|
|
8
8
|
for (const node of nodes) {
|
|
9
|
-
|
|
9
|
+
if (onProgress) onProgress(node, results.length, nodes.length);
|
|
10
|
+
const probes = [];
|
|
11
|
+
for (let index = 0; index < 3; index += 1) {
|
|
12
|
+
const result = await testNode(node.url);
|
|
13
|
+
probes.push(result);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const availableProbes = probes.filter((probe) => probe.available);
|
|
17
|
+
const latencies = availableProbes.map((probe) => probe.latency);
|
|
18
|
+
const avgLatency = latencies.length
|
|
19
|
+
? Math.round(latencies.reduce((sum, value) => sum + value, 0) / latencies.length)
|
|
20
|
+
: -1;
|
|
21
|
+
const latencyStdDev = latencies.length
|
|
22
|
+
? Math.round(Math.sqrt(latencies.reduce((sum, value) => sum + Math.pow(value - avgLatency, 2), 0) / latencies.length))
|
|
23
|
+
: 0;
|
|
24
|
+
const successRate = availableProbes.length / probes.length;
|
|
25
|
+
const available = successRate > 0 && avgLatency >= 0;
|
|
26
|
+
|
|
10
27
|
results.push({
|
|
11
28
|
...node,
|
|
12
|
-
|
|
29
|
+
available,
|
|
30
|
+
reachable: available,
|
|
31
|
+
latency: avgLatency,
|
|
32
|
+
avgLatency,
|
|
33
|
+
latencyStdDev,
|
|
34
|
+
successRate,
|
|
35
|
+
score: scoreNode({ available, avgLatency, latencyStdDev, successRate }),
|
|
36
|
+
error: available ? undefined : probes.find((probe) => probe.error)?.error || "节点不可达",
|
|
13
37
|
});
|
|
14
38
|
}
|
|
15
39
|
|
|
16
40
|
return results;
|
|
17
41
|
}
|
|
18
42
|
|
|
19
|
-
function
|
|
20
|
-
|
|
43
|
+
function scoreNode(result) {
|
|
44
|
+
if (!result.available) return 0;
|
|
45
|
+
const latencyScore = Math.max(0, 100 - Math.round(result.avgLatency / 4));
|
|
46
|
+
const stabilityScore = Math.max(0, 100 - result.latencyStdDev * 2);
|
|
47
|
+
const reliabilityScore = Math.round((result.successRate || 0) * 100);
|
|
48
|
+
return Math.round(latencyScore * 0.7 + stabilityScore * 0.2 + reliabilityScore * 0.1);
|
|
49
|
+
}
|
|
21
50
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
function getResultLatency(result) {
|
|
52
|
+
return typeof result.avgLatency === "number" && result.avgLatency >= 0
|
|
53
|
+
? result.avgLatency
|
|
54
|
+
: result.latency;
|
|
55
|
+
}
|
|
25
56
|
|
|
26
|
-
|
|
57
|
+
function getResultScore(result) {
|
|
58
|
+
return typeof result.score === "number"
|
|
59
|
+
? result.score
|
|
60
|
+
: scoreNode({
|
|
61
|
+
available: result.available,
|
|
62
|
+
avgLatency: getResultLatency(result),
|
|
63
|
+
latencyStdDev: result.latencyStdDev || 0,
|
|
64
|
+
successRate: result.successRate || (result.available ? 1 : 0),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
27
67
|
|
|
68
|
+
function selectBestNode(testResults) {
|
|
69
|
+
const available = testResults.filter((result) => result.available);
|
|
70
|
+
if (available.length === 0) return null;
|
|
71
|
+
available.sort((left, right) => getResultScore(right) - getResultScore(left) || getResultLatency(left) - getResultLatency(right));
|
|
28
72
|
return available[0];
|
|
29
73
|
}
|
|
30
74
|
|
|
75
|
+
function latencyLabel(latency) {
|
|
76
|
+
if (latency <= 50) return `${latency}ms`;
|
|
77
|
+
if (latency <= 100) return `${latency}ms`;
|
|
78
|
+
if (latency <= 300) return `${latency}ms`;
|
|
79
|
+
return `${latency}ms`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stabilityLabel(stdDev) {
|
|
83
|
+
if (stdDev <= 5) return "稳定";
|
|
84
|
+
if (stdDev <= 15) return "良好";
|
|
85
|
+
if (stdDev <= 30) return "一般";
|
|
86
|
+
return "波动";
|
|
87
|
+
}
|
|
88
|
+
|
|
31
89
|
function formatNodeResults(results) {
|
|
90
|
+
const sorted = [...results].sort((left, right) => right.score - left.score || left.avgLatency - right.avgLatency);
|
|
91
|
+
const best = sorted.find((result) => result.available);
|
|
32
92
|
const lines = [];
|
|
33
93
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const name = result.name || result.url;
|
|
94
|
+
lines.push(" 节点测试结果");
|
|
95
|
+
lines.push(" ───────────────────────────────────────────────────");
|
|
96
|
+
lines.push("");
|
|
38
97
|
|
|
39
|
-
|
|
98
|
+
for (const result of sorted) {
|
|
99
|
+
const mark = result.available ? "✓" : "✗";
|
|
100
|
+
const name = String(result.name || result.url).padEnd(10);
|
|
101
|
+
if (!result.available) {
|
|
102
|
+
lines.push(` ${mark} ${name} 不可达`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const bestMark = best && best.url === result.url ? " ★ 最优" : "";
|
|
106
|
+
lines.push(` ${mark} ${name} ${latencyLabel(result.avgLatency)} (±${result.latencyStdDev}ms) ${stabilityLabel(result.latencyStdDev)} ${result.score}分${bestMark}`);
|
|
40
107
|
}
|
|
41
108
|
|
|
109
|
+
const availableCount = results.filter((result) => result.available).length;
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push(" ───────────────────────────────────────────────────");
|
|
112
|
+
lines.push(` 测试完成,共 ${results.length} 个节点,${availableCount} 个可用`);
|
|
42
113
|
return lines.join("\n");
|
|
43
114
|
}
|
|
44
115
|
|