fogact 1.1.10 → 1.2.2

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.
@@ -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;
@@ -6,7 +6,7 @@ const http = require("http");
6
6
 
7
7
  // 支持环境变量覆盖 API 地址;默认连接公网 FogAct 面板。
8
8
  const DEFAULT_API_BASE = "https://cliproxy.fogidc.com";
9
- const API_BASE = process.env.FOGACT_API_BASE || process.env.CLIPROXY_API_BASE || DEFAULT_API_BASE;
9
+ const API_BASE = process.env.FOGACT_API_BASE || DEFAULT_API_BASE;
10
10
 
11
11
  function makeRequest(path, options = {}) {
12
12
  return new Promise((resolve, reject) => {
@@ -57,7 +57,6 @@ function makeRequest(path, options = {}) {
57
57
 
58
58
  async function verifyActivationCode(code, service) {
59
59
  try {
60
- // 调用本地激活 API
61
60
  const response = await makeRequest("/api/activate", {
62
61
  method: "POST",
63
62
  body: {
@@ -67,17 +66,18 @@ async function verifyActivationCode(code, service) {
67
66
  },
68
67
  });
69
68
 
70
- if (response.status === 200 && response.data.success) {
69
+ const normalized = normalizeActivationResponse(response);
70
+
71
+ if (response.status >= 200 && response.status < 300 && normalized.valid) {
71
72
  return {
72
73
  valid: true,
73
- service: response.data.data.service,
74
- expiresAt: response.data.data.expiresAt,
75
- quota: response.data.data.quota,
76
- nodes: response.data.data.nodes || [],
74
+ ...normalized.data,
75
+ data: normalized.data,
76
+ nodes: normalized.data.nodes || [],
77
77
  };
78
78
  }
79
79
 
80
- return { valid: false, error: response.data.message || "Invalid code" };
80
+ return { valid: false, error: normalized.message || "Invalid code" };
81
81
  } catch (err) {
82
82
  return { valid: false, error: err.message };
83
83
  }
@@ -116,45 +116,59 @@ async function inspectActivationCode(code) {
116
116
  }
117
117
 
118
118
  async function redeemActivationCode(code, service) {
119
- return verifyActivationCode(code, service);
119
+ const result = await inspectActivationCode(code);
120
+ return result.valid
121
+ ? { valid: true, service: result.service, expiresAt: result.expiresAt, quota: result.quota, nodes: result.nodes || [] }
122
+ : result;
120
123
  }
121
124
 
122
125
  async function getNodes(service) {
123
126
  try {
124
- const response = await makeRequest(`/api/nodes?service=${service}`);
127
+ const response = await makeRequest(`/api/nodes?service=${encodeURIComponent(service || "")}`);
125
128
 
126
129
  if (response.status === 200 && Array.isArray(response.data.nodes)) {
127
130
  return response.data.nodes;
128
131
  }
129
132
 
130
- // 返回默认节点
131
- return [
132
- { name: "FogAct Local Node", url: "http://localhost:34020", region: "Global" }
133
- ];
133
+ return [{ name: "FogAct", url: API_BASE, region: "Global" }];
134
134
  } 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
- ];
135
+ return [{ name: "FogAct", url: API_BASE, region: "Global" }];
140
136
  }
141
137
  }
142
138
 
139
+ function requestUrl(urlString, options = {}) {
140
+ return new Promise((resolve, reject) => {
141
+ const url = new URL(urlString);
142
+ const isHttps = url.protocol === "https:";
143
+ const client = isHttps ? https : http;
144
+ const req = client.request({
145
+ hostname: url.hostname,
146
+ port: url.port || (isHttps ? 443 : 80),
147
+ path: url.pathname + url.search,
148
+ method: options.method || "GET",
149
+ headers: { "User-Agent": `fogact/${packageJson.version}`, ...options.headers },
150
+ timeout: options.timeout || 8000,
151
+ }, (res) => {
152
+ res.resume();
153
+ res.on("end", () => resolve({ status: res.statusCode }));
154
+ });
155
+ req.on("timeout", () => req.destroy(new Error("Request timed out")));
156
+ req.on("error", reject);
157
+ req.end();
158
+ });
159
+ }
160
+
143
161
  async function testNode(nodeUrl) {
144
162
  const start = Date.now();
145
163
 
146
164
  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
-
165
+ const healthUrl = new URL("/health", nodeUrl).toString();
166
+ const response = await requestUrl(healthUrl);
153
167
  const latency = Date.now() - start;
154
168
 
155
169
  return {
156
170
  url: nodeUrl,
157
- available: response.status === 200,
171
+ available: response.status >= 200 && response.status < 500,
158
172
  latency,
159
173
  };
160
174
  } 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.2",
4
4
  "description": "FogAct activation helper for Claude Code and Codex",
5
5
  "keywords": [
6
6
  "fogact",