diff --git a/backend/internal/handler/sora_client_handler_test.go b/backend/internal/handler/sora_client_handler_test.go index 78931c88ca..c8b7a5ff6e 100644 --- a/backend/internal/handler/sora_client_handler_test.go +++ b/backend/internal/handler/sora_client_handler_test.go @@ -2132,7 +2132,7 @@ func (r *stubAccountRepoForHandler) BulkUpdate(context.Context, []int64, service return 0, nil } -func (r *stubAccountRepoForHandler) IncrementQuotaUsed(context.Context, int64, float64, string) error { +func (r *stubAccountRepoForHandler) IncrementQuotaUsed(context.Context, int64, float64) error { return nil } diff --git a/backend/internal/handler/sora_gateway_handler_test.go b/backend/internal/handler/sora_gateway_handler_test.go index 2d1fb0a97f..637462ad50 100644 --- a/backend/internal/handler/sora_gateway_handler_test.go +++ b/backend/internal/handler/sora_gateway_handler_test.go @@ -216,7 +216,7 @@ func (r *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates s return 0, nil } -func (r *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error { +func (r *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error { return nil } diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index 6fda0c182a..2e4c7ec9b6 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -1676,54 +1676,53 @@ func (r *accountRepository) FindByExtraField(ctx context.Context, key string, va return r.accountsToService(ctx, accounts) } -// IncrementQuotaUsed 原子递增账号的 extra.quota_used 字段 -// period 为 "daily"/"weekly" 时,使用 CTE 原子重置+递增;为空时保持累计制。 -func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, period string) error { - var rows *sql.Rows - var err error +// nowUTC is a SQL expression to generate a UTC RFC3339 timestamp string. +const nowUTC = `to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')` - interval := periodToInterval(period) - if interval == "" { - // 累计制:保持原有逻辑 - rows, err = r.sql.QueryContext(ctx, - `UPDATE accounts SET extra = jsonb_set( - COALESCE(extra, '{}'::jsonb), - '{quota_used}', - to_jsonb(COALESCE((extra->>'quota_used')::numeric, 0) + $1) - ), updated_at = NOW() - WHERE id = $2 AND deleted_at IS NULL - RETURNING - COALESCE((extra->>'quota_used')::numeric, 0), - COALESCE((extra->>'quota_limit')::numeric, 0)`, - amount, id) - } else { - // 周期制:单条 UPDATE 内联过期判断,避免 CTE snapshot 在 READ COMMITTED 下的 lost update - rows, err = r.sql.QueryContext(ctx, - `UPDATE accounts SET extra = jsonb_set( - jsonb_set( - COALESCE(extra, '{}'::jsonb), - '{quota_used}', - to_jsonb( - CASE WHEN COALESCE((extra->>'quota_period_start')::timestamptz, '1970-01-01'::timestamptz) - + $3::interval <= NOW() - THEN $1 - ELSE COALESCE((extra->>'quota_used')::numeric, 0) + $1 END - ) - ), - '{quota_period_start}', - to_jsonb( - CASE WHEN COALESCE((extra->>'quota_period_start')::timestamptz, '1970-01-01'::timestamptz) - + $3::interval <= NOW() - THEN to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') - ELSE COALESCE(extra->>'quota_period_start', to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')) END +// IncrementQuotaUsed 原子递增账号的配额用量(总/日/周三个维度) +// 日/周额度在周期过期时自动重置为 0 再递增。 +func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error { + rows, err := r.sql.QueryContext(ctx, + `UPDATE accounts SET extra = ( + COALESCE(extra, '{}'::jsonb) + -- 总额度:始终递增 + || jsonb_build_object('quota_used', COALESCE((extra->>'quota_used')::numeric, 0) + $1) + -- 日额度:仅在 quota_daily_limit > 0 时处理 + || CASE WHEN COALESCE((extra->>'quota_daily_limit')::numeric, 0) > 0 THEN + jsonb_build_object( + 'quota_daily_used', + CASE WHEN COALESCE((extra->>'quota_daily_start')::timestamptz, '1970-01-01'::timestamptz) + + '24 hours'::interval <= NOW() + THEN $1 + ELSE COALESCE((extra->>'quota_daily_used')::numeric, 0) + $1 END, + 'quota_daily_start', + CASE WHEN COALESCE((extra->>'quota_daily_start')::timestamptz, '1970-01-01'::timestamptz) + + '24 hours'::interval <= NOW() + THEN `+nowUTC+` + ELSE COALESCE(extra->>'quota_daily_start', `+nowUTC+`) END ) - ), updated_at = NOW() - WHERE id = $2 AND deleted_at IS NULL - RETURNING - COALESCE((extra->>'quota_used')::numeric, 0), - COALESCE((extra->>'quota_limit')::numeric, 0)`, - amount, id, interval) - } + ELSE '{}'::jsonb END + -- 周额度:仅在 quota_weekly_limit > 0 时处理 + || CASE WHEN COALESCE((extra->>'quota_weekly_limit')::numeric, 0) > 0 THEN + jsonb_build_object( + 'quota_weekly_used', + CASE WHEN COALESCE((extra->>'quota_weekly_start')::timestamptz, '1970-01-01'::timestamptz) + + '168 hours'::interval <= NOW() + THEN $1 + ELSE COALESCE((extra->>'quota_weekly_used')::numeric, 0) + $1 END, + 'quota_weekly_start', + CASE WHEN COALESCE((extra->>'quota_weekly_start')::timestamptz, '1970-01-01'::timestamptz) + + '168 hours'::interval <= NOW() + THEN `+nowUTC+` + ELSE COALESCE(extra->>'quota_weekly_start', `+nowUTC+`) END + ) + ELSE '{}'::jsonb END + ), updated_at = NOW() + WHERE id = $2 AND deleted_at IS NULL + RETURNING + COALESCE((extra->>'quota_used')::numeric, 0), + COALESCE((extra->>'quota_limit')::numeric, 0)`, + amount, id) if err != nil { return err } @@ -1739,7 +1738,7 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am return err } - // 配额刚超限时触发调度快照刷新,使账号及时从调度候选中移除 + // 任一维度配额刚超限时触发调度快照刷新 if limit > 0 && newUsed >= limit && (newUsed-amount) < limit { if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil { logger.LegacyPrintf("repository.account", "[SchedulerOutbox] enqueue quota exceeded failed: account=%d err=%v", id, err) @@ -1748,25 +1747,13 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am return nil } -// periodToInterval 将周期类型转为 PostgreSQL interval 字符串 -func periodToInterval(period string) string { - switch period { - case "daily": - return "24 hours" - case "weekly": - return "168 hours" - default: - return "" - } -} - -// ResetQuotaUsed 重置账号的 extra.quota_used 为 0,同时清除 quota_period_start 以开始新周期 +// ResetQuotaUsed 重置账号所有维度的配额用量为 0 func (r *accountRepository) ResetQuotaUsed(ctx context.Context, id int64) error { _, err := r.sql.ExecContext(ctx, `UPDATE accounts SET extra = ( COALESCE(extra, '{}'::jsonb) - || '{"quota_used": 0}'::jsonb - ) - 'quota_period_start', updated_at = NOW() + || '{"quota_used": 0, "quota_daily_used": 0, "quota_weekly_used": 0}'::jsonb + ) - 'quota_daily_start' - 'quota_weekly_start', updated_at = NOW() WHERE id = $1 AND deleted_at IS NULL`, id) if err != nil { diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index e6b2399c85..7e7c898220 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -1096,7 +1096,7 @@ func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map return errors.New("not implemented") } -func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error { +func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error { return errors.New("not implemented") } diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index ce1b5c309d..4429107fe7 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -1152,45 +1152,51 @@ func (a *Account) GetCacheTTLOverrideTarget() string { // GetQuotaLimit 获取 API Key 账号的配额限制(美元) // 返回 0 表示未启用 func (a *Account) GetQuotaLimit() float64 { - if a.Extra == nil { - return 0 - } - if v, ok := a.Extra["quota_limit"]; ok { - return parseExtraFloat64(v) - } - return 0 + return a.getExtraFloat64("quota_limit") } // GetQuotaUsed 获取 API Key 账号的已用配额(美元) func (a *Account) GetQuotaUsed() float64 { + return a.getExtraFloat64("quota_used") +} + +// GetQuotaDailyLimit 获取日额度限制(美元),0 表示未启用 +func (a *Account) GetQuotaDailyLimit() float64 { + return a.getExtraFloat64("quota_daily_limit") +} + +// GetQuotaDailyUsed 获取当日已用额度(美元) +func (a *Account) GetQuotaDailyUsed() float64 { + return a.getExtraFloat64("quota_daily_used") +} + +// GetQuotaWeeklyLimit 获取周额度限制(美元),0 表示未启用 +func (a *Account) GetQuotaWeeklyLimit() float64 { + return a.getExtraFloat64("quota_weekly_limit") +} + +// GetQuotaWeeklyUsed 获取本周已用额度(美元) +func (a *Account) GetQuotaWeeklyUsed() float64 { + return a.getExtraFloat64("quota_weekly_used") +} + +// getExtraFloat64 从 Extra 中读取指定 key 的 float64 值 +func (a *Account) getExtraFloat64(key string) float64 { if a.Extra == nil { return 0 } - if v, ok := a.Extra["quota_used"]; ok { + if v, ok := a.Extra[key]; ok { return parseExtraFloat64(v) } return 0 } -// GetQuotaPeriod 获取配额周期类型("daily"/"weekly",空字符串表示累计制) -func (a *Account) GetQuotaPeriod() string { - if a.Extra == nil { - return "" - } - if v, ok := a.Extra["quota_period"]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - -// GetQuotaPeriodStart 获取当前周期的起始时间 -func (a *Account) GetQuotaPeriodStart() time.Time { +// getExtraTime 从 Extra 中读取 RFC3339 时间戳 +func (a *Account) getExtraTime(key string) time.Time { if a.Extra == nil { return time.Time{} } - if v, ok := a.Extra["quota_period_start"]; ok { + if v, ok := a.Extra[key]; ok { if s, ok := v.(string); ok { if t, err := time.Parse(time.RFC3339Nano, s); err == nil { return t @@ -1203,38 +1209,40 @@ func (a *Account) GetQuotaPeriodStart() time.Time { return time.Time{} } -// quotaPeriodDuration 返回周期长度 -func quotaPeriodDuration(period string) time.Duration { - switch period { - case "daily": - return 24 * time.Hour - case "weekly": - return 7 * 24 * time.Hour - default: - return 0 - } +// HasAnyQuotaLimit 检查是否配置了任一维度的配额限制 +func (a *Account) HasAnyQuotaLimit() bool { + return a.GetQuotaLimit() > 0 || a.GetQuotaDailyLimit() > 0 || a.GetQuotaWeeklyLimit() > 0 } -// isQuotaPeriodExpired 检查配额周期是否已过期 -func isQuotaPeriodExpired(period string, periodStart time.Time) bool { - dur := quotaPeriodDuration(period) - if dur == 0 || periodStart.IsZero() { - return false +// isPeriodExpired 检查指定周期(自 periodStart 起经过 dur)是否已过期 +func isPeriodExpired(periodStart time.Time, dur time.Duration) bool { + if periodStart.IsZero() { + return true // 从未使用过,视为过期(下次 increment 会初始化) } return time.Since(periodStart) >= dur } -// IsQuotaExceeded 检查 API Key 账号配额是否已超限 +// IsQuotaExceeded 检查 API Key 账号配额是否已超限(任一维度超限即返回 true) func (a *Account) IsQuotaExceeded() bool { - limit := a.GetQuotaLimit() - if limit <= 0 { - return false + // 总额度 + if limit := a.GetQuotaLimit(); limit > 0 && a.GetQuotaUsed() >= limit { + return true } - // 周期性配额:如果当前周期已过期,视为未超限(下次 increment 时会自动重置) - if isQuotaPeriodExpired(a.GetQuotaPeriod(), a.GetQuotaPeriodStart()) { - return false + // 日额度(周期过期视为未超限,下次 increment 会重置) + if limit := a.GetQuotaDailyLimit(); limit > 0 { + start := a.getExtraTime("quota_daily_start") + if !isPeriodExpired(start, 24*time.Hour) && a.GetQuotaDailyUsed() >= limit { + return true + } } - return a.GetQuotaUsed() >= limit + // 周额度 + if limit := a.GetQuotaWeeklyLimit(); limit > 0 { + start := a.getExtraTime("quota_weekly_start") + if !isPeriodExpired(start, 7*24*time.Hour) && a.GetQuotaWeeklyUsed() >= limit { + return true + } + } + return false } // GetWindowCostLimit 获取 5h 窗口费用阈值(美元) diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go index 85c1398cf3..7ba4918961 100644 --- a/backend/internal/service/account_service.go +++ b/backend/internal/service/account_service.go @@ -68,8 +68,8 @@ type AccountRepository interface { UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error UpdateExtra(ctx context.Context, id int64, updates map[string]any) error BulkUpdate(ctx context.Context, ids []int64, updates AccountBulkUpdate) (int64, error) - // IncrementQuotaUsed 原子递增 API Key 账号的配额用量 - IncrementQuotaUsed(ctx context.Context, id int64, amount float64, period string) error + // IncrementQuotaUsed 原子递增 API Key 账号的配额用量(总/日/周) + IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error // ResetQuotaUsed 重置 API Key 账号的配额用量为 0 ResetQuotaUsed(ctx context.Context, id int64) error } diff --git a/backend/internal/service/account_service_delete_test.go b/backend/internal/service/account_service_delete_test.go index bfb57bdb44..c96b436f02 100644 --- a/backend/internal/service/account_service_delete_test.go +++ b/backend/internal/service/account_service_delete_test.go @@ -199,7 +199,7 @@ func (s *accountRepoStub) BulkUpdate(ctx context.Context, ids []int64, updates A panic("unexpected BulkUpdate call") } -func (s *accountRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error { +func (s *accountRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error { return nil } diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index 6f800f3387..466ff294bb 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -187,7 +187,7 @@ func (m *mockAccountRepoForPlatform) BulkUpdate(ctx context.Context, ids []int64 return 0, nil } -func (m *mockAccountRepoForPlatform) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error { +func (m *mockAccountRepoForPlatform) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error { return nil } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 73b726c843..bf03b8a0ec 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -6583,10 +6583,9 @@ func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *bill } // 4. 账号配额用量(账号口径:TotalCost × 账号计费倍率) - if cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.GetQuotaLimit() > 0 { + if cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.HasAnyQuotaLimit() { accountCost := cost.TotalCost * p.AccountRateMultiplier - period := p.Account.GetQuotaPeriod() - if err := deps.accountRepo.IncrementQuotaUsed(ctx, p.Account.ID, accountCost, period); err != nil { + if err := deps.accountRepo.IncrementQuotaUsed(ctx, p.Account.ID, accountCost); err != nil { slog.Error("increment account quota used failed", "account_id", p.Account.ID, "cost", accountCost, "error", err) } } diff --git a/backend/internal/service/gemini_multiplatform_test.go b/backend/internal/service/gemini_multiplatform_test.go index 5a9dc69a14..56f94d5f1f 100644 --- a/backend/internal/service/gemini_multiplatform_test.go +++ b/backend/internal/service/gemini_multiplatform_test.go @@ -176,7 +176,7 @@ func (m *mockAccountRepoForGemini) BulkUpdate(ctx context.Context, ids []int64, return 0, nil } -func (m *mockAccountRepoForGemini) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error { +func (m *mockAccountRepoForGemini) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error { return nil } diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index c50712ff10..5cbc9691df 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -1228,7 +1228,7 @@ - +
@@ -2473,7 +2473,8 @@ const addMethod = ref('oauth') // For oauth-based: 'oauth' or 'setup- const apiKeyBaseUrl = ref('https://api.anthropic.com') const apiKeyValue = ref('') const editQuotaLimit = ref(null) -const editQuotaPeriod = ref('') +const editQuotaDailyLimit = ref(null) +const editQuotaWeeklyLimit = ref(null) const modelMappings = ref([]) const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist') const allowedModels = ref([]) @@ -3137,7 +3138,8 @@ const resetForm = () => { apiKeyBaseUrl.value = 'https://api.anthropic.com' apiKeyValue.value = '' editQuotaLimit.value = null - editQuotaPeriod.value = '' + editQuotaDailyLimit.value = null + editQuotaWeeklyLimit.value = null modelMappings.value = [] modelRestrictionMode.value = 'whitelist' allowedModels.value = [...claudeModels] // Default fill related models @@ -3552,12 +3554,23 @@ const createAccountAndFinish = async ( if (!applyTempUnschedConfig(credentials)) { return } - // Inject quota_limit for apikey accounts + // Inject quota limits for apikey accounts let finalExtra = extra - if (type === 'apikey' && editQuotaLimit.value != null && editQuotaLimit.value > 0) { - finalExtra = { ...(extra || {}), quota_limit: editQuotaLimit.value } - if (editQuotaPeriod.value) { - finalExtra.quota_period = editQuotaPeriod.value + if (type === 'apikey') { + const hasAnyQuota = (editQuotaLimit.value != null && editQuotaLimit.value > 0) || + (editQuotaDailyLimit.value != null && editQuotaDailyLimit.value > 0) || + (editQuotaWeeklyLimit.value != null && editQuotaWeeklyLimit.value > 0) + if (hasAnyQuota) { + finalExtra = { ...(extra || {}) } + if (editQuotaLimit.value != null && editQuotaLimit.value > 0) { + finalExtra.quota_limit = editQuotaLimit.value + } + if (editQuotaDailyLimit.value != null && editQuotaDailyLimit.value > 0) { + finalExtra.quota_daily_limit = editQuotaDailyLimit.value + } + if (editQuotaWeeklyLimit.value != null && editQuotaWeeklyLimit.value > 0) { + finalExtra.quota_weekly_limit = editQuotaWeeklyLimit.value + } } } await doCreateAccount({ diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 826caf6496..0efa0b5743 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -798,7 +798,7 @@
- +
(OPENAI_WS_MODE_OF const codexCLIOnlyEnabled = ref(false) const anthropicPassthroughEnabled = ref(false) const editQuotaLimit = ref(null) -const editQuotaPeriod = ref('') +const editQuotaDailyLimit = ref(null) +const editQuotaWeeklyLimit = ref(null) const openAIWSModeOptions = computed(() => [ { value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') }, // TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复 @@ -1594,10 +1595,14 @@ watch( if (newAccount.type === 'apikey') { const quotaVal = extra?.quota_limit as number | undefined editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null - editQuotaPeriod.value = (extra?.quota_period as string) || '' + const dailyVal = extra?.quota_daily_limit as number | undefined + editQuotaDailyLimit.value = (dailyVal && dailyVal > 0) ? dailyVal : null + const weeklyVal = extra?.quota_weekly_limit as number | undefined + editQuotaWeeklyLimit.value = (weeklyVal && weeklyVal > 0) ? weeklyVal : null } else { editQuotaLimit.value = null - editQuotaPeriod.value = '' + editQuotaDailyLimit.value = null + editQuotaWeeklyLimit.value = null } // Load antigravity model mapping (Antigravity 只支持映射模式) @@ -2390,22 +2395,32 @@ const handleSubmit = async () => { updatePayload.extra = newExtra } - // For apikey accounts, handle quota_limit in extra + // For apikey accounts, handle quota limits in extra if (props.account.type === 'apikey') { const currentExtra = (updatePayload.extra as Record) || (props.account.extra as Record) || {} const newExtra: Record = { ...currentExtra } + // Total quota if (editQuotaLimit.value != null && editQuotaLimit.value > 0) { newExtra.quota_limit = editQuotaLimit.value - if (editQuotaPeriod.value) { - newExtra.quota_period = editQuotaPeriod.value - } else { - delete newExtra.quota_period - } } else { delete newExtra.quota_limit - delete newExtra.quota_period - delete newExtra.quota_period_start + } + // Daily quota + if (editQuotaDailyLimit.value != null && editQuotaDailyLimit.value > 0) { + newExtra.quota_daily_limit = editQuotaDailyLimit.value + } else { + delete newExtra.quota_daily_limit + delete newExtra.quota_daily_used + delete newExtra.quota_daily_start + } + // Weekly quota + if (editQuotaWeeklyLimit.value != null && editQuotaWeeklyLimit.value > 0) { + newExtra.quota_weekly_limit = editQuotaWeeklyLimit.value + } else { + delete newExtra.quota_weekly_limit + delete newExtra.quota_weekly_used + delete newExtra.quota_weekly_start } updatePayload.extra = newExtra } diff --git a/frontend/src/components/account/QuotaLimitCard.vue b/frontend/src/components/account/QuotaLimitCard.vue index a3c38cebbb..ed257746ff 100644 --- a/frontend/src/components/account/QuotaLimitCard.vue +++ b/frontend/src/components/account/QuotaLimitCard.vue @@ -1,40 +1,54 @@ @@ -57,29 +71,30 @@ const onInput = (e: Event) => {
-
+
+
- +
$ { :placeholder="t('admin.accounts.quotaLimitPlaceholder')" />
-

{{ t('admin.accounts.quotaLimitAmountHint') }}

+

{{ t('admin.accounts.quotaDailyLimitHint') }}

+
- - -

{{ t('admin.accounts.quotaPeriodHint') }}

+ +
+ $ + +
+

{{ t('admin.accounts.quotaWeeklyLimitHint') }}

+
+ + +
+ +
+ $ + +
+

{{ t('admin.accounts.quotaTotalLimitHint') }}

diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 4927a245d2..d72597d64e 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1797,13 +1797,14 @@ export default { quotaLimitHint: 'Set max spending limit (USD). Account will be paused when reached. Changing limit won\'t reset usage.', quotaLimitToggle: 'Enable Quota Limit', quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit', - quotaLimitAmount: 'Limit Amount', - quotaLimitAmountHint: 'Maximum spending limit (USD). Account will be auto-paused when reached. Changing limit won\'t reset usage.', - quotaPeriod: 'Reset Period', - quotaPeriodNone: 'Never (cumulative)', - quotaPeriodDaily: 'Daily (every 24 hours)', - quotaPeriodWeekly: 'Weekly (every 7 days)', - quotaPeriodHint: 'Choose how often the usage counter resets. Period starts from first usage.', + quotaLimitAmount: 'Total Limit', + quotaLimitAmountHint: 'Cumulative spending limit (USD). Never resets automatically.', + quotaDailyLimit: 'Daily Limit', + quotaDailyLimitHint: 'Maximum spending per 24-hour period. Resets automatically from first usage.', + quotaWeeklyLimit: 'Weekly Limit', + quotaWeeklyLimitHint: 'Maximum spending per 7-day period. Resets automatically from first usage.', + quotaTotalLimit: 'Total Limit', + quotaTotalLimitHint: 'Cumulative spending limit (USD). Never resets automatically.', testConnection: 'Test Connection', reAuthorize: 'Re-Authorize', refreshToken: 'Refresh Token', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index c715d9ecea..3cf3190c6f 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1796,13 +1796,14 @@ export default { quotaLimitHint: '设置最大使用额度(美元),达到后账号暂停调度。修改限额不会重置已用额度。', quotaLimitToggle: '启用配额限制', quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度', - quotaLimitAmount: '限额金额', - quotaLimitAmountHint: '账号最大可用额度(美元),达到后自动暂停。修改限额不会重置已用额度。', - quotaPeriod: '重置周期', - quotaPeriodNone: '不重置(累计)', - quotaPeriodDaily: '每日(24小时周期)', - quotaPeriodWeekly: '每周(7天周期)', - quotaPeriodHint: '选择用量计数器自动重置的频率。周期从首次使用时开始计算。', + quotaLimitAmount: '总限额', + quotaLimitAmountHint: '累计用量上限(美元),不自动重置。', + quotaDailyLimit: '日限额', + quotaDailyLimitHint: '每24小时最大可用额度,从首次使用时开始自动重置。', + quotaWeeklyLimit: '周限额', + quotaWeeklyLimitHint: '每7天最大可用额度,从首次使用时开始自动重置。', + quotaTotalLimit: '总限额', + quotaTotalLimitHint: '累计用量上限(美元),不自动重置。', testConnection: '测试连接', reAuthorize: '重新授权', refreshToken: '刷新令牌',