mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-21 06:06:24 +08:00
feat(quota): enhance quota management with new data structures and improved UI components
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": "Получить все"
|
||||
},
|
||||
|
||||
@@ -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": "获取全部"
|
||||
},
|
||||
|
||||
@@ -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": "取得全部"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user