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] 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() { )} + {/* 模型勾选列表 */}
-