Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,268 @@
import type { TFunction } from 'i18next';
import iconAntigravity from '@/assets/icons/antigravity.svg';
import iconClaude from '@/assets/icons/claude.svg';
import iconCodex from '@/assets/icons/codex.svg';
import iconGemini from '@/assets/icons/gemini.svg';
import iconGrok from '@/assets/icons/grok.svg';
import iconGrokDark from '@/assets/icons/grok-dark.svg';
import iconIflow from '@/assets/icons/iflow.svg';
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
import iconKimiLight from '@/assets/icons/kimi-light.svg';
import iconQwen from '@/assets/icons/qwen.svg';
import iconVertex from '@/assets/icons/vertex.svg';
import type { AuthFileItem, ResolvedTheme, ThemeColors } from '@/types';
import { normalizeOAuthProviderKey } from '@/utils/providerKeys';
import { parseTimestamp } from '@/utils/timestamp';
import { TYPE_COLORS } from '@/utils/quota';
export type { ResolvedTheme, ThemeColors, TypeColorSet } from '@/types';
export type AuthFileModelItem = {
id: string;
display_name?: string;
type?: string;
owned_by?: string;
};
export type AuthFileIconAsset = string | { light: string; dark: string };
export type QuotaProviderType = 'antigravity' | 'claude' | 'codex' | 'kimi' | 'xai';
export type OAuthConfigLoadError = 'loading' | 'unsupported' | 'load' | null;
export const QUOTA_PROVIDER_TYPES = new Set<QuotaProviderType>([
'antigravity',
'claude',
'codex',
'kimi',
'xai',
]);
export const OAUTH_PROVIDER_PRESETS = [
'vertex',
'aistudio',
'antigravity',
'xai',
'claude',
'codex',
'kimi',
];
const OAUTH_PROVIDER_EXCLUDES = new Set(['all', 'unknown', 'empty']);
export const MIN_CARD_PAGE_SIZE = 3;
export const MAX_CARD_PAGE_SIZE = 30;
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']);
export const AUTH_FILE_WEBSOCKET_PROVIDERS = new Set(['codex', 'xai']);
export const AUTH_FILE_USING_API_PROVIDERS = new Set(['xai']);
export const AUTH_FILE_MANUAL_REFRESH_PROVIDERS = new Set([
'antigravity',
'claude',
'codex',
'kimi',
'xai',
]);
// 标签类型颜色配置:权威版本在 @/utils/quota/constants.ts此处仅转发
export { TYPE_COLORS } from '@/utils/quota';
export const AUTH_FILE_ICONS: Record<string, AuthFileIconAsset> = {
antigravity: iconAntigravity,
aistudio: iconGemini,
claude: iconClaude,
codex: iconCodex,
gemini: iconGemini,
xai: { light: iconGrok, dark: iconGrokDark },
iflow: iconIflow,
kimi: { light: iconKimiDark, dark: iconKimiLight },
qwen: iconQwen,
vertex: iconVertex,
};
export const clampCardPageSize = (value: number) =>
Math.min(MAX_CARD_PAGE_SIZE, Math.max(MIN_CARD_PAGE_SIZE, Math.round(value)));
export const normalizeProviderKey = normalizeOAuthProviderKey;
export const supportsAuthFileManualRefresh = (provider: unknown): boolean =>
AUTH_FILE_MANUAL_REFRESH_PROVIDERS.has(normalizeProviderKey(String(provider ?? '')));
export const buildOAuthProviderOptions = (values: Iterable<unknown>): string[] => {
const extraProviders = new Set<string>();
Array.from(values).forEach((value) => {
const key = normalizeProviderKey(String(value ?? ''));
if (!key || OAUTH_PROVIDER_EXCLUDES.has(key)) return;
extraProviders.add(key);
});
const baseSet = new Set(OAUTH_PROVIDER_PRESETS.map((value) => normalizeProviderKey(value)));
const extraList = Array.from(extraProviders)
.filter((value) => !baseSet.has(value))
.sort((a, b) => a.localeCompare(b));
return [...OAUTH_PROVIDER_PRESETS, ...extraList];
};
export const getAuthFileStatusMessage = (file: AuthFileItem): string => {
const raw = file['status_message'] ?? file.statusMessage;
if (typeof raw === 'string') return raw.trim();
if (raw == null) return '';
return String(raw).trim();
};
/** 这些 status_message 视为健康,不触发告警态。 */
export const HEALTHY_AUTH_FILE_STATUS_MESSAGES = new Set([
'ok',
'healthy',
'ready',
'success',
'available',
]);
/** 是否存在非健康的 status_message卡片告警态 / 谱条琥珀色共用判定)。 */
export const hasAuthFileStatusWarning = (file: AuthFileItem): boolean => {
const message = getAuthFileStatusMessage(file);
return Boolean(message) && !HEALTHY_AUTH_FILE_STATUS_MESSAGES.has(message.toLowerCase());
};
/**
*
*
*/
export const isProblemAuthFile = (file: AuthFileItem): boolean => {
const status = typeof file.status === 'string' ? file.status.trim().toLowerCase() : '';
if (file.disabled === true || status === 'disabled') return false;
return file.unavailable === true || status === 'error' || hasAuthFileStatusWarning(file);
};
export const getTypeLabel = (t: TFunction, type: string): string => {
const providerKey = normalizeProviderKey(type);
const key = `auth_files.filter_${providerKey}`;
const translated = t(key);
if (translated !== key) return translated;
if (providerKey === 'iflow') return 'iFlow';
return type.charAt(0).toUpperCase() + type.slice(1);
};
export const getTypeColor = (type: string, resolvedTheme: ResolvedTheme): ThemeColors => {
const set = TYPE_COLORS[normalizeProviderKey(type)] || TYPE_COLORS.unknown;
return resolvedTheme === 'dark' && set.dark ? set.dark : set.light;
};
export const getAuthFileIcon = (type: string, resolvedTheme: ResolvedTheme): string | null => {
const iconEntry = AUTH_FILE_ICONS[normalizeProviderKey(type)];
if (!iconEntry) return null;
return typeof iconEntry === 'string'
? iconEntry
: resolvedTheme === 'dark'
? iconEntry.dark
: iconEntry.light;
};
// 与 AI 提供商界面PROVIDER_LOGOS 的 themeSurface保持一致
// 这些提供商的图标底座颜色随主题切换(浅色主题黑底,深色主题白底)
export const THEME_SURFACE_ICON_PROVIDERS = new Set(['kimi']);
export const isThemeSurfaceIconProvider = (type: string): boolean =>
THEME_SURFACE_ICON_PROVIDERS.has(normalizeProviderKey(type));
export const getThemeSurfaceIconBackground = (resolvedTheme: ResolvedTheme): string =>
resolvedTheme === 'dark' ? '#ffffff' : '#000000';
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 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;
};
export const readAuthFileDisableCooling = (value: Record<string, unknown>): boolean => {
const canonical = parseDisableCoolingValue(value.disable_cooling);
if (canonical !== undefined) return canonical;
return parseDisableCoolingValue(value['disable-cooling']) ?? false;
};
export const supportsAuthFileWebsockets = (providerKey: string): boolean =>
AUTH_FILE_WEBSOCKET_PROVIDERS.has(normalizeProviderKey(providerKey));
export const readAuthFileWebsockets = (value: Record<string, unknown>): boolean =>
parseDisableCoolingValue(value.websockets ?? value.websocket) ?? false;
export const applyAuthFileWebsockets = (
value: Record<string, unknown>,
websockets: boolean
): Record<string, unknown> => {
const next = { ...value };
delete next.websocket;
next.websockets = websockets;
return next;
};
export const supportsAuthFileUsingApi = (providerKey: string): boolean =>
AUTH_FILE_USING_API_PROVIDERS.has(normalizeProviderKey(providerKey));
export const readAuthFileUsingApi = (value: Record<string, unknown>): boolean =>
parseDisableCoolingValue(value.using_api) ?? false;
export const applyAuthFileUsingApi = (
value: Record<string, unknown>,
usingApi: boolean
): Record<string, unknown> => ({ ...value, using_api: usingApi });
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 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)
: (parseTimestamp(raw) ?? 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();
});
};