mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 18:50:49 +08:00
Merge pull request #93 from teidesu/claude-quota
feat(ui): added claude quota display
This commit is contained in:
@@ -18,6 +18,9 @@ import type {
|
|||||||
GeminiCliParsedBucket,
|
GeminiCliParsedBucket,
|
||||||
GeminiCliQuotaBucketState,
|
GeminiCliQuotaBucketState,
|
||||||
GeminiCliQuotaState,
|
GeminiCliQuotaState,
|
||||||
|
ClaudeQuotaState,
|
||||||
|
ClaudeProfileResponse,
|
||||||
|
ClaudeUsageResponse,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
|
import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
@@ -50,13 +53,18 @@ import {
|
|||||||
isDisabledAuthFile,
|
isDisabledAuthFile,
|
||||||
isGeminiCliFile,
|
isGeminiCliFile,
|
||||||
isRuntimeOnlyAuthFile,
|
isRuntimeOnlyAuthFile,
|
||||||
|
isClaudeFile,
|
||||||
|
CLAUDE_REQUEST_HEADERS,
|
||||||
|
CLAUDE_PROFILE_URL,
|
||||||
|
CLAUDE_USAGE_URL,
|
||||||
|
formatUnixSeconds,
|
||||||
} from '@/utils/quota';
|
} from '@/utils/quota';
|
||||||
import type { QuotaRenderHelpers } from './QuotaCard';
|
import type { QuotaRenderHelpers } from './QuotaCard';
|
||||||
import styles from '@/pages/QuotaPage.module.scss';
|
import styles from '@/pages/QuotaPage.module.scss';
|
||||||
|
|
||||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||||
|
|
||||||
type QuotaType = 'antigravity' | 'codex' | 'gemini-cli';
|
type QuotaType = 'antigravity' | 'codex' | 'gemini-cli' | 'claude';
|
||||||
|
|
||||||
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
||||||
|
|
||||||
@@ -64,9 +72,11 @@ export interface QuotaStore {
|
|||||||
antigravityQuota: Record<string, AntigravityQuotaState>;
|
antigravityQuota: Record<string, AntigravityQuotaState>;
|
||||||
codexQuota: Record<string, CodexQuotaState>;
|
codexQuota: Record<string, CodexQuotaState>;
|
||||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||||
|
claudeQuota: Record<string, ClaudeQuotaState>;
|
||||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||||
|
setClaudeQuota: (updater: QuotaUpdater<Record<string, ClaudeQuotaState>>) => void;
|
||||||
clearQuotaCache: () => void;
|
clearQuotaCache: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +409,52 @@ const fetchGeminiCliQuota = async (
|
|||||||
return buildGeminiCliQuotaBuckets(parsedBuckets);
|
return buildGeminiCliQuotaBuckets(parsedBuckets);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchClaudeQuota = async (
|
||||||
|
file: AuthFileItem,
|
||||||
|
t: TFunction
|
||||||
|
): Promise<{ planType: string; usage: ClaudeUsageResponse }> => {
|
||||||
|
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||||
|
const authIndex = normalizeAuthIndexValue(rawAuthIndex);
|
||||||
|
if (!authIndex) {
|
||||||
|
throw new Error(t('claude_quota.missing_auth_index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [profileRes, usageRes] = await Promise.all([
|
||||||
|
apiCallApi.request({
|
||||||
|
authIndex,
|
||||||
|
method: 'GET',
|
||||||
|
url: CLAUDE_PROFILE_URL,
|
||||||
|
header: CLAUDE_REQUEST_HEADERS,
|
||||||
|
}),
|
||||||
|
apiCallApi.request({
|
||||||
|
authIndex,
|
||||||
|
method: 'GET',
|
||||||
|
url: CLAUDE_USAGE_URL,
|
||||||
|
header: CLAUDE_REQUEST_HEADERS,
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
if (profileRes.statusCode < 200 || profileRes.statusCode >= 300) {
|
||||||
|
throw createStatusError(getApiCallErrorMessage(profileRes), profileRes.statusCode);
|
||||||
|
}
|
||||||
|
if (usageRes.statusCode < 200 || usageRes.statusCode >= 300) {
|
||||||
|
throw createStatusError(getApiCallErrorMessage(usageRes), usageRes.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { organization } = JSON.parse(profileRes.bodyText) as ClaudeProfileResponse;
|
||||||
|
const tier = organization?.rate_limit_tier
|
||||||
|
const tierToPlanMap: 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 planType = tierToPlanMap[tier ?? ''] ?? 'plan_unknown';
|
||||||
|
const usage = JSON.parse(usageRes.bodyText) as ClaudeUsageResponse
|
||||||
|
|
||||||
|
return { planType, usage };
|
||||||
|
};
|
||||||
|
|
||||||
const renderAntigravityItems = (
|
const renderAntigravityItems = (
|
||||||
quota: AntigravityQuotaState,
|
quota: AntigravityQuotaState,
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
@@ -558,6 +614,95 @@ const renderGeminiCliItems = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderClaudeItems = (
|
||||||
|
{ planType, usage }: ClaudeQuotaState,
|
||||||
|
t: TFunction,
|
||||||
|
helpers: QuotaRenderHelpers
|
||||||
|
): ReactNode => {
|
||||||
|
if (!usage) return null
|
||||||
|
|
||||||
|
const { styles: styleMap, QuotaProgressBar } = helpers;
|
||||||
|
const { createElement: h, Fragment } = React;
|
||||||
|
|
||||||
|
const planLabel = t('claude_quota.' + planType);
|
||||||
|
const nodes: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (planLabel) {
|
||||||
|
nodes.push(
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ key: 'plan', className: styleMap.claudePlan },
|
||||||
|
h('span', { className: styleMap.claudePlanLabel }, t('claude_quota.plan_label')),
|
||||||
|
h('span', { className: styleMap.claudePlanValue }, planLabel)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Window = {
|
||||||
|
key: string
|
||||||
|
usage: number | null
|
||||||
|
resetDate: Date
|
||||||
|
}
|
||||||
|
const windows: Window[] = []
|
||||||
|
|
||||||
|
if (usage.five_hour) {
|
||||||
|
windows.push({
|
||||||
|
key: 'primary_window',
|
||||||
|
usage: usage.five_hour.utilization,
|
||||||
|
resetDate: new Date(usage.five_hour.resets_at)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (usage.seven_day) {
|
||||||
|
windows.push({
|
||||||
|
key: 'secondary_window',
|
||||||
|
usage: usage.seven_day.utilization,
|
||||||
|
resetDate: new Date(usage.seven_day.resets_at)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (usage.seven_day_sonnet) {
|
||||||
|
windows.push({
|
||||||
|
key: 'sonnet_window',
|
||||||
|
usage: usage.seven_day_sonnet.utilization,
|
||||||
|
resetDate: new Date(usage.seven_day_sonnet.resets_at)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windows.length === 0) {
|
||||||
|
nodes.push(
|
||||||
|
h('div', { key: 'empty', className: styleMap.quotaMessage }, t('claude_quota.empty_windows'))
|
||||||
|
);
|
||||||
|
return h(Fragment, null, ...nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
...windows.map((window) => {
|
||||||
|
const used = window.usage;
|
||||||
|
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)}%`;
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{ key: window.key, className: styleMap.quotaRow },
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaRowHeader },
|
||||||
|
h('span', { className: styleMap.quotaModel }, t('claude_quota.' + window.key)),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ className: styleMap.quotaMeta },
|
||||||
|
h('span', { className: styleMap.quotaPercent }, percentLabel),
|
||||||
|
h('span', { className: styleMap.quotaReset }, formatUnixSeconds(window.resetDate))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
h(QuotaProgressBar, { percent: remaining, highThreshold: 80, mediumThreshold: 50 })
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return h(Fragment, null, ...nodes);
|
||||||
|
};
|
||||||
|
|
||||||
export const ANTIGRAVITY_CONFIG: QuotaConfig<AntigravityQuotaState, AntigravityQuotaGroup[]> = {
|
export const ANTIGRAVITY_CONFIG: QuotaConfig<AntigravityQuotaState, AntigravityQuotaGroup[]> = {
|
||||||
type: 'antigravity',
|
type: 'antigravity',
|
||||||
i18nPrefix: 'antigravity_quota',
|
i18nPrefix: 'antigravity_quota',
|
||||||
@@ -631,3 +776,25 @@ export const GEMINI_CLI_CONFIG: QuotaConfig<GeminiCliQuotaState, GeminiCliQuotaB
|
|||||||
gridClassName: styles.geminiCliGrid,
|
gridClassName: styles.geminiCliGrid,
|
||||||
renderQuotaItems: renderGeminiCliItems,
|
renderQuotaItems: renderGeminiCliItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CLAUDE_CONFIG: QuotaConfig<ClaudeQuotaState, { planType: string; usage: ClaudeUsageResponse }> = {
|
||||||
|
type: 'claude',
|
||||||
|
i18nPrefix: 'claude_quota',
|
||||||
|
filterFn: (file) =>
|
||||||
|
isClaudeFile(file) && !isRuntimeOnlyAuthFile(file) && !isDisabledAuthFile(file),
|
||||||
|
fetchQuota: fetchClaudeQuota,
|
||||||
|
storeSelector: (state) => state.claudeQuota,
|
||||||
|
storeSetter: 'setClaudeQuota',
|
||||||
|
buildLoadingState: () => ({ status: 'loading' }),
|
||||||
|
buildSuccessState: ({ planType, usage }) => ({ status: 'success', planType, usage }),
|
||||||
|
buildErrorState: (message, status) => ({
|
||||||
|
status: 'error',
|
||||||
|
error: message,
|
||||||
|
errorStatus: status,
|
||||||
|
}),
|
||||||
|
cardClassName: styles.claudeCard,
|
||||||
|
controlsClassName: styles.claudeControls,
|
||||||
|
controlClassName: styles.claudeControl,
|
||||||
|
gridClassName: styles.claudeGrid,
|
||||||
|
renderQuotaItems: renderClaudeItems,
|
||||||
|
};
|
||||||
|
|||||||
@@ -456,6 +456,28 @@
|
|||||||
"plan_team": "Team",
|
"plan_team": "Team",
|
||||||
"plan_free": "Free"
|
"plan_free": "Free"
|
||||||
},
|
},
|
||||||
|
"claude_quota": {
|
||||||
|
"title": "Claude Code Quota",
|
||||||
|
"empty_title": "No Claude Code Auth Files",
|
||||||
|
"empty_desc": "Upload a Claude Code credential to view quota.",
|
||||||
|
"idle": "Not loaded. Click Refresh Button.",
|
||||||
|
"loading": "Loading quota...",
|
||||||
|
"load_failed": "Failed to load quota: {{message}}",
|
||||||
|
"missing_auth_index": "Auth file missing auth_index",
|
||||||
|
"empty_windows": "No quota data available",
|
||||||
|
"no_access": "This credential has no Claude Code access (plan: free).",
|
||||||
|
"refresh_button": "Refresh Quota",
|
||||||
|
"fetch_all": "Fetch All",
|
||||||
|
"primary_window": "5-hour limit",
|
||||||
|
"secondary_window": "Weekly limit",
|
||||||
|
"sonnet_window": "Weekly Sonnet limit",
|
||||||
|
"plan_label": "Plan",
|
||||||
|
"plan_unknown": "Unknown",
|
||||||
|
"plan_free": "Free",
|
||||||
|
"plan_pro": "Pro",
|
||||||
|
"plan_max5": "Max 5x",
|
||||||
|
"plan_max20": "Max 20x"
|
||||||
|
},
|
||||||
"gemini_cli_quota": {
|
"gemini_cli_quota": {
|
||||||
"title": "Gemini CLI Quota",
|
"title": "Gemini CLI Quota",
|
||||||
"empty_title": "No Gemini CLI Auth Files",
|
"empty_title": "No Gemini CLI Auth Files",
|
||||||
@@ -1122,4 +1144,4 @@
|
|||||||
"version": "Management UI Version",
|
"version": "Management UI Version",
|
||||||
"author": "Author"
|
"author": "Author"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
} from '@/utils/usage';
|
} from '@/utils/usage';
|
||||||
import { formatFileSize } from '@/utils/format';
|
import { formatFileSize } from '@/utils/format';
|
||||||
import styles from './AuthFilesPage.module.scss';
|
import styles from './AuthFilesPage.module.scss';
|
||||||
|
import { CLAUDE_CONFIG } from '../components/quota/quotaConfigs.ts';
|
||||||
|
|
||||||
type ThemeColors = { bg: string; text: string; border?: string };
|
type ThemeColors = { bg: string; text: string; border?: string };
|
||||||
type TypeColorSet = { light: ThemeColors; dark?: ThemeColors };
|
type TypeColorSet = { light: ThemeColors; dark?: ThemeColors };
|
||||||
@@ -98,9 +99,9 @@ const AUTH_FILES_UI_STATE_KEY = 'authFilesPage.uiState';
|
|||||||
const clampCardPageSize = (value: number) =>
|
const clampCardPageSize = (value: number) =>
|
||||||
Math.min(MAX_CARD_PAGE_SIZE, Math.max(MIN_CARD_PAGE_SIZE, Math.round(value)));
|
Math.min(MAX_CARD_PAGE_SIZE, Math.max(MIN_CARD_PAGE_SIZE, Math.round(value)));
|
||||||
|
|
||||||
type QuotaProviderType = 'antigravity' | 'codex' | 'gemini-cli';
|
type QuotaProviderType = 'antigravity' | 'codex' | 'gemini-cli' | 'claude';
|
||||||
|
|
||||||
const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>(['antigravity', 'codex', 'gemini-cli']);
|
const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>(['antigravity', 'codex', 'gemini-cli', 'claude']);
|
||||||
|
|
||||||
const resolveQuotaErrorMessage = (
|
const resolveQuotaErrorMessage = (
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
@@ -248,9 +249,11 @@ export function AuthFilesPage() {
|
|||||||
const antigravityQuota = useQuotaStore((state) => state.antigravityQuota);
|
const antigravityQuota = useQuotaStore((state) => state.antigravityQuota);
|
||||||
const codexQuota = useQuotaStore((state) => state.codexQuota);
|
const codexQuota = useQuotaStore((state) => state.codexQuota);
|
||||||
const geminiCliQuota = useQuotaStore((state) => state.geminiCliQuota);
|
const geminiCliQuota = useQuotaStore((state) => state.geminiCliQuota);
|
||||||
|
const claudeQuota = useQuotaStore((state) => state.claudeQuota);
|
||||||
const setAntigravityQuota = useQuotaStore((state) => state.setAntigravityQuota);
|
const setAntigravityQuota = useQuotaStore((state) => state.setAntigravityQuota);
|
||||||
const setCodexQuota = useQuotaStore((state) => state.setCodexQuota);
|
const setCodexQuota = useQuotaStore((state) => state.setCodexQuota);
|
||||||
const setGeminiCliQuota = useQuotaStore((state) => state.setGeminiCliQuota);
|
const setGeminiCliQuota = useQuotaStore((state) => state.setGeminiCliQuota);
|
||||||
|
const setClaudeQuota = useQuotaStore((state) => state.setClaudeQuota);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [files, setFiles] = useState<AuthFileItem[]>([]);
|
const [files, setFiles] = useState<AuthFileItem[]>([]);
|
||||||
@@ -1468,12 +1471,14 @@ export function AuthFilesPage() {
|
|||||||
const getQuotaConfig = (type: QuotaProviderType) => {
|
const getQuotaConfig = (type: QuotaProviderType) => {
|
||||||
if (type === 'antigravity') return ANTIGRAVITY_CONFIG;
|
if (type === 'antigravity') return ANTIGRAVITY_CONFIG;
|
||||||
if (type === 'codex') return CODEX_CONFIG;
|
if (type === 'codex') return CODEX_CONFIG;
|
||||||
|
if (type === 'claude') return CLAUDE_CONFIG;
|
||||||
return GEMINI_CLI_CONFIG;
|
return GEMINI_CLI_CONFIG;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getQuotaState = (type: QuotaProviderType, fileName: string) => {
|
const getQuotaState = (type: QuotaProviderType, fileName: string) => {
|
||||||
if (type === 'antigravity') return antigravityQuota[fileName];
|
if (type === 'antigravity') return antigravityQuota[fileName];
|
||||||
if (type === 'codex') return codexQuota[fileName];
|
if (type === 'codex') return codexQuota[fileName];
|
||||||
|
if (type === 'claude') return claudeQuota[fileName];
|
||||||
return geminiCliQuota[fileName];
|
return geminiCliQuota[fileName];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1490,9 +1495,13 @@ export function AuthFilesPage() {
|
|||||||
setCodexQuota(updater as never);
|
setCodexQuota(updater as never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (type === 'claude') {
|
||||||
|
setClaudeQuota(updater as never);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setGeminiCliQuota(updater as never);
|
setGeminiCliQuota(updater as never);
|
||||||
},
|
},
|
||||||
[setAntigravityQuota, setCodexQuota, setGeminiCliQuota]
|
[setAntigravityQuota, setClaudeQuota, setCodexQuota, setGeminiCliQuota]
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshQuotaForFile = useCallback(
|
const refreshQuotaForFile = useCallback(
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
|
|
||||||
.antigravityGrid,
|
.antigravityGrid,
|
||||||
.codexGrid,
|
.codexGrid,
|
||||||
|
.claudeGrid,
|
||||||
.geminiCliGrid {
|
.geminiCliGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $spacing-md;
|
gap: $spacing-md;
|
||||||
@@ -116,6 +117,7 @@
|
|||||||
|
|
||||||
.antigravityControls,
|
.antigravityControls,
|
||||||
.codexControls,
|
.codexControls,
|
||||||
|
.claudeControls,
|
||||||
.geminiCliControls {
|
.geminiCliControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-md;
|
gap: $spacing-md;
|
||||||
@@ -126,6 +128,7 @@
|
|||||||
|
|
||||||
.antigravityControl,
|
.antigravityControl,
|
||||||
.codexControl,
|
.codexControl,
|
||||||
|
.claudeControl,
|
||||||
.geminiCliControl {
|
.geminiCliControl {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -157,6 +160,12 @@
|
|||||||
rgba(255, 243, 224, 0));
|
rgba(255, 243, 224, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.claudeCard {
|
||||||
|
background-image: linear-gradient(180deg,
|
||||||
|
rgba(255, 231, 245, 0.2),
|
||||||
|
rgba(231, 239, 255, 0));
|
||||||
|
}
|
||||||
|
|
||||||
.geminiCliCard {
|
.geminiCliCard {
|
||||||
background-image: linear-gradient(180deg,
|
background-image: linear-gradient(180deg,
|
||||||
rgba(231, 239, 255, 0.2),
|
rgba(231, 239, 255, 0.2),
|
||||||
@@ -282,7 +291,8 @@
|
|||||||
padding: $spacing-xs $spacing-sm;
|
padding: $spacing-xs $spacing-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.codexPlan {
|
.codexPlan,
|
||||||
|
.claudePlan {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -290,11 +300,13 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.codexPlanLabel {
|
.codexPlanLabel,
|
||||||
|
.claudePlanLabel {
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.codexPlanValue {
|
.codexPlanValue,
|
||||||
|
.claudePlanValue {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '@/components/quota';
|
} from '@/components/quota';
|
||||||
import type { AuthFileItem } from '@/types';
|
import type { AuthFileItem } from '@/types';
|
||||||
import styles from './QuotaPage.module.scss';
|
import styles from './QuotaPage.module.scss';
|
||||||
|
import { CLAUDE_CONFIG } from '../components/quota/quotaConfigs.ts';
|
||||||
|
|
||||||
export function QuotaPage() {
|
export function QuotaPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -87,6 +88,12 @@ export function QuotaPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disableControls}
|
disabled={disableControls}
|
||||||
/>
|
/>
|
||||||
|
<QuotaSection
|
||||||
|
config={CLAUDE_CONFIG}
|
||||||
|
files={files}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disableControls}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { AntigravityQuotaState, CodexQuotaState, GeminiCliQuotaState } from '@/types';
|
import type { AntigravityQuotaState, ClaudeQuotaState, CodexQuotaState, GeminiCliQuotaState } from '@/types';
|
||||||
|
|
||||||
type QuotaUpdater<T> = T | ((prev: T) => T);
|
type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||||
|
|
||||||
@@ -11,9 +11,11 @@ interface QuotaStoreState {
|
|||||||
antigravityQuota: Record<string, AntigravityQuotaState>;
|
antigravityQuota: Record<string, AntigravityQuotaState>;
|
||||||
codexQuota: Record<string, CodexQuotaState>;
|
codexQuota: Record<string, CodexQuotaState>;
|
||||||
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
geminiCliQuota: Record<string, GeminiCliQuotaState>;
|
||||||
|
claudeQuota: Record<string, ClaudeQuotaState>;
|
||||||
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
|
||||||
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
|
||||||
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
|
||||||
|
setClaudeQuota: (updater: QuotaUpdater<Record<string, ClaudeQuotaState>>) => void;
|
||||||
clearQuotaCache: () => void;
|
clearQuotaCache: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
|||||||
antigravityQuota: {},
|
antigravityQuota: {},
|
||||||
codexQuota: {},
|
codexQuota: {},
|
||||||
geminiCliQuota: {},
|
geminiCliQuota: {},
|
||||||
|
claudeQuota: {},
|
||||||
setAntigravityQuota: (updater) =>
|
setAntigravityQuota: (updater) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
antigravityQuota: resolveUpdater(updater, state.antigravityQuota)
|
antigravityQuota: resolveUpdater(updater, state.antigravityQuota)
|
||||||
@@ -40,10 +43,15 @@ export const useQuotaStore = create<QuotaStoreState>((set) => ({
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota)
|
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota)
|
||||||
})),
|
})),
|
||||||
|
setClaudeQuota: (updater) =>
|
||||||
|
set((state) => ({
|
||||||
|
claudeQuota: resolveUpdater(updater, state.claudeQuota)
|
||||||
|
})),
|
||||||
clearQuotaCache: () =>
|
clearQuotaCache: () =>
|
||||||
set({
|
set({
|
||||||
antigravityQuota: {},
|
antigravityQuota: {},
|
||||||
codexQuota: {},
|
codexQuota: {},
|
||||||
geminiCliQuota: {}
|
geminiCliQuota: {},
|
||||||
|
claudeQuota: {}
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -145,3 +145,54 @@ export interface CodexQuotaState {
|
|||||||
error?: string;
|
error?: string;
|
||||||
errorStatus?: number;
|
errorStatus?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ClaudeUsageWindow {
|
||||||
|
utilization: number | null
|
||||||
|
resets_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaudeUsageResponse {
|
||||||
|
five_hour: ClaudeUsageWindow | null
|
||||||
|
seven_day: ClaudeUsageWindow | null
|
||||||
|
seven_day_oauth_apps: ClaudeUsageWindow | null // currently unused
|
||||||
|
seven_day_opus: ClaudeUsageWindow | null // currently unused
|
||||||
|
seven_day_sonnet: ClaudeUsageWindow | null
|
||||||
|
seven_day_cowork: ClaudeUsageWindow | null // currently unused
|
||||||
|
extra_usage: {
|
||||||
|
is_enabled: boolean
|
||||||
|
monthly_limit: number
|
||||||
|
used_credits: number
|
||||||
|
utilization: number | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaudeQuotaState {
|
||||||
|
status: 'idle' | 'loading' | 'success' | 'error';
|
||||||
|
usage?: ClaudeUsageResponse;
|
||||||
|
planType?: string | null;
|
||||||
|
error?: string;
|
||||||
|
errorStatus?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,3 +159,16 @@ export const CODEX_REQUEST_HEADERS = {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal',
|
'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Claude Code configuration
|
||||||
|
export const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage'
|
||||||
|
export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile'
|
||||||
|
|
||||||
|
export const CLAUDE_REQUEST_HEADERS = {
|
||||||
|
Authorization: 'Bearer $TOKEN$',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'claude-cli/1.0.83 (external, cli)',
|
||||||
|
'Anthropic-Beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,prompt-caching-2024-07-31',
|
||||||
|
'Anthropic-Version': '2023-06-01',
|
||||||
|
'x-app': 'cli',
|
||||||
|
};
|
||||||
@@ -18,9 +18,9 @@ export function formatQuotaResetTime(value?: string): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatUnixSeconds(value: number | null): string {
|
export function formatUnixSeconds(value: Date | number | null): string {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
const date = new Date(value * 1000);
|
const date = typeof value === 'number' ? new Date(value * 1000) : value;
|
||||||
if (Number.isNaN(date.getTime())) return '-';
|
if (Number.isNaN(date.getTime())) return '-';
|
||||||
return date.toLocaleString(undefined, {
|
return date.toLocaleString(undefined, {
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export function isGeminiCliFile(file: AuthFileItem): boolean {
|
|||||||
return resolveAuthProvider(file) === 'gemini-cli';
|
return resolveAuthProvider(file) === 'gemini-cli';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isClaudeFile(file: AuthFileItem): boolean {
|
||||||
|
return resolveAuthProvider(file) === 'claude';
|
||||||
|
}
|
||||||
|
|
||||||
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
||||||
const raw = file['runtime_only'] ?? file.runtimeOnly;
|
const raw = file['runtime_only'] ?? file.runtimeOnly;
|
||||||
if (typeof raw === 'boolean') return raw;
|
if (typeof raw === 'boolean') return raw;
|
||||||
|
|||||||
Reference in New Issue
Block a user