fogact 1.1.9 → 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.
@@ -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
 
@@ -428,31 +475,29 @@ async function selectPlatforms(detectedPlatforms, options = {}) {
428
475
  if (options.platforms) {
429
476
  return getActivationTargets(detectedPlatforms, false, options.platforms);
430
477
  }
431
- if (options.yes || options.auto) {
432
- return getActivationTargets(detectedPlatforms, Boolean(options.all));
433
- }
478
+ return getActivationTargets(detectedPlatforms, Boolean(options.all));
479
+ }
434
480
 
435
- const choices = detectedPlatforms.map(({ platform, detection }) => {
436
- const selectable = canSelectPlatform(platform, detection);
437
- return {
438
- title: `${platform.name}(${getStatusLabel(platform, detection)})`,
439
- value: platform.id,
440
- selected: platform.required || detection.installed,
441
- disabled: selectable ? false : "未安装,无法自动配置",
442
- };
443
- });
481
+ async function verifyPrimaryNode() {
482
+ const nodes = await getNodes("codex");
483
+ const primary = nodes[0];
484
+ if (!primary) return null;
444
485
 
445
- const response = await prompts({
446
- type: "multiselect",
447
- name: "platformIds",
448
- message: "请选择要激活的平台",
449
- choices,
450
- min: 1,
451
- hint: "空格选择,回车确认",
452
- }, { onCancel: () => false });
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
+ }
453
496
 
454
- const selectedIds = response.platformIds || [];
455
- return getActivationTargets(detectedPlatforms, false, selectedIds);
497
+ console.log(` ✗ ${primary.name || "FogAct"} 连接失败`);
498
+ console.log(` 地址: ${primary.url}`);
499
+ console.log("");
500
+ return null;
456
501
  }
457
502
 
458
503
  async function resolveCodeCredential(options, upstream) {
@@ -462,7 +507,7 @@ async function resolveCodeCredential(options, upstream) {
462
507
  }
463
508
 
464
509
  console.log("");
465
- console.log("正在读取激活码能力...");
510
+ console.log("正在验证激活码...");
466
511
  const inspection = await inspectActivationCode(code);
467
512
  if (!inspection.valid) {
468
513
  console.log(`✗ 无法读取激活码能力: ${inspection.error || "接口未返回有效信息"}`);
@@ -494,47 +539,29 @@ async function resolveApiKeyCredential(options, upstream) {
494
539
  }
495
540
 
496
541
  async function activateTargets({ service, upstream, apiKey, targets, activationCode, options = {} }) {
497
- console.log("");
498
- console.log("正在创建备份...");
499
542
  const backupPath = createActivationBackup(service, getBackupPaths(targets), {
500
543
  upstream: upstream.baseUrl,
501
544
  targets: targets.map(({ platform }) => platform.id),
502
545
  });
503
- if (backupPath) {
504
- console.log(`✓ 备份完成: ${backupPath}`);
505
- } else {
506
- console.log("ℹ 没有旧配置需要备份");
507
- }
508
546
 
509
- console.log("");
510
- console.log("正在激活平台...");
511
547
  const results = [];
512
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
+ }
513
553
  try {
514
- const result = platform.activate({ service, upstream, apiKey, detection });
554
+ const result = platform.activate({ service, upstream, apiKey, options, detection });
515
555
  results.push({ platform, result });
516
- if (result.success) {
517
- console.log(`✓ ${platform.name}`);
518
- } else {
519
- console.log(`⚠ ${platform.name}: ${result.message || "已跳过"}`);
520
- }
521
556
  } catch (err) {
522
557
  results.push({ platform, result: { success: false, error: err.message } });
523
- console.log(`✗ ${platform.name}: ${err.message}`);
524
558
  }
525
559
  }
526
560
 
527
561
  let redeemResult = null;
528
562
  const failures = results.filter(({ result }) => !result.success && !result.skipped);
529
563
  if (activationCode && failures.length === 0 && !options.noRedeem) {
530
- console.log("");
531
- console.log("正在完成兑换记录...");
532
564
  redeemResult = await redeemActivationCode(activationCode, service);
533
- if (redeemResult.valid) {
534
- console.log("✓ 兑换记录已完成");
535
- } else {
536
- console.log(`⚠ 兑换记录未完成: ${redeemResult.error || "接口不可用"}`);
537
- }
538
565
  }
539
566
 
540
567
  return { backupPath, results, redeemResult };
@@ -587,19 +614,22 @@ async function runNewApiActivation(options = {}) {
587
614
  const detectedPlatforms = detectPlatforms(service);
588
615
  const selectedPlatformIds = options.platforms ? parsePlatformIds(options.platforms) : null;
589
616
  const targets = getActivationTargets(detectedPlatforms, Boolean(options.all), selectedPlatformIds);
590
- const skipped = detectedPlatforms.filter((entry) => !targets.includes(entry));
591
617
 
592
- console.log("");
593
- printPlan(service, upstream, apiKey, targets, skipped);
618
+ printCredentialProfile(service, upstream, apiKey, entitlement);
594
619
 
595
- if (!(await confirmActivation(Boolean(options.yes || options.auto)))) {
596
- console.log("Activation cancelled.");
620
+ if (!(await confirmActivation(Boolean(options.yes || options.auto), service))) {
621
+ console.log("");
622
+ console.log(" 已取消");
623
+ console.log("");
597
624
  return { success: false, cancelled: true };
598
625
  }
599
626
 
627
+ console.log("");
628
+ console.log(" 正在写入配置...");
600
629
  const activation = await activateTargets({ service, upstream, apiKey, targets, options });
601
630
  const failures = activation.results.filter(({ result }) => !result.success && !result.skipped);
602
- printResultSummary(service, upstream, activation.backupPath, activation.results, activation.redeemResult);
631
+ console.log(" 配置完成");
632
+ printResultSummary(service, activation.backupPath, activation.results, activation.redeemResult);
603
633
 
604
634
  return {
605
635
  success: failures.length === 0,
@@ -609,7 +639,13 @@ async function runNewApiActivation(options = {}) {
609
639
  }
610
640
 
611
641
  async function runActivationWizard(options = {}) {
612
- printBanner();
642
+ if (!options.noNodeCheck) {
643
+ const node = await verifyPrimaryNode();
644
+ if (!node) {
645
+ return { success: false, cancelled: true };
646
+ }
647
+ }
648
+
613
649
  const baseUpstream = loadUpstreamConfig({ configPath: options.upstreamConfig });
614
650
  const credentialType = !options.code && options.apiKey ? "api-key" : "code";
615
651
 
@@ -640,30 +676,29 @@ async function runActivationWizard(options = {}) {
640
676
  if (!verification.valid) {
641
677
  return { success: false, verification };
642
678
  }
643
- } else {
644
- console.log("✓ 已按激活码能力限制可选平台");
645
679
  }
646
680
 
647
681
  const allDetectedPlatforms = detectPlatforms(service);
648
682
  const allowedPlatforms = allDetectedPlatforms.filter((entry) => isPlatformAllowed(entry, credential.entitlement, service));
649
- const blockedPlatforms = allDetectedPlatforms.filter((entry) => !allowedPlatforms.includes(entry));
650
-
651
- console.log("");
652
- printDetection(service, allowedPlatforms, blockedPlatforms);
653
683
 
654
684
  const targets = await selectPlatforms(allowedPlatforms, options);
655
685
  if (targets.length === 0) {
656
- console.log("没有选择任何平台,已取消。");
686
+ console.log("");
687
+ console.log(" ✗ 当前环境没有可激活目标");
688
+ console.log("");
657
689
  return { success: false, cancelled: true };
658
690
  }
659
- const skipped = allowedPlatforms.filter((entry) => !targets.includes(entry)).concat(blockedPlatforms);
660
691
 
661
- printPlan(service, upstream, credential.apiKey, targets, skipped);
662
- if (!(await confirmActivation(Boolean(options.yes || options.auto)))) {
663
- 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("");
664
697
  return { success: false, cancelled: true };
665
698
  }
666
699
 
700
+ console.log("");
701
+ console.log(" 正在写入配置...");
667
702
  const activation = await activateTargets({
668
703
  service,
669
704
  upstream,
@@ -673,7 +708,8 @@ async function runActivationWizard(options = {}) {
673
708
  options,
674
709
  });
675
710
  const failures = activation.results.filter(({ result }) => !result.success && !result.skipped);
676
- printResultSummary(service, upstream, activation.backupPath, activation.results, activation.redeemResult);
711
+ console.log(" 配置完成");
712
+ printResultSummary(service, activation.backupPath, activation.results, activation.redeemResult);
677
713
 
678
714
  return {
679
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) {