feat(quota): restore Claude plan detection via profile API

This commit is contained in:
Supra4E8C
2026-03-07 15:48:03 +08:00
parent 38d7e20320
commit ccf90f88dd
6 changed files with 126 additions and 12 deletions
+80 -9
View File
@@ -11,6 +11,7 @@ import type {
AntigravityQuotaState,
AuthFileItem,
ClaudeExtraUsage,
ClaudeProfileResponse,
ClaudeQuotaState,
ClaudeQuotaWindow,
ClaudeUsagePayload,
@@ -29,6 +30,7 @@ import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api
import {
ANTIGRAVITY_QUOTA_URLS,
ANTIGRAVITY_REQUEST_HEADERS,
CLAUDE_PROFILE_URL,
CLAUDE_USAGE_URL,
CLAUDE_REQUEST_HEADERS,
CLAUDE_USAGE_WINDOW_KEYS,
@@ -673,22 +675,69 @@ const buildClaudeQuotaWindows = (
return windows;
};
const CLAUDE_PLAN_TYPE_MAP: Record<string, string> = {
default_claude_max_5x: 'plan_max5',
default_claude_max_20x: 'plan_max20',
default_claude_pro: 'plan_pro',
default_claude_ai: 'plan_free',
};
const parseClaudeProfilePayload = (payload: unknown): ClaudeProfileResponse | null => {
if (payload === undefined || payload === null) return null;
if (typeof payload === 'string') {
const trimmed = payload.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed) as ClaudeProfileResponse;
} catch {
return null;
}
}
if (typeof payload === 'object') {
return payload as ClaudeProfileResponse;
}
return null;
};
const resolveClaudePlanType = (profile: ClaudeProfileResponse | null): string | null => {
if (!profile) return null;
const tier = normalizeStringValue(profile.organization?.rate_limit_tier);
if (!tier) return null;
return CLAUDE_PLAN_TYPE_MAP[tier] ?? 'plan_unknown';
};
const fetchClaudeQuota = async (
file: AuthFileItem,
t: TFunction
): Promise<{ windows: ClaudeQuotaWindow[]; extraUsage?: ClaudeExtraUsage | null }> => {
): Promise<{ windows: ClaudeQuotaWindow[]; extraUsage?: ClaudeExtraUsage | null; planType?: string | null }> => {
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
const authIndex = normalizeAuthIndex(rawAuthIndex);
if (!authIndex) {
throw new Error(t('claude_quota.missing_auth_index'));
}
const result = await apiCallApi.request({
authIndex,
method: 'GET',
url: CLAUDE_USAGE_URL,
header: { ...CLAUDE_REQUEST_HEADERS },
});
const [usageResult, profileResult] = await Promise.allSettled([
apiCallApi.request({
authIndex,
method: 'GET',
url: CLAUDE_USAGE_URL,
header: { ...CLAUDE_REQUEST_HEADERS },
}),
apiCallApi.request({
authIndex,
method: 'GET',
url: CLAUDE_PROFILE_URL,
header: { ...CLAUDE_REQUEST_HEADERS },
}),
]);
if (usageResult.status === 'rejected') {
throw usageResult.reason;
}
const result = usageResult.value;
if (result.statusCode < 200 || result.statusCode >= 300) {
throw createStatusError(getApiCallErrorMessage(result), result.statusCode);
@@ -700,7 +749,16 @@ const fetchClaudeQuota = async (
}
const windows = buildClaudeQuotaWindows(payload, t);
return { windows, extraUsage: payload.extra_usage };
const planType =
profileResult.status === 'fulfilled' &&
profileResult.value.statusCode >= 200 &&
profileResult.value.statusCode < 300
? resolveClaudePlanType(
parseClaudeProfilePayload(profileResult.value.body ?? profileResult.value.bodyText)
)
: null;
return { windows, extraUsage: payload.extra_usage, planType };
};
const renderClaudeItems = (
@@ -712,8 +770,20 @@ const renderClaudeItems = (
const { createElement: h, Fragment } = React;
const windows = quota.windows ?? [];
const extraUsage = quota.extraUsage ?? null;
const planType = quota.planType ?? null;
const nodes: ReactNode[] = [];
if (planType) {
nodes.push(
h(
'div',
{ key: 'plan', className: styleMap.codexPlan },
h('span', { className: styleMap.codexPlanLabel }, t('claude_quota.plan_label')),
h('span', { className: styleMap.codexPlanValue }, t(`claude_quota.${planType}`))
)
);
}
if (extraUsage && extraUsage.is_enabled) {
const usedLabel = `$${(extraUsage.used_credits / 100).toFixed(2)} / $${(extraUsage.monthly_limit / 100).toFixed(2)}`;
nodes.push(
@@ -765,7 +835,7 @@ const renderClaudeItems = (
export const CLAUDE_CONFIG: QuotaConfig<
ClaudeQuotaState,
{ windows: ClaudeQuotaWindow[]; extraUsage?: ClaudeExtraUsage | null }
{ windows: ClaudeQuotaWindow[]; extraUsage?: ClaudeExtraUsage | null; planType?: string | null }
> = {
type: 'claude',
i18nPrefix: 'claude_quota',
@@ -779,6 +849,7 @@ export const CLAUDE_CONFIG: QuotaConfig<
status: 'success',
windows: data.windows,
extraUsage: data.extraUsage,
planType: data.planType,
}),
buildErrorState: (message, status) => ({
status: 'error',
+7 -1
View File
@@ -588,7 +588,13 @@
"seven_day_sonnet": "7-day Sonnet",
"seven_day_cowork": "7-day Cowork",
"iguana_necktie": "Iguana Necktie",
"extra_usage_label": "Extra Usage"
"extra_usage_label": "Extra Usage",
"plan_label": "Plan",
"plan_unknown": "Unknown",
"plan_free": "Free",
"plan_pro": "Pro",
"plan_max5": "Max 5x",
"plan_max20": "Max 20x"
},
"codex_quota": {
"title": "Codex Quota",
+7 -1
View File
@@ -591,7 +591,13 @@
"seven_day_sonnet": "7 дней Sonnet",
"seven_day_cowork": "7 дней Cowork",
"iguana_necktie": "Iguana Necktie",
"extra_usage_label": "Дополнительное использование"
"extra_usage_label": "Дополнительное использование",
"plan_label": "План",
"plan_unknown": "Неизвестно",
"plan_free": "Free",
"plan_pro": "Pro",
"plan_max5": "Max 5x",
"plan_max20": "Max 20x"
},
"codex_quota": {
"title": "Квота Codex",
+7 -1
View File
@@ -588,7 +588,13 @@
"seven_day_sonnet": "7 天 Sonnet",
"seven_day_cowork": "7 天 Cowork",
"iguana_necktie": "Iguana Necktie",
"extra_usage_label": "额外用量"
"extra_usage_label": "额外用量",
"plan_label": "套餐",
"plan_unknown": "未知",
"plan_free": "免费版",
"plan_pro": "专业版",
"plan_max5": "Max 5x",
"plan_max20": "Max 20x"
},
"codex_quota": {
"title": "Codex 额度",
+23
View File
@@ -132,6 +132,28 @@ export interface ClaudeUsagePayload {
extra_usage?: ClaudeExtraUsage | null;
}
export interface ClaudeProfileResponse {
account?: {
uuid?: string;
full_name?: string;
display_name?: string;
email?: string;
has_claude_max?: boolean;
has_claude_pro?: boolean;
created_at?: string;
};
organization?: {
uuid?: string;
name?: string;
organization_type?: string;
billing_type?: string;
rate_limit_tier?: string;
has_extra_usage_enabled?: boolean;
subscription_status?: string;
subscription_created_at?: string;
};
}
export interface ClaudeQuotaWindow {
id: string;
label: string;
@@ -144,6 +166,7 @@ export interface ClaudeQuotaState {
status: 'idle' | 'loading' | 'success' | 'error';
windows: ClaudeQuotaWindow[];
extraUsage?: ClaudeExtraUsage | null;
planType?: string | null;
error?: string;
errorStatus?: number;
}
+2
View File
@@ -156,6 +156,8 @@ export const GEMINI_CLI_GROUP_LOOKUP = new Map(
export const GEMINI_CLI_IGNORED_MODEL_PREFIXES = ['gemini-2.0-flash'];
// Claude API configuration
export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
export const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
export const CLAUDE_REQUEST_HEADERS = {