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 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 plan and restart the target tool.
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
@@ -51,7 +51,7 @@ fogact
51
51
  2. 选择 `1. 激活服务`。
52
52
  3. 输入激活码 / 兑换码。
53
53
  4. FogAct 自动识别 Codex / Claude 能力,并只展示可激活的平台。
54
- 5. 确认激活计划,然后重启对应工具。
54
+ 5. 确认激活后自动写入配置,然后重启对应工具。
55
55
 
56
56
  FogAct 写入新配置前会自动备份旧配置。
57
57
 
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
- /* Built user app polish */
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;
@@ -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=20260616-upstream2" />
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>
@@ -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("=== Restore Backup ===");
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: "Select service",
23
+ message: "请选择要查看的备份",
18
24
  choices: [
19
25
  { title: "Claude Code", value: "claude" },
20
26
  { title: "Codex", value: "codex" },
21
- { title: "All services", value: null },
27
+ { title: "全部备份", value: null },
22
28
  ],
23
- });
29
+ initial: 2,
30
+ }, { onCancel: () => false });
24
31
 
25
32
  if (response.service === undefined) {
26
- console.log("Restore cancelled.");
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("No backups found.");
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: "Select backup to restore",
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("Restore cancelled.");
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: "Are you sure you want to clear all backups?",
70
+ message: "确认清空这些备份?",
71
71
  initial: false,
72
- });
72
+ }, { onCancel: () => false });
73
73
 
74
74
  if (!confirm.value) {
75
- console.log("Clear cancelled.");
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(`✓ Cleared ${count} backup(s)`);
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("Restoring backup...");
89
-
89
+ console.log(" 正在恢复备份...");
90
90
  try {
91
- const restoredPath = restoreBackup(response.backup);
92
- console.log(`✓ Backup restored: ${restoredPath}`);
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("Please restart your application to apply changes.");
97
+ console.log(" 请重启相关工具以应用恢复后的配置");
95
98
  console.log("");
96
99
  } catch (err) {
97
- console.log(`✗ Restore failed: ${err.message}`);
100
+ console.log(` ✗ 恢复失败: ${err.message}`);
98
101
  console.log("");
99
102
  }
100
103
  }
@@ -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("=== Node Testing ===");
9
- console.log("");
8
+ console.log(" 测试节点");
9
+ console.log(" ─────────────────────────────────────");
10
10
 
11
- // Test Claude nodes
12
- console.log("Testing Claude Code nodes...");
13
- const claudeNodes = await getNodes("claude");
11
+ const services = [
12
+ { key: "claude", label: "Claude Code" },
13
+ { key: "codex", label: "Codex" },
14
+ ];
14
15
 
15
- if (claudeNodes.length > 0) {
16
- const claudeResults = await testNodes(claudeNodes);
16
+ for (const service of services) {
17
17
  console.log("");
18
- console.log(formatNodeResults(claudeResults));
19
- } else {
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
- // Test Codex nodes
26
- console.log("Testing Codex nodes...");
27
- const codexNodes = await getNodes("codex");
21
+ if (nodes.length === 0) {
22
+ console.log(" 暂无可用节点");
23
+ continue;
24
+ }
28
25
 
29
- if (codexNodes.length > 0) {
30
- const codexResults = await testNodes(codexNodes);
26
+ const results = await testNodes(nodes);
31
27
  console.log("");
32
- console.log(formatNodeResults(codexResults));
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 runActivationWizard();
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
- const action = await selectMenuAction();
248
-
249
- switch (action) {
250
- case "activate":
251
- await runActivationWizard();
252
- break;
253
- case "test":
254
- await runTestCommand();
255
- break;
256
- case "restore":
257
- await runRestoreCommand();
258
- break;
259
- default:
260
- console.log("");
261
- console.log("再见。");
262
- console.log("");
263
- break;
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
- "newapi-claude": {
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: "newapi-claude/claude-opus-4-6",
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
- "newapi-codex": {
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: "newapi-codex/gpt-5.2",
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: "newapi-claude",
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: "newapi-codex",
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: "text",
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 printDetection(service, detectedPlatforms, blockedPlatforms = []) {
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
- function printPlan(service, upstream, apiKey, targets, skipped = []) {
379
- console.log("激活计划:");
380
- console.log(` 能力: ${getServiceLabel(service)}`);
381
- console.log(` 上游: ${upstream.baseUrl}`);
382
- console.log(` 密钥: ${maskKey(apiKey)}`);
383
- console.log(" 平台:");
384
- for (const { platform, detection } of targets) {
385
- console.log(`${platform.name} (${getStatusLabel(platform, detection)})`);
386
- }
387
- for (const { platform } of skipped) {
388
- console.log(` - ${platform.name} (未选择)`);
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, upstream, backupPath, results, redeemResult) {
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
- console.log("激活结果:");
399
- for (const { platform, result } of results) {
400
- if (result.success) {
401
- console.log(` ✓ ${platform.name}`);
402
- for (const file of result.files || []) {
403
- console.log(` ${file}`);
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
- console.log("汇总:");
414
- console.log(` 能力: ${getServiceLabel(service)}`);
415
- console.log(` 上游: ${upstream.baseUrl}`);
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(` 兑换: ${redeemResult.valid ? "已完成" : `未完成(${redeemResult.error || "接口不可用"})`}`);
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
- console.log("");
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("Activation cancelled.");
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
- printResultSummary(service, upstream, activation.backupPath, activation.results, activation.redeemResult);
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
- printBanner();
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
- printPlan(service, upstream, credential.apiKey, targets, skipped);
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
- printResultSummary(service, upstream, activation.backupPath, activation.results, activation.redeemResult);
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 files = fs.readdirSync(BACKUP_DIR);
124
+ const entries = fs.readdirSync(BACKUP_DIR, { withFileTypes: true });
99
125
  const backups = [];
100
126
 
101
- for (const file of files) {
102
- if (!file.endsWith(".json")) continue;
103
-
104
- const filePath = path.join(BACKUP_DIR, file);
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 content = fs.readFileSync(filePath, "utf8");
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: filePath,
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.unlinkSync(backup.path);
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
- console.error("Failed to fetch nodes:", err.message);
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 url = new URL("/health", nodeUrl);
148
- const response = await makeRequest(url.pathname, {
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 === 200,
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
- const result = await testNode(node.url);
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
- ...result,
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 selectBestNode(testResults) {
20
- const available = testResults.filter((r) => r.available);
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
- if (available.length === 0) {
23
- return null;
24
- }
51
+ function getResultLatency(result) {
52
+ return typeof result.avgLatency === "number" && result.avgLatency >= 0
53
+ ? result.avgLatency
54
+ : result.latency;
55
+ }
25
56
 
26
- available.sort((a, b) => a.latency - b.latency);
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
- for (const result of results) {
35
- const status = result.available ? "" : "✗";
36
- const latency = result.available ? `${result.latency}ms` : "N/A";
37
- const name = result.name || result.url;
94
+ lines.push(" 节点测试结果");
95
+ lines.push(" ───────────────────────────────────────────────────");
96
+ lines.push("");
38
97
 
39
- lines.push(` ${status} ${name} - ${latency}`);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fogact",
3
- "version": "1.1.10",
3
+ "version": "1.2.0",
4
4
  "description": "FogAct activation helper for Claude Code and Codex",
5
5
  "keywords": [
6
6
  "fogact",