fogact 1.2.4 → 1.2.5

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.
@@ -3,49 +3,72 @@
3
3
  const prompts = require("prompts");
4
4
  const { listBackups, restoreBackup, clearBackups } = require("../services/backup-service");
5
5
 
6
+ function getServiceLabel(service) {
7
+ if (service === "codex") return "Codex";
8
+ if (service === "claude") return "Claude Code";
9
+ return service || "未知服务";
10
+ }
11
+
12
+ function getBackupFiles(backup) {
13
+ if (Array.isArray(backup.files)) {
14
+ return backup.files.map((file) => file.originalPath).filter(Boolean);
15
+ }
16
+ return [backup.originalPath].filter(Boolean);
17
+ }
18
+
19
+ function formatBackupTime(backup) {
20
+ const date = backup.timestamp ? new Date(backup.timestamp) : null;
21
+ if (!date || Number.isNaN(date.getTime())) return "未知时间";
22
+ return date.toLocaleString("zh-CN");
23
+ }
24
+
6
25
  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}`;
26
+ const fileCount = getBackupFiles(backup).length;
27
+ return `${getServiceLabel(backup.service)} · ${formatBackupTime(backup)} · ${fileCount} 个文件`;
11
28
  }
12
29
 
13
- async function runRestoreCommand(options = {}) {
30
+ function printRestoreHeader() {
14
31
  console.log("");
15
32
  console.log(" 恢复备份");
16
33
  console.log(" ─────────────────────────────────────");
34
+ }
17
35
 
18
- let service = options.service;
19
- if (!service) {
20
- const response = await prompts({
21
- type: "select",
22
- name: "service",
23
- message: "请选择要查看的备份",
24
- choices: [
25
- { title: "Claude Code", value: "claude" },
26
- { title: "Codex", value: "codex" },
27
- { title: "全部备份", value: null },
28
- ],
29
- initial: 2,
30
- }, { onCancel: () => false });
31
-
32
- if (response.service === undefined) {
33
- console.log("");
34
- console.log(" 已取消");
35
- console.log("");
36
- return;
37
- }
38
- service = response.service;
36
+ function printBackupDetail(backup) {
37
+ console.log("");
38
+ console.log(" 备份详情");
39
+ console.log(" ─────────────────────────────────────");
40
+ console.log(` 服务类型: ${getServiceLabel(backup.service)}`);
41
+ console.log(` 备份时间: ${formatBackupTime(backup)}`);
42
+ console.log(` 备份类型: ${backup.kind === "manifest" ? "激活配置备份" : "单文件备份"}`);
43
+ console.log(` 文件数量: ${getBackupFiles(backup).length}`);
44
+ if (backup.path) console.log(` 备份位置: ${backup.path}`);
45
+ console.log("");
46
+ console.log(" 将恢复文件:");
47
+ for (const filePath of getBackupFiles(backup)) {
48
+ console.log(` ${filePath}`);
39
49
  }
50
+ console.log("");
51
+ }
40
52
 
41
- const backups = listBackups(service);
42
- if (backups.length === 0) {
43
- console.log("");
44
- console.log(" 暂无可恢复备份");
45
- console.log("");
46
- return;
47
- }
53
+ async function promptBackupService(defaultService) {
54
+ if (defaultService) return defaultService;
55
+
56
+ const response = await prompts({
57
+ type: "select",
58
+ name: "service",
59
+ message: "请选择要查看的备份",
60
+ choices: [
61
+ { title: "Claude Code", value: "claude" },
62
+ { title: "Codex", value: "codex" },
63
+ { title: "全部备份", value: null },
64
+ ],
65
+ initial: 2,
66
+ }, { onCancel: () => false });
67
+
68
+ return response.service;
69
+ }
48
70
 
71
+ async function promptBackup(backups) {
49
72
  const response = await prompts({
50
73
  type: "select",
51
74
  name: "backup",
@@ -56,14 +79,71 @@ async function runRestoreCommand(options = {}) {
56
79
  ],
57
80
  }, { onCancel: () => false });
58
81
 
59
- if (!response.backup) {
82
+ return response.backup;
83
+ }
84
+
85
+ async function confirmRestore(backup) {
86
+ printBackupDetail(backup);
87
+ const response = await prompts({
88
+ type: "confirm",
89
+ name: "confirmed",
90
+ message: "确认恢复此备份?",
91
+ initial: true,
92
+ }, { onCancel: () => false });
93
+ return Boolean(response.confirmed);
94
+ }
95
+
96
+ function printRestoreResult(success, restoredPaths, service, error = null) {
97
+ console.log("");
98
+ if (success) {
99
+ console.log(" ✓ 备份已恢复");
100
+ console.log("");
101
+ console.log(" 已恢复文件:");
102
+ for (const restoredPath of restoredPaths) {
103
+ console.log(` ${restoredPath}`);
104
+ }
105
+ console.log("");
106
+ const tip = service === "claude"
107
+ ? "请重启 Claude Code 以使配置生效"
108
+ : "请重启 Codex / VSCode / Cursor / OpenCode 以使配置生效";
109
+ console.log(` ${tip}`);
110
+ console.log("");
111
+ return;
112
+ }
113
+
114
+ console.log(" ✗ 恢复失败");
115
+ if (error) console.log(` ${error}`);
116
+ console.log("");
117
+ }
118
+
119
+ async function runRestoreCommand(options = {}) {
120
+ printRestoreHeader();
121
+
122
+ const service = await promptBackupService(options.service);
123
+ if (service === undefined) {
60
124
  console.log("");
61
125
  console.log(" 已取消");
62
126
  console.log("");
63
127
  return;
64
128
  }
65
129
 
66
- if (response.backup === "__clear__") {
130
+ const backups = listBackups(service);
131
+ if (backups.length === 0) {
132
+ console.log("");
133
+ console.log(" ℹ 暂无可恢复备份");
134
+ console.log("");
135
+ return;
136
+ }
137
+
138
+ const backupPath = await promptBackup(backups);
139
+ if (!backupPath) {
140
+ console.log("");
141
+ console.log(" 已取消");
142
+ console.log("");
143
+ return;
144
+ }
145
+
146
+ if (backupPath === "__clear__") {
67
147
  const confirm = await prompts({
68
148
  type: "confirm",
69
149
  name: "value",
@@ -85,21 +165,30 @@ async function runRestoreCommand(options = {}) {
85
165
  return;
86
166
  }
87
167
 
88
- console.log("");
89
- console.log(" 正在恢复备份...");
90
- try {
91
- const restoredPaths = restoreBackup(response.backup);
92
- console.log(" ✓ 备份已恢复");
93
- for (const restoredPath of restoredPaths) {
94
- console.log(` ${restoredPath}`);
95
- }
168
+ const backup = backups.find((item) => item.path === backupPath);
169
+ if (!backup) {
96
170
  console.log("");
97
- console.log(" 请重启相关工具以应用恢复后的配置");
171
+ console.log(" ✗ 备份不存在");
98
172
  console.log("");
99
- } catch (err) {
100
- console.log(` ✗ 恢复失败: ${err.message}`);
173
+ return;
174
+ }
175
+
176
+ if (!await confirmRestore(backup)) {
177
+ console.log(" 已取消恢复");
101
178
  console.log("");
179
+ return;
180
+ }
181
+
182
+ try {
183
+ const restoredPaths = restoreBackup(backupPath);
184
+ printRestoreResult(true, restoredPaths, backup.service);
185
+ } catch (err) {
186
+ printRestoreResult(false, [], backup.service, err.message);
102
187
  }
103
188
  }
104
189
 
105
- module.exports = { runRestoreCommand };
190
+ module.exports = {
191
+ formatBackupTitle,
192
+ printBackupDetail,
193
+ runRestoreCommand,
194
+ };
@@ -3,31 +3,57 @@
3
3
  const { getNodes } = require("../services/fogact-api");
4
4
  const { testNodes, formatNodeResults } = require("../services/node-service");
5
5
 
6
+ function updateProgress(message) {
7
+ if (process.stdout.isTTY) {
8
+ process.stdout.write(`\r ${message}${" ".repeat(20)}`);
9
+ return;
10
+ }
11
+ console.log(` ${message}`);
12
+ }
13
+
14
+ async function collectServiceNodes(services) {
15
+ const allNodes = [];
16
+ for (const service of services) {
17
+ const nodes = await getNodes(service.key);
18
+ for (const node of nodes) {
19
+ allNodes.push({
20
+ ...node,
21
+ name: node.name || "FogAct",
22
+ service: service.key,
23
+ serviceLabel: service.label,
24
+ });
25
+ }
26
+ }
27
+ return allNodes;
28
+ }
29
+
6
30
  async function runTestCommand() {
7
31
  console.log("");
8
- console.log(" 测试节点");
9
- console.log(" ─────────────────────────────────────");
32
+ console.log(" 正在测试所有节点...");
10
33
 
11
34
  const services = [
12
- { key: "claude", label: "Claude Code" },
35
+ { key: "claude", label: "Claude" },
13
36
  { key: "codex", label: "Codex" },
14
37
  ];
38
+ const nodes = await collectServiceNodes(services);
15
39
 
16
- for (const service of services) {
40
+ if (nodes.length === 0) {
17
41
  console.log("");
18
- console.log(` 正在测试 ${service.label} 节点...`);
19
- const nodes = await getNodes(service.key);
20
-
21
- if (nodes.length === 0) {
22
- console.log(" ℹ 暂无可用节点");
23
- continue;
24
- }
25
-
26
- const results = await testNodes(nodes);
42
+ console.log(" 暂无可用节点");
27
43
  console.log("");
28
- console.log(formatNodeResults(results));
44
+ return;
29
45
  }
30
46
 
47
+ updateProgress(`测试中... (0/${nodes.length} 完成)`);
48
+ const results = await testNodes(nodes, (_node, done, total) => {
49
+ updateProgress(`测试中... (${done}/${total} 完成)`);
50
+ });
51
+
52
+ if (process.stdout.isTTY) {
53
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
54
+ }
55
+ console.log("");
56
+ console.log(formatNodeResults(results));
31
57
  console.log("");
32
58
  }
33
59
 
@@ -353,6 +353,7 @@ function printResultSummary(service, backupPath, results, redeemResult) {
353
353
  console.log("");
354
354
  if (backupPath) {
355
355
  console.log(" ✓ 备份已创建");
356
+ console.log(` 位置: ${backupPath}`);
356
357
  }
357
358
 
358
359
  const printConfigured = (entry, label) => {
@@ -6,7 +6,6 @@ async function testNodes(nodes, onProgress = null) {
6
6
  const results = [];
7
7
 
8
8
  for (const node of nodes) {
9
- if (onProgress) onProgress(node, results.length, nodes.length);
10
9
  const probes = [];
11
10
  for (let index = 0; index < 3; index += 1) {
12
11
  const result = await testNode(node.url);
@@ -35,6 +34,8 @@ async function testNodes(nodes, onProgress = null) {
35
34
  score: scoreNode({ available, avgLatency, latencyStdDev, successRate }),
36
35
  error: available ? undefined : probes.find((probe) => probe.error)?.error || "节点不可达",
37
36
  });
37
+
38
+ if (onProgress) onProgress(node, results.length, nodes.length);
38
39
  }
39
40
 
40
41
  return results;
@@ -72,11 +73,15 @@ function selectBestNode(testResults) {
72
73
  return available[0];
73
74
  }
74
75
 
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`;
76
+ function sortNodeResults(results) {
77
+ return [...results].sort((left, right) => getResultScore(right) - getResultScore(left) || getResultLatency(left) - getResultLatency(right));
78
+ }
79
+
80
+ function latencyLevel(latency) {
81
+ if (latency <= 50) return "优秀";
82
+ if (latency <= 100) return "良好";
83
+ if (latency <= 300) return "一般";
84
+ return "较慢";
80
85
  }
81
86
 
82
87
  function stabilityLabel(stdDev) {
@@ -86,30 +91,53 @@ function stabilityLabel(stdDev) {
86
91
  return "波动";
87
92
  }
88
93
 
89
- function formatNodeResults(results) {
90
- const sorted = [...results].sort((left, right) => right.score - left.score || left.avgLatency - right.avgLatency);
94
+ function padCell(value, width) {
95
+ const text = String(value || "");
96
+ const displayWidth = Array.from(text).reduce((sum, char) => sum + (char.charCodeAt(0) > 0xff ? 2 : 1), 0);
97
+ return `${text}${" ".repeat(Math.max(0, width - displayWidth))}`;
98
+ }
99
+
100
+ function formatNodeLine(result, best) {
101
+ const mark = result.available ? "✓" : "✗";
102
+ const name = padCell(result.name || result.url || "FogAct", 12);
103
+ const service = padCell(result.serviceLabel || result.region || "Global", 9);
104
+
105
+ if (!result.available) {
106
+ return ` ${mark} ${name} ${service} -- -- 不可达 --`;
107
+ }
108
+
109
+ const latency = padCell(`${result.avgLatency}ms`, 8);
110
+ const jitter = padCell(`±${result.latencyStdDev}ms`, 7);
111
+ const stability = padCell(stabilityLabel(result.latencyStdDev), 6);
112
+ const score = padCell(`${getResultScore(result)}分`, 5);
113
+ const level = padCell(latencyLevel(result.avgLatency), 4);
114
+ const bestMark = best && best === result ? " ★ 最优" : "";
115
+ return ` ${mark} ${name} ${service} ${latency} ${jitter} ${stability} ${score} ${level}${bestMark}`;
116
+ }
117
+
118
+ function formatNodeResults(results, options = {}) {
119
+ const sorted = sortNodeResults(results);
91
120
  const best = sorted.find((result) => result.available);
121
+ const availableCount = results.filter((result) => result.available).length;
92
122
  const lines = [];
123
+ const title = options.title || "节点测试结果";
93
124
 
94
- lines.push(" 节点测试结果");
95
- lines.push(" ───────────────────────────────────────────────────");
96
- lines.push("");
125
+ lines.push(` ${title}`);
126
+ lines.push(" ────────────────────────────────────────────────────────────");
127
+ lines.push(" 状态 节点 服务 延迟 波动 稳定性 评分 等级");
128
+ lines.push(" ────────────────────────────────────────────────────────────");
97
129
 
98
130
  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}`);
131
+ lines.push(formatNodeLine(result, best));
107
132
  }
108
133
 
109
- const availableCount = results.filter((result) => result.available).length;
110
- lines.push("");
111
- lines.push(" ───────────────────────────────────────────────────");
134
+ lines.push(" ────────────────────────────────────────────────────────────");
112
135
  lines.push(` 测试完成,共 ${results.length} 个节点,${availableCount} 个可用`);
136
+ if (best) {
137
+ lines.push(` 推荐节点: ${best.name || best.url} · ${best.avgLatency}ms · ${getResultScore(best)}分`);
138
+ } else {
139
+ lines.push(" 暂无可用节点,请稍后重试或联系管理员");
140
+ }
113
141
  return lines.join("\n");
114
142
  }
115
143
 
@@ -117,4 +145,5 @@ module.exports = {
117
145
  testNodes,
118
146
  selectBestNode,
119
147
  formatNodeResults,
148
+ sortNodeResults,
120
149
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fogact",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "FogAct activation helper for Claude Code and Codex",
5
5
  "keywords": [
6
6
  "fogact",