fogact 1.1.10 → 1.2.0
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 +23 -0
- package/frontend/assets/market-ui.css +11 -84
- package/frontend/user/index.html +1 -1
- package/lib/commands/restore.js +41 -38
- package/lib/commands/test.js +15 -21
- package/lib/index.js +20 -18
- package/lib/platforms/openclaw.js +4 -4
- package/lib/platforms/opencode.js +2 -2
- package/lib/services/activation-orchestrator.js +156 -96
- package/lib/services/backup-service.js +65 -13
- package/lib/services/fogact-api.js +28 -17
- package/lib/services/node-service.js +85 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Minimum bootstrap requirement: the machine needs `curl` or `wget`. The installer
|
|
|
51
51
|
2. Choose `1. Activate service`.
|
|
52
52
|
3. Enter the activation / redeem code.
|
|
53
53
|
4. FogAct auto-detects the Codex / Claude entitlement and shows only supported targets.
|
|
54
|
-
5. Confirm the
|
|
54
|
+
5. Confirm activation, let FogAct write the config, then restart the target tool.
|
|
55
55
|
|
|
56
56
|
FogAct backs up existing configuration before writing new files.
|
|
57
57
|
|
package/README.zh-CN.md
CHANGED
package/bin/web-server.js
CHANGED
|
@@ -585,6 +585,29 @@ const server = http.createServer((req, res) => {
|
|
|
585
585
|
return;
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
if (urlPath === "/health" && req.method === "GET") {
|
|
589
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
590
|
+
res.end(JSON.stringify({ success: true, status: 'ok', service: 'fogact' }));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (urlPath === "/api/nodes" && req.method === "GET") {
|
|
595
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
596
|
+
const service = getServiceKey(url.searchParams.get("service") || "codex", "codex");
|
|
597
|
+
const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
|
|
598
|
+
const upstreamUrl = getServiceBaseUrl(upstream, service) || upstream.baseUrl;
|
|
599
|
+
const publicUrl = `https://${req.headers.host}`.replace(/\/+$/, "");
|
|
600
|
+
const nodes = [
|
|
601
|
+
{ name: "FogAct", url: publicUrl, region: "Global" },
|
|
602
|
+
];
|
|
603
|
+
if (upstreamUrl) {
|
|
604
|
+
nodes.push({ name: service === "claude" ? "Claude Upstream" : "Codex Upstream", url: upstreamUrl, region: "Upstream" });
|
|
605
|
+
}
|
|
606
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
607
|
+
res.end(JSON.stringify({ success: true, nodes }));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
588
611
|
// Handle users API
|
|
589
612
|
if (urlPath === "/api/users" && req.method === "GET") {
|
|
590
613
|
if (!isAuthenticated(req)) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
color-scheme: light;
|
|
3
|
-
--primary: #6d5dfc;
|
|
4
3
|
--market-bg: #f7f7fb;
|
|
5
4
|
--market-bg-soft: #ffffff;
|
|
6
5
|
--market-ink: #101014;
|
|
@@ -29,7 +28,6 @@ html.dark,
|
|
|
29
28
|
html.dark:root,
|
|
30
29
|
body.market-dark {
|
|
31
30
|
color-scheme: dark;
|
|
32
|
-
--primary: #9b8cff;
|
|
33
31
|
--market-bg: #07070a;
|
|
34
32
|
--market-bg-soft: #101016;
|
|
35
33
|
--market-ink: #f7f7fb;
|
|
@@ -49,8 +47,7 @@ body.market-dark {
|
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
body.market-page,
|
|
52
|
-
body.market-admin
|
|
53
|
-
body.market-user {
|
|
50
|
+
body.market-admin {
|
|
54
51
|
min-height: 100vh;
|
|
55
52
|
margin: 0;
|
|
56
53
|
color: var(--market-ink);
|
|
@@ -63,8 +60,7 @@ body.market-user {
|
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
body.market-page,
|
|
66
|
-
body.market-admin
|
|
67
|
-
body.market-user {
|
|
63
|
+
body.market-admin {
|
|
68
64
|
background:
|
|
69
65
|
radial-gradient(circle at 18% 10%, rgba(109, 93, 252, 0.18), transparent 30%),
|
|
70
66
|
radial-gradient(circle at 88% 4%, rgba(255, 122, 89, 0.18), transparent 28%),
|
|
@@ -73,8 +69,7 @@ body.market-user {
|
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
body.market-page::before,
|
|
76
|
-
body.market-admin::before
|
|
77
|
-
body.market-user::before {
|
|
72
|
+
body.market-admin::before {
|
|
78
73
|
content: "";
|
|
79
74
|
position: fixed;
|
|
80
75
|
inset: 0;
|
|
@@ -88,8 +83,7 @@ body.market-user::before {
|
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
body.market-page::after,
|
|
91
|
-
body.market-admin::after
|
|
92
|
-
body.market-user::after {
|
|
86
|
+
body.market-admin::after {
|
|
93
87
|
content: "";
|
|
94
88
|
position: fixed;
|
|
95
89
|
inset: 0;
|
|
@@ -814,12 +808,12 @@ body.market-dark .market-auth-card::before {
|
|
|
814
808
|
}
|
|
815
809
|
}
|
|
816
810
|
|
|
817
|
-
.animate-slide-in-up,
|
|
818
|
-
.animate-slide-in-right,
|
|
819
|
-
.animate-fade-in,
|
|
820
|
-
.animate-bounce-in,
|
|
821
|
-
.fade-in,
|
|
822
|
-
.motion-once {
|
|
811
|
+
body.market-page .animate-slide-in-up,
|
|
812
|
+
body.market-page .animate-slide-in-right,
|
|
813
|
+
body.market-page .animate-fade-in,
|
|
814
|
+
body.market-page .animate-bounce-in,
|
|
815
|
+
body.market-page .fade-in,
|
|
816
|
+
body.market-page .motion-once {
|
|
823
817
|
animation: none !important;
|
|
824
818
|
}
|
|
825
819
|
|
|
@@ -1115,74 +1109,7 @@ body.market-dark .market-admin [class*="text-on-error-container"] {
|
|
|
1115
1109
|
color: color-mix(in srgb, var(--market-danger) 76%, #fff 24%) !important;
|
|
1116
1110
|
}
|
|
1117
1111
|
|
|
1118
|
-
/*
|
|
1119
|
-
body.market-user {
|
|
1120
|
-
overflow-x: hidden;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
.market-user #app {
|
|
1124
|
-
position: relative;
|
|
1125
|
-
isolation: isolate;
|
|
1126
|
-
min-height: 100vh;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
.market-user [class~="bg-white"],
|
|
1130
|
-
.market-user [class~="bg-gray-50"],
|
|
1131
|
-
.market-user [class~="bg-slate-50"],
|
|
1132
|
-
.market-user [class~="bg-zinc-50"],
|
|
1133
|
-
.market-user [class~="bg-background"],
|
|
1134
|
-
.market-user [class~="bg-card"],
|
|
1135
|
-
.market-user [class~="bg-muted"],
|
|
1136
|
-
.market-user [class~="dark:bg-gray-950"],
|
|
1137
|
-
.market-user [class~="dark:bg-slate-950"],
|
|
1138
|
-
.market-user [class~="dark:bg-zinc-950"] {
|
|
1139
|
-
background-color: var(--market-panel) !important;
|
|
1140
|
-
backdrop-filter: blur(24px) saturate(135%);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
.market-user [class~="border"],
|
|
1144
|
-
.market-user [class*="border-gray"],
|
|
1145
|
-
.market-user [class*="border-slate"],
|
|
1146
|
-
.market-user [class*="border-zinc"],
|
|
1147
|
-
.market-user [class*="border-border"] {
|
|
1148
|
-
border-color: var(--market-line) !important;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
.market-user [class~="rounded-xl"],
|
|
1152
|
-
.market-user [class~="rounded-2xl"],
|
|
1153
|
-
.market-user [class~="rounded-3xl"] {
|
|
1154
|
-
border-radius: 24px !important;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
.market-user [class*="shadow"] {
|
|
1158
|
-
box-shadow: var(--market-shadow-soft) !important;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
.market-user [class~="text-gray-900"],
|
|
1162
|
-
.market-user [class~="text-slate-900"],
|
|
1163
|
-
.market-user [class~="text-zinc-900"],
|
|
1164
|
-
.market-user [class~="text-foreground"] {
|
|
1165
|
-
color: var(--market-ink) !important;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
.market-user [class~="text-gray-500"],
|
|
1169
|
-
.market-user [class~="text-gray-600"],
|
|
1170
|
-
.market-user [class~="text-slate-500"],
|
|
1171
|
-
.market-user [class~="text-slate-600"],
|
|
1172
|
-
.market-user [class~="text-muted-foreground"] {
|
|
1173
|
-
color: var(--market-muted) !important;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
.market-user button,
|
|
1177
|
-
.market-user a {
|
|
1178
|
-
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
.market-user button:hover,
|
|
1182
|
-
.market-user a:hover {
|
|
1183
|
-
transform: translateY(-1px);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1112
|
+
/* User app keeps its bundled styles; avoid overriding /user/ here. */
|
|
1186
1113
|
.market-card-lab {
|
|
1187
1114
|
min-height: 318px;
|
|
1188
1115
|
padding: 0;
|
package/frontend/user/index.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<script type="module" crossorigin src="assets/index-Da98HOxL.js"></script>
|
|
32
32
|
<link rel="modulepreload" crossorigin href="assets/chart-vendor-CULJE59K.js">
|
|
33
33
|
<link rel="stylesheet" crossorigin href="assets/index-B8QSyYhS.css">
|
|
34
|
-
<link rel="stylesheet" href="/assets/market-ui.css?v=
|
|
34
|
+
<link rel="stylesheet" href="/assets/market-ui.css?v=20260619-user-restore" />
|
|
35
35
|
</head>
|
|
36
36
|
<body class="market-user min-h-screen">
|
|
37
37
|
<span class="market-mouse-light" aria-hidden="true"></span>
|
package/lib/commands/restore.js
CHANGED
|
@@ -3,98 +3,101 @@
|
|
|
3
3
|
const prompts = require("prompts");
|
|
4
4
|
const { listBackups, restoreBackup, clearBackups } = require("../services/backup-service");
|
|
5
5
|
|
|
6
|
+
function formatBackupTitle(backup) {
|
|
7
|
+
const service = backup.service === "codex" ? "Codex" : backup.service === "claude" ? "Claude Code" : backup.service;
|
|
8
|
+
const time = backup.timestamp ? new Date(backup.timestamp).toLocaleString("zh-CN") : "未知时间";
|
|
9
|
+
const count = Array.isArray(backup.files) ? ` · ${backup.files.length} 个文件` : "";
|
|
10
|
+
return `${service || "未知服务"} · ${time}${count}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
async function runRestoreCommand(options = {}) {
|
|
7
14
|
console.log("");
|
|
8
|
-
console.log("
|
|
9
|
-
console.log("");
|
|
15
|
+
console.log(" 恢复备份");
|
|
16
|
+
console.log(" ─────────────────────────────────────");
|
|
10
17
|
|
|
11
|
-
// Step 1: Select service
|
|
12
18
|
let service = options.service;
|
|
13
19
|
if (!service) {
|
|
14
20
|
const response = await prompts({
|
|
15
21
|
type: "select",
|
|
16
22
|
name: "service",
|
|
17
|
-
message: "
|
|
23
|
+
message: "请选择要查看的备份",
|
|
18
24
|
choices: [
|
|
19
25
|
{ title: "Claude Code", value: "claude" },
|
|
20
26
|
{ title: "Codex", value: "codex" },
|
|
21
|
-
{ title: "
|
|
27
|
+
{ title: "全部备份", value: null },
|
|
22
28
|
],
|
|
23
|
-
|
|
29
|
+
initial: 2,
|
|
30
|
+
}, { onCancel: () => false });
|
|
24
31
|
|
|
25
32
|
if (response.service === undefined) {
|
|
26
|
-
console.log("
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log(" 已取消");
|
|
35
|
+
console.log("");
|
|
27
36
|
return;
|
|
28
37
|
}
|
|
29
|
-
|
|
30
38
|
service = response.service;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
// Step 2: List backups
|
|
34
41
|
const backups = listBackups(service);
|
|
35
|
-
|
|
36
42
|
if (backups.length === 0) {
|
|
37
|
-
console.log("
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log(" ℹ 暂无可恢复备份");
|
|
38
45
|
console.log("");
|
|
39
46
|
return;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
console.log(`Found ${backups.length} backup(s):`);
|
|
43
|
-
console.log("");
|
|
44
|
-
|
|
45
|
-
// Step 3: Select backup or clear all
|
|
46
|
-
const choices = backups.map((backup, index) => ({
|
|
47
|
-
title: `${backup.service} - ${new Date(backup.timestamp).toLocaleString()}`,
|
|
48
|
-
value: backup.path,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
choices.push({ title: "Clear all backups", value: "__clear__" });
|
|
52
|
-
|
|
53
49
|
const response = await prompts({
|
|
54
50
|
type: "select",
|
|
55
51
|
name: "backup",
|
|
56
|
-
message: "
|
|
57
|
-
choices
|
|
58
|
-
|
|
52
|
+
message: "请选择要恢复的备份",
|
|
53
|
+
choices: [
|
|
54
|
+
...backups.map((backup) => ({ title: formatBackupTitle(backup), value: backup.path })),
|
|
55
|
+
{ title: "清空当前筛选的备份", value: "__clear__" },
|
|
56
|
+
],
|
|
57
|
+
}, { onCancel: () => false });
|
|
59
58
|
|
|
60
59
|
if (!response.backup) {
|
|
61
|
-
console.log("
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(" 已取消");
|
|
62
|
+
console.log("");
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// Step 4: Handle clear all
|
|
66
66
|
if (response.backup === "__clear__") {
|
|
67
67
|
const confirm = await prompts({
|
|
68
68
|
type: "confirm",
|
|
69
69
|
name: "value",
|
|
70
|
-
message: "
|
|
70
|
+
message: "确认清空这些备份?",
|
|
71
71
|
initial: false,
|
|
72
|
-
});
|
|
72
|
+
}, { onCancel: () => false });
|
|
73
73
|
|
|
74
74
|
if (!confirm.value) {
|
|
75
|
-
console.log("
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(" 已取消");
|
|
77
|
+
console.log("");
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const count = clearBackups(service);
|
|
80
82
|
console.log("");
|
|
81
|
-
console.log(
|
|
83
|
+
console.log(` ✓ 已清理 ${count} 个备份`);
|
|
82
84
|
console.log("");
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
// Step 5: Restore backup
|
|
87
88
|
console.log("");
|
|
88
|
-
console.log("
|
|
89
|
-
|
|
89
|
+
console.log(" 正在恢复备份...");
|
|
90
90
|
try {
|
|
91
|
-
const
|
|
92
|
-
console.log(
|
|
91
|
+
const restoredPaths = restoreBackup(response.backup);
|
|
92
|
+
console.log(" ✓ 备份已恢复");
|
|
93
|
+
for (const restoredPath of restoredPaths) {
|
|
94
|
+
console.log(` ${restoredPath}`);
|
|
95
|
+
}
|
|
93
96
|
console.log("");
|
|
94
|
-
console.log("
|
|
97
|
+
console.log(" 请重启相关工具以应用恢复后的配置");
|
|
95
98
|
console.log("");
|
|
96
99
|
} catch (err) {
|
|
97
|
-
console.log(
|
|
100
|
+
console.log(` ✗ 恢复失败: ${err.message}`);
|
|
98
101
|
console.log("");
|
|
99
102
|
}
|
|
100
103
|
}
|
package/lib/commands/test.js
CHANGED
|
@@ -5,33 +5,27 @@ const { testNodes, formatNodeResults } = require("../services/node-service");
|
|
|
5
5
|
|
|
6
6
|
async function runTestCommand() {
|
|
7
7
|
console.log("");
|
|
8
|
-
console.log("
|
|
9
|
-
console.log("");
|
|
8
|
+
console.log(" 测试节点");
|
|
9
|
+
console.log(" ─────────────────────────────────────");
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const services = [
|
|
12
|
+
{ key: "claude", label: "Claude Code" },
|
|
13
|
+
{ key: "codex", label: "Codex" },
|
|
14
|
+
];
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
const claudeResults = await testNodes(claudeNodes);
|
|
16
|
+
for (const service of services) {
|
|
17
17
|
console.log("");
|
|
18
|
-
console.log(
|
|
19
|
-
|
|
20
|
-
console.log(" No Claude nodes available");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
console.log("");
|
|
18
|
+
console.log(` 正在测试 ${service.label} 节点...`);
|
|
19
|
+
const nodes = await getNodes(service.key);
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
if (nodes.length === 0) {
|
|
22
|
+
console.log(" ℹ 暂无可用节点");
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
const codexResults = await testNodes(codexNodes);
|
|
26
|
+
const results = await testNodes(nodes);
|
|
31
27
|
console.log("");
|
|
32
|
-
console.log(formatNodeResults(
|
|
33
|
-
} else {
|
|
34
|
-
console.log(" No Codex nodes available");
|
|
28
|
+
console.log(formatNodeResults(results));
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
console.log("");
|
package/lib/index.js
CHANGED
|
@@ -171,7 +171,7 @@ function printBanner() {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
async function runInteractiveMenu() {
|
|
174
|
-
await
|
|
174
|
+
await runToolsMenu();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
function selectMenuAction() {
|
|
@@ -244,23 +244,25 @@ function selectMenuAction() {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
async function runToolsMenu() {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
247
|
+
for (;;) {
|
|
248
|
+
const action = await selectMenuAction();
|
|
249
|
+
|
|
250
|
+
switch (action) {
|
|
251
|
+
case "activate":
|
|
252
|
+
await runActivationWizard();
|
|
253
|
+
break;
|
|
254
|
+
case "test":
|
|
255
|
+
await runTestCommand();
|
|
256
|
+
break;
|
|
257
|
+
case "restore":
|
|
258
|
+
await runRestoreCommand();
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
console.log("");
|
|
262
|
+
console.log(" 再见!");
|
|
263
|
+
console.log("");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
|
|
@@ -22,7 +22,7 @@ function buildClaudeConfig(existingConfig, baseUrl, apiKey) {
|
|
|
22
22
|
models: {
|
|
23
23
|
mode: "merge",
|
|
24
24
|
providers: {
|
|
25
|
-
"
|
|
25
|
+
"fogact-claude": {
|
|
26
26
|
baseUrl,
|
|
27
27
|
apiKey,
|
|
28
28
|
auth: "api-key",
|
|
@@ -37,7 +37,7 @@ function buildClaudeConfig(existingConfig, baseUrl, apiKey) {
|
|
|
37
37
|
...agentRest,
|
|
38
38
|
defaults: {
|
|
39
39
|
model: {
|
|
40
|
-
primary: "
|
|
40
|
+
primary: "fogact-claude/claude-opus-4-6",
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
},
|
|
@@ -52,7 +52,7 @@ function buildCodexConfig(existingConfig, baseUrl, apiKey) {
|
|
|
52
52
|
models: {
|
|
53
53
|
mode: "merge",
|
|
54
54
|
providers: {
|
|
55
|
-
"
|
|
55
|
+
"fogact-codex": {
|
|
56
56
|
baseUrl,
|
|
57
57
|
apiKey,
|
|
58
58
|
auth: "api-key",
|
|
@@ -77,7 +77,7 @@ function buildCodexConfig(existingConfig, baseUrl, apiKey) {
|
|
|
77
77
|
...agentRest,
|
|
78
78
|
defaults: {
|
|
79
79
|
model: {
|
|
80
|
-
primary: "
|
|
80
|
+
primary: "fogact-codex/gpt-5.2",
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
83
|
},
|
|
@@ -18,7 +18,7 @@ function buildClaudeConfig(existingConfig, baseUrl, apiKey) {
|
|
|
18
18
|
const { model, small_model, ...rest } = existingConfig || {};
|
|
19
19
|
const provider = rest.provider || {};
|
|
20
20
|
provider.anthropic = {
|
|
21
|
-
name: "
|
|
21
|
+
name: "fogact-claude",
|
|
22
22
|
npm: "@ai-sdk/anthropic",
|
|
23
23
|
options: {
|
|
24
24
|
baseURL: `${baseUrl.replace(/\/+$/, "")}/v1`,
|
|
@@ -38,7 +38,7 @@ function buildCodexConfig(existingConfig, baseUrl, apiKey) {
|
|
|
38
38
|
const { model, small_model, ...rest } = existingConfig || {};
|
|
39
39
|
const provider = rest.provider || {};
|
|
40
40
|
provider.openai = {
|
|
41
|
-
name: "
|
|
41
|
+
name: "fogact-codex",
|
|
42
42
|
npm: "@ai-sdk/openai",
|
|
43
43
|
api: "responses",
|
|
44
44
|
options: {
|
|
@@ -4,7 +4,7 @@ const prompts = require("prompts");
|
|
|
4
4
|
const { detectPlatforms, getPlatforms } = require("../platforms");
|
|
5
5
|
const { loadUpstreamConfig } = require("../config/upstream");
|
|
6
6
|
const { createActivationBackup } = require("./backup-service");
|
|
7
|
-
const { inspectActivationCode, redeemActivationCode } = require("./fogact-api");
|
|
7
|
+
const { getNodes, inspectActivationCode, redeemActivationCode, testNode } = require("./fogact-api");
|
|
8
8
|
const { maskKey, verifyNewApiKey } = require("./newapi");
|
|
9
9
|
|
|
10
10
|
const SUPPORTED_SERVICES = ["codex", "claude"];
|
|
@@ -219,21 +219,17 @@ async function promptService(defaultService, entitlement = normalizeEntitlement(
|
|
|
219
219
|
if (!isServiceAllowed(entitlement, normalized)) {
|
|
220
220
|
throw new Error(`当前激活码不支持 ${getServiceLabel(normalized)}`);
|
|
221
221
|
}
|
|
222
|
-
console.log(`能力范围: ${getServiceLabel(normalized)}`);
|
|
223
222
|
return normalized;
|
|
224
223
|
}
|
|
225
224
|
|
|
226
225
|
const allowedServices = entitlement.services.length ? entitlement.services : [];
|
|
227
226
|
if (allowedServices.length === 1) {
|
|
228
|
-
console.log(`能力范围: ${getServiceLabel(allowedServices[0])}`);
|
|
229
227
|
return allowedServices[0];
|
|
230
228
|
}
|
|
231
229
|
|
|
232
230
|
if (!allowPrompt) {
|
|
233
231
|
if (allowedServices.length > 1) {
|
|
234
232
|
const service = allowedServices[0];
|
|
235
|
-
const labels = allowedServices.map(getServiceLabel).join(" / ");
|
|
236
|
-
console.log(`能力范围: ${labels},本次自动激活 ${getServiceLabel(service)}`);
|
|
237
233
|
return service;
|
|
238
234
|
}
|
|
239
235
|
console.log("✗ 激活码没有返回 Codex / Claude 能力,无法自动识别。请联系管理员重新生成激活码。");
|
|
@@ -289,10 +285,10 @@ async function promptActivationCode(defaultCode) {
|
|
|
289
285
|
}
|
|
290
286
|
|
|
291
287
|
const response = await prompts({
|
|
292
|
-
type: "
|
|
288
|
+
type: "password",
|
|
293
289
|
name: "code",
|
|
294
|
-
message: "
|
|
295
|
-
validate: (value) => value && value.trim() ? true : "激活码不能为空",
|
|
290
|
+
message: "请输入 API Key / 激活码:",
|
|
291
|
+
validate: (value) => value && value.trim() ? true : "API Key / 激活码不能为空",
|
|
296
292
|
}, { onCancel: () => false });
|
|
297
293
|
|
|
298
294
|
return response.code ? response.code.trim() : null;
|
|
@@ -321,7 +317,7 @@ async function promptCredentialType(options, upstream) {
|
|
|
321
317
|
return response.credentialType || null;
|
|
322
318
|
}
|
|
323
319
|
|
|
324
|
-
async function confirmActivation(yes) {
|
|
320
|
+
async function confirmActivation(yes, service) {
|
|
325
321
|
if (yes) {
|
|
326
322
|
return true;
|
|
327
323
|
}
|
|
@@ -329,7 +325,7 @@ async function confirmActivation(yes) {
|
|
|
329
325
|
const response = await prompts({
|
|
330
326
|
type: "confirm",
|
|
331
327
|
name: "confirmed",
|
|
332
|
-
message:
|
|
328
|
+
message: `确认激活 ${getServiceLabel(service)} 配置?`,
|
|
333
329
|
initial: true,
|
|
334
330
|
}, { onCancel: () => false });
|
|
335
331
|
|
|
@@ -353,6 +349,10 @@ function getBackupPaths(targets) {
|
|
|
353
349
|
return targets.flatMap(({ detection }) => detection.paths || []);
|
|
354
350
|
}
|
|
355
351
|
|
|
352
|
+
function divider(width = 37) {
|
|
353
|
+
return ` ${"─".repeat(width)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
356
|
function printBanner() {
|
|
357
357
|
console.log("");
|
|
358
358
|
console.log("╭────────────────────────────────────────╮");
|
|
@@ -362,65 +362,112 @@ function printBanner() {
|
|
|
362
362
|
console.log("");
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
function
|
|
366
|
-
console.log("检测结果:");
|
|
367
|
-
console.log(` 当前能力: ${getServiceLabel(service)}`);
|
|
368
|
-
for (const { platform, detection } of detectedPlatforms) {
|
|
369
|
-
const mark = canSelectPlatform(platform, detection) ? "✓" : "-";
|
|
370
|
-
console.log(` ${mark} ${platform.name}:${getStatusLabel(platform, detection)}`);
|
|
371
|
-
}
|
|
372
|
-
for (const { platform } of blockedPlatforms) {
|
|
373
|
-
console.log(` - ${platform.name}:当前激活码能力不包含`);
|
|
374
|
-
}
|
|
365
|
+
function printCredentialProfile(service, upstream, apiKey, entitlement) {
|
|
375
366
|
console.log("");
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
console.log(
|
|
380
|
-
console.log(`
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
console.log(`
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
367
|
+
console.log(" 账号信息");
|
|
368
|
+
console.log(divider());
|
|
369
|
+
console.log(` 服务类型: ${getServiceLabel(service)}`);
|
|
370
|
+
console.log(` 接入地址: ${upstream.baseUrl}`);
|
|
371
|
+
console.log(` API Key: ${maskKey(apiKey)}`);
|
|
372
|
+
if (entitlement.planName) {
|
|
373
|
+
console.log(` 套餐名称: ${entitlement.planName}`);
|
|
374
|
+
}
|
|
375
|
+
if (entitlement.raw && entitlement.raw.expiresAt) {
|
|
376
|
+
console.log(` 到期时间: ${entitlement.raw.expiresAt}`);
|
|
377
|
+
}
|
|
378
|
+
const quota = entitlement.raw && entitlement.raw.quota;
|
|
379
|
+
if (quota && typeof quota === "object") {
|
|
380
|
+
const total = quota.total ?? quota.total_quota ?? quota.dailyLimit ?? quota.daily;
|
|
381
|
+
const used = quota.used ?? quota.used_quota ?? quota.dailyUsed;
|
|
382
|
+
if (total !== undefined) console.log(` 总配额: ${total}`);
|
|
383
|
+
if (used !== undefined) console.log(` 已使用: ${used}`);
|
|
389
384
|
}
|
|
385
|
+
console.log("");
|
|
390
386
|
}
|
|
391
387
|
|
|
392
|
-
function printResultSummary(service,
|
|
388
|
+
function printResultSummary(service, backupPath, results, redeemResult) {
|
|
393
389
|
const succeeded = results.filter(({ result }) => result.success);
|
|
394
390
|
const skipped = results.filter(({ result }) => !result.success && result.skipped);
|
|
395
391
|
const failed = results.filter(({ result }) => !result.success && !result.skipped);
|
|
392
|
+
const byId = new Map(results.map((entry) => [entry.platform.id, entry]));
|
|
396
393
|
|
|
397
394
|
console.log("");
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
395
|
+
if (backupPath) {
|
|
396
|
+
console.log(" ✓ 备份已创建");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const printConfigured = (entry, label) => {
|
|
400
|
+
if (!entry) return false;
|
|
401
|
+
if (entry.result.success) {
|
|
402
|
+
console.log(` ✓ ${label} 已激活`);
|
|
403
|
+
const files = entry.result.files || [];
|
|
404
|
+
if (files.length) console.log(` 配置: ${files.join(", ")}`);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
if (!entry.result.skipped) {
|
|
408
|
+
console.log(` ✗ ${label} 激活失败: ${entry.result.error || entry.result.message || "未知错误"}`);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
if (service === "codex") {
|
|
415
|
+
printConfigured(byId.get("codex-cli"), "Codex CLI");
|
|
416
|
+
} else {
|
|
417
|
+
printConfigured(byId.get("claude-code"), "Claude Code");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const opencode = byId.get("opencode");
|
|
421
|
+
if (!printConfigured(opencode, "OpenCode")) {
|
|
422
|
+
console.log("");
|
|
423
|
+
console.log(" ℹ 已跳过 OpenCode 配置(未检测到安装)");
|
|
424
|
+
console.log(" 如需使用,请先运行一次 opencode 初始化后重新激活");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const openclaw = byId.get("openclaw");
|
|
428
|
+
if (!printConfigured(openclaw, "OpenClaw")) {
|
|
429
|
+
console.log("");
|
|
430
|
+
console.log(" ℹ 已跳过 OpenClaw 配置(未检测到安装)");
|
|
431
|
+
console.log(" 如需使用,请先运行一次 openclaw 初始化后重新激活");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const extensionResults = [byId.get("vscode-codex-plugin"), byId.get("cursor-codex-plugin")]
|
|
435
|
+
.filter(Boolean)
|
|
436
|
+
.filter((entry) => entry.result.success || !entry.result.skipped);
|
|
437
|
+
if (extensionResults.length) {
|
|
438
|
+
for (const entry of extensionResults) {
|
|
439
|
+
if (entry.result.success) {
|
|
440
|
+
console.log("");
|
|
441
|
+
console.log(` ✓ ${entry.platform.name} 已激活`);
|
|
442
|
+
for (const file of entry.result.files || []) console.log(` 目录: ${file}`);
|
|
443
|
+
} else {
|
|
444
|
+
console.log("");
|
|
445
|
+
console.log(` ⚠ ${entry.platform.name}: ${entry.result.error || entry.result.message || "无法激活"}`);
|
|
404
446
|
}
|
|
405
|
-
} else if (result.skipped) {
|
|
406
|
-
console.log(` - ${platform.name}: ${result.message || "已跳过"}`);
|
|
407
|
-
} else {
|
|
408
|
-
console.log(` ✗ ${platform.name}: ${result.error || result.message || "失败"}`);
|
|
409
447
|
}
|
|
448
|
+
} else if (service === "codex") {
|
|
449
|
+
console.log("");
|
|
450
|
+
console.log(" ℹ 已跳过编辑器插件配置(未检测到 Codex 插件)");
|
|
410
451
|
}
|
|
411
452
|
|
|
412
453
|
console.log("");
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
console.log(` 成功: ${succeeded.length}`);
|
|
417
|
-
console.log(` 跳过: ${skipped.length}`);
|
|
418
|
-
console.log(` 失败: ${failed.length}`);
|
|
419
|
-
console.log(` 备份: ${backupPath || "无旧配置需要备份"}`);
|
|
454
|
+
if (failed.length) {
|
|
455
|
+
console.log(` 激活完成:${succeeded.length} 成功,${failed.length} 失败,${skipped.length} 跳过`);
|
|
456
|
+
}
|
|
420
457
|
if (redeemResult) {
|
|
421
|
-
console.log(`
|
|
458
|
+
console.log(` 兑换记录: ${redeemResult.valid ? "已完成" : `未完成(${redeemResult.error || "接口不可用"})`}`);
|
|
459
|
+
}
|
|
460
|
+
if (service === "claude") {
|
|
461
|
+
const tools = ["Claude Code"];
|
|
462
|
+
if (byId.get("opencode")?.result.success) tools.push("OpenCode");
|
|
463
|
+
if (byId.get("openclaw")?.result.success) tools.push("OpenClaw");
|
|
464
|
+
console.log(` 请重启相关工具(${tools.join("/")})以应用新配置`);
|
|
465
|
+
} else {
|
|
466
|
+
const tools = ["Codex", "VSCode", "Cursor"];
|
|
467
|
+
if (byId.get("opencode")?.result.success) tools.push("OpenCode");
|
|
468
|
+
if (byId.get("openclaw")?.result.success) tools.push("OpenClaw");
|
|
469
|
+
console.log(` 请重启相关工具(${tools.join("/")})以应用新配置`);
|
|
422
470
|
}
|
|
423
|
-
console.log(" 提示: 重启相关工具后生效");
|
|
424
471
|
console.log("");
|
|
425
472
|
}
|
|
426
473
|
|
|
@@ -431,6 +478,28 @@ async function selectPlatforms(detectedPlatforms, options = {}) {
|
|
|
431
478
|
return getActivationTargets(detectedPlatforms, Boolean(options.all));
|
|
432
479
|
}
|
|
433
480
|
|
|
481
|
+
async function verifyPrimaryNode() {
|
|
482
|
+
const nodes = await getNodes("codex");
|
|
483
|
+
const primary = nodes[0];
|
|
484
|
+
if (!primary) return null;
|
|
485
|
+
|
|
486
|
+
console.log("");
|
|
487
|
+
console.log(" 正在验证节点...");
|
|
488
|
+
const result = await testNode(primary.url);
|
|
489
|
+
if (result.available) {
|
|
490
|
+
console.log(` ✓ ${primary.name || "FogAct"} 已连接`);
|
|
491
|
+
console.log(` 延迟: ${result.latency}ms`);
|
|
492
|
+
console.log(` 地址: ${primary.url}`);
|
|
493
|
+
console.log("");
|
|
494
|
+
return { node: primary, latency: result.latency };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.log(` ✗ ${primary.name || "FogAct"} 连接失败`);
|
|
498
|
+
console.log(` 地址: ${primary.url}`);
|
|
499
|
+
console.log("");
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
434
503
|
async function resolveCodeCredential(options, upstream) {
|
|
435
504
|
const code = await promptActivationCode(options.code);
|
|
436
505
|
if (!code) {
|
|
@@ -438,7 +507,7 @@ async function resolveCodeCredential(options, upstream) {
|
|
|
438
507
|
}
|
|
439
508
|
|
|
440
509
|
console.log("");
|
|
441
|
-
console.log("
|
|
510
|
+
console.log("正在验证激活码...");
|
|
442
511
|
const inspection = await inspectActivationCode(code);
|
|
443
512
|
if (!inspection.valid) {
|
|
444
513
|
console.log(`✗ 无法读取激活码能力: ${inspection.error || "接口未返回有效信息"}`);
|
|
@@ -470,47 +539,29 @@ async function resolveApiKeyCredential(options, upstream) {
|
|
|
470
539
|
}
|
|
471
540
|
|
|
472
541
|
async function activateTargets({ service, upstream, apiKey, targets, activationCode, options = {} }) {
|
|
473
|
-
console.log("");
|
|
474
|
-
console.log("正在创建备份...");
|
|
475
542
|
const backupPath = createActivationBackup(service, getBackupPaths(targets), {
|
|
476
543
|
upstream: upstream.baseUrl,
|
|
477
544
|
targets: targets.map(({ platform }) => platform.id),
|
|
478
545
|
});
|
|
479
|
-
if (backupPath) {
|
|
480
|
-
console.log(`✓ 备份完成: ${backupPath}`);
|
|
481
|
-
} else {
|
|
482
|
-
console.log("ℹ 没有旧配置需要备份");
|
|
483
|
-
}
|
|
484
546
|
|
|
485
|
-
console.log("");
|
|
486
|
-
console.log("正在激活平台...");
|
|
487
547
|
const results = [];
|
|
488
548
|
for (const { platform, detection } of targets) {
|
|
549
|
+
if (!canSelectPlatform(platform, detection)) {
|
|
550
|
+
results.push({ platform, result: { success: false, skipped: true, message: "未安装" } });
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
489
553
|
try {
|
|
490
|
-
const result = platform.activate({ service, upstream, apiKey, detection });
|
|
554
|
+
const result = platform.activate({ service, upstream, apiKey, options, detection });
|
|
491
555
|
results.push({ platform, result });
|
|
492
|
-
if (result.success) {
|
|
493
|
-
console.log(`✓ ${platform.name}`);
|
|
494
|
-
} else {
|
|
495
|
-
console.log(`⚠ ${platform.name}: ${result.message || "已跳过"}`);
|
|
496
|
-
}
|
|
497
556
|
} catch (err) {
|
|
498
557
|
results.push({ platform, result: { success: false, error: err.message } });
|
|
499
|
-
console.log(`✗ ${platform.name}: ${err.message}`);
|
|
500
558
|
}
|
|
501
559
|
}
|
|
502
560
|
|
|
503
561
|
let redeemResult = null;
|
|
504
562
|
const failures = results.filter(({ result }) => !result.success && !result.skipped);
|
|
505
563
|
if (activationCode && failures.length === 0 && !options.noRedeem) {
|
|
506
|
-
console.log("");
|
|
507
|
-
console.log("正在完成兑换记录...");
|
|
508
564
|
redeemResult = await redeemActivationCode(activationCode, service);
|
|
509
|
-
if (redeemResult.valid) {
|
|
510
|
-
console.log("✓ 兑换记录已完成");
|
|
511
|
-
} else {
|
|
512
|
-
console.log(`⚠ 兑换记录未完成: ${redeemResult.error || "接口不可用"}`);
|
|
513
|
-
}
|
|
514
565
|
}
|
|
515
566
|
|
|
516
567
|
return { backupPath, results, redeemResult };
|
|
@@ -563,19 +614,22 @@ async function runNewApiActivation(options = {}) {
|
|
|
563
614
|
const detectedPlatforms = detectPlatforms(service);
|
|
564
615
|
const selectedPlatformIds = options.platforms ? parsePlatformIds(options.platforms) : null;
|
|
565
616
|
const targets = getActivationTargets(detectedPlatforms, Boolean(options.all), selectedPlatformIds);
|
|
566
|
-
const skipped = detectedPlatforms.filter((entry) => !targets.includes(entry));
|
|
567
617
|
|
|
568
|
-
|
|
569
|
-
printPlan(service, upstream, apiKey, targets, skipped);
|
|
618
|
+
printCredentialProfile(service, upstream, apiKey, entitlement);
|
|
570
619
|
|
|
571
|
-
if (!(await confirmActivation(Boolean(options.yes || options.auto)))) {
|
|
572
|
-
console.log("
|
|
620
|
+
if (!(await confirmActivation(Boolean(options.yes || options.auto), service))) {
|
|
621
|
+
console.log("");
|
|
622
|
+
console.log(" 已取消");
|
|
623
|
+
console.log("");
|
|
573
624
|
return { success: false, cancelled: true };
|
|
574
625
|
}
|
|
575
626
|
|
|
627
|
+
console.log("");
|
|
628
|
+
console.log(" 正在写入配置...");
|
|
576
629
|
const activation = await activateTargets({ service, upstream, apiKey, targets, options });
|
|
577
630
|
const failures = activation.results.filter(({ result }) => !result.success && !result.skipped);
|
|
578
|
-
|
|
631
|
+
console.log(" 配置完成");
|
|
632
|
+
printResultSummary(service, activation.backupPath, activation.results, activation.redeemResult);
|
|
579
633
|
|
|
580
634
|
return {
|
|
581
635
|
success: failures.length === 0,
|
|
@@ -585,7 +639,13 @@ async function runNewApiActivation(options = {}) {
|
|
|
585
639
|
}
|
|
586
640
|
|
|
587
641
|
async function runActivationWizard(options = {}) {
|
|
588
|
-
|
|
642
|
+
if (!options.noNodeCheck) {
|
|
643
|
+
const node = await verifyPrimaryNode();
|
|
644
|
+
if (!node) {
|
|
645
|
+
return { success: false, cancelled: true };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
589
649
|
const baseUpstream = loadUpstreamConfig({ configPath: options.upstreamConfig });
|
|
590
650
|
const credentialType = !options.code && options.apiKey ? "api-key" : "code";
|
|
591
651
|
|
|
@@ -616,30 +676,29 @@ async function runActivationWizard(options = {}) {
|
|
|
616
676
|
if (!verification.valid) {
|
|
617
677
|
return { success: false, verification };
|
|
618
678
|
}
|
|
619
|
-
} else {
|
|
620
|
-
console.log("✓ 已按激活码能力限制可选平台");
|
|
621
679
|
}
|
|
622
680
|
|
|
623
681
|
const allDetectedPlatforms = detectPlatforms(service);
|
|
624
682
|
const allowedPlatforms = allDetectedPlatforms.filter((entry) => isPlatformAllowed(entry, credential.entitlement, service));
|
|
625
|
-
const blockedPlatforms = allDetectedPlatforms.filter((entry) => !allowedPlatforms.includes(entry));
|
|
626
|
-
|
|
627
|
-
console.log("");
|
|
628
|
-
printDetection(service, allowedPlatforms, blockedPlatforms);
|
|
629
683
|
|
|
630
684
|
const targets = await selectPlatforms(allowedPlatforms, options);
|
|
631
685
|
if (targets.length === 0) {
|
|
632
|
-
console.log("
|
|
686
|
+
console.log("");
|
|
687
|
+
console.log(" ✗ 当前环境没有可激活目标");
|
|
688
|
+
console.log("");
|
|
633
689
|
return { success: false, cancelled: true };
|
|
634
690
|
}
|
|
635
|
-
const skipped = allowedPlatforms.filter((entry) => !targets.includes(entry)).concat(blockedPlatforms);
|
|
636
691
|
|
|
637
|
-
|
|
638
|
-
if (!(await confirmActivation(Boolean(options.yes || options.auto)))) {
|
|
639
|
-
console.log("
|
|
692
|
+
printCredentialProfile(service, upstream, credential.apiKey, credential.entitlement);
|
|
693
|
+
if (!(await confirmActivation(Boolean(options.yes || options.auto), service))) {
|
|
694
|
+
console.log("");
|
|
695
|
+
console.log(" 已取消");
|
|
696
|
+
console.log("");
|
|
640
697
|
return { success: false, cancelled: true };
|
|
641
698
|
}
|
|
642
699
|
|
|
700
|
+
console.log("");
|
|
701
|
+
console.log(" 正在写入配置...");
|
|
643
702
|
const activation = await activateTargets({
|
|
644
703
|
service,
|
|
645
704
|
upstream,
|
|
@@ -649,7 +708,8 @@ async function runActivationWizard(options = {}) {
|
|
|
649
708
|
options,
|
|
650
709
|
});
|
|
651
710
|
const failures = activation.results.filter(({ result }) => !result.success && !result.skipped);
|
|
652
|
-
|
|
711
|
+
console.log(" 配置完成");
|
|
712
|
+
printResultSummary(service, activation.backupPath, activation.results, activation.redeemResult);
|
|
653
713
|
|
|
654
714
|
return {
|
|
655
715
|
success: failures.length === 0,
|
|
@@ -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;
|
|
@@ -121,40 +121,51 @@ async function redeemActivationCode(code, service) {
|
|
|
121
121
|
|
|
122
122
|
async function getNodes(service) {
|
|
123
123
|
try {
|
|
124
|
-
const response = await makeRequest(`/api/nodes?service=${service}`);
|
|
124
|
+
const response = await makeRequest(`/api/nodes?service=${encodeURIComponent(service || "")}`);
|
|
125
125
|
|
|
126
126
|
if (response.status === 200 && Array.isArray(response.data.nodes)) {
|
|
127
127
|
return response.data.nodes;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
return [
|
|
132
|
-
{ name: "FogAct Local Node", url: "http://localhost:34020", region: "Global" }
|
|
133
|
-
];
|
|
130
|
+
return [{ name: "FogAct", url: API_BASE, region: "Global" }];
|
|
134
131
|
} catch (err) {
|
|
135
|
-
|
|
136
|
-
// 返回默认节点
|
|
137
|
-
return [
|
|
138
|
-
{ name: "FogAct Local Node", url: "http://localhost:34020", region: "Global" }
|
|
139
|
-
];
|
|
132
|
+
return [{ name: "FogAct", url: API_BASE, region: "Global" }];
|
|
140
133
|
}
|
|
141
134
|
}
|
|
142
135
|
|
|
136
|
+
function requestUrl(urlString, options = {}) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const url = new URL(urlString);
|
|
139
|
+
const isHttps = url.protocol === "https:";
|
|
140
|
+
const client = isHttps ? https : http;
|
|
141
|
+
const req = client.request({
|
|
142
|
+
hostname: url.hostname,
|
|
143
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
144
|
+
path: url.pathname + url.search,
|
|
145
|
+
method: options.method || "GET",
|
|
146
|
+
headers: { "User-Agent": `fogact/${packageJson.version}`, ...options.headers },
|
|
147
|
+
timeout: options.timeout || 8000,
|
|
148
|
+
}, (res) => {
|
|
149
|
+
res.resume();
|
|
150
|
+
res.on("end", () => resolve({ status: res.statusCode }));
|
|
151
|
+
});
|
|
152
|
+
req.on("timeout", () => req.destroy(new Error("Request timed out")));
|
|
153
|
+
req.on("error", reject);
|
|
154
|
+
req.end();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
143
158
|
async function testNode(nodeUrl) {
|
|
144
159
|
const start = Date.now();
|
|
145
160
|
|
|
146
161
|
try {
|
|
147
|
-
const
|
|
148
|
-
const response = await
|
|
149
|
-
method: "GET",
|
|
150
|
-
headers: { Host: url.hostname },
|
|
151
|
-
});
|
|
152
|
-
|
|
162
|
+
const healthUrl = new URL("/health", nodeUrl).toString();
|
|
163
|
+
const response = await requestUrl(healthUrl);
|
|
153
164
|
const latency = Date.now() - start;
|
|
154
165
|
|
|
155
166
|
return {
|
|
156
167
|
url: nodeUrl,
|
|
157
|
-
available: response.status
|
|
168
|
+
available: response.status >= 200 && response.status < 500,
|
|
158
169
|
latency,
|
|
159
170
|
};
|
|
160
171
|
} 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
|
|