Files
Aether/frontend/src/utils/providerKeyQuota.ts

258 lines
9.2 KiB
TypeScript
Raw Normal View History

import type {
ProviderKeyStatusSnapshot,
QuotaStatusSnapshot,
QuotaWindowSnapshot,
} from '@/api/endpoints/types/statusSnapshot'
export interface ProviderKeyQuotaCarrier {
account_quota?: string | null
status_snapshot?: ProviderKeyStatusSnapshot | null
}
function normalizeText(value: unknown): string | null {
if (typeof value !== 'string') return null
const text = value.trim()
return text || null
}
function clampPercent(value: number): number {
if (!Number.isFinite(value)) return 0
if (value < 0) return 0
if (value > 100) return 100
return value
}
function formatPercent(value: number): string {
return `${clampPercent(value).toFixed(1)}%`
}
function getQuotaSnapshot(
input: ProviderKeyQuotaCarrier,
): QuotaStatusSnapshot | null {
return input.status_snapshot?.quota ?? null
}
function getQuotaProviderType(
quota: QuotaStatusSnapshot | null | undefined,
fallbackProviderType?: string | null,
): string {
const snapshotProviderType = normalizeText(quota?.provider_type)?.toLowerCase()
if (snapshotProviderType) return snapshotProviderType
return normalizeText(fallbackProviderType)?.toLowerCase() || ''
}
function getQuotaWindows(
quota: QuotaStatusSnapshot | null | undefined,
): QuotaWindowSnapshot[] {
return Array.isArray(quota?.windows) ? quota.windows : []
}
function getQuotaWindowRemainingPercent(
window: QuotaWindowSnapshot | null | undefined,
): number | null {
if (!window) return null
if (typeof window.remaining_ratio === 'number') {
return clampPercent(window.remaining_ratio * 100)
}
if (typeof window.used_ratio === 'number') {
return clampPercent((1 - window.used_ratio) * 100)
}
if (typeof window.limit_value === 'number' && window.limit_value > 0) {
if (typeof window.remaining_value === 'number') {
return clampPercent((window.remaining_value / window.limit_value) * 100)
}
if (typeof window.used_value === 'number') {
return clampPercent((1 - (window.used_value / window.limit_value)) * 100)
}
}
return null
}
function getQuotaWindow(
quota: QuotaStatusSnapshot | null | undefined,
code: string,
): QuotaWindowSnapshot | null {
const normalizedCode = code.trim().toLowerCase()
return getQuotaWindows(quota).find(window => normalizeText(window.code)?.toLowerCase() === normalizedCode) ?? null
}
function getQuotaWindowsByScope(
quota: QuotaStatusSnapshot | null | undefined,
scope: string,
): QuotaWindowSnapshot[] {
const normalizedScope = scope.trim().toLowerCase()
return getQuotaWindows(quota).filter(window => normalizeText(window.scope)?.toLowerCase() === normalizedScope)
}
function formatQuotaValue(value: number | null | undefined): string {
const normalized = Number(value)
if (!Number.isFinite(normalized)) return '0'
const rounded = Math.round(normalized)
if (Math.abs(normalized - rounded) < 1e-6) {
return String(rounded)
}
return normalized.toFixed(1)
}
function getCodexQuotaText(quota: QuotaStatusSnapshot): string | null {
const parts: string[] = []
for (const [label, code] of [
['周', 'weekly'],
['5H', '5h'],
['Spark5H', 'spark_5h'],
['Spark周', 'spark_weekly'],
] as const) {
const remainingPercent = getQuotaWindowRemainingPercent(getQuotaWindow(quota, code))
if (remainingPercent == null) continue
parts.push(`${label}剩余 ${formatPercent(remainingPercent)}`)
}
if (parts.length > 0) return parts.join(' | ')
if (quota.credits?.has_credits === true && typeof quota.credits.balance === 'number') {
return `积分 ${quota.credits.balance.toFixed(2)}`
}
if (quota.credits?.has_credits === true) return '有积分'
if (quota.credits?.has_credits === false) return '无可用积分'
return normalizeText(quota.label)
}
function getKiroQuotaText(quota: QuotaStatusSnapshot): string | null {
const code = normalizeText(quota.code)?.toLowerCase()
if (code === 'banned') {
return normalizeText(quota.label) || '账号已封禁'
}
const window = getQuotaWindow(quota, 'usage') ?? getQuotaWindowsByScope(quota, 'account')[0] ?? null
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (typeof window?.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0 && window.remaining_value <= 0) {
return `剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
}
if (remainingPercent != null) {
if (typeof window?.used_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
return `剩余 ${formatPercent(remainingPercent)} (${formatQuotaValue(window.used_value)}/${formatQuotaValue(window.limit_value)})`
}
return `剩余 ${formatPercent(remainingPercent)}`
}
if (typeof window?.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
return `剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
}
return normalizeText(quota.label)
}
function getAntigravityQuotaText(quota: QuotaStatusSnapshot): string | null {
const code = normalizeText(quota.code)?.toLowerCase()
if (code === 'forbidden') {
return normalizeText(quota.label) || '访问受限'
}
const remainingList = getQuotaWindowsByScope(quota, 'model')
.map(getQuotaWindowRemainingPercent)
.filter((value): value is number => value != null)
if (remainingList.length === 0) return normalizeText(quota.label)
const minimumRemaining = Math.min(...remainingList)
if (remainingList.length === 1) {
return `剩余 ${formatPercent(minimumRemaining)}`
}
return `最低剩余 ${formatPercent(minimumRemaining)} (${remainingList.length} 模型)`
}
function getGeminiCliQuotaText(quota: QuotaStatusSnapshot): string | null {
const modelWindows = getQuotaWindowsByScope(quota, 'model')
const activeCoolingModels = modelWindows
.filter((window) => {
if (window.is_exhausted === true) return true
if (typeof window.used_ratio === 'number') return window.used_ratio >= 1.0 - 1e-6
return false
})
.filter((window) => {
if (typeof window.reset_at !== 'number') return true
return window.reset_at > Math.floor(Date.now() / 1000)
})
.map((window) => normalizeText(window.label) || normalizeText(window.model) || '模型')
if (activeCoolingModels.length === 1) {
return `${activeCoolingModels[0]} 冷却中`
}
if (activeCoolingModels.length > 1) {
return `${activeCoolingModels.length} 个模型冷却中`
}
const remainingList = modelWindows
.map(getQuotaWindowRemainingPercent)
.filter((value): value is number => value != null)
if (remainingList.length === 0) return normalizeText(quota.label)
const minimumRemaining = Math.min(...remainingList)
if (remainingList.length === 1) {
return `剩余 ${formatPercent(minimumRemaining)}`
}
return `最低剩余 ${formatPercent(minimumRemaining)} (${remainingList.length} 模型)`
}
2026-05-06 22:58:47 +08:00
function getChatGPTWebQuotaText(quota: QuotaStatusSnapshot): string | null {
const window = getQuotaWindow(quota, 'image_gen') ?? getQuotaWindowsByScope(quota, 'account')[0] ?? null
if (!window) return normalizeText(quota.label)
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (typeof window.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0 && window.remaining_value <= 0) {
return `生图剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
}
if (remainingPercent != null) {
if (typeof window.used_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
return `生图剩余 ${formatPercent(remainingPercent)} (${formatQuotaValue(window.used_value)}/${formatQuotaValue(window.limit_value)})`
}
return `生图剩余 ${formatPercent(remainingPercent)}`
}
if (typeof window.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
return `生图剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
}
if (typeof window.remaining_value === 'number') {
return `生图剩余 ${formatQuotaValue(window.remaining_value)}`
}
return normalizeText(quota.label)
}
export function getLegacyAccountQuotaText(
input: ProviderKeyQuotaCarrier,
): string | null {
return normalizeText(input.account_quota)
}
export function getQuotaSnapshotFallbackText(
input: ProviderKeyQuotaCarrier,
fallbackProviderType?: string | null,
): string | null {
const quota = getQuotaSnapshot(input)
if (!quota) return null
const providerType = getQuotaProviderType(quota, fallbackProviderType)
switch (providerType) {
case 'codex':
return getCodexQuotaText(quota)
case 'kiro':
return getKiroQuotaText(quota)
case 'antigravity':
return getAntigravityQuotaText(quota)
case 'gemini_cli':
return getGeminiCliQuotaText(quota)
2026-05-06 22:58:47 +08:00
case 'chatgpt_web':
return getChatGPTWebQuotaText(quota)
default:
return normalizeText(quota.label)
}
}
export function getQuotaDisplayText(
input: ProviderKeyQuotaCarrier,
fallbackProviderType?: string | null,
): string | null {
return getQuotaSnapshotFallbackText(input, fallbackProviderType) || getLegacyAccountQuotaText(input)
}