import { formatCurrencyFromUSD, formatQuotaWithCurrency } from '@/lib/currency' import dayjs from '@/lib/dayjs' import { formatTimestampToDate } from '@/lib/format' import { CHANNEL_STATUS_CONFIG, CHANNEL_TYPES, MULTI_KEY_STATUS_CONFIG, RESPONSE_TIME_CONFIG, RESPONSE_TIME_THRESHOLDS, TYPE_TO_KEY_PROMPT, } from '../constants' import type { Channel, ChannelSettings, ChannelOtherSettings } from '../types' // ============================================================================ // Channel Type Utilities // ============================================================================ /** * Get human-readable channel type label */ export function getChannelTypeLabel(type: number): string { return CHANNEL_TYPES[type as keyof typeof CHANNEL_TYPES] || 'Unknown' } /** * Get channel type icon name for getLobeIcon * Maps channel types to Lobe icon names using type number (language-independent) */ export function getChannelTypeIcon(type: number): string { const TYPE_TO_ICON: Record = { // OpenAI family 1: 'OpenAI', // OpenAI 6: 'OpenAI', // OpenAIMax 7: 'OpenAI', // OhMyGPT 8: 'OpenAI', // Custom 3: 'Azure', // Azure // Anthropic 14: 'Claude', // Anthropic // Google family 24: 'Gemini', // Gemini 11: 'Google', // PaLM 41: 'Gemini', // Vertex AI // Cloud providers 33: 'Aws', // AWS 39: 'Cloudflare', // Cloudflare // Chinese providers 15: 'Baidu', // Baidu 46: 'Baidu', // Baidu V2 16: 'Zhipu', // Zhipu 26: 'Zhipu', // Zhipu V4 17: 'Qwen', // Ali 18: 'Spark', // Xunfei 23: 'Hunyuan', // Tencent 19: 'Ai360', // 360 25: 'Moonshot', // Moonshot 31: 'Yi', // LingYiWanWu 35: 'Minimax', // MiniMax 45: 'Volcengine', // VolcEngine // Other AI providers 4: 'Ollama', // Ollama 27: 'Perplexity', // Perplexity 34: 'Cohere', // Cohere 42: 'Mistral', // Mistral 43: 'DeepSeek', // DeepSeek 48: 'XAI', // xAI 49: 'Coze', // Coze 40: 'SiliconCloud', // SiliconFlow 44: 'OpenAI', // MokaAI 20: 'OpenRouter', // OpenRouter // Image/Video generation 2: 'Midjourney', // Midjourney 5: 'Midjourney', // MidjourneyPlus 50: 'Kling', // Kling 51: 'Jimeng', // Jimeng 52: 'Vidu', // Vidu 36: 'Suno', // SunoAPI 55: 'OpenAI', // Sora 54: 'Doubao', // DoubaoVideo 56: 'Replicate', // Replicate // Tools & Platforms 37: 'Dify', // Dify 38: 'Jina', // Jina 22: 'FastGPT', // FastGPT 47: 'Xinference', // Xinference 53: 'OpenAI', // Submodel // AI Proxy services 10: 'OpenAI', // AI Proxy 21: 'OpenAI', // AI Proxy Library 12: 'OpenAI', // API2GPT 13: 'OpenAI', // AIGC2D 9: 'OpenAI', // AILS } return TYPE_TO_ICON[type] || 'OpenAI' } // ============================================================================ // Status Utilities // ============================================================================ /** * Get status badge configuration */ export function getChannelStatusBadge(status: number) { return ( CHANNEL_STATUS_CONFIG[status as keyof typeof CHANNEL_STATUS_CONFIG] || CHANNEL_STATUS_CONFIG[0] ) } /** * Get multi-key status badge configuration */ export function getMultiKeyStatusBadge(status: number) { return ( MULTI_KEY_STATUS_CONFIG[status as keyof typeof MULTI_KEY_STATUS_CONFIG] || MULTI_KEY_STATUS_CONFIG[1] ) } /** * Check if channel is enabled */ export function isChannelEnabled(channel: Channel): boolean { return channel.status === 1 } /** * Check if channel is multi-key */ export function isMultiKeyChannel(channel: Channel): boolean { return channel.channel_info?.is_multi_key || false } // ============================================================================ // Key Formatting // ============================================================================ /** * Format channel key for display * Masks the key for security, showing only first and last few characters */ export function formatChannelKey( key: string, isMultiKey: boolean = false ): string { if (!key) return '' if (isMultiKey) { const keys = key.split('\n').filter((k) => k.trim()) return `${keys.length} keys` } if (key.length <= 16) { // For short keys, mask middle part return `${key.slice(0, 4)}...${key.slice(-4)}` } // For longer keys, show more context return `${key.slice(0, 8)}...${key.slice(-8)}` } /** * Format key preview for multi-key display */ export function formatKeyPreview(key: string, maxLength: number = 10): string { if (!key) return '' if (key.length <= maxLength) return key return `${key.slice(0, maxLength)}...` } /** * Count keys in multi-key string */ export function countKeys(key: string): number { if (!key) return 0 return key.split('\n').filter((k) => k.trim()).length } // ============================================================================ // Model & Group Parsing // ============================================================================ /** * Parse comma-separated models list */ export function parseModelsList(models: string): string[] { if (!models) return [] return models .split(',') .map((m) => m.trim()) .filter((m) => m.length > 0) } /** * Parse comma-separated groups list. * Sorts with 'default' group first, then locale-sorted alphabetically. */ export function parseGroupsList(groups: string): string[] { if (!groups) return [] const list = groups .split(',') .map((g) => g.trim()) .filter((g) => g.length > 0) return list.sort((a, b) => { if (a === 'default') return -1 if (b === 'default') return 1 return a.localeCompare(b) }) } /** * Format models array back to string */ export function formatModelsString(models: string[]): string { return models.join(',') } /** * Format groups array back to string */ export function formatGroupsString(groups: string[]): string { return groups.join(',') } // ============================================================================ // Settings Parsing // ============================================================================ /** * Parse channel settings JSON */ export function parseChannelSettings( settingStr: string | null | undefined ): ChannelSettings { if (!settingStr) return {} try { return JSON.parse(settingStr) as ChannelSettings } catch { return {} } } /** * Parse channel other settings JSON */ export function parseChannelOtherSettings( settingsStr: string | null | undefined ): ChannelOtherSettings { if (!settingsStr || settingsStr === '{}') return {} try { return JSON.parse(settingsStr) as ChannelOtherSettings } catch { return {} } } /** * Validate JSON string */ export function validateChannelSettings(settings: string): boolean { if (!settings || settings.trim() === '') return true try { JSON.parse(settings) return true } catch { return false } } // ============================================================================ // Balance Formatting // ============================================================================ /** * Format balance with currency symbol */ export function formatBalance(balance: number | null | undefined): string { if (balance == null || Number.isNaN(balance)) return '-' return formatCurrencyFromUSD(balance, { digitsLarge: 2, digitsSmall: 4, abbreviate: false, }) } /** * Get balance status color */ export function getBalanceVariant( balance: number ): 'success' | 'warning' | 'danger' | 'neutral' { if (balance === 0) return 'neutral' if (balance < 1) return 'danger' if (balance < 10) return 'warning' return 'success' } // ============================================================================ // Response Time Utilities // ============================================================================ /** Optional i18n: (key, options) => string, e.g. useTranslation().t */ type TFunction = (key: string, options?: { value?: number | string }) => string /** * Format response time in milliseconds to human-readable. * Pass `t` from useTranslation() for i18n (e.g. "Not tested", "{{value}}ms", "{{value}}s"). */ export function formatResponseTime(timeMs: number, t?: TFunction): string { if (timeMs === 0) return t ? t('Not tested') : 'Not tested' if (timeMs < 1000) return t ? t('{{value}}ms', { value: timeMs }) : `${timeMs}ms` return t ? t('{{value}}s', { value: (timeMs / 1000).toFixed(2) }) : `${(timeMs / 1000).toFixed(2)}s` } /** * Get response time performance rating */ export function getResponseTimeConfig(timeMs: number) { if (timeMs === 0) return RESPONSE_TIME_CONFIG.UNKNOWN if (timeMs <= RESPONSE_TIME_THRESHOLDS.EXCELLENT) return RESPONSE_TIME_CONFIG.EXCELLENT if (timeMs <= RESPONSE_TIME_THRESHOLDS.GOOD) return RESPONSE_TIME_CONFIG.GOOD if (timeMs <= RESPONSE_TIME_THRESHOLDS.FAIR) return RESPONSE_TIME_CONFIG.FAIR if (timeMs <= RESPONSE_TIME_THRESHOLDS.POOR) return RESPONSE_TIME_CONFIG.POOR return RESPONSE_TIME_CONFIG.POOR } // ============================================================================ // Time Formatting // ============================================================================ /** * Format Unix timestamp to relative time * e.g., "2 hours ago", "3 days ago" */ export function formatRelativeTime(timestamp: number): string { if (!timestamp || timestamp === 0) return 'Never' try { return dayjs(timestamp * 1000).fromNow() } catch { return 'Unknown' } } /** * Format Unix timestamp to date string */ export function formatTimestamp(timestamp: number): string { if (!timestamp || timestamp === 0) return 'N/A' try { return formatTimestampToDate(timestamp) } catch { return 'Invalid date' } } // ============================================================================ // Quota Formatting // ============================================================================ /** Format quota units using the global currency display configuration. */ export function formatQuota(quota: number): string { return formatQuotaWithCurrency(quota, { digitsLarge: 2, digitsSmall: 4, abbreviate: true, }) } // ============================================================================ // Priority & Weight Utilities // ============================================================================ /** * Get priority display value */ export function getPriorityDisplay( priority: number | null | undefined ): string { if (priority === null || priority === undefined) return '0' return String(priority) } /** * Get weight display value */ export function getWeightDisplay(weight: number | null | undefined): string { if (weight === null || weight === undefined) return '0' return String(weight) } // ============================================================================ // Validation Utilities // ============================================================================ /** * Validate channel name */ export function validateChannelName(name: string): boolean { return name.trim().length > 0 } /** * Validate API key format */ export function validateApiKey(key: string): boolean { return key.trim().length > 0 } /** * Validate models list */ export function validateModels(models: string): boolean { return parseModelsList(models).length > 0 } /** * Validate groups list */ export function validateGroups(groups: string): boolean { return parseGroupsList(groups).length > 0 } /** * Check if channel needs attention (low balance, auto-disabled, etc.) */ export function channelNeedsAttention(channel: Channel): boolean { // Auto-disabled if (channel.status === 3) return true // Low balance (less than $1) if (channel.balance > 0 && channel.balance < 1) return true // Multi-key channel with all keys disabled if ( channel.channel_info?.is_multi_key && channel.channel_info.multi_key_status_list && Object.keys(channel.channel_info.multi_key_status_list).length >= channel.channel_info.multi_key_size ) { return true } return false } /** * Get attention reason for channel */ export function getAttentionReason(channel: Channel): string | null { if (channel.status === 3) return 'Auto-disabled' if (channel.balance > 0 && channel.balance < 1) return 'Low balance' if ( channel.channel_info?.is_multi_key && channel.channel_info.multi_key_status_list && Object.keys(channel.channel_info.multi_key_status_list).length >= channel.channel_info.multi_key_size ) { return 'All keys disabled' } return null } // ============================================================================ // Tag Aggregation Utilities // ============================================================================ /** * Tag row type (extends Channel with children) */ export type TagRow = Channel & { children: Channel[] } /** * Type guard to check whether a row is a tag aggregate row */ export function isTagAggregateRow(row: Channel | TagRow): row is TagRow { return Array.isArray((row as TagRow).children) } /** * Aggregate channels by tag for tag mode display * Converts flat array into tree structure grouped by tag */ export function aggregateChannelsByTag( channels: Channel[] ): (Channel | TagRow)[] { const tagMap = new Map() const result: (Channel | TagRow)[] = [] for (const channel of channels) { const tag = channel.tag || '' if (!tagMap.has(tag)) { // Create tag aggregate row const tagRow = { ...channel, key: tag, id: tag as unknown as number, tag: tag, name: tag, type: 0, status: undefined as unknown as number, group: '', used_quota: 0, response_time: 0, priority: -1 as unknown as number | null, weight: -1 as unknown as number | null, balance: 0, test_time: 0, created_time: 0, balance_updated_time: 0, models: '', children: [], } as TagRow tagMap.set(tag, tagRow) result.push(tagRow) } const tagRow = tagMap.get(tag)! // Add to children tagRow.children.push(channel) const childCount = tagRow.children.length // Aggregate used_quota (sum) tagRow.used_quota += channel.used_quota // Aggregate response_time (average) tagRow.response_time = (tagRow.response_time * (childCount - 1) + channel.response_time) / childCount // Aggregate priority (same value or null if different) if (tagRow.priority === -1) { tagRow.priority = channel.priority } else if (tagRow.priority !== channel.priority) { tagRow.priority = null } // Aggregate weight (same value or null if different) if (tagRow.weight === -1) { tagRow.weight = channel.weight } else if (tagRow.weight !== channel.weight) { tagRow.weight = null } // Aggregate group (concatenate and deduplicate) if (tagRow.group === '') { tagRow.group = channel.group } else { const existingGroups = new Set(tagRow.group.split(',').filter(Boolean)) const newGroups = channel.group.split(',').filter(Boolean) newGroups.forEach((g) => { if (!existingGroups.has(g)) { tagRow.group += ',' + g } }) } // Aggregate status (enabled if any child is enabled) if (channel.status === 1) { tagRow.status = 1 } else if (tagRow.status === undefined) { tagRow.status = channel.status } } return result } // ============================================================================ // Key Management Utilities // ============================================================================ /** * Deduplicate keys from a multiline string * @param keysText - Text with one key per line * @returns Object with deduplicated keys and statistics */ export function deduplicateKeys(keysText: string): { deduplicatedText: string beforeCount: number afterCount: number removedCount: number } { if (!keysText || keysText.trim() === '') { return { deduplicatedText: '', beforeCount: 0, afterCount: 0, removedCount: 0, } } // Split by lines const keyLines = keysText.split('\n') const beforeCount = keyLines.length // Use Set for deduplication, maintaining order const keySet = new Set() const deduplicatedKeys: string[] = [] keyLines.forEach((line) => { const trimmedLine = line.trim() if (trimmedLine && !keySet.has(trimmedLine)) { keySet.add(trimmedLine) deduplicatedKeys.push(trimmedLine) } }) const afterCount = deduplicatedKeys.length const deduplicatedText = deduplicatedKeys.join('\n') return { deduplicatedText, beforeCount, afterCount, removedCount: beforeCount - afterCount, } } /** * Get key prompt based on channel type */ export function getKeyPromptForType(type: number): string { return TYPE_TO_KEY_PROMPT[type] || 'Enter API key for this channel' }