fix(providers): retry initial empty quota before showing feedback

This commit is contained in:
elky
2026-09-07 11:18:31 +08:00
parent 741107bf71
commit 808946312a
4 changed files with 126 additions and 4 deletions
@@ -980,6 +980,7 @@ import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQu
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
import { useProxyNodesStore } from '@/stores/proxy-nodes'
import { resolveAntigravityQuotaGroupLabel } from '@/features/providers/utils/antigravityQuota'
import { refreshQuotaInBackground } from '@/features/providers/utils/refreshQuotaInBackground'
import {
deleteEndpointKey,
recoverKeyHealth,
@@ -2862,15 +2863,21 @@ async function autoRefreshQuotaInBackground(): Promise<boolean> {
}
refreshingQuota.value = true
const isCurrent = () => props.open && props.providerId === providerId
try {
const result = await refreshProviderQuota(providerId)
const result = await refreshQuotaInBackground({
refresh: () => refreshProviderQuota(providerId),
isCurrent,
retryInitialEmptyQuota: providerType === 'antigravity' && !hadCachedQuota,
})
if (!result) return false
const applied = applyQuotaResults(result.results)
if (result.success <= 0 && applied === 0 && !hadCachedQuota && providerType === 'antigravity') {
showError(legacyT('没有获取到配额信息(请检查账号是否已授权、project_id 是否存在)'), legacyT('提示'))
showWarning(legacyT('配额暂未就绪,请稍后刷新'), legacyT('提示'))
}
return applied > 0
} catch (err: unknown) {
if (!hadCachedQuota && providerType === 'antigravity') {
if (isCurrent() && !hadCachedQuota && providerType === 'antigravity') {
showError(localizedApiError(err, '后台刷新配额失败'), legacyT('错误'))
}
return false
@@ -0,0 +1,80 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RefreshQuotaResult } from '@/api/endpoints/keys'
import { refreshQuotaInBackground } from '../refreshQuotaInBackground'
function emptyQuota(status: 'error' | 'no_metadata' | 'forbidden' = 'no_metadata'): RefreshQuotaResult {
return {
success: 0,
failed: 1,
total: 1,
results: [{ key_id: 'key-1', key_name: 'account', status }],
}
}
const readyQuota: RefreshQuotaResult = {
success: 1,
failed: 0,
total: 1,
results: [{ key_id: 'key-1', key_name: 'account', status: 'success' }],
}
afterEach(() => vi.useRealTimers())
describe('initial background quota refresh', () => {
it('waits and retries an initial empty response once', async () => {
vi.useFakeTimers()
const refresh = vi.fn().mockResolvedValueOnce(emptyQuota()).mockResolvedValueOnce(readyQuota)
const pending = refreshQuotaInBackground({ refresh, isCurrent: () => true, retryInitialEmptyQuota: true })
await vi.advanceTimersByTimeAsync(999)
expect(refresh).toHaveBeenCalledTimes(1)
await vi.advanceTimersByTimeAsync(1)
expect(await pending).toBe(readyQuota)
expect(refresh).toHaveBeenCalledTimes(2)
})
it('returns persistent failures after at most one retry', async () => {
vi.useFakeTimers()
const result = emptyQuota('error')
const refresh = vi.fn().mockResolvedValue(result)
const pending = refreshQuotaInBackground({ refresh, isCurrent: () => true, retryInitialEmptyQuota: true })
await vi.runAllTimersAsync()
expect(await pending).toBe(result)
expect(refresh).toHaveBeenCalledTimes(2)
})
it.each([readyQuota, emptyQuota('forbidden'), {
...emptyQuota('error'),
results: [{ ...emptyQuota('error').results[0]!, status_code: 401 }],
}])('does not retry successful or rejected authorization results', async (result) => {
const refresh = vi.fn().mockResolvedValue(result)
expect(await refreshQuotaInBackground({ refresh, isCurrent: () => true, retryInitialEmptyQuota: true })).toBe(result)
expect(refresh).toHaveBeenCalledTimes(1)
})
it('does not retry when initial quota retry is disabled', async () => {
const refresh = vi.fn().mockResolvedValue(emptyQuota())
await refreshQuotaInBackground({ refresh, isCurrent: () => true, retryInitialEmptyQuota: false })
expect(refresh).toHaveBeenCalledTimes(1)
})
it('cancels the retry when the drawer closes or switches providers', async () => {
vi.useFakeTimers()
let current = true
const refresh = vi.fn().mockResolvedValue(emptyQuota())
const pending = refreshQuotaInBackground({ refresh, isCurrent: () => current, retryInitialEmptyQuota: true })
await vi.advanceTimersByTimeAsync(1)
current = false
await vi.runAllTimersAsync()
expect(await pending).toBeNull()
expect(refresh).toHaveBeenCalledTimes(1)
})
it('discards an in-flight response from a closed drawer', async () => {
let current = true
const refresh = vi.fn(async () => {
current = false
return readyQuota
})
expect(await refreshQuotaInBackground({ refresh, isCurrent: () => current, retryInitialEmptyQuota: true })).toBeNull()
})
})
@@ -0,0 +1,35 @@
import type { RefreshQuotaResult } from '@/api/endpoints/keys'
interface BackgroundQuotaRefreshOptions {
refresh: () => Promise<RefreshQuotaResult>
isCurrent: () => boolean
retryInitialEmptyQuota: boolean
}
function shouldRetryEmptyQuota(result: RefreshQuotaResult): boolean {
return result.success === 0
&& result.results.length > 0
&& result.results.every(item =>
(item.status === 'no_metadata' || item.status === 'error')
&& item.status_code !== 401
&& item.status_code !== 403
&& !item.metadata
&& !item.quota_snapshot,
)
}
export async function refreshQuotaInBackground({
refresh,
isCurrent,
retryInitialEmptyQuota,
}: BackgroundQuotaRefreshOptions): Promise<RefreshQuotaResult | null> {
if (!isCurrent()) return null
const result = await refresh()
if (!isCurrent()) return null
if (!retryInitialEmptyQuota || !shouldRetryEmptyQuota(result)) return result
await new Promise<void>(resolve => setTimeout(resolve, 1000))
if (!isCurrent()) return null
const retried = await refresh()
return isCurrent() ? retried : null
}
+1 -1
View File
@@ -2158,7 +2158,7 @@ const legacyExactEnglishMessages: Record<string, string> = {
'倍率必须在 0.01 到 100 之间': 'Multiplier must be between 0.01 and 100',
'切换格式转换失败': 'Failed to toggle format conversion',
'后台刷新配额失败': 'Failed to refresh quota in the background',
'没有获取到配额信息(请检查账号是否已授权、project_id 是否存在)': 'No quota information was returned. Check whether the account is authorized and project_id exists.',
'配额暂未就绪,请稍后刷新': 'Quota information is not ready yet. Please refresh again shortly.',
'Provider 不存在': 'Provider does not exist',
'未知节点': 'Unknown node',
'冷却中': 'Cooling down',