diff --git a/src/components/quota/quotaConfigs.ts b/src/components/quota/quotaConfigs.ts index 252b935..026877d 100644 --- a/src/components/quota/quotaConfigs.ts +++ b/src/components/quota/quotaConfigs.ts @@ -213,11 +213,14 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex const rateLimit = payload.rate_limit ?? payload.rateLimit ?? undefined; const codeReviewLimit = payload.code_review_rate_limit ?? payload.codeReviewRateLimit ?? undefined; + const additionalRateLimits = payload.additional_rate_limits ?? payload.additionalRateLimits ?? []; const windows: CodexQuotaWindow[] = []; const addWindow = ( id: string, - labelKey: string, + label: string, + labelKey: string | undefined, + labelParams: Record | undefined, window?: CodexUsageWindow | null, limitReached?: boolean, allowed?: boolean @@ -229,8 +232,9 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex const usedPercent = usedPercentRaw ?? (isLimitReached && resetLabel !== '-' ? 100 : null); windows.push({ id, - label: t(labelKey), + label, labelKey, + labelParams, usedPercent, resetLabel, }); @@ -245,12 +249,13 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex const rawAllowed = rateLimit?.allowed; const pickClassifiedWindows = ( - limitInfo?: CodexRateLimitInfo | null + limitInfo?: CodexRateLimitInfo | null, + options?: { allowOrderFallback?: boolean } ): { fiveHourWindow: CodexUsageWindow | null; weeklyWindow: CodexUsageWindow | null } => { - const rawWindows = [ - limitInfo?.primary_window ?? limitInfo?.primaryWindow ?? null, - limitInfo?.secondary_window ?? limitInfo?.secondaryWindow ?? null, - ]; + const allowOrderFallback = options?.allowOrderFallback ?? true; + const primaryWindow = limitInfo?.primary_window ?? limitInfo?.primaryWindow ?? null; + const secondaryWindow = limitInfo?.secondary_window ?? limitInfo?.secondaryWindow ?? null; + const rawWindows = [primaryWindow, secondaryWindow]; let fiveHourWindow: CodexUsageWindow | null = null; let weeklyWindow: CodexUsageWindow | null = null; @@ -265,20 +270,34 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex } } + // For legacy payloads without window duration, fallback to primary/secondary ordering. + if (allowOrderFallback) { + if (!fiveHourWindow) { + fiveHourWindow = primaryWindow && primaryWindow !== weeklyWindow ? primaryWindow : null; + } + if (!weeklyWindow) { + weeklyWindow = secondaryWindow && secondaryWindow !== fiveHourWindow ? secondaryWindow : null; + } + } + return { fiveHourWindow, weeklyWindow }; }; const rateWindows = pickClassifiedWindows(rateLimit); addWindow( WINDOW_META.codeFiveHour.id, + t(WINDOW_META.codeFiveHour.labelKey), WINDOW_META.codeFiveHour.labelKey, + undefined, rateWindows.fiveHourWindow, rawLimitReached, rawAllowed ); addWindow( WINDOW_META.codeWeekly.id, + t(WINDOW_META.codeWeekly.labelKey), WINDOW_META.codeWeekly.labelKey, + undefined, rateWindows.weeklyWindow, rawLimitReached, rawAllowed @@ -289,19 +308,67 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex const codeReviewAllowed = codeReviewLimit?.allowed; addWindow( WINDOW_META.codeReviewFiveHour.id, + t(WINDOW_META.codeReviewFiveHour.labelKey), WINDOW_META.codeReviewFiveHour.labelKey, + undefined, codeReviewWindows.fiveHourWindow, codeReviewLimitReached, codeReviewAllowed ); addWindow( WINDOW_META.codeReviewWeekly.id, + t(WINDOW_META.codeReviewWeekly.labelKey), WINDOW_META.codeReviewWeekly.labelKey, + undefined, codeReviewWindows.weeklyWindow, codeReviewLimitReached, codeReviewAllowed ); + const normalizeWindowId = (raw: string) => + raw + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + + if (Array.isArray(additionalRateLimits)) { + additionalRateLimits.forEach((limitItem, index) => { + const rateInfo = limitItem?.rate_limit ?? limitItem?.rateLimit ?? null; + if (!rateInfo) return; + + const limitName = + normalizeStringValue(limitItem?.limit_name ?? limitItem?.limitName) ?? + normalizeStringValue(limitItem?.metered_feature ?? limitItem?.meteredFeature) ?? + `additional-${index + 1}`; + + const idPrefix = normalizeWindowId(limitName) || `additional-${index + 1}`; + const additionalPrimaryWindow = rateInfo.primary_window ?? rateInfo.primaryWindow ?? null; + const additionalSecondaryWindow = rateInfo.secondary_window ?? rateInfo.secondaryWindow ?? null; + const additionalLimitReached = rateInfo.limit_reached ?? rateInfo.limitReached; + const additionalAllowed = rateInfo.allowed; + + addWindow( + `${idPrefix}-five-hour-${index}`, + t('codex_quota.additional_primary_window', { name: limitName }), + 'codex_quota.additional_primary_window', + { name: limitName }, + additionalPrimaryWindow, + additionalLimitReached, + additionalAllowed + ); + addWindow( + `${idPrefix}-weekly-${index}`, + t('codex_quota.additional_secondary_window', { name: limitName }), + 'codex_quota.additional_secondary_window', + { name: limitName }, + additionalSecondaryWindow, + additionalLimitReached, + additionalAllowed + ); + }); + } + return windows; }; @@ -493,7 +560,9 @@ const renderCodexItems = ( const clampedUsed = used === null ? null : Math.max(0, Math.min(100, used)); const remaining = clampedUsed === null ? null : Math.max(0, Math.min(100, 100 - clampedUsed)); const percentLabel = remaining === null ? '--' : `${Math.round(remaining)}%`; - const windowLabel = window.labelKey ? t(window.labelKey) : window.label; + const windowLabel = window.labelKey + ? t(window.labelKey, window.labelParams as Record) + : window.label; return h( 'div', diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 25372be..e80722f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -490,6 +490,8 @@ "secondary_window": "Weekly limit", "code_review_primary_window": "Code review 5-hour limit", "code_review_secondary_window": "Code review weekly limit", + "additional_primary_window": "{{name}} 5-hour limit", + "additional_secondary_window": "{{name}} weekly limit", "plan_label": "Plan", "plan_plus": "Plus", "plan_team": "Team", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 3de775f..0be9e95 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -493,6 +493,8 @@ "secondary_window": "Недельный лимит", "code_review_primary_window": "Лимит code review на 5 часов", "code_review_secondary_window": "Недельный лимит code review", + "additional_primary_window": "{{name}}: лимит на 5 часов", + "additional_secondary_window": "{{name}}: недельный лимит", "plan_label": "Тариф", "plan_plus": "Plus", "plan_team": "Team", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 208c6cb..d176a4a 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -490,6 +490,8 @@ "secondary_window": "周限额", "code_review_primary_window": "代码审查 5 小时限额", "code_review_secondary_window": "代码审查周限额", + "additional_primary_window": "{{name}} 5 小时限额", + "additional_secondary_window": "{{name}} 周限额", "plan_label": "套餐", "plan_plus": "Plus", "plan_team": "Team", diff --git a/src/types/quota.ts b/src/types/quota.ts index baf2d0b..15d58b8 100644 --- a/src/types/quota.ts +++ b/src/types/quota.ts @@ -88,6 +88,15 @@ export interface CodexRateLimitInfo { secondaryWindow?: CodexUsageWindow | null; } +export interface CodexAdditionalRateLimit { + limit_name?: string; + limitName?: string; + metered_feature?: string; + meteredFeature?: string; + rate_limit?: CodexRateLimitInfo | null; + rateLimit?: CodexRateLimitInfo | null; +} + export interface CodexUsagePayload { plan_type?: string; planType?: string; @@ -95,6 +104,8 @@ export interface CodexUsagePayload { rateLimit?: CodexRateLimitInfo | null; code_review_rate_limit?: CodexRateLimitInfo | null; codeReviewRateLimit?: CodexRateLimitInfo | null; + additional_rate_limits?: CodexAdditionalRateLimit[] | null; + additionalRateLimits?: CodexAdditionalRateLimit[] | null; } // Claude API payload types @@ -174,6 +185,7 @@ export interface CodexQuotaWindow { id: string; label: string; labelKey?: string; + labelParams?: Record; usedPercent: number | null; resetLabel: string; }