mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-08 20:20:19 +08:00
fix(frontend): deduplicate Antigravity quota groups
This commit is contained in:
@@ -31,6 +31,11 @@ export interface QuotaWindowSnapshot {
|
||||
scope?: 'account' | 'workspace' | 'model' | string
|
||||
unit?: 'percent' | 'count' | 'usd' | 'tokens' | string
|
||||
model?: string | null
|
||||
quota_group?: string | null
|
||||
quota_group_label?: string | null
|
||||
bucket_id?: string | null
|
||||
window?: string | null
|
||||
description?: string | null
|
||||
used_ratio?: number | null
|
||||
remaining_ratio?: number | null
|
||||
used_value?: number | null
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('pool key display panels', () => {
|
||||
const app = createApp(PoolKeyQuotaPanel, {
|
||||
items: [
|
||||
{
|
||||
label: 'Gemini额度',
|
||||
label: 'Gemini Models · 周',
|
||||
remainingPercent: 90.6,
|
||||
resetText: '1h 后重置',
|
||||
meterText: '90.6%–100%',
|
||||
@@ -104,7 +104,7 @@ describe('pool key display panels', () => {
|
||||
meterClass: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
label: 'Claude & ChatGPT',
|
||||
label: 'Claude and GPT models · 周',
|
||||
remainingPercent: 100,
|
||||
resetText: '1h 后重置',
|
||||
meterText: '100%',
|
||||
@@ -118,8 +118,8 @@ describe('pool key display panels', () => {
|
||||
|
||||
expect(root.querySelector('[data-testid="pool-quota-rows"]')?.className).toContain('space-y-2')
|
||||
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-period-label"]')).map(node => node.textContent)).toEqual([
|
||||
'Gemini额度',
|
||||
'Claude & ChatGPT',
|
||||
'Gemini Models · 周',
|
||||
'Claude and GPT models · 周',
|
||||
])
|
||||
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-meter-text"]')).map(node => node.textContent)).toEqual(['90.6%–100%', '100%'])
|
||||
expect(root.querySelectorAll('[data-testid="pool-quota-progress-track"]')).toHaveLength(2)
|
||||
|
||||
@@ -117,12 +117,13 @@ import Button from '@/components/ui/button.vue'
|
||||
import { testModel } from '@/api/endpoints/providers'
|
||||
import type { UpstreamMetadata, QuotaStatusSnapshot, QuotaWindowSnapshot } from '@/api/endpoints/types'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import {
|
||||
compareAntigravityQuotaItems,
|
||||
dedupeAntigravityQuotaItemsByLabel,
|
||||
resolveAntigravityQuotaGroupLabel,
|
||||
resolveAntigravityQuotaLabel,
|
||||
summarizeAntigravityQuotaItems,
|
||||
} from '@/features/providers/utils/antigravityQuota'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -148,6 +149,7 @@ interface QuotaItem {
|
||||
}
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
const { t } = useI18n()
|
||||
const testingModel = ref<string | null>(null)
|
||||
|
||||
function getQuotaSnapshotUpdatedAt(quota: QuotaStatusSnapshot | null | undefined): number | undefined {
|
||||
@@ -253,7 +255,7 @@ function buildGroupedItemsFromQuotaSnapshot(
|
||||
return windows
|
||||
.map((window) => {
|
||||
const code = String(window.code || '').trim()
|
||||
const label = String(window.label || code).trim()
|
||||
const label = resolveAntigravityQuotaGroupLabel(window, t)
|
||||
if (!code || !label) return null
|
||||
|
||||
const usedPercent =
|
||||
@@ -332,9 +334,8 @@ const rawItems = computed<QuotaItem[]>(() => {
|
||||
return dedupeAntigravityQuotaItemsByLabel(result)
|
||||
})
|
||||
|
||||
const items = computed<QuotaItem[]>(() => summarizeAntigravityQuotaItems(rawItems.value))
|
||||
const groupedItems = computed<QuotaItem[]>(() => buildGroupedItemsFromQuotaSnapshot(props.quotaSnapshot))
|
||||
const displayItems = computed<QuotaItem[]>(() => [...groupedItems.value, ...items.value])
|
||||
const displayItems = computed<QuotaItem[]>(() => groupedItems.value)
|
||||
|
||||
async function handleTestModel(modelName: string) {
|
||||
if (!props.providerId || testingModel.value) return
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<ProviderQuotaProgressRow
|
||||
v-for="item in getAntigravityQuotaSummaryForKey(key)"
|
||||
v-for="item in getAntigravityQuotaGroupItems(key)"
|
||||
:key="item.model"
|
||||
:label="item.label"
|
||||
:title="item.label"
|
||||
@@ -979,12 +979,7 @@ import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMo
|
||||
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
|
||||
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import {
|
||||
compareAntigravityQuotaItems,
|
||||
dedupeAntigravityQuotaItemsByLabel,
|
||||
resolveAntigravityQuotaLabel,
|
||||
summarizeAntigravityQuotaItems,
|
||||
} from '@/features/providers/utils/antigravityQuota'
|
||||
import { resolveAntigravityQuotaGroupLabel } from '@/features/providers/utils/antigravityQuota'
|
||||
import {
|
||||
deleteEndpointKey,
|
||||
recoverKeyHealth,
|
||||
@@ -1004,7 +999,6 @@ import {
|
||||
} from '@/api/endpoints'
|
||||
import type {
|
||||
UpstreamMetadata,
|
||||
AntigravityModelQuota,
|
||||
CodexUpstreamMetadata,
|
||||
ChatGPTWebUpstreamMetadata,
|
||||
GrokUpstreamMetadata,
|
||||
@@ -1078,7 +1072,7 @@ const { error: showError, success: showSuccess, warning: showWarning } = useToas
|
||||
const { confirm } = useConfirm()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer()
|
||||
const { legacyT, locale } = useI18n()
|
||||
const { legacyT, locale, t } = useI18n()
|
||||
|
||||
function localizedApiError(err: unknown, fallback: string): string {
|
||||
return legacyT(parseApiError(err, fallback))
|
||||
@@ -3398,17 +3392,8 @@ interface GeminiCliQuotaItem {
|
||||
resetSeconds: number | null
|
||||
}
|
||||
|
||||
function hasAntigravityQuotaData(metadata: UpstreamMetadata | null | undefined): boolean {
|
||||
const quotaByModel = metadata?.antigravity?.quota_by_model
|
||||
return !!quotaByModel && typeof quotaByModel === 'object' && Object.keys(quotaByModel).length > 0
|
||||
}
|
||||
|
||||
function hasAntigravityQuotaDisplayData(key: EndpointAPIKey): boolean {
|
||||
const quota = getQuotaSnapshotForProvider(key, 'antigravity')
|
||||
if (Array.isArray(quota?.windows) && quota.windows.length > 0) {
|
||||
return true
|
||||
}
|
||||
return hasAntigravityQuotaData(key.upstream_metadata)
|
||||
return getAntigravityQuotaGroupItems(key).length > 0
|
||||
}
|
||||
|
||||
function getGeminiCliQuotaUpdatedAt(key: EndpointAPIKey): number | undefined {
|
||||
@@ -3480,84 +3465,14 @@ function formatUpdatedAt(updatedAt: number): string {
|
||||
const formatCodexUpdatedAt = formatUpdatedAt
|
||||
const formatAntigravityUpdatedAt = formatUpdatedAt
|
||||
|
||||
function secondsUntilReset(resetTime: string): number | null {
|
||||
if (!resetTime) return null
|
||||
const ts = Date.parse(resetTime)
|
||||
if (Number.isNaN(ts)) return null
|
||||
const diff = Math.floor((ts - Date.now()) / 1000)
|
||||
return diff > 0 ? diff : 0
|
||||
}
|
||||
|
||||
function secondsUntilUnixReset(resetAt: number | string | null | undefined): number | null {
|
||||
const numericResetAt = Number(resetAt)
|
||||
if (!Number.isFinite(numericResetAt) || numericResetAt <= 0) return null
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
return Math.max(Math.floor(numericResetAt - now), 0)
|
||||
}
|
||||
|
||||
function coerceAntigravityPercent(value: number | string | null | undefined): number | undefined {
|
||||
const numericValue = Number(value)
|
||||
if (!Number.isFinite(numericValue)) return undefined
|
||||
return Math.min(Math.max(numericValue, 0), 100)
|
||||
}
|
||||
|
||||
function coerceAntigravityRemainingFraction(value: number | string | null | undefined): number | undefined {
|
||||
const numericValue = Number(value)
|
||||
if (!Number.isFinite(numericValue)) return undefined
|
||||
return Math.min(Math.max(numericValue, 0), 1)
|
||||
}
|
||||
|
||||
function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaItem[] {
|
||||
const quotaByModel = metadata?.antigravity?.quota_by_model
|
||||
if (!quotaByModel || typeof quotaByModel !== 'object') return []
|
||||
|
||||
const items: AntigravityQuotaItem[] = []
|
||||
const opaqueDisplayIndex = { value: 1 }
|
||||
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
|
||||
if (!model) continue
|
||||
const info: Partial<AntigravityModelQuota> = rawInfo || {}
|
||||
|
||||
let usedPercent = coerceAntigravityPercent(info.used_percent)
|
||||
if (usedPercent === undefined) {
|
||||
const remainingFraction = coerceAntigravityRemainingFraction(info.remaining_fraction)
|
||||
if (remainingFraction !== undefined) {
|
||||
usedPercent = (1 - remainingFraction) * 100
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
usedPercent = coerceAntigravityPercent(usedPercent) ?? 0
|
||||
|
||||
const remainingPercent = Math.max(100 - usedPercent, 0)
|
||||
|
||||
let resetSeconds = secondsUntilUnixReset(info.reset_at)
|
||||
if (typeof info.reset_time === 'string' && info.reset_time.trim()) {
|
||||
resetSeconds = secondsUntilReset(info.reset_time.trim()) ?? resetSeconds
|
||||
}
|
||||
|
||||
items.push({
|
||||
model,
|
||||
label: resolveAntigravityQuotaLabel(model, info.display_name, opaqueDisplayIndex),
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
resetSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
items.sort(compareAntigravityQuotaItems)
|
||||
return dedupeAntigravityQuotaItemsByLabel(items)
|
||||
}
|
||||
|
||||
function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQuotaItem[] {
|
||||
function getAntigravityQuotaGroupItems(key: EndpointAPIKey): AntigravityQuotaItem[] {
|
||||
const quota = getQuotaSnapshotForProvider(key, 'antigravity')
|
||||
const windows = getQuotaWindowByScope(quota, 'model')
|
||||
const windows = getQuotaWindowByScope(quota, 'quota_group')
|
||||
if (!quota || windows.length === 0) return []
|
||||
const opaqueDisplayIndex = { value: 1 }
|
||||
|
||||
const items = windows
|
||||
return windows
|
||||
.map((window) => {
|
||||
const model = String(window.model || window.label || window.code || '').trim()
|
||||
const model = String(window.code || window.bucket_id || window.label || '').trim()
|
||||
if (!model) return null
|
||||
|
||||
const usedPercent = getQuotaWindowUsedPercent(window)
|
||||
@@ -3577,30 +3492,13 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
|
||||
|
||||
return {
|
||||
model,
|
||||
label: resolveAntigravityQuotaLabel(
|
||||
model,
|
||||
window.label || window.model,
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
label: resolveAntigravityQuotaGroupLabel(window, t),
|
||||
usedPercent: normalizedUsedPercent,
|
||||
remainingPercent: normalizedRemainingPercent,
|
||||
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
|
||||
} satisfies AntigravityQuotaItem
|
||||
})
|
||||
.filter((item): item is AntigravityQuotaItem => item !== null)
|
||||
|
||||
items.sort(compareAntigravityQuotaItems)
|
||||
return dedupeAntigravityQuotaItemsByLabel(items)
|
||||
}
|
||||
|
||||
function getAntigravityQuotaItemsForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
|
||||
const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key)
|
||||
if (snapshotItems.length > 0) return snapshotItems
|
||||
return getAntigravityQuotaItems(key.upstream_metadata)
|
||||
}
|
||||
|
||||
function getAntigravityQuotaSummaryForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
|
||||
return summarizeAntigravityQuotaItems(getAntigravityQuotaItemsForKey(key))
|
||||
}
|
||||
|
||||
function getResetCountdownText(
|
||||
|
||||
+80
-91
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h } from 'vue'
|
||||
|
||||
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
|
||||
import type { QuotaStatusSnapshot, UpstreamMetadata } from '@/api/endpoints/types'
|
||||
import { createI18n, setI18nLocale, type Locale } from '@/i18n'
|
||||
|
||||
vi.mock('@/components/ui', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
@@ -77,7 +78,12 @@ vi.mock('@/utils/errorParser', () => ({
|
||||
parseApiError: (value: unknown) => String(value),
|
||||
}))
|
||||
|
||||
function mount(metadata: UpstreamMetadata, quotaSnapshot?: QuotaStatusSnapshot) {
|
||||
function mount(
|
||||
metadata: UpstreamMetadata,
|
||||
quotaSnapshot?: QuotaStatusSnapshot,
|
||||
locale: Locale = 'zh-CN',
|
||||
) {
|
||||
setI18nLocale(locale)
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
|
||||
@@ -91,6 +97,7 @@ function mount(metadata: UpstreamMetadata, quotaSnapshot?: QuotaStatusSnapshot)
|
||||
})
|
||||
},
|
||||
}))
|
||||
app.use(createI18n())
|
||||
app.mount(root)
|
||||
|
||||
return {
|
||||
@@ -103,140 +110,122 @@ function mount(metadata: UpstreamMetadata, quotaSnapshot?: QuotaStatusSnapshot)
|
||||
}
|
||||
|
||||
describe('AntigravityQuotaDialog', () => {
|
||||
it('renders grouped five-hour and weekly quota windows', () => {
|
||||
afterEach(() => {
|
||||
setI18nLocale('zh-CN')
|
||||
})
|
||||
|
||||
it('renders only compact localized quota-group windows in Chinese', () => {
|
||||
const { root, unmount } = mount({}, {
|
||||
code: 'ok',
|
||||
provider_type: 'antigravity',
|
||||
exhausted: false,
|
||||
windows: [{
|
||||
code: 'group:0:3p-5h',
|
||||
code: 'group:0:gemini-weekly',
|
||||
label: 'Gemini Models · Weekly Limit Remaining',
|
||||
scope: 'quota_group',
|
||||
quota_group_label: 'Gemini models',
|
||||
bucket_id: 'gemini-weekly',
|
||||
window: 'weekly',
|
||||
used_ratio: 0.1,
|
||||
remaining_ratio: 0.9,
|
||||
}, {
|
||||
code: 'group:1:3p-5h',
|
||||
label: 'Claude and GPT models · 5 hour',
|
||||
scope: 'quota_group',
|
||||
quota_group_label: 'Claude and GPT models',
|
||||
bucket_id: '3p-5h',
|
||||
window: '5h',
|
||||
used_ratio: 0.75,
|
||||
remaining_ratio: 0.25,
|
||||
}, {
|
||||
code: 'group:0:3p-weekly',
|
||||
label: 'Claude and GPT models · weekly',
|
||||
code: 'group:1:3p-weekly',
|
||||
label: 'Claude and GPT models · Weekly Limit Remaining',
|
||||
scope: 'quota_group',
|
||||
quota_group_label: 'Claude and GPT models',
|
||||
bucket_id: '3p-weekly',
|
||||
window: 'weekly',
|
||||
used_ratio: 0.2,
|
||||
remaining_ratio: 0.8,
|
||||
}, {
|
||||
code: 'model:gemini-3.7-flash-tiered',
|
||||
label: 'Gemini 3.7 Flash',
|
||||
scope: 'model',
|
||||
model: 'gemini-3.7-flash-tiered',
|
||||
used_ratio: 0.05,
|
||||
remaining_ratio: 0.95,
|
||||
}],
|
||||
})
|
||||
const text = root.textContent || ''
|
||||
|
||||
expect(text).toContain('Claude and GPT models · 5 hour')
|
||||
expect(text).toContain('Gemini Models · 周')
|
||||
expect(text).toContain('90.0%')
|
||||
expect(text).toContain('Claude and GPT models · 5小时')
|
||||
expect(text).toContain('25.0%')
|
||||
expect(text).toContain('Claude and GPT models · weekly')
|
||||
expect(text).toContain('Claude and GPT models · 周')
|
||||
expect(text).toContain('80.0%')
|
||||
expect(text).not.toContain('Weekly Limit Remaining')
|
||||
expect(text).not.toContain('Gemini额度')
|
||||
expect(text).not.toContain('Claude & ChatGPT')
|
||||
expect(text).not.toContain('Gemini 3.7 Flash')
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('hides quota buckets outside the shared pool summary families', () => {
|
||||
const rawIdentifier = 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb'
|
||||
const { root, unmount } = mount({
|
||||
antigravity: {
|
||||
quota_by_model: {
|
||||
[rawIdentifier]: {
|
||||
display_name: rawIdentifier,
|
||||
remaining_fraction: 0.25,
|
||||
used_percent: 75,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
it('uses Weekly and 5 Hours for the English locale', () => {
|
||||
const { root, unmount } = mount({}, {
|
||||
code: 'ok',
|
||||
provider_type: 'antigravity',
|
||||
exhausted: false,
|
||||
windows: [{
|
||||
code: 'group:0:gemini-weekly',
|
||||
label: 'Gemini Models · Weekly Limit Remaining',
|
||||
scope: 'quota_group',
|
||||
quota_group_label: 'Gemini Models',
|
||||
bucket_id: 'gemini-weekly',
|
||||
window: 'weekly',
|
||||
remaining_ratio: 0.9,
|
||||
}, {
|
||||
code: 'group:1:3p-5h',
|
||||
label: 'Claude and GPT models · 5 hour',
|
||||
scope: 'quota_group',
|
||||
quota_group_label: 'Claude and GPT models',
|
||||
bucket_id: '3p-5h',
|
||||
window: '5h',
|
||||
remaining_ratio: 0.25,
|
||||
}],
|
||||
}, 'en-US')
|
||||
const text = root.textContent || ''
|
||||
|
||||
expect(root.textContent).toContain('暂无配额数据')
|
||||
expect(root.textContent).not.toContain('Key-1')
|
||||
expect(root.textContent).not.toContain(rawIdentifier)
|
||||
expect(root.textContent).not.toContain('25.0%')
|
||||
expect(text).toContain('Gemini Models · Weekly')
|
||||
expect(text).toContain('Claude and GPT models · 5 Hours')
|
||||
expect(text).not.toContain('Weekly Limit Remaining')
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('renders only the shared Gemini and Claude ChatGPT family summaries', () => {
|
||||
it('does not fall back to the removed model-family summaries', () => {
|
||||
const { root, unmount } = mount({
|
||||
antigravity: {
|
||||
quota_by_model: {
|
||||
tab_flash_lite_preview: {
|
||||
display_name: 'Tab Flash Lite Preview',
|
||||
remaining_fraction: 0.01,
|
||||
used_percent: 99,
|
||||
},
|
||||
'gemini-3-flash-agent': {
|
||||
display_name: 'Gemini 3.5 Flash (High)',
|
||||
remaining_fraction: 0.9,
|
||||
used_percent: 10,
|
||||
},
|
||||
'gemini-pro-agent': {
|
||||
display_name: 'gemini-pro-agent',
|
||||
remaining_fraction: 0.95,
|
||||
used_percent: 5,
|
||||
},
|
||||
'gemini-3.5-flash-low': {
|
||||
display_name: 'Gemini 3.5 Flash (Medium)',
|
||||
remaining_fraction: 0.8,
|
||||
used_percent: 20,
|
||||
},
|
||||
'claude-opus-4-6-thinking': {
|
||||
display_name: 'Claude Opus 4.6 (Thinking)',
|
||||
remaining_fraction: 1,
|
||||
used_percent: 0,
|
||||
},
|
||||
chat_20706: {
|
||||
display_name: 'chat_20706',
|
||||
remaining_fraction: 0,
|
||||
used_percent: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const text = root.textContent || ''
|
||||
|
||||
expect(text).not.toContain('gemini-pro-agent')
|
||||
expect(text).not.toContain('claude-opus-4-6-thinking')
|
||||
expect(text).toContain('Gemini额度')
|
||||
expect(text).toContain('80%–95%')
|
||||
expect(text).toContain('Claude & ChatGPT')
|
||||
expect(text).toContain('100%')
|
||||
expect(text).not.toContain('Gemini 3.1 Pro (High)')
|
||||
expect(text).not.toContain('Gemini 3.5 Flash (High)')
|
||||
expect(text).not.toContain('Gemini 3.5 Flash (Medium)')
|
||||
expect(text).not.toContain('Tab Flash Lite Preview')
|
||||
expect(text).not.toContain('chat_20706')
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('collapses duplicate model buckets into one family range', () => {
|
||||
const { root, unmount } = mount({
|
||||
antigravity: {
|
||||
quota_by_model: {
|
||||
'gemini-3.1-pro-high': {
|
||||
display_name: 'Gemini 3.1 Pro (High)',
|
||||
remaining_fraction: 0.95,
|
||||
used_percent: 5,
|
||||
},
|
||||
'gemini-pro-agent': {
|
||||
display_name: 'Gemini 3.1 Pro (High)',
|
||||
remaining_fraction: 0.4,
|
||||
used_percent: 60,
|
||||
},
|
||||
'gemini-3-flash-agent': {
|
||||
display_name: 'Gemini 3.5 Flash (High)',
|
||||
remaining_fraction: 0.9,
|
||||
used_percent: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const text = root.textContent || ''
|
||||
|
||||
expect(text.match(/Gemini额度/g)).toHaveLength(1)
|
||||
expect(text).toContain('40%–90%')
|
||||
expect(text).not.toContain('95.0%')
|
||||
expect(text).not.toContain('Gemini 3.1 Pro (High)')
|
||||
expect(text).not.toContain('Gemini 3.5 Flash (High)')
|
||||
expect(root.textContent).toContain('暂无配额数据')
|
||||
expect(root.textContent).not.toContain('Gemini额度')
|
||||
expect(root.textContent).not.toContain('Claude & ChatGPT')
|
||||
expect(root.textContent).not.toContain('Gemini 3.5 Flash (High)')
|
||||
expect(root.textContent).not.toContain('Claude Opus 4.6 (Thinking)')
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { messages } from '@/i18n/messages'
|
||||
import { resolveAntigravityQuotaGroupLabel } from '@/features/providers/utils/antigravityQuota'
|
||||
|
||||
describe('resolveAntigravityQuotaGroupLabel', () => {
|
||||
it('uses the localized compact weekly label instead of the upstream display name', () => {
|
||||
const window = {
|
||||
code: 'group:0:gemini-weekly',
|
||||
label: 'Gemini Models · Weekly Limit Remaining',
|
||||
quota_group_label: 'Gemini models',
|
||||
bucket_id: 'gemini-weekly',
|
||||
window: 'weekly',
|
||||
}
|
||||
|
||||
expect(resolveAntigravityQuotaGroupLabel(
|
||||
window,
|
||||
key => messages['zh-CN'][key],
|
||||
)).toBe('Gemini Models · 周')
|
||||
expect(resolveAntigravityQuotaGroupLabel(
|
||||
window,
|
||||
key => messages['en-US'][key],
|
||||
)).toBe('Gemini Models · Weekly')
|
||||
})
|
||||
|
||||
it('normalizes the legacy Claude family name and detects a five-hour bucket', () => {
|
||||
const window = {
|
||||
code: 'group:1:3p-5h',
|
||||
label: 'Claude & ChatGPT · 5 hour',
|
||||
quota_group_label: 'Claude & ChatGPT',
|
||||
bucket_id: '3p-5h',
|
||||
}
|
||||
|
||||
expect(resolveAntigravityQuotaGroupLabel(
|
||||
window,
|
||||
key => messages['zh-CN'][key],
|
||||
)).toBe('Claude and GPT models · 5小时')
|
||||
expect(resolveAntigravityQuotaGroupLabel(
|
||||
window,
|
||||
key => messages['en-US'][key],
|
||||
)).toBe('Claude and GPT models · 5 Hours')
|
||||
})
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { summarizeAntigravityQuotaItems } from '@/features/providers/utils/antigravityQuota'
|
||||
|
||||
describe('summarizeAntigravityQuotaItems', () => {
|
||||
it('groups model families without collapsing their independent quota values', () => {
|
||||
const items = summarizeAntigravityQuotaItems([
|
||||
{ model: 'claude-opus-4-6-thinking', label: 'Claude Opus', remainingPercent: 100, resetSeconds: 60 },
|
||||
{ model: 'claude-sonnet-4-6', label: 'Claude Sonnet', remainingPercent: 82, resetSeconds: 120 },
|
||||
{ model: 'gemini-3.1-pro-high', label: 'Gemini Pro', remainingPercent: 90.6, resetSeconds: 180 },
|
||||
{ model: 'gemini-3-flash-agent', label: 'Gemini Flash', remainingPercent: 95, resetSeconds: 240 },
|
||||
{ model: 'gpt-oss-120b-medium', label: 'GPT-OSS', remainingPercent: 100, resetSeconds: 300 },
|
||||
{ model: 'tab_flash_lite_preview', label: 'Tab', remainingPercent: 76, resetSeconds: 360 },
|
||||
{ model: 'chat_20706', label: 'Chat', remainingPercent: 64, resetSeconds: 420 },
|
||||
])
|
||||
|
||||
expect(items.map(item => [item.label, item.remainingPercent, item.detail])).toEqual([
|
||||
['Gemini额度', 90.6, '90.6%–95%'],
|
||||
['Claude & ChatGPT', 82, '82%–100%'],
|
||||
])
|
||||
expect(items.map(item => item.resetSeconds)).toEqual([180, 120])
|
||||
expect(items[1]?.model).toBe('claude-sonnet-4-6')
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { QuotaWindowSnapshot } from '@/api/endpoints/types'
|
||||
|
||||
export interface AntigravityQuotaSortableItem {
|
||||
model: string
|
||||
label: string
|
||||
@@ -6,13 +8,11 @@ export interface AntigravityQuotaSortableItem {
|
||||
detail?: string
|
||||
}
|
||||
|
||||
const ANTIGRAVITY_QUOTA_GROUPS = [
|
||||
{ label: 'Gemini额度', matches: (model: string) => model.startsWith('gemini-') },
|
||||
{
|
||||
label: 'Claude & ChatGPT',
|
||||
matches: (model: string) => model.startsWith('claude-') || model.startsWith('gpt-'),
|
||||
},
|
||||
] as const
|
||||
type AntigravityQuotaPeriodMessageKey =
|
||||
| 'providers.antigravity.quota.period.weekly'
|
||||
| 'providers.antigravity.quota.period.fiveHours'
|
||||
|
||||
type AntigravityQuotaTranslator = (key: AntigravityQuotaPeriodMessageKey) => string
|
||||
|
||||
const ANTIGRAVITY_MODEL_LABELS: Record<string, string> = {
|
||||
'gemini-pro-agent': 'Gemini 3.1 Pro (High)',
|
||||
@@ -125,39 +125,63 @@ export function dedupeAntigravityQuotaItemsByLabel<T extends AntigravityQuotaSor
|
||||
return Array.from(selectedByLabel.values()).sort(compareAntigravityQuotaItems)
|
||||
}
|
||||
|
||||
export function summarizeAntigravityQuotaItems<T extends AntigravityQuotaSortableItem>(
|
||||
items: T[],
|
||||
): T[] {
|
||||
const itemsByGroup = new Map<string, T[]>()
|
||||
for (const item of items) {
|
||||
const normalizedModel = item.model.trim().toLowerCase().replace(/^model:/, '')
|
||||
const group = ANTIGRAVITY_QUOTA_GROUPS.find(candidate => candidate.matches(normalizedModel))
|
||||
if (!group) continue
|
||||
const groupedItems = itemsByGroup.get(group.label) ?? []
|
||||
groupedItems.push(item)
|
||||
itemsByGroup.set(group.label, groupedItems)
|
||||
function canonicalizeAntigravityQuotaGroupLabel(rawLabel: string): string {
|
||||
const normalized = rawLabel
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[._-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
|
||||
if (normalized === 'gemini额度' || /^gemini(?: models?)?$/.test(normalized)) {
|
||||
return 'Gemini Models'
|
||||
}
|
||||
if (
|
||||
normalized === 'claude & chatgpt'
|
||||
|| /^claude\s*(?:&|and)\s*(?:gpt|chatgpt)(?: models?)?$/.test(normalized)
|
||||
) {
|
||||
return 'Claude and GPT models'
|
||||
}
|
||||
return rawLabel.trim()
|
||||
}
|
||||
|
||||
function getAntigravityQuotaGroupName(window: QuotaWindowSnapshot): string {
|
||||
const explicitGroupLabel = String(window.quota_group_label || '').trim()
|
||||
if (explicitGroupLabel) {
|
||||
return canonicalizeAntigravityQuotaGroupLabel(explicitGroupLabel)
|
||||
}
|
||||
|
||||
return ANTIGRAVITY_QUOTA_GROUPS.map(group => group.label)
|
||||
.map((label) => {
|
||||
const groupedItems = itemsByGroup.get(label)
|
||||
if (!groupedItems?.length) return undefined
|
||||
const remainingValues = groupedItems
|
||||
.map(item => item.remainingPercent)
|
||||
.sort((left, right) => left - right)
|
||||
const minRemaining = remainingValues[0] ?? 0
|
||||
const maxRemaining = remainingValues.at(-1) ?? minRemaining
|
||||
const selected = groupedItems.find(item => item.remainingPercent === minRemaining) ?? groupedItems[0]
|
||||
const detail = Math.abs(maxRemaining - minRemaining) < 1e-6
|
||||
? formatAntigravityQuotaValue(minRemaining)
|
||||
: `${formatAntigravityQuotaValue(minRemaining)}–${formatAntigravityQuotaValue(maxRemaining)}`
|
||||
return { ...selected, label, remainingPercent: minRemaining, detail }
|
||||
})
|
||||
.filter((item): item is T => item !== undefined)
|
||||
const rawLabel = String(window.label || '').trim()
|
||||
const groupLabel = rawLabel.split(/\s*·\s*/, 1)[0]
|
||||
?.replace(/\s+(?:weekly limit remaining|weekly|5\s*hours?|5h)$/i, '')
|
||||
.trim()
|
||||
return canonicalizeAntigravityQuotaGroupLabel(groupLabel || rawLabel)
|
||||
}
|
||||
|
||||
function formatAntigravityQuotaValue(value: number): string {
|
||||
const rounded = Math.round(value)
|
||||
const formatted = Math.abs(value - rounded) < 1e-6 ? String(rounded) : value.toFixed(1)
|
||||
return `${formatted}%`
|
||||
function getAntigravityQuotaPeriodLabel(
|
||||
window: QuotaWindowSnapshot,
|
||||
translate: AntigravityQuotaTranslator,
|
||||
): string {
|
||||
const periodCandidates = [window.window, window.bucket_id, window.code, window.label]
|
||||
.map(value => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
const normalized = periodCandidates.join(' ').toLowerCase()
|
||||
|
||||
if (/(?:^|[^a-z])weekly(?:[^a-z]|$)/.test(normalized)) {
|
||||
return translate('providers.antigravity.quota.period.weekly')
|
||||
}
|
||||
if (/(?:^|[^a-z0-9])5\s*(?:h|hours?)(?:[^a-z0-9]|$)/.test(normalized)) {
|
||||
return translate('providers.antigravity.quota.period.fiveHours')
|
||||
}
|
||||
|
||||
return String(window.window || '').trim()
|
||||
}
|
||||
|
||||
export function resolveAntigravityQuotaGroupLabel(
|
||||
window: QuotaWindowSnapshot,
|
||||
translate: AntigravityQuotaTranslator,
|
||||
): string {
|
||||
const groupName = getAntigravityQuotaGroupName(window)
|
||||
const periodLabel = getAntigravityQuotaPeriodLabel(window, translate)
|
||||
if (groupName && periodLabel) return `${groupName} · ${periodLabel}`
|
||||
return groupName || String(window.label || window.code || '').trim()
|
||||
}
|
||||
|
||||
@@ -334,6 +334,8 @@ export const messages = {
|
||||
'nav.cacheMonitoring': '缓存监控',
|
||||
'nav.moduleManagement': '模块管理',
|
||||
'nav.systemSettings': '系统设置',
|
||||
'providers.antigravity.quota.period.weekly': '周',
|
||||
'providers.antigravity.quota.period.fiveHours': '5小时',
|
||||
'providers.modelMapping.scope.allRequests': '所有请求',
|
||||
'providers.modelMapping.scope.sessionCompactionOnly': '仅会话压缩',
|
||||
'providers.modelMapping.scope.customOperations': '仅匹配:{operations}',
|
||||
@@ -684,6 +686,8 @@ export const messages = {
|
||||
'nav.cacheMonitoring': 'Cache monitoring',
|
||||
'nav.moduleManagement': 'Modules',
|
||||
'nav.systemSettings': 'System settings',
|
||||
'providers.antigravity.quota.period.weekly': 'Weekly',
|
||||
'providers.antigravity.quota.period.fiveHours': '5 Hours',
|
||||
'providers.modelMapping.scope.allRequests': 'All requests',
|
||||
'providers.modelMapping.scope.sessionCompactionOnly': 'Session compaction only',
|
||||
'providers.modelMapping.scope.customOperations': 'Match only: {operations}',
|
||||
|
||||
@@ -593,8 +593,8 @@
|
||||
<button
|
||||
v-if="item.key === 'priority'"
|
||||
type="button"
|
||||
class="inline-flex max-w-full items-center rounded-full border px-2 py-0.5 text-[10px] font-medium leading-4"
|
||||
:class="`${getMobileTagClass(item)} hover:border-primary/40 hover:text-foreground`"
|
||||
class="inline-flex max-w-full items-center rounded-full border px-2 py-0.5 text-[10px] font-medium leading-4 hover:border-primary/40 hover:text-foreground"
|
||||
:class="getMobileTagClass(item)"
|
||||
:title="`${item.label},点击编辑优先级`"
|
||||
@click="quickEditInternalPriority(key)"
|
||||
>
|
||||
@@ -1143,11 +1143,7 @@ import {
|
||||
} from '@/features/pool/utils/poolStatsDisplay'
|
||||
import { resetCodexCycleUsageWindows } from '@/features/pool/utils/poolCycleStats'
|
||||
import { mergePoolKeyQuotaSnapshots } from '@/features/pool/utils/poolQuotaRefresh'
|
||||
import {
|
||||
dedupeAntigravityQuotaItemsByLabel,
|
||||
resolveAntigravityQuotaLabel,
|
||||
summarizeAntigravityQuotaItems,
|
||||
} from '@/features/providers/utils/antigravityQuota'
|
||||
import { resolveAntigravityQuotaGroupLabel } from '@/features/providers/utils/antigravityQuota'
|
||||
import {
|
||||
clearPendingCodexResetCreditIdempotencyKey,
|
||||
clearPendingCodexResetCreditIdempotencyKeyForOutcome,
|
||||
@@ -1197,7 +1193,7 @@ function prefetchProviderDetailDrawer(): void {
|
||||
type PoolKeyScore = NonNullable<PoolKeyDetail['pool_score']>
|
||||
|
||||
const { success, error: showError, warning: showWarning } = useToast()
|
||||
const { legacyT } = useI18n()
|
||||
const { legacyT, t } = useI18n()
|
||||
const { confirm } = useConfirm()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
const { tick: countdownTick, start: startCountdownTimer } = useCountdownTimer()
|
||||
@@ -3864,17 +3860,17 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
|
||||
}
|
||||
|
||||
if (providerType === 'antigravity') {
|
||||
const windows = getQuotaSnapshotWindowsByScope(quota, 'model')
|
||||
const windows = getQuotaSnapshotWindowsByScope(quota, 'quota_group')
|
||||
if (windows.length === 0) return []
|
||||
const opaqueDisplayIndex = { value: 1 }
|
||||
return summarizeAntigravityQuotaItems(dedupeAntigravityQuotaItemsByLabel(windows
|
||||
.map((window): (QuotaProgressItem & { model: string, resetSeconds: number | null }) | null => {
|
||||
return windows
|
||||
.map((window, index): QuotaProgressItem | null => {
|
||||
const remainingPercent = getQuotaWindowRemainingPercent(window)
|
||||
if (remainingPercent == null) return null
|
||||
const model = String(window.model || window.code || '').trim().replace(/^model:/i, '')
|
||||
const label = resolveAntigravityQuotaGroupLabel(window, t)
|
||||
if (!label) return null
|
||||
return {
|
||||
model,
|
||||
label: resolveAntigravityQuotaLabel(model, window.label, opaqueDisplayIndex),
|
||||
label,
|
||||
sortOrder: index,
|
||||
remainingPercent,
|
||||
resetAtSeconds: normalizeUnixSeconds(window.reset_at ?? quota.reset_at ?? null),
|
||||
resetSeconds: normalizeRemainingSeconds(window.reset_seconds ?? quota.reset_seconds ?? null),
|
||||
@@ -3882,7 +3878,7 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
|
||||
allowDynamicReset: true,
|
||||
}
|
||||
})
|
||||
.filter((item): item is QuotaProgressItem & { model: string, resetSeconds: number | null } => item != null)))
|
||||
.filter((item): item is QuotaProgressItem => item != null)
|
||||
}
|
||||
|
||||
if (providerType === 'gemini_cli') {
|
||||
|
||||
Reference in New Issue
Block a user