refactor(core): harden API parsing and improve type safety

This commit is contained in:
LTbinglingfeng
2026-02-08 09:42:00 +08:00
parent 3783bec983
commit 6c2cd761ba
39 changed files with 689 additions and 404 deletions

View File

@@ -10,7 +10,10 @@ import type {
import type { Config } from '@/types/config';
import { buildHeaderObject } from '@/utils/headers';
const normalizeBoolean = (value: any): boolean | undefined => {
const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const normalizeBoolean = (value: unknown): boolean | undefined => {
if (value === undefined || value === null) return undefined;
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
@@ -22,11 +25,17 @@ const normalizeBoolean = (value: any): boolean | undefined => {
return Boolean(value);
};
const normalizeModelAliases = (models: any): ModelAlias[] => {
const normalizeModelAliases = (models: unknown): ModelAlias[] => {
if (!Array.isArray(models)) return [];
return models
.map((item) => {
if (!item) return null;
if (item === undefined || item === null) return null;
if (typeof item === 'string') {
const trimmed = item.trim();
return trimmed ? ({ name: trimmed } satisfies ModelAlias) : null;
}
if (!isRecord(item)) return null;
const name = item.name || item.id || item.model;
if (!name) return null;
const alias = item.alias || item.display_name || item.displayName;
@@ -37,7 +46,10 @@ const normalizeModelAliases = (models: any): ModelAlias[] => {
entry.alias = String(alias);
}
if (priority !== undefined) {
entry.priority = Number(priority);
const parsed = Number(priority);
if (Number.isFinite(parsed)) {
entry.priority = parsed;
}
}
if (testModel) {
entry.testModel = String(testModel);
@@ -47,13 +59,17 @@ const normalizeModelAliases = (models: any): ModelAlias[] => {
.filter(Boolean) as ModelAlias[];
};
const normalizeHeaders = (headers: any) => {
const normalizeHeaders = (headers: unknown) => {
if (!headers || typeof headers !== 'object') return undefined;
const normalized = buildHeaderObject(headers as Record<string, string>);
const normalized = buildHeaderObject(
Array.isArray(headers)
? (headers as Array<{ key: string; value: string }>)
: (headers as Record<string, string | undefined | null>)
);
return Object.keys(normalized).length ? normalized : undefined;
};
const normalizeExcludedModels = (input: any): string[] => {
const normalizeExcludedModels = (input: unknown): string[] => {
const rawList = Array.isArray(input) ? input : typeof input === 'string' ? input.split(/[\n,]/) : [];
const seen = new Set<string>();
const normalized: string[] = [];
@@ -70,20 +86,22 @@ const normalizeExcludedModels = (input: any): string[] => {
return normalized;
};
const normalizePrefix = (value: any): string | undefined => {
const normalizePrefix = (value: unknown): string | undefined => {
if (value === undefined || value === null) return undefined;
const trimmed = String(value).trim();
return trimmed ? trimmed : undefined;
};
const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => {
if (!entry) return null;
const apiKey = entry['api-key'] ?? entry.apiKey ?? entry.key ?? (typeof entry === 'string' ? entry : '');
const normalizeApiKeyEntry = (entry: unknown): ApiKeyEntry | null => {
if (entry === undefined || entry === null) return null;
const record = isRecord(entry) ? entry : null;
const apiKey =
record?.['api-key'] ?? record?.apiKey ?? record?.key ?? (typeof entry === 'string' ? entry : '');
const trimmed = String(apiKey || '').trim();
if (!trimmed) return null;
const proxyUrl = entry['proxy-url'] ?? entry.proxyUrl;
const headers = normalizeHeaders(entry.headers);
const proxyUrl = record ? record['proxy-url'] ?? record.proxyUrl : undefined;
const headers = record ? normalizeHeaders(record.headers) : undefined;
return {
apiKey: trimmed,
@@ -92,33 +110,38 @@ const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => {
};
};
const normalizeProviderKeyConfig = (item: any): ProviderKeyConfig | null => {
if (!item) return null;
const apiKey = item['api-key'] ?? item.apiKey ?? (typeof item === 'string' ? item : '');
const normalizeProviderKeyConfig = (item: unknown): ProviderKeyConfig | null => {
if (item === undefined || item === null) return null;
const record = isRecord(item) ? item : null;
const apiKey = record?.['api-key'] ?? record?.apiKey ?? (typeof item === 'string' ? item : '');
const trimmed = String(apiKey || '').trim();
if (!trimmed) return null;
const config: ProviderKeyConfig = { apiKey: trimmed };
const prefix = normalizePrefix(item.prefix ?? item['prefix']);
const prefix = normalizePrefix(record?.prefix ?? record?.['prefix']);
if (prefix) config.prefix = prefix;
const baseUrl = item['base-url'] ?? item.baseUrl;
const proxyUrl = item['proxy-url'] ?? item.proxyUrl;
const baseUrl = record ? record['base-url'] ?? record.baseUrl : undefined;
const proxyUrl = record ? record['proxy-url'] ?? record.proxyUrl : undefined;
if (baseUrl) config.baseUrl = String(baseUrl);
if (proxyUrl) config.proxyUrl = String(proxyUrl);
const headers = normalizeHeaders(item.headers);
const headers = normalizeHeaders(record?.headers);
if (headers) config.headers = headers;
const models = normalizeModelAliases(item.models);
const models = normalizeModelAliases(record?.models);
if (models.length) config.models = models;
const excludedModels = normalizeExcludedModels(
item['excluded-models'] ?? item.excludedModels ?? item['excluded_models'] ?? item.excluded_models
record?.['excluded-models'] ??
record?.excludedModels ??
record?.['excluded_models'] ??
record?.excluded_models
);
if (excludedModels.length) config.excludedModels = excludedModels;
return config;
};
const normalizeGeminiKeyConfig = (item: any): GeminiKeyConfig | null => {
if (!item) return null;
let apiKey = item['api-key'] ?? item.apiKey;
const normalizeGeminiKeyConfig = (item: unknown): GeminiKeyConfig | null => {
if (item === undefined || item === null) return null;
const record = isRecord(item) ? item : null;
let apiKey = record?.['api-key'] ?? record?.apiKey;
if (!apiKey && typeof item === 'string') {
apiKey = item;
}
@@ -126,19 +149,19 @@ const normalizeGeminiKeyConfig = (item: any): GeminiKeyConfig | null => {
if (!trimmed) return null;
const config: GeminiKeyConfig = { apiKey: trimmed };
const prefix = normalizePrefix(item.prefix ?? item['prefix']);
const prefix = normalizePrefix(record?.prefix ?? record?.['prefix']);
if (prefix) config.prefix = prefix;
const baseUrl = item['base-url'] ?? item.baseUrl ?? item['base_url'];
const baseUrl = record ? record['base-url'] ?? record.baseUrl ?? record['base_url'] : undefined;
if (baseUrl) config.baseUrl = String(baseUrl);
const headers = normalizeHeaders(item.headers);
const headers = normalizeHeaders(record?.headers);
if (headers) config.headers = headers;
const excludedModels = normalizeExcludedModels(item['excluded-models'] ?? item.excludedModels);
const excludedModels = normalizeExcludedModels(record?.['excluded-models'] ?? record?.excludedModels);
if (excludedModels.length) config.excludedModels = excludedModels;
return config;
};
const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null => {
if (!provider || typeof provider !== 'object') return null;
const normalizeOpenAIProvider = (provider: unknown): OpenAIProviderConfig | null => {
if (!isRecord(provider)) return null;
const name = provider.name || provider.id;
const baseUrl = provider['base-url'] ?? provider.baseUrl;
if (!name || !baseUrl) return null;
@@ -146,11 +169,11 @@ const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null =>
let apiKeyEntries: ApiKeyEntry[] = [];
if (Array.isArray(provider['api-key-entries'])) {
apiKeyEntries = provider['api-key-entries']
.map((entry: any) => normalizeApiKeyEntry(entry))
.map((entry) => normalizeApiKeyEntry(entry))
.filter(Boolean) as ApiKeyEntry[];
} else if (Array.isArray(provider['api-keys'])) {
apiKeyEntries = provider['api-keys']
.map((key: any) => normalizeApiKeyEntry({ 'api-key': key }))
.map((key) => normalizeApiKeyEntry({ 'api-key': key }))
.filter(Boolean) as ApiKeyEntry[];
}
@@ -174,10 +197,10 @@ const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null =>
return result;
};
const normalizeOauthExcluded = (payload: any): Record<string, string[]> | undefined => {
if (!payload || typeof payload !== 'object') return undefined;
const normalizeOauthExcluded = (payload: unknown): Record<string, string[]> | undefined => {
if (!isRecord(payload)) return undefined;
const source = payload['oauth-excluded-models'] ?? payload.items ?? payload;
if (!source || typeof source !== 'object') return undefined;
if (!isRecord(source)) return undefined;
const map: Record<string, string[]> = {};
Object.entries(source).forEach(([provider, models]) => {
const key = String(provider || '').trim();
@@ -188,13 +211,13 @@ const normalizeOauthExcluded = (payload: any): Record<string, string[]> | undefi
return map;
};
const normalizeAmpcodeModelMappings = (input: any): AmpcodeModelMapping[] => {
const normalizeAmpcodeModelMappings = (input: unknown): AmpcodeModelMapping[] => {
if (!Array.isArray(input)) return [];
const seen = new Set<string>();
const mappings: AmpcodeModelMapping[] = [];
input.forEach((entry) => {
if (!entry || typeof entry !== 'object') return;
if (!isRecord(entry)) return;
const from = String(entry.from ?? entry['from'] ?? '').trim();
const to = String(entry.to ?? entry['to'] ?? '').trim();
if (!from || !to) return;
@@ -207,9 +230,10 @@ const normalizeAmpcodeModelMappings = (input: any): AmpcodeModelMapping[] => {
return mappings;
};
const normalizeAmpcodeConfig = (payload: any): AmpcodeConfig | undefined => {
const source = payload?.ampcode ?? payload;
if (!source || typeof source !== 'object') return undefined;
const normalizeAmpcodeConfig = (payload: unknown): AmpcodeConfig | undefined => {
const sourceRaw = isRecord(payload) ? (payload.ampcode ?? payload) : payload;
if (!isRecord(sourceRaw)) return undefined;
const source = sourceRaw;
const config: AmpcodeConfig = {};
const upstreamUrl = source['upstream-url'] ?? source.upstreamUrl ?? source['upstream_url'];
@@ -237,70 +261,94 @@ const normalizeAmpcodeConfig = (payload: any): AmpcodeConfig | undefined => {
/**
* 规范化 /config 返回值
*/
export const normalizeConfigResponse = (raw: any): Config => {
const config: Config = { raw: raw || {} };
if (!raw || typeof raw !== 'object') {
export const normalizeConfigResponse = (raw: unknown): Config => {
const config: Config = { raw: isRecord(raw) ? raw : {} };
if (!isRecord(raw)) {
return config;
}
config.debug = raw.debug;
config.proxyUrl = raw['proxy-url'] ?? raw.proxyUrl;
config.requestRetry = raw['request-retry'] ?? raw.requestRetry;
config.debug = normalizeBoolean(raw.debug);
const proxyUrl = raw['proxy-url'] ?? raw.proxyUrl;
config.proxyUrl =
typeof proxyUrl === 'string' ? proxyUrl : proxyUrl === undefined || proxyUrl === null ? undefined : String(proxyUrl);
const requestRetry = raw['request-retry'] ?? raw.requestRetry;
if (typeof requestRetry === 'number' && Number.isFinite(requestRetry)) {
config.requestRetry = requestRetry;
} else if (typeof requestRetry === 'string' && requestRetry.trim() !== '') {
const parsed = Number(requestRetry);
if (Number.isFinite(parsed)) {
config.requestRetry = parsed;
}
}
const quota = raw['quota-exceeded'] ?? raw.quotaExceeded;
if (quota && typeof quota === 'object') {
if (isRecord(quota)) {
config.quotaExceeded = {
switchProject: quota['switch-project'] ?? quota.switchProject,
switchPreviewModel: quota['switch-preview-model'] ?? quota.switchPreviewModel
switchProject: normalizeBoolean(quota['switch-project'] ?? quota.switchProject),
switchPreviewModel: normalizeBoolean(quota['switch-preview-model'] ?? quota.switchPreviewModel)
};
}
config.usageStatisticsEnabled = raw['usage-statistics-enabled'] ?? raw.usageStatisticsEnabled;
config.requestLog = raw['request-log'] ?? raw.requestLog;
config.loggingToFile = raw['logging-to-file'] ?? raw.loggingToFile;
config.logsMaxTotalSizeMb = raw['logs-max-total-size-mb'] ?? raw.logsMaxTotalSizeMb;
config.wsAuth = raw['ws-auth'] ?? raw.wsAuth;
config.forceModelPrefix = raw['force-model-prefix'] ?? raw.forceModelPrefix;
const routing = raw.routing;
if (routing && typeof routing === 'object') {
config.routingStrategy = routing.strategy ?? routing['strategy'];
} else {
config.routingStrategy = raw['routing-strategy'] ?? raw.routingStrategy;
config.usageStatisticsEnabled = normalizeBoolean(
raw['usage-statistics-enabled'] ?? raw.usageStatisticsEnabled
);
config.requestLog = normalizeBoolean(raw['request-log'] ?? raw.requestLog);
config.loggingToFile = normalizeBoolean(raw['logging-to-file'] ?? raw.loggingToFile);
const logsMaxTotalSizeMb = raw['logs-max-total-size-mb'] ?? raw.logsMaxTotalSizeMb;
if (typeof logsMaxTotalSizeMb === 'number' && Number.isFinite(logsMaxTotalSizeMb)) {
config.logsMaxTotalSizeMb = logsMaxTotalSizeMb;
} else if (typeof logsMaxTotalSizeMb === 'string' && logsMaxTotalSizeMb.trim() !== '') {
const parsed = Number(logsMaxTotalSizeMb);
if (Number.isFinite(parsed)) {
config.logsMaxTotalSizeMb = parsed;
}
}
config.wsAuth = normalizeBoolean(raw['ws-auth'] ?? raw.wsAuth);
config.forceModelPrefix = normalizeBoolean(raw['force-model-prefix'] ?? raw.forceModelPrefix);
const routing = raw.routing;
const strategyRaw = isRecord(routing)
? (routing.strategy ?? routing['strategy'])
: (raw['routing-strategy'] ?? raw.routingStrategy);
if (strategyRaw !== undefined && strategyRaw !== null) {
config.routingStrategy = String(strategyRaw);
}
const apiKeysRaw = raw['api-keys'] ?? raw.apiKeys;
if (Array.isArray(apiKeysRaw)) {
config.apiKeys = apiKeysRaw.map((key) => String(key)).filter((key) => key.trim() !== '');
}
config.apiKeys = Array.isArray(raw['api-keys']) ? raw['api-keys'].slice() : raw.apiKeys;
const geminiList = raw['gemini-api-key'] ?? raw.geminiApiKey ?? raw.geminiApiKeys;
if (Array.isArray(geminiList)) {
config.geminiApiKeys = geminiList
.map((item: any) => normalizeGeminiKeyConfig(item))
.map((item) => normalizeGeminiKeyConfig(item))
.filter(Boolean) as GeminiKeyConfig[];
}
const codexList = raw['codex-api-key'] ?? raw.codexApiKey ?? raw.codexApiKeys;
if (Array.isArray(codexList)) {
config.codexApiKeys = codexList
.map((item: any) => normalizeProviderKeyConfig(item))
.map((item) => normalizeProviderKeyConfig(item))
.filter(Boolean) as ProviderKeyConfig[];
}
const claudeList = raw['claude-api-key'] ?? raw.claudeApiKey ?? raw.claudeApiKeys;
if (Array.isArray(claudeList)) {
config.claudeApiKeys = claudeList
.map((item: any) => normalizeProviderKeyConfig(item))
.map((item) => normalizeProviderKeyConfig(item))
.filter(Boolean) as ProviderKeyConfig[];
}
const vertexList = raw['vertex-api-key'] ?? raw.vertexApiKey ?? raw.vertexApiKeys;
if (Array.isArray(vertexList)) {
config.vertexApiKeys = vertexList
.map((item: any) => normalizeProviderKeyConfig(item))
.map((item) => normalizeProviderKeyConfig(item))
.filter(Boolean) as ProviderKeyConfig[];
}
const openaiList = raw['openai-compatibility'] ?? raw.openaiCompatibility ?? raw.openAICompatibility;
if (Array.isArray(openaiList)) {
config.openaiCompatibility = openaiList
.map((item: any) => normalizeOpenAIProvider(item))
.map((item) => normalizeOpenAIProvider(item))
.filter(Boolean) as OpenAIProviderConfig[];
}