From 0c53dcfa80b753e1aec1fabc58ad6bc50b8cb2e0 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:00:42 +0800 Subject: [PATCH 1/5] docs(i18n): rename model mappings to aliases in ui strings --- src/i18n/locales/en.json | 34 +++++++++++++++++----------------- src/i18n/locales/zh-CN.json | 36 ++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f424856..0f6c411 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -249,10 +249,10 @@ "vertex_edit_modal_url_label": "Base URL (Required):", "vertex_edit_modal_proxy_label": "Proxy URL (Optional):", "vertex_delete_confirm": "Are you sure you want to delete this Vertex configuration?", - "vertex_models_label": "Model mappings (alias required):", + "vertex_models_label": "Model aliases (alias required):", "vertex_models_add_btn": "Add Mapping", - "vertex_models_hint": "Each mapping needs both the original model and its alias.", - "vertex_models_count": "Mapping count", + "vertex_models_hint": "Each alias needs both the original model and the alias.", + "vertex_models_count": "Alias count", "ampcode_title": "Amp CLI Integration (ampcode)", "ampcode_modal_title": "Configure Ampcode", "ampcode_upstream_url_label": "Upstream URL", @@ -513,9 +513,9 @@ "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": { - "title": "OAuth Model Mappings", - "add": "Add Mapping", - "add_title": "Add provider model mappings", + "title": "OAuth Model Aliases", + "add": "Add Alias", + "add_title": "Add provider model aliases", "provider_label": "Provider", "provider_placeholder": "e.g. gemini-cli / vertex", "provider_hint": "Defaults to the current filter; pick an existing provider or type a new name.", @@ -525,26 +525,26 @@ "model_source_loading": "Loading models...", "model_source_unsupported": "The current CPA version does not support fetching model lists (manual input still works).", "model_source_loaded": "{{count}} models loaded. Use the dropdown in “Source model name”, or type custom values.", - "mappings_label": "Model mappings", + "mappings_label": "Model aliases", "mapping_name_placeholder": "Source model name", "mapping_alias_placeholder": "Alias (required)", "mapping_fork_label": "Keep original", "mappings_hint": "Saving an empty list removes that provider. Enable “Keep original” to keep the original name while adding the alias.", - "add_mapping": "Add mapping", + "add_mapping": "Add alias", "save": "Save/Update", - "save_success": "Model mappings updated", - "save_failed": "Failed to update model mappings", + "save_success": "Model aliases updated", + "save_failed": "Failed to update model aliases", "delete": "Delete Provider", - "delete_confirm": "Delete model mappings for {{provider}}?", - "delete_success": "Model mappings removed", - "delete_failed": "Failed to delete model mappings", - "no_models": "No model mappings", - "model_count": "{{count}} mappings", - "list_empty_all": "No model mappings yet—use “Add Mapping” to create one.", + "delete_confirm": "Delete model aliases for {{provider}}?", + "delete_success": "Model aliases removed", + "delete_failed": "Failed to delete model aliases", + "no_models": "No model aliases", + "model_count": "{{count}} aliases", + "list_empty_all": "No model aliases yet—use “Add Alias” to create one.", "provider_required": "Please enter a provider first", "upgrade_required": "This feature requires a newer CLI Proxy API (CPA) version. Please upgrade.", "upgrade_required_title": "Please upgrade CLI Proxy API", - "upgrade_required_desc": "The current server does not support the OAuth model mappings API. Please upgrade to the latest CLI Proxy API (CPA) version." + "upgrade_required_desc": "The current server does not support the OAuth model aliases API. Please upgrade to the latest CLI Proxy API (CPA) version." }, "auth_login": { "codex_oauth_title": "Codex OAuth", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 98b64c3..819e63b 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -249,10 +249,10 @@ "vertex_edit_modal_url_label": "Base URL (必填):", "vertex_edit_modal_proxy_label": "代理 URL (可选):", "vertex_delete_confirm": "确定要删除这个Vertex配置吗?", - "vertex_models_label": "模型映射 (别名必填):", + "vertex_models_label": "模型别名 (别名必填):", "vertex_models_add_btn": "添加映射", - "vertex_models_hint": "每条映射需要填写原模型与别名。", - "vertex_models_count": "映射数量", + "vertex_models_hint": "每条别名需要填写原模型与别名。", + "vertex_models_count": "别名数量", "ampcode_title": "Amp CLI 集成 (ampcode)", "ampcode_modal_title": "配置 Ampcode", "ampcode_upstream_url_label": "Upstream URL", @@ -513,9 +513,9 @@ "upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。" }, "oauth_model_mappings": { - "title": "OAuth 模型映射", - "add": "新增映射", - "add_title": "新增提供商模型映射", + "title": "OAuth 模型别名", + "add": "新增别名", + "add_title": "新增提供商模型别名", "provider_label": "提供商", "provider_placeholder": "例如 gemini-cli / vertex", "provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。", @@ -525,26 +525,26 @@ "model_source_loading": "正在加载模型列表...", "model_source_unsupported": "当前 CPA 版本不支持获取模型列表(仍可手动输入)。", "model_source_loaded": "已加载 {{count}} 个模型,可在“原模型名称”中下拉选择;也可手动输入。", - "mappings_label": "模型映射", + "mappings_label": "模型别名", "mapping_name_placeholder": "原模型名称", "mapping_alias_placeholder": "别名 (必填)", "mapping_fork_label": "保留原名", "mappings_hint": "留空保存将删除该提供商记录;开启“保留原名”会在保留原模型名的同时新增别名。", - "add_mapping": "添加映射", + "add_mapping": "添加别名", "save": "保存/更新", - "save_success": "模型映射已更新", - "save_failed": "更新模型映射失败", + "save_success": "模型别名已更新", + "save_failed": "更新模型别名失败", "delete": "删除提供商", - "delete_confirm": "确定要删除 {{provider}} 的模型映射吗?", - "delete_success": "已删除该提供商的模型映射", - "delete_failed": "删除模型映射失败", - "no_models": "未配置模型映射", - "model_count": "映射 {{count}} 条模型", - "list_empty_all": "暂无任何提供商的模型映射,点击“新增映射”创建。", + "delete_confirm": "确定要删除 {{provider}} 的模型别名吗?", + "delete_success": "已删除该提供商的模型别名", + "delete_failed": "删除模型别名失败", + "no_models": "未配置模型别名", + "model_count": "{{count}} 条别名", + "list_empty_all": "暂无任何提供商的模型别名,点击“新增别名”创建。", "provider_required": "请先填写提供商名称", - "upgrade_required": "当前 CPA 版本不支持模型映射功能,请升级 CPA 版本", + "upgrade_required": "当前 CPA 版本不支持模型别名功能,请升级 CPA 版本", "upgrade_required_title": "需要升级 CPA 版本", - "upgrade_required_desc": "当前服务器版本不支持 OAuth 模型映射功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。" + "upgrade_required_desc": "当前服务器版本不支持 OAuth 模型别名功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。" }, "auth_login": { "codex_oauth_title": "Codex OAuth", 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 2/5] 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; From 9515d88e3c6fa31a66a8883e5898ed9189e86a85 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:56:23 +0800 Subject: [PATCH 3/5] feat(ui): add model checklist for oauth exclusions --- src/i18n/locales/en.json | 6 +- src/i18n/locales/zh-CN.json | 6 +- src/pages/AuthFilesPage.module.scss | 50 +++++++++++ src/pages/AuthFilesPage.tsx | 128 ++++++++++++++++++++++++---- 4 files changed, 170 insertions(+), 20 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 36cd168..b016186 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -488,8 +488,10 @@ "provider_placeholder": "e.g. gemini-cli", "provider_hint": "Defaults to the current filter; pick an existing provider or type a new name.", "models_label": "Models to exclude", - "models_placeholder": "gpt-4.1-mini\n*-preview", - "models_hint": "Separate by commas or new lines; saving an empty list removes that provider. * wildcards are supported.", + "models_loading": "Loading models...", + "models_unsupported": "Current CPA version does not support fetching model lists.", + "models_loaded": "{{count}} models loaded. Check the models to exclude.", + "no_models_available": "No models available for this provider.", "save": "Save/Update", "saving": "Saving...", "save_success": "Excluded models updated", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index be4dd93..a87f8cd 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -488,8 +488,10 @@ "provider_placeholder": "例如 gemini-cli / openai", "provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。", "models_label": "排除的模型", - "models_placeholder": "gpt-4.1-mini\n*-preview", - "models_hint": "逗号或换行分隔;留空保存将删除该提供商记录;支持 * 通配符。", + "models_loading": "正在加载模型列表...", + "models_unsupported": "当前 CPA 版本不支持获取模型列表。", + "models_loaded": "已加载 {{count}} 个模型,勾选要排除的模型。", + "no_models_available": "该提供商暂无可用模型列表。", "save": "保存/更新", "saving": "正在保存...", "save_success": "排除列表已更新", diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index 08dbd5c..0230d0d 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -995,3 +995,53 @@ border: 1px solid var(--danger-color); flex-shrink: 0; } + +// 排除模型勾选列表 +.excludedCheckList { + display: flex; + flex-direction: column; + gap: $spacing-xs; + max-height: 280px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: $radius-md; + padding: $spacing-sm; + background-color: var(--bg-secondary); +} + +.excludedCheckItem { + display: flex; + align-items: center; + gap: $spacing-sm; + padding: $spacing-xs $spacing-sm; + border-radius: $radius-sm; + cursor: pointer; + transition: background-color $transition-fast; + + &:hover { + background-color: var(--bg-hover); + } + + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: var(--primary-color); + } +} + +.excludedCheckLabel { + display: flex; + align-items: center; + gap: $spacing-sm; + font-size: 13px; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + color: var(--text-primary); + word-break: break-all; +} + +.excludedCheckDisplayName { + font-size: 12px; + color: var(--text-tertiary); + font-family: inherit; +} diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 8a43efe..6f46cfa 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -104,7 +104,7 @@ const clampCardPageSize = (value: number) => interface ExcludedFormState { provider: string; - modelsText: string; + selectedModels: Set; } type OAuthModelMappingFormEntry = OAuthModelAliasEntry & { id: string }; @@ -232,8 +232,11 @@ export function AuthFilesPage() { const [excludedModalOpen, setExcludedModalOpen] = useState(false); const [excludedForm, setExcludedForm] = useState({ provider: '', - modelsText: '', + selectedModels: new Set(), }); + const [excludedModelsList, setExcludedModelsList] = useState([]); + const [excludedModelsLoading, setExcludedModelsLoading] = useState(false); + const [excludedModelsError, setExcludedModelsError] = useState<'unsupported' | null>(null); const [savingExcluded, setSavingExcluded] = useState(false); // OAuth 模型映射相关 @@ -321,6 +324,61 @@ export function AuthFilesPage() { }; }, [mappingModalOpen, mappingForm.provider, showNotification, t]); + // 排除列表弹窗:根据 provider 加载模型定义 + useEffect(() => { + if (!excludedModalOpen) return; + + const channel = normalizeProviderKey(excludedForm.provider); + if (!channel) { + setExcludedModelsList([]); + setExcludedModelsError(null); + setExcludedModelsLoading(false); + return; + } + + const cached = modelDefinitionsCacheRef.current.get(channel); + if (cached) { + setExcludedModelsList(cached); + setExcludedModelsError(null); + setExcludedModelsLoading(false); + return; + } + + let cancelled = false; + setExcludedModelsLoading(true); + setExcludedModelsError(null); + + authFilesApi + .getModelDefinitions(channel) + .then((models) => { + if (cancelled) return; + modelDefinitionsCacheRef.current.set(channel, models); + setExcludedModelsList(models); + }) + .catch((err: unknown) => { + if (cancelled) return; + const errorMessage = err instanceof Error ? err.message : ''; + if ( + errorMessage.includes('404') || + errorMessage.includes('not found') || + errorMessage.includes('Not Found') + ) { + setExcludedModelsList([]); + setExcludedModelsError('unsupported'); + return; + } + showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error'); + }) + .finally(() => { + if (cancelled) return; + setExcludedModelsLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [excludedModalOpen, excludedForm.provider, showNotification, t]); + const prefixProxyUpdatedText = useMemo(() => { if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? ''; const next: Record = { ...prefixProxyEditor.json }; @@ -1008,11 +1066,13 @@ export function AuthFilesPage() { const fallbackProvider = normalizedProvider || (filter !== 'all' ? normalizeProviderKey(String(filter)) : ''); const lookupKey = fallbackProvider ? excludedProviderLookup.get(fallbackProvider) : undefined; - const models = lookupKey ? excluded[lookupKey] : []; + const existingModels = lookupKey ? excluded[lookupKey] : []; setExcludedForm({ provider: lookupKey || fallbackProvider, - modelsText: Array.isArray(models) ? models.join('\n') : '', + selectedModels: new Set(existingModels), }); + setExcludedModelsList([]); + setExcludedModelsError(null); setExcludedModalOpen(true); }; @@ -1022,10 +1082,7 @@ export function AuthFilesPage() { showNotification(t('oauth_excluded.provider_required'), 'error'); return; } - const models = excludedForm.modelsText - .split(/[\n,]+/) - .map((item) => item.trim()) - .filter(Boolean); + const models = [...excludedForm.selectedModels]; setSavingExcluded(true); try { if (models.length) { @@ -1886,16 +1943,55 @@ export function AuthFilesPage() {
)} + {/* 模型勾选列表 */}
-