fogact 1.2.0 → 1.2.3
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 +829 -230
- package/frontend/admin/admin-panel-v2.js +8 -5
- package/frontend/user/assets/AnnouncementDetail-Dvxmwz0A-fogact-live.js +12 -0
- package/frontend/user/assets/Announcements-CS1tF2mx-fogact-live.js +11 -0
- package/frontend/user/assets/CardBind-CsCxihhP-fogact-live.js +21 -0
- package/frontend/user/assets/CardContent.vue_vue_type_script_setup_true_lang-D2L-uqSl-fogact-live.js +1 -0
- package/frontend/user/assets/CardDescription.vue_vue_type_script_setup_true_lang-D-v5Pl7F-fogact-live.js +1 -0
- package/frontend/user/assets/CardTitle.vue_vue_type_script_setup_true_lang-a0CCN6D5-fogact-live.js +1 -0
- package/frontend/user/assets/Dashboard-rPsmltm5-fogact-live.js +51 -0
- package/frontend/user/assets/Dashboard-rPsmltm5.js +1 -1
- package/frontend/user/assets/DashboardLayout-DDkxHYFj-fogact-live.js +80 -0
- package/frontend/user/assets/DashboardLayout-DDkxHYFj.js +2 -2
- package/frontend/user/assets/Input.vue_vue_type_script_setup_true_lang-B0SyPmYb-fogact-live.js +6 -0
- package/frontend/user/assets/Label.vue_vue_type_script_setup_true_lang-CxYORSgN-fogact-live.js +1 -0
- package/frontend/user/assets/Progress.vue_vue_type_script_setup_true_lang-2_QbPsEQ-fogact-live.js +1 -0
- package/frontend/user/assets/QuotaPack-B_tJ7Psm-fogact-live.js +6 -0
- package/frontend/user/assets/Renewal-BSDhDmwv-fogact-live.js +6 -0
- package/frontend/user/assets/ScrollArea.vue_vue_type_script_setup_true_lang-DMYwcfpz-fogact-live.js +1 -0
- package/frontend/user/assets/Separator.vue_vue_type_script_setup_true_lang-Ckg8EXj_-fogact-live.js +1 -0
- package/frontend/user/assets/Settings-CBdAa3lw-fogact-live.js +11 -0
- package/frontend/user/assets/TooltipTrigger.vue_vue_type_script_setup_true_lang-DtSBjzGo-fogact-live.js +16 -0
- package/frontend/user/assets/Welcome-Dtfp6oER-fogact-live.js +1 -0
- package/frontend/user/assets/Welcome-Dtfp6oER.js +1 -1
- package/frontend/user/assets/_plugin-vue_export-helper-5cjT4u0R-fogact-live.js +16 -0
- package/frontend/user/assets/activity-wYWtyqTJ-fogact-live.js +6 -0
- package/frontend/user/assets/announcement-35mOnjRL-fogact-live.js +16 -0
- package/frontend/user/assets/announcement-35mOnjRL.js +1 -1
- package/frontend/user/assets/calendar-BFNuCata-fogact-live.js +6 -0
- package/frontend/user/assets/chevron-down-kDbuU1Py-fogact-live.js +6 -0
- package/frontend/user/assets/chevron-right-BayASIm0-fogact-live.js +6 -0
- package/frontend/user/assets/eye-CY62vip0-fogact-live.js +6 -0
- package/frontend/user/assets/gauge-C5NQ-mV8-fogact-live.js +6 -0
- package/frontend/user/assets/index-Da98HOxL.js +3 -3
- package/frontend/user/assets/index-fogact-live.js +91 -0
- package/frontend/user/assets/link-2-DT5R5nGO-fogact-live.js +6 -0
- package/frontend/user/assets/package-rUbExUEn-fogact-live.js +6 -0
- package/frontend/user/assets/plus-CQc6C8wG-fogact-live.js +11 -0
- package/frontend/user/assets/refresh-cw-Y9hCloPL-fogact-live.js +6 -0
- package/frontend/user/assets/useUserPageRefresh-BYZvpNR9-fogact-live.js +1 -0
- package/frontend/user/assets/zap-l5zbZqrM-fogact-live.js +11 -0
- 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 +109 -29
- package/lib/platforms/opencode.js +1 -1
- package/lib/services/activation-orchestrator.js +30 -166
- package/lib/services/database.js +47 -0
- package/lib/services/fogact-api.js +12 -9
- package/package.json +1 -1
package/bin/web-server.js
CHANGED
|
@@ -5,7 +5,7 @@ const https = require("https");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const os = require("os");
|
|
8
|
-
const { userDb, codeDb, initializeSampleData } = require("../lib/services/database");
|
|
8
|
+
const { userDb, codeDb, usageDb, initializeSampleData } = require("../lib/services/database");
|
|
9
9
|
const { DEFAULT_CONFIG_PATH, getServiceBaseUrl, loadUpstreamConfig } = require("../lib/config/upstream");
|
|
10
10
|
const { readJsonFile, writeJsonFile } = require("../lib/utils/json-file");
|
|
11
11
|
const { maskKey, verifyNewApiKey } = require("../lib/services/newapi");
|
|
@@ -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,626 @@ 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 getRequestCode(req) {
|
|
317
|
+
return String(getBearerToken(req) || "").trim();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function findCodeByRequest(req) {
|
|
321
|
+
const token = getRequestCode(req);
|
|
322
|
+
return token ? codeDb.getByCode(token) : null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getActiveProxyCode(codeValue, serviceKey) {
|
|
326
|
+
const code = codeDb.getByCode(String(codeValue || "").trim());
|
|
327
|
+
if (!code) return { ok: false, status: 401, message: "激活码不存在或无效" };
|
|
328
|
+
|
|
329
|
+
const serializedCode = serializeCode(code);
|
|
330
|
+
if (!serviceMatches(serializedCode, serviceKey)) {
|
|
331
|
+
return { ok: false, status: 403, message: `此激活码不支持 ${normalizeService(serviceKey)}` };
|
|
332
|
+
}
|
|
333
|
+
if (serializedCode.status === "disabled") {
|
|
334
|
+
return { ok: false, status: 403, message: "此激活码已被禁用,无法访问中转" };
|
|
335
|
+
}
|
|
336
|
+
if (serializedCode.status === "expired") {
|
|
337
|
+
return { ok: false, status: 403, message: "激活码已过期" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { ok: true, code, serializedCode };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function getRemainingQuota(code) {
|
|
344
|
+
const quota = code?.quota || {};
|
|
345
|
+
const total = Number(quota.total || 0);
|
|
346
|
+
const loggedUsed = aggregateUsageItems(getUsageItemsForCode(code)).tokens;
|
|
347
|
+
const used = Math.max(loggedUsed, Number(quota.used || 0));
|
|
348
|
+
if (!Number.isFinite(total) || total <= 0) return Infinity;
|
|
349
|
+
return Math.max(total - (Number.isFinite(used) ? used : 0), 0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getRemainingDailyQuota(code) {
|
|
353
|
+
const quota = code?.quota || {};
|
|
354
|
+
const dailyLimit = Number(quota.dailyLimit || quota.daily || 0);
|
|
355
|
+
const todayItems = getUsageItemsForCode(code).filter((item) => String(item.createdAt || '').startsWith(getTodayKey()));
|
|
356
|
+
const todayStats = code?.usage?.dailyStats?.[getTodayKey()];
|
|
357
|
+
const dailyUsed = aggregateUsageItems(todayItems).tokens || Number(todayStats?.tokens || 0);
|
|
358
|
+
if (!Number.isFinite(dailyLimit) || dailyLimit <= 0) return Infinity;
|
|
359
|
+
return Math.max(dailyLimit - (Number.isFinite(dailyUsed) ? dailyUsed : 0), 0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function ensureQuotaAvailable(res, code) {
|
|
363
|
+
if (getRemainingQuota(code) <= 0) {
|
|
364
|
+
res.writeHead(402, { "Content-Type": "application/json" });
|
|
365
|
+
res.end(JSON.stringify({ error: { message: "额度已用尽,请续费或更换可用激活码" } }));
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (getRemainingDailyQuota(code) <= 0) {
|
|
370
|
+
res.writeHead(429, { "Content-Type": "application/json" });
|
|
371
|
+
res.end(JSON.stringify({ error: { message: "今日额度已用尽,请明日再试或联系管理员调整额度" } }));
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getPeriodBuckets(period = "7d") {
|
|
379
|
+
const normalized = String(period || "7d").toLowerCase();
|
|
380
|
+
const hourly = normalized === "24h";
|
|
381
|
+
const bucketCount = hourly ? 24 : Math.max(1, Math.min(90, parseInt(normalized, 10) || 7));
|
|
382
|
+
const now = new Date();
|
|
383
|
+
const buckets = [];
|
|
384
|
+
|
|
385
|
+
for (let index = bucketCount - 1; index >= 0; index -= 1) {
|
|
386
|
+
const date = new Date(now);
|
|
387
|
+
if (hourly) {
|
|
388
|
+
date.setMinutes(0, 0, 0);
|
|
389
|
+
date.setHours(date.getHours() - index);
|
|
390
|
+
buckets.push(date.toISOString().slice(0, 13));
|
|
391
|
+
} else {
|
|
392
|
+
date.setDate(date.getDate() - index);
|
|
393
|
+
buckets.push(date.toISOString().slice(0, 10));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { period: normalized, hourly, buckets };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function getUsageBucketKey(createdAt, hourly) {
|
|
401
|
+
const value = String(createdAt || "");
|
|
402
|
+
return hourly ? value.slice(0, 13) : value.slice(0, 10);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseJsonBody(body) {
|
|
406
|
+
if (!body) return null;
|
|
407
|
+
try {
|
|
408
|
+
return JSON.parse(body);
|
|
409
|
+
} catch (_error) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function extractRequestModel(rawBody) {
|
|
415
|
+
const body = parseJsonBody(rawBody);
|
|
416
|
+
return String(body?.model || body?.model_name || "unknown");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function extractUsageFromResponse(data) {
|
|
420
|
+
const usage = data?.usage || data?.message?.usage || data?.response?.usage || data?.data?.usage || null;
|
|
421
|
+
if (!usage || typeof usage !== "object") {
|
|
422
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0 };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const inputTokens = Number(
|
|
426
|
+
usage.prompt_tokens ??
|
|
427
|
+
usage.input_tokens ??
|
|
428
|
+
usage.inputTokens ??
|
|
429
|
+
usage.promptTokens ??
|
|
430
|
+
0
|
|
431
|
+
);
|
|
432
|
+
const outputTokens = Number(
|
|
433
|
+
usage.completion_tokens ??
|
|
434
|
+
usage.output_tokens ??
|
|
435
|
+
usage.outputTokens ??
|
|
436
|
+
usage.completionTokens ??
|
|
437
|
+
0
|
|
438
|
+
);
|
|
439
|
+
const explicitTotal = Number(
|
|
440
|
+
usage.total_tokens ??
|
|
441
|
+
usage.totalTokens ??
|
|
442
|
+
usage.tokens ??
|
|
443
|
+
0
|
|
444
|
+
);
|
|
445
|
+
const totalTokens = explicitTotal > 0
|
|
446
|
+
? explicitTotal
|
|
447
|
+
: Math.max(inputTokens, 0) + Math.max(outputTokens, 0);
|
|
448
|
+
const cost = Number(usage.cost ?? usage.total_cost ?? usage.quota ?? usage.used_quota ?? 0);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
inputTokens: Number.isFinite(inputTokens) ? Math.max(inputTokens, 0) : 0,
|
|
452
|
+
outputTokens: Number.isFinite(outputTokens) ? Math.max(outputTokens, 0) : 0,
|
|
453
|
+
totalTokens: Number.isFinite(totalTokens) ? Math.max(totalTokens, 0) : 0,
|
|
454
|
+
cost: Number.isFinite(cost) ? Math.max(cost, 0) : 0,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function extractModelFromResponse(data, fallbackModel) {
|
|
459
|
+
return String(data?.model || data?.message?.model || data?.response?.model || data?.data?.model || fallbackModel || "unknown");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function parseResponseUsagePayloads(responseBody) {
|
|
463
|
+
const json = parseJsonBody(responseBody);
|
|
464
|
+
if (json) return [json];
|
|
465
|
+
|
|
466
|
+
return String(responseBody || "")
|
|
467
|
+
.split(/\r?\n/)
|
|
468
|
+
.map((line) => line.trim())
|
|
469
|
+
.filter((line) => line.startsWith("data:"))
|
|
470
|
+
.map((line) => line.slice(5).trim())
|
|
471
|
+
.filter((line) => line && line !== "[DONE]")
|
|
472
|
+
.map((line) => parseJsonBody(line))
|
|
473
|
+
.filter(Boolean);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function summarizeResponseUsage(responseBody, fallbackModel) {
|
|
477
|
+
const payloads = parseResponseUsagePayloads(responseBody);
|
|
478
|
+
let model = fallbackModel || "unknown";
|
|
479
|
+
const totals = { inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0 };
|
|
480
|
+
|
|
481
|
+
for (const payload of payloads) {
|
|
482
|
+
model = extractModelFromResponse(payload, model);
|
|
483
|
+
const usage = extractUsageFromResponse(payload);
|
|
484
|
+
totals.inputTokens = Math.max(totals.inputTokens, usage.inputTokens);
|
|
485
|
+
totals.outputTokens = Math.max(totals.outputTokens, usage.outputTokens);
|
|
486
|
+
totals.totalTokens = Math.max(totals.totalTokens, usage.totalTokens);
|
|
487
|
+
totals.cost = Math.max(totals.cost, usage.cost);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (totals.totalTokens <= 0) {
|
|
491
|
+
totals.totalTokens = totals.inputTokens + totals.outputTokens;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { model, usage: totals };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function settleProxyUsage(code, serializedCode, serviceKey, requestPath, statusCode, rawBody, responseBody) {
|
|
498
|
+
const requestModel = extractRequestModel(rawBody);
|
|
499
|
+
const summary = summarizeResponseUsage(responseBody, requestModel);
|
|
500
|
+
const model = summary.model;
|
|
501
|
+
const usageTokens = summary.usage;
|
|
502
|
+
const success = statusCode >= 200 && statusCode < 300;
|
|
503
|
+
const now = new Date().toISOString();
|
|
504
|
+
|
|
505
|
+
usageDb.create({
|
|
506
|
+
codeId: code.id,
|
|
507
|
+
code: code.code,
|
|
508
|
+
service: serviceKey,
|
|
509
|
+
model,
|
|
510
|
+
inputTokens: usageTokens.inputTokens,
|
|
511
|
+
outputTokens: usageTokens.outputTokens,
|
|
512
|
+
totalTokens: usageTokens.totalTokens,
|
|
513
|
+
cost: usageTokens.cost,
|
|
514
|
+
statusCode,
|
|
515
|
+
success,
|
|
516
|
+
path: requestPath,
|
|
517
|
+
createdAt: now,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const current = codeDb.getById(code.id) || code;
|
|
521
|
+
const nextQuota = { ...(current.quota || {}) };
|
|
522
|
+
const currentUsage = current.usage || {};
|
|
523
|
+
const dailyStats = { ...(currentUsage.dailyStats || {}) };
|
|
524
|
+
const modelStats = { ...(currentUsage.modelStats || {}) };
|
|
525
|
+
const today = getTodayKey();
|
|
526
|
+
const quotaDelta = success ? usageTokens.totalTokens : 0;
|
|
527
|
+
|
|
528
|
+
if (quotaDelta > 0) {
|
|
529
|
+
const totalLoggedUsage = aggregateUsageItems(getUsageItemsForCode(current)).tokens;
|
|
530
|
+
nextQuota.used = Math.max(Number(nextQuota.used || 0) + quotaDelta, totalLoggedUsage);
|
|
531
|
+
nextQuota.dailyUsed = Number(nextQuota.dailyUsed || 0) + quotaDelta;
|
|
532
|
+
|
|
533
|
+
const day = dailyStats[today] || { requests: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
534
|
+
dailyStats[today] = {
|
|
535
|
+
requests: Number(day.requests || 0) + 1,
|
|
536
|
+
tokens: Number(day.tokens || 0) + quotaDelta,
|
|
537
|
+
inputTokens: Number(day.inputTokens || 0) + usageTokens.inputTokens,
|
|
538
|
+
outputTokens: Number(day.outputTokens || 0) + usageTokens.outputTokens,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const modelEntry = modelStats[model] || { requests: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
542
|
+
modelStats[model] = {
|
|
543
|
+
requests: Number(modelEntry.requests || 0) + 1,
|
|
544
|
+
tokens: Number(modelEntry.tokens || 0) + quotaDelta,
|
|
545
|
+
inputTokens: Number(modelEntry.inputTokens || 0) + usageTokens.inputTokens,
|
|
546
|
+
outputTokens: Number(modelEntry.outputTokens || 0) + usageTokens.outputTokens,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
codeDb.update(code.id, {
|
|
551
|
+
status: "active",
|
|
552
|
+
usedBy: current.usedBy || "proxy-user",
|
|
553
|
+
lastUsedAt: now,
|
|
554
|
+
activatedService: serializedCode.service,
|
|
555
|
+
activatedServiceKey: serializedCode.serviceKey,
|
|
556
|
+
quota: nextQuota,
|
|
557
|
+
usage: {
|
|
558
|
+
totalRequests: Number(currentUsage.totalRequests || 0) + (quotaDelta > 0 ? 1 : 0),
|
|
559
|
+
totalTokens: Number(currentUsage.totalTokens || 0) + quotaDelta,
|
|
560
|
+
inputTokens: Number(currentUsage.inputTokens || 0) + (quotaDelta > 0 ? usageTokens.inputTokens : 0),
|
|
561
|
+
outputTokens: Number(currentUsage.outputTokens || 0) + (quotaDelta > 0 ? usageTokens.outputTokens : 0),
|
|
562
|
+
lastModel: quotaDelta > 0 ? model : currentUsage.lastModel,
|
|
563
|
+
lastUsedAt: now,
|
|
564
|
+
dailyStats: trimUsageHistory(dailyStats),
|
|
565
|
+
modelStats,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function buildProxyHeaders(req, upstreamApiKey, bodyLength) {
|
|
571
|
+
const headers = {};
|
|
572
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
573
|
+
const key = name.toLowerCase();
|
|
574
|
+
if (HOP_BY_HOP_HEADERS.has(key)) continue;
|
|
575
|
+
if (key === "authorization" || key === "x-api-key" || key === "api-key") continue;
|
|
576
|
+
if (key === "accept-encoding") continue;
|
|
577
|
+
headers[name] = value;
|
|
578
|
+
}
|
|
579
|
+
headers.authorization = `Bearer ${upstreamApiKey}`;
|
|
580
|
+
headers["accept-encoding"] = "identity";
|
|
581
|
+
if (bodyLength !== undefined) headers["content-length"] = Buffer.byteLength(bodyLength);
|
|
582
|
+
return headers;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function sanitizeProxyBody(req, rawBody) {
|
|
586
|
+
if (!rawBody || !String(req.headers["content-type"] || "").includes("application/json")) {
|
|
587
|
+
return rawBody;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const body = JSON.parse(rawBody);
|
|
592
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
593
|
+
delete body.fogact_code;
|
|
594
|
+
delete body.activation_code;
|
|
595
|
+
delete body.api_key;
|
|
596
|
+
return JSON.stringify(body);
|
|
597
|
+
}
|
|
598
|
+
} catch (_error) {
|
|
599
|
+
return rawBody;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return rawBody;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function getTodayKey() {
|
|
606
|
+
return new Date().toISOString().slice(0, 10);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function trimUsageHistory(history, limit = 90) {
|
|
610
|
+
const entries = Object.entries(history || {}).sort(([a], [b]) => a.localeCompare(b));
|
|
611
|
+
return Object.fromEntries(entries.slice(Math.max(0, entries.length - limit)));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function getUsageItemsForCode(code) {
|
|
615
|
+
if (!code?.code) return [];
|
|
616
|
+
return usageDb.getByCode(code.code).filter((item) => item.success !== false && Number(item.totalTokens || 0) > 0);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function aggregateUsageItems(items) {
|
|
620
|
+
return items.reduce((totals, item) => {
|
|
621
|
+
totals.requests += Number(item.requests || 1);
|
|
622
|
+
totals.tokens += Number(item.totalTokens || 0);
|
|
623
|
+
totals.inputTokens += Number(item.inputTokens || 0);
|
|
624
|
+
totals.outputTokens += Number(item.outputTokens || 0);
|
|
625
|
+
totals.cost += Number(item.cost || 0);
|
|
626
|
+
return totals;
|
|
627
|
+
}, { requests: 0, tokens: 0, inputTokens: 0, outputTokens: 0, cost: 0 });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function emptyUsageBucket(date) {
|
|
631
|
+
return {
|
|
632
|
+
date,
|
|
633
|
+
requests: 0,
|
|
634
|
+
total_tokens: 0,
|
|
635
|
+
input_tokens: 0,
|
|
636
|
+
output_tokens: 0,
|
|
637
|
+
cache_read_tokens: 0,
|
|
638
|
+
cache_create_tokens: 0,
|
|
639
|
+
cost: 0,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function getUsageHistoryForPeriod(code, period = "7d") {
|
|
644
|
+
const { period: normalizedPeriod, hourly, buckets } = getPeriodBuckets(period);
|
|
645
|
+
const items = getUsageItemsForCode(code);
|
|
646
|
+
const byBucket = new Map(buckets.map((bucket) => [bucket, emptyUsageBucket(bucket)]));
|
|
647
|
+
const bucketSet = new Set(buckets);
|
|
648
|
+
|
|
649
|
+
for (const item of items) {
|
|
650
|
+
const bucket = getUsageBucketKey(item.createdAt, hourly);
|
|
651
|
+
if (!bucketSet.has(bucket)) continue;
|
|
652
|
+
const current = byBucket.get(bucket) || emptyUsageBucket(bucket);
|
|
653
|
+
current.requests += Number(item.requests || 1);
|
|
654
|
+
current.total_tokens += Number(item.totalTokens || 0);
|
|
655
|
+
current.input_tokens += Number(item.inputTokens || 0);
|
|
656
|
+
current.output_tokens += Number(item.outputTokens || 0);
|
|
657
|
+
current.cost += Number(item.cost || 0);
|
|
658
|
+
byBucket.set(bucket, current);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return { period: normalizedPeriod, data: buckets.map((bucket) => byBucket.get(bucket) || emptyUsageBucket(bucket)) };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function getCodeUsagePayload(code, period = "7d") {
|
|
665
|
+
const quota = code?.quota || {};
|
|
666
|
+
const items = getUsageItemsForCode(code);
|
|
667
|
+
const totals = aggregateUsageItems(items);
|
|
668
|
+
const today = getTodayKey();
|
|
669
|
+
const todayTotals = aggregateUsageItems(items.filter((item) => String(item.createdAt || '').startsWith(today)));
|
|
670
|
+
const totalQuota = Number(quota.total || 0) > 0 ? Number(quota.total) : 100000;
|
|
671
|
+
const history = getUsageHistoryForPeriod(code, period);
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
period: history.period,
|
|
675
|
+
data: history.data,
|
|
676
|
+
total_requests: totals.requests,
|
|
677
|
+
total_tokens: totals.tokens,
|
|
678
|
+
input_tokens: totals.inputTokens,
|
|
679
|
+
output_tokens: totals.outputTokens,
|
|
680
|
+
today_requests: todayTotals.requests,
|
|
681
|
+
today_tokens: todayTotals.tokens,
|
|
682
|
+
quota: {
|
|
683
|
+
total: totalQuota,
|
|
684
|
+
used: totals.tokens,
|
|
685
|
+
remaining: Math.max(0, totalQuota - totals.tokens),
|
|
686
|
+
daily_limit: Number(quota.dailyLimit || quota.daily || 0),
|
|
687
|
+
daily_used: todayTotals.tokens,
|
|
688
|
+
},
|
|
689
|
+
code_info: code ? {
|
|
690
|
+
code: code.code,
|
|
691
|
+
service: code.service,
|
|
692
|
+
expires_at: code.expiresAt,
|
|
693
|
+
status: code.status,
|
|
694
|
+
last_used_at: code.lastUsedAt || null,
|
|
695
|
+
} : null,
|
|
696
|
+
daily_stats: history.data,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function getCodeModelTrends(code, period = "7d") {
|
|
701
|
+
const { period: normalizedPeriod, hourly, buckets } = getPeriodBuckets(period);
|
|
702
|
+
const byBucket = new Map(buckets.map((bucket) => [bucket, new Map()]));
|
|
703
|
+
const bucketSet = new Set(buckets);
|
|
704
|
+
|
|
705
|
+
for (const item of getUsageItemsForCode(code)) {
|
|
706
|
+
const bucket = getUsageBucketKey(item.createdAt, hourly);
|
|
707
|
+
if (!bucketSet.has(bucket)) continue;
|
|
708
|
+
const model = item.model || "unknown";
|
|
709
|
+
const models = byBucket.get(bucket) || new Map();
|
|
710
|
+
const current = models.get(model) || { model, requests: 0, total_tokens: 0, input_tokens: 0, output_tokens: 0, cost: 0 };
|
|
711
|
+
current.requests += Number(item.requests || 1);
|
|
712
|
+
current.total_tokens += Number(item.totalTokens || 0);
|
|
713
|
+
current.input_tokens += Number(item.inputTokens || 0);
|
|
714
|
+
current.output_tokens += Number(item.outputTokens || 0);
|
|
715
|
+
current.cost += Number(item.cost || 0);
|
|
716
|
+
models.set(model, current);
|
|
717
|
+
byBucket.set(bucket, models);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
period: normalizedPeriod,
|
|
722
|
+
data: buckets.map((bucket) => ({
|
|
723
|
+
date: bucket,
|
|
724
|
+
models: [...(byBucket.get(bucket) || new Map()).values()].sort((a, b) => b.total_tokens - a.total_tokens),
|
|
725
|
+
})),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function getCodeUsageSummary(code) {
|
|
730
|
+
const items = getUsageItemsForCode(code);
|
|
731
|
+
const totals = aggregateUsageItems(items);
|
|
732
|
+
const todayTotals = aggregateUsageItems(items.filter((item) => String(item.createdAt || '').startsWith(getTodayKey())));
|
|
733
|
+
const legacyUsed = Number(code?.quota?.used || 0);
|
|
734
|
+
|
|
735
|
+
if (totals.tokens <= 0 && legacyUsed > 0) {
|
|
736
|
+
totals.tokens = legacyUsed;
|
|
737
|
+
totals.totalTokens = legacyUsed;
|
|
738
|
+
totals.requests = Number(code?.usage?.totalRequests || 0);
|
|
739
|
+
totals.inputTokens = Number(code?.usage?.inputTokens || 0);
|
|
740
|
+
totals.outputTokens = Number(code?.usage?.outputTokens || 0);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return { totals, todayTotals };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function serializeCodeForUserApi(code) {
|
|
747
|
+
if (!code) return null;
|
|
748
|
+
|
|
749
|
+
const serialized = serializeCode(code);
|
|
750
|
+
const { totals, todayTotals } = getCodeUsageSummary(code);
|
|
751
|
+
const quota = code.quota || {};
|
|
752
|
+
const totalQuota = Number(quota.total || 0) > 0 ? Number(quota.total) : 100000;
|
|
753
|
+
const usedQuota = totals.tokens;
|
|
754
|
+
const dailyLimit = Number(quota.dailyLimit || quota.daily || 0);
|
|
755
|
+
const status = serialized.status === "unused" ? "inactive" : serialized.status;
|
|
756
|
+
const activatedAt = code.activatedAt || (status === "active" ? code.createdAt : null);
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
id: code.id,
|
|
760
|
+
key_preview: maskKey(code.code),
|
|
761
|
+
service_type: serialized.serviceKey,
|
|
762
|
+
sub_service_type_name: serialized.category || "",
|
|
763
|
+
billing_type: "quota",
|
|
764
|
+
status,
|
|
765
|
+
status_label: status === "inactive" ? "未激活" : serialized.statusLabel,
|
|
766
|
+
is_bound: false,
|
|
767
|
+
channel_group_id: code.channelGroupId || null,
|
|
768
|
+
quota: {
|
|
769
|
+
total_quota: totalQuota,
|
|
770
|
+
used_quota: usedQuota,
|
|
771
|
+
remaining_quota: Math.max(0, totalQuota - usedQuota),
|
|
772
|
+
daily_quota: dailyLimit,
|
|
773
|
+
daily_spent: todayTotals.tokens,
|
|
774
|
+
daily_remaining: dailyLimit > 0 ? Math.max(0, dailyLimit - todayTotals.tokens) : 0,
|
|
775
|
+
reset_timezone: SERVER_TIMEZONE,
|
|
776
|
+
next_reset_at: getNextResetAt(),
|
|
777
|
+
},
|
|
778
|
+
usage: {
|
|
779
|
+
total_spent: totals.cost,
|
|
780
|
+
daily_spent: todayTotals.cost,
|
|
781
|
+
daily_total_spent: todayTotals.cost,
|
|
782
|
+
request_count: totals.requests,
|
|
783
|
+
daily_request_count: todayTotals.requests,
|
|
784
|
+
input_tokens: totals.inputTokens,
|
|
785
|
+
output_tokens: totals.outputTokens,
|
|
786
|
+
cache_read_tokens: 0,
|
|
787
|
+
cache_write_tokens: 0,
|
|
788
|
+
total_tokens: totals.tokens,
|
|
789
|
+
},
|
|
790
|
+
timestamps: {
|
|
791
|
+
activated_at: activatedAt,
|
|
792
|
+
last_used_at: code.lastUsedAt || null,
|
|
793
|
+
expires_at: code.expiresAt || null,
|
|
794
|
+
validity_days: code.validity?.days || null,
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function getNextResetAt() {
|
|
800
|
+
const next = new Date();
|
|
801
|
+
const serverTzDate = new Date(next.toLocaleString("en-US", { timeZone: SERVER_TIMEZONE }));
|
|
802
|
+
const serverMidnight = new Date(serverTzDate);
|
|
803
|
+
serverMidnight.setHours(24, 0, 0, 0);
|
|
804
|
+
const delay = serverMidnight.getTime() - serverTzDate.getTime();
|
|
805
|
+
return new Date(next.getTime() + Math.max(delay, 1000)).toISOString();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function resolveUserApiCode(req) {
|
|
809
|
+
const code = findCodeByRequest(req);
|
|
810
|
+
return code || null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function sendUserApiUnauthorized(res, message = "请先添加有效的 FogAct Key") {
|
|
814
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
815
|
+
res.end(JSON.stringify({ success: false, message }));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function getCodeUsageRank(code) {
|
|
819
|
+
const items = getUsageItemsForCode(code);
|
|
820
|
+
const totals = aggregateUsageItems(items);
|
|
821
|
+
const latestUsageAt = items.reduce((latest, item) => {
|
|
822
|
+
const createdAt = new Date(item.createdAt || 0).getTime();
|
|
823
|
+
return Number.isFinite(createdAt) ? Math.max(latest, createdAt) : latest;
|
|
824
|
+
}, 0);
|
|
825
|
+
const lastUsedAt = new Date(code?.lastUsedAt || 0).getTime();
|
|
826
|
+
|
|
827
|
+
return {
|
|
828
|
+
tokens: totals.tokens,
|
|
829
|
+
requests: totals.requests,
|
|
830
|
+
latestAt: Math.max(
|
|
831
|
+
Number.isFinite(latestUsageAt) ? latestUsageAt : 0,
|
|
832
|
+
Number.isFinite(lastUsedAt) ? lastUsedAt : 0
|
|
833
|
+
),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function sortCodesByUsageActivity(codes) {
|
|
838
|
+
return [...codes].sort((a, b) => {
|
|
839
|
+
const left = getCodeUsageRank(a);
|
|
840
|
+
const right = getCodeUsageRank(b);
|
|
841
|
+
if (right.tokens !== left.tokens) return right.tokens - left.tokens;
|
|
842
|
+
if (right.requests !== left.requests) return right.requests - left.requests;
|
|
843
|
+
return right.latestAt - left.latestAt;
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function findUsageCode(username, user) {
|
|
848
|
+
const codes = codeDb.getAll();
|
|
849
|
+
const requestedUser = String(username || '').trim();
|
|
850
|
+
const candidateValues = [requestedUser];
|
|
851
|
+
if (!requestedUser || user?.username === requestedUser) {
|
|
852
|
+
candidateValues.push(user?.username, user?.email, user?.id);
|
|
853
|
+
}
|
|
854
|
+
const candidates = new Set(candidateValues.filter(Boolean).map(String));
|
|
855
|
+
const matchedCodes = codes.filter((code) => candidates.has(String(code.usedBy || '')));
|
|
856
|
+
|
|
857
|
+
return sortCodesByUsageActivity(matchedCodes)[0] ||
|
|
858
|
+
sortCodesByUsageActivity(codes.filter((code) => normalizeCodeStatus(code) === 'active'))[0] ||
|
|
859
|
+
null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
function proxyUpstreamRequest(req, res, serviceKey, code, rawBody) {
|
|
864
|
+
const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
|
|
865
|
+
const upstreamUrl = getServiceBaseUrl(upstream, serviceKey) || upstream.baseUrl;
|
|
866
|
+
const upstreamApiKey = upstream.apiKey;
|
|
867
|
+
if (!upstreamUrl || !upstreamApiKey) {
|
|
868
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
869
|
+
res.end(JSON.stringify({ success: false, error: { message: "上游服务未配置完整" } }));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const upstreamBase = trimTrailingSlash(upstreamUrl);
|
|
874
|
+
const requestPath = req.url.split("?")[0];
|
|
875
|
+
const query = req.url.includes("?") ? `?${req.url.split("?").slice(1).join("?")}` : "";
|
|
876
|
+
const upstreamPath = serviceKey === "claude"
|
|
877
|
+
? requestPath
|
|
878
|
+
: requestPath.replace(/^\/v1(?=\/|$)/, "") || "/";
|
|
879
|
+
const target = new URL(`${upstreamBase}${upstreamPath}${query}`);
|
|
880
|
+
const client = target.protocol === "https:" ? https : http;
|
|
881
|
+
const upstreamBody = sanitizeProxyBody(req, rawBody);
|
|
882
|
+
const headers = buildProxyHeaders(req, upstreamApiKey, upstreamBody);
|
|
883
|
+
|
|
884
|
+
const proxyReq = client.request({
|
|
885
|
+
protocol: target.protocol,
|
|
886
|
+
hostname: target.hostname,
|
|
887
|
+
port: target.port || (target.protocol === "https:" ? 443 : 80),
|
|
888
|
+
path: `${target.pathname}${target.search}`,
|
|
889
|
+
method: req.method,
|
|
890
|
+
headers,
|
|
891
|
+
}, (proxyRes) => {
|
|
892
|
+
const responseHeaders = { ...proxyRes.headers };
|
|
893
|
+
delete responseHeaders["transfer-encoding"];
|
|
894
|
+
delete responseHeaders.connection;
|
|
895
|
+
res.writeHead(proxyRes.statusCode || 502, responseHeaders);
|
|
896
|
+
|
|
897
|
+
const chunks = [];
|
|
898
|
+
proxyRes.on("data", (chunk) => {
|
|
899
|
+
chunks.push(chunk);
|
|
900
|
+
res.write(chunk);
|
|
901
|
+
});
|
|
902
|
+
proxyRes.on("end", () => {
|
|
903
|
+
res.end();
|
|
904
|
+
const rawResponseBody = Buffer.concat(chunks).toString("utf8");
|
|
905
|
+
settleProxyUsage(code, code.serializedCode || serializeCode(code), serviceKey, requestPath, proxyRes.statusCode || 502, upstreamBody, rawResponseBody);
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
proxyReq.on("error", (error) => {
|
|
910
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
911
|
+
res.end(JSON.stringify({ success: false, error: { message: error.message } }));
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
if (upstreamBody) proxyReq.write(upstreamBody);
|
|
915
|
+
proxyReq.end();
|
|
916
|
+
|
|
917
|
+
}
|
|
918
|
+
|
|
275
919
|
function trimTrailingSlash(value) {
|
|
276
920
|
return String(value || "").trim().replace(/\/+$/, "");
|
|
277
921
|
}
|
|
@@ -392,10 +1036,11 @@ function serveStaticFile(filePath, res) {
|
|
|
392
1036
|
|
|
393
1037
|
// For admin panel files, always use no-cache to prevent stale content
|
|
394
1038
|
const isAdminFile = filePath.includes('admin-panel');
|
|
1039
|
+
const isUserAsset = filePath.includes(`${path.sep}user${path.sep}assets${path.sep}`);
|
|
395
1040
|
|
|
396
1041
|
res.writeHead(200, {
|
|
397
1042
|
"Content-Type": contentType,
|
|
398
|
-
"Cache-Control": (ext === '.html' || isAdminFile)
|
|
1043
|
+
"Cache-Control": (ext === '.html' || isAdminFile || isUserAsset)
|
|
399
1044
|
? 'no-cache, no-store, must-revalidate'
|
|
400
1045
|
: 'public, max-age=31536000',
|
|
401
1046
|
"Pragma": "no-cache",
|
|
@@ -409,7 +1054,7 @@ const server = http.createServer((req, res) => {
|
|
|
409
1054
|
// Add CORS headers for external access
|
|
410
1055
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
411
1056
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
412
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1057
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, API-Key');
|
|
413
1058
|
|
|
414
1059
|
if (req.method === 'OPTIONS') {
|
|
415
1060
|
res.writeHead(200);
|
|
@@ -420,6 +1065,46 @@ const server = http.createServer((req, res) => {
|
|
|
420
1065
|
// Parse URL and remove query string
|
|
421
1066
|
let urlPath = req.url.split('?')[0];
|
|
422
1067
|
|
|
1068
|
+
const isCodexProxyPath = urlPath === "/v1" || urlPath.startsWith("/v1/");
|
|
1069
|
+
const isClaudeProxyPath = urlPath.startsWith("/anthropic/") || urlPath === "/v1/messages" || urlPath.startsWith("/v1/messages/");
|
|
1070
|
+
if (req.method !== "OPTIONS" && (isCodexProxyPath || isClaudeProxyPath)) {
|
|
1071
|
+
const serviceKey = isClaudeProxyPath ? "claude" : "codex";
|
|
1072
|
+
let rawBody = "";
|
|
1073
|
+
req.on("data", (chunk) => {
|
|
1074
|
+
rawBody += chunk.toString();
|
|
1075
|
+
});
|
|
1076
|
+
req.on("end", () => {
|
|
1077
|
+
let body = null;
|
|
1078
|
+
if (rawBody && String(req.headers["content-type"] || "").includes("application/json")) {
|
|
1079
|
+
try {
|
|
1080
|
+
body = JSON.parse(rawBody);
|
|
1081
|
+
} catch (_error) {
|
|
1082
|
+
body = null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const token = getBearerToken(req);
|
|
1087
|
+
const codeValue = getProxyCode(token, body);
|
|
1088
|
+
const codeCheck = getActiveProxyCode(codeValue, serviceKey);
|
|
1089
|
+
if (!codeCheck.ok) {
|
|
1090
|
+
res.writeHead(codeCheck.status, { "Content-Type": "application/json" });
|
|
1091
|
+
res.end(JSON.stringify({ error: { message: codeCheck.message } }));
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (!ensureQuotaAvailable(res, codeCheck.code)) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
proxyUpstreamRequest(req, res, serviceKey, codeCheck.code, rawBody);
|
|
1100
|
+
});
|
|
1101
|
+
req.on("error", () => {
|
|
1102
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1103
|
+
res.end(JSON.stringify({ error: { message: "请求读取失败" } }));
|
|
1104
|
+
});
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
423
1108
|
// Handle login API
|
|
424
1109
|
if (urlPath === "/api/login" && req.method === "POST") {
|
|
425
1110
|
let body = '';
|
|
@@ -592,17 +1277,10 @@ const server = http.createServer((req, res) => {
|
|
|
592
1277
|
}
|
|
593
1278
|
|
|
594
1279
|
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(/\/+$/, "");
|
|
1280
|
+
const publicUrl = getPublicBaseUrl(req);
|
|
600
1281
|
const nodes = [
|
|
601
1282
|
{ name: "FogAct", url: publicUrl, region: "Global" },
|
|
602
1283
|
];
|
|
603
|
-
if (upstreamUrl) {
|
|
604
|
-
nodes.push({ name: service === "claude" ? "Claude Upstream" : "Codex Upstream", url: upstreamUrl, region: "Upstream" });
|
|
605
|
-
}
|
|
606
1284
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
607
1285
|
res.end(JSON.stringify({ success: true, nodes }));
|
|
608
1286
|
return;
|
|
@@ -933,10 +1611,15 @@ const server = http.createServer((req, res) => {
|
|
|
933
1611
|
return;
|
|
934
1612
|
}
|
|
935
1613
|
|
|
936
|
-
|
|
937
|
-
if (
|
|
1614
|
+
const statusKey = normalizeCodeStatus(code);
|
|
1615
|
+
if (statusKey === 'disabled') {
|
|
1616
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1617
|
+
res.end(JSON.stringify({ success: false, message: '此激活码已被禁用,无法激活配置' }));
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
if (statusKey === 'expired') {
|
|
938
1621
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
939
|
-
res.end(JSON.stringify({ success: false, message: '
|
|
1622
|
+
res.end(JSON.stringify({ success: false, message: '激活码已过期' }));
|
|
940
1623
|
return;
|
|
941
1624
|
}
|
|
942
1625
|
|
|
@@ -947,15 +1630,15 @@ const server = http.createServer((req, res) => {
|
|
|
947
1630
|
return;
|
|
948
1631
|
}
|
|
949
1632
|
|
|
950
|
-
const activationData = buildActivationData(serializedCode);
|
|
951
|
-
if (!
|
|
1633
|
+
const activationData = buildActivationData(serializedCode, req);
|
|
1634
|
+
if (!ensureProxyReady(res, serializedCode.serviceKey)) {
|
|
952
1635
|
return;
|
|
953
1636
|
}
|
|
954
1637
|
|
|
955
|
-
//
|
|
1638
|
+
// 记录最近一次配置时间;不消费激活码,允许同一个 Key 反复自动配置。
|
|
956
1639
|
const updatedCode = codeDb.update(code.id, {
|
|
957
|
-
status: '
|
|
958
|
-
usedBy: userId || username || email || 'unknown',
|
|
1640
|
+
status: 'active',
|
|
1641
|
+
usedBy: userId || username || email || code.usedBy || 'unknown',
|
|
959
1642
|
lastUsedAt: new Date().toISOString(),
|
|
960
1643
|
activatedService: serializedCode.service,
|
|
961
1644
|
activatedServiceKey: serializedCode.serviceKey
|
|
@@ -973,7 +1656,7 @@ const server = http.createServer((req, res) => {
|
|
|
973
1656
|
success: true,
|
|
974
1657
|
message: '激活成功',
|
|
975
1658
|
data: {
|
|
976
|
-
...buildActivationData(serializeCode(updatedCode)),
|
|
1659
|
+
...buildActivationData(serializeCode(updatedCode), req),
|
|
977
1660
|
activatedAt: updatedCode.lastUsedAt
|
|
978
1661
|
}
|
|
979
1662
|
}));
|
|
@@ -1029,11 +1712,11 @@ const server = http.createServer((req, res) => {
|
|
|
1029
1712
|
}
|
|
1030
1713
|
|
|
1031
1714
|
// 刷新额度
|
|
1032
|
-
const dailyQuota = code.quota?.daily || 100000;
|
|
1715
|
+
const dailyQuota = code.quota?.dailyLimit || code.quota?.daily || 100000;
|
|
1033
1716
|
const updatedCode = codeDb.update(codeId, {
|
|
1034
1717
|
quota: {
|
|
1035
1718
|
...code.quota,
|
|
1036
|
-
|
|
1719
|
+
dailyUsed: 0,
|
|
1037
1720
|
daily: dailyQuota
|
|
1038
1721
|
},
|
|
1039
1722
|
lastQuotaRefresh: now.toISOString()
|
|
@@ -1069,9 +1752,9 @@ const server = http.createServer((req, res) => {
|
|
|
1069
1752
|
const totalUsers = users.length;
|
|
1070
1753
|
const activeUsers = users.filter(u => u.statusKey === 'active').length;
|
|
1071
1754
|
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
|
|
1755
|
+
const usedCodes = codes.filter(c => normalizeCodeStatus(c) === 'active').length;
|
|
1756
|
+
const unusedCodes = codes.filter(c => normalizeCodeStatus(c) === 'unused').length;
|
|
1757
|
+
const expiredCodes = codes.filter(c => normalizeCodeStatus(c) === 'expired').length;
|
|
1075
1758
|
|
|
1076
1759
|
const stats = {
|
|
1077
1760
|
totalUsers,
|
|
@@ -1089,51 +1772,31 @@ const server = http.createServer((req, res) => {
|
|
|
1089
1772
|
return;
|
|
1090
1773
|
}
|
|
1091
1774
|
|
|
1092
|
-
//
|
|
1775
|
+
// Legacy user usage endpoint kept for older clients; backed by the real activation code usage log.
|
|
1093
1776
|
if (urlPath === "/api/user/usage" && req.method === "GET") {
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
total: 10000000,
|
|
1103
|
-
used: 2456789,
|
|
1104
|
-
remaining: 7543211
|
|
1105
|
-
},
|
|
1106
|
-
daily_stats: [
|
|
1107
|
-
{ date: '2026-03-30', requests: 234, tokens: 34567 },
|
|
1108
|
-
{ date: '2026-03-31', requests: 289, tokens: 42345 },
|
|
1109
|
-
{ date: '2026-04-01', requests: 312, tokens: 45678 },
|
|
1110
|
-
{ date: '2026-04-02', requests: 298, tokens: 43210 },
|
|
1111
|
-
{ date: '2026-04-03', requests: 276, tokens: 39876 },
|
|
1112
|
-
{ date: '2026-04-04', requests: 301, tokens: 44321 },
|
|
1113
|
-
{ date: '2026-04-05', requests: 342, tokens: 45678 }
|
|
1114
|
-
]
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1777
|
+
const code = resolveUserApiCode(req);
|
|
1778
|
+
if (!code) {
|
|
1779
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
1784
|
+
const period = url.searchParams.get("period") || "7d";
|
|
1117
1785
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1118
|
-
res.end(JSON.stringify(
|
|
1786
|
+
res.end(JSON.stringify({ success: true, data: getCodeUsagePayload(code, period) }));
|
|
1119
1787
|
return;
|
|
1120
1788
|
}
|
|
1121
1789
|
|
|
1122
|
-
//
|
|
1790
|
+
// Legacy user info endpoint kept for older clients; backed by the real activation code.
|
|
1123
1791
|
if (urlPath === "/api/user/info" && req.method === "GET") {
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
status: 'active',
|
|
1131
|
-
created_at: '2026-03-15T00:00:00.000Z',
|
|
1132
|
-
api_key: 'sk-test-' + Math.random().toString(36).substring(2, 15)
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1792
|
+
const data = serializeCodeForUserApi(resolveUserApiCode(req));
|
|
1793
|
+
if (!data) {
|
|
1794
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1135
1798
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1136
|
-
res.end(JSON.stringify(
|
|
1799
|
+
res.end(JSON.stringify({ success: true, data }));
|
|
1137
1800
|
return;
|
|
1138
1801
|
}
|
|
1139
1802
|
|
|
@@ -1171,14 +1834,19 @@ const server = http.createServer((req, res) => {
|
|
|
1171
1834
|
}
|
|
1172
1835
|
|
|
1173
1836
|
const serializedCode = serializeCode(code);
|
|
1174
|
-
if (serializedCode.status
|
|
1837
|
+
if (serializedCode.status === 'expired') {
|
|
1175
1838
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1176
|
-
res.end(JSON.stringify({ success: false, valid: false, message:
|
|
1839
|
+
res.end(JSON.stringify({ success: false, valid: false, message: '激活码已过期' }));
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
if (serializedCode.status === 'disabled') {
|
|
1843
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1844
|
+
res.end(JSON.stringify({ success: false, valid: false, message: '此激活码已被禁用,无法激活配置' }));
|
|
1177
1845
|
return;
|
|
1178
1846
|
}
|
|
1179
1847
|
|
|
1180
|
-
const activationData = buildActivationData(serializedCode);
|
|
1181
|
-
if (!
|
|
1848
|
+
const activationData = buildActivationData(serializedCode, req);
|
|
1849
|
+
if (!ensureProxyReady(res, serializedCode.serviceKey)) {
|
|
1182
1850
|
return;
|
|
1183
1851
|
}
|
|
1184
1852
|
|
|
@@ -1192,26 +1860,11 @@ const server = http.createServer((req, res) => {
|
|
|
1192
1860
|
return;
|
|
1193
1861
|
}
|
|
1194
1862
|
|
|
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
1863
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1211
1864
|
res.end(JSON.stringify({
|
|
1212
1865
|
success: false,
|
|
1213
1866
|
valid: false,
|
|
1214
|
-
message: '
|
|
1867
|
+
message: '请使用 FogAct 激活码验证'
|
|
1215
1868
|
}));
|
|
1216
1869
|
}).catch(() => {
|
|
1217
1870
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1226,107 +1879,46 @@ const server = http.createServer((req, res) => {
|
|
|
1226
1879
|
|
|
1227
1880
|
// GET /user/api/v1/me - Get current user info
|
|
1228
1881
|
if (apiPath === "/me" && req.method === "GET") {
|
|
1229
|
-
|
|
1230
|
-
const
|
|
1231
|
-
const user = userDb.getByUsername(username) || userDb.getAll()[0];
|
|
1882
|
+
const code = resolveUserApiCode(req);
|
|
1883
|
+
const data = serializeCodeForUserApi(code);
|
|
1232
1884
|
|
|
1233
|
-
if (!
|
|
1234
|
-
res
|
|
1235
|
-
res.end(JSON.stringify({ success: false, message: 'User not found' }));
|
|
1885
|
+
if (!data) {
|
|
1886
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1236
1887
|
return;
|
|
1237
1888
|
}
|
|
1238
1889
|
|
|
1239
1890
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1240
|
-
res.end(JSON.stringify(
|
|
1241
|
-
success: true,
|
|
1242
|
-
data: {
|
|
1243
|
-
id: user.id,
|
|
1244
|
-
username: user.username,
|
|
1245
|
-
email: user.email,
|
|
1246
|
-
service: user.service,
|
|
1247
|
-
status: user.status,
|
|
1248
|
-
created_at: user.registeredAt || user.createdAt,
|
|
1249
|
-
api_key: user.apiKey || 'sk-demo-' + user.id
|
|
1250
|
-
}
|
|
1251
|
-
}));
|
|
1891
|
+
res.end(JSON.stringify(data));
|
|
1252
1892
|
return;
|
|
1253
1893
|
}
|
|
1254
1894
|
|
|
1255
1895
|
// GET /user/api/v1/usage/history - Get usage history
|
|
1256
1896
|
if (apiPath.startsWith("/usage/history") && req.method === "GET") {
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const codes = codeDb.getAll().filter(c =>
|
|
1262
|
-
c.usedBy === username ||
|
|
1263
|
-
c.usedBy === user?.username ||
|
|
1264
|
-
c.status === 'used'
|
|
1265
|
-
);
|
|
1266
|
-
const code = codes[0];
|
|
1267
|
-
|
|
1268
|
-
// 计算真实额度数据
|
|
1269
|
-
const totalQuota = code?.quota?.total || 100000;
|
|
1270
|
-
const usedQuota = code?.quota?.used || 0;
|
|
1271
|
-
const dailyLimit = code?.quota?.dailyLimit || 5000;
|
|
1272
|
-
const dailyUsed = code?.quota?.dailyUsed || 0;
|
|
1273
|
-
|
|
1274
|
-
// 生成最近7天的模拟数据(实际生产中应该记录真实使用数据)
|
|
1275
|
-
const today = new Date();
|
|
1276
|
-
const dailyStats = [];
|
|
1277
|
-
for (let i = 6; i >= 0; i--) {
|
|
1278
|
-
const date = new Date(today);
|
|
1279
|
-
date.setDate(date.getDate() - i);
|
|
1280
|
-
const requests = Math.floor(Math.random() * 200) + 100;
|
|
1281
|
-
const tokens = requests * Math.floor(Math.random() * 150) + 100;
|
|
1282
|
-
dailyStats.push({
|
|
1283
|
-
date: date.toISOString().split('T')[0],
|
|
1284
|
-
requests,
|
|
1285
|
-
tokens
|
|
1286
|
-
});
|
|
1897
|
+
const code = resolveUserApiCode(req);
|
|
1898
|
+
if (!code) {
|
|
1899
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1900
|
+
return;
|
|
1287
1901
|
}
|
|
1288
1902
|
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
data: {
|
|
1292
|
-
total_requests: usedQuota,
|
|
1293
|
-
total_tokens: usedQuota * 12, // 估算 token 数量
|
|
1294
|
-
today_requests: dailyUsed,
|
|
1295
|
-
today_tokens: dailyUsed * 12,
|
|
1296
|
-
quota: {
|
|
1297
|
-
total: totalQuota,
|
|
1298
|
-
used: usedQuota,
|
|
1299
|
-
remaining: totalQuota - usedQuota,
|
|
1300
|
-
daily_limit: dailyLimit,
|
|
1301
|
-
daily_used: dailyUsed
|
|
1302
|
-
},
|
|
1303
|
-
code_info: code ? {
|
|
1304
|
-
code: code.code,
|
|
1305
|
-
service: code.service,
|
|
1306
|
-
expires_at: code.expiresAt,
|
|
1307
|
-
status: code.status
|
|
1308
|
-
} : null,
|
|
1309
|
-
daily_stats: dailyStats
|
|
1310
|
-
}
|
|
1311
|
-
};
|
|
1903
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
1904
|
+
const period = url.searchParams.get("period") || "7d";
|
|
1312
1905
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1313
|
-
res.end(JSON.stringify(
|
|
1906
|
+
res.end(JSON.stringify(getCodeUsagePayload(code, period)));
|
|
1314
1907
|
return;
|
|
1315
1908
|
}
|
|
1316
1909
|
|
|
1317
1910
|
// GET /user/api/v1/usage/model-trends - Get model usage trends
|
|
1318
1911
|
if (apiPath.startsWith("/usage/model-trends") && req.method === "GET") {
|
|
1912
|
+
const code = resolveUserApiCode(req);
|
|
1913
|
+
if (!code) {
|
|
1914
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
1919
|
+
const period = url.searchParams.get("period") || "7d";
|
|
1319
1920
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1320
|
-
res.end(JSON.stringify(
|
|
1321
|
-
success: true,
|
|
1322
|
-
data: {
|
|
1323
|
-
models: [
|
|
1324
|
-
{ name: 'claude-opus-4-6', requests: 8234, tokens: 1456789 },
|
|
1325
|
-
{ name: 'claude-sonnet-4-6', requests: 5000, tokens: 800000 },
|
|
1326
|
-
{ name: 'claude-haiku-4-5', requests: 2000, tokens: 200000 }
|
|
1327
|
-
]
|
|
1328
|
-
}
|
|
1329
|
-
}));
|
|
1921
|
+
res.end(JSON.stringify(getCodeModelTrends(code, period)));
|
|
1330
1922
|
return;
|
|
1331
1923
|
}
|
|
1332
1924
|
|
|
@@ -1350,42 +1942,45 @@ const server = http.createServer((req, res) => {
|
|
|
1350
1942
|
return;
|
|
1351
1943
|
}
|
|
1352
1944
|
|
|
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
1945
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1946
|
+
// POST /user/api/v1/batch-info - Validate multiple saved keys
|
|
1947
|
+
if (apiPath === "/batch-info" && req.method === "POST") {
|
|
1948
|
+
parseRequestBody(req).then((data) => {
|
|
1949
|
+
const keys = Array.isArray(data.keys) ? data.keys : [];
|
|
1950
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1951
|
+
res.end(JSON.stringify({
|
|
1952
|
+
success: true,
|
|
1953
|
+
results: keys.map((key) => serializeCodeForUserApi(codeDb.getByCode(String(key || '').trim()))),
|
|
1954
|
+
}));
|
|
1955
|
+
}).catch(() => {
|
|
1956
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1957
|
+
res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
|
|
1958
|
+
});
|
|
1367
1959
|
return;
|
|
1368
1960
|
}
|
|
1369
1961
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1962
|
+
// Lightweight fallbacks for optional user center actions.
|
|
1963
|
+
if (apiPath === "/card-bind/info" && req.method === "GET") {
|
|
1964
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1965
|
+
res.end(JSON.stringify({ success: true, data: { parent: null, children: [] } }));
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1376
1968
|
|
|
1377
|
-
|
|
1378
|
-
res.writeHead(
|
|
1379
|
-
|
|
1380
|
-
|
|
1969
|
+
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)) {
|
|
1970
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1971
|
+
res.end(JSON.stringify({ success: true, data: null, message: '操作已提交' }));
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1381
1974
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
res.
|
|
1385
|
-
|
|
1386
|
-
}
|
|
1975
|
+
if (apiPath === "/channel-group" && req.method === "PUT") {
|
|
1976
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1977
|
+
res.end(JSON.stringify({ success: true }));
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1387
1980
|
|
|
1388
|
-
|
|
1981
|
+
// Default response for unhandled user API endpoints
|
|
1982
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1983
|
+
res.end(JSON.stringify({ success: false, message: '接口不存在' }));
|
|
1389
1984
|
return;
|
|
1390
1985
|
}
|
|
1391
1986
|
|
|
@@ -1394,8 +1989,12 @@ const server = http.createServer((req, res) => {
|
|
|
1394
1989
|
urlPath = "/index.html";
|
|
1395
1990
|
}
|
|
1396
1991
|
|
|
1397
|
-
// Handle
|
|
1398
|
-
if (
|
|
1992
|
+
// Handle user frontend routes - serve SPA entry for direct navigation.
|
|
1993
|
+
if (
|
|
1994
|
+
urlPath === "/user" ||
|
|
1995
|
+
urlPath === "/user/" ||
|
|
1996
|
+
(urlPath.startsWith("/user/") && !urlPath.startsWith("/user/assets/"))
|
|
1997
|
+
) {
|
|
1399
1998
|
urlPath = "/user/index.html";
|
|
1400
1999
|
}
|
|
1401
2000
|
|
|
@@ -1482,7 +2081,7 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
1482
2081
|
let refreshedCount = 0;
|
|
1483
2082
|
|
|
1484
2083
|
for (const code of codes) {
|
|
1485
|
-
if (code
|
|
2084
|
+
if (normalizeCodeStatus(code) !== 'active') continue;
|
|
1486
2085
|
|
|
1487
2086
|
const lastRefresh = code.lastQuotaRefresh ? new Date(code.lastQuotaRefresh) : null;
|
|
1488
2087
|
const lastRefreshDate = lastRefresh
|
|
@@ -1490,12 +2089,12 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
1490
2089
|
: null;
|
|
1491
2090
|
if (lastRefreshDate && lastRefreshDate.toISOString().split('T')[0] === serverDate) continue;
|
|
1492
2091
|
|
|
1493
|
-
const dailyQuota = code.quota?.daily || 100000;
|
|
2092
|
+
const dailyQuota = code.quota?.daily || code.quota?.dailyLimit || 100000;
|
|
1494
2093
|
codeDb.update(code.id, {
|
|
1495
2094
|
quota: {
|
|
1496
2095
|
...code.quota,
|
|
1497
|
-
|
|
1498
|
-
|
|
2096
|
+
daily: dailyQuota,
|
|
2097
|
+
dailyUsed: 0
|
|
1499
2098
|
},
|
|
1500
2099
|
lastQuotaRefresh: new Date().toISOString()
|
|
1501
2100
|
});
|