fogact 1.2.2 → 1.2.4
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 +557 -141
- package/config/upstream.example.json +1 -1
- 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/DashboardLayout-DDkxHYFj-fogact-live.js +80 -0
- 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/_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/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-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 +1 -1
- package/lib/index.js +91 -13
- package/lib/services/database.js +47 -0
- 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");
|
|
@@ -313,6 +313,15 @@ function getProxyCode(token, body) {
|
|
|
313
313
|
).trim();
|
|
314
314
|
}
|
|
315
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
|
+
|
|
316
325
|
function getActiveProxyCode(codeValue, serviceKey) {
|
|
317
326
|
const code = codeDb.getByCode(String(codeValue || "").trim());
|
|
318
327
|
if (!code) return { ok: false, status: 401, message: "激活码不存在或无效" };
|
|
@@ -331,15 +340,244 @@ function getActiveProxyCode(codeValue, serviceKey) {
|
|
|
331
340
|
return { ok: true, code, serializedCode };
|
|
332
341
|
}
|
|
333
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
|
+
|
|
334
570
|
function buildProxyHeaders(req, upstreamApiKey, bodyLength) {
|
|
335
571
|
const headers = {};
|
|
336
572
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
337
573
|
const key = name.toLowerCase();
|
|
338
574
|
if (HOP_BY_HOP_HEADERS.has(key)) continue;
|
|
339
575
|
if (key === "authorization" || key === "x-api-key" || key === "api-key") continue;
|
|
576
|
+
if (key === "accept-encoding") continue;
|
|
340
577
|
headers[name] = value;
|
|
341
578
|
}
|
|
342
579
|
headers.authorization = `Bearer ${upstreamApiKey}`;
|
|
580
|
+
headers["accept-encoding"] = "identity";
|
|
343
581
|
if (bodyLength !== undefined) headers["content-length"] = Buffer.byteLength(bodyLength);
|
|
344
582
|
return headers;
|
|
345
583
|
}
|
|
@@ -364,6 +602,264 @@ function sanitizeProxyBody(req, rawBody) {
|
|
|
364
602
|
return rawBody;
|
|
365
603
|
}
|
|
366
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
|
+
|
|
367
863
|
function proxyUpstreamRequest(req, res, serviceKey, code, rawBody) {
|
|
368
864
|
const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
|
|
369
865
|
const upstreamUrl = getServiceBaseUrl(upstream, serviceKey) || upstream.baseUrl;
|
|
@@ -397,7 +893,17 @@ function proxyUpstreamRequest(req, res, serviceKey, code, rawBody) {
|
|
|
397
893
|
delete responseHeaders["transfer-encoding"];
|
|
398
894
|
delete responseHeaders.connection;
|
|
399
895
|
res.writeHead(proxyRes.statusCode || 502, responseHeaders);
|
|
400
|
-
|
|
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
|
+
});
|
|
401
907
|
});
|
|
402
908
|
|
|
403
909
|
proxyReq.on("error", (error) => {
|
|
@@ -408,11 +914,6 @@ function proxyUpstreamRequest(req, res, serviceKey, code, rawBody) {
|
|
|
408
914
|
if (upstreamBody) proxyReq.write(upstreamBody);
|
|
409
915
|
proxyReq.end();
|
|
410
916
|
|
|
411
|
-
codeDb.update(code.id, {
|
|
412
|
-
status: "active",
|
|
413
|
-
usedBy: code.usedBy || "proxy-user",
|
|
414
|
-
lastUsedAt: new Date().toISOString(),
|
|
415
|
-
});
|
|
416
917
|
}
|
|
417
918
|
|
|
418
919
|
function trimTrailingSlash(value) {
|
|
@@ -591,6 +1092,10 @@ const server = http.createServer((req, res) => {
|
|
|
591
1092
|
return;
|
|
592
1093
|
}
|
|
593
1094
|
|
|
1095
|
+
if (!ensureQuotaAvailable(res, codeCheck.code)) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
594
1099
|
proxyUpstreamRequest(req, res, serviceKey, codeCheck.code, rawBody);
|
|
595
1100
|
});
|
|
596
1101
|
req.on("error", () => {
|
|
@@ -1207,11 +1712,11 @@ const server = http.createServer((req, res) => {
|
|
|
1207
1712
|
}
|
|
1208
1713
|
|
|
1209
1714
|
// 刷新额度
|
|
1210
|
-
const dailyQuota = code.quota?.daily || 100000;
|
|
1715
|
+
const dailyQuota = code.quota?.dailyLimit || code.quota?.daily || 100000;
|
|
1211
1716
|
const updatedCode = codeDb.update(codeId, {
|
|
1212
1717
|
quota: {
|
|
1213
1718
|
...code.quota,
|
|
1214
|
-
|
|
1719
|
+
dailyUsed: 0,
|
|
1215
1720
|
daily: dailyQuota
|
|
1216
1721
|
},
|
|
1217
1722
|
lastQuotaRefresh: now.toISOString()
|
|
@@ -1267,51 +1772,31 @@ const server = http.createServer((req, res) => {
|
|
|
1267
1772
|
return;
|
|
1268
1773
|
}
|
|
1269
1774
|
|
|
1270
|
-
//
|
|
1775
|
+
// Legacy user usage endpoint kept for older clients; backed by the real activation code usage log.
|
|
1271
1776
|
if (urlPath === "/api/user/usage" && req.method === "GET") {
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
total: 10000000,
|
|
1281
|
-
used: 2456789,
|
|
1282
|
-
remaining: 7543211
|
|
1283
|
-
},
|
|
1284
|
-
daily_stats: [
|
|
1285
|
-
{ date: '2026-03-30', requests: 234, tokens: 34567 },
|
|
1286
|
-
{ date: '2026-03-31', requests: 289, tokens: 42345 },
|
|
1287
|
-
{ date: '2026-04-01', requests: 312, tokens: 45678 },
|
|
1288
|
-
{ date: '2026-04-02', requests: 298, tokens: 43210 },
|
|
1289
|
-
{ date: '2026-04-03', requests: 276, tokens: 39876 },
|
|
1290
|
-
{ date: '2026-04-04', requests: 301, tokens: 44321 },
|
|
1291
|
-
{ date: '2026-04-05', requests: 342, tokens: 45678 }
|
|
1292
|
-
]
|
|
1293
|
-
}
|
|
1294
|
-
};
|
|
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";
|
|
1295
1785
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1296
|
-
res.end(JSON.stringify(
|
|
1786
|
+
res.end(JSON.stringify({ success: true, data: getCodeUsagePayload(code, period) }));
|
|
1297
1787
|
return;
|
|
1298
1788
|
}
|
|
1299
1789
|
|
|
1300
|
-
//
|
|
1790
|
+
// Legacy user info endpoint kept for older clients; backed by the real activation code.
|
|
1301
1791
|
if (urlPath === "/api/user/info" && req.method === "GET") {
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
status: 'active',
|
|
1309
|
-
created_at: '2026-03-15T00:00:00.000Z',
|
|
1310
|
-
api_key: 'sk-test-' + Math.random().toString(36).substring(2, 15)
|
|
1311
|
-
}
|
|
1312
|
-
};
|
|
1792
|
+
const data = serializeCodeForUserApi(resolveUserApiCode(req));
|
|
1793
|
+
if (!data) {
|
|
1794
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1313
1798
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1314
|
-
res.end(JSON.stringify(
|
|
1799
|
+
res.end(JSON.stringify({ success: true, data }));
|
|
1315
1800
|
return;
|
|
1316
1801
|
}
|
|
1317
1802
|
|
|
@@ -1394,107 +1879,46 @@ const server = http.createServer((req, res) => {
|
|
|
1394
1879
|
|
|
1395
1880
|
// GET /user/api/v1/me - Get current user info
|
|
1396
1881
|
if (apiPath === "/me" && req.method === "GET") {
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
const user = userDb.getByUsername(username) || userDb.getAll()[0];
|
|
1882
|
+
const code = resolveUserApiCode(req);
|
|
1883
|
+
const data = serializeCodeForUserApi(code);
|
|
1400
1884
|
|
|
1401
|
-
if (!
|
|
1402
|
-
res
|
|
1403
|
-
res.end(JSON.stringify({ success: false, message: 'User not found' }));
|
|
1885
|
+
if (!data) {
|
|
1886
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1404
1887
|
return;
|
|
1405
1888
|
}
|
|
1406
1889
|
|
|
1407
1890
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1408
|
-
res.end(JSON.stringify(
|
|
1409
|
-
success: true,
|
|
1410
|
-
data: {
|
|
1411
|
-
id: user.id,
|
|
1412
|
-
username: user.username,
|
|
1413
|
-
email: user.email,
|
|
1414
|
-
service: user.service,
|
|
1415
|
-
status: user.status,
|
|
1416
|
-
created_at: user.registeredAt || user.createdAt,
|
|
1417
|
-
api_key: user.apiKey || 'sk-demo-' + user.id
|
|
1418
|
-
}
|
|
1419
|
-
}));
|
|
1891
|
+
res.end(JSON.stringify(data));
|
|
1420
1892
|
return;
|
|
1421
1893
|
}
|
|
1422
1894
|
|
|
1423
1895
|
// GET /user/api/v1/usage/history - Get usage history
|
|
1424
1896
|
if (apiPath.startsWith("/usage/history") && req.method === "GET") {
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const codes = codeDb.getAll().filter(c =>
|
|
1430
|
-
c.usedBy === username ||
|
|
1431
|
-
c.usedBy === user?.username ||
|
|
1432
|
-
normalizeCodeStatus(c) === 'active'
|
|
1433
|
-
);
|
|
1434
|
-
const code = codes[0];
|
|
1435
|
-
|
|
1436
|
-
// 计算真实额度数据
|
|
1437
|
-
const totalQuota = code?.quota?.total || 100000;
|
|
1438
|
-
const usedQuota = code?.quota?.used || 0;
|
|
1439
|
-
const dailyLimit = code?.quota?.dailyLimit || 5000;
|
|
1440
|
-
const dailyUsed = code?.quota?.dailyUsed || 0;
|
|
1441
|
-
|
|
1442
|
-
// 生成最近7天的模拟数据(实际生产中应该记录真实使用数据)
|
|
1443
|
-
const today = new Date();
|
|
1444
|
-
const dailyStats = [];
|
|
1445
|
-
for (let i = 6; i >= 0; i--) {
|
|
1446
|
-
const date = new Date(today);
|
|
1447
|
-
date.setDate(date.getDate() - i);
|
|
1448
|
-
const requests = Math.floor(Math.random() * 200) + 100;
|
|
1449
|
-
const tokens = requests * Math.floor(Math.random() * 150) + 100;
|
|
1450
|
-
dailyStats.push({
|
|
1451
|
-
date: date.toISOString().split('T')[0],
|
|
1452
|
-
requests,
|
|
1453
|
-
tokens
|
|
1454
|
-
});
|
|
1897
|
+
const code = resolveUserApiCode(req);
|
|
1898
|
+
if (!code) {
|
|
1899
|
+
sendUserApiUnauthorized(res, "FogAct Key 不存在或已失效");
|
|
1900
|
+
return;
|
|
1455
1901
|
}
|
|
1456
1902
|
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
data: {
|
|
1460
|
-
total_requests: usedQuota,
|
|
1461
|
-
total_tokens: usedQuota * 12, // 估算 token 数量
|
|
1462
|
-
today_requests: dailyUsed,
|
|
1463
|
-
today_tokens: dailyUsed * 12,
|
|
1464
|
-
quota: {
|
|
1465
|
-
total: totalQuota,
|
|
1466
|
-
used: usedQuota,
|
|
1467
|
-
remaining: totalQuota - usedQuota,
|
|
1468
|
-
daily_limit: dailyLimit,
|
|
1469
|
-
daily_used: dailyUsed
|
|
1470
|
-
},
|
|
1471
|
-
code_info: code ? {
|
|
1472
|
-
code: code.code,
|
|
1473
|
-
service: code.service,
|
|
1474
|
-
expires_at: code.expiresAt,
|
|
1475
|
-
status: code.status
|
|
1476
|
-
} : null,
|
|
1477
|
-
daily_stats: dailyStats
|
|
1478
|
-
}
|
|
1479
|
-
};
|
|
1903
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
1904
|
+
const period = url.searchParams.get("period") || "7d";
|
|
1480
1905
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1481
|
-
res.end(JSON.stringify(
|
|
1906
|
+
res.end(JSON.stringify(getCodeUsagePayload(code, period)));
|
|
1482
1907
|
return;
|
|
1483
1908
|
}
|
|
1484
1909
|
|
|
1485
1910
|
// GET /user/api/v1/usage/model-trends - Get model usage trends
|
|
1486
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";
|
|
1487
1920
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1488
|
-
res.end(JSON.stringify(
|
|
1489
|
-
success: true,
|
|
1490
|
-
data: {
|
|
1491
|
-
models: [
|
|
1492
|
-
{ name: 'claude-opus-4-6', requests: 8234, tokens: 1456789 },
|
|
1493
|
-
{ name: 'claude-sonnet-4-6', requests: 5000, tokens: 800000 },
|
|
1494
|
-
{ name: 'claude-haiku-4-5', requests: 2000, tokens: 200000 }
|
|
1495
|
-
]
|
|
1496
|
-
}
|
|
1497
|
-
}));
|
|
1921
|
+
res.end(JSON.stringify(getCodeModelTrends(code, period)));
|
|
1498
1922
|
return;
|
|
1499
1923
|
}
|
|
1500
1924
|
|
|
@@ -1523,18 +1947,10 @@ const server = http.createServer((req, res) => {
|
|
|
1523
1947
|
if (apiPath === "/batch-info" && req.method === "POST") {
|
|
1524
1948
|
parseRequestBody(req).then((data) => {
|
|
1525
1949
|
const keys = Array.isArray(data.keys) ? data.keys : [];
|
|
1526
|
-
const now = new Date().toISOString();
|
|
1527
1950
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1528
1951
|
res.end(JSON.stringify({
|
|
1529
1952
|
success: true,
|
|
1530
|
-
results: keys.map((key
|
|
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
|
-
})),
|
|
1953
|
+
results: keys.map((key) => serializeCodeForUserApi(codeDb.getByCode(String(key || '').trim()))),
|
|
1538
1954
|
}));
|
|
1539
1955
|
}).catch(() => {
|
|
1540
1956
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1673,12 +2089,12 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
1673
2089
|
: null;
|
|
1674
2090
|
if (lastRefreshDate && lastRefreshDate.toISOString().split('T')[0] === serverDate) continue;
|
|
1675
2091
|
|
|
1676
|
-
const dailyQuota = code.quota?.daily || 100000;
|
|
2092
|
+
const dailyQuota = code.quota?.daily || code.quota?.dailyLimit || 100000;
|
|
1677
2093
|
codeDb.update(code.id, {
|
|
1678
2094
|
quota: {
|
|
1679
2095
|
...code.quota,
|
|
1680
|
-
|
|
1681
|
-
|
|
2096
|
+
daily: dailyQuota,
|
|
2097
|
+
dailyUsed: 0
|
|
1682
2098
|
},
|
|
1683
2099
|
lastQuotaRefresh: new Date().toISOString()
|
|
1684
2100
|
});
|