fix: flaky WebSocket test, usage request queue, and test improvements

- Fix flaky WebSocket passthrough test: allow StatusNormalClosure after
  client close instead of requiring NoError (race condition fix)
- Fix ratelimit 401 test: use PlatformOpenAI instead of PlatformGemini
  for OAuth token cache invalidation scenario (more accurate)
- Add usageLoadQueue: Anthropic OAuth/setup-token accounts sharing the
  same proxy exit are serialized with 1-2s jitter to prevent upstream 429
- AccountUsageCell: add module-level usage cache (5min TTL), unmounted
  safety guard, and integrate enqueueUsageRequest for throttled fetching
This commit is contained in:
erio
2026-04-14 20:13:59 +08:00
parent 5240b44452
commit 3fa5b8bca5
5 changed files with 344 additions and 11 deletions

View File

@@ -439,15 +439,20 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api/admin'
import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types'
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
import { enqueueUsageRequest } from '@/utils/usageLoadQueue'
import { formatCompactNumber } from '@/utils/format'
import UsageProgressBar from './UsageProgressBar.vue'
import AccountQuotaInfo from './AccountQuotaInfo.vue'
// Module-level cache shared across all AccountUsageCell instances
const _usageCache = new Map<number, { data: AccountUsageInfo; ts: number }>()
const USAGE_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
const props = withDefaults(
defineProps<{
account: Account
@@ -465,6 +470,9 @@ const props = withDefaults(
const { t } = useI18n()
const desktopViewportQuery = '(min-width: 768px)'
const unmounted = ref(false)
onBeforeUnmount(() => { unmounted.value = true })
const loading = ref(false)
const activeQueryLoading = ref(false)
const error = ref<string | null>(null)
@@ -941,19 +949,36 @@ const isAnthropicOAuthOrSetupToken = computed(() => {
return props.account.platform === 'anthropic' && (props.account.type === 'oauth' || props.account.type === 'setup-token')
})
const loadUsage = async (source?: 'passive' | 'active') => {
const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?: boolean }) => {
if (!shouldFetchUsage.value) return
// Check cache
if (!options?.bypassCache) {
const cached = _usageCache.get(props.account.id)
if (cached && Date.now() - cached.ts < USAGE_CACHE_TTL) {
usageInfo.value = cached.data
loading.value = false
return
}
}
loading.value = true
error.value = null
try {
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, source)
const fetchFn = () => adminAPI.accounts.getUsage(props.account.id, options?.source)
const result = await enqueueUsageRequest(props.account, fetchFn)
if (!unmounted.value) {
usageInfo.value = result
_usageCache.set(props.account.id, { data: result, ts: Date.now() })
}
} catch (e: any) {
error.value = t('common.error')
console.error('Failed to load usage:', e)
if (!unmounted.value) {
error.value = t('common.error')
console.error('Failed to load usage:', e)
}
} finally {
loading.value = false
if (!unmounted.value) loading.value = false
}
}
@@ -962,7 +987,7 @@ const flushPendingAutoLoad = () => {
const source = pendingAutoLoadSource.value
pendingAutoLoad.value = false
pendingAutoLoadSource.value = undefined
loadUsage(source).catch((e) => {
loadUsage({ source }).catch((e) => {
console.error('Failed to load deferred usage:', e)
})
}
@@ -974,7 +999,7 @@ const requestAutoLoad = (source?: 'passive' | 'active') => {
pendingAutoLoadSource.value = source
return
}
loadUsage(source).catch((e) => {
loadUsage({ source }).catch((e) => {
console.error('Failed to auto load usage:', e)
})
}
@@ -1138,7 +1163,10 @@ watch(
if (!shouldFetchUsage.value) return
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
requestAutoLoad(source)
_usageCache.delete(props.account.id)
loadUsage({ source, bypassCache: true }).catch((e) => {
console.error('Failed to refresh usage after manual refresh:', e)
})
}
)