feat(quota): enhance quota management with new data structures and improved UI components

This commit is contained in:
LTbinglingfeng
2026-06-17 12:30:03 +08:00
parent dbeefda721
commit e95cc2b621
12 changed files with 358 additions and 226 deletions
+1 -1
View File
@@ -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 (
<div className={styles.quotaBar}>
+159 -37
View File
@@ -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> = 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<string, number>();
@@ -144,10 +148,33 @@ export interface QuotaConfig<TState, TData> {
}
const resolveAntigravityProjectId = async (file: AuthFileItem): Promise<string> => {
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<string, unknown>)
: 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<string, unknown>)
: 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<string, unknown>;
const topLevel = normalizeStringValue(parsed.project_id ?? parsed.projectId);
@@ -169,16 +196,28 @@ const resolveAntigravityProjectId = async (file: AuthFileItem): Promise<string>
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<string, string[]> | 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<AntigravityQuotaGroup[]> => {
): Promise<AntigravityQuotaData> => {
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<AntigravityQuotaState, AntigravityQuotaGroup[]> = {
export const ANTIGRAVITY_CONFIG: QuotaConfig<AntigravityQuotaState, AntigravityQuotaData> = {
type: 'antigravity',
i18nPrefix: 'antigravity_quota',
cardIdleMessageKey: 'quota_management.card_idle_hint',
@@ -1351,11 +1468,16 @@ export const ANTIGRAVITY_CONFIG: QuotaConfig<AntigravityQuotaState, AntigravityQ
fetchQuota: fetchAntigravityQuota,
storeSelector: (state) => 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,
}),
@@ -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 (
<div className={styles.quotaBar}>
@@ -25,4 +25,3 @@ export function QuotaProgressBar({ percent, highThreshold, mediumThreshold }: Qu
</div>
);
}
+9
View File
@@ -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"
},
+9
View File
@@ -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": "Получить все"
},
+9
View File
@@ -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": "获取全部"
},
+9
View File
@@ -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": "取得全部"
},
+31
View File
@@ -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;
+31
View File
@@ -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;
+28 -22
View File
@@ -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<string, AntigravityQuotaInfo>;
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;
}
+66 -111
View File
@@ -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<string, number>([
['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<typeof entry> => 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 {
+5 -53
View File
@@ -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<string, TypeColorSet> = {
@@ -66,61 +62,17 @@ export const TYPE_COLORS: Record<string, TypeColorSet> = {
// 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';