mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 18:50:49 +08:00
refactor(quota): modularize QuotaPage into separate section components
This commit is contained in:
212
src/utils/quota/builders.ts
Normal file
212
src/utils/quota/builders.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Builder functions for constructing quota data structures.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AntigravityQuotaGroup,
|
||||
AntigravityQuotaGroupDefinition,
|
||||
AntigravityQuotaInfo,
|
||||
AntigravityModelsPayload,
|
||||
GeminiCliParsedBucket,
|
||||
GeminiCliQuotaBucketState
|
||||
} from '@/types';
|
||||
import { ANTIGRAVITY_QUOTA_GROUPS, GEMINI_CLI_GROUP_LOOKUP } from './constants';
|
||||
import { normalizeQuotaFraction } from './parsers';
|
||||
import { isIgnoredGeminiCliModel } from './validators';
|
||||
|
||||
export function pickEarlierResetTime(current?: string, next?: string): string | undefined {
|
||||
if (!current) return next;
|
||||
if (!next) return current;
|
||||
const currentTime = new Date(current).getTime();
|
||||
const nextTime = new Date(next).getTime();
|
||||
if (Number.isNaN(currentTime)) return next;
|
||||
if (Number.isNaN(nextTime)) return current;
|
||||
return currentTime <= nextTime ? current : next;
|
||||
}
|
||||
|
||||
export function minNullableNumber(current: number | null, next: number | null): number | null {
|
||||
if (current === null) return next;
|
||||
if (next === null) return current;
|
||||
return Math.min(current, next);
|
||||
}
|
||||
|
||||
export function buildGeminiCliQuotaBuckets(
|
||||
buckets: GeminiCliParsedBucket[]
|
||||
): GeminiCliQuotaBucketState[] {
|
||||
if (buckets.length === 0) return [];
|
||||
|
||||
const grouped = new Map<string, GeminiCliQuotaBucketState & { modelIds: string[] }>();
|
||||
|
||||
buckets.forEach((bucket) => {
|
||||
if (isIgnoredGeminiCliModel(bucket.modelId)) return;
|
||||
const group = GEMINI_CLI_GROUP_LOOKUP.get(bucket.modelId);
|
||||
const groupId = group?.id ?? bucket.modelId;
|
||||
const label = group?.label ?? bucket.modelId;
|
||||
const tokenKey = bucket.tokenType ?? '';
|
||||
const mapKey = `${groupId}::${tokenKey}`;
|
||||
const existing = grouped.get(mapKey);
|
||||
|
||||
if (!existing) {
|
||||
grouped.set(mapKey, {
|
||||
id: `${groupId}${tokenKey ? `-${tokenKey}` : ''}`,
|
||||
label,
|
||||
remainingFraction: bucket.remainingFraction,
|
||||
remainingAmount: bucket.remainingAmount,
|
||||
resetTime: bucket.resetTime,
|
||||
tokenType: bucket.tokenType,
|
||||
modelIds: [bucket.modelId]
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
existing.remainingFraction = minNullableNumber(
|
||||
existing.remainingFraction,
|
||||
bucket.remainingFraction
|
||||
);
|
||||
existing.remainingAmount = minNullableNumber(existing.remainingAmount, bucket.remainingAmount);
|
||||
existing.resetTime = pickEarlierResetTime(existing.resetTime, bucket.resetTime);
|
||||
existing.modelIds.push(bucket.modelId);
|
||||
});
|
||||
|
||||
return Array.from(grouped.values()).map((bucket) => {
|
||||
const uniqueModelIds = Array.from(new Set(bucket.modelIds));
|
||||
return {
|
||||
id: bucket.id,
|
||||
label: bucket.label,
|
||||
remainingFraction: bucket.remainingFraction,
|
||||
remainingAmount: bucket.remainingAmount,
|
||||
resetTime: bucket.resetTime,
|
||||
tokenType: bucket.tokenType,
|
||||
modelIds: uniqueModelIds
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getAntigravityQuotaInfo(entry?: AntigravityQuotaInfo): {
|
||||
remainingFraction: number | null;
|
||||
resetTime?: string;
|
||||
displayName?: string;
|
||||
} {
|
||||
if (!entry) {
|
||||
return { remainingFraction: null };
|
||||
}
|
||||
const quotaInfo = entry.quotaInfo ?? entry.quota_info ?? {};
|
||||
const remainingValue =
|
||||
quotaInfo.remainingFraction ?? quotaInfo.remaining_fraction ?? quotaInfo.remaining;
|
||||
const remainingFraction = normalizeQuotaFraction(remainingValue);
|
||||
const resetValue = quotaInfo.resetTime ?? quotaInfo.reset_time;
|
||||
const resetTime = typeof resetValue === 'string' ? resetValue : undefined;
|
||||
const displayName = typeof entry.displayName === 'string' ? entry.displayName : undefined;
|
||||
|
||||
return {
|
||||
remainingFraction,
|
||||
resetTime,
|
||||
displayName
|
||||
};
|
||||
}
|
||||
|
||||
export function findAntigravityModel(
|
||||
models: AntigravityModelsPayload,
|
||||
identifier: string
|
||||
): { id: string; entry: AntigravityQuotaInfo } | null {
|
||||
const direct = models[identifier];
|
||||
if (direct) {
|
||||
return { id: identifier, entry: direct };
|
||||
}
|
||||
|
||||
const match = Object.entries(models).find(([, entry]) => {
|
||||
const name = typeof entry?.displayName === 'string' ? entry.displayName : '';
|
||||
return name.toLowerCase() === identifier.toLowerCase();
|
||||
});
|
||||
if (match) {
|
||||
return { id: match[0], entry: match[1] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildAntigravityQuotaGroups(
|
||||
models: AntigravityModelsPayload
|
||||
): AntigravityQuotaGroup[] {
|
||||
const groups: AntigravityQuotaGroup[] = [];
|
||||
let geminiProResetTime: string | undefined;
|
||||
const [claudeDef, geminiProDef, flashDef, flashLiteDef, cuDef, geminiFlashDef, imageDef] =
|
||||
ANTIGRAVITY_QUOTA_GROUPS;
|
||||
|
||||
const buildGroup = (
|
||||
def: AntigravityQuotaGroupDefinition,
|
||||
overrideResetTime?: string
|
||||
): AntigravityQuotaGroup | null => {
|
||||
const matches = def.identifiers
|
||||
.map((identifier) => findAntigravityModel(models, identifier))
|
||||
.filter((entry): entry is { id: string; entry: AntigravityQuotaInfo } => Boolean(entry));
|
||||
|
||||
const quotaEntries = matches
|
||||
.map(({ id, entry }) => {
|
||||
const info = getAntigravityQuotaInfo(entry);
|
||||
const remainingFraction = info.remainingFraction ?? (info.resetTime ? 0 : null);
|
||||
if (remainingFraction === null) return null;
|
||||
return {
|
||||
id,
|
||||
remainingFraction,
|
||||
resetTime: info.resetTime,
|
||||
displayName: info.displayName
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
||||
|
||||
if (quotaEntries.length === 0) return null;
|
||||
|
||||
const remainingFraction = Math.min(...quotaEntries.map((entry) => entry.remainingFraction));
|
||||
const resetTime =
|
||||
overrideResetTime ?? quotaEntries.map((entry) => entry.resetTime).find(Boolean);
|
||||
const displayName = quotaEntries.map((entry) => entry.displayName).find(Boolean);
|
||||
const label = def.labelFromModel && displayName ? displayName : def.label;
|
||||
|
||||
return {
|
||||
id: def.id,
|
||||
label,
|
||||
models: quotaEntries.map((entry) => entry.id),
|
||||
remainingFraction,
|
||||
resetTime
|
||||
};
|
||||
};
|
||||
|
||||
const claudeGroup = buildGroup(claudeDef);
|
||||
if (claudeGroup) {
|
||||
groups.push(claudeGroup);
|
||||
}
|
||||
|
||||
const geminiProGroup = buildGroup(geminiProDef);
|
||||
if (geminiProGroup) {
|
||||
geminiProResetTime = geminiProGroup.resetTime;
|
||||
groups.push(geminiProGroup);
|
||||
}
|
||||
|
||||
const flashGroup = buildGroup(flashDef);
|
||||
if (flashGroup) {
|
||||
groups.push(flashGroup);
|
||||
}
|
||||
|
||||
const flashLiteGroup = buildGroup(flashLiteDef);
|
||||
if (flashLiteGroup) {
|
||||
groups.push(flashLiteGroup);
|
||||
}
|
||||
|
||||
const cuGroup = buildGroup(cuDef);
|
||||
if (cuGroup) {
|
||||
groups.push(cuGroup);
|
||||
}
|
||||
|
||||
const geminiFlashGroup = buildGroup(geminiFlashDef);
|
||||
if (geminiFlashGroup) {
|
||||
groups.push(geminiFlashGroup);
|
||||
}
|
||||
|
||||
const imageGroup = buildGroup(imageDef, geminiProResetTime);
|
||||
if (imageGroup) {
|
||||
groups.push(imageGroup);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
159
src/utils/quota/constants.ts
Normal file
159
src/utils/quota/constants.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Quota constants for API URLs, headers, and theme colors.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AntigravityQuotaGroupDefinition,
|
||||
GeminiCliQuotaGroupDefinition,
|
||||
TypeColorSet
|
||||
} from '@/types';
|
||||
|
||||
// Theme colors for type badges
|
||||
export const TYPE_COLORS: Record<string, TypeColorSet> = {
|
||||
qwen: {
|
||||
light: { bg: '#e8f5e9', text: '#2e7d32' },
|
||||
dark: { bg: '#1b5e20', text: '#81c784' }
|
||||
},
|
||||
gemini: {
|
||||
light: { bg: '#e3f2fd', text: '#1565c0' },
|
||||
dark: { bg: '#0d47a1', text: '#64b5f6' }
|
||||
},
|
||||
'gemini-cli': {
|
||||
light: { bg: '#e7efff', text: '#1e4fa3' },
|
||||
dark: { bg: '#1c3f73', text: '#a8c7ff' }
|
||||
},
|
||||
aistudio: {
|
||||
light: { bg: '#f0f2f5', text: '#2f343c' },
|
||||
dark: { bg: '#373c42', text: '#cfd3db' }
|
||||
},
|
||||
claude: {
|
||||
light: { bg: '#fce4ec', text: '#c2185b' },
|
||||
dark: { bg: '#880e4f', text: '#f48fb1' }
|
||||
},
|
||||
codex: {
|
||||
light: { bg: '#fff3e0', text: '#ef6c00' },
|
||||
dark: { bg: '#e65100', text: '#ffb74d' }
|
||||
},
|
||||
antigravity: {
|
||||
light: { bg: '#e0f7fa', text: '#006064' },
|
||||
dark: { bg: '#004d40', text: '#80deea' }
|
||||
},
|
||||
iflow: {
|
||||
light: { bg: '#f3e5f5', text: '#7b1fa2' },
|
||||
dark: { bg: '#4a148c', text: '#ce93d8' }
|
||||
},
|
||||
empty: {
|
||||
light: { bg: '#f5f5f5', text: '#616161' },
|
||||
dark: { bg: '#424242', text: '#bdbdbd' }
|
||||
},
|
||||
unknown: {
|
||||
light: { bg: '#f0f0f0', text: '#666666', border: '1px dashed #999999' },
|
||||
dark: { bg: '#3a3a3a', text: '#aaaaaa', border: '1px dashed #666666' }
|
||||
}
|
||||
};
|
||||
|
||||
// Antigravity API configuration
|
||||
export const ANTIGRAVITY_QUOTA_URLS = [
|
||||
'https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels',
|
||||
'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
|
||||
'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels'
|
||||
];
|
||||
|
||||
export const ANTIGRAVITY_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'antigravity/1.11.5 windows/amd64'
|
||||
};
|
||||
|
||||
export const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroupDefinition[] = [
|
||||
{
|
||||
id: 'claude-gpt',
|
||||
label: 'Claude/GPT',
|
||||
identifiers: [
|
||||
'claude-sonnet-4-5-thinking',
|
||||
'claude-opus-4-5-thinking',
|
||||
'claude-sonnet-4-5',
|
||||
'gpt-oss-120b-medium'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'gemini-3-pro',
|
||||
label: 'Gemini 3 Pro',
|
||||
identifiers: ['gemini-3-pro-high', 'gemini-3-pro-low']
|
||||
},
|
||||
{
|
||||
id: 'gemini-2-5-flash',
|
||||
label: 'Gemini 2.5 Flash',
|
||||
identifiers: ['gemini-2.5-flash', 'gemini-2.5-flash-thinking']
|
||||
},
|
||||
{
|
||||
id: 'gemini-2-5-flash-lite',
|
||||
label: 'Gemini 2.5 Flash Lite',
|
||||
identifiers: ['gemini-2.5-flash-lite']
|
||||
},
|
||||
{
|
||||
id: 'gemini-2-5-cu',
|
||||
label: 'Gemini 2.5 CU',
|
||||
identifiers: ['rev19-uic3-1p']
|
||||
},
|
||||
{
|
||||
id: 'gemini-3-flash',
|
||||
label: 'Gemini 3 Flash',
|
||||
identifiers: ['gemini-3-flash']
|
||||
},
|
||||
{
|
||||
id: 'gemini-image',
|
||||
label: 'gemini-3-pro-image',
|
||||
identifiers: ['gemini-3-pro-image'],
|
||||
labelFromModel: true
|
||||
}
|
||||
];
|
||||
|
||||
// Gemini CLI API configuration
|
||||
export const GEMINI_CLI_QUOTA_URL =
|
||||
'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota';
|
||||
|
||||
export const GEMINI_CLI_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
export const GEMINI_CLI_QUOTA_GROUPS: GeminiCliQuotaGroupDefinition[] = [
|
||||
{
|
||||
id: 'gemini-2-5-flash-series',
|
||||
label: 'Gemini 2.5 Flash Series',
|
||||
modelIds: ['gemini-2.5-flash', 'gemini-2.5-flash-lite']
|
||||
},
|
||||
{
|
||||
id: 'gemini-2-5-pro',
|
||||
label: 'Gemini 2.5 Pro',
|
||||
modelIds: ['gemini-2.5-pro']
|
||||
},
|
||||
{
|
||||
id: 'gemini-3-pro-preview',
|
||||
label: 'Gemini 3 Pro Preview',
|
||||
modelIds: ['gemini-3-pro-preview']
|
||||
},
|
||||
{
|
||||
id: 'gemini-3-flash-preview',
|
||||
label: 'Gemini 3 Flash Preview',
|
||||
modelIds: ['gemini-3-flash-preview']
|
||||
}
|
||||
];
|
||||
|
||||
export const GEMINI_CLI_GROUP_LOOKUP = new Map(
|
||||
GEMINI_CLI_QUOTA_GROUPS.flatMap((group) =>
|
||||
group.modelIds.map((modelId) => [modelId, group] as const)
|
||||
)
|
||||
);
|
||||
|
||||
export const GEMINI_CLI_IGNORED_MODEL_PREFIXES = ['gemini-2.0-flash'];
|
||||
|
||||
// Codex API configuration
|
||||
export const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
||||
|
||||
export const CODEX_REQUEST_HEADERS = {
|
||||
Authorization: 'Bearer $TOKEN$',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal'
|
||||
};
|
||||
68
src/utils/quota/formatters.ts
Normal file
68
src/utils/quota/formatters.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Formatting functions for quota display.
|
||||
*/
|
||||
|
||||
import type { CodexUsageWindow } from '@/types';
|
||||
import { normalizeNumberValue } from './parsers';
|
||||
|
||||
export function formatQuotaResetTime(value?: string): string {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleString(undefined, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
export function formatUnixSeconds(value: number | null): string {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value * 1000);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleString(undefined, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCodexResetLabel(window?: CodexUsageWindow | null): string {
|
||||
if (!window) return '-';
|
||||
const resetAt = normalizeNumberValue(window.reset_at ?? window.resetAt);
|
||||
if (resetAt !== null && resetAt > 0) {
|
||||
return formatUnixSeconds(resetAt);
|
||||
}
|
||||
const resetAfter = normalizeNumberValue(window.reset_after_seconds ?? window.resetAfterSeconds);
|
||||
if (resetAfter !== null && resetAfter > 0) {
|
||||
const targetSeconds = Math.floor(Date.now() / 1000 + resetAfter);
|
||||
return formatUnixSeconds(targetSeconds);
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
export function createStatusError(message: string, status?: number): Error & { status?: number } {
|
||||
const error = new Error(message) as Error & { status?: number };
|
||||
if (status !== undefined) {
|
||||
error.status = status;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getStatusFromError(err: unknown): number | undefined {
|
||||
if (typeof err === 'object' && err !== null && 'status' in err) {
|
||||
const rawStatus = (err as { status?: unknown }).status;
|
||||
if (typeof rawStatus === 'number' && Number.isFinite(rawStatus)) {
|
||||
return rawStatus;
|
||||
}
|
||||
const asNumber = Number(rawStatus);
|
||||
if (Number.isFinite(asNumber) && asNumber > 0) {
|
||||
return asNumber;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
10
src/utils/quota/index.ts
Normal file
10
src/utils/quota/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Quota utility functions barrel export.
|
||||
*/
|
||||
|
||||
export * from './constants';
|
||||
export * from './parsers';
|
||||
export * from './resolvers';
|
||||
export * from './formatters';
|
||||
export * from './validators';
|
||||
export * from './builders';
|
||||
153
src/utils/quota/parsers.ts
Normal file
153
src/utils/quota/parsers.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Normalization and parsing functions for quota data.
|
||||
*/
|
||||
|
||||
import type { CodexUsagePayload, GeminiCliQuotaPayload } from '@/types';
|
||||
|
||||
export function normalizeAuthIndexValue(value: unknown): string | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value.toString();
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeStringValue(value: unknown): string | null {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeNumberValue(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const parsed = Number(trimmed);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeQuotaFraction(value: unknown): number | null {
|
||||
const normalized = normalizeNumberValue(value);
|
||||
if (normalized !== null) return normalized;
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.endsWith('%')) {
|
||||
const parsed = Number(trimmed.slice(0, -1));
|
||||
return Number.isFinite(parsed) ? parsed / 100 : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizePlanType(value: unknown): string | null {
|
||||
const normalized = normalizeStringValue(value);
|
||||
return normalized ? normalized.toLowerCase() : null;
|
||||
}
|
||||
|
||||
export function decodeBase64UrlPayload(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
const normalized = trimmed.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
|
||||
if (typeof window !== 'undefined' && typeof window.atob === 'function') {
|
||||
return window.atob(padded);
|
||||
}
|
||||
if (typeof atob === 'function') {
|
||||
return atob(padded);
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseIdTokenPayload(value: unknown): Record<string, unknown> | null {
|
||||
if (!value) return null;
|
||||
if (typeof value === 'object') {
|
||||
return Array.isArray(value) ? null : (value as Record<string, unknown>);
|
||||
}
|
||||
if (typeof value !== 'string') return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
if (parsed && typeof parsed === 'object') return parsed;
|
||||
} catch {
|
||||
// Continue to JWT parsing
|
||||
}
|
||||
const segments = trimmed.split('.');
|
||||
if (segments.length < 2) return null;
|
||||
const decoded = decodeBase64UrlPayload(segments[1]);
|
||||
if (!decoded) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(decoded) as Record<string, unknown>;
|
||||
if (parsed && typeof parsed === 'object') return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseAntigravityPayload(payload: unknown): Record<string, unknown> | 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 Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof payload === 'object') {
|
||||
return payload as Record<string, unknown>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseCodexUsagePayload(payload: unknown): CodexUsagePayload | 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 CodexUsagePayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof payload === 'object') {
|
||||
return payload as CodexUsagePayload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseGeminiCliQuotaPayload(payload: unknown): GeminiCliQuotaPayload | 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 GeminiCliQuotaPayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof payload === 'object') {
|
||||
return payload as GeminiCliQuotaPayload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
112
src/utils/quota/resolvers.ts
Normal file
112
src/utils/quota/resolvers.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Resolver functions for extracting data from auth files.
|
||||
*/
|
||||
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import {
|
||||
normalizeStringValue,
|
||||
normalizePlanType,
|
||||
parseIdTokenPayload
|
||||
} from './parsers';
|
||||
|
||||
export function extractCodexChatgptAccountId(value: unknown): string | null {
|
||||
const payload = parseIdTokenPayload(value);
|
||||
if (!payload) return null;
|
||||
return normalizeStringValue(payload.chatgpt_account_id ?? payload.chatgptAccountId);
|
||||
}
|
||||
|
||||
export function resolveCodexChatgptAccountId(file: AuthFileItem): string | null {
|
||||
const metadata =
|
||||
file && typeof file.metadata === 'object' && file.metadata !== null
|
||||
? (file.metadata as Record<string, unknown>)
|
||||
: null;
|
||||
const attributes =
|
||||
file && typeof file.attributes === 'object' && file.attributes !== null
|
||||
? (file.attributes as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const candidates = [file.id_token, metadata?.id_token, attributes?.id_token];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const id = extractCodexChatgptAccountId(candidate);
|
||||
if (id) return id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveCodexPlanType(file: AuthFileItem): string | null {
|
||||
const metadata =
|
||||
file && typeof file.metadata === 'object' && file.metadata !== null
|
||||
? (file.metadata as Record<string, unknown>)
|
||||
: null;
|
||||
const attributes =
|
||||
file && typeof file.attributes === 'object' && file.attributes !== null
|
||||
? (file.attributes as Record<string, unknown>)
|
||||
: null;
|
||||
const idToken =
|
||||
file && typeof file.id_token === 'object' && file.id_token !== null
|
||||
? (file.id_token as Record<string, unknown>)
|
||||
: null;
|
||||
const metadataIdToken =
|
||||
metadata && typeof metadata.id_token === 'object' && metadata.id_token !== null
|
||||
? (metadata.id_token as Record<string, unknown>)
|
||||
: null;
|
||||
const candidates = [
|
||||
file.plan_type,
|
||||
file.planType,
|
||||
file['plan_type'],
|
||||
file['planType'],
|
||||
file.id_token,
|
||||
idToken?.plan_type,
|
||||
idToken?.planType,
|
||||
metadata?.plan_type,
|
||||
metadata?.planType,
|
||||
metadata?.id_token,
|
||||
metadataIdToken?.plan_type,
|
||||
metadataIdToken?.planType,
|
||||
attributes?.plan_type,
|
||||
attributes?.planType,
|
||||
attributes?.id_token
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const planType = normalizePlanType(candidate);
|
||||
if (planType) return planType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractGeminiCliProjectId(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const matches = Array.from(value.matchAll(/\(([^()]+)\)/g));
|
||||
if (matches.length === 0) return null;
|
||||
const candidate = matches[matches.length - 1]?.[1]?.trim();
|
||||
return candidate ? candidate : null;
|
||||
}
|
||||
|
||||
export function resolveGeminiCliProjectId(file: AuthFileItem): string | null {
|
||||
const metadata =
|
||||
file && typeof file.metadata === 'object' && file.metadata !== null
|
||||
? (file.metadata as Record<string, unknown>)
|
||||
: null;
|
||||
const attributes =
|
||||
file && typeof file.attributes === 'object' && file.attributes !== null
|
||||
? (file.attributes as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
const candidates = [
|
||||
file.account,
|
||||
file['account'],
|
||||
metadata?.account,
|
||||
attributes?.account
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const projectId = extractGeminiCliProjectId(candidate);
|
||||
if (projectId) return projectId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
36
src/utils/quota/validators.ts
Normal file
36
src/utils/quota/validators.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Validation and type checking functions for quota management.
|
||||
*/
|
||||
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { GEMINI_CLI_IGNORED_MODEL_PREFIXES } from './constants';
|
||||
|
||||
export function resolveAuthProvider(file: AuthFileItem): string {
|
||||
const raw = file.provider ?? file.type ?? '';
|
||||
return String(raw).trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function isAntigravityFile(file: AuthFileItem): boolean {
|
||||
return resolveAuthProvider(file) === 'antigravity';
|
||||
}
|
||||
|
||||
export function isCodexFile(file: AuthFileItem): boolean {
|
||||
return resolveAuthProvider(file) === 'codex';
|
||||
}
|
||||
|
||||
export function isGeminiCliFile(file: AuthFileItem): boolean {
|
||||
return resolveAuthProvider(file) === 'gemini-cli';
|
||||
}
|
||||
|
||||
export function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean {
|
||||
const raw = file['runtime_only'] ?? file.runtimeOnly;
|
||||
if (typeof raw === 'boolean') return raw;
|
||||
if (typeof raw === 'string') return raw.trim().toLowerCase() === 'true';
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isIgnoredGeminiCliModel(modelId: string): boolean {
|
||||
return GEMINI_CLI_IGNORED_MODEL_PREFIXES.some(
|
||||
(prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user