From ccf90f88dd96ca3ade0e7e4c0f9926c2822cb0aa Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 7 Mar 2026 15:48:03 +0800 Subject: [PATCH] feat(quota): restore Claude plan detection via profile API --- src/components/quota/quotaConfigs.ts | 89 +++++++++++++++++++++++++--- src/i18n/locales/en.json | 8 ++- src/i18n/locales/ru.json | 8 ++- src/i18n/locales/zh-CN.json | 8 ++- src/types/quota.ts | 23 +++++++ src/utils/quota/constants.ts | 2 + 6 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/components/quota/quotaConfigs.ts b/src/components/quota/quotaConfigs.ts index cabdcbb..27b852b 100644 --- a/src/components/quota/quotaConfigs.ts +++ b/src/components/quota/quotaConfigs.ts @@ -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 = { + 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', diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 86dac52..6227f44 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 9c87d9c..0d56827 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -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", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index f3478f2..aecdbbd 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -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 额度", diff --git a/src/types/quota.ts b/src/types/quota.ts index ebabf91..1d010af 100644 --- a/src/types/quota.ts +++ b/src/types/quota.ts @@ -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; } diff --git a/src/utils/quota/constants.ts b/src/utils/quota/constants.ts index 4315efa..a9e17ce 100644 --- a/src/utils/quota/constants.ts +++ b/src/utils/quota/constants.ts @@ -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 = {