From 2bf721974bc545e410fd71e3098bfb33aa84cc5e Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:27:26 +0800 Subject: [PATCH] feat(auth): load model lists via /model-definitions/{channel} instead of per-file model sources. --- src/i18n/locales/en.json | 2 +- src/i18n/locales/zh-CN.json | 2 +- src/pages/AuthFilesPage.tsx | 229 +++++++++++++--------------------- src/services/api/authFiles.ts | 76 ++++------- src/types/oauth.ts | 6 +- 5 files changed, 114 insertions(+), 201 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0f6c411..36cd168 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -512,7 +512,7 @@ "upgrade_required_title": "Please upgrade CLI Proxy API", "upgrade_required_desc": "The current server does not support the OAuth excluded models API. Please upgrade to the latest CLI Proxy API (CPA) version." }, - "oauth_model_mappings": { + "oauth_model_alias": { "title": "OAuth Model Aliases", "add": "Add Alias", "add_title": "Add provider model aliases", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 819e63b..be4dd93 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -512,7 +512,7 @@ "upgrade_required_title": "需要升级 CPA 版本", "upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。" }, - "oauth_model_mappings": { + "oauth_model_alias": { "title": "OAuth 模型别名", "add": "新增别名", "add_title": "新增提供商模型别名", diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 58b2eb3..8a43efe 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -21,7 +21,7 @@ import { import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores'; import { authFilesApi, usageApi } from '@/services/api'; import { apiClient } from '@/services/api/client'; -import type { AuthFileItem, OAuthModelMappingEntry } from '@/types'; +import type { AuthFileItem, OAuthModelAliasEntry } from '@/types'; import { calculateStatusBarData, collectUsageDetails, @@ -107,9 +107,9 @@ interface ExcludedFormState { modelsText: string; } -type OAuthModelMappingFormEntry = OAuthModelMappingEntry & { id: string }; +type OAuthModelMappingFormEntry = OAuthModelAliasEntry & { id: string }; -interface ModelMappingsFormState { +interface ModelAliasFormState { provider: string; mappings: OAuthModelMappingFormEntry[]; } @@ -237,14 +237,13 @@ export function AuthFilesPage() { const [savingExcluded, setSavingExcluded] = useState(false); // OAuth 模型映射相关 - const [modelMappings, setModelMappings] = useState>({}); - const [modelMappingsError, setModelMappingsError] = useState<'unsupported' | null>(null); + const [modelAlias, setModelAlias] = useState>({}); + const [modelAliasError, setModelAliasError] = useState<'unsupported' | null>(null); const [mappingModalOpen, setMappingModalOpen] = useState(false); - const [mappingForm, setMappingForm] = useState({ + const [mappingForm, setMappingForm] = useState({ provider: '', mappings: [buildEmptyMappingEntry()], }); - const [mappingModelsFileName, setMappingModelsFileName] = useState(''); const [mappingModelsList, setMappingModelsList] = useState([]); const [mappingModelsLoading, setMappingModelsLoading] = useState(false); const [mappingModelsError, setMappingModelsError] = useState<'unsupported' | null>(null); @@ -265,55 +264,21 @@ export function AuthFilesPage() { setPageSizeInput(String(pageSize)); }, [pageSize]); - const modelSourceFileOptions = useMemo(() => { - const normalizedProvider = normalizeProviderKey(mappingForm.provider); - const matching: string[] = []; - const others: string[] = []; - const seen = new Set(); - - files.forEach((file) => { - const isRuntimeOnly = isRuntimeOnlyAuthFile(file); - const isAistudio = (file.type || '').toLowerCase() === 'aistudio'; - const canShowModels = !isRuntimeOnly || isAistudio; - if (!canShowModels) return; - - const fileName = String(file.name || '').trim(); - if (!fileName) return; - if (seen.has(fileName)) return; - seen.add(fileName); - - if (!normalizedProvider) { - matching.push(fileName); - return; - } - - const typeKey = normalizeProviderKey(String(file.type || '')); - const providerKey = normalizeProviderKey(String(file.provider || '')); - const isMatch = typeKey === normalizedProvider || providerKey === normalizedProvider; - if (isMatch) { - matching.push(fileName); - } else { - others.push(fileName); - } - }); - - matching.sort((a, b) => a.localeCompare(b)); - others.sort((a, b) => a.localeCompare(b)); - return [...matching, ...others]; - }, [files, mappingForm.provider]); + // 模型定义缓存(按 channel 缓存) + const modelDefinitionsCacheRef = useRef>(new Map()); useEffect(() => { if (!mappingModalOpen) return; - const fileName = mappingModelsFileName.trim(); - if (!fileName) { + const channel = normalizeProviderKey(mappingForm.provider); + if (!channel) { setMappingModelsList([]); setMappingModelsError(null); setMappingModelsLoading(false); return; } - const cached = modelsCacheRef.current.get(fileName); + const cached = modelDefinitionsCacheRef.current.get(channel); if (cached) { setMappingModelsList(cached); setMappingModelsError(null); @@ -326,10 +291,10 @@ export function AuthFilesPage() { setMappingModelsError(null); authFilesApi - .getModelsForAuthFile(fileName) + .getModelDefinitions(channel) .then((models) => { if (cancelled) return; - modelsCacheRef.current.set(fileName, models); + modelDefinitionsCacheRef.current.set(channel, models); setMappingModelsList(models); }) .catch((err: unknown) => { @@ -354,7 +319,7 @@ export function AuthFilesPage() { return () => { cancelled = true; }; - }, [mappingModalOpen, mappingModelsFileName, showNotification, t]); + }, [mappingModalOpen, mappingForm.provider, showNotification, t]); const prefixProxyUpdatedText = useMemo(() => { if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? ''; @@ -489,12 +454,12 @@ export function AuthFilesPage() { }, [showNotification, t]); // 加载 OAuth 模型映射 - const loadModelMappings = useCallback(async () => { + const loadModelAlias = useCallback(async () => { try { - const res = await authFilesApi.getOauthModelMappings(); + const res = await authFilesApi.getOauthModelAlias(); mappingsUnsupportedRef.current = false; - setModelMappings(res || {}); - setModelMappingsError(null); + setModelAlias(res || {}); + setModelAliasError(null); } catch (err: unknown) { const status = typeof err === 'object' && err !== null && 'status' in err @@ -502,11 +467,11 @@ export function AuthFilesPage() { : undefined; if (status === 404) { - setModelMappings({}); - setModelMappingsError('unsupported'); + setModelAlias({}); + setModelAliasError('unsupported'); if (!mappingsUnsupportedRef.current) { mappingsUnsupportedRef.current = true; - showNotification(t('oauth_model_mappings.upgrade_required'), 'warning'); + showNotification(t('oauth_model_alias.upgrade_required'), 'warning'); } return; } @@ -515,8 +480,8 @@ export function AuthFilesPage() { }, [showNotification, t]); const handleHeaderRefresh = useCallback(async () => { - await Promise.all([loadFiles(), loadKeyStats(), loadExcluded(), loadModelMappings()]); - }, [loadFiles, loadKeyStats, loadExcluded, loadModelMappings]); + await Promise.all([loadFiles(), loadKeyStats(), loadExcluded(), loadModelAlias()]); + }, [loadFiles, loadKeyStats, loadExcluded, loadModelAlias]); useHeaderRefresh(handleHeaderRefresh); @@ -524,8 +489,8 @@ export function AuthFilesPage() { loadFiles(); loadKeyStats(); loadExcluded(); - loadModelMappings(); - }, [loadFiles, loadKeyStats, loadExcluded, loadModelMappings]); + loadModelAlias(); + }, [loadFiles, loadKeyStats, loadExcluded, loadModelAlias]); // 定时刷新状态数据(每240秒) useInterval(loadKeyStats, 240_000); @@ -554,14 +519,14 @@ export function AuthFilesPage() { const mappingProviderLookup = useMemo(() => { const lookup = new Map(); - Object.keys(modelMappings).forEach((provider) => { + Object.keys(modelAlias).forEach((provider) => { const key = provider.trim().toLowerCase(); if (key && !lookup.has(key)) { lookup.set(key, provider); } }); return lookup; - }, [modelMappings]); + }, [modelAlias]); const providerOptions = useMemo(() => { const extraProviders = new Set(); @@ -569,7 +534,7 @@ export function AuthFilesPage() { Object.keys(excluded).forEach((provider) => { extraProviders.add(provider); }); - Object.keys(modelMappings).forEach((provider) => { + Object.keys(modelAlias).forEach((provider) => { extraProviders.add(provider); }); files.forEach((file) => { @@ -591,7 +556,7 @@ export function AuthFilesPage() { .sort((a, b) => a.localeCompare(b)); return [...OAUTH_PROVIDER_PRESETS, ...extraList]; - }, [excluded, files, modelMappings]); + }, [excluded, files, modelAlias]); // 过滤和搜索 const filtered = useMemo(() => { @@ -1123,7 +1088,7 @@ export function AuthFilesPage() { // OAuth 模型映射相关方法 const normalizeMappingEntries = ( - entries?: OAuthModelMappingEntry[] + entries?: OAuthModelAliasEntry[] ): OAuthModelMappingFormEntry[] => { if (!Array.isArray(entries) || entries.length === 0) { return [buildEmptyMappingEntry()]; @@ -1142,29 +1107,13 @@ export function AuthFilesPage() { const lookupKey = fallbackProvider ? mappingProviderLookup.get(fallbackProvider.toLowerCase()) : undefined; - const mappings = lookupKey ? modelMappings[lookupKey] : []; + const mappings = lookupKey ? modelAlias[lookupKey] : []; const providerValue = lookupKey || fallbackProvider; - const normalizedProviderKey = normalizeProviderKey(providerValue); - const defaultModelsFileName = files - .filter((file) => { - const isRuntimeOnly = isRuntimeOnlyAuthFile(file); - const isAistudio = (file.type || '').toLowerCase() === 'aistudio'; - const canShowModels = !isRuntimeOnly || isAistudio; - if (!canShowModels) return false; - if (!normalizedProviderKey) return false; - const typeKey = normalizeProviderKey(String(file.type || '')); - const providerKey = normalizeProviderKey(String(file.provider || '')); - return typeKey === normalizedProviderKey || providerKey === normalizedProviderKey; - }) - .map((file) => file.name) - .sort((a, b) => a.localeCompare(b))[0]; - setMappingForm({ provider: providerValue, mappings: normalizeMappingEntries(mappings), }); - setMappingModelsFileName(defaultModelsFileName || ''); setMappingModelsList([]); setMappingModelsError(null); setMappingModalOpen(true); @@ -1172,7 +1121,7 @@ export function AuthFilesPage() { const updateMappingEntry = ( index: number, - field: keyof OAuthModelMappingEntry, + field: keyof OAuthModelAliasEntry, value: string | boolean ) => { setMappingForm((prev) => ({ @@ -1200,10 +1149,10 @@ export function AuthFilesPage() { }); }; - const saveModelMappings = async () => { + const saveModelAlias = async () => { const provider = mappingForm.provider.trim(); if (!provider) { - showNotification(t('oauth_model_mappings.provider_required'), 'error'); + showNotification(t('oauth_model_alias.provider_required'), 'error'); return; } @@ -1218,40 +1167,40 @@ export function AuthFilesPage() { seen.add(key); return entry.fork ? { name, alias, fork: true } : { name, alias }; }) - .filter(Boolean) as OAuthModelMappingEntry[]; + .filter(Boolean) as OAuthModelAliasEntry[]; setSavingMappings(true); try { if (mappings.length) { - await authFilesApi.saveOauthModelMappings(provider, mappings); + await authFilesApi.saveOauthModelAlias(provider, mappings); } else { - await authFilesApi.deleteOauthModelMappings(provider); + await authFilesApi.deleteOauthModelAlias(provider); } - await loadModelMappings(); - showNotification(t('oauth_model_mappings.save_success'), 'success'); + await loadModelAlias(); + showNotification(t('oauth_model_alias.save_success'), 'success'); setMappingModalOpen(false); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('oauth_model_mappings.save_failed')}: ${errorMessage}`, 'error'); + showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error'); } finally { setSavingMappings(false); } }; - const deleteModelMappings = async (provider: string) => { + const deleteModelAlias = async (provider: string) => { showConfirmation({ - title: t('oauth_model_mappings.delete_title', { defaultValue: 'Delete Mappings' }), - message: t('oauth_model_mappings.delete_confirm', { provider }), + title: t('oauth_model_alias.delete_title', { defaultValue: 'Delete Mappings' }), + message: t('oauth_model_alias.delete_confirm', { provider }), variant: 'danger', confirmText: t('common.confirm'), onConfirm: async () => { try { - await authFilesApi.deleteOauthModelMappings(provider); - await loadModelMappings(); - showNotification(t('oauth_model_mappings.delete_success'), 'success'); + await authFilesApi.deleteOauthModelAlias(provider); + await loadModelAlias(); + showNotification(t('oauth_model_alias.delete_success'), 'success'); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('oauth_model_mappings.delete_failed')}: ${errorMessage}`, 'error'); + showNotification(`${t('oauth_model_alias.delete_failed')}: ${errorMessage}`, 'error'); } }, }); @@ -1657,42 +1606,42 @@ export function AuthFilesPage() { {/* OAuth 模型映射卡片 */} openMappingsModal()} - disabled={disableControls || modelMappingsError === 'unsupported'} + disabled={disableControls || modelAliasError === 'unsupported'} > - {t('oauth_model_mappings.add')} + {t('oauth_model_alias.add')} } > - {modelMappingsError === 'unsupported' ? ( + {modelAliasError === 'unsupported' ? ( - ) : Object.keys(modelMappings).length === 0 ? ( - + ) : Object.keys(modelAlias).length === 0 ? ( + ) : (
- {Object.entries(modelMappings).map(([provider, mappings]) => ( + {Object.entries(modelAlias).map(([provider, mappings]) => (
{provider}
{mappings?.length - ? t('oauth_model_mappings.model_count', { count: mappings.length }) - : t('oauth_model_mappings.no_models')} + ? t('oauth_model_alias.model_count', { count: mappings.length }) + : t('oauth_model_alias.no_models')}
-
@@ -1954,7 +1903,7 @@ export function AuthFilesPage() { setMappingModalOpen(false)} - title={t('oauth_model_mappings.add_title')} + title={t('oauth_model_alias.add_title')} footer={ <> - } @@ -1973,9 +1922,9 @@ export function AuthFilesPage() {
setMappingForm((prev) => ({ ...prev, provider: val }))} options={providerOptions} @@ -2000,37 +1949,27 @@ export function AuthFilesPage() {
)}
-
- setMappingModelsFileName(val)} - disabled={savingMappings} - options={modelSourceFileOptions} - /> -
+ {/* 模型定义加载状态提示 */} + {mappingForm.provider.trim() && ( +
+ {mappingModelsLoading + ? t('oauth_model_alias.model_source_loading') + : mappingModelsError === 'unsupported' + ? t('oauth_model_alias.model_source_unsupported') + : t('oauth_model_alias.model_source_loaded', { + count: mappingModelsList.length, + })} +
+ )}
- +
{(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map( (entry, index) => (
updateMappingEntry(index, 'name', val)} disabled={savingMappings} @@ -2042,7 +1981,7 @@ export function AuthFilesPage() { updateMappingEntry(index, 'alias', e.target.value)} disabled={savingMappings} @@ -2050,7 +1989,7 @@ export function AuthFilesPage() { />
updateMappingEntry(index, 'fork', value)} @@ -2077,10 +2016,10 @@ export function AuthFilesPage() { disabled={savingMappings} className="align-start" > - {t('oauth_model_mappings.add_mapping')} + {t('oauth_model_alias.add_mapping')}
-
{t('oauth_model_mappings.mappings_hint')}
+
{t('oauth_model_alias.mappings_hint')}
diff --git a/src/services/api/authFiles.ts b/src/services/api/authFiles.ts index 35334e3..05a6dd7 100644 --- a/src/services/api/authFiles.ts +++ b/src/services/api/authFiles.ts @@ -4,7 +4,7 @@ import { apiClient } from './client'; import type { AuthFilesResponse } from '@/types/authFile'; -import type { OAuthModelMappingEntry } from '@/types'; +import type { OAuthModelAliasEntry } from '@/types'; type StatusError = { status?: number }; type AuthFileStatusResponse = { status: string; disabled: boolean }; @@ -53,18 +53,17 @@ const normalizeOauthExcludedModels = (payload: unknown): Record => { +const normalizeOauthModelAlias = (payload: unknown): Record => { if (!payload || typeof payload !== 'object') return {}; const record = payload as Record; const source = - record['oauth-model-mappings'] ?? record['oauth-model-alias'] ?? record.items ?? payload; if (!source || typeof source !== 'object') return {}; - const result: Record = {}; + const result: Record = {}; Object.entries(source as Record).forEach(([channel, mappings]) => { const key = String(channel ?? '') @@ -86,12 +85,12 @@ const normalizeOauthModelMappings = (payload: unknown): Record { - const mapping = entry as OAuthModelMappingEntry; - const dedupeKey = `${mapping.name.toLowerCase()}::${mapping.alias.toLowerCase()}::${mapping.fork ? '1' : '0'}`; + const aliasEntry = entry as OAuthModelAliasEntry; + const dedupeKey = `${aliasEntry.name.toLowerCase()}::${aliasEntry.alias.toLowerCase()}::${aliasEntry.fork ? '1' : '0'}`; if (seen.has(dedupeKey)) return false; seen.add(dedupeKey); return true; - }) as OAuthModelMappingEntry[]; + }) as OAuthModelAliasEntry[]; if (normalized.length) { result[key] = normalized; @@ -101,8 +100,7 @@ const normalizeOauthModelMappings = (payload: unknown): Record apiClient.get('/auth-files'), @@ -143,63 +141,31 @@ export const authFilesApi = { replaceOauthExcludedModels: (map: Record) => apiClient.put('/oauth-excluded-models', normalizeOauthExcludedModels(map)), - // OAuth 模型映射 - async getOauthModelMappings(): Promise> { - try { - const data = await apiClient.get(OAUTH_MODEL_MAPPINGS_ENDPOINT); - return normalizeOauthModelMappings(data); - } catch (err: unknown) { - if (getStatusCode(err) !== 404) throw err; - const data = await apiClient.get(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT); - return normalizeOauthModelMappings(data); - } + // OAuth 模型别名 + async getOauthModelAlias(): Promise> { + const data = await apiClient.get(OAUTH_MODEL_ALIAS_ENDPOINT); + return normalizeOauthModelAlias(data); }, - saveOauthModelMappings: async (channel: string, mappings: OAuthModelMappingEntry[]) => { + saveOauthModelAlias: async (channel: string, aliases: OAuthModelAliasEntry[]) => { const normalizedChannel = String(channel ?? '') .trim() .toLowerCase(); - const normalizedMappings = normalizeOauthModelMappings({ [normalizedChannel]: mappings })[normalizedChannel] ?? []; - - try { - await apiClient.patch(OAUTH_MODEL_MAPPINGS_ENDPOINT, { channel: normalizedChannel, mappings: normalizedMappings }); - return; - } catch (err: unknown) { - if (getStatusCode(err) !== 404) throw err; - await apiClient.patch(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT, { channel: normalizedChannel, aliases: normalizedMappings }); - } + const normalizedAliases = normalizeOauthModelAlias({ [normalizedChannel]: aliases })[normalizedChannel] ?? []; + await apiClient.patch(OAUTH_MODEL_ALIAS_ENDPOINT, { channel: normalizedChannel, aliases: normalizedAliases }); }, - deleteOauthModelMappings: async (channel: string) => { + deleteOauthModelAlias: async (channel: string) => { const normalizedChannel = String(channel ?? '') .trim() .toLowerCase(); - const deleteViaPatch = async () => { - try { - await apiClient.patch(OAUTH_MODEL_MAPPINGS_ENDPOINT, { channel: normalizedChannel, mappings: [] }); - return true; - } catch (err: unknown) { - if (getStatusCode(err) !== 404) throw err; - await apiClient.patch(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT, { channel: normalizedChannel, aliases: [] }); - return true; - } - }; - try { - await deleteViaPatch(); - return; + await apiClient.patch(OAUTH_MODEL_ALIAS_ENDPOINT, { channel: normalizedChannel, aliases: [] }); } catch (err: unknown) { const status = getStatusCode(err); if (status !== 405) throw err; - } - - try { - await apiClient.delete(`${OAUTH_MODEL_MAPPINGS_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`); - return; - } catch (err: unknown) { - if (getStatusCode(err) !== 404) throw err; - await apiClient.delete(`${OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`); + await apiClient.delete(`${OAUTH_MODEL_ALIAS_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`); } }, @@ -207,5 +173,13 @@ export const authFilesApi = { async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> { const data = await apiClient.get(`/auth-files/models?name=${encodeURIComponent(name)}`); return (data && Array.isArray(data['models'])) ? data['models'] : []; + }, + + // 获取指定 channel 的模型定义 + async getModelDefinitions(channel: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> { + const normalizedChannel = String(channel ?? '').trim().toLowerCase(); + if (!normalizedChannel) return []; + const data = await apiClient.get(`/model-definitions/${encodeURIComponent(normalizedChannel)}`); + return (data && Array.isArray(data['models'])) ? data['models'] : []; } }; diff --git a/src/types/oauth.ts b/src/types/oauth.ts index 4f36dcd..2a32cde 100644 --- a/src/types/oauth.ts +++ b/src/types/oauth.ts @@ -34,11 +34,11 @@ export interface OAuthExcludedModels { models: string[]; } -// OAuth 模型映射 -export interface OAuthModelMappingEntry { +// OAuth 模型别名 +export interface OAuthModelAliasEntry { name: string; alias: string; fork?: boolean; } -export type OAuthModelMappings = Record; +export type OAuthModelAlias = Record;