mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +08:00
feat(claude-code): 提供商详情抽屉展示账号 5H/周额度
This commit is contained in:
@@ -529,8 +529,21 @@ export interface GeminiCliUpstreamMetadata {
|
|||||||
quota_by_model?: Record<string, GeminiCliModelQuota> | null
|
quota_by_model?: Record<string, GeminiCliModelQuota> | 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 {
|
export interface UpstreamMetadata {
|
||||||
codex?: CodexUpstreamMetadata
|
codex?: CodexUpstreamMetadata
|
||||||
|
claude_code?: ClaudeCodeUpstreamMetadata
|
||||||
antigravity?: AntigravityUpstreamMetadata
|
antigravity?: AntigravityUpstreamMetadata
|
||||||
kiro?: KiroUpstreamMetadata
|
kiro?: KiroUpstreamMetadata
|
||||||
windsurf?: WindsurfUpstreamMetadata
|
windsurf?: WindsurfUpstreamMetadata
|
||||||
|
|||||||
@@ -406,6 +406,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Claude Code 账号额度(5H / 周窗口) -->
|
||||||
|
<div
|
||||||
|
v-if="provider.provider_type === 'claude_code' && hasClaudeCodeQuotaDisplayData(key)"
|
||||||
|
class="mt-2 p-2 rounded-md bg-muted/30"
|
||||||
|
>
|
||||||
|
<ProviderQuotaSectionHeader
|
||||||
|
:title="legacyT('账号配额')"
|
||||||
|
:loading="refreshingQuota"
|
||||||
|
:updated-text="getClaudeCodeQuotaUpdatedAt(key) ? formatUpdatedAt(getClaudeCodeQuotaUpdatedAt(key) || 0) : null"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<ProviderQuotaProgressRow
|
||||||
|
v-for="item in getClaudeCodeQuotaItems(key)"
|
||||||
|
:key="item.code"
|
||||||
|
:label="item.label"
|
||||||
|
:used-percent="item.usedPercent"
|
||||||
|
:remaining-percent="item.remainingPercent"
|
||||||
|
:meter-class="getQuotaRemainingClass(item.usedPercent)"
|
||||||
|
:bar-class="getQuotaRemainingBarColor(item.usedPercent)"
|
||||||
|
>
|
||||||
|
<template #footer>
|
||||||
|
<div
|
||||||
|
v-if="item.resetSeconds !== null && item.remainingPercent < 100"
|
||||||
|
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||||
|
>
|
||||||
|
<template v-if="item.resetSeconds > 0">
|
||||||
|
{{ formatResetTime(item.resetSeconds) }}{{ legacyT('后重置') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ legacyT('已重置') }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ProviderQuotaProgressRow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Gemini CLI 上游模型配额 -->
|
<!-- Gemini CLI 上游模型配额 -->
|
||||||
<div
|
<div
|
||||||
v-if="provider.provider_type === 'gemini_cli' && hasGeminiCliQuotaDisplayData(key)"
|
v-if="provider.provider_type === 'gemini_cli' && hasGeminiCliQuotaDisplayData(key)"
|
||||||
@@ -1000,6 +1036,7 @@ import {
|
|||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||||
import { getI18nLocale, useI18n } from '@/i18n'
|
import { getI18nLocale, useI18n } from '@/i18n'
|
||||||
|
import type { MessageKey } from '@/i18n/messages'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
@@ -1918,7 +1955,7 @@ function quotaSnapshotHasDisplayData(quota: QuotaStatusSnapshot | null | undefin
|
|||||||
|
|
||||||
function getQuotaSnapshotForProvider(
|
function getQuotaSnapshotForProvider(
|
||||||
key: EndpointAPIKey,
|
key: EndpointAPIKey,
|
||||||
providerType: 'codex' | 'kiro' | 'windsurf' | 'antigravity' | 'chatgpt_web' | 'gemini_cli' | 'grok' | 'xai',
|
providerType: 'codex' | 'kiro' | 'windsurf' | 'antigravity' | 'chatgpt_web' | 'gemini_cli' | 'claude_code' | 'grok' | 'xai',
|
||||||
): QuotaStatusSnapshot | null {
|
): QuotaStatusSnapshot | null {
|
||||||
const quota = key.status_snapshot?.quota
|
const quota = key.status_snapshot?.quota
|
||||||
if (!quota) return null
|
if (!quota) return null
|
||||||
@@ -2795,6 +2832,24 @@ function shouldAutoRefreshGeminiCliQuota(): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldAutoRefreshClaudeCodeQuota(): boolean {
|
||||||
|
if (provider.value?.provider_type !== 'claude_code') return false
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
for (const { key } of allKeys.value) {
|
||||||
|
if (!key.is_active) continue
|
||||||
|
if (isTokenExpiringSoon(key, now)) return true
|
||||||
|
if (!hasClaudeCodeQuotaDisplayData(key)) return true
|
||||||
|
|
||||||
|
const updatedAt = getClaudeCodeQuotaUpdatedAt(key)
|
||||||
|
if (typeof updatedAt !== 'number' || (now - updatedAt) > AUTO_QUOTA_REFRESH_STALE_SECONDS) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function shouldAutoRefreshKiroQuota(): boolean {
|
function shouldAutoRefreshKiroQuota(): boolean {
|
||||||
if (provider.value?.provider_type !== 'kiro') return false
|
if (provider.value?.provider_type !== 'kiro') return false
|
||||||
const now = Math.floor(Date.now() / 1000)
|
const now = Math.floor(Date.now() / 1000)
|
||||||
@@ -2990,12 +3045,14 @@ async function autoRefreshQuotaInBackground(): Promise<boolean> {
|
|||||||
if (refreshingQuota.value) return false
|
if (refreshingQuota.value) return false
|
||||||
|
|
||||||
const providerType = provider.value?.provider_type
|
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
|
let shouldRefresh = false
|
||||||
if (providerType === 'codex') {
|
if (providerType === 'codex') {
|
||||||
shouldRefresh = shouldAutoRefreshCodexQuota()
|
shouldRefresh = shouldAutoRefreshCodexQuota()
|
||||||
|
} else if (providerType === 'claude_code') {
|
||||||
|
shouldRefresh = shouldAutoRefreshClaudeCodeQuota()
|
||||||
} else if (providerType === 'gemini_cli') {
|
} else if (providerType === 'gemini_cli') {
|
||||||
shouldRefresh = shouldAutoRefreshGeminiCliQuota()
|
shouldRefresh = shouldAutoRefreshGeminiCliQuota()
|
||||||
} else if (providerType === 'antigravity') {
|
} else if (providerType === 'antigravity') {
|
||||||
@@ -3016,6 +3073,8 @@ async function autoRefreshQuotaInBackground(): Promise<boolean> {
|
|||||||
let hadCachedQuota = false
|
let hadCachedQuota = false
|
||||||
if (providerType === 'codex') {
|
if (providerType === 'codex') {
|
||||||
hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasCodexQuotaDisplayData(key))
|
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') {
|
} else if (providerType === 'gemini_cli') {
|
||||||
hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasGeminiCliQuotaDisplayData(key))
|
hadCachedQuota = allKeys.value.some(({ key }) => key.is_active && hasGeminiCliQuotaDisplayData(key))
|
||||||
} else if (providerType === 'antigravity') {
|
} else if (providerType === 'antigravity') {
|
||||||
@@ -3564,6 +3623,71 @@ interface AntigravityQuotaItem {
|
|||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClaudeCodeQuotaItem {
|
||||||
|
code: string
|
||||||
|
label: string
|
||||||
|
usedPercent: number
|
||||||
|
remainingPercent: number
|
||||||
|
resetSeconds: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLAUDE_CODE_QUOTA_WINDOWS: Record<string, { labelKey: MessageKey, sortOrder: number }> = {
|
||||||
|
'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 {
|
interface GeminiCliQuotaItem {
|
||||||
model: string
|
model: string
|
||||||
label: string
|
label: string
|
||||||
|
|||||||
Reference in New Issue
Block a user