mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 22:20:19 +08:00
fix(providers): retry initial empty quota before showing feedback
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user