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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/web-server.js +303 -97
- package/frontend/admin/admin-panel-v2.js +8 -5
- package/frontend/assets/market-ui.css +11 -84
- package/frontend/user/assets/Dashboard-rPsmltm5.js +1 -1
- package/frontend/user/assets/DashboardLayout-DDkxHYFj.js +2 -2
- 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 +3 -3
- package/frontend/user/index.html +5 -5
- package/lib/commands/activate.js +1 -8
- package/lib/commands/restore.js +41 -38
- package/lib/commands/test.js +15 -21
- package/lib/config/claude.js +1 -0
- package/lib/config/codex.js +1 -1
- package/lib/config/upstream.js +3 -0
- package/lib/index.js +77 -35
- package/lib/platforms/openclaw.js +4 -4
- package/lib/platforms/opencode.js +3 -3
- package/lib/services/activation-orchestrator.js +170 -246
- package/lib/services/backup-service.js +65 -13
- package/lib/services/fogact-api.js +40 -26
- package/lib/services/node-service.js +85 -14
- package/package.json +1 -1
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
|
|
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
package/bin/web-server.js
CHANGED
|
@@ -36,18 +36,41 @@ const USER_STATUS_KEY_MAP = {
|
|
|
36
36
|
const CODE_STATUS_MAP = {
|
|
37
37
|
unused: "unused",
|
|
38
38
|
"未使用": "unused",
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
"未激活": "unused",
|
|
40
|
+
active: "active",
|
|
41
|
+
"活跃": "active",
|
|
42
|
+
"已激活": "active",
|
|
43
|
+
used: "active",
|
|
44
|
+
"已使用": "active",
|
|
45
|
+
disabled: "disabled",
|
|
46
|
+
blocked: "disabled",
|
|
47
|
+
banned: "disabled",
|
|
48
|
+
"已禁用": "disabled",
|
|
49
|
+
"禁用": "disabled",
|
|
41
50
|
expired: "expired",
|
|
42
51
|
"已过期": "expired",
|
|
43
52
|
};
|
|
44
53
|
|
|
45
54
|
const CODE_STATUS_LABEL_MAP = {
|
|
46
|
-
unused: "
|
|
47
|
-
|
|
55
|
+
unused: "未激活",
|
|
56
|
+
active: "活跃",
|
|
57
|
+
disabled: "已禁用",
|
|
48
58
|
expired: "已过期",
|
|
49
59
|
};
|
|
50
60
|
|
|
61
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
62
|
+
"connection",
|
|
63
|
+
"keep-alive",
|
|
64
|
+
"proxy-authenticate",
|
|
65
|
+
"proxy-authorization",
|
|
66
|
+
"te",
|
|
67
|
+
"trailer",
|
|
68
|
+
"transfer-encoding",
|
|
69
|
+
"upgrade",
|
|
70
|
+
"host",
|
|
71
|
+
"content-length",
|
|
72
|
+
]);
|
|
73
|
+
|
|
51
74
|
// 初始化示例数据
|
|
52
75
|
initializeSampleData();
|
|
53
76
|
|
|
@@ -181,7 +204,7 @@ function normalizeCodeStatus(codeOrStatus, expiresAt) {
|
|
|
181
204
|
}
|
|
182
205
|
|
|
183
206
|
function getCodeStatusLabel(status) {
|
|
184
|
-
return CODE_STATUS_LABEL_MAP[normalizeCodeStatus(status)] || "
|
|
207
|
+
return CODE_STATUS_LABEL_MAP[normalizeCodeStatus(status)] || "未激活";
|
|
185
208
|
}
|
|
186
209
|
|
|
187
210
|
function serializeUser(user) {
|
|
@@ -219,30 +242,27 @@ function getActivationPlatforms(serviceKey) {
|
|
|
219
242
|
return [];
|
|
220
243
|
}
|
|
221
244
|
|
|
222
|
-
function
|
|
245
|
+
function getPublicBaseUrl(req) {
|
|
246
|
+
const forwardedHost = String(req?.headers?.['x-forwarded-host'] || req?.headers?.host || '').split(',')[0].trim();
|
|
247
|
+
const publicHost = forwardedHost && !forwardedHost.includes('fogact.fogact.com')
|
|
248
|
+
? forwardedHost
|
|
249
|
+
: 'cliproxy.fogidc.com';
|
|
250
|
+
const isLocalHost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[?::1\]?)(:\d+)?$/i.test(publicHost);
|
|
251
|
+
const defaultProtocol = isLocalHost ? 'http' : 'https';
|
|
252
|
+
const publicProtocol = String(req?.headers?.['x-forwarded-proto'] || defaultProtocol).split(',')[0].trim() || defaultProtocol;
|
|
253
|
+
return trimTrailingSlash(process.env.FOGACT_PUBLIC_BASE_URL || `${publicProtocol}://${publicHost}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getProxyBaseUrl(req, serviceKey) {
|
|
257
|
+
const publicBaseUrl = getPublicBaseUrl(req);
|
|
258
|
+
return serviceKey === "claude" ? publicBaseUrl : `${publicBaseUrl}/v1`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildActivationData(serializedCode, req) {
|
|
223
262
|
const serviceKey = serializedCode.serviceKey;
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
serializedCode.baseUrl ||
|
|
228
|
-
serializedCode.baseURL ||
|
|
229
|
-
serializedCode.url ||
|
|
230
|
-
serviceConfig.baseUrl ||
|
|
231
|
-
serviceConfig.baseURL ||
|
|
232
|
-
serviceConfig.url ||
|
|
233
|
-
getServiceBaseUrl(upstream, serviceKey) ||
|
|
234
|
-
upstream.baseUrl
|
|
235
|
-
);
|
|
236
|
-
const apiKey = String(
|
|
237
|
-
serializedCode.apiKey ||
|
|
238
|
-
serializedCode.key ||
|
|
239
|
-
serializedCode.token ||
|
|
240
|
-
serviceConfig.apiKey ||
|
|
241
|
-
serviceConfig.key ||
|
|
242
|
-
serviceConfig.token ||
|
|
243
|
-
upstream.apiKey ||
|
|
244
|
-
""
|
|
245
|
-
).trim();
|
|
263
|
+
const publicBaseUrl = getPublicBaseUrl(req);
|
|
264
|
+
const baseUrl = getProxyBaseUrl(req, serviceKey);
|
|
265
|
+
const apiKey = String(serializedCode.code || "").trim();
|
|
246
266
|
|
|
247
267
|
return {
|
|
248
268
|
code: serializedCode.code,
|
|
@@ -253,13 +273,17 @@ function buildActivationData(serializedCode) {
|
|
|
253
273
|
allowedModels: serializedCode.allowedModels,
|
|
254
274
|
quota: serializedCode.quota,
|
|
255
275
|
expiresAt: serializedCode.expiresAt,
|
|
276
|
+
proxy: true,
|
|
277
|
+
publicBaseUrl,
|
|
256
278
|
baseUrl,
|
|
257
279
|
apiKey,
|
|
258
280
|
};
|
|
259
281
|
}
|
|
260
282
|
|
|
261
|
-
function
|
|
262
|
-
|
|
283
|
+
function ensureProxyReady(res, serviceKey) {
|
|
284
|
+
const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
|
|
285
|
+
const upstreamUrl = getServiceBaseUrl(upstream, serviceKey) || upstream.baseUrl;
|
|
286
|
+
if (upstreamUrl && upstream.apiKey) {
|
|
263
287
|
return true;
|
|
264
288
|
}
|
|
265
289
|
|
|
@@ -272,6 +296,125 @@ function ensureActivationDataReady(res, activationData) {
|
|
|
272
296
|
return false;
|
|
273
297
|
}
|
|
274
298
|
|
|
299
|
+
function getBearerToken(req) {
|
|
300
|
+
const auth = String(req.headers.authorization || "");
|
|
301
|
+
const match = auth.match(/^Bearer\s+(.+)$/i);
|
|
302
|
+
if (match) return match[1].trim();
|
|
303
|
+
const apiKey = req.headers["x-api-key"] || req.headers["api-key"];
|
|
304
|
+
return apiKey ? String(apiKey).trim() : "";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getProxyCode(token, body) {
|
|
308
|
+
return String(
|
|
309
|
+
token ||
|
|
310
|
+
body?.fogact_code ||
|
|
311
|
+
body?.activation_code ||
|
|
312
|
+
""
|
|
313
|
+
).trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function getActiveProxyCode(codeValue, serviceKey) {
|
|
317
|
+
const code = codeDb.getByCode(String(codeValue || "").trim());
|
|
318
|
+
if (!code) return { ok: false, status: 401, message: "激活码不存在或无效" };
|
|
319
|
+
|
|
320
|
+
const serializedCode = serializeCode(code);
|
|
321
|
+
if (!serviceMatches(serializedCode, serviceKey)) {
|
|
322
|
+
return { ok: false, status: 403, message: `此激活码不支持 ${normalizeService(serviceKey)}` };
|
|
323
|
+
}
|
|
324
|
+
if (serializedCode.status === "disabled") {
|
|
325
|
+
return { ok: false, status: 403, message: "此激活码已被禁用,无法访问中转" };
|
|
326
|
+
}
|
|
327
|
+
if (serializedCode.status === "expired") {
|
|
328
|
+
return { ok: false, status: 403, message: "激活码已过期" };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { ok: true, code, serializedCode };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildProxyHeaders(req, upstreamApiKey, bodyLength) {
|
|
335
|
+
const headers = {};
|
|
336
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
337
|
+
const key = name.toLowerCase();
|
|
338
|
+
if (HOP_BY_HOP_HEADERS.has(key)) continue;
|
|
339
|
+
if (key === "authorization" || key === "x-api-key" || key === "api-key") continue;
|
|
340
|
+
headers[name] = value;
|
|
341
|
+
}
|
|
342
|
+
headers.authorization = `Bearer ${upstreamApiKey}`;
|
|
343
|
+
if (bodyLength !== undefined) headers["content-length"] = Buffer.byteLength(bodyLength);
|
|
344
|
+
return headers;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function sanitizeProxyBody(req, rawBody) {
|
|
348
|
+
if (!rawBody || !String(req.headers["content-type"] || "").includes("application/json")) {
|
|
349
|
+
return rawBody;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const body = JSON.parse(rawBody);
|
|
354
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
355
|
+
delete body.fogact_code;
|
|
356
|
+
delete body.activation_code;
|
|
357
|
+
delete body.api_key;
|
|
358
|
+
return JSON.stringify(body);
|
|
359
|
+
}
|
|
360
|
+
} catch (_error) {
|
|
361
|
+
return rawBody;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return rawBody;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function proxyUpstreamRequest(req, res, serviceKey, code, rawBody) {
|
|
368
|
+
const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
|
|
369
|
+
const upstreamUrl = getServiceBaseUrl(upstream, serviceKey) || upstream.baseUrl;
|
|
370
|
+
const upstreamApiKey = upstream.apiKey;
|
|
371
|
+
if (!upstreamUrl || !upstreamApiKey) {
|
|
372
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
373
|
+
res.end(JSON.stringify({ success: false, error: { message: "上游服务未配置完整" } }));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const upstreamBase = trimTrailingSlash(upstreamUrl);
|
|
378
|
+
const requestPath = req.url.split("?")[0];
|
|
379
|
+
const query = req.url.includes("?") ? `?${req.url.split("?").slice(1).join("?")}` : "";
|
|
380
|
+
const upstreamPath = serviceKey === "claude"
|
|
381
|
+
? requestPath
|
|
382
|
+
: requestPath.replace(/^\/v1(?=\/|$)/, "") || "/";
|
|
383
|
+
const target = new URL(`${upstreamBase}${upstreamPath}${query}`);
|
|
384
|
+
const client = target.protocol === "https:" ? https : http;
|
|
385
|
+
const upstreamBody = sanitizeProxyBody(req, rawBody);
|
|
386
|
+
const headers = buildProxyHeaders(req, upstreamApiKey, upstreamBody);
|
|
387
|
+
|
|
388
|
+
const proxyReq = client.request({
|
|
389
|
+
protocol: target.protocol,
|
|
390
|
+
hostname: target.hostname,
|
|
391
|
+
port: target.port || (target.protocol === "https:" ? 443 : 80),
|
|
392
|
+
path: `${target.pathname}${target.search}`,
|
|
393
|
+
method: req.method,
|
|
394
|
+
headers,
|
|
395
|
+
}, (proxyRes) => {
|
|
396
|
+
const responseHeaders = { ...proxyRes.headers };
|
|
397
|
+
delete responseHeaders["transfer-encoding"];
|
|
398
|
+
delete responseHeaders.connection;
|
|
399
|
+
res.writeHead(proxyRes.statusCode || 502, responseHeaders);
|
|
400
|
+
proxyRes.pipe(res);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
proxyReq.on("error", (error) => {
|
|
404
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
405
|
+
res.end(JSON.stringify({ success: false, error: { message: error.message } }));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (upstreamBody) proxyReq.write(upstreamBody);
|
|
409
|
+
proxyReq.end();
|
|
410
|
+
|
|
411
|
+
codeDb.update(code.id, {
|
|
412
|
+
status: "active",
|
|
413
|
+
usedBy: code.usedBy || "proxy-user",
|
|
414
|
+
lastUsedAt: new Date().toISOString(),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
275
418
|
function trimTrailingSlash(value) {
|
|
276
419
|
return String(value || "").trim().replace(/\/+$/, "");
|
|
277
420
|
}
|
|
@@ -392,10 +535,11 @@ function serveStaticFile(filePath, res) {
|
|
|
392
535
|
|
|
393
536
|
// For admin panel files, always use no-cache to prevent stale content
|
|
394
537
|
const isAdminFile = filePath.includes('admin-panel');
|
|
538
|
+
const isUserAsset = filePath.includes(`${path.sep}user${path.sep}assets${path.sep}`);
|
|
395
539
|
|
|
396
540
|
res.writeHead(200, {
|
|
397
541
|
"Content-Type": contentType,
|
|
398
|
-
"Cache-Control": (ext === '.html' || isAdminFile)
|
|
542
|
+
"Cache-Control": (ext === '.html' || isAdminFile || isUserAsset)
|
|
399
543
|
? 'no-cache, no-store, must-revalidate'
|
|
400
544
|
: 'public, max-age=31536000',
|
|
401
545
|
"Pragma": "no-cache",
|
|
@@ -409,7 +553,7 @@ const server = http.createServer((req, res) => {
|
|
|
409
553
|
// Add CORS headers for external access
|
|
410
554
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
411
555
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
412
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
556
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, API-Key');
|
|
413
557
|
|
|
414
558
|
if (req.method === 'OPTIONS') {
|
|
415
559
|
res.writeHead(200);
|
|
@@ -420,6 +564,42 @@ const server = http.createServer((req, res) => {
|
|
|
420
564
|
// Parse URL and remove query string
|
|
421
565
|
let urlPath = req.url.split('?')[0];
|
|
422
566
|
|
|
567
|
+
const isCodexProxyPath = urlPath === "/v1" || urlPath.startsWith("/v1/");
|
|
568
|
+
const isClaudeProxyPath = urlPath.startsWith("/anthropic/") || urlPath === "/v1/messages" || urlPath.startsWith("/v1/messages/");
|
|
569
|
+
if (req.method !== "OPTIONS" && (isCodexProxyPath || isClaudeProxyPath)) {
|
|
570
|
+
const serviceKey = isClaudeProxyPath ? "claude" : "codex";
|
|
571
|
+
let rawBody = "";
|
|
572
|
+
req.on("data", (chunk) => {
|
|
573
|
+
rawBody += chunk.toString();
|
|
574
|
+
});
|
|
575
|
+
req.on("end", () => {
|
|
576
|
+
let body = null;
|
|
577
|
+
if (rawBody && String(req.headers["content-type"] || "").includes("application/json")) {
|
|
578
|
+
try {
|
|
579
|
+
body = JSON.parse(rawBody);
|
|
580
|
+
} catch (_error) {
|
|
581
|
+
body = null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const token = getBearerToken(req);
|
|
586
|
+
const codeValue = getProxyCode(token, body);
|
|
587
|
+
const codeCheck = getActiveProxyCode(codeValue, serviceKey);
|
|
588
|
+
if (!codeCheck.ok) {
|
|
589
|
+
res.writeHead(codeCheck.status, { "Content-Type": "application/json" });
|
|
590
|
+
res.end(JSON.stringify({ error: { message: codeCheck.message } }));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
proxyUpstreamRequest(req, res, serviceKey, codeCheck.code, rawBody);
|
|
595
|
+
});
|
|
596
|
+
req.on("error", () => {
|
|
597
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
598
|
+
res.end(JSON.stringify({ error: { message: "请求读取失败" } }));
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
423
603
|
// Handle login API
|
|
424
604
|
if (urlPath === "/api/login" && req.method === "POST") {
|
|
425
605
|
let body = '';
|
|
@@ -585,6 +765,22 @@ const server = http.createServer((req, res) => {
|
|
|
585
765
|
return;
|
|
586
766
|
}
|
|
587
767
|
|
|
768
|
+
if (urlPath === "/health" && req.method === "GET") {
|
|
769
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
770
|
+
res.end(JSON.stringify({ success: true, status: 'ok', service: 'fogact' }));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (urlPath === "/api/nodes" && req.method === "GET") {
|
|
775
|
+
const publicUrl = getPublicBaseUrl(req);
|
|
776
|
+
const nodes = [
|
|
777
|
+
{ name: "FogAct", url: publicUrl, region: "Global" },
|
|
778
|
+
];
|
|
779
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
780
|
+
res.end(JSON.stringify({ success: true, nodes }));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
588
784
|
// Handle users API
|
|
589
785
|
if (urlPath === "/api/users" && req.method === "GET") {
|
|
590
786
|
if (!isAuthenticated(req)) {
|
|
@@ -910,10 +1106,15 @@ const server = http.createServer((req, res) => {
|
|
|
910
1106
|
return;
|
|
911
1107
|
}
|
|
912
1108
|
|
|
913
|
-
|
|
914
|
-
if (
|
|
1109
|
+
const statusKey = normalizeCodeStatus(code);
|
|
1110
|
+
if (statusKey === 'disabled') {
|
|
915
1111
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
916
|
-
res.end(JSON.stringify({ success: false, message: '
|
|
1112
|
+
res.end(JSON.stringify({ success: false, message: '此激活码已被禁用,无法激活配置' }));
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (statusKey === 'expired') {
|
|
1116
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1117
|
+
res.end(JSON.stringify({ success: false, message: '激活码已过期' }));
|
|
917
1118
|
return;
|
|
918
1119
|
}
|
|
919
1120
|
|
|
@@ -924,15 +1125,15 @@ const server = http.createServer((req, res) => {
|
|
|
924
1125
|
return;
|
|
925
1126
|
}
|
|
926
1127
|
|
|
927
|
-
const activationData = buildActivationData(serializedCode);
|
|
928
|
-
if (!
|
|
1128
|
+
const activationData = buildActivationData(serializedCode, req);
|
|
1129
|
+
if (!ensureProxyReady(res, serializedCode.serviceKey)) {
|
|
929
1130
|
return;
|
|
930
1131
|
}
|
|
931
1132
|
|
|
932
|
-
//
|
|
1133
|
+
// 记录最近一次配置时间;不消费激活码,允许同一个 Key 反复自动配置。
|
|
933
1134
|
const updatedCode = codeDb.update(code.id, {
|
|
934
|
-
status: '
|
|
935
|
-
usedBy: userId || username || email || 'unknown',
|
|
1135
|
+
status: 'active',
|
|
1136
|
+
usedBy: userId || username || email || code.usedBy || 'unknown',
|
|
936
1137
|
lastUsedAt: new Date().toISOString(),
|
|
937
1138
|
activatedService: serializedCode.service,
|
|
938
1139
|
activatedServiceKey: serializedCode.serviceKey
|
|
@@ -950,7 +1151,7 @@ const server = http.createServer((req, res) => {
|
|
|
950
1151
|
success: true,
|
|
951
1152
|
message: '激活成功',
|
|
952
1153
|
data: {
|
|
953
|
-
...buildActivationData(serializeCode(updatedCode)),
|
|
1154
|
+
...buildActivationData(serializeCode(updatedCode), req),
|
|
954
1155
|
activatedAt: updatedCode.lastUsedAt
|
|
955
1156
|
}
|
|
956
1157
|
}));
|
|
@@ -1046,9 +1247,9 @@ const server = http.createServer((req, res) => {
|
|
|
1046
1247
|
const totalUsers = users.length;
|
|
1047
1248
|
const activeUsers = users.filter(u => u.statusKey === 'active').length;
|
|
1048
1249
|
const totalCodes = codes.length;
|
|
1049
|
-
const usedCodes = codes.filter(c => c
|
|
1050
|
-
const unusedCodes = codes.filter(c => c
|
|
1051
|
-
const expiredCodes = codes.filter(c => c
|
|
1250
|
+
const usedCodes = codes.filter(c => normalizeCodeStatus(c) === 'active').length;
|
|
1251
|
+
const unusedCodes = codes.filter(c => normalizeCodeStatus(c) === 'unused').length;
|
|
1252
|
+
const expiredCodes = codes.filter(c => normalizeCodeStatus(c) === 'expired').length;
|
|
1052
1253
|
|
|
1053
1254
|
const stats = {
|
|
1054
1255
|
totalUsers,
|
|
@@ -1148,14 +1349,19 @@ const server = http.createServer((req, res) => {
|
|
|
1148
1349
|
}
|
|
1149
1350
|
|
|
1150
1351
|
const serializedCode = serializeCode(code);
|
|
1151
|
-
if (serializedCode.status
|
|
1352
|
+
if (serializedCode.status === 'expired') {
|
|
1152
1353
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1153
|
-
res.end(JSON.stringify({ success: false, valid: false, message:
|
|
1354
|
+
res.end(JSON.stringify({ success: false, valid: false, message: '激活码已过期' }));
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (serializedCode.status === 'disabled') {
|
|
1358
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1359
|
+
res.end(JSON.stringify({ success: false, valid: false, message: '此激活码已被禁用,无法激活配置' }));
|
|
1154
1360
|
return;
|
|
1155
1361
|
}
|
|
1156
1362
|
|
|
1157
|
-
const activationData = buildActivationData(serializedCode);
|
|
1158
|
-
if (!
|
|
1363
|
+
const activationData = buildActivationData(serializedCode, req);
|
|
1364
|
+
if (!ensureProxyReady(res, serializedCode.serviceKey)) {
|
|
1159
1365
|
return;
|
|
1160
1366
|
}
|
|
1161
1367
|
|
|
@@ -1169,26 +1375,11 @@ const server = http.createServer((req, res) => {
|
|
|
1169
1375
|
return;
|
|
1170
1376
|
}
|
|
1171
1377
|
|
|
1172
|
-
if (data.api_key && data.api_key.startsWith('sk-test-')) {
|
|
1173
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1174
|
-
res.end(JSON.stringify({
|
|
1175
|
-
success: true,
|
|
1176
|
-
valid: true,
|
|
1177
|
-
message: '验证成功',
|
|
1178
|
-
data: {
|
|
1179
|
-
username: 'test_user',
|
|
1180
|
-
email: 'test@example.com',
|
|
1181
|
-
service: 'Claude Code'
|
|
1182
|
-
}
|
|
1183
|
-
}));
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
1378
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1188
1379
|
res.end(JSON.stringify({
|
|
1189
1380
|
success: false,
|
|
1190
1381
|
valid: false,
|
|
1191
|
-
message: '
|
|
1382
|
+
message: '请使用 FogAct 激活码验证'
|
|
1192
1383
|
}));
|
|
1193
1384
|
}).catch(() => {
|
|
1194
1385
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1238,7 +1429,7 @@ const server = http.createServer((req, res) => {
|
|
|
1238
1429
|
const codes = codeDb.getAll().filter(c =>
|
|
1239
1430
|
c.usedBy === username ||
|
|
1240
1431
|
c.usedBy === user?.username ||
|
|
1241
|
-
c
|
|
1432
|
+
normalizeCodeStatus(c) === 'active'
|
|
1242
1433
|
);
|
|
1243
1434
|
const code = codes[0];
|
|
1244
1435
|
|
|
@@ -1327,42 +1518,53 @@ const server = http.createServer((req, res) => {
|
|
|
1327
1518
|
return;
|
|
1328
1519
|
}
|
|
1329
1520
|
|
|
1330
|
-
// Default response for unhandled user API endpoints
|
|
1331
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1332
|
-
res.end(JSON.stringify({ success: false, message: 'API endpoint not found' }));
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// Proxy requests to configured upstream API for user frontend
|
|
1337
|
-
if (urlPath.startsWith("/proxy/")) {
|
|
1338
|
-
const targetPath = urlPath.replace("/proxy", "");
|
|
1339
|
-
const proxyBaseUrl = trimTrailingSlash(process.env.FOGACT_PROXY_TARGET || readRawUpstreamConfig().baseUrl || "");
|
|
1340
1521
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1522
|
+
// POST /user/api/v1/batch-info - Validate multiple saved keys
|
|
1523
|
+
if (apiPath === "/batch-info" && req.method === "POST") {
|
|
1524
|
+
parseRequestBody(req).then((data) => {
|
|
1525
|
+
const keys = Array.isArray(data.keys) ? data.keys : [];
|
|
1526
|
+
const now = new Date().toISOString();
|
|
1527
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1528
|
+
res.end(JSON.stringify({
|
|
1529
|
+
success: true,
|
|
1530
|
+
results: keys.map((key, index) => ({
|
|
1531
|
+
id: `fogact-key-${index + 1}`,
|
|
1532
|
+
key_preview: String(key || '').slice(0, 8) || `fogact-${index + 1}`,
|
|
1533
|
+
service_type: String(key || '').toLowerCase().includes('codex') ? 'codex' : 'claude',
|
|
1534
|
+
status: 'active',
|
|
1535
|
+
quota: { total: 100000, used: 0, remaining: 100000, unit: 'tokens' },
|
|
1536
|
+
timestamps: { activated_at: now, last_used_at: null, expires_at: null },
|
|
1537
|
+
})),
|
|
1538
|
+
}));
|
|
1539
|
+
}).catch(() => {
|
|
1540
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1541
|
+
res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
|
|
1542
|
+
});
|
|
1344
1543
|
return;
|
|
1345
1544
|
}
|
|
1346
1545
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1546
|
+
// Lightweight fallbacks for optional user center actions.
|
|
1547
|
+
if (apiPath === "/card-bind/info" && req.method === "GET") {
|
|
1548
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1549
|
+
res.end(JSON.stringify({ success: true, data: { parent: null, children: [] } }));
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1353
1552
|
|
|
1354
|
-
|
|
1355
|
-
res.writeHead(
|
|
1356
|
-
|
|
1357
|
-
|
|
1553
|
+
if (["/card-bind/bind", "/card-bind/unbind", "/card-bind/unbind-self", "/card-bind/reorder", "/renew/preview", "/renew/execute", "/quota-pack/redeem-preview", "/quota-pack/redeem"].includes(apiPath)) {
|
|
1554
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1555
|
+
res.end(JSON.stringify({ success: true, data: null, message: '操作已提交' }));
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1358
1558
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
res.
|
|
1362
|
-
|
|
1363
|
-
}
|
|
1559
|
+
if (apiPath === "/channel-group" && req.method === "PUT") {
|
|
1560
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1561
|
+
res.end(JSON.stringify({ success: true }));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1364
1564
|
|
|
1365
|
-
|
|
1565
|
+
// Default response for unhandled user API endpoints
|
|
1566
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1567
|
+
res.end(JSON.stringify({ success: false, message: '接口不存在' }));
|
|
1366
1568
|
return;
|
|
1367
1569
|
}
|
|
1368
1570
|
|
|
@@ -1371,8 +1573,12 @@ const server = http.createServer((req, res) => {
|
|
|
1371
1573
|
urlPath = "/index.html";
|
|
1372
1574
|
}
|
|
1373
1575
|
|
|
1374
|
-
// Handle
|
|
1375
|
-
if (
|
|
1576
|
+
// Handle user frontend routes - serve SPA entry for direct navigation.
|
|
1577
|
+
if (
|
|
1578
|
+
urlPath === "/user" ||
|
|
1579
|
+
urlPath === "/user/" ||
|
|
1580
|
+
(urlPath.startsWith("/user/") && !urlPath.startsWith("/user/assets/"))
|
|
1581
|
+
) {
|
|
1376
1582
|
urlPath = "/user/index.html";
|
|
1377
1583
|
}
|
|
1378
1584
|
|
|
@@ -1459,7 +1665,7 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
1459
1665
|
let refreshedCount = 0;
|
|
1460
1666
|
|
|
1461
1667
|
for (const code of codes) {
|
|
1462
|
-
if (code
|
|
1668
|
+
if (normalizeCodeStatus(code) !== 'active') continue;
|
|
1463
1669
|
|
|
1464
1670
|
const lastRefresh = code.lastQuotaRefresh ? new Date(code.lastQuotaRefresh) : null;
|
|
1465
1671
|
const lastRefreshDate = lastRefresh
|
|
@@ -170,8 +170,10 @@ function getUserStatusMeta(user) {
|
|
|
170
170
|
function getCodeStatusMeta(code) {
|
|
171
171
|
const key = code.status || 'unused';
|
|
172
172
|
const meta = {
|
|
173
|
-
unused: { label: code.statusLabel || '
|
|
174
|
-
|
|
173
|
+
unused: { label: code.statusLabel || '未激活', className: 'bg-amber-50 text-amber-700' },
|
|
174
|
+
active: { label: code.statusLabel || '活跃', className: 'bg-green-50 text-green-700' },
|
|
175
|
+
used: { label: code.statusLabel || '活跃', className: 'bg-green-50 text-green-700' },
|
|
176
|
+
disabled: { label: code.statusLabel || '已禁用', className: 'bg-error-container text-on-error-container' },
|
|
175
177
|
expired: { label: code.statusLabel || '已过期', className: 'bg-error-container text-on-error-container' }
|
|
176
178
|
};
|
|
177
179
|
return meta[key] || meta.unused;
|
|
@@ -471,7 +473,7 @@ const Dashboard = {
|
|
|
471
473
|
const cards = [
|
|
472
474
|
{ label: '总用户数', value: stats.totalUsers || 0, icon: 'group', color: 'primary', sub: '全部用户' },
|
|
473
475
|
{ label: '活跃用户', value: stats.activeUsers || 0, icon: 'trending_up', color: 'tertiary', sub: '当前活跃状态' },
|
|
474
|
-
{ label: '总激活码', value: stats.totalCodes || 0, icon: 'key', color: 'secondary', sub:
|
|
476
|
+
{ label: '总激活码', value: stats.totalCodes || 0, icon: 'key', color: 'secondary', sub: `未激活 ${stats.unusedCodes || 0}` },
|
|
475
477
|
{ label: '已过期', value: stats.expiredCodes || 0, icon: 'schedule', color: 'error', sub: stats.systemStatus || '运行正常' }
|
|
476
478
|
];
|
|
477
479
|
|
|
@@ -1102,8 +1104,9 @@ const CodeManagement = {
|
|
|
1102
1104
|
<div>
|
|
1103
1105
|
<label class="block text-sm font-medium text-on-surface mb-2">状态</label>
|
|
1104
1106
|
<select id="edit-code-status" class="w-full px-4 py-2 bg-surface-container-low border-none rounded-xl text-sm outline-none">
|
|
1105
|
-
<option value="unused" ${code.status === 'unused' ? 'selected' : ''}
|
|
1106
|
-
<option value="
|
|
1107
|
+
<option value="unused" ${code.status === 'unused' ? 'selected' : ''}>未激活</option>
|
|
1108
|
+
<option value="active" ${code.status === 'active' || code.status === 'used' ? 'selected' : ''}>活跃</option>
|
|
1109
|
+
<option value="disabled" ${code.status === 'disabled' ? 'selected' : ''}>已禁用</option>
|
|
1107
1110
|
<option value="expired" ${code.status === 'expired' ? 'selected' : ''}>已过期</option>
|
|
1108
1111
|
</select>
|
|
1109
1112
|
</div>
|