diff --git a/.gitattributes b/.gitattributes index 3db3b83d..37e3bee2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,13 @@ backend/migrations/*.sql text eol=lf # Go 源代码文件 *.go text eol=lf +# 前端 源代码文件 +*.ts text eol=lf +*.tsx text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.vue text eol=lf + # Shell 脚本 *.sh text eol=lf diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index f117abfd..959c1182 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -446,23 +446,17 @@ func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Accou } if stats, err := s.usageLogRepo.GetAccountWindowStats(ctx, account.ID, now.Add(-5*time.Hour)); err == nil { - windowStats := windowStatsFromAccountStats(stats) - if hasMeaningfulWindowStats(windowStats) { - if usage.FiveHour == nil { - usage.FiveHour = &UsageProgress{Utilization: 0} - } - usage.FiveHour.WindowStats = windowStats + if usage.FiveHour == nil { + usage.FiveHour = &UsageProgress{Utilization: 0} } + usage.FiveHour.WindowStats = windowStatsFromAccountStats(stats) } if stats, err := s.usageLogRepo.GetAccountWindowStats(ctx, account.ID, now.Add(-7*24*time.Hour)); err == nil { - windowStats := windowStatsFromAccountStats(stats) - if hasMeaningfulWindowStats(windowStats) { - if usage.SevenDay == nil { - usage.SevenDay = &UsageProgress{Utilization: 0} - } - usage.SevenDay.WindowStats = windowStats + if usage.SevenDay == nil { + usage.SevenDay = &UsageProgress{Utilization: 0} } + usage.SevenDay.WindowStats = windowStatsFromAccountStats(stats) } return usage, nil @@ -992,13 +986,6 @@ func windowStatsFromAccountStats(stats *usagestats.AccountStats) *WindowStats { } } -func hasMeaningfulWindowStats(stats *WindowStats) bool { - if stats == nil { - return false - } - return stats.Requests > 0 || stats.Tokens > 0 || stats.Cost > 0 || stats.StandardCost > 0 || stats.UserCost > 0 -} - func buildCodexUsageProgressFromExtra(extra map[string]any, window string, now time.Time) *UsageProgress { if len(extra) == 0 { return nil diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index ea76e171..5eeac183 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -1530,7 +1530,9 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U if len(input.Credentials) > 0 { account.Credentials = input.Credentials } - if len(input.Extra) > 0 { + // Extra 使用 map:需要区分“未提供(nil)”与“显式清空({})”。 + // 关闭配额限制时前端会删除 quota_* 键并提交 extra:{},此时也必须落库。 + if input.Extra != nil { // 保留配额用量字段,防止编辑账号时意外重置 for _, key := range []string{"quota_used", "quota_daily_used", "quota_daily_start", "quota_weekly_used", "quota_weekly_start"} { if v, ok := account.Extra[key]; ok { diff --git a/backend/internal/service/admin_service_overages_test.go b/backend/internal/service/admin_service_overages_test.go index 779b08b9..d6380f4d 100644 --- a/backend/internal/service/admin_service_overages_test.go +++ b/backend/internal/service/admin_service_overages_test.go @@ -121,3 +121,35 @@ func TestUpdateAccount_EnableOveragesClearsModelRateLimitsBeforePersist(t *testi _, exists := repo.account.Extra[modelRateLimitsKey] require.False(t, exists, "开启 overages 时应在持久化前清掉旧模型限流") } + +func TestUpdateAccount_EmptyExtraPayloadCanClearQuotaLimits(t *testing.T) { + accountID := int64(103) + repo := &updateAccountOveragesRepoStub{ + account: &Account{ + ID: accountID, + Platform: PlatformAnthropic, + Type: AccountTypeAPIKey, + Status: StatusActive, + Extra: map[string]any{ + "quota_limit": 100.0, + "quota_daily_limit": 10.0, + "quota_weekly_limit": 40.0, + }, + }, + } + + svc := &adminServiceImpl{accountRepo: repo} + updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{ + // 显式空对象:语义是“清空 extra 中的可配置键”(例如关闭配额限制) + Extra: map[string]any{}, + }) + + require.NoError(t, err) + require.NotNil(t, updated) + require.Equal(t, 1, repo.updateCalls) + require.NotNil(t, repo.account.Extra) + require.NotContains(t, repo.account.Extra, "quota_limit") + require.NotContains(t, repo.account.Extra, "quota_daily_limit") + require.NotContains(t, repo.account.Extra, "quota_weekly_limit") + require.Len(t, repo.account.Extra, 0) +} diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 09236edd..9c145530 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -75,7 +75,7 @@ @@ -389,8 +371,43 @@
- -
+ +
+ +
+
+ + {{ formatKeyRequests }} req + + + {{ formatKeyTokens }} + + + A ${{ formatKeyCost }} + + + U ${{ formatKeyUserCost }} + +
+
+ +
+
+
+
+
+ + + + +
-
-
-
@@ -423,12 +442,23 @@ import { adminAPI } from '@/api/admin' import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types' import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh' import { resolveCodexUsageWindow } from '@/utils/codexUsage' +import { formatCompactNumber } from '@/utils/format' import UsageProgressBar from './UsageProgressBar.vue' import AccountQuotaInfo from './AccountQuotaInfo.vue' -const props = defineProps<{ - account: Account -}>() +const props = withDefaults( + defineProps<{ + account: Account + todayStats?: WindowStats | null + todayStatsLoading?: boolean + manualRefreshToken?: number + }>(), + { + todayStats: null, + todayStatsLoading: false, + manualRefreshToken: 0 + } +) const { t } = useI18n() @@ -490,26 +520,9 @@ const isActiveOpenAIRateLimited = computed(() => { return !Number.isNaN(resetAt) && resetAt > Date.now() }) -const preferFetchedOpenAIUsage = computed(() => { - return (isActiveOpenAIRateLimited.value || isOpenAICodexSnapshotStale.value) && hasOpenAIUsageFallback.value -}) - const openAIUsageRefreshKey = computed(() => buildOpenAIUsageRefreshKey(props.account)) -const isOpenAICodexSnapshotStale = computed(() => { - if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return false - const extra = props.account.extra as Record | undefined - const updatedAtRaw = extra?.codex_usage_updated_at - if (!updatedAtRaw) return true - const updatedAt = Date.parse(String(updatedAtRaw)) - if (Number.isNaN(updatedAt)) return true - return Date.now() - updatedAt >= 10 * 60 * 1000 -}) - const shouldAutoLoadUsageOnMount = computed(() => { - if (props.account.platform === 'openai' && props.account.type === 'oauth') { - return isActiveOpenAIRateLimited.value || !hasCodexUsage.value || isOpenAICodexSnapshotStale.value - } return shouldFetchUsage.value }) @@ -1006,6 +1019,28 @@ const quotaTotalBar = computed((): QuotaBarInfo | null => { return makeQuotaBar(props.account.quota_used ?? 0, limit) }) +// ===== Key account today stats formatters ===== + +const formatKeyRequests = computed(() => { + if (!props.todayStats) return '' + return formatCompactNumber(props.todayStats.requests, { allowBillions: false }) +}) + +const formatKeyTokens = computed(() => { + if (!props.todayStats) return '' + return formatCompactNumber(props.todayStats.tokens) +}) + +const formatKeyCost = computed(() => { + if (!props.todayStats) return '0.00' + return props.todayStats.cost.toFixed(2) +}) + +const formatKeyUserCost = computed(() => { + if (!props.todayStats || props.todayStats.user_cost == null) return '0.00' + return props.todayStats.user_cost.toFixed(2) +}) + onMounted(() => { if (!shouldAutoLoadUsageOnMount.value) return loadUsage() @@ -1014,10 +1049,21 @@ onMounted(() => { watch(openAIUsageRefreshKey, (nextKey, prevKey) => { if (!prevKey || nextKey === prevKey) return if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return - if (!isActiveOpenAIRateLimited.value && hasCodexUsage.value && !isOpenAICodexSnapshotStale.value) return loadUsage().catch((e) => { console.error('Failed to refresh OpenAI usage:', e) }) }) + +watch( + () => props.manualRefreshToken, + (nextToken, prevToken) => { + if (nextToken === prevToken) return + if (!shouldFetchUsage.value) return + + loadUsage().catch((e) => { + console.error('Failed to refresh usage after manual refresh:', e) + }) + } +) diff --git a/frontend/src/components/account/UsageProgressBar.vue b/frontend/src/components/account/UsageProgressBar.vue index cd5c991f..5ce8bfe0 100644 --- a/frontend/src/components/account/UsageProgressBar.vue +++ b/frontend/src/components/account/UsageProgressBar.vue @@ -2,7 +2,7 @@
@@ -12,12 +12,13 @@ {{ formatTokens }} - + A ${{ formatAccountCost }} U ${{ formatUserCost }} @@ -56,7 +57,9 @@