mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
237 lines
8.0 KiB
TypeScript
237 lines
8.0 KiB
TypeScript
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();
|
||
});
|
||
};
|