fix: zero expired codex windows in backend, use /usage API as single frontend data source

This commit is contained in:
Ethan0x0000
2026-03-16 19:46:07 +08:00
parent aa5846b282
commit 7fde9ebbc2
6 changed files with 61 additions and 394 deletions

View File

@@ -73,7 +73,7 @@
<div v-else class="text-xs text-gray-400">-</div>
</template>
<!-- OpenAI OAuth accounts: prefer fresh usage query for active rate-limited rows -->
<!-- OpenAI OAuth accounts: single source from /usage API -->
<template v-else-if="account.platform === 'openai' && account.type === 'oauth'">
<div v-if="hasOpenAIUsageFallback" class="space-y-1">
<UsageProgressBar
@@ -93,37 +93,6 @@
color="emerald"
/>
</div>
<div v-else-if="isActiveOpenAIRateLimited && loading" class="space-y-1.5">
<div class="flex items-center gap-1">
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
<div class="flex items-center gap-1">
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
</div>
<div v-else-if="hasCodexUsage" class="space-y-1">
<!-- 5h Window -->
<UsageProgressBar
v-if="codex5hUsedPercent !== null"
label="5h"
:utilization="codex5hUsedPercent"
:resets-at="codex5hResetAt"
color="indigo"
/>
<!-- 7d Window -->
<UsageProgressBar
v-if="codex7dUsedPercent !== null"
label="7d"
:utilization="codex7dUsedPercent"
:resets-at="codex7dResetAt"
color="emerald"
/>
</div>
<div v-else-if="loading" class="space-y-1.5">
<div class="flex items-center gap-1">
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
@@ -441,7 +410,6 @@ import { useI18n } from 'vue-i18n'
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'
@@ -500,37 +468,17 @@ const geminiUsageAvailable = computed(() => {
)
})
const codex5hWindow = computed(() => resolveCodexUsageWindow(props.account.extra, '5h'))
const codex7dWindow = computed(() => resolveCodexUsageWindow(props.account.extra, '7d'))
// OpenAI Codex usage computed properties
const hasCodexUsage = computed(() => {
return codex5hWindow.value.usedPercent !== null || codex7dWindow.value.usedPercent !== null
})
const hasOpenAIUsageFallback = computed(() => {
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return false
return !!usageInfo.value?.five_hour || !!usageInfo.value?.seven_day
})
const isActiveOpenAIRateLimited = computed(() => {
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return false
if (!props.account.rate_limit_reset_at) return false
const resetAt = Date.parse(props.account.rate_limit_reset_at)
return !Number.isNaN(resetAt) && resetAt > Date.now()
})
const openAIUsageRefreshKey = computed(() => buildOpenAIUsageRefreshKey(props.account))
const shouldAutoLoadUsageOnMount = computed(() => {
return shouldFetchUsage.value
})
const codex5hUsedPercent = computed(() => codex5hWindow.value.usedPercent)
const codex5hResetAt = computed(() => codex5hWindow.value.resetAt)
const codex7dUsedPercent = computed(() => codex7dWindow.value.usedPercent)
const codex7dResetAt = computed(() => codex7dWindow.value.resetAt)
// Antigravity quota types (用于 API 返回的数据)
interface AntigravityUsageResult {
utilization: number

View File

@@ -198,7 +198,7 @@ describe('AccountUsageCell', () => {
expect(wrapper.text()).toContain('7d|77|300')
})
it('OpenAI OAuth 有现成快照时首屏先显示快照再加载 usage 覆盖', async () => {
it('OpenAI OAuth 有 codex 快照时仍然使用 /usage API 数据渲染', async () => {
getUsage.mockResolvedValue({
five_hour: {
utilization: 18,
@@ -254,8 +254,8 @@ describe('AccountUsageCell', () => {
await flushPromises()
// 始终拉 usagefetched data 优先显示(包含 window_stats
expect(getUsage).toHaveBeenCalledWith(2001)
// 单一数据源:始终使用 /usage API 返回值,忽略 codex 快照
expect(wrapper.text()).toContain('5h|18|900')
expect(wrapper.text()).toContain('7d|36|900')
})
@@ -326,7 +326,7 @@ describe('AccountUsageCell', () => {
// 手动刷新再拉一次
expect(getUsage).toHaveBeenCalledTimes(2)
expect(getUsage).toHaveBeenCalledWith(2010)
// fetched data 优先显示,包含 window_stats
// 单一数据源:始终使用 /usage API 值
expect(wrapper.text()).toContain('5h|18|900')
})
@@ -458,7 +458,7 @@ describe('AccountUsageCell', () => {
expect(wrapper.text()).toContain('5h|0|200')
})
it('OpenAI OAuth 已限额时首屏优先展示重新查询后的 usage而不是旧 codex 快照', async () => {
it('OpenAI OAuth 已限额时显示 /usage API 返回的限额数据', async () => {
getUsage.mockResolvedValue({
five_hour: {
utilization: 100,
@@ -515,7 +515,6 @@ describe('AccountUsageCell', () => {
expect(getUsage).toHaveBeenCalledWith(2004)
expect(wrapper.text()).toContain('5h|100|106540000')
expect(wrapper.text()).toContain('7d|100|106540000')
expect(wrapper.text()).not.toContain('5h|0|')
})
it('Key 账号会展示 today stats 徽章并带 A/U 提示', async () => {