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(['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 = { 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(); 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 ): 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(); }); };