From f663b83ac8a9d92605fd1b6df02e81fbc5cdb18c Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Wed, 7 Jan 2026 12:26:33 +0800 Subject: [PATCH] feat(auth-files): normalize OAuth excluded models handling and update related API methods --- src/i18n/locales/en.json | 2 +- src/i18n/locales/zh-CN.json | 2 +- src/pages/AuthFilesPage.tsx | 87 +++++++++++++++++++++-------------- src/services/api/authFiles.ts | 43 ++++++++++++++++- 4 files changed, 96 insertions(+), 38 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2eb89df..86b2684 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -612,7 +612,7 @@ "iflow_oauth_polling_error": "Failed to check authentication status:", "iflow_cookie_title": "iFlow Cookie Login", "iflow_cookie_label": "Cookie Value:", - "iflow_cookie_placeholder": "Paste browser cookie, e.g. sessionid=...;", + "iflow_cookie_placeholder": "Enter the BXAuth value, starting with BXAuth=", "iflow_cookie_hint": "Submit an existing cookie to finish login without opening the authorization link; the credential file will be saved automatically.", "iflow_cookie_key_hint": "Note: Create a key on the platform first.", "iflow_cookie_button": "Submit Cookie Login", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 7848504..f126768 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -612,7 +612,7 @@ "iflow_oauth_polling_error": "检查认证状态失败:", "iflow_cookie_title": "iFlow Cookie 登录", "iflow_cookie_label": "Cookie 内容:", - "iflow_cookie_placeholder": "粘贴浏览器中的 Cookie,例如 sessionid=...;", + "iflow_cookie_placeholder": "填入BXAuth值 以BXAuth=开头", "iflow_cookie_hint": "直接提交 Cookie 以完成登录(无需打开授权链接),服务端将自动保存凭据。", "iflow_cookie_key_hint": "提示:需在平台上先创建 Key。", "iflow_cookie_button": "提交 Cookie 登录", diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index adb3341..e2bdf7e 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -68,7 +68,6 @@ const TYPE_COLORS: Record = { }; const OAUTH_PROVIDER_PRESETS = [ - 'gemini', 'gemini-cli', 'vertex', 'aistudio', @@ -160,11 +159,11 @@ function resolveAuthFileStats( return defaultStats; } -export function AuthFilesPage() { - const { t } = useTranslation(); - const { showNotification } = useNotificationStore(); - const connectionStatus = useAuthStore((state) => state.connectionStatus); - const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); +export function AuthFilesPage() { + const { t } = useTranslation(); + const { showNotification } = useNotificationStore(); + const connectionStatus = useAuthStore((state) => state.connectionStatus); + const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); @@ -215,6 +214,8 @@ export function AuthFilesPage() { const disableControls = connectionStatus !== 'connected'; + const normalizeProviderKey = (value: string) => value.trim().toLowerCase(); + const handlePageSizeChange = (event: React.ChangeEvent) => { const value = event.currentTarget.valueAsNumber; if (!Number.isFinite(value)) return; @@ -626,13 +627,14 @@ export function AuthFilesPage() { }; // 检查模型是否被 OAuth 排除 - const isModelExcluded = (modelId: string, providerType: string): boolean => { - const excludedModels = excluded[providerType] || []; - return excludedModels.some(pattern => { - if (pattern.includes('*')) { - // 支持通配符匹配 - const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i'); - return regex.test(modelId); + const isModelExcluded = (modelId: string, providerType: string): boolean => { + const providerKey = normalizeProviderKey(providerType); + const excludedModels = excluded[providerKey] || excluded[providerType] || []; + return excludedModels.some(pattern => { + if (pattern.includes('*')) { + // 支持通配符匹配 + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i'); + return regex.test(modelId); } return pattern.toLowerCase() === modelId.toLowerCase(); }); @@ -655,11 +657,10 @@ export function AuthFilesPage() { // OAuth 排除相关方法 const openExcludedModal = (provider?: string) => { - const normalizedProvider = (provider || '').trim(); - const fallbackProvider = normalizedProvider || (filter !== 'all' ? String(filter) : ''); - const lookupKey = fallbackProvider - ? excludedProviderLookup.get(fallbackProvider.toLowerCase()) - : undefined; + const normalizedProvider = normalizeProviderKey(provider || ''); + const fallbackProvider = + normalizedProvider || (filter !== 'all' ? normalizeProviderKey(String(filter)) : ''); + const lookupKey = fallbackProvider ? excludedProviderLookup.get(fallbackProvider) : undefined; const models = lookupKey ? excluded[lookupKey] : []; setExcludedForm({ provider: lookupKey || fallbackProvider, @@ -669,7 +670,7 @@ export function AuthFilesPage() { }; const saveExcludedModels = async () => { - const provider = excludedForm.provider.trim(); + const provider = normalizeProviderKey(excludedForm.provider); if (!provider) { showNotification(t('oauth_excluded.provider_required'), 'error'); return; @@ -679,13 +680,13 @@ export function AuthFilesPage() { .map((item) => item.trim()) .filter(Boolean); setSavingExcluded(true); - try { - if (models.length) { - await authFilesApi.saveOauthExcludedModels(provider, models); - } else { - await authFilesApi.deleteOauthExcludedEntry(provider); - } - await loadExcluded(); + try { + if (models.length) { + await authFilesApi.saveOauthExcludedModels(provider, models); + } else { + await authFilesApi.deleteOauthExcludedEntry(provider); + } + await loadExcluded(); showNotification(t('oauth_excluded.save_success'), 'success'); setExcludedModalOpen(false); } catch (err: unknown) { @@ -697,14 +698,32 @@ export function AuthFilesPage() { }; const deleteExcluded = async (provider: string) => { - if (!window.confirm(t('oauth_excluded.delete_confirm', { provider }))) return; - try { - await authFilesApi.deleteOauthExcludedEntry(provider); - await loadExcluded(); - showNotification(t('oauth_excluded.delete_success'), 'success'); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); + const providerLabel = provider.trim() || provider; + if (!window.confirm(t('oauth_excluded.delete_confirm', { provider: providerLabel }))) return; + const providerKey = normalizeProviderKey(provider); + if (!providerKey) { + showNotification(t('oauth_excluded.provider_required'), 'error'); + return; + } + try { + await authFilesApi.deleteOauthExcludedEntry(providerKey); + await loadExcluded(); + showNotification(t('oauth_excluded.delete_success'), 'success'); + } catch (err: unknown) { + try { + const current = await authFilesApi.getOauthExcludedModels(); + const next: Record = {}; + Object.entries(current).forEach(([key, models]) => { + if (normalizeProviderKey(key) === providerKey) return; + next[key] = models; + }); + await authFilesApi.replaceOauthExcludedModels(next); + await loadExcluded(); + showNotification(t('oauth_excluded.delete_success'), 'success'); + } catch (fallbackErr: unknown) { + const errorMessage = fallbackErr instanceof Error ? fallbackErr.message : err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); + } } }; diff --git a/src/services/api/authFiles.ts b/src/services/api/authFiles.ts index 4e41f31..054b1cf 100644 --- a/src/services/api/authFiles.ts +++ b/src/services/api/authFiles.ts @@ -6,6 +6,43 @@ import { apiClient } from './client'; import type { AuthFilesResponse } from '@/types/authFile'; import type { OAuthModelMappingEntry } from '@/types'; +const normalizeOauthExcludedModels = (payload: unknown): Record => { + if (!payload || typeof payload !== 'object') return {}; + + const source = (payload as any)['oauth-excluded-models'] ?? (payload as any).items ?? payload; + if (!source || typeof source !== 'object') return {}; + + const result: Record = {}; + + Object.entries(source as Record).forEach(([provider, models]) => { + const key = String(provider ?? '') + .trim() + .toLowerCase(); + if (!key) return; + + const rawList = Array.isArray(models) + ? models + : typeof models === 'string' + ? models.split(/[\n,]+/) + : []; + + const seen = new Set(); + const normalized: string[] = []; + rawList.forEach((item) => { + const trimmed = String(item ?? '').trim(); + if (!trimmed) return; + const modelKey = trimmed.toLowerCase(); + if (seen.has(modelKey)) return; + seen.add(modelKey); + normalized.push(trimmed); + }); + + result[key] = normalized; + }); + + return result; +}; + export const authFilesApi = { list: () => apiClient.get('/auth-files'), @@ -22,8 +59,7 @@ export const authFilesApi = { // OAuth 排除模型 async getOauthExcludedModels(): Promise> { const data = await apiClient.get('/oauth-excluded-models'); - const payload = (data && (data['oauth-excluded-models'] ?? data.items ?? data)) as any; - return payload && typeof payload === 'object' ? payload : {}; + return normalizeOauthExcludedModels(data); }, saveOauthExcludedModels: (provider: string, models: string[]) => @@ -32,6 +68,9 @@ export const authFilesApi = { deleteOauthExcludedEntry: (provider: string) => apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`), + replaceOauthExcludedModels: (map: Record) => + apiClient.put('/oauth-excluded-models', normalizeOauthExcludedModels(map)), + // OAuth 模型映射 async getOauthModelMappings(): Promise> { const data = await apiClient.get('/oauth-model-mappings');