diff --git a/frontend/src/api/endpoints/types/provider.ts b/frontend/src/api/endpoints/types/provider.ts index fb2d27970..46abebf12 100644 --- a/frontend/src/api/endpoints/types/provider.ts +++ b/frontend/src/api/endpoints/types/provider.ts @@ -529,8 +529,21 @@ export interface GeminiCliUpstreamMetadata { quota_by_model?: Record | null } +export interface ClaudeCodeUpstreamMetadata { + updated_at?: number + five_hour_used_percent?: number + five_hour_reset_at?: number + seven_day_used_percent?: number + seven_day_reset_at?: number + seven_day_sonnet_used_percent?: number + seven_day_sonnet_reset_at?: number + seven_day_fable_used_percent?: number + seven_day_fable_reset_at?: number +} + export interface UpstreamMetadata { codex?: CodexUpstreamMetadata + claude_code?: ClaudeCodeUpstreamMetadata antigravity?: AntigravityUpstreamMetadata kiro?: KiroUpstreamMetadata windsurf?: WindsurfUpstreamMetadata diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index 43967910e..851779a53 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -406,6 +406,42 @@ + +
+ +
+ + + +
+
AUTO_QUOTA_REFRESH_STALE_SECONDS) { + return true + } + } + + return false +} + function shouldAutoRefreshKiroQuota(): boolean { if (provider.value?.provider_type !== 'kiro') return false const now = Math.floor(Date.now() / 1000) @@ -2990,12 +3045,14 @@ async function autoRefreshQuotaInBackground(): Promise { if (refreshingQuota.value) return false const providerType = provider.value?.provider_type - if (providerType !== 'codex' && providerType !== 'gemini_cli' && providerType !== 'antigravity' && providerType !== 'kiro' && providerType !== 'windsurf' && providerType !== 'chatgpt_web' && providerType !== 'grok' && providerType !== 'xai') return false + if (providerType !== 'codex' && providerType !== 'claude_code' && providerType !== 'gemini_cli' && providerType !== 'antigravity' && providerType !== 'kiro' && providerType !== 'windsurf' && providerType !== 'chatgpt_web' && providerType !== 'grok' && providerType !== 'xai') return false // 检查是否需要刷新 let shouldRefresh = false if (providerType === 'codex') { shouldRefresh = shouldAutoRefreshCodexQuota() + } else if (providerType === 'claude_code') { + shouldRefresh = shouldAutoRefreshClaudeCodeQuota() } else if (providerType === 'gemini_cli') { shouldRefresh = shouldAutoRefreshGeminiCliQuota() } else if (providerType === 'antigravity') { @@ -3016,6 +3073,8 @@ async function autoRefreshQuotaInBackground(): Promise { let hadCachedQuota = false if (providerType === 'codex') { hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasCodexQuotaDisplayData(key)) + } else if (providerType === 'claude_code') { + hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasClaudeCodeQuotaDisplayData(key)) } else if (providerType === 'gemini_cli') { hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasGeminiCliQuotaDisplayData(key)) } else if (providerType === 'antigravity') { @@ -3564,6 +3623,71 @@ interface AntigravityQuotaItem { detail?: string } +interface ClaudeCodeQuotaItem { + code: string + label: string + usedPercent: number + remainingPercent: number + resetSeconds: number | null +} + +const CLAUDE_CODE_QUOTA_WINDOWS: Record = { + '5h': { labelKey: 'poolQuota.claudeCode.window5h', sortOrder: 0 }, + weekly: { labelKey: 'poolQuota.claudeCode.weekly', sortOrder: 1 }, + weekly_sonnet: { labelKey: 'poolQuota.claudeCode.weeklySonnet', sortOrder: 2 }, + weekly_fable: { labelKey: 'poolQuota.claudeCode.weeklyFable', sortOrder: 3 }, +} + +function getClaudeCodeQuotaUpdatedAt(key: EndpointAPIKey): number | undefined { + const quota = getQuotaSnapshotForProvider(key, 'claude_code') + const quotaUpdatedAt = getQuotaSnapshotUpdatedAt(quota) + if (typeof quotaUpdatedAt === 'number') return quotaUpdatedAt + const updatedAt = Number(key.upstream_metadata?.claude_code?.updated_at ?? NaN) + return Number.isFinite(updatedAt) && updatedAt > 0 ? updatedAt : undefined +} + +function hasClaudeCodeQuotaDisplayData(key: EndpointAPIKey): boolean { + return getClaudeCodeQuotaItems(key).length > 0 +} + +function getClaudeCodeQuotaItems(key: EndpointAPIKey): ClaudeCodeQuotaItem[] { + const quota = getQuotaSnapshotForProvider(key, 'claude_code') + const windows = quota?.windows + if (!quota || !Array.isArray(windows)) return [] + + return windows + .map((window) => { + const code = String(window?.code || '').trim().toLowerCase() + if (!code) return null + + const usedPercent = getQuotaWindowUsedPercent(window) + const remainingPercent = getQuotaWindowRemainingPercent(window) + if (usedPercent === undefined && remainingPercent === undefined) return null + + const normalizedUsedPercent = usedPercent !== undefined + ? usedPercent + : Math.max(100 - (remainingPercent ?? 0), 0) + const normalizedRemainingPercent = remainingPercent !== undefined + ? remainingPercent + : Math.max(100 - normalizedUsedPercent, 0) + const presentation = CLAUDE_CODE_QUOTA_WINDOWS[code] + + return { + item: { + code, + label: t(presentation?.labelKey ?? 'poolQuota.claudeCode.unknownWindow'), + usedPercent: normalizedUsedPercent, + remainingPercent: normalizedRemainingPercent, + resetSeconds: getQuotaWindowLiveResetSeconds(quota, window), + } satisfies ClaudeCodeQuotaItem, + sortOrder: presentation?.sortOrder ?? 9, + } + }) + .filter((entry): entry is { item: ClaudeCodeQuotaItem, sortOrder: number } => entry !== null) + .sort((a, b) => a.sortOrder - b.sortOrder) + .map(entry => entry.item) +} + interface GeminiCliQuotaItem { model: string label: string