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): restore Claude plan detection via profile API
This commit is contained in:
@@ -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<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 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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 额度",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user