fogact 1.2.2 → 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.
Files changed (38) hide show
  1. package/bin/web-server.js +557 -141
  2. package/frontend/user/assets/AnnouncementDetail-Dvxmwz0A-fogact-live.js +12 -0
  3. package/frontend/user/assets/Announcements-CS1tF2mx-fogact-live.js +11 -0
  4. package/frontend/user/assets/CardBind-CsCxihhP-fogact-live.js +21 -0
  5. package/frontend/user/assets/CardContent.vue_vue_type_script_setup_true_lang-D2L-uqSl-fogact-live.js +1 -0
  6. package/frontend/user/assets/CardDescription.vue_vue_type_script_setup_true_lang-D-v5Pl7F-fogact-live.js +1 -0
  7. package/frontend/user/assets/CardTitle.vue_vue_type_script_setup_true_lang-a0CCN6D5-fogact-live.js +1 -0
  8. package/frontend/user/assets/Dashboard-rPsmltm5-fogact-live.js +51 -0
  9. package/frontend/user/assets/DashboardLayout-DDkxHYFj-fogact-live.js +80 -0
  10. package/frontend/user/assets/Input.vue_vue_type_script_setup_true_lang-B0SyPmYb-fogact-live.js +6 -0
  11. package/frontend/user/assets/Label.vue_vue_type_script_setup_true_lang-CxYORSgN-fogact-live.js +1 -0
  12. package/frontend/user/assets/Progress.vue_vue_type_script_setup_true_lang-2_QbPsEQ-fogact-live.js +1 -0
  13. package/frontend/user/assets/QuotaPack-B_tJ7Psm-fogact-live.js +6 -0
  14. package/frontend/user/assets/Renewal-BSDhDmwv-fogact-live.js +6 -0
  15. package/frontend/user/assets/ScrollArea.vue_vue_type_script_setup_true_lang-DMYwcfpz-fogact-live.js +1 -0
  16. package/frontend/user/assets/Separator.vue_vue_type_script_setup_true_lang-Ckg8EXj_-fogact-live.js +1 -0
  17. package/frontend/user/assets/Settings-CBdAa3lw-fogact-live.js +11 -0
  18. package/frontend/user/assets/TooltipTrigger.vue_vue_type_script_setup_true_lang-DtSBjzGo-fogact-live.js +16 -0
  19. package/frontend/user/assets/Welcome-Dtfp6oER-fogact-live.js +1 -0
  20. package/frontend/user/assets/_plugin-vue_export-helper-5cjT4u0R-fogact-live.js +16 -0
  21. package/frontend/user/assets/activity-wYWtyqTJ-fogact-live.js +6 -0
  22. package/frontend/user/assets/announcement-35mOnjRL-fogact-live.js +16 -0
  23. package/frontend/user/assets/calendar-BFNuCata-fogact-live.js +6 -0
  24. package/frontend/user/assets/chevron-down-kDbuU1Py-fogact-live.js +6 -0
  25. package/frontend/user/assets/chevron-right-BayASIm0-fogact-live.js +6 -0
  26. package/frontend/user/assets/eye-CY62vip0-fogact-live.js +6 -0
  27. package/frontend/user/assets/gauge-C5NQ-mV8-fogact-live.js +6 -0
  28. package/frontend/user/assets/index-fogact-live.js +91 -0
  29. package/frontend/user/assets/link-2-DT5R5nGO-fogact-live.js +6 -0
  30. package/frontend/user/assets/package-rUbExUEn-fogact-live.js +6 -0
  31. package/frontend/user/assets/plus-CQc6C8wG-fogact-live.js +11 -0
  32. package/frontend/user/assets/refresh-cw-Y9hCloPL-fogact-live.js +6 -0
  33. package/frontend/user/assets/useUserPageRefresh-BYZvpNR9-fogact-live.js +1 -0
  34. package/frontend/user/assets/zap-l5zbZqrM-fogact-live.js +11 -0
  35. package/frontend/user/index.html +1 -1
  36. package/lib/index.js +53 -13
  37. package/lib/services/database.js +47 -0
  38. 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
- proxyRes.pipe(res);
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
- used: 0,
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
- // Mock API for user frontend - usage data
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 mockUsageData = {
1273
- success: true,
1274
- data: {
1275
- total_requests: 15234,
1276
- total_tokens: 2456789,
1277
- today_requests: 342,
1278
- today_tokens: 45678,
1279
- quota: {
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(mockUsageData));
1786
+ res.end(JSON.stringify({ success: true, data: getCodeUsagePayload(code, period) }));
1297
1787
  return;
1298
1788
  }
1299
1789
 
1300
- // Mock API for user frontend - user info
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 mockUserInfo = {
1303
- success: true,
1304
- data: {
1305
- username: 'test_user',
1306
- email: 'test@example.com',
1307
- service: 'Claude Code',
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(mockUserInfo));
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
- // session 或 query 获取用户信息
1398
- const username = req.headers['x-user-id'] || 'demo_user';
1399
- const user = userDb.getByUsername(username) || userDb.getAll()[0];
1882
+ const code = resolveUserApiCode(req);
1883
+ const data = serializeCodeForUserApi(code);
1400
1884
 
1401
- if (!user) {
1402
- res.writeHead(404, { 'Content-Type': 'application/json' });
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 username = req.headers['x-user-id'] || 'demo_user';
1426
- const user = userDb.getByUsername(username) || userDb.getAll()[0];
1427
-
1428
- // 查找用户对应的激活码(通过 usedBy 或 username)
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 mockUsageData = {
1458
- success: true,
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(mockUsageData));
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, index) => ({
1531
- id: `fogact-key-${index + 1}`,
1532
- key_preview: String(key || '').slice(0, 8) || `fogact-${index + 1}`,
1533
- service_type: String(key || '').toLowerCase().includes('codex') ? 'codex' : 'claude',
1534
- status: 'active',
1535
- quota: { total: 100000, used: 0, remaining: 100000, unit: 'tokens' },
1536
- timestamps: { activated_at: now, last_used_at: null, expires_at: null },
1537
- })),
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
- used: 0,
1681
- daily: dailyQuota
2096
+ daily: dailyQuota,
2097
+ dailyUsed: 0
1682
2098
  },
1683
2099
  lastQuotaRefresh: new Date().toISOString()
1684
2100
  });