fogact 1.2.0 → 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/bin/web-server.js +288 -105
- package/frontend/admin/admin-panel-v2.js +8 -5
- 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/config/claude.js +1 -0
- package/lib/config/codex.js +1 -1
- package/lib/config/upstream.js +3 -0
- package/lib/index.js +57 -17
- package/lib/platforms/opencode.js +1 -1
- package/lib/services/activation-orchestrator.js +30 -166
- package/lib/services/fogact-api.js +12 -9
- package/package.json +1 -1
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 = '';
|
|
@@ -592,17 +772,10 @@ const server = http.createServer((req, res) => {
|
|
|
592
772
|
}
|
|
593
773
|
|
|
594
774
|
if (urlPath === "/api/nodes" && req.method === "GET") {
|
|
595
|
-
const
|
|
596
|
-
const service = getServiceKey(url.searchParams.get("service") || "codex", "codex");
|
|
597
|
-
const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
|
|
598
|
-
const upstreamUrl = getServiceBaseUrl(upstream, service) || upstream.baseUrl;
|
|
599
|
-
const publicUrl = `https://${req.headers.host}`.replace(/\/+$/, "");
|
|
775
|
+
const publicUrl = getPublicBaseUrl(req);
|
|
600
776
|
const nodes = [
|
|
601
777
|
{ name: "FogAct", url: publicUrl, region: "Global" },
|
|
602
778
|
];
|
|
603
|
-
if (upstreamUrl) {
|
|
604
|
-
nodes.push({ name: service === "claude" ? "Claude Upstream" : "Codex Upstream", url: upstreamUrl, region: "Upstream" });
|
|
605
|
-
}
|
|
606
779
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
607
780
|
res.end(JSON.stringify({ success: true, nodes }));
|
|
608
781
|
return;
|
|
@@ -933,10 +1106,15 @@ const server = http.createServer((req, res) => {
|
|
|
933
1106
|
return;
|
|
934
1107
|
}
|
|
935
1108
|
|
|
936
|
-
|
|
937
|
-
if (
|
|
1109
|
+
const statusKey = normalizeCodeStatus(code);
|
|
1110
|
+
if (statusKey === 'disabled') {
|
|
938
1111
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
939
|
-
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: '激活码已过期' }));
|
|
940
1118
|
return;
|
|
941
1119
|
}
|
|
942
1120
|
|
|
@@ -947,15 +1125,15 @@ const server = http.createServer((req, res) => {
|
|
|
947
1125
|
return;
|
|
948
1126
|
}
|
|
949
1127
|
|
|
950
|
-
const activationData = buildActivationData(serializedCode);
|
|
951
|
-
if (!
|
|
1128
|
+
const activationData = buildActivationData(serializedCode, req);
|
|
1129
|
+
if (!ensureProxyReady(res, serializedCode.serviceKey)) {
|
|
952
1130
|
return;
|
|
953
1131
|
}
|
|
954
1132
|
|
|
955
|
-
//
|
|
1133
|
+
// 记录最近一次配置时间;不消费激活码,允许同一个 Key 反复自动配置。
|
|
956
1134
|
const updatedCode = codeDb.update(code.id, {
|
|
957
|
-
status: '
|
|
958
|
-
usedBy: userId || username || email || 'unknown',
|
|
1135
|
+
status: 'active',
|
|
1136
|
+
usedBy: userId || username || email || code.usedBy || 'unknown',
|
|
959
1137
|
lastUsedAt: new Date().toISOString(),
|
|
960
1138
|
activatedService: serializedCode.service,
|
|
961
1139
|
activatedServiceKey: serializedCode.serviceKey
|
|
@@ -973,7 +1151,7 @@ const server = http.createServer((req, res) => {
|
|
|
973
1151
|
success: true,
|
|
974
1152
|
message: '激活成功',
|
|
975
1153
|
data: {
|
|
976
|
-
...buildActivationData(serializeCode(updatedCode)),
|
|
1154
|
+
...buildActivationData(serializeCode(updatedCode), req),
|
|
977
1155
|
activatedAt: updatedCode.lastUsedAt
|
|
978
1156
|
}
|
|
979
1157
|
}));
|
|
@@ -1069,9 +1247,9 @@ const server = http.createServer((req, res) => {
|
|
|
1069
1247
|
const totalUsers = users.length;
|
|
1070
1248
|
const activeUsers = users.filter(u => u.statusKey === 'active').length;
|
|
1071
1249
|
const totalCodes = codes.length;
|
|
1072
|
-
const usedCodes = codes.filter(c => c
|
|
1073
|
-
const unusedCodes = codes.filter(c => c
|
|
1074
|
-
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;
|
|
1075
1253
|
|
|
1076
1254
|
const stats = {
|
|
1077
1255
|
totalUsers,
|
|
@@ -1171,14 +1349,19 @@ const server = http.createServer((req, res) => {
|
|
|
1171
1349
|
}
|
|
1172
1350
|
|
|
1173
1351
|
const serializedCode = serializeCode(code);
|
|
1174
|
-
if (serializedCode.status
|
|
1352
|
+
if (serializedCode.status === 'expired') {
|
|
1175
1353
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1176
|
-
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: '此激活码已被禁用,无法激活配置' }));
|
|
1177
1360
|
return;
|
|
1178
1361
|
}
|
|
1179
1362
|
|
|
1180
|
-
const activationData = buildActivationData(serializedCode);
|
|
1181
|
-
if (!
|
|
1363
|
+
const activationData = buildActivationData(serializedCode, req);
|
|
1364
|
+
if (!ensureProxyReady(res, serializedCode.serviceKey)) {
|
|
1182
1365
|
return;
|
|
1183
1366
|
}
|
|
1184
1367
|
|
|
@@ -1192,26 +1375,11 @@ const server = http.createServer((req, res) => {
|
|
|
1192
1375
|
return;
|
|
1193
1376
|
}
|
|
1194
1377
|
|
|
1195
|
-
if (data.api_key && data.api_key.startsWith('sk-test-')) {
|
|
1196
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1197
|
-
res.end(JSON.stringify({
|
|
1198
|
-
success: true,
|
|
1199
|
-
valid: true,
|
|
1200
|
-
message: '验证成功',
|
|
1201
|
-
data: {
|
|
1202
|
-
username: 'test_user',
|
|
1203
|
-
email: 'test@example.com',
|
|
1204
|
-
service: 'Claude Code'
|
|
1205
|
-
}
|
|
1206
|
-
}));
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
1378
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1211
1379
|
res.end(JSON.stringify({
|
|
1212
1380
|
success: false,
|
|
1213
1381
|
valid: false,
|
|
1214
|
-
message: '
|
|
1382
|
+
message: '请使用 FogAct 激活码验证'
|
|
1215
1383
|
}));
|
|
1216
1384
|
}).catch(() => {
|
|
1217
1385
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1261,7 +1429,7 @@ const server = http.createServer((req, res) => {
|
|
|
1261
1429
|
const codes = codeDb.getAll().filter(c =>
|
|
1262
1430
|
c.usedBy === username ||
|
|
1263
1431
|
c.usedBy === user?.username ||
|
|
1264
|
-
c
|
|
1432
|
+
normalizeCodeStatus(c) === 'active'
|
|
1265
1433
|
);
|
|
1266
1434
|
const code = codes[0];
|
|
1267
1435
|
|
|
@@ -1350,42 +1518,53 @@ const server = http.createServer((req, res) => {
|
|
|
1350
1518
|
return;
|
|
1351
1519
|
}
|
|
1352
1520
|
|
|
1353
|
-
// Default response for unhandled user API endpoints
|
|
1354
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1355
|
-
res.end(JSON.stringify({ success: false, message: 'API endpoint not found' }));
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
1521
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
+
});
|
|
1367
1543
|
return;
|
|
1368
1544
|
}
|
|
1369
1545
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
}
|
|
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
|
+
}
|
|
1376
1552
|
|
|
1377
|
-
|
|
1378
|
-
res.writeHead(
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
+
}
|
|
1381
1558
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
res.
|
|
1385
|
-
|
|
1386
|
-
}
|
|
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
|
+
}
|
|
1387
1564
|
|
|
1388
|
-
|
|
1565
|
+
// Default response for unhandled user API endpoints
|
|
1566
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1567
|
+
res.end(JSON.stringify({ success: false, message: '接口不存在' }));
|
|
1389
1568
|
return;
|
|
1390
1569
|
}
|
|
1391
1570
|
|
|
@@ -1394,8 +1573,12 @@ const server = http.createServer((req, res) => {
|
|
|
1394
1573
|
urlPath = "/index.html";
|
|
1395
1574
|
}
|
|
1396
1575
|
|
|
1397
|
-
// Handle
|
|
1398
|
-
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
|
+
) {
|
|
1399
1582
|
urlPath = "/user/index.html";
|
|
1400
1583
|
}
|
|
1401
1584
|
|
|
@@ -1482,7 +1665,7 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
1482
1665
|
let refreshedCount = 0;
|
|
1483
1666
|
|
|
1484
1667
|
for (const code of codes) {
|
|
1485
|
-
if (code
|
|
1668
|
+
if (normalizeCodeStatus(code) !== 'active') continue;
|
|
1486
1669
|
|
|
1487
1670
|
const lastRefresh = code.lastQuotaRefresh ? new Date(code.lastQuotaRefresh) : null;
|
|
1488
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>
|