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): implement manual quota reset functionality with confirmation and UI updates
This commit is contained in:
@@ -56,6 +56,7 @@ export function QuotaProgressBar({
|
||||
export interface QuotaRenderHelpers {
|
||||
styles: typeof styles;
|
||||
QuotaProgressBar: (props: QuotaProgressBarProps) => ReactElement;
|
||||
resetQuotaAction?: ReactNode;
|
||||
}
|
||||
|
||||
interface QuotaCardProps<TState extends QuotaStatusState> {
|
||||
@@ -68,6 +69,7 @@ interface QuotaCardProps<TState extends QuotaStatusState> {
|
||||
defaultType: string;
|
||||
canRefresh?: boolean;
|
||||
onRefresh?: () => void;
|
||||
resetQuotaAction?: ReactNode;
|
||||
renderQuotaItems: (quota: TState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode;
|
||||
}
|
||||
|
||||
@@ -81,6 +83,7 @@ export function QuotaCard<TState extends QuotaStatusState>({
|
||||
defaultType,
|
||||
canRefresh = false,
|
||||
onRefresh,
|
||||
resetQuotaAction,
|
||||
renderQuotaItems
|
||||
}: QuotaCardProps<TState>) {
|
||||
const { t } = useTranslation();
|
||||
@@ -146,7 +149,7 @@ export function QuotaCard<TState extends QuotaStatusState>({
|
||||
})}
|
||||
</div>
|
||||
) : quota ? (
|
||||
renderQuotaItems(quota, t, { styles, QuotaProgressBar })
|
||||
renderQuotaItems(quota, t, { styles, QuotaProgressBar, resetQuotaAction })
|
||||
) : (
|
||||
<div className={styles.quotaMessage}>{t(idleMessageKey)}</div>
|
||||
)}
|
||||
|
||||
@@ -107,6 +107,7 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
const { t } = useTranslation();
|
||||
const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
||||
const showNotification = useNotificationStore((state) => state.showNotification);
|
||||
const showConfirmation = useNotificationStore((state) => state.showConfirmation);
|
||||
const setQuota = useQuotaStore((state) => state[config.storeSetter]) as QuotaSetter<
|
||||
Record<string, TState>
|
||||
>;
|
||||
@@ -115,6 +116,7 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
const [columns, gridRef] = useGridColumns(380); // Min card width 380px matches SCSS
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('paged');
|
||||
const [showTooManyWarning, setShowTooManyWarning] = useState(false);
|
||||
const [resettingQuotaName, setResettingQuotaName] = useState<string | null>(null);
|
||||
|
||||
const filteredFiles = useMemo(() => files.filter((file) => config.filterFn(file)), [
|
||||
files,
|
||||
@@ -237,6 +239,52 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
[config, disabled, quota, setQuota, showNotification, t]
|
||||
);
|
||||
|
||||
const resetQuotaForFile = useCallback(
|
||||
(file: AuthFileItem) => {
|
||||
const resetQuota = config.resetQuota;
|
||||
if (!resetQuota) return;
|
||||
if (disabled || file.disabled) return;
|
||||
if (quota[file.name]?.status === 'loading') return;
|
||||
if (resettingQuotaName === file.name) return;
|
||||
|
||||
showConfirmation({
|
||||
title: t('codex_quota.reset_confirm_title'),
|
||||
message: t('codex_quota.reset_confirm_message', { name: file.name }),
|
||||
confirmText: t('codex_quota.reset_confirm_button'),
|
||||
variant: 'primary',
|
||||
onConfirm: async () => {
|
||||
setResettingQuotaName(file.name);
|
||||
try {
|
||||
const data = await resetQuota(file, t);
|
||||
setQuota((prev) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildSuccessState(data)
|
||||
}));
|
||||
showNotification(t('codex_quota.reset_success', { name: file.name }), 'success');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('common.unknown_error');
|
||||
showNotification(
|
||||
t('codex_quota.reset_failed', { name: file.name, message }),
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
setResettingQuotaName((current) => (current === file.name ? null : current));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[
|
||||
config,
|
||||
disabled,
|
||||
quota,
|
||||
resettingQuotaName,
|
||||
setQuota,
|
||||
showConfirmation,
|
||||
showNotification,
|
||||
t
|
||||
]
|
||||
);
|
||||
|
||||
const titleNode = (
|
||||
<div className={styles.titleWrapper}>
|
||||
<span>{t(`${config.i18nPrefix}.title`)}</span>
|
||||
@@ -307,21 +355,45 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
) : (
|
||||
<>
|
||||
<div ref={gridRef} className={config.gridClassName}>
|
||||
{pageItems.map((item) => (
|
||||
<QuotaCard
|
||||
key={item.name}
|
||||
item={item}
|
||||
quota={quota[item.name]}
|
||||
resolvedTheme={resolvedTheme}
|
||||
i18nPrefix={config.i18nPrefix}
|
||||
cardIdleMessageKey={config.cardIdleMessageKey}
|
||||
cardClassName={config.cardClassName}
|
||||
defaultType={config.type}
|
||||
canRefresh={!disabled && !item.disabled}
|
||||
onRefresh={() => void refreshQuotaForFile(item)}
|
||||
renderQuotaItems={config.renderQuotaItems}
|
||||
/>
|
||||
))}
|
||||
{pageItems.map((item) => {
|
||||
const itemQuota = quota[item.name];
|
||||
const isResettingQuota = resettingQuotaName === item.name;
|
||||
const canUseQuotaAction =
|
||||
!disabled && !item.disabled && itemQuota?.status !== 'loading';
|
||||
const resetQuotaAction = config.resetQuota ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={styles.quotaResetCreditButton}
|
||||
onClick={() => resetQuotaForFile(item)}
|
||||
disabled={!canUseQuotaAction || isResettingQuota}
|
||||
loading={isResettingQuota}
|
||||
title={t('codex_quota.reset_button')}
|
||||
aria-label={t('codex_quota.reset_button')}
|
||||
>
|
||||
{!isResettingQuota && <IconRefreshCw size={14} />}
|
||||
{t('codex_quota.reset_button')}
|
||||
</Button>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<QuotaCard
|
||||
key={item.name}
|
||||
item={item}
|
||||
quota={itemQuota}
|
||||
resolvedTheme={resolvedTheme}
|
||||
i18nPrefix={config.i18nPrefix}
|
||||
cardIdleMessageKey={config.cardIdleMessageKey}
|
||||
cardClassName={config.cardClassName}
|
||||
defaultType={config.type}
|
||||
canRefresh={canUseQuotaAction && !isResettingQuota}
|
||||
onRefresh={() => void refreshQuotaForFile(item)}
|
||||
resetQuotaAction={resetQuotaAction}
|
||||
renderQuotaItems={config.renderQuotaItems}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{filteredFiles.length > pageSize && effectiveViewMode === 'paged' && (
|
||||
<div className={styles.pagination}>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
CLAUDE_USAGE_URL,
|
||||
CLAUDE_REQUEST_HEADERS,
|
||||
CLAUDE_USAGE_WINDOW_KEYS,
|
||||
CODEX_RATE_LIMIT_RESET_CREDITS_CONSUME_URL,
|
||||
CODEX_USAGE_URL,
|
||||
CODEX_REQUEST_HEADERS,
|
||||
GEMINI_CLI_QUOTA_URL,
|
||||
@@ -128,6 +129,7 @@ export interface QuotaConfig<TState, TData> {
|
||||
cardIdleMessageKey?: string;
|
||||
filterFn: (file: AuthFileItem) => boolean;
|
||||
fetchQuota: (file: AuthFileItem, t: TFunction) => Promise<TData>;
|
||||
resetQuota?: (file: AuthFileItem, t: TFunction) => Promise<TData>;
|
||||
storeSelector: (state: QuotaStore) => Record<string, TState>;
|
||||
storeSetter: keyof QuotaStore;
|
||||
buildLoadingState: () => TState;
|
||||
@@ -429,6 +431,7 @@ const fetchCodexQuota = async (
|
||||
): Promise<{
|
||||
planType: string | null;
|
||||
subscriptionActiveUntil: string | number | null;
|
||||
rateLimitResetCreditsAvailableCount: number | null;
|
||||
windows: CodexQuotaWindow[];
|
||||
}> => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
@@ -465,8 +468,75 @@ const fetchCodexQuota = async (
|
||||
}
|
||||
|
||||
const planTypeFromUsage = normalizePlanType(payload.plan_type ?? payload.planType);
|
||||
const resetCredits = payload.rate_limit_reset_credits ?? payload.rateLimitResetCredits ?? null;
|
||||
const rateLimitResetCreditsAvailableCount = normalizeNumberValue(
|
||||
resetCredits?.available_count ?? resetCredits?.availableCount
|
||||
);
|
||||
const windows = buildCodexQuotaWindows(payload, t);
|
||||
return { planType: planTypeFromUsage ?? planTypeFromFile, subscriptionActiveUntil, windows };
|
||||
return {
|
||||
planType: planTypeFromUsage ?? planTypeFromFile,
|
||||
subscriptionActiveUntil,
|
||||
rateLimitResetCreditsAvailableCount,
|
||||
windows,
|
||||
};
|
||||
};
|
||||
|
||||
const createCodexRedeemRequestId = (): string => {
|
||||
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
||||
const value = Math.floor(Math.random() * 16);
|
||||
const segment = char === 'x' ? value : (value & 0x3) | 0x8;
|
||||
return segment.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
const consumeCodexRateLimitResetCredit = async (
|
||||
file: AuthFileItem,
|
||||
t: TFunction
|
||||
): Promise<void> => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndex = normalizeAuthIndex(rawAuthIndex);
|
||||
if (!authIndex) {
|
||||
throw new Error(t('codex_quota.missing_auth_index'));
|
||||
}
|
||||
|
||||
const accountId = resolveCodexChatgptAccountId(file);
|
||||
const requestHeader: Record<string, string> = {
|
||||
...CODEX_REQUEST_HEADERS,
|
||||
};
|
||||
if (accountId) {
|
||||
requestHeader['Chatgpt-Account-Id'] = accountId;
|
||||
}
|
||||
|
||||
const result = await apiCallApi.request({
|
||||
authIndex,
|
||||
method: 'POST',
|
||||
url: CODEX_RATE_LIMIT_RESET_CREDITS_CONSUME_URL,
|
||||
header: requestHeader,
|
||||
data: JSON.stringify({
|
||||
redeem_request_id: createCodexRedeemRequestId(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (result.statusCode < 200 || result.statusCode >= 300) {
|
||||
throw createStatusError(getApiCallErrorMessage(result), result.statusCode);
|
||||
}
|
||||
};
|
||||
|
||||
const resetCodexQuota = async (
|
||||
file: AuthFileItem,
|
||||
t: TFunction
|
||||
): Promise<{
|
||||
planType: string | null;
|
||||
subscriptionActiveUntil: string | number | null;
|
||||
rateLimitResetCreditsAvailableCount: number | null;
|
||||
windows: CodexQuotaWindow[];
|
||||
}> => {
|
||||
await consumeCodexRateLimitResetCredit(file, t);
|
||||
return fetchCodexQuota(file, t);
|
||||
};
|
||||
|
||||
const GEMINI_CLI_G1_CREDIT_TYPE = 'GOOGLE_ONE_AI';
|
||||
@@ -768,6 +838,8 @@ const renderCodexItems = (
|
||||
const windows = quota.windows ?? [];
|
||||
const planType = quota.planType ?? null;
|
||||
const subscriptionActiveUntil = quota.subscriptionActiveUntil ?? null;
|
||||
const rateLimitResetCreditsAvailableCount =
|
||||
quota.rateLimitResetCreditsAvailableCount ?? null;
|
||||
|
||||
const getPlanLabel = (pt?: string | null): string | null => {
|
||||
const normalized = normalizePlanType(pt);
|
||||
@@ -785,11 +857,24 @@ const renderCodexItems = (
|
||||
const planLabel = getPlanLabel(planType);
|
||||
const isPremiumPlan = PREMIUM_CODEX_PLAN_TYPES.has(normalizePlanType(planType) ?? '');
|
||||
const expiryLabel = subscriptionActiveUntil ? formatDateTimeValue(subscriptionActiveUntil) : '';
|
||||
const resetQuotaAction =
|
||||
rateLimitResetCreditsAvailableCount !== null && rateLimitResetCreditsAvailableCount > 0
|
||||
? helpers.resetQuotaAction
|
||||
: null;
|
||||
const nodes: ReactNode[] = [];
|
||||
|
||||
if (planLabel || expiryLabel) {
|
||||
if (planLabel || expiryLabel || rateLimitResetCreditsAvailableCount !== null) {
|
||||
const planValueClass = isPremiumPlan ? styleMap.premiumPlanValue : styleMap.codexPlanValue;
|
||||
const planNodes: ReactNode[] = [];
|
||||
const appendSeparator = (key: string) => {
|
||||
if (planNodes.length === 0) return;
|
||||
planNodes.push(
|
||||
h('span', {
|
||||
key,
|
||||
className: styleMap.codexPlanSeparator,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (planLabel) {
|
||||
planNodes.push(
|
||||
@@ -803,14 +888,7 @@ const renderCodexItems = (
|
||||
}
|
||||
|
||||
if (expiryLabel) {
|
||||
if (planNodes.length > 0) {
|
||||
planNodes.push(
|
||||
h('span', {
|
||||
key: 'subscription-expiry-separator',
|
||||
className: styleMap.codexPlanSeparator,
|
||||
})
|
||||
);
|
||||
}
|
||||
appendSeparator('subscription-expiry-separator');
|
||||
planNodes.push(
|
||||
h(
|
||||
'span',
|
||||
@@ -825,9 +903,35 @@ const renderCodexItems = (
|
||||
);
|
||||
}
|
||||
|
||||
if (rateLimitResetCreditsAvailableCount !== null) {
|
||||
appendSeparator('reset-credits-separator');
|
||||
planNodes.push(
|
||||
h(
|
||||
'span',
|
||||
{ key: 'reset-credits-label', className: styleMap.codexPlanLabel },
|
||||
t('codex_quota.reset_credits_label')
|
||||
),
|
||||
h(
|
||||
'span',
|
||||
{ key: 'reset-credits-value', className: styleMap.codexPlanValue },
|
||||
rateLimitResetCreditsAvailableCount.toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
nodes.push(h('div', { key: 'plan', className: styleMap.codexPlan }, ...planNodes));
|
||||
}
|
||||
|
||||
if (resetQuotaAction) {
|
||||
nodes.push(
|
||||
h(
|
||||
'div',
|
||||
{ key: 'reset-credits-action', className: styleMap.quotaInlineActions },
|
||||
resetQuotaAction
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (windows.length === 0) {
|
||||
nodes.push(
|
||||
h('div', { key: 'empty', className: styleMap.quotaMessage }, t('codex_quota.empty_windows'))
|
||||
@@ -1240,6 +1344,7 @@ export const CODEX_CONFIG: QuotaConfig<
|
||||
{
|
||||
planType: string | null;
|
||||
subscriptionActiveUntil: string | number | null;
|
||||
rateLimitResetCreditsAvailableCount: number | null;
|
||||
windows: CodexQuotaWindow[];
|
||||
}
|
||||
> = {
|
||||
@@ -1248,6 +1353,7 @@ export const CODEX_CONFIG: QuotaConfig<
|
||||
cardIdleMessageKey: 'quota_management.card_idle_hint',
|
||||
filterFn: (file) => isCodexFile(file) && !isDisabledAuthFile(file),
|
||||
fetchQuota: fetchCodexQuota,
|
||||
resetQuota: resetCodexQuota,
|
||||
storeSelector: (state) => state.codexQuota,
|
||||
storeSetter: 'setCodexQuota',
|
||||
buildLoadingState: () => ({ status: 'loading', windows: [] }),
|
||||
@@ -1256,6 +1362,7 @@ export const CODEX_CONFIG: QuotaConfig<
|
||||
windows: data.windows,
|
||||
planType: data.planType,
|
||||
subscriptionActiveUntil: data.subscriptionActiveUntil,
|
||||
rateLimitResetCreditsAvailableCount: data.rateLimitResetCreditsAvailableCount,
|
||||
}),
|
||||
buildErrorState: (message, status) => ({
|
||||
status: 'error',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, type ReactNode } from 'react';
|
||||
import { useCallback, useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import {
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
resolveQuotaErrorMessage,
|
||||
type QuotaProviderType,
|
||||
} from '@/features/authFiles/constants';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { IconRefreshCw } from '@/components/ui/icons';
|
||||
import { QuotaProgressBar } from '@/features/authFiles/components/QuotaProgressBar';
|
||||
import styles from '@/pages/AuthFilesPage.module.scss';
|
||||
|
||||
@@ -41,6 +43,8 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
const { file, quotaType, disableControls } = props;
|
||||
const { t } = useTranslation();
|
||||
const showNotification = useNotificationStore((state) => state.showNotification);
|
||||
const showConfirmation = useNotificationStore((state) => state.showConfirmation);
|
||||
const [resettingQuota, setResettingQuota] = useState(false);
|
||||
|
||||
const quota = useQuotaStore((state) => {
|
||||
if (quotaType === 'antigravity') return state.antigravityQuota[file.name] as QuotaState;
|
||||
@@ -100,13 +104,79 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
}
|
||||
}, [disableControls, file, quota?.status, quotaType, showNotification, t, updateQuotaState]);
|
||||
|
||||
const resetQuotaForFile = useCallback(() => {
|
||||
if (disableControls) return;
|
||||
if (isRuntimeOnlyAuthFile(file)) return;
|
||||
if (file.disabled) return;
|
||||
if (quota?.status === 'loading') return;
|
||||
if (resettingQuota) return;
|
||||
|
||||
const config = getQuotaConfig(quotaType) as unknown as {
|
||||
resetQuota?: (file: AuthFileItem, t: TFunction) => Promise<unknown>;
|
||||
buildSuccessState: (data: unknown) => unknown;
|
||||
};
|
||||
const resetQuota = config.resetQuota;
|
||||
if (!resetQuota) return;
|
||||
|
||||
showConfirmation({
|
||||
title: t('codex_quota.reset_confirm_title'),
|
||||
message: t('codex_quota.reset_confirm_message', { name: file.name }),
|
||||
confirmText: t('codex_quota.reset_confirm_button'),
|
||||
variant: 'primary',
|
||||
onConfirm: async () => {
|
||||
setResettingQuota(true);
|
||||
try {
|
||||
const data = await resetQuota(file, t);
|
||||
updateQuotaState((prev: Record<string, unknown>) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildSuccessState(data),
|
||||
}));
|
||||
showNotification(t('codex_quota.reset_success', { name: file.name }), 'success');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('common.unknown_error');
|
||||
showNotification(t('codex_quota.reset_failed', { name: file.name, message }), 'error');
|
||||
} finally {
|
||||
setResettingQuota(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [
|
||||
disableControls,
|
||||
file,
|
||||
quota?.status,
|
||||
quotaType,
|
||||
resettingQuota,
|
||||
showConfirmation,
|
||||
showNotification,
|
||||
t,
|
||||
updateQuotaState,
|
||||
]);
|
||||
|
||||
const config = getQuotaConfig(quotaType) as unknown as {
|
||||
i18nPrefix: string;
|
||||
resetQuota?: (file: AuthFileItem, t: TFunction) => Promise<unknown>;
|
||||
renderQuotaItems: (quota: unknown, t: TFunction, helpers: unknown) => unknown;
|
||||
};
|
||||
|
||||
const quotaStatus = quota?.status ?? 'idle';
|
||||
const canRefreshQuota = !disableControls && !file.disabled;
|
||||
const canRefreshQuota = !disableControls && !file.disabled && !resettingQuota;
|
||||
const canResetQuota = canRefreshQuota && quotaStatus !== 'loading';
|
||||
const resetQuotaAction = config.resetQuota ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={styles.quotaResetCreditButton}
|
||||
onClick={() => resetQuotaForFile()}
|
||||
disabled={!canResetQuota}
|
||||
loading={resettingQuota}
|
||||
title={t('codex_quota.reset_button')}
|
||||
aria-label={t('codex_quota.reset_button')}
|
||||
>
|
||||
{!resettingQuota && <IconRefreshCw size={14} />}
|
||||
{t('codex_quota.reset_button')}
|
||||
</Button>
|
||||
) : undefined;
|
||||
const quotaErrorMessage = resolveQuotaErrorMessage(
|
||||
t,
|
||||
quota?.errorStatus,
|
||||
@@ -133,7 +203,11 @@ export function AuthFileQuotaSection(props: AuthFileQuotaSectionProps) {
|
||||
})}
|
||||
</div>
|
||||
) : quota ? (
|
||||
(config.renderQuotaItems(quota, t, { styles, QuotaProgressBar }) as ReactNode)
|
||||
(config.renderQuotaItems(quota, t, {
|
||||
styles,
|
||||
QuotaProgressBar,
|
||||
resetQuotaAction,
|
||||
}) as ReactNode)
|
||||
) : (
|
||||
<div className={styles.quotaMessage}>{t(`${config.i18nPrefix}.idle`)}</div>
|
||||
)}
|
||||
|
||||
@@ -437,6 +437,13 @@
|
||||
"additional_secondary_window": "{{name}} weekly limit",
|
||||
"plan_label": "Plan",
|
||||
"expires_label": "Expires",
|
||||
"reset_credits_label": "Manual resets",
|
||||
"reset_button": "Reset quota",
|
||||
"reset_confirm_title": "Reset Codex quota",
|
||||
"reset_confirm_message": "This will consume 1 manual reset to reset the Codex quota for \"{{name}}\". Continue?",
|
||||
"reset_confirm_button": "Reset quota",
|
||||
"reset_success": "Reset Codex quota for \"{{name}}\"",
|
||||
"reset_failed": "Failed to reset Codex quota for \"{{name}}\": {{message}}",
|
||||
"plan_plus": "Plus",
|
||||
"plan_team": "Team",
|
||||
"plan_free": "Free",
|
||||
|
||||
@@ -437,6 +437,13 @@
|
||||
"additional_secondary_window": "{{name}} 周限额",
|
||||
"plan_label": "套餐",
|
||||
"expires_label": "到期时间",
|
||||
"reset_credits_label": "主动重置次数",
|
||||
"reset_button": "重置额度",
|
||||
"reset_confirm_title": "重置 Codex 额度",
|
||||
"reset_confirm_message": "将消耗 1 次主动重置次数来重置 \"{{name}}\" 的 Codex 额度。是否继续?",
|
||||
"reset_confirm_button": "确认重置",
|
||||
"reset_success": "已重置 \"{{name}}\" 的 Codex 额度",
|
||||
"reset_failed": "重置 \"{{name}}\" 的 Codex 额度失败:{{message}}",
|
||||
"plan_plus": "Plus",
|
||||
"plan_team": "Team",
|
||||
"plan_free": "Free",
|
||||
|
||||
@@ -437,6 +437,13 @@
|
||||
"additional_secondary_window": "{{name}} 週限額",
|
||||
"plan_label": "方案",
|
||||
"expires_label": "到期時間",
|
||||
"reset_credits_label": "主動重置次數",
|
||||
"reset_button": "重置配額",
|
||||
"reset_confirm_title": "重置 Codex 配額",
|
||||
"reset_confirm_message": "將消耗 1 次主動重置次數來重置「{{name}}」的 Codex 配額。是否繼續?",
|
||||
"reset_confirm_button": "確認重置",
|
||||
"reset_success": "已重置「{{name}}」的 Codex 配額",
|
||||
"reset_failed": "重置「{{name}}」的 Codex 配額失敗:{{message}}",
|
||||
"plan_plus": "Plus",
|
||||
"plan_team": "Team",
|
||||
"plan_free": "Free",
|
||||
|
||||
@@ -640,6 +640,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.quotaInlineActions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.quotaResetCreditButton:global(.btn.btn-sm) {
|
||||
border-radius: 999px;
|
||||
padding-inline: 12px;
|
||||
|
||||
> span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.quotaError {
|
||||
font-size: 12px;
|
||||
color: var(--danger-color);
|
||||
@@ -676,6 +694,12 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.codexPlanSeparator {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.premiumPlanValue {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -392,12 +392,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.quotaInlineActions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.quotaCardActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.quotaResetCreditButton:global(.btn.btn-sm),
|
||||
.quotaRefreshButton:global(.btn.btn-sm) {
|
||||
border-radius: 999px;
|
||||
padding-inline: 12px;
|
||||
|
||||
@@ -119,6 +119,11 @@ export interface CodexAdditionalRateLimit {
|
||||
rateLimit?: CodexRateLimitInfo | null;
|
||||
}
|
||||
|
||||
export interface CodexRateLimitResetCredits {
|
||||
available_count?: number | string;
|
||||
availableCount?: number | string;
|
||||
}
|
||||
|
||||
export interface CodexUsagePayload {
|
||||
plan_type?: string;
|
||||
planType?: string;
|
||||
@@ -128,6 +133,8 @@ export interface CodexUsagePayload {
|
||||
codeReviewRateLimit?: CodexRateLimitInfo | null;
|
||||
additional_rate_limits?: CodexAdditionalRateLimit[] | null;
|
||||
additionalRateLimits?: CodexAdditionalRateLimit[] | null;
|
||||
rate_limit_reset_credits?: CodexRateLimitResetCredits | null;
|
||||
rateLimitResetCredits?: CodexRateLimitResetCredits | null;
|
||||
}
|
||||
|
||||
// Claude API payload types
|
||||
@@ -243,6 +250,7 @@ export interface CodexQuotaState {
|
||||
windows: CodexQuotaWindow[];
|
||||
planType?: string | null;
|
||||
subscriptionActiveUntil?: string | number | null;
|
||||
rateLimitResetCreditsAvailableCount?: number | null;
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
}
|
||||
|
||||
@@ -193,6 +193,8 @@ export const CLAUDE_USAGE_WINDOW_KEYS = [
|
||||
|
||||
// Codex API configuration
|
||||
export const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
||||
export const CODEX_RATE_LIMIT_RESET_CREDITS_CONSUME_URL =
|
||||
'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume';
|
||||
|
||||
export const CODEX_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
|
||||
Reference in New Issue
Block a user