From a71f51711a394a2c9482ee593f507a42f0a8b35f Mon Sep 17 00:00:00 2001 From: ilnli <220916405+ilnli@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:59:59 +0800 Subject: [PATCH 1/3] feat(quota): add total cost limit to provider quotas and update related components --- messages/en/quota.json | 3 +++ messages/ja/quota.json | 3 +++ messages/ru/quota.json | 3 +++ messages/zh-CN/quota.json | 3 +++ messages/zh-TW/quota.json | 3 +++ src/actions/providers.ts | 27 ++++++++++++++++--- .../_components/provider-quota-list-item.tsx | 9 +++++++ .../_components/providers-quota-client.tsx | 10 ++++++- .../dashboard/quotas/providers/page.tsx | 1 + 9 files changed, 57 insertions(+), 5 deletions(-) diff --git a/messages/en/quota.json b/messages/en/quota.json index 3d4e4cc40..a8f529703 100644 --- a/messages/en/quota.json +++ b/messages/en/quota.json @@ -153,6 +153,9 @@ "label": "Monthly Cost", "resetAt": "Resets at" }, + "costTotal": { + "label": "Total Cost" + }, "concurrentSessions": { "label": "Concurrent Sessions" }, diff --git a/messages/ja/quota.json b/messages/ja/quota.json index 2a09ce1e8..9dea72235 100644 --- a/messages/ja/quota.json +++ b/messages/ja/quota.json @@ -130,6 +130,9 @@ "label": "月次コスト", "resetAt": "リセット時刻" }, + "costTotal": { + "label": "総コスト" + }, "concurrentSessions": { "label": "同時セッション" }, diff --git a/messages/ru/quota.json b/messages/ru/quota.json index 6b23d09c7..4bd61cb23 100644 --- a/messages/ru/quota.json +++ b/messages/ru/quota.json @@ -153,6 +153,9 @@ "label": "Ежемесячные расходы", "resetAt": "Сброс в" }, + "costTotal": { + "label": "Общие расходы" + }, "concurrentSessions": { "label": "Параллельные сессии" }, diff --git a/messages/zh-CN/quota.json b/messages/zh-CN/quota.json index 52227fa68..96af2d031 100644 --- a/messages/zh-CN/quota.json +++ b/messages/zh-CN/quota.json @@ -153,6 +153,9 @@ "label": "月消费", "resetAt": "重置于" }, + "costTotal": { + "label": "总消费" + }, "concurrentSessions": { "label": "并发 Session" }, diff --git a/messages/zh-TW/quota.json b/messages/zh-TW/quota.json index acac4bc86..ecb0e0adb 100644 --- a/messages/zh-TW/quota.json +++ b/messages/zh-TW/quota.json @@ -128,6 +128,9 @@ "label": "月消費", "resetAt": "重置於" }, + "costTotal": { + "label": "總消費" + }, "concurrentSessions": { "label": "並發 Session" }, diff --git a/src/actions/providers.ts b/src/actions/providers.ts index 918a694a8..4ea3eafd5 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -2690,6 +2690,7 @@ export async function getProviderLimitUsage(providerId: number): Promise< costDaily: { current: number; limit: number | null; resetAt?: Date }; costWeekly: { current: number; limit: number | null; resetAt: Date }; costMonthly: { current: number; limit: number | null; resetAt: Date }; + limitTotalUsd: { current: number; limit: number | null }; concurrentSessions: { current: number; limit: number }; }> > { @@ -2713,7 +2714,9 @@ export async function getProviderLimitUsage(providerId: number): Promise< getTimeRangeForPeriodWithMode, } = await import("@/lib/rate-limit/time-utils"); const { RateLimitService } = await import("@/lib/rate-limit"); - const { sumProviderCostInTimeRange } = await import("@/repository/statistics"); + const { sumProviderCostInTimeRange, sumProviderTotalCost } = await import( + "@/repository/statistics" + ); const limit5hResetMode = provider.limit5hResetMode ?? "rolling"; // 计算各周期的时间范围 @@ -2732,13 +2735,15 @@ export async function getProviderLimitUsage(providerId: number): Promise< ]); // 获取金额消费(直接查询数据库,确保配额显示与 DB 一致) - const [cost5h, costDaily, costWeekly, costMonthly, concurrentSessions] = await Promise.all([ + const [cost5h, costDaily, costWeekly, costMonthly, totalCost, concurrentSessions] = + await Promise.all([ limit5hResetMode === "fixed" ? RateLimitService.getCurrentCost(providerId, "provider", "5h", undefined, limit5hResetMode) : sumProviderCostInTimeRange(providerId, range5h.startTime, range5h.endTime), sumProviderCostInTimeRange(providerId, rangeDaily.startTime, rangeDaily.endTime), sumProviderCostInTimeRange(providerId, rangeWeekly.startTime, rangeWeekly.endTime), sumProviderCostInTimeRange(providerId, rangeMonthly.startTime, rangeMonthly.endTime), + sumProviderTotalCost(providerId), SessionTracker.getProviderSessionCount(providerId), ]); @@ -2779,6 +2784,10 @@ export async function getProviderLimitUsage(providerId: number): Promise< limit: provider.limitMonthlyUsd, resetAt: resetMonthly.resetAt!, }, + limitTotalUsd: { + current: totalCost, + limit: provider.limitTotalUsd ?? null, + }, concurrentSessions: { current: concurrentSessions, limit: provider.limitConcurrentSessions || 0, @@ -2800,6 +2809,7 @@ export type ProviderLimitUsageData = { costDaily: { current: number; limit: number | null; resetAt?: Date }; costWeekly: { current: number; limit: number | null; resetAt: Date }; costMonthly: { current: number; limit: number | null; resetAt: Date }; + limitTotalUsd: { current: number; limit: number | null }; concurrentSessions: { current: number; limit: number }; }; @@ -2820,6 +2830,7 @@ export async function getProviderLimitUsageBatch( limitDailyUsd?: number | null; limitWeeklyUsd?: number | null; limitMonthlyUsd?: number | null; + limitTotalUsd?: number | null; limitConcurrentSessions?: number | null; }> ): Promise> { @@ -2845,7 +2856,9 @@ export async function getProviderLimitUsageBatch( getTimeRangeForPeriodWithMode, } = await import("@/lib/rate-limit/time-utils"); const { RateLimitService } = await import("@/lib/rate-limit"); - const { sumProviderCostInTimeRange } = await import("@/repository/statistics"); + const { sumProviderCostInTimeRange, sumProviderTotalCost } = await import( + "@/repository/statistics" + ); const providerIds = providers.map((p) => p.id); @@ -2871,7 +2884,8 @@ export async function getProviderLimitUsageBatch( ); // 并行查询该供应商的各周期消费(直接查询数据库) - const [cost5h, resetAt5h, costDaily, costWeekly, costMonthly] = await Promise.all([ + const [cost5h, resetAt5h, costDaily, costWeekly, costMonthly, totalCost] = + await Promise.all([ limit5hResetMode === "fixed" ? RateLimitService.getCurrentCost( provider.id, @@ -2887,6 +2901,7 @@ export async function getProviderLimitUsageBatch( sumProviderCostInTimeRange(provider.id, rangeDaily.startTime, rangeDaily.endTime), sumProviderCostInTimeRange(provider.id, rangeWeekly.startTime, rangeWeekly.endTime), sumProviderCostInTimeRange(provider.id, rangeMonthly.startTime, rangeMonthly.endTime), + sumProviderTotalCost(provider.id), ]); const sessionCount = sessionCountMap.get(provider.id) || 0; @@ -2926,6 +2941,10 @@ export async function getProviderLimitUsageBatch( limit: provider.limitMonthlyUsd ?? null, resetAt: resetMonthly.resetAt!, }, + limitTotalUsd: { + current: totalCost, + limit: provider.limitTotalUsd ?? null, + }, concurrentSessions: { current: sessionCount, limit: provider.limitConcurrentSessions || 0, diff --git a/src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx b/src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx index 322b5f941..25784b4b4 100644 --- a/src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx +++ b/src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx @@ -16,6 +16,7 @@ interface ProviderQuota { costWeekly: { current: number; limit: number | null; resetAt: Date }; costMonthly: { current: number; limit: number | null; resetAt: Date }; concurrentSessions: { current: number; limit: number }; + limitTotalUsd: { current: number; limit: number | null }; } interface ProviderWithQuota { @@ -217,6 +218,14 @@ export function ProviderQuotaListItem({ provider.quota.costMonthly.resetAt )} + {/* 总限额 */} + {provider.quota.limitTotalUsd.limit && provider.quota.limitTotalUsd.limit > 0 && + renderQuotaItem( + t("costTotal.label"), + provider.quota.limitTotalUsd.current, + provider.quota.limitTotalUsd.limit + )} + {/* 并发Session */} {renderConcurrentSessionsItem()} diff --git a/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx b/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx index 811cbb18e..107914ead 100644 --- a/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx +++ b/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx @@ -15,6 +15,7 @@ interface ProviderQuota { costWeekly: { current: number; limit: number | null; resetAt: Date }; costMonthly: { current: number; limit: number | null; resetAt: Date }; concurrentSessions: { current: number; limit: number }; + limitTotalUsd: { current: number; limit: number | null }; } interface ProviderWithQuota { @@ -35,10 +36,12 @@ interface ProvidersQuotaClientProps { currencyCode?: CurrencyCode; } -// 判断供应商是否设置了限额 +// 判断供应商是否设置了任意限额 function hasQuotaLimit(quota: ProviderQuota | null): boolean { if (!quota) return false; + return ( + (quota.limitTotalUsd.limit !== null && quota.limitTotalUsd.limit > 0) || (quota.cost5h.limit !== null && quota.cost5h.limit > 0) || (quota.costDaily.limit !== null && quota.costDaily.limit > 0) || (quota.costWeekly.limit !== null && quota.costWeekly.limit > 0) || @@ -70,6 +73,11 @@ function calculateMaxUsage(provider: ProviderWithQuota): number { (provider.quota.concurrentSessions.current / provider.quota.concurrentSessions.limit) * 100 ); } + if (provider.quota.limitTotalUsd.limit && provider.quota.limitTotalUsd.limit > 0) { + usages.push( + (provider.quota.limitTotalUsd.current / provider.quota.limitTotalUsd.limit) * 100 + ); + } return usages.length > 0 ? Math.max(...usages) : 0; } diff --git a/src/app/[locale]/dashboard/quotas/providers/page.tsx b/src/app/[locale]/dashboard/quotas/providers/page.tsx index 6b74c94c0..4bb26d437 100644 --- a/src/app/[locale]/dashboard/quotas/providers/page.tsx +++ b/src/app/[locale]/dashboard/quotas/providers/page.tsx @@ -25,6 +25,7 @@ async function getProvidersWithQuotas() { limitDailyUsd: p.limitDailyUsd, limitWeeklyUsd: p.limitWeeklyUsd, limitMonthlyUsd: p.limitMonthlyUsd, + limitTotalUsd: p.limitTotalUsd, limitConcurrentSessions: p.limitConcurrentSessions, })) ); From e6a0151b4ca5766a3f7efd3f97d44da3c9ef1861 Mon Sep 17 00:00:00 2001 From: ilnli <220916405+ilnli@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:13:59 +0800 Subject: [PATCH 2/3] feat(quota): add total quota fields and UI components for total limit --- messages/en/quota.json | 6 +++ messages/ja/quota.json | 6 +++ messages/ru/quota.json | 6 +++ messages/zh-CN/quota.json | 6 +++ messages/zh-TW/quota.json | 6 +++ .../_components/edit-key-quota-dialog.tsx | 30 +++++++++++ .../keys/_components/keys-quota-client.tsx | 50 +++++++++++++++++++ .../keys/_components/keys-quota-manager.tsx | 1 + 8 files changed, 111 insertions(+) diff --git a/messages/en/quota.json b/messages/en/quota.json index a8f529703..124ab271c 100644 --- a/messages/en/quota.json +++ b/messages/en/quota.json @@ -186,6 +186,7 @@ "costDaily": "Daily Quota", "costWeekly": "Weekly Quota", "costMonthly": "Monthly Quota", + "costTotal": "Total Quota", "concurrentSessions": "Concurrent Limit", "status": "Status", "actions": "Actions" @@ -238,6 +239,11 @@ "placeholder": "Unlimited", "current": "Current usage: {currency}{current} / {currency}{limit}" }, + "limitTotalUsd": { + "label": "Total Quota (USD)", + "placeholder": "Unlimited", + "current": "Current usage: {currency}{current} / {currency}{limit}" + }, "concurrentSessions": { "label": "Concurrent Session Quota", "placeholder": "0 = Unlimited", diff --git a/messages/ja/quota.json b/messages/ja/quota.json index 9dea72235..bb6c51510 100644 --- a/messages/ja/quota.json +++ b/messages/ja/quota.json @@ -163,6 +163,7 @@ "costDaily": "日次クォータ", "costWeekly": "週次クォータ", "costMonthly": "月次クォータ", + "costTotal": "総クォータ", "concurrentSessions": "同時制限", "status": "ステータス", "actions": "アクション" @@ -215,6 +216,11 @@ "placeholder": "無制限", "current": "現在使用: {currency}{current} / {currency}{limit}" }, + "limitTotalUsd": { + "label": "総クォータ (USD)", + "placeholder": "無制限", + "current": "現在使用: {currency}{current} / {currency}{limit}" + }, "concurrentSessions": { "label": "同時セッションクォータ", "placeholder": "0 = 無制限", diff --git a/messages/ru/quota.json b/messages/ru/quota.json index 4bd61cb23..0ebe777cc 100644 --- a/messages/ru/quota.json +++ b/messages/ru/quota.json @@ -186,6 +186,7 @@ "costDaily": "Дневная квота", "costWeekly": "Еженедельная квота", "costMonthly": "Ежемесячная квота", + "costTotal": "Общая квота", "concurrentSessions": "Лимит параллельных", "status": "Статус", "actions": "Действия" @@ -238,6 +239,11 @@ "placeholder": "Неограниченно", "current": "Использовано: {currency}{current} из {currency}{limit}" }, + "limitTotalUsd": { + "label": "Общая квота (USD)", + "placeholder": "Неограниченно", + "current": "Использовано: {currency}{current} из {currency}{limit}" + }, "concurrentSessions": { "label": "Квота параллельных сессий", "placeholder": "0 = без ограничений", diff --git a/messages/zh-CN/quota.json b/messages/zh-CN/quota.json index 96af2d031..63918d285 100644 --- a/messages/zh-CN/quota.json +++ b/messages/zh-CN/quota.json @@ -186,6 +186,7 @@ "costDaily": "每日限额", "costWeekly": "周限额", "costMonthly": "月限额", + "costTotal": "总限额", "concurrentSessions": "并发限制", "status": "状态", "actions": "操作" @@ -238,6 +239,11 @@ "placeholder": "不限制", "current": "当前已用: {currency}{current} / {currency}{limit}" }, + "limitTotalUsd": { + "label": "总限额(USD)", + "placeholder": "不限制", + "current": "当前已用: {currency}{current} / {currency}{limit}" + }, "concurrentSessions": { "label": "并发 Session 限额", "placeholder": "0 = 不限制", diff --git a/messages/zh-TW/quota.json b/messages/zh-TW/quota.json index ecb0e0adb..bfa7a99fc 100644 --- a/messages/zh-TW/quota.json +++ b/messages/zh-TW/quota.json @@ -161,6 +161,7 @@ "costDaily": "每日限額", "costWeekly": "周限額", "costMonthly": "月限額", + "costTotal": "總限額", "concurrentSessions": "並發限制", "status": "狀態", "actions": "操作" @@ -213,6 +214,11 @@ "placeholder": "不限制", "current": "當前已用: {currency}{current} / {currency}{limit}" }, + "limitTotalUsd": { + "label": "總限額 (USD)", + "placeholder": "不限制", + "current": "當前已用: {currency}{current} / {currency}{limit}" + }, "concurrentSessions": { "label": "並發 Session 限額", "placeholder": "0 = 不限制", diff --git a/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx b/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx index b7ab7e873..3be41923c 100644 --- a/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx +++ b/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx @@ -32,6 +32,7 @@ interface KeyQuota { costDaily: { current: number; limit: number | null; resetAt?: Date }; costWeekly: { current: number; limit: number | null }; costMonthly: { current: number; limit: number | null }; + costTotal: { current: number; limit: number | null }; concurrentSessions: { current: number; limit: number }; } @@ -79,6 +80,7 @@ export function EditKeyQuotaDialog({ const [limitMonthly, setLimitMonthly] = useState( currentQuota?.costMonthly.limit?.toString() ?? "" ); + const [limitTotal, setLimitTotal] = useState(currentQuota?.costTotal.limit?.toString() ?? ""); const [limitConcurrent, setLimitConcurrent] = useState( currentQuota?.concurrentSessions.limit?.toString() ?? "0" ); @@ -98,6 +100,7 @@ export function EditKeyQuotaDialog({ dailyResetTime: resetTime, limitWeeklyUsd: limitWeekly ? parseFloat(limitWeekly) : null, limitMonthlyUsd: limitMonthly ? parseFloat(limitMonthly) : null, + limitTotalUsd: limitTotal ? parseFloat(limitTotal) : null, limitConcurrentSessions: limitConcurrent ? parseInt(limitConcurrent, 10) : 0, }); @@ -127,6 +130,7 @@ export function EditKeyQuotaDialog({ dailyResetTime: resetTime, limitWeeklyUsd: null, limitMonthlyUsd: null, + limitTotalUsd: null, limitConcurrentSessions: 0, }); @@ -335,6 +339,32 @@ export function EditKeyQuotaDialog({ )} + {/* 总限额 */} +
+ + setLimitTotal(e.target.value)} + className="h-9" + /> + {currentQuota?.costTotal.limit && ( +

+ {t("limitTotalUsd.current", { + currency: currencySymbol, + current: Number(currentQuota.costTotal.current).toFixed(4), + limit: Number(currentQuota.costTotal.limit).toFixed(2), + })} +

+ )} +
+ {/* 并发限额 */}