From e95cc2b621e3b5836091a03c9b6be22aacdbd8cb Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Wed, 17 Jun 2026 12:30:03 +0800 Subject: [PATCH] feat(quota): enhance quota management with new data structures and improved UI components --- src/components/quota/QuotaCard.tsx | 2 +- src/components/quota/quotaConfigs.ts | 196 ++++++++++++++---- .../authFiles/components/QuotaProgressBar.tsx | 3 +- src/i18n/locales/en.json | 9 + src/i18n/locales/ru.json | 9 + src/i18n/locales/zh-CN.json | 9 + src/i18n/locales/zh-TW.json | 9 + src/pages/AuthFilesPage.module.scss | 31 +++ src/pages/QuotaPage.module.scss | 31 +++ src/types/quota.ts | 50 +++-- src/utils/quota/builders.ts | 177 ++++++---------- src/utils/quota/constants.ts | 58 +----- 12 files changed, 358 insertions(+), 226 deletions(-) diff --git a/src/components/quota/QuotaCard.tsx b/src/components/quota/QuotaCard.tsx index 6c1f3f23..b1b6ca8d 100644 --- a/src/components/quota/QuotaCard.tsx +++ b/src/components/quota/QuotaCard.tsx @@ -41,7 +41,7 @@ export function QuotaProgressBar({ : normalized >= mediumThreshold ? styles.quotaBarFillMedium : styles.quotaBarFillLow; - const widthPercent = Math.round(normalized ?? 0); + const widthPercent = Math.round((normalized ?? 0) * 100) / 100; return (
diff --git a/src/components/quota/quotaConfigs.ts b/src/components/quota/quotaConfigs.ts index d5f81dd5..2f8be849 100644 --- a/src/components/quota/quotaConfigs.ts +++ b/src/components/quota/quotaConfigs.ts @@ -7,7 +7,7 @@ import type { ReactNode } from 'react'; import type { TFunction } from 'i18next'; import type { AntigravityQuotaGroup, - AntigravityModelsPayload, + AntigravityQuotaSummaryPayload, AntigravityQuotaState, AuthFileItem, ClaudeExtraUsage, @@ -93,7 +93,11 @@ type QuotaUpdater = T | ((prev: T) => T); type QuotaType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi' | 'xai'; -const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn'; +type AntigravityQuotaData = { + groups: AntigravityQuotaGroup[]; + serverTimeOffsetMs: number | null; +}; + const QUOTA_PROGRESS_HIGH_THRESHOLD = 70; const QUOTA_PROGRESS_MEDIUM_THRESHOLD = 30; const geminiCliSupplementaryRequestIds = new Map(); @@ -144,10 +148,33 @@ export interface QuotaConfig { } const resolveAntigravityProjectId = async (file: AuthFileItem): Promise => { + const directProjectId = normalizeStringValue(file.project_id ?? file.projectId); + if (directProjectId) return directProjectId; + + const metadata = + file.metadata && typeof file.metadata === 'object' && file.metadata !== null + ? (file.metadata as Record) + : null; + const metadataProjectId = metadata + ? normalizeStringValue(metadata.project_id ?? metadata.projectId) + : null; + if (metadataProjectId) return metadataProjectId; + + const attributes = + file.attributes && typeof file.attributes === 'object' && file.attributes !== null + ? (file.attributes as Record) + : null; + const attributesProjectId = attributes + ? normalizeStringValue( + attributes.project_id ?? attributes.projectId ?? attributes.gemini_virtual_project + ) + : null; + if (attributesProjectId) return attributesProjectId; + try { const text = await authFilesApi.downloadText(file.name); const trimmed = text.trim(); - if (!trimmed) return DEFAULT_ANTIGRAVITY_PROJECT_ID; + if (!trimmed) return ''; const parsed = JSON.parse(trimmed) as Record; const topLevel = normalizeStringValue(parsed.project_id ?? parsed.projectId); @@ -169,16 +196,28 @@ const resolveAntigravityProjectId = async (file: AuthFileItem): Promise const webProjectId = web ? normalizeStringValue(web.project_id ?? web.projectId) : null; if (webProjectId) return webProjectId; } catch { - return DEFAULT_ANTIGRAVITY_PROJECT_ID; + return ''; } - return DEFAULT_ANTIGRAVITY_PROJECT_ID; + return ''; +}; + +const resolveResponseServerTimeOffsetMs = ( + header: Record | undefined +): number | null => { + if (!header) return null; + const dateEntry = Object.entries(header).find(([key]) => key.toLowerCase() === 'date'); + const rawDate = dateEntry?.[1]?.[0]; + if (!rawDate) return null; + const serverTime = new Date(rawDate).getTime(); + if (Number.isNaN(serverTime)) return null; + return serverTime - Date.now(); }; const fetchAntigravityQuota = async ( file: AuthFileItem, t: TFunction -): Promise => { +): Promise => { const rawAuthIndex = file['auth_index'] ?? file.authIndex; const authIndex = normalizeAuthIndex(rawAuthIndex); if (!authIndex) { @@ -186,6 +225,9 @@ const fetchAntigravityQuota = async ( } const projectId = await resolveAntigravityProjectId(file); + if (!projectId) { + throw new Error(t('antigravity_quota.missing_project_id')); + } const requestBody = JSON.stringify({ project: projectId }); let lastError = ''; @@ -213,20 +255,24 @@ const fetchAntigravityQuota = async ( } hadSuccess = true; - const payload = parseAntigravityPayload(result.body ?? result.bodyText); - const models = payload?.models; - if (!models || typeof models !== 'object' || Array.isArray(models)) { + const payload = parseAntigravityPayload( + result.body ?? result.bodyText + ) as AntigravityQuotaSummaryPayload | null; + if (!payload || !Array.isArray(payload.groups)) { lastError = t('antigravity_quota.empty_models'); continue; } - const groups = buildAntigravityQuotaGroups(models as AntigravityModelsPayload); + const groups = buildAntigravityQuotaGroups(payload); if (groups.length === 0) { lastError = t('antigravity_quota.empty_models'); continue; } - return groups; + return { + groups, + serverTimeOffsetMs: resolveResponseServerTimeOffsetMs(result.header), + }; } catch (err: unknown) { lastError = err instanceof Error ? err.message : t('common.unknown_error'); const status = getStatusFromError(err); @@ -240,7 +286,7 @@ const fetchAntigravityQuota = async ( } if (hadSuccess) { - return []; + return { groups: [], serverTimeOffsetMs: null }; } throw createStatusError(lastError || t('common.unknown_error'), priorityStatus ?? lastStatus); @@ -825,45 +871,116 @@ const fetchGeminiCliQuota = async ( }; }; +const formatAntigravityDuration = (t: TFunction, deltaMs: number): string => { + const totalMinutes = Math.max(1, Math.ceil(deltaMs / 60000)); + const days = Math.floor(totalMinutes / 1440); + const hours = Math.floor((totalMinutes % 1440) / 60); + const minutes = totalMinutes % 60; + + if (days > 0) { + return t('antigravity_quota.duration_day_hour', { + days, + hours, + }); + } + if (hours > 0) { + return t('antigravity_quota.duration_hour_minute', { + hours, + minutes, + }); + } + if (minutes > 0) { + return t('antigravity_quota.duration_minute', { + minutes, + }); + } + return t('antigravity_quota.duration_less_than_minute'); +}; + +const formatAntigravityResetLabel = ( + resetTime: string | undefined, + t: TFunction, + nowMs: number +): string => { + if (!resetTime) return '-'; + const resetMs = new Date(resetTime).getTime(); + if (Number.isNaN(resetMs)) return '-'; + const deltaMs = resetMs - nowMs; + if (deltaMs <= 0) return t('antigravity_quota.refresh_available'); + return t('antigravity_quota.refreshes_in', { + duration: formatAntigravityDuration(t, deltaMs), + }); +}; + const renderAntigravityItems = ( quota: AntigravityQuotaState, t: TFunction, helpers: QuotaRenderHelpers ): ReactNode => { const { styles: styleMap, QuotaProgressBar } = helpers; - const { createElement: h } = React; + const { createElement: h, Fragment } = React; const groups = quota.groups ?? []; if (groups.length === 0) { return h('div', { className: styleMap.quotaMessage }, t('antigravity_quota.empty_models')); } - return groups.map((group) => { - const clamped = Math.max(0, Math.min(1, group.remainingFraction)); - const percent = Math.round(clamped * 100); - const resetLabel = formatQuotaResetTime(group.resetTime); + const nowMs = Date.now() + (quota.serverTimeOffsetMs ?? 0); - return h( - 'div', - { key: group.id, className: styleMap.quotaRow }, + return h( + Fragment, + null, + ...groups.map((group) => h( 'div', - { className: styleMap.quotaRowHeader }, - h('span', { className: styleMap.quotaModel, title: group.models.join(', ') }, group.label), + { key: group.id, className: styleMap.antigravityQuotaGroup }, h( 'div', - { className: styleMap.quotaMeta }, - h('span', { className: styleMap.quotaPercent }, `${percent}%`), - h('span', { className: styleMap.quotaReset }, resetLabel) - ) - ), - h(QuotaProgressBar, { - percent, - highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD, - mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD, - }) - ); - }); + { className: styleMap.antigravityQuotaGroupHeader }, + h('span', { className: styleMap.antigravityQuotaGroupTitle }, group.label), + group.description + ? h('span', { className: styleMap.antigravityQuotaGroupDescription }, group.description) + : null + ), + ...group.buckets.map((bucket) => { + const clamped = Math.max(0, Math.min(1, bucket.remainingFraction)); + const percent = clamped * 100; + const percentLabel = + bucket.remainingFraction === 1 + ? t('antigravity_quota.quota_available') + : t('antigravity_quota.remaining_percent', { + percent: Math.round(percent), + }); + const resetLabel = formatAntigravityResetLabel(bucket.resetTime, t, nowMs); + + return h( + 'div', + { key: bucket.id, className: styleMap.quotaRow }, + h( + 'div', + { className: styleMap.quotaRowHeader }, + h( + 'span', + { className: styleMap.quotaModel, title: bucket.description }, + bucket.label + ), + h( + 'div', + { className: styleMap.quotaMeta }, + h('span', { className: styleMap.quotaPercent }, percentLabel), + h('span', { className: styleMap.quotaReset }, resetLabel) + ) + ), + h(QuotaProgressBar, { + percent, + highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD, + mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD, + }) + ); + }) + ) + ) + ); }; const PREMIUM_GEMINI_CLI_TIER_IDS = new Set(['g1-ultra-tier']); @@ -1343,7 +1460,7 @@ export const CLAUDE_CONFIG: QuotaConfig< renderQuotaItems: renderClaudeItems, }; -export const ANTIGRAVITY_CONFIG: QuotaConfig = { +export const ANTIGRAVITY_CONFIG: QuotaConfig = { type: 'antigravity', i18nPrefix: 'antigravity_quota', cardIdleMessageKey: 'quota_management.card_idle_hint', @@ -1351,11 +1468,16 @@ export const ANTIGRAVITY_CONFIG: QuotaConfig state.antigravityQuota, storeSetter: 'setAntigravityQuota', - buildLoadingState: () => ({ status: 'loading', groups: [] }), - buildSuccessState: (groups) => ({ status: 'success', groups }), + buildLoadingState: () => ({ status: 'loading', groups: [], serverTimeOffsetMs: null }), + buildSuccessState: (data) => ({ + status: 'success', + groups: data.groups, + serverTimeOffsetMs: data.serverTimeOffsetMs, + }), buildErrorState: (message, status) => ({ status: 'error', groups: [], + serverTimeOffsetMs: null, error: message, errorStatus: status, }), diff --git a/src/features/authFiles/components/QuotaProgressBar.tsx b/src/features/authFiles/components/QuotaProgressBar.tsx index 7ae10e98..4119d502 100644 --- a/src/features/authFiles/components/QuotaProgressBar.tsx +++ b/src/features/authFiles/components/QuotaProgressBar.tsx @@ -17,7 +17,7 @@ export function QuotaProgressBar({ percent, highThreshold, mediumThreshold }: Qu : normalized >= mediumThreshold ? styles.quotaBarFillMedium : styles.quotaBarFillLow; - const widthPercent = Math.round(normalized ?? 0); + const widthPercent = Math.round((normalized ?? 0) * 100) / 100; return (
@@ -25,4 +25,3 @@ export function QuotaProgressBar({ percent, highThreshold, mediumThreshold }: Qu
); } - diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7ed094d2..76236b63 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -386,7 +386,16 @@ "loading": "Loading quota...", "load_failed": "Failed to load quota: {{message}}", "missing_auth_index": "Auth file missing auth_index", + "missing_project_id": "Antigravity credential missing project_id. Re-login or refresh the credential to discover the project.", "empty_models": "No quota data available", + "quota_available": "Quota available", + "remaining_percent": "{{percent}}% remaining", + "refreshes_in": "Refreshes in {{duration}}", + "refresh_available": "Refresh available", + "duration_day_hour": "{{days}}d {{hours}}h", + "duration_hour_minute": "{{hours}}h {{minutes}}m", + "duration_minute": "{{minutes}}m", + "duration_less_than_minute": "<1m", "refresh_button": "Refresh Quota", "fetch_all": "Fetch All" }, diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 68d963f2..c09798fc 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -380,7 +380,16 @@ "loading": "Загрузка квоты...", "load_failed": "Не удалось загрузить квоту: {{message}}", "missing_auth_index": "В файле авторизации отсутствует auth_index", + "missing_project_id": "В учётных данных Antigravity отсутствует project_id. Войдите заново или обновите учётные данные, чтобы определить проект.", "empty_models": "Данные по квоте отсутствуют", + "quota_available": "Квота доступна", + "remaining_percent": "Осталось {{percent}}%", + "refreshes_in": "Обновится через {{duration}}", + "refresh_available": "Можно обновить", + "duration_day_hour": "{{days}} д {{hours}} ч", + "duration_hour_minute": "{{hours}} ч {{minutes}} мин", + "duration_minute": "{{minutes}} мин", + "duration_less_than_minute": "<1 мин", "refresh_button": "Обновить квоту", "fetch_all": "Получить все" }, diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 9c4432b5..a922ec0b 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -386,7 +386,16 @@ "loading": "正在加载额度...", "load_failed": "额度获取失败:{{message}}", "missing_auth_index": "认证文件缺少 auth_index", + "missing_project_id": "Antigravity 凭证缺少 project_id。请重新登录或刷新凭证以发现项目。", "empty_models": "暂无额度数据", + "quota_available": "额度可用", + "remaining_percent": "剩余 {{percent}}%", + "refreshes_in": "{{duration}} 后刷新", + "refresh_available": "可刷新", + "duration_day_hour": "{{days}} 天 {{hours}} 小时", + "duration_hour_minute": "{{hours}} 小时 {{minutes}} 分钟", + "duration_minute": "{{minutes}} 分钟", + "duration_less_than_minute": "小于 1 分钟", "refresh_button": "刷新额度", "fetch_all": "获取全部" }, diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 414391fa..d8e0e11e 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -386,7 +386,16 @@ "loading": "正在載入配額...", "load_failed": "配額取得失敗:{{message}}", "missing_auth_index": "驗證檔案缺少 auth_index", + "missing_project_id": "Antigravity 憑證缺少 project_id。請重新登入或重新整理憑證以探索專案。", "empty_models": "暫無配額資料", + "quota_available": "配額可用", + "remaining_percent": "剩餘 {{percent}}%", + "refreshes_in": "{{duration}} 後重新整理", + "refresh_available": "可重新整理", + "duration_day_hour": "{{days}} 天 {{hours}} 小時", + "duration_hour_minute": "{{hours}} 小時 {{minutes}} 分鐘", + "duration_minute": "{{minutes}} 分鐘", + "duration_less_than_minute": "小於 1 分鐘", "refresh_button": "重新整理配額", "fetch_all": "取得全部" }, diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index fc917c1d..12a8aa68 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -536,6 +536,37 @@ gap: $spacing-xs; } +.antigravityQuotaGroup { + display: flex; + flex-direction: column; + gap: $spacing-sm; + padding-bottom: $spacing-xs; +} + +.antigravityQuotaGroup + .antigravityQuotaGroup { + padding-top: $spacing-sm; + border-top: 1px solid color-mix(in srgb, var(--border-color) 65%, transparent); +} + +.antigravityQuotaGroupHeader { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.antigravityQuotaGroupTitle { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); +} + +.antigravityQuotaGroupDescription { + font-size: 12px; + line-height: 1.35; + color: var(--text-tertiary); +} + .quotaRowHeader { display: flex; align-items: center; diff --git a/src/pages/QuotaPage.module.scss b/src/pages/QuotaPage.module.scss index 3bcc079c..7823d979 100644 --- a/src/pages/QuotaPage.module.scss +++ b/src/pages/QuotaPage.module.scss @@ -288,6 +288,37 @@ gap: $spacing-xs; } +.antigravityQuotaGroup { + display: flex; + flex-direction: column; + gap: $spacing-sm; + padding-bottom: $spacing-xs; +} + +.antigravityQuotaGroup + .antigravityQuotaGroup { + padding-top: $spacing-sm; + border-top: 1px solid color-mix(in srgb, var(--border-color) 65%, transparent); +} + +.antigravityQuotaGroupHeader { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.antigravityQuotaGroupTitle { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); +} + +.antigravityQuotaGroupDescription { + font-size: 12px; + line-height: 1.35; + color: var(--text-tertiary); +} + .quotaRowHeader { display: flex; align-items: center; diff --git a/src/types/quota.ts b/src/types/quota.ts index 982069cc..d4d8677b 100644 --- a/src/types/quota.ts +++ b/src/types/quota.ts @@ -47,31 +47,28 @@ export interface GeminiCliCodeAssistPayload { paid_tier?: GeminiCliUserTier | null; } -export interface AntigravityQuotaInfo { +export interface AntigravityQuotaSummaryBucketPayload { + bucketId?: string; + bucket_id?: string; displayName?: string; - quotaInfo?: { - remainingFraction?: number | string; - remaining_fraction?: number | string; - remaining?: number | string; - resetTime?: string; - reset_time?: string; - }; - quota_info?: { - remainingFraction?: number | string; - remaining_fraction?: number | string; - remaining?: number | string; - resetTime?: string; - reset_time?: string; - }; + display_name?: string; + window?: string; + resetTime?: string; + reset_time?: string; + remainingFraction?: number | string; + remaining_fraction?: number | string; + description?: string; } -export type AntigravityModelsPayload = Record; +export interface AntigravityQuotaSummaryGroupPayload { + displayName?: string; + display_name?: string; + description?: string; + buckets?: AntigravityQuotaSummaryBucketPayload[]; +} -export interface AntigravityQuotaGroupDefinition { - id: string; - label: string; - identifiers: string[]; - labelFromModel?: boolean; +export interface AntigravityQuotaSummaryPayload { + groups?: AntigravityQuotaSummaryGroupPayload[]; } export interface GeminiCliQuotaGroupDefinition { @@ -204,14 +201,23 @@ export interface ClaudeQuotaState { export interface AntigravityQuotaGroup { id: string; label: string; - models: string[]; + description?: string; + buckets: AntigravityQuotaBucket[]; +} + +export interface AntigravityQuotaBucket { + id: string; + label: string; + window?: string; remainingFraction: number; resetTime?: string; + description?: string; } export interface AntigravityQuotaState { status: 'idle' | 'loading' | 'success' | 'error'; groups: AntigravityQuotaGroup[]; + serverTimeOffsetMs?: number | null; error?: string; errorStatus?: number; } diff --git a/src/utils/quota/builders.ts b/src/utils/quota/builders.ts index 51800b61..2dc9108f 100644 --- a/src/utils/quota/builders.ts +++ b/src/utils/quota/builders.ts @@ -3,10 +3,9 @@ */ import type { + AntigravityQuotaBucket, AntigravityQuotaGroup, - AntigravityQuotaGroupDefinition, - AntigravityQuotaInfo, - AntigravityModelsPayload, + AntigravityQuotaSummaryPayload, GeminiCliParsedBucket, GeminiCliQuotaBucketState, KimiUsagePayload, @@ -15,12 +14,8 @@ import type { KimiLimitWindow, KimiQuotaRow, } from '@/types'; -import { - ANTIGRAVITY_QUOTA_GROUPS, - GEMINI_CLI_GROUP_LOOKUP, - GEMINI_CLI_GROUP_ORDER, -} from './constants'; -import { normalizeQuotaFraction } from './parsers'; +import { GEMINI_CLI_GROUP_LOOKUP, GEMINI_CLI_GROUP_ORDER } from './constants'; +import { normalizeQuotaFraction, normalizeStringValue } from './parsers'; import { isIgnoredGeminiCliModel } from './validators'; export function pickEarlierResetTime(current?: string, next?: string): string | undefined { @@ -137,120 +132,80 @@ export function buildGeminiCliQuotaBuckets( }); } -export function getAntigravityQuotaInfo(entry?: AntigravityQuotaInfo): { - remainingFraction: number | null; - resetTime?: string; - displayName?: string; -} { - if (!entry) { - return { remainingFraction: null }; - } - const quotaInfo = entry.quotaInfo ?? entry.quota_info ?? {}; - const remainingValue = - quotaInfo.remainingFraction ?? quotaInfo.remaining_fraction ?? quotaInfo.remaining; - const remainingFraction = normalizeQuotaFraction(remainingValue); - const resetValue = quotaInfo.resetTime ?? quotaInfo.reset_time; - const resetTime = typeof resetValue === 'string' ? resetValue : undefined; - const displayName = typeof entry.displayName === 'string' ? entry.displayName : undefined; +const ANTIGRAVITY_BUCKET_WINDOW_ORDER = new Map([ + ['weekly', 0], + ['week', 0], + ['5h', 1], + ['five-hour', 1], + ['five_hour', 1], +]); - return { - remainingFraction, - resetTime, - displayName, - }; +function toStableId(value: string, fallback: string): string { + const normalized = value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + return normalized || fallback; } -export function findAntigravityModel( - models: AntigravityModelsPayload, - identifier: string -): { id: string; entry: AntigravityQuotaInfo } | null { - const direct = models[identifier]; - if (direct) { - return { id: identifier, entry: direct }; - } - - const match = Object.entries(models).find(([, entry]) => { - const name = typeof entry?.displayName === 'string' ? entry.displayName : ''; - return name.toLowerCase() === identifier.toLowerCase(); - }); - if (match) { - return { id: match[0], entry: match[1] }; - } - - return null; +function getAntigravityWindowOrder(bucket: AntigravityQuotaBucket): number { + const window = bucket.window?.toLowerCase(); + if (!window) return Number.MAX_SAFE_INTEGER; + return ANTIGRAVITY_BUCKET_WINDOW_ORDER.get(window) ?? Number.MAX_SAFE_INTEGER; } export function buildAntigravityQuotaGroups( - models: AntigravityModelsPayload + payload: AntigravityQuotaSummaryPayload ): AntigravityQuotaGroup[] { - const groups: AntigravityQuotaGroup[] = []; - const definitions = new Map( - ANTIGRAVITY_QUOTA_GROUPS.map((definition) => [definition.id, definition] as const) - ); + const groups = Array.isArray(payload.groups) ? payload.groups : []; - const buildGroup = ( - def: AntigravityQuotaGroupDefinition, - overrideResetTime?: string - ): AntigravityQuotaGroup | null => { - const matches = def.identifiers - .map((identifier) => findAntigravityModel(models, identifier)) - .filter((entry): entry is { id: string; entry: AntigravityQuotaInfo } => Boolean(entry)); + return groups + .map((group, groupIndex): AntigravityQuotaGroup | null => { + const label = + normalizeStringValue(group.displayName ?? group.display_name) ?? + `Quota Group ${groupIndex + 1}`; + const groupId = toStableId(label, `quota-group-${groupIndex + 1}`); + const buckets = Array.isArray(group.buckets) ? group.buckets : []; + const parsedBuckets = buckets + .map((bucket, bucketIndex): AntigravityQuotaBucket | null => { + const remainingFraction = normalizeQuotaFraction( + bucket.remainingFraction ?? bucket.remaining_fraction + ); + if (remainingFraction === null) return null; - const quotaEntries = matches - .map(({ id, entry }) => { - const info = getAntigravityQuotaInfo(entry); - const remainingFraction = info.remainingFraction ?? (info.resetTime ? 0 : null); - if (remainingFraction === null) return null; - return { - id, - remainingFraction, - resetTime: info.resetTime, - displayName: info.displayName, - }; - }) - .filter((entry): entry is NonNullable => entry !== null); + const window = normalizeStringValue(bucket.window) ?? undefined; + const rawId = + normalizeStringValue(bucket.bucketId ?? bucket.bucket_id) ?? + `${groupId}-${window ?? `bucket-${bucketIndex + 1}`}`; + const label = normalizeStringValue(bucket.displayName ?? bucket.display_name) ?? rawId; - if (quotaEntries.length === 0) return null; + return { + id: rawId, + label, + window, + remainingFraction, + resetTime: normalizeStringValue(bucket.resetTime ?? bucket.reset_time) ?? undefined, + description: normalizeStringValue(bucket.description) ?? undefined, + }; + }) + .filter((bucket): bucket is AntigravityQuotaBucket => bucket !== null) + .sort((a, b) => { + const orderDiff = getAntigravityWindowOrder(a) - getAntigravityWindowOrder(b); + if (orderDiff !== 0) return orderDiff; + return a.label.localeCompare(b.label); + }); - const remainingFraction = Math.min(...quotaEntries.map((entry) => entry.remainingFraction)); - const resetTime = - overrideResetTime ?? quotaEntries.map((entry) => entry.resetTime).find(Boolean); - const displayName = quotaEntries.map((entry) => entry.displayName).find(Boolean); - const label = def.labelFromModel && displayName ? displayName : def.label; + if (parsedBuckets.length === 0) return null; - return { - id: def.id, - label, - models: quotaEntries.map((entry) => entry.id), - remainingFraction, - resetTime, - }; - }; - - const appendGroup = ( - id: string, - overrideResetTime?: string - ): AntigravityQuotaGroup | null => { - const definition = definitions.get(id); - if (!definition) return null; - const group = buildGroup(definition, overrideResetTime); - if (group) { - groups.push(group); - } - return group; - }; - - appendGroup('claude-gpt'); - const gemini31ProGroup = appendGroup('gemini-3-1-pro-series'); - const geminiProGroup = appendGroup('gemini-3-pro'); - const geminiProResetTime = gemini31ProGroup?.resetTime ?? geminiProGroup?.resetTime; - appendGroup('gemini-2-5-flash'); - appendGroup('gemini-2-5-flash-lite'); - appendGroup('gemini-2-5-cu'); - appendGroup('gemini-3-flash'); - appendGroup('gemini-image', geminiProResetTime); - - return groups; + return { + id: groupId, + label, + description: normalizeStringValue(group.description) ?? undefined, + buckets: parsedBuckets, + }; + }) + .filter((group): group is AntigravityQuotaGroup => group !== null); } function toInt(value: unknown): number | null { diff --git a/src/utils/quota/constants.ts b/src/utils/quota/constants.ts index 43643d4b..3663c19e 100644 --- a/src/utils/quota/constants.ts +++ b/src/utils/quota/constants.ts @@ -2,11 +2,7 @@ * Quota constants for API URLs, headers, and theme colors. */ -import type { - AntigravityQuotaGroupDefinition, - GeminiCliQuotaGroupDefinition, - TypeColorSet, -} from '@/types'; +import type { GeminiCliQuotaGroupDefinition, TypeColorSet } from '@/types'; // Theme colors for type badges — 与 authFiles/constants.ts 保持同步 export const TYPE_COLORS: Record = { @@ -66,61 +62,17 @@ export const TYPE_COLORS: Record = { // Antigravity API configuration export const ANTIGRAVITY_QUOTA_URLS = [ - 'https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels', - 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels', - 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels', + 'https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary', + 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:retrieveUserQuotaSummary', + 'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary', ]; export const ANTIGRAVITY_REQUEST_HEADERS = { Authorization: 'Bearer $TOKEN$', 'Content-Type': 'application/json', - 'User-Agent': 'antigravity/1.11.5 windows/amd64', + 'User-Agent': 'antigravity/cli/1.0.8 darwin/arm64', }; -export const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroupDefinition[] = [ - { - id: 'claude-gpt', - label: 'Claude/GPT', - identifiers: ['claude-sonnet-4-6', 'claude-opus-4-6-thinking', 'gpt-oss-120b-medium'], - }, - { - id: 'gemini-3-pro', - label: 'Gemini 3 Pro', - identifiers: ['gemini-3-pro-high', 'gemini-3-pro-low'], - }, - { - id: 'gemini-3-1-pro-series', - label: 'Gemini 3.1 Pro Series', - identifiers: ['gemini-3.1-pro-high', 'gemini-3.1-pro-low'], - }, - { - id: 'gemini-2-5-flash', - label: 'Gemini 2.5 Flash', - identifiers: ['gemini-2.5-flash', 'gemini-2.5-flash-thinking'], - }, - { - id: 'gemini-2-5-flash-lite', - label: 'Gemini 2.5 Flash Lite', - identifiers: ['gemini-2.5-flash-lite'], - }, - { - id: 'gemini-2-5-cu', - label: 'Gemini 2.5 CU', - identifiers: ['rev19-uic3-1p'], - }, - { - id: 'gemini-3-flash', - label: 'Gemini 3 Flash', - identifiers: ['gemini-3-flash'], - }, - { - id: 'gemini-image', - label: 'gemini-3.1-flash-image', - identifiers: ['gemini-3.1-flash-image'], - labelFromModel: true, - }, -]; - // Gemini CLI API configuration export const GEMINI_CLI_QUOTA_URL = 'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota';