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.
- 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/index.html +2 -2
- package/frontend/user/assets/DashboardLayout-DDkxHYFj.js +1 -1
- package/frontend/user/assets/Welcome-Dtfp6oER.js +1 -1
- package/frontend/user/assets/announcement-35mOnjRL.js +1 -1
- package/frontend/user/assets/index-Da98HOxL.js +2 -2
- package/frontend/user/index.html +4 -4
- 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 +154 -118
- 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
|
@@ -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
|
|
|
@@ -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
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
478
|
+
return getActivationTargets(detectedPlatforms, Boolean(options.all));
|
|
479
|
+
}
|
|
434
480
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|