@@ -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
diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts
index e8fc5346..9158da64 100644
--- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts
+++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts
@@ -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()
- // 始终拉 usage,fetched 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 () => {
diff --git a/frontend/src/utils/__tests__/codexUsage.spec.ts b/frontend/src/utils/__tests__/codexUsage.spec.ts
deleted file mode 100644
index cea8abe2..00000000
--- a/frontend/src/utils/__tests__/codexUsage.spec.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { resolveCodexUsageWindow } from '@/utils/codexUsage'
-
-describe('resolveCodexUsageWindow', () => {
- it('快照为空时返回空窗口', () => {
- const result = resolveCodexUsageWindow(null, '5h', new Date('2026-02-20T08:00:00Z'))
- expect(result).toEqual({ usedPercent: null, resetAt: null })
- })
-
- it('优先使用后端提供的绝对重置时间', () => {
- const now = new Date('2026-02-20T08:00:00Z')
- const result = resolveCodexUsageWindow(
- {
- codex_5h_used_percent: 55,
- codex_5h_reset_at: '2026-02-20T10:00:00Z',
- codex_5h_reset_after_seconds: 1
- },
- '5h',
- now
- )
-
- expect(result.usedPercent).toBe(55)
- expect(result.resetAt).toBe('2026-02-20T10:00:00.000Z')
- })
-
- it('窗口已过期时自动归零', () => {
- const now = new Date('2026-02-20T08:00:00Z')
- const result = resolveCodexUsageWindow(
- {
- codex_7d_used_percent: 100,
- codex_7d_reset_at: '2026-02-20T07:00:00Z'
- },
- '7d',
- now
- )
-
- expect(result.usedPercent).toBe(0)
- expect(result.resetAt).toBe('2026-02-20T07:00:00.000Z')
- })
-
- it('无绝对时间时使用 updated_at + seconds 回退计算', () => {
- const now = new Date('2026-02-20T07:00:00Z')
- const result = resolveCodexUsageWindow(
- {
- codex_5h_used_percent: 20,
- codex_5h_reset_after_seconds: 3600,
- codex_usage_updated_at: '2026-02-20T06:30:00Z'
- },
- '5h',
- now
- )
-
- expect(result.usedPercent).toBe(20)
- expect(result.resetAt).toBe('2026-02-20T07:30:00.000Z')
- })
-
- it('支持 legacy primary/secondary 字段映射', () => {
- const now = new Date('2026-02-20T07:05:00Z')
- const result5h = resolveCodexUsageWindow(
- {
- codex_primary_window_minutes: 10080,
- codex_primary_used_percent: 70,
- codex_primary_reset_after_seconds: 86400,
- codex_secondary_window_minutes: 300,
- codex_secondary_used_percent: 15,
- codex_secondary_reset_after_seconds: 1200,
- codex_usage_updated_at: '2026-02-20T07:00:00Z'
- },
- '5h',
- now
- )
- const result7d = resolveCodexUsageWindow(
- {
- codex_primary_window_minutes: 10080,
- codex_primary_used_percent: 70,
- codex_primary_reset_after_seconds: 86400,
- codex_secondary_window_minutes: 300,
- codex_secondary_used_percent: 15,
- codex_secondary_reset_after_seconds: 1200,
- codex_usage_updated_at: '2026-02-20T07:00:00Z'
- },
- '7d',
- now
- )
-
- expect(result5h.usedPercent).toBe(15)
- expect(result5h.resetAt).toBe('2026-02-20T07:20:00.000Z')
- expect(result7d.usedPercent).toBe(70)
- expect(result7d.resetAt).toBe('2026-02-21T07:00:00.000Z')
- })
-
- it('legacy 5h 在 primary<=360 时优先 primary 并支持字符串数字', () => {
- const result = resolveCodexUsageWindow(
- {
- codex_primary_window_minutes: '300',
- codex_primary_used_percent: '21',
- codex_primary_reset_after_seconds: '1800',
- codex_secondary_window_minutes: '10080',
- codex_secondary_used_percent: '99',
- codex_secondary_reset_after_seconds: '99999',
- codex_usage_updated_at: '2026-02-20T08:00:00Z'
- },
- '5h',
- new Date('2026-02-20T08:10:00Z')
- )
-
- expect(result.usedPercent).toBe(21)
- expect(result.resetAt).toBe('2026-02-20T08:30:00.000Z')
- })
-
- it('legacy 5h 在无窗口信息时回退 secondary', () => {
- const result = resolveCodexUsageWindow(
- {
- codex_secondary_used_percent: 19,
- codex_secondary_reset_after_seconds: 120,
- codex_usage_updated_at: '2026-02-20T08:00:00Z'
- },
- '5h',
- new Date('2026-02-20T08:00:01Z')
- )
-
- expect(result.usedPercent).toBe(19)
- expect(result.resetAt).toBe('2026-02-20T08:02:00.000Z')
- })
-
- it('legacy 场景下 secondary 为 7d 时能正确识别', () => {
- const now = new Date('2026-02-20T07:30:00Z')
- const result = resolveCodexUsageWindow(
- {
- codex_primary_window_minutes: 300,
- codex_primary_used_percent: 5,
- codex_primary_reset_after_seconds: 600,
- codex_secondary_window_minutes: 10080,
- codex_secondary_used_percent: 66,
- codex_secondary_reset_after_seconds: 7200,
- codex_usage_updated_at: '2026-02-20T07:00:00Z'
- },
- '7d',
- now
- )
-
- expect(result.usedPercent).toBe(66)
- expect(result.resetAt).toBe('2026-02-20T09:00:00.000Z')
- })
-
- it('绝对时间非法时回退到 updated_at + seconds', () => {
- const now = new Date('2026-02-20T07:40:00Z')
- const result = resolveCodexUsageWindow(
- {
- codex_5h_used_percent: 33,
- codex_5h_reset_at: 'not-a-date',
- codex_5h_reset_after_seconds: 900,
- codex_usage_updated_at: '2026-02-20T07:30:00Z'
- },
- '5h',
- now
- )
-
- expect(result.usedPercent).toBe(33)
- expect(result.resetAt).toBe('2026-02-20T07:45:00.000Z')
- })
-
- it('updated_at 非法且无绝对时间时 resetAt 返回 null', () => {
- const result = resolveCodexUsageWindow(
- {
- codex_5h_used_percent: 10,
- codex_5h_reset_after_seconds: 123,
- codex_usage_updated_at: 'invalid-time'
- },
- '5h',
- new Date('2026-02-20T08:00:00Z')
- )
-
- expect(result.usedPercent).toBe(10)
- expect(result.resetAt).toBeNull()
- })
-
- it('reset_after_seconds 为负数时按 0 秒处理', () => {
- const result = resolveCodexUsageWindow(
- {
- codex_5h_used_percent: 80,
- codex_5h_reset_after_seconds: -30,
- codex_usage_updated_at: '2026-02-20T08:00:00Z'
- },
- '5h',
- new Date('2026-02-20T07:59:00Z')
- )
-
- expect(result.usedPercent).toBe(80)
- expect(result.resetAt).toBe('2026-02-20T08:00:00.000Z')
- })
-
- it('百分比缺失时仍可计算 resetAt 供倒计时展示', () => {
- const result = resolveCodexUsageWindow(
- {
- codex_7d_reset_after_seconds: 60,
- codex_usage_updated_at: '2026-02-20T08:00:00Z'
- },
- '7d',
- new Date('2026-02-20T08:00:01Z')
- )
-
- expect(result.usedPercent).toBeNull()
- expect(result.resetAt).toBe('2026-02-20T08:01:00.000Z')
- })
-})
diff --git a/frontend/src/utils/codexUsage.ts b/frontend/src/utils/codexUsage.ts
deleted file mode 100644
index abe09d74..00000000
--- a/frontend/src/utils/codexUsage.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import type { CodexUsageSnapshot } from '@/types'
-
-export interface ResolvedCodexUsageWindow {
- usedPercent: number | null
- resetAt: string | null
-}
-
-type WindowKind = '5h' | '7d'
-
-function asNumber(value: unknown): number | null {
- if (typeof value === 'number' && Number.isFinite(value)) return value
- if (typeof value === 'string' && value.trim() !== '') {
- const n = Number(value)
- if (Number.isFinite(n)) return n
- }
- return null
-}
-
-function asString(value: unknown): string | null {
- if (typeof value !== 'string') return null
- const trimmed = value.trim()
- return trimmed === '' ? null : trimmed
-}
-
-function asISOTime(value: unknown): string | null {
- const raw = asString(value)
- if (!raw) return null
- const date = new Date(raw)
- if (Number.isNaN(date.getTime())) return null
- return date.toISOString()
-}
-
-function resolveLegacy5h(snapshot: Record
): { used: number | null; resetAfterSeconds: number | null } {
- const primaryWindow = asNumber(snapshot.codex_primary_window_minutes)
- const secondaryWindow = asNumber(snapshot.codex_secondary_window_minutes)
- const primaryUsed = asNumber(snapshot.codex_primary_used_percent)
- const secondaryUsed = asNumber(snapshot.codex_secondary_used_percent)
- const primaryReset = asNumber(snapshot.codex_primary_reset_after_seconds)
- const secondaryReset = asNumber(snapshot.codex_secondary_reset_after_seconds)
-
- if (primaryWindow != null && primaryWindow <= 360) {
- return { used: primaryUsed, resetAfterSeconds: primaryReset }
- }
- if (secondaryWindow != null && secondaryWindow <= 360) {
- return { used: secondaryUsed, resetAfterSeconds: secondaryReset }
- }
- return { used: secondaryUsed, resetAfterSeconds: secondaryReset }
-}
-
-function resolveLegacy7d(snapshot: Record): { used: number | null; resetAfterSeconds: number | null } {
- const primaryWindow = asNumber(snapshot.codex_primary_window_minutes)
- const secondaryWindow = asNumber(snapshot.codex_secondary_window_minutes)
- const primaryUsed = asNumber(snapshot.codex_primary_used_percent)
- const secondaryUsed = asNumber(snapshot.codex_secondary_used_percent)
- const primaryReset = asNumber(snapshot.codex_primary_reset_after_seconds)
- const secondaryReset = asNumber(snapshot.codex_secondary_reset_after_seconds)
-
- if (primaryWindow != null && primaryWindow >= 10000) {
- return { used: primaryUsed, resetAfterSeconds: primaryReset }
- }
- if (secondaryWindow != null && secondaryWindow >= 10000) {
- return { used: secondaryUsed, resetAfterSeconds: secondaryReset }
- }
- return { used: primaryUsed, resetAfterSeconds: primaryReset }
-}
-
-function resolveFromSeconds(snapshot: Record, resetAfterSeconds: number | null): string | null {
- if (resetAfterSeconds == null) return null
-
- const baseRaw = asString(snapshot.codex_usage_updated_at)
- const base = baseRaw ? new Date(baseRaw) : new Date()
- if (Number.isNaN(base.getTime())) {
- return null
- }
-
- const sec = Math.max(0, resetAfterSeconds)
- const resetAt = new Date(base.getTime() + sec * 1000)
- return resetAt.toISOString()
-}
-
-function applyExpiredRule(window: ResolvedCodexUsageWindow, now: Date): ResolvedCodexUsageWindow {
- if (window.usedPercent == null || !window.resetAt) return window
- const resetDate = new Date(window.resetAt)
- if (Number.isNaN(resetDate.getTime())) return window
- if (resetDate.getTime() <= now.getTime()) {
- return { usedPercent: 0, resetAt: resetDate.toISOString() }
- }
- return window
-}
-
-export function resolveCodexUsageWindow(
- snapshot: (CodexUsageSnapshot & Record) | null | undefined,
- window: WindowKind,
- now: Date = new Date()
-): ResolvedCodexUsageWindow {
- if (!snapshot) {
- return { usedPercent: null, resetAt: null }
- }
-
- const typedSnapshot = snapshot as Record
- let usedPercent: number | null
- let resetAfterSeconds: number | null
- let resetAt: string | null
-
- if (window === '5h') {
- usedPercent = asNumber(typedSnapshot.codex_5h_used_percent)
- resetAfterSeconds = asNumber(typedSnapshot.codex_5h_reset_after_seconds)
- resetAt = asISOTime(typedSnapshot.codex_5h_reset_at)
- if (usedPercent == null || (resetAfterSeconds == null && !resetAt)) {
- const legacy = resolveLegacy5h(typedSnapshot)
- if (usedPercent == null) usedPercent = legacy.used
- if (resetAfterSeconds == null) resetAfterSeconds = legacy.resetAfterSeconds
- }
- } else {
- usedPercent = asNumber(typedSnapshot.codex_7d_used_percent)
- resetAfterSeconds = asNumber(typedSnapshot.codex_7d_reset_after_seconds)
- resetAt = asISOTime(typedSnapshot.codex_7d_reset_at)
- if (usedPercent == null || (resetAfterSeconds == null && !resetAt)) {
- const legacy = resolveLegacy7d(typedSnapshot)
- if (usedPercent == null) usedPercent = legacy.used
- if (resetAfterSeconds == null) resetAfterSeconds = legacy.resetAfterSeconds
- }
- }
-
- if (!resetAt) {
- resetAt = resolveFromSeconds(typedSnapshot, resetAfterSeconds)
- }
-
- return applyExpiredRule({ usedPercent, resetAt }, now)
-}