fix: throttle Anthropic usage queries and pass through upstream HTTP errors

- Frontend: queue Anthropic OAuth/setup-token usage requests by proxy
  with random 1-1.5s interval to prevent upstream 429
- Backend: return ApplicationError with actual upstream status code
  instead of wrapping all errors as 500
- Handle component unmount to skip stale updates on page navigation
This commit is contained in:
erio
2026-03-05 19:12:49 +08:00
parent 9245e197a8
commit 99331a5285
6 changed files with 194 additions and 7 deletions

View File

@@ -278,11 +278,12 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api/admin'
import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types'
import { resolveCodexUsageWindow } from '@/utils/codexUsage'
import { enqueueUsageRequest } from '@/utils/usageLoadQueue'
import UsageProgressBar from './UsageProgressBar.vue'
import AccountQuotaInfo from './AccountQuotaInfo.vue'
@@ -292,6 +293,9 @@ const props = defineProps<{
const { t } = useI18n()
const unmounted = ref(false)
onBeforeUnmount(() => { unmounted.value = true })
const loading = ref(false)
const error = ref<string | null>(null)
const usageInfo = ref<AccountUsageInfo | null>(null)
@@ -701,12 +705,30 @@ const loadUsage = async () => {
error.value = null
try {
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id)
const fetchFn = () => adminAPI.accounts.getUsage(props.account.id)
let result: AccountUsageInfo
// Only throttle Anthropic OAuth/setup-token accounts to avoid upstream 429
if (
props.account.platform === 'anthropic' &&
(props.account.type === 'oauth' || props.account.type === 'setup-token')
) {
result = await enqueueUsageRequest(
props.account.platform,
'claude_code',
props.account.proxy_id,
fetchFn
)
} else {
result = await fetchFn()
}
if (!unmounted.value) usageInfo.value = result
} 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
}
}

View File

@@ -14,6 +14,10 @@ vi.mock('@/api/admin', () => ({
}
}))
vi.mock('@/utils/usageLoadQueue', () => ({
enqueueUsageRequest: (_p: string, _t: string, _id: unknown, fn: () => Promise<unknown>) => fn()
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {