mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(quota): add subscription expiry handling and improve UI elements
This commit is contained in:
@@ -64,6 +64,7 @@ import {
|
||||
parseXaiBillingPayload,
|
||||
resolveCodexChatgptAccountId,
|
||||
resolveCodexPlanType,
|
||||
resolveCodexSubscriptionActiveUntil,
|
||||
resolveGeminiCliProjectId,
|
||||
formatCodexResetLabel,
|
||||
formatQuotaResetTime,
|
||||
@@ -83,6 +84,7 @@ import {
|
||||
isXaiFile,
|
||||
} from '@/utils/quota';
|
||||
import { normalizeAuthIndex } from '@/utils/authIndex';
|
||||
import { formatDateTimeValue } from '@/utils/format';
|
||||
import type { QuotaRenderHelpers } from './QuotaCard';
|
||||
import styles from '@/pages/QuotaPage.module.scss';
|
||||
|
||||
@@ -424,7 +426,11 @@ const buildCodexQuotaWindows = (payload: CodexUsagePayload, t: TFunction): Codex
|
||||
const fetchCodexQuota = async (
|
||||
file: AuthFileItem,
|
||||
t: TFunction
|
||||
): Promise<{ planType: string | null; windows: CodexQuotaWindow[] }> => {
|
||||
): Promise<{
|
||||
planType: string | null;
|
||||
subscriptionActiveUntil: string | number | null;
|
||||
windows: CodexQuotaWindow[];
|
||||
}> => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndex = normalizeAuthIndex(rawAuthIndex);
|
||||
if (!authIndex) {
|
||||
@@ -432,6 +438,7 @@ const fetchCodexQuota = async (
|
||||
}
|
||||
|
||||
const planTypeFromFile = resolveCodexPlanType(file);
|
||||
const subscriptionActiveUntil = resolveCodexSubscriptionActiveUntil(file);
|
||||
const accountId = resolveCodexChatgptAccountId(file);
|
||||
|
||||
const requestHeader: Record<string, string> = {
|
||||
@@ -459,7 +466,7 @@ const fetchCodexQuota = async (
|
||||
|
||||
const planTypeFromUsage = normalizePlanType(payload.plan_type ?? payload.planType);
|
||||
const windows = buildCodexQuotaWindows(payload, t);
|
||||
return { planType: planTypeFromUsage ?? planTypeFromFile, windows };
|
||||
return { planType: planTypeFromUsage ?? planTypeFromFile, subscriptionActiveUntil, windows };
|
||||
};
|
||||
|
||||
const GEMINI_CLI_G1_CREDIT_TYPE = 'GOOGLE_ONE_AI';
|
||||
@@ -760,6 +767,7 @@ const renderCodexItems = (
|
||||
const { createElement: h, Fragment } = React;
|
||||
const windows = quota.windows ?? [];
|
||||
const planType = quota.planType ?? null;
|
||||
const subscriptionActiveUntil = quota.subscriptionActiveUntil ?? null;
|
||||
|
||||
const getPlanLabel = (pt?: string | null): string | null => {
|
||||
const normalized = normalizePlanType(pt);
|
||||
@@ -776,18 +784,48 @@ const renderCodexItems = (
|
||||
|
||||
const planLabel = getPlanLabel(planType);
|
||||
const isPremiumPlan = PREMIUM_CODEX_PLAN_TYPES.has(normalizePlanType(planType) ?? '');
|
||||
const expiryLabel = subscriptionActiveUntil ? formatDateTimeValue(subscriptionActiveUntil) : '';
|
||||
const nodes: ReactNode[] = [];
|
||||
|
||||
if (planLabel) {
|
||||
const valueClass = isPremiumPlan ? styleMap.premiumPlanValue : styleMap.codexPlanValue;
|
||||
nodes.push(
|
||||
h(
|
||||
'div',
|
||||
{ key: 'plan', className: styleMap.codexPlan },
|
||||
h('span', { className: styleMap.codexPlanLabel }, t('codex_quota.plan_label')),
|
||||
h('span', { className: valueClass }, planLabel)
|
||||
)
|
||||
);
|
||||
if (planLabel || expiryLabel) {
|
||||
const planValueClass = isPremiumPlan ? styleMap.premiumPlanValue : styleMap.codexPlanValue;
|
||||
const planNodes: ReactNode[] = [];
|
||||
|
||||
if (planLabel) {
|
||||
planNodes.push(
|
||||
h(
|
||||
'span',
|
||||
{ key: 'plan-label', className: styleMap.codexPlanLabel },
|
||||
t('codex_quota.plan_label')
|
||||
),
|
||||
h('span', { key: 'plan-value', className: planValueClass }, planLabel)
|
||||
);
|
||||
}
|
||||
|
||||
if (expiryLabel) {
|
||||
if (planNodes.length > 0) {
|
||||
planNodes.push(
|
||||
h('span', {
|
||||
key: 'subscription-expiry-separator',
|
||||
className: styleMap.codexPlanSeparator,
|
||||
})
|
||||
);
|
||||
}
|
||||
planNodes.push(
|
||||
h(
|
||||
'span',
|
||||
{ key: 'subscription-expiry-label', className: styleMap.codexPlanLabel },
|
||||
t('codex_quota.expires_label')
|
||||
),
|
||||
h(
|
||||
'span',
|
||||
{ key: 'subscription-expiry-value', className: styleMap.codexPlanValue },
|
||||
expiryLabel
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
nodes.push(h('div', { key: 'plan', className: styleMap.codexPlan }, ...planNodes));
|
||||
}
|
||||
|
||||
if (windows.length === 0) {
|
||||
@@ -1199,7 +1237,11 @@ export const ANTIGRAVITY_CONFIG: QuotaConfig<AntigravityQuotaState, AntigravityQ
|
||||
|
||||
export const CODEX_CONFIG: QuotaConfig<
|
||||
CodexQuotaState,
|
||||
{ planType: string | null; windows: CodexQuotaWindow[] }
|
||||
{
|
||||
planType: string | null;
|
||||
subscriptionActiveUntil: string | number | null;
|
||||
windows: CodexQuotaWindow[];
|
||||
}
|
||||
> = {
|
||||
type: 'codex',
|
||||
i18nPrefix: 'codex_quota',
|
||||
@@ -1213,6 +1255,7 @@ export const CODEX_CONFIG: QuotaConfig<
|
||||
status: 'success',
|
||||
windows: data.windows,
|
||||
planType: data.planType,
|
||||
subscriptionActiveUntil: data.subscriptionActiveUntil,
|
||||
}),
|
||||
buildErrorState: (message, status) => ({
|
||||
status: 'error',
|
||||
|
||||
@@ -436,6 +436,7 @@
|
||||
"additional_primary_window": "{{name}} 5-hour limit",
|
||||
"additional_secondary_window": "{{name}} weekly limit",
|
||||
"plan_label": "Plan",
|
||||
"expires_label": "Expires",
|
||||
"plan_plus": "Plus",
|
||||
"plan_team": "Team",
|
||||
"plan_free": "Free",
|
||||
|
||||
@@ -430,6 +430,7 @@
|
||||
"additional_primary_window": "{{name}}: лимит на 5 часов",
|
||||
"additional_secondary_window": "{{name}}: недельный лимит",
|
||||
"plan_label": "Тариф",
|
||||
"expires_label": "Истекает",
|
||||
"plan_plus": "Plus",
|
||||
"plan_team": "Team",
|
||||
"plan_free": "Free",
|
||||
|
||||
@@ -436,6 +436,7 @@
|
||||
"additional_primary_window": "{{name}} 5 小时限额",
|
||||
"additional_secondary_window": "{{name}} 周限额",
|
||||
"plan_label": "套餐",
|
||||
"expires_label": "到期时间",
|
||||
"plan_plus": "Plus",
|
||||
"plan_team": "Team",
|
||||
"plan_free": "Free",
|
||||
|
||||
@@ -436,6 +436,7 @@
|
||||
"additional_primary_window": "{{name}} 5 小時限額",
|
||||
"additional_secondary_window": "{{name}} 週限額",
|
||||
"plan_label": "方案",
|
||||
"expires_label": "到期時間",
|
||||
"plan_plus": "Plus",
|
||||
"plan_team": "Team",
|
||||
"plan_free": "Free",
|
||||
|
||||
@@ -434,6 +434,7 @@
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.codexPlanLabel {
|
||||
@@ -446,6 +447,12 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.codexPlanSeparator {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.premiumPlanValue {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -242,6 +242,7 @@ export interface CodexQuotaState {
|
||||
status: 'idle' | 'loading' | 'success' | 'error';
|
||||
windows: CodexQuotaWindow[];
|
||||
planType?: string | null;
|
||||
subscriptionActiveUntil?: string | number | null;
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,24 @@
|
||||
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import {
|
||||
normalizeNumberValue,
|
||||
normalizeStringValue,
|
||||
normalizePlanType,
|
||||
parseIdTokenPayload
|
||||
parseIdTokenPayload,
|
||||
} from './parsers';
|
||||
|
||||
const toRecord = (value: unknown): Record<string, unknown> | null => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
return value as Record<string, unknown>;
|
||||
};
|
||||
|
||||
const resolveCodexAuthInfo = (value: unknown): Record<string, unknown> | null => {
|
||||
const payload = parseIdTokenPayload(value);
|
||||
if (!payload) return null;
|
||||
const nested = toRecord(payload['https://api.openai.com/auth']);
|
||||
return nested ?? payload;
|
||||
};
|
||||
|
||||
export function extractCodexChatgptAccountId(value: unknown): string | null {
|
||||
const payload = parseIdTokenPayload(value);
|
||||
if (!payload) return null;
|
||||
@@ -67,7 +80,7 @@ export function resolveCodexPlanType(file: AuthFileItem): string | null {
|
||||
metadataIdToken?.planType,
|
||||
attributes?.plan_type,
|
||||
attributes?.planType,
|
||||
attributes?.id_token
|
||||
attributes?.id_token,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
@@ -78,6 +91,61 @@ export function resolveCodexPlanType(file: AuthFileItem): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizeDateLikeValue = (value: unknown): string | number | null => {
|
||||
const numberValue = normalizeNumberValue(value);
|
||||
if (numberValue === 0) return null;
|
||||
if (numberValue !== null) return numberValue;
|
||||
|
||||
const stringValue = normalizeStringValue(value);
|
||||
if (!stringValue || stringValue === '0') return null;
|
||||
return stringValue;
|
||||
};
|
||||
|
||||
export function resolveCodexSubscriptionActiveUntil(file: AuthFileItem): string | number | null {
|
||||
const metadata = toRecord(file.metadata);
|
||||
const attributes = toRecord(file.attributes);
|
||||
const idToken = resolveCodexAuthInfo(file.id_token);
|
||||
const metadataIdToken = resolveCodexAuthInfo(metadata?.id_token);
|
||||
const attributesIdToken = resolveCodexAuthInfo(attributes?.id_token);
|
||||
const subscription = toRecord(file.subscription);
|
||||
const metadataSubscription = toRecord(metadata?.subscription);
|
||||
const attributesSubscription = toRecord(attributes?.subscription);
|
||||
|
||||
const candidates = [
|
||||
file.chatgpt_subscription_active_until,
|
||||
file.chatgptSubscriptionActiveUntil,
|
||||
file.subscription_active_until,
|
||||
file.subscriptionActiveUntil,
|
||||
subscription?.active_until,
|
||||
subscription?.activeUntil,
|
||||
idToken?.chatgpt_subscription_active_until,
|
||||
idToken?.chatgptSubscriptionActiveUntil,
|
||||
metadata?.chatgpt_subscription_active_until,
|
||||
metadata?.chatgptSubscriptionActiveUntil,
|
||||
metadata?.subscription_active_until,
|
||||
metadata?.subscriptionActiveUntil,
|
||||
metadataSubscription?.active_until,
|
||||
metadataSubscription?.activeUntil,
|
||||
metadataIdToken?.chatgpt_subscription_active_until,
|
||||
metadataIdToken?.chatgptSubscriptionActiveUntil,
|
||||
attributes?.chatgpt_subscription_active_until,
|
||||
attributes?.chatgptSubscriptionActiveUntil,
|
||||
attributes?.subscription_active_until,
|
||||
attributes?.subscriptionActiveUntil,
|
||||
attributesSubscription?.active_until,
|
||||
attributesSubscription?.activeUntil,
|
||||
attributesIdToken?.chatgpt_subscription_active_until,
|
||||
attributesIdToken?.chatgptSubscriptionActiveUntil,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const value = normalizeDateLikeValue(candidate);
|
||||
if (value !== null) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractGeminiCliProjectId(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const matches = Array.from(value.matchAll(/\(([^()]+)\)/g));
|
||||
@@ -96,12 +164,7 @@ export function resolveGeminiCliProjectId(file: AuthFileItem): string | null {
|
||||
? (file.attributes as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const candidates = [
|
||||
file.account,
|
||||
file['account'],
|
||||
metadata?.account,
|
||||
attributes?.account
|
||||
];
|
||||
const candidates = [file.account, file['account'], metadata?.account, attributes?.account];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const projectId = extractGeminiCliProjectId(candidate);
|
||||
|
||||
Reference in New Issue
Block a user