mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-24 16:04:35 +08:00
feat(antigravity): add subscription management with UI integration and localization support
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
type ResolvedTheme,
|
||||
} from '@/features/authFiles/constants';
|
||||
import type { AuthFileStatusBarData } from '@/features/authFiles/hooks/useAuthFilesStatusBarCache';
|
||||
import type { AntigravitySubscriptionState } from '@/features/authFiles/hooks/useAntigravitySubscriptions';
|
||||
import { AuthFileQuotaSection } from '@/features/authFiles/components/AuthFileQuotaSection';
|
||||
import styles from '@/pages/AuthFilesPage.module.scss';
|
||||
|
||||
@@ -49,6 +50,7 @@ export type AuthFileCardProps = {
|
||||
statusUpdating: Record<string, boolean>;
|
||||
quotaFilterType: QuotaProviderType | null;
|
||||
statusBarCache: Map<string, AuthFileStatusBarData>;
|
||||
antigravitySubscription?: AntigravitySubscriptionState;
|
||||
onShowModels: (file: AuthFileItem) => void;
|
||||
onDownload: (name: string) => void;
|
||||
onOpenPrefixProxyEditor: (file: AuthFileItem) => void;
|
||||
@@ -75,6 +77,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
statusUpdating,
|
||||
quotaFilterType,
|
||||
statusBarCache,
|
||||
antigravitySubscription,
|
||||
onShowModels,
|
||||
onDownload,
|
||||
onOpenPrefixProxyEditor,
|
||||
@@ -90,6 +93,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
};
|
||||
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
|
||||
const providerKey = normalizeProviderKey(String(file.type ?? file.provider ?? 'unknown'));
|
||||
const isAntigravity = providerKey === 'antigravity';
|
||||
const isAistudio = providerKey === 'aistudio';
|
||||
const showModelsButton = !isRuntimeOnly || isAistudio;
|
||||
const typeColor = getTypeColor(providerKey, resolvedTheme);
|
||||
@@ -127,6 +131,53 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
|
||||
const priorityValue = parsePriorityValue(file.priority ?? file['priority']);
|
||||
const noteValue = typeof file.note === 'string' ? file.note.trim() : '';
|
||||
const subscription =
|
||||
isAntigravity && !isRuntimeOnly ? antigravitySubscription : undefined;
|
||||
const subscriptionData = subscription?.status === 'success' ? subscription.data : undefined;
|
||||
const subscriptionPlanLabel =
|
||||
subscriptionData?.plan === 'free'
|
||||
? t('antigravity_subscription.plan_free')
|
||||
: subscriptionData?.plan === 'pro'
|
||||
? t('antigravity_subscription.plan_pro')
|
||||
: subscriptionData?.plan === 'ultra'
|
||||
? t('antigravity_subscription.plan_ultra')
|
||||
: subscriptionData?.plan === 'ultra-lite'
|
||||
? t('antigravity_subscription.plan_ultra_lite')
|
||||
: subscriptionData
|
||||
? subscriptionData.tierName ||
|
||||
subscriptionData.tierId ||
|
||||
t('antigravity_subscription.plan_unknown')
|
||||
: '';
|
||||
const subscriptionBadgeLabel =
|
||||
subscription?.status === 'loading'
|
||||
? t('antigravity_subscription.loading_short')
|
||||
: subscription?.status === 'error'
|
||||
? t('antigravity_subscription.error_badge')
|
||||
: subscriptionData
|
||||
? t('antigravity_subscription.plan_badge', {
|
||||
plan: subscriptionPlanLabel,
|
||||
})
|
||||
: '';
|
||||
const subscriptionTitle =
|
||||
subscription?.status === 'error'
|
||||
? subscription.error || t('common.unknown_error')
|
||||
: subscriptionData?.tierName && subscriptionData.tierId
|
||||
? `${subscriptionData.tierName} (${subscriptionData.tierId})`
|
||||
: subscriptionData?.tierName || subscriptionData?.tierId || subscriptionBadgeLabel;
|
||||
const subscriptionBadgeClass =
|
||||
subscription?.status === 'loading'
|
||||
? styles.subscriptionBadgeLoading
|
||||
: subscription?.status === 'error'
|
||||
? styles.subscriptionBadgeError
|
||||
: subscriptionData?.plan === 'free'
|
||||
? styles.subscriptionBadgeFree
|
||||
: subscriptionData?.plan === 'unknown'
|
||||
? styles.subscriptionBadgeUnknown
|
||||
: styles.subscriptionBadgePaid;
|
||||
const subscriptionErrorMessage =
|
||||
subscription?.status === 'error'
|
||||
? subscription.error || t('common.unknown_error')
|
||||
: '';
|
||||
const stateLabel = isRuntimeOnly
|
||||
? t('auth_files.type_virtual') || '虚拟认证文件'
|
||||
: file.disabled
|
||||
@@ -191,10 +242,28 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
{typeLabel}
|
||||
</span>
|
||||
<span className={`${styles.stateBadge} ${stateBadgeClass}`}>{stateLabel}</span>
|
||||
{subscriptionBadgeLabel && (
|
||||
<span
|
||||
className={`${styles.subscriptionBadge} ${subscriptionBadgeClass}`}
|
||||
title={subscriptionTitle}
|
||||
>
|
||||
{subscriptionBadgeLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={styles.fileName} title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
{!compact && subscriptionData?.tierName && (
|
||||
<div className={styles.subscriptionSubtitle} title={subscriptionTitle}>
|
||||
<span className={styles.subscriptionSubtitleLabel}>
|
||||
{t('antigravity_subscription.subscription_label')}
|
||||
</span>
|
||||
<span className={styles.subscriptionSubtitleValue}>
|
||||
{subscriptionData.tierName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!compact && noteValue && (
|
||||
<div className={styles.noteText} title={noteValue}>
|
||||
<span className={styles.noteLabel}>{t('auth_files.note_display')}</span>
|
||||
@@ -232,6 +301,17 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subscriptionErrorMessage && (
|
||||
<div className={styles.subscriptionError} title={subscriptionErrorMessage}>
|
||||
<IconInfo className={styles.messageIcon} size={14} />
|
||||
<span>
|
||||
{t('antigravity_subscription.load_failed', {
|
||||
message: subscriptionErrorMessage,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${styles.cardInsights} ${compact ? styles.cardInsightsCompact : ''}`}>
|
||||
<div className={`${styles.cardStats} ${compact ? styles.cardStatsCompact : ''}`}>
|
||||
<div className={`${styles.statPill} ${styles.statSuccess}`}>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
antigravitySubscriptionApi,
|
||||
type AntigravitySubscriptionSummary,
|
||||
} from '@/services/api';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { normalizeAuthIndex } from '@/utils/authIndex';
|
||||
import { getStatusFromError, isAntigravityFile, isRuntimeOnlyAuthFile } from '@/utils/quota';
|
||||
|
||||
export type AntigravitySubscriptionState = {
|
||||
status: 'idle' | 'loading' | 'success' | 'error';
|
||||
data?: AntigravitySubscriptionSummary;
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
};
|
||||
|
||||
type SubscriptionTarget = {
|
||||
file: AuthFileItem;
|
||||
authIndex: string | null;
|
||||
cacheKey: string;
|
||||
};
|
||||
|
||||
type SubscriptionResult =
|
||||
| {
|
||||
name: string;
|
||||
cacheKey: string;
|
||||
status: 'success';
|
||||
data: AntigravitySubscriptionSummary;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
cacheKey: string;
|
||||
status: 'error';
|
||||
error: string;
|
||||
errorStatus?: number;
|
||||
};
|
||||
|
||||
const buildCacheKey = (file: AuthFileItem, authIndex: string | null): string =>
|
||||
`${file.name}\n${authIndex ?? ''}`;
|
||||
|
||||
export function useAntigravitySubscriptions(files: AuthFileItem[]) {
|
||||
const { t } = useTranslation();
|
||||
const [subscriptions, setSubscriptions] = useState<Record<string, AntigravitySubscriptionState>>(
|
||||
{}
|
||||
);
|
||||
const cacheKeysRef = useRef(new Map<string, string>());
|
||||
|
||||
const targets = useMemo(
|
||||
() =>
|
||||
files.reduce<SubscriptionTarget[]>((result, file) => {
|
||||
if (!isAntigravityFile(file) || isRuntimeOnlyAuthFile(file)) return result;
|
||||
const authIndex = normalizeAuthIndex(file['auth_index'] ?? file.authIndex);
|
||||
result.push({
|
||||
file,
|
||||
authIndex,
|
||||
cacheKey: buildCacheKey(file, authIndex),
|
||||
});
|
||||
return result;
|
||||
}, []),
|
||||
[files]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const targetsToLoad = targets.filter(
|
||||
(target) => cacheKeysRef.current.get(target.file.name) !== target.cacheKey
|
||||
);
|
||||
if (targetsToLoad.length === 0) return;
|
||||
|
||||
let cancelled = false;
|
||||
const requestTargets = targetsToLoad.filter((target) => target.authIndex);
|
||||
|
||||
setSubscriptions((prev) => {
|
||||
const next = { ...prev };
|
||||
targetsToLoad.forEach((target) => {
|
||||
cacheKeysRef.current.set(target.file.name, target.cacheKey);
|
||||
next[target.file.name] = target.authIndex
|
||||
? { status: 'loading' }
|
||||
: {
|
||||
status: 'error',
|
||||
error: t('antigravity_subscription.missing_auth_index'),
|
||||
};
|
||||
});
|
||||
return next;
|
||||
});
|
||||
|
||||
if (requestTargets.length === 0) return;
|
||||
|
||||
void (async () => {
|
||||
const results = await Promise.all(
|
||||
requestTargets.map(async (target): Promise<SubscriptionResult> => {
|
||||
try {
|
||||
const data = await antigravitySubscriptionApi.get(target.authIndex as string);
|
||||
if (!data) {
|
||||
return {
|
||||
name: target.file.name,
|
||||
cacheKey: target.cacheKey,
|
||||
status: 'error',
|
||||
error: t('antigravity_subscription.empty_data'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: target.file.name,
|
||||
cacheKey: target.cacheKey,
|
||||
status: 'success',
|
||||
data,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
return {
|
||||
name: target.file.name,
|
||||
cacheKey: target.cacheKey,
|
||||
status: 'error',
|
||||
error: err instanceof Error ? err.message : t('common.unknown_error'),
|
||||
errorStatus: getStatusFromError(err),
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
setSubscriptions((prev) => {
|
||||
const next = { ...prev };
|
||||
results.forEach((result) => {
|
||||
if (cacheKeysRef.current.get(result.name) !== result.cacheKey) return;
|
||||
if (result.status === 'success') {
|
||||
next[result.name] = { status: 'success', data: result.data };
|
||||
} else {
|
||||
next[result.name] = {
|
||||
status: 'error',
|
||||
error: result.error,
|
||||
errorStatus: result.errorStatus,
|
||||
};
|
||||
}
|
||||
});
|
||||
return next;
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [t, targets]);
|
||||
|
||||
return subscriptions;
|
||||
}
|
||||
@@ -378,6 +378,20 @@
|
||||
"quota_refresh_success": "Quota refreshed for \"{{name}}\"",
|
||||
"quota_refresh_failed": "Failed to refresh quota for \"{{name}}\": {{message}}"
|
||||
},
|
||||
"antigravity_subscription": {
|
||||
"subscription_label": "Subscription",
|
||||
"plan_badge": "Plan {{plan}}",
|
||||
"loading_short": "Plan...",
|
||||
"error_badge": "Plan error",
|
||||
"load_failed": "Subscription check failed: {{message}}",
|
||||
"missing_auth_index": "Auth file missing auth_index",
|
||||
"empty_data": "No subscription data available",
|
||||
"plan_unknown": "Unknown",
|
||||
"plan_free": "Free",
|
||||
"plan_pro": "Pro",
|
||||
"plan_ultra": "Ultra",
|
||||
"plan_ultra_lite": "Ultra Lite"
|
||||
},
|
||||
"antigravity_quota": {
|
||||
"title": "Antigravity Quota",
|
||||
"empty_title": "No Antigravity Auth Files",
|
||||
|
||||
@@ -372,6 +372,20 @@
|
||||
"quota_refresh_success": "Квота для \"{{name}}\" обновлена",
|
||||
"quota_refresh_failed": "Не удалось обновить квоту для \"{{name}}\": {{message}}"
|
||||
},
|
||||
"antigravity_subscription": {
|
||||
"subscription_label": "Подписка",
|
||||
"plan_badge": "План {{plan}}",
|
||||
"loading_short": "План...",
|
||||
"error_badge": "Ошибка плана",
|
||||
"load_failed": "Не удалось проверить подписку: {{message}}",
|
||||
"missing_auth_index": "В файле авторизации отсутствует auth_index",
|
||||
"empty_data": "Данные подписки отсутствуют",
|
||||
"plan_unknown": "Неизвестно",
|
||||
"plan_free": "Free",
|
||||
"plan_pro": "Pro",
|
||||
"plan_ultra": "Ultra",
|
||||
"plan_ultra_lite": "Ultra Lite"
|
||||
},
|
||||
"antigravity_quota": {
|
||||
"title": "Квота Antigravity",
|
||||
"empty_title": "Файлы авторизации Antigravity отсутствуют",
|
||||
|
||||
@@ -378,6 +378,20 @@
|
||||
"quota_refresh_success": "已刷新 \"{{name}}\" 的额度",
|
||||
"quota_refresh_failed": "刷新 \"{{name}}\" 的额度失败:{{message}}"
|
||||
},
|
||||
"antigravity_subscription": {
|
||||
"subscription_label": "订阅",
|
||||
"plan_badge": "套餐 {{plan}}",
|
||||
"loading_short": "套餐...",
|
||||
"error_badge": "套餐错误",
|
||||
"load_failed": "订阅状态获取失败:{{message}}",
|
||||
"missing_auth_index": "认证文件缺少 auth_index",
|
||||
"empty_data": "暂无订阅数据",
|
||||
"plan_unknown": "未知",
|
||||
"plan_free": "免费版",
|
||||
"plan_pro": "Pro",
|
||||
"plan_ultra": "Ultra",
|
||||
"plan_ultra_lite": "Ultra Lite"
|
||||
},
|
||||
"antigravity_quota": {
|
||||
"title": "Antigravity 额度",
|
||||
"empty_title": "暂无 Antigravity 认证",
|
||||
|
||||
@@ -378,6 +378,20 @@
|
||||
"quota_refresh_success": "已重新整理「{{name}}」的配額",
|
||||
"quota_refresh_failed": "重新整理「{{name}}」的配額失敗:{{message}}"
|
||||
},
|
||||
"antigravity_subscription": {
|
||||
"subscription_label": "訂閱",
|
||||
"plan_badge": "方案 {{plan}}",
|
||||
"loading_short": "方案...",
|
||||
"error_badge": "方案錯誤",
|
||||
"load_failed": "訂閱狀態取得失敗:{{message}}",
|
||||
"missing_auth_index": "驗證檔案缺少 auth_index",
|
||||
"empty_data": "暫無訂閱資料",
|
||||
"plan_unknown": "未知",
|
||||
"plan_free": "免費版",
|
||||
"plan_pro": "Pro",
|
||||
"plan_ultra": "Ultra",
|
||||
"plan_ultra_lite": "Ultra Lite"
|
||||
},
|
||||
"antigravity_quota": {
|
||||
"title": "Antigravity 配額",
|
||||
"empty_title": "暫無 Antigravity 驗證",
|
||||
|
||||
@@ -795,6 +795,7 @@
|
||||
.cardHeaderContent,
|
||||
.cardMeta,
|
||||
.healthStatusMessage,
|
||||
.subscriptionError,
|
||||
.cardInsights {
|
||||
opacity: 0.72;
|
||||
}
|
||||
@@ -851,7 +852,8 @@
|
||||
}
|
||||
|
||||
.typeBadge,
|
||||
.stateBadge {
|
||||
.stateBadge,
|
||||
.subscriptionBadge {
|
||||
padding: 3px 7px;
|
||||
font-size: 10px;
|
||||
}
|
||||
@@ -862,7 +864,8 @@
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.healthStatusMessage {
|
||||
.healthStatusMessage,
|
||||
.subscriptionError {
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
@@ -1022,6 +1025,56 @@
|
||||
border: 1px dashed color-mix(in srgb, var(--border-primary) 90%, transparent);
|
||||
}
|
||||
|
||||
.subscriptionBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.subscriptionBadgeLoading {
|
||||
color: var(--text-secondary);
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 86%, transparent);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.subscriptionBadgeFree {
|
||||
color: var(--text-secondary);
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 82%, transparent);
|
||||
border-color: color-mix(in srgb, var(--border-color) 86%, transparent);
|
||||
}
|
||||
|
||||
.subscriptionBadgePaid {
|
||||
color: #8a5b00;
|
||||
background: rgba(217, 165, 22, 0.14);
|
||||
border-color: rgba(217, 165, 22, 0.34);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .subscriptionBadgePaid {
|
||||
color: #ffd862;
|
||||
background: rgba(226, 180, 50, 0.12);
|
||||
border-color: rgba(226, 180, 50, 0.28);
|
||||
}
|
||||
|
||||
.subscriptionBadgeUnknown {
|
||||
color: var(--text-secondary);
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 80%, transparent);
|
||||
border-color: color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
}
|
||||
|
||||
.subscriptionBadgeError {
|
||||
color: var(--danger-color);
|
||||
background: rgba($error-color, 0.08);
|
||||
border-color: color-mix(in srgb, var(--danger-color) 70%, transparent);
|
||||
}
|
||||
|
||||
.fileName {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
@@ -1116,6 +1169,28 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.subscriptionSubtitle {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subscriptionSubtitleLabel {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-tertiary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subscriptionSubtitleValue {
|
||||
@include text-ellipsis-multiline(2);
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.sortSelect {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
@@ -1134,6 +1209,19 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.subscriptionError {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--danger-color);
|
||||
background-color: rgba($error-color, 0.08);
|
||||
border: 1px solid color-mix(in srgb, var(--danger-color) 58%, transparent);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.messageIcon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
|
||||
@@ -50,6 +50,7 @@ import { useAuthFilesModels } from '@/features/authFiles/hooks/useAuthFilesModel
|
||||
import { useAuthFilesOauth } from '@/features/authFiles/hooks/useAuthFilesOauth';
|
||||
import { useAuthFilesPrefixProxyEditor } from '@/features/authFiles/hooks/useAuthFilesPrefixProxyEditor';
|
||||
import { useAuthFilesStatusBarCache } from '@/features/authFiles/hooks/useAuthFilesStatusBarCache';
|
||||
import { useAntigravitySubscriptions } from '@/features/authFiles/hooks/useAntigravitySubscriptions';
|
||||
import {
|
||||
isAuthFilesSortMode,
|
||||
readAuthFilesUiState,
|
||||
@@ -430,7 +431,8 @@ export function AuthFilesPage() {
|
||||
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const start = (currentPage - 1) * pageSize;
|
||||
const pageItems = sorted.slice(start, start + pageSize);
|
||||
const pageItems = useMemo(() => sorted.slice(start, start + pageSize), [pageSize, sorted, start]);
|
||||
const antigravitySubscriptions = useAntigravitySubscriptions(pageItems);
|
||||
const selectablePageItems = useMemo(
|
||||
() => pageItems.filter((file) => !isRuntimeOnlyAuthFile(file)),
|
||||
[pageItems]
|
||||
@@ -811,6 +813,7 @@ export function AuthFilesPage() {
|
||||
statusUpdating={statusUpdating}
|
||||
quotaFilterType={quotaFilterType}
|
||||
statusBarCache={statusBarCache}
|
||||
antigravitySubscription={antigravitySubscriptions[file.name]}
|
||||
onShowModels={showModels}
|
||||
onDownload={handleDownload}
|
||||
onOpenPrefixProxyEditor={openPrefixProxyEditor}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { apiCallApi, getApiCallErrorMessage } from './apiCall';
|
||||
import {
|
||||
ANTIGRAVITY_CODE_ASSIST_URL,
|
||||
ANTIGRAVITY_REQUEST_HEADERS,
|
||||
createStatusError,
|
||||
normalizeStringValue,
|
||||
parseAntigravityPayload,
|
||||
} from '@/utils/quota';
|
||||
|
||||
export type AntigravitySubscriptionPlan = 'free' | 'pro' | 'ultra' | 'ultra-lite' | 'unknown';
|
||||
|
||||
export type AntigravitySubscriptionTier = {
|
||||
id: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
|
||||
export type AntigravitySubscriptionCredit = {
|
||||
creditType: string | null;
|
||||
creditAmount: number | string | null;
|
||||
minimumCreditAmountForUsage: number | string | null;
|
||||
};
|
||||
|
||||
export type AntigravitySubscriptionSummary = {
|
||||
plan: AntigravitySubscriptionPlan;
|
||||
tierId: string | null;
|
||||
tierName: string | null;
|
||||
source: 'paid' | 'current';
|
||||
currentTier: AntigravitySubscriptionTier | null;
|
||||
paidTier: AntigravitySubscriptionTier | null;
|
||||
availableCredits: AntigravitySubscriptionCredit[];
|
||||
};
|
||||
|
||||
type RawTierPayload = {
|
||||
id?: unknown;
|
||||
name?: unknown;
|
||||
availableCredits?: unknown;
|
||||
available_credits?: unknown;
|
||||
};
|
||||
|
||||
const CODE_ASSIST_REQUEST_BODY = JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } });
|
||||
|
||||
const PLAN_BY_TIER_ID = new Map<string, AntigravitySubscriptionPlan>([
|
||||
['free-tier', 'free'],
|
||||
['g1-pro-tier', 'pro'],
|
||||
['g1-ultra-tier', 'ultra'],
|
||||
['g1-ultra-lite-tier', 'ultra-lite'],
|
||||
]);
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const normalizeTier = (value: unknown): AntigravitySubscriptionTier | null => {
|
||||
if (!isRecord(value)) return null;
|
||||
const rawTier = value as RawTierPayload;
|
||||
return {
|
||||
id: normalizeStringValue(rawTier.id),
|
||||
name: normalizeStringValue(rawTier.name),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeCreditValue = (value: unknown): number | string | null => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
return normalizeStringValue(value);
|
||||
};
|
||||
|
||||
const normalizeCredits = (value: unknown): AntigravitySubscriptionCredit[] => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.filter(isRecord)
|
||||
.map((credit) => ({
|
||||
creditType: normalizeStringValue(credit.creditType ?? credit.credit_type),
|
||||
creditAmount: normalizeCreditValue(credit.creditAmount ?? credit.credit_amount),
|
||||
minimumCreditAmountForUsage: normalizeCreditValue(
|
||||
credit.minimumCreditAmountForUsage ?? credit.minimum_credit_amount_for_usage
|
||||
),
|
||||
}))
|
||||
.filter((credit) => credit.creditType || credit.creditAmount !== null);
|
||||
};
|
||||
|
||||
const resolvePlan = (tierId: string | null): AntigravitySubscriptionPlan => {
|
||||
if (!tierId) return 'unknown';
|
||||
return PLAN_BY_TIER_ID.get(tierId) ?? 'unknown';
|
||||
};
|
||||
|
||||
export const parseAntigravitySubscriptionSummary = (
|
||||
payload: unknown
|
||||
): AntigravitySubscriptionSummary | null => {
|
||||
const parsed = parseAntigravityPayload(payload);
|
||||
if (!parsed) return null;
|
||||
|
||||
const currentTierPayload = parsed.currentTier ?? parsed.current_tier;
|
||||
const paidTierPayload = parsed.paidTier ?? parsed.paid_tier;
|
||||
const currentTier = normalizeTier(currentTierPayload);
|
||||
const paidTier = normalizeTier(paidTierPayload);
|
||||
const effectiveTier = paidTier?.id ? paidTier : currentTier;
|
||||
if (!effectiveTier?.id && !effectiveTier?.name) return null;
|
||||
|
||||
const rawCredits = isRecord(paidTierPayload)
|
||||
? paidTierPayload.availableCredits ?? paidTierPayload.available_credits
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
plan: resolvePlan(effectiveTier.id),
|
||||
tierId: effectiveTier.id,
|
||||
tierName: effectiveTier.name,
|
||||
source: paidTier?.id ? 'paid' : 'current',
|
||||
currentTier,
|
||||
paidTier,
|
||||
availableCredits: normalizeCredits(rawCredits),
|
||||
};
|
||||
};
|
||||
|
||||
export const antigravitySubscriptionApi = {
|
||||
async get(authIndex: string): Promise<AntigravitySubscriptionSummary | null> {
|
||||
const result = await apiCallApi.request({
|
||||
authIndex,
|
||||
method: 'POST',
|
||||
url: ANTIGRAVITY_CODE_ASSIST_URL,
|
||||
header: { ...ANTIGRAVITY_REQUEST_HEADERS },
|
||||
data: CODE_ASSIST_REQUEST_BODY,
|
||||
});
|
||||
|
||||
if (result.statusCode < 200 || result.statusCode >= 300) {
|
||||
throw createStatusError(getApiCallErrorMessage(result), result.statusCode);
|
||||
}
|
||||
|
||||
return parseAntigravitySubscriptionSummary(result.body ?? result.bodyText);
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './client';
|
||||
export * from './apiCall';
|
||||
export * from './antigravitySubscription';
|
||||
export * from './apiKeyUsage';
|
||||
export * from './config';
|
||||
export * from './configFile';
|
||||
|
||||
@@ -67,6 +67,9 @@ export const ANTIGRAVITY_QUOTA_URLS = [
|
||||
'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary',
|
||||
];
|
||||
|
||||
export const ANTIGRAVITY_CODE_ASSIST_URL =
|
||||
'https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist';
|
||||
|
||||
export const ANTIGRAVITY_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user