Compare commits

..

4 Commits

Author SHA1 Message Date
Supra4E8C
8a59ab73a1 chore(i18n): update antigravity refresh label 2025-12-29 12:33:04 +08:00
Supra4E8C
66d58288b4 fix(auth): update antigravity fetchAvailableModels endpoint 2025-12-29 12:09:37 +08:00
Supra4E8C
be3f58f0a8 fix(auth-files): cache Antigravity quota to avoid auto refresh on reopen 2025-12-29 01:18:18 +08:00
Supra4E8C
c299e403cc feat(auth-files): add Antigravity quota page size 2025-12-29 00:48:31 +08:00
4 changed files with 158 additions and 77 deletions

View File

@@ -361,10 +361,13 @@
"title": "Antigravity Quota",
"empty_title": "No Antigravity Auth Files",
"empty_desc": "Upload an Antigravity credential to view remaining quota.",
"idle": "Not loaded. Click Refresh Button.",
"loading": "Loading quota...",
"load_failed": "Failed to load quota: {{message}}",
"missing_auth_index": "Auth file missing auth_index",
"empty_models": "No quota data available"
"empty_models": "No quota data available",
"refresh_button": "Refresh Quota",
"fetch_all": "Fetch All"
},
"vertex_import": {
"title": "Vertex JSON Login",

View File

@@ -361,10 +361,13 @@
"title": "Antigravity 额度",
"empty_title": "暂无 Antigravity 认证",
"empty_desc": "上传 Antigravity 认证文件后即可查看额度。",
"idle": "尚未加载额度,请点击刷新按钮。",
"loading": "正在加载额度...",
"load_failed": "额度获取失败:{{message}}",
"missing_auth_index": "认证文件缺少 auth_index",
"empty_models": "暂无额度数据"
"empty_models": "暂无额度数据",
"refresh_button": "刷新额度",
"fetch_all": "获取全部"
},
"vertex_import": {
"title": "Vertex JSON 登录",

View File

@@ -176,6 +176,27 @@
}
}
.antigravityControls {
display: flex;
gap: $spacing-md;
flex-wrap: wrap;
align-items: flex-end;
margin-bottom: $spacing-md;
}
.antigravityControl {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
white-space: nowrap;
}
}
.antigravityCard {
background-image: linear-gradient(
180deg,

View File

@@ -126,7 +126,7 @@ interface AntigravityQuotaGroupDefinition {
}
const ANTIGRAVITY_QUOTA_URLS = [
'https://cloudcode-pa-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
'https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels',
'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels'
];
@@ -171,6 +171,8 @@ const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroupDefinition[] = [
labelFromModel: true
}
];
// 标准化 auth_index 值(与 usage.ts 中的 normalizeAuthIndex 保持一致)
function normalizeAuthIndexValue(value: unknown): string | null {
@@ -393,6 +395,7 @@ export function AuthFilesPage() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(9);
const [antigravityPage, setAntigravityPage] = useState(1);
const [antigravityPageSize, setAntigravityPageSize] = useState(6);
const [uploading, setUploading] = useState(false);
const [deleting, setDeleting] = useState<string | null>(null);
const [deletingAll, setDeletingAll] = useState(false);
@@ -402,6 +405,9 @@ export function AuthFilesPage() {
{}
);
const [antigravityLoading, setAntigravityLoading] = useState(false);
const [antigravityLoadingScope, setAntigravityLoadingScope] = useState<
'page' | 'all' | null
>(null);
// 详情弹窗相关
const [detailModalOpen, setDetailModalOpen] = useState(false);
@@ -508,7 +514,6 @@ export function AuthFilesPage() {
[files]
);
const antigravityPageSize = 6;
const antigravityTotalPages = Math.max(
1,
Math.ceil(antigravityFiles.length / antigravityPageSize)
@@ -569,71 +574,77 @@ export function AuthFilesPage() {
[t]
);
const loadAntigravityQuota = useCallback(async () => {
if (antigravityLoadingRef.current) return;
antigravityLoadingRef.current = true;
const requestId = ++antigravityRequestIdRef.current;
setAntigravityLoading(true);
const loadAntigravityQuota = useCallback(
async (targets: AuthFileItem[], scope: 'page' | 'all') => {
if (antigravityLoadingRef.current) return;
antigravityLoadingRef.current = true;
const requestId = ++antigravityRequestIdRef.current;
setAntigravityLoading(true);
setAntigravityLoadingScope(scope);
try {
if (antigravityFiles.length === 0) {
setAntigravityQuota({});
return;
}
try {
if (targets.length === 0) return;
const loadingState: Record<string, AntigravityQuotaState> = {};
antigravityFiles.forEach((file) => {
loadingState[file.name] = { status: 'loading', groups: [] };
});
setAntigravityQuota(loadingState);
setAntigravityQuota((prev) => {
const nextState = { ...prev };
targets.forEach((file) => {
nextState[file.name] = { status: 'loading', groups: [] };
});
return nextState;
});
const results = await Promise.all(
antigravityFiles.map(async (file) => {
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
const authIndex = normalizeAuthIndexValue(rawAuthIndex);
if (!authIndex) {
return {
name: file.name,
status: 'error' as const,
error: t('antigravity_quota.missing_auth_index')
};
}
const results = await Promise.all(
targets.map(async (file) => {
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
const authIndex = normalizeAuthIndexValue(rawAuthIndex);
if (!authIndex) {
return {
name: file.name,
status: 'error' as const,
error: t('antigravity_quota.missing_auth_index')
};
}
try {
const groups = await fetchAntigravityQuota(authIndex);
return { name: file.name, status: 'success' as const, groups };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('common.unknown_error');
return { name: file.name, status: 'error' as const, error: message };
}
})
);
try {
const groups = await fetchAntigravityQuota(authIndex);
return { name: file.name, status: 'success' as const, groups };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('common.unknown_error');
return { name: file.name, status: 'error' as const, error: message };
}
})
);
if (requestId !== antigravityRequestIdRef.current) return;
if (requestId !== antigravityRequestIdRef.current) return;
const nextState: Record<string, AntigravityQuotaState> = {};
results.forEach((result) => {
if (result.status === 'success') {
nextState[result.name] = {
status: 'success',
groups: result.groups
};
} else {
nextState[result.name] = {
status: 'error',
groups: [],
error: result.error
};
setAntigravityQuota((prev) => {
const nextState = { ...prev };
results.forEach((result) => {
if (result.status === 'success') {
nextState[result.name] = {
status: 'success',
groups: result.groups
};
} else {
nextState[result.name] = {
status: 'error',
groups: [],
error: result.error
};
}
});
return nextState;
});
} finally {
if (requestId === antigravityRequestIdRef.current) {
setAntigravityLoading(false);
setAntigravityLoadingScope(null);
antigravityLoadingRef.current = false;
}
});
setAntigravityQuota(nextState);
} finally {
if (requestId === antigravityRequestIdRef.current) {
setAntigravityLoading(false);
antigravityLoadingRef.current = false;
}
}
}, [antigravityFiles, fetchAntigravityQuota, t]);
},
[fetchAntigravityQuota, t]
);
useEffect(() => {
loadFiles();
@@ -646,15 +657,20 @@ export function AuthFilesPage() {
setAntigravityQuota({});
return;
}
loadAntigravityQuota();
}, [antigravityFiles, loadAntigravityQuota]);
setAntigravityQuota((prev) => {
const nextState: Record<string, AntigravityQuotaState> = {};
antigravityFiles.forEach((file) => {
const cached = prev[file.name];
if (cached) {
nextState[file.name] = cached;
}
});
return nextState;
});
}, [antigravityFiles]);
// 定时刷新状态数据每240秒
useInterval(loadKeyStats, 240_000);
useInterval(() => {
if (antigravityFiles.length === 0) return;
loadAntigravityQuota();
}, 240_000);
// 提取所有存在的类型
const existingTypes = useMemo(() => {
@@ -1211,8 +1227,10 @@ export function AuthFilesPage() {
</div>
<div className={styles.quotaSection}>
{quotaStatus === 'loading' || quotaStatus === 'idle' ? (
{quotaStatus === 'loading' ? (
<div className={styles.quotaMessage}>{t('antigravity_quota.loading')}</div>
) : quotaStatus === 'idle' ? (
<div className={styles.quotaMessage}>{t('antigravity_quota.idle')}</div>
) : quotaStatus === 'error' ? (
<div className={styles.quotaError}>
{t('antigravity_quota.load_failed', {
@@ -1383,15 +1401,26 @@ export function AuthFilesPage() {
<Card
title={t('antigravity_quota.title')}
extra={
<Button
variant="secondary"
size="sm"
onClick={loadAntigravityQuota}
disabled={disableControls || antigravityLoading || antigravityFiles.length === 0}
loading={antigravityLoading}
>
{t('common.refresh')}
</Button>
<div className={styles.headerActions}>
<Button
variant="secondary"
size="sm"
onClick={() => loadAntigravityQuota(antigravityPageItems, 'page')}
disabled={disableControls || antigravityLoading || antigravityPageItems.length === 0}
loading={antigravityLoading && antigravityLoadingScope === 'page'}
>
{t('antigravity_quota.refresh_button')}
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => loadAntigravityQuota(antigravityFiles, 'all')}
disabled={disableControls || antigravityLoading || antigravityFiles.length === 0}
loading={antigravityLoading && antigravityLoadingScope === 'all'}
>
{t('antigravity_quota.fetch_all')}
</Button>
</div>
}
>
{antigravityFiles.length === 0 ? (
@@ -1401,6 +1430,31 @@ export function AuthFilesPage() {
/>
) : (
<>
<div className={styles.antigravityControls}>
<div className={styles.antigravityControl}>
<label>{t('auth_files.page_size_label')}</label>
<select
className={styles.pageSizeSelect}
value={antigravityPageSize}
onChange={(e) => {
setAntigravityPageSize(Number(e.target.value) || 6);
setAntigravityPage(1);
}}
>
<option value={6}>6</option>
<option value={9}>9</option>
<option value={12}>12</option>
<option value={18}>18</option>
<option value={24}>24</option>
</select>
</div>
<div className={styles.antigravityControl}>
<label>{t('common.info')}</label>
<div className={styles.statsInfo}>
{antigravityFiles.length} {t('auth_files.files_count')}
</div>
</div>
</div>
<div className={styles.antigravityGrid}>
{antigravityPageItems.map(renderAntigravityCard)}
</div>