Files
Cli-Proxy-API-Management-Ce…/src/features/authFiles/constants.ts
T

237 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { TFunction } from 'i18next';
import type { AuthFileItem } from '@/types';
import {
normalizeUsageSourceId,
type KeyStatBucket,
type KeyStats
} from '@/utils/usage';
export type ThemeColors = { bg: string; text: string; border?: string };
export type TypeColorSet = { light: ThemeColors; dark?: ThemeColors };
export type ResolvedTheme = 'light' | 'dark';
export type AuthFileModelItem = { id: string; display_name?: string; type?: string; owned_by?: string };
export type QuotaProviderType = 'antigravity' | 'codex' | 'gemini-cli';
export const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>(['antigravity', 'codex', 'gemini-cli']);
export const MIN_CARD_PAGE_SIZE = 3;
export const MAX_CARD_PAGE_SIZE = 30;
export const AUTH_FILE_REFRESH_WARNING_MS = 24 * 60 * 60 * 1000;
export const INTEGER_STRING_PATTERN = /^[+-]?\d+$/;
export const TRUTHY_TEXT_VALUES = new Set(['true', '1', 'yes', 'y', 'on']);
export const FALSY_TEXT_VALUES = new Set(['false', '0', 'no', 'n', 'off']);
// 标签类型颜色配置(对齐重构前 styles.css 的 file-type-badge 颜色)
export const TYPE_COLORS: Record<string, TypeColorSet> = {
qwen: {
light: { bg: '#e8f5e9', text: '#2e7d32' },
dark: { bg: '#1b5e20', text: '#81c784' }
},
kimi: {
light: { bg: '#fff4e5', text: '#ad6800' },
dark: { bg: '#7c4a03', text: '#ffd591' }
},
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' }
}
};
export const clampCardPageSize = (value: number) =>
Math.min(MAX_CARD_PAGE_SIZE, Math.max(MIN_CARD_PAGE_SIZE, Math.round(value)));
export const resolveQuotaErrorMessage = (
t: TFunction,
status: number | undefined,
fallback: string
): string => {
if (status === 404) return t('common.quota_update_required');
if (status === 403) return t('common.quota_check_credential');
return fallback;
};
export const normalizeProviderKey = (value: string) => value.trim().toLowerCase();
export const getTypeLabel = (t: TFunction, type: string): string => {
const key = `auth_files.filter_${type}`;
const translated = t(key);
if (translated !== key) return translated;
if (type.toLowerCase() === 'iflow') return 'iFlow';
return type.charAt(0).toUpperCase() + type.slice(1);
};
export const getTypeColor = (type: string, resolvedTheme: ResolvedTheme): ThemeColors => {
const set = TYPE_COLORS[type] || TYPE_COLORS.unknown;
return resolvedTheme === 'dark' && set.dark ? set.dark : set.light;
};
export const parsePriorityValue = (value: unknown): number | undefined => {
if (typeof value === 'number') {
return Number.isInteger(value) ? value : undefined;
}
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
if (!trimmed || !INTEGER_STRING_PATTERN.test(trimmed)) return undefined;
const parsed = Number.parseInt(trimmed, 10);
return Number.isSafeInteger(parsed) ? parsed : undefined;
};
export const normalizeExcludedModels = (value: unknown): string[] => {
if (!Array.isArray(value)) return [];
const seen = new Set<string>();
const normalized: string[] = [];
value.forEach((entry) => {
const model = String(entry ?? '')
.trim()
.toLowerCase();
if (!model || seen.has(model)) return;
seen.add(model);
normalized.push(model);
});
return normalized.sort((a, b) => a.localeCompare(b));
};
export const parseExcludedModelsText = (value: string): string[] =>
normalizeExcludedModels(value.split(/[\n,]+/));
export const parseDisableCoolingValue = (value: unknown): boolean | undefined => {
if (typeof value === 'boolean') return value;
if (typeof value === 'number' && Number.isFinite(value)) return value !== 0;
if (typeof value !== 'string') return undefined;
const normalized = value.trim().toLowerCase();
if (!normalized) return undefined;
if (TRUTHY_TEXT_VALUES.has(normalized)) return true;
if (FALSY_TEXT_VALUES.has(normalized)) return false;
return undefined;
};
// 标准化 auth_index 值(与 usage.ts 中的 normalizeAuthIndex 保持一致)
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 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 resolveAuthFileStats(file: AuthFileItem, stats: KeyStats): KeyStatBucket {
const defaultStats: KeyStatBucket = { success: 0, failure: 0 };
const rawFileName = file?.name || '';
// 兼容 auth_index 和 authIndex 两种字段名(API 返回的是 auth_index
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
const authIndexKey = normalizeAuthIndexValue(rawAuthIndex);
// 尝试根据 authIndex 匹配
if (authIndexKey && stats.byAuthIndex?.[authIndexKey]) {
return stats.byAuthIndex[authIndexKey];
}
// 尝试根据 source (文件名) 匹配
const fileNameId = rawFileName ? normalizeUsageSourceId(rawFileName) : '';
if (fileNameId && stats.bySource?.[fileNameId]) {
const fromName = stats.bySource[fileNameId];
if (fromName.success > 0 || fromName.failure > 0) {
return fromName;
}
}
// 尝试去掉扩展名后匹配
if (rawFileName) {
const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, '');
if (nameWithoutExt && nameWithoutExt !== rawFileName) {
const nameWithoutExtId = normalizeUsageSourceId(nameWithoutExt);
const fromNameWithoutExt = nameWithoutExtId ? stats.bySource?.[nameWithoutExtId] : undefined;
if (
fromNameWithoutExt &&
(fromNameWithoutExt.success > 0 || fromNameWithoutExt.failure > 0)
) {
return fromNameWithoutExt;
}
}
}
return defaultStats;
}
export const formatModified = (item: AuthFileItem): string => {
const raw = item['modtime'] ?? item.modified;
if (!raw) return '-';
const asNumber = Number(raw);
const date =
Number.isFinite(asNumber) && !Number.isNaN(asNumber)
? new Date(asNumber < 1e12 ? asNumber * 1000 : asNumber)
: new Date(String(raw));
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
};
// 检查模型是否被 OAuth 排除
export const isModelExcluded = (
modelId: string,
providerType: string,
excluded: Record<string, string[]>
): boolean => {
const providerKey = normalizeProviderKey(providerType);
const excludedModels = excluded[providerKey] || excluded[providerType] || [];
return excludedModels.some((pattern) => {
if (pattern.includes('*')) {
// 支持通配符匹配:先转义正则特殊字符,再将 * 视为通配符
const regexSafePattern = pattern
.split('*')
.map((segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('.*');
const regex = new RegExp(`^${regexSafePattern}$`, 'i');
return regex.test(modelId);
}
return pattern.toLowerCase() === modelId.toLowerCase();
});
};