diff --git a/frontend/src/api/endpoints/types/statusSnapshot.ts b/frontend/src/api/endpoints/types/statusSnapshot.ts index 17b6f0605..83ebde78c 100644 --- a/frontend/src/api/endpoints/types/statusSnapshot.ts +++ b/frontend/src/api/endpoints/types/statusSnapshot.ts @@ -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 diff --git a/frontend/src/features/pool/components/__tests__/PoolKeyDisplayPanels.spec.ts b/frontend/src/features/pool/components/__tests__/PoolKeyDisplayPanels.spec.ts index e2246ce07..fc46ea9fc 100644 --- a/frontend/src/features/pool/components/__tests__/PoolKeyDisplayPanels.spec.ts +++ b/frontend/src/features/pool/components/__tests__/PoolKeyDisplayPanels.spec.ts @@ -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) diff --git a/frontend/src/features/providers/components/AntigravityQuotaDialog.vue b/frontend/src/features/providers/components/AntigravityQuotaDialog.vue index 0a60ba705..932aaa83f 100644 --- a/frontend/src/features/providers/components/AntigravityQuotaDialog.vue +++ b/frontend/src/features/providers/components/AntigravityQuotaDialog.vue @@ -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(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(() => { return dedupeAntigravityQuotaItemsByLabel(result) }) -const items = computed(() => summarizeAntigravityQuotaItems(rawItems.value)) const groupedItems = computed(() => buildGroupedItemsFromQuotaSnapshot(props.quotaSnapshot)) -const displayItems = computed(() => [...groupedItems.value, ...items.value]) +const displayItems = computed(() => groupedItems.value) async function handleTestModel(modelName: string) { if (!props.providerId || testingModel.value) return diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index c607745f3..470bc095e 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -377,7 +377,7 @@ />
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 = 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( diff --git a/frontend/src/features/providers/components/__tests__/AntigravityQuotaDialog.spec.ts b/frontend/src/features/providers/components/__tests__/AntigravityQuotaDialog.spec.ts index ded9c2119..8045f085c 100644 --- a/frontend/src/features/providers/components/__tests__/AntigravityQuotaDialog.spec.ts +++ b/frontend/src/features/providers/components/__tests__/AntigravityQuotaDialog.spec.ts @@ -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() }) diff --git a/frontend/src/features/providers/utils/__tests__/antigravityQuotaGroupLabel.spec.ts b/frontend/src/features/providers/utils/__tests__/antigravityQuotaGroupLabel.spec.ts new file mode 100644 index 000000000..2d519aba1 --- /dev/null +++ b/frontend/src/features/providers/utils/__tests__/antigravityQuotaGroupLabel.spec.ts @@ -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') + }) +}) diff --git a/frontend/src/features/providers/utils/__tests__/antigravityQuotaSummary.spec.ts b/frontend/src/features/providers/utils/__tests__/antigravityQuotaSummary.spec.ts deleted file mode 100644 index dfb91417d..000000000 --- a/frontend/src/features/providers/utils/__tests__/antigravityQuotaSummary.spec.ts +++ /dev/null @@ -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') - }) -}) \ No newline at end of file diff --git a/frontend/src/features/providers/utils/antigravityQuota.ts b/frontend/src/features/providers/utils/antigravityQuota.ts index 6dfc88b59..d5980cf99 100644 --- a/frontend/src/features/providers/utils/antigravityQuota.ts +++ b/frontend/src/features/providers/utils/antigravityQuota.ts @@ -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 = { 'gemini-pro-agent': 'Gemini 3.1 Pro (High)', @@ -125,39 +125,63 @@ export function dedupeAntigravityQuotaItemsByLabel( - items: T[], -): T[] { - const itemsByGroup = new Map() - 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() } diff --git a/frontend/src/i18n/messages.ts b/frontend/src/i18n/messages.ts index ca1351218..60142b6ed 100644 --- a/frontend/src/i18n/messages.ts +++ b/frontend/src/i18n/messages.ts @@ -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}', diff --git a/frontend/src/views/admin/PoolManagement.vue b/frontend/src/views/admin/PoolManagement.vue index 0b0cc0c79..684e7a7b8 100644 --- a/frontend/src/views/admin/PoolManagement.vue +++ b/frontend/src/views/admin/PoolManagement.vue @@ -593,8 +593,8 @@