Compare commits

...

2 Commits

Author SHA1 Message Date
Supra4E8C
a44257edda fix(antigravity): enhance error handling and support multiple request bodies 2026-01-14 17:13:07 +08:00
Supra4E8C
ebb80df24a fix(quota): include project_id in antigravity quota requests 2026-01-14 16:44:36 +08:00
2 changed files with 95 additions and 36 deletions

View File

@@ -18,7 +18,7 @@ import type {
GeminiCliQuotaBucketState, GeminiCliQuotaBucketState,
GeminiCliQuotaState GeminiCliQuotaState
} from '@/types'; } from '@/types';
import { apiCallApi, getApiCallErrorMessage } from '@/services/api'; import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
import { import {
ANTIGRAVITY_QUOTA_URLS, ANTIGRAVITY_QUOTA_URLS,
ANTIGRAVITY_REQUEST_HEADERS, ANTIGRAVITY_REQUEST_HEADERS,
@@ -55,6 +55,8 @@ type QuotaUpdater<T> = T | ((prev: T) => T);
type QuotaType = 'antigravity' | 'codex' | 'gemini-cli'; type QuotaType = 'antigravity' | 'codex' | 'gemini-cli';
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
export interface QuotaStore { export interface QuotaStore {
antigravityQuota: Record<string, AntigravityQuotaState>; antigravityQuota: Record<string, AntigravityQuotaState>;
codexQuota: Record<string, CodexQuotaState>; codexQuota: Record<string, CodexQuotaState>;
@@ -82,6 +84,43 @@ export interface QuotaConfig<TState, TData> {
renderQuotaItems: (quota: TState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode; renderQuotaItems: (quota: TState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode;
} }
const resolveAntigravityProjectId = async (file: AuthFileItem): Promise<string> => {
try {
const text = await authFilesApi.downloadText(file.name);
const trimmed = text.trim();
if (!trimmed) return DEFAULT_ANTIGRAVITY_PROJECT_ID;
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
const topLevel = normalizeStringValue(parsed.project_id ?? parsed.projectId);
if (topLevel) return topLevel;
const installed =
parsed.installed && typeof parsed.installed === 'object' && parsed.installed !== null
? (parsed.installed as Record<string, unknown>)
: null;
const installedProjectId = installed
? normalizeStringValue(installed.project_id ?? installed.projectId)
: null;
if (installedProjectId) return installedProjectId;
const web =
parsed.web && typeof parsed.web === 'object' && parsed.web !== null
? (parsed.web as Record<string, unknown>)
: null;
const webProjectId = web ? normalizeStringValue(web.project_id ?? web.projectId) : null;
if (webProjectId) return webProjectId;
} catch {
return DEFAULT_ANTIGRAVITY_PROJECT_ID;
}
return DEFAULT_ANTIGRAVITY_PROJECT_ID;
};
const isAntigravityUnknownFieldError = (message: string): boolean => {
const normalized = message.toLowerCase();
return normalized.includes('unknown name') && normalized.includes('cannot find field');
};
const fetchAntigravityQuota = async ( const fetchAntigravityQuota = async (
file: AuthFileItem, file: AuthFileItem,
t: TFunction t: TFunction
@@ -92,52 +131,64 @@ const fetchAntigravityQuota = async (
throw new Error(t('antigravity_quota.missing_auth_index')); throw new Error(t('antigravity_quota.missing_auth_index'));
} }
const projectId = await resolveAntigravityProjectId(file);
const requestBodies = [JSON.stringify({ projectId }), JSON.stringify({ project: projectId })];
let lastError = ''; let lastError = '';
let lastStatus: number | undefined; let lastStatus: number | undefined;
let priorityStatus: number | undefined; let priorityStatus: number | undefined;
let hadSuccess = false; let hadSuccess = false;
for (const url of ANTIGRAVITY_QUOTA_URLS) { for (const url of ANTIGRAVITY_QUOTA_URLS) {
try { for (let attempt = 0; attempt < requestBodies.length; attempt++) {
const result = await apiCallApi.request({ try {
authIndex, const result = await apiCallApi.request({
method: 'POST', authIndex,
url, method: 'POST',
header: { ...ANTIGRAVITY_REQUEST_HEADERS }, url,
data: '{}' header: { ...ANTIGRAVITY_REQUEST_HEADERS },
}); data: requestBodies[attempt]
});
if (result.statusCode < 200 || result.statusCode >= 300) { if (result.statusCode < 200 || result.statusCode >= 300) {
lastError = getApiCallErrorMessage(result); lastError = getApiCallErrorMessage(result);
lastStatus = result.statusCode; lastStatus = result.statusCode;
if (result.statusCode === 403 || result.statusCode === 404) { if (result.statusCode === 403 || result.statusCode === 404) {
priorityStatus ??= result.statusCode; priorityStatus ??= result.statusCode;
}
if (
result.statusCode === 400 &&
isAntigravityUnknownFieldError(lastError) &&
attempt < requestBodies.length - 1
) {
continue;
}
break;
} }
continue;
}
hadSuccess = true; hadSuccess = true;
const payload = parseAntigravityPayload(result.body ?? result.bodyText); const payload = parseAntigravityPayload(result.body ?? result.bodyText);
const models = payload?.models; const models = payload?.models;
if (!models || typeof models !== 'object' || Array.isArray(models)) { if (!models || typeof models !== 'object' || Array.isArray(models)) {
lastError = t('antigravity_quota.empty_models'); lastError = t('antigravity_quota.empty_models');
continue; continue;
} }
const groups = buildAntigravityQuotaGroups(models as AntigravityModelsPayload); const groups = buildAntigravityQuotaGroups(models as AntigravityModelsPayload);
if (groups.length === 0) { if (groups.length === 0) {
lastError = t('antigravity_quota.empty_models'); lastError = t('antigravity_quota.empty_models');
continue; continue;
} }
return groups; return groups;
} catch (err: unknown) { } catch (err: unknown) {
lastError = err instanceof Error ? err.message : t('common.unknown_error'); lastError = err instanceof Error ? err.message : t('common.unknown_error');
const status = getStatusFromError(err); const status = getStatusFromError(err);
if (status) { if (status) {
lastStatus = status; lastStatus = status;
if (status === 403 || status === 404) { if (status === 403 || status === 404) {
priorityStatus ??= status; priorityStatus ??= status;
}
} }
} }
} }

View File

@@ -56,6 +56,14 @@ export const authFilesApi = {
deleteAll: () => apiClient.delete('/auth-files', { params: { all: true } }), deleteAll: () => apiClient.delete('/auth-files', { params: { all: true } }),
downloadText: async (name: string): Promise<string> => {
const response = await apiClient.getRaw(`/auth-files/download?name=${encodeURIComponent(name)}`, {
responseType: 'blob'
});
const blob = response.data as Blob;
return blob.text();
},
// OAuth 排除模型 // OAuth 排除模型
async getOauthExcludedModels(): Promise<Record<string, string[]>> { async getOauthExcludedModels(): Promise<Record<string, string[]>> {
const data = await apiClient.get('/oauth-excluded-models'); const data = await apiClient.get('/oauth-excluded-models');