feat(auth-files): normalize OAuth excluded models handling and update related API methods

This commit is contained in:
Supra4E8C
2026-01-07 12:26:33 +08:00
parent ee99836285
commit f663b83ac8
4 changed files with 96 additions and 38 deletions

View File

@@ -612,7 +612,7 @@
"iflow_oauth_polling_error": "Failed to check authentication status:", "iflow_oauth_polling_error": "Failed to check authentication status:",
"iflow_cookie_title": "iFlow Cookie Login", "iflow_cookie_title": "iFlow Cookie Login",
"iflow_cookie_label": "Cookie Value:", "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_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_key_hint": "Note: Create a key on the platform first.",
"iflow_cookie_button": "Submit Cookie Login", "iflow_cookie_button": "Submit Cookie Login",

View File

@@ -612,7 +612,7 @@
"iflow_oauth_polling_error": "检查认证状态失败:", "iflow_oauth_polling_error": "检查认证状态失败:",
"iflow_cookie_title": "iFlow Cookie 登录", "iflow_cookie_title": "iFlow Cookie 登录",
"iflow_cookie_label": "Cookie 内容:", "iflow_cookie_label": "Cookie 内容:",
"iflow_cookie_placeholder": "粘贴浏览器中的 Cookie例如 sessionid=...;", "iflow_cookie_placeholder": "填入BXAuth值 以BXAuth=开头",
"iflow_cookie_hint": "直接提交 Cookie 以完成登录(无需打开授权链接),服务端将自动保存凭据。", "iflow_cookie_hint": "直接提交 Cookie 以完成登录(无需打开授权链接),服务端将自动保存凭据。",
"iflow_cookie_key_hint": "提示:需在平台上先创建 Key。", "iflow_cookie_key_hint": "提示:需在平台上先创建 Key。",
"iflow_cookie_button": "提交 Cookie 登录", "iflow_cookie_button": "提交 Cookie 登录",

View File

@@ -68,7 +68,6 @@ const TYPE_COLORS: Record<string, TypeColorSet> = {
}; };
const OAUTH_PROVIDER_PRESETS = [ const OAUTH_PROVIDER_PRESETS = [
'gemini',
'gemini-cli', 'gemini-cli',
'vertex', 'vertex',
'aistudio', 'aistudio',
@@ -160,11 +159,11 @@ function resolveAuthFileStats(
return defaultStats; return defaultStats;
} }
export function AuthFilesPage() { export function AuthFilesPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { showNotification } = useNotificationStore(); const { showNotification } = useNotificationStore();
const connectionStatus = useAuthStore((state) => state.connectionStatus); const connectionStatus = useAuthStore((state) => state.connectionStatus);
const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme);
const [files, setFiles] = useState<AuthFileItem[]>([]); const [files, setFiles] = useState<AuthFileItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -215,6 +214,8 @@ export function AuthFilesPage() {
const disableControls = connectionStatus !== 'connected'; const disableControls = connectionStatus !== 'connected';
const normalizeProviderKey = (value: string) => value.trim().toLowerCase();
const handlePageSizeChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handlePageSizeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.valueAsNumber; const value = event.currentTarget.valueAsNumber;
if (!Number.isFinite(value)) return; if (!Number.isFinite(value)) return;
@@ -626,13 +627,14 @@ export function AuthFilesPage() {
}; };
// 检查模型是否被 OAuth 排除 // 检查模型是否被 OAuth 排除
const isModelExcluded = (modelId: string, providerType: string): boolean => { const isModelExcluded = (modelId: string, providerType: string): boolean => {
const excludedModels = excluded[providerType] || []; const providerKey = normalizeProviderKey(providerType);
return excludedModels.some(pattern => { const excludedModels = excluded[providerKey] || excluded[providerType] || [];
if (pattern.includes('*')) { return excludedModels.some(pattern => {
// 支持通配符匹配 if (pattern.includes('*')) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i'); // 支持通配符匹配
return regex.test(modelId); const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
return regex.test(modelId);
} }
return pattern.toLowerCase() === modelId.toLowerCase(); return pattern.toLowerCase() === modelId.toLowerCase();
}); });
@@ -655,11 +657,10 @@ export function AuthFilesPage() {
// OAuth 排除相关方法 // OAuth 排除相关方法
const openExcludedModal = (provider?: string) => { const openExcludedModal = (provider?: string) => {
const normalizedProvider = (provider || '').trim(); const normalizedProvider = normalizeProviderKey(provider || '');
const fallbackProvider = normalizedProvider || (filter !== 'all' ? String(filter) : ''); const fallbackProvider =
const lookupKey = fallbackProvider normalizedProvider || (filter !== 'all' ? normalizeProviderKey(String(filter)) : '');
? excludedProviderLookup.get(fallbackProvider.toLowerCase()) const lookupKey = fallbackProvider ? excludedProviderLookup.get(fallbackProvider) : undefined;
: undefined;
const models = lookupKey ? excluded[lookupKey] : []; const models = lookupKey ? excluded[lookupKey] : [];
setExcludedForm({ setExcludedForm({
provider: lookupKey || fallbackProvider, provider: lookupKey || fallbackProvider,
@@ -669,7 +670,7 @@ export function AuthFilesPage() {
}; };
const saveExcludedModels = async () => { const saveExcludedModels = async () => {
const provider = excludedForm.provider.trim(); const provider = normalizeProviderKey(excludedForm.provider);
if (!provider) { if (!provider) {
showNotification(t('oauth_excluded.provider_required'), 'error'); showNotification(t('oauth_excluded.provider_required'), 'error');
return; return;
@@ -679,13 +680,13 @@ export function AuthFilesPage() {
.map((item) => item.trim()) .map((item) => item.trim())
.filter(Boolean); .filter(Boolean);
setSavingExcluded(true); setSavingExcluded(true);
try { try {
if (models.length) { if (models.length) {
await authFilesApi.saveOauthExcludedModels(provider, models); await authFilesApi.saveOauthExcludedModels(provider, models);
} else { } else {
await authFilesApi.deleteOauthExcludedEntry(provider); await authFilesApi.deleteOauthExcludedEntry(provider);
} }
await loadExcluded(); await loadExcluded();
showNotification(t('oauth_excluded.save_success'), 'success'); showNotification(t('oauth_excluded.save_success'), 'success');
setExcludedModalOpen(false); setExcludedModalOpen(false);
} catch (err: unknown) { } catch (err: unknown) {
@@ -697,14 +698,32 @@ export function AuthFilesPage() {
}; };
const deleteExcluded = async (provider: string) => { const deleteExcluded = async (provider: string) => {
if (!window.confirm(t('oauth_excluded.delete_confirm', { provider }))) return; const providerLabel = provider.trim() || provider;
try { if (!window.confirm(t('oauth_excluded.delete_confirm', { provider: providerLabel }))) return;
await authFilesApi.deleteOauthExcludedEntry(provider); const providerKey = normalizeProviderKey(provider);
await loadExcluded(); if (!providerKey) {
showNotification(t('oauth_excluded.delete_success'), 'success'); showNotification(t('oauth_excluded.provider_required'), 'error');
} catch (err: unknown) { return;
const errorMessage = err instanceof Error ? err.message : ''; }
showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); 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<string, string[]> = {};
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');
}
} }
}; };

View File

@@ -6,6 +6,43 @@ import { apiClient } from './client';
import type { AuthFilesResponse } from '@/types/authFile'; import type { AuthFilesResponse } from '@/types/authFile';
import type { OAuthModelMappingEntry } from '@/types'; import type { OAuthModelMappingEntry } from '@/types';
const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]> => {
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<string, string[]> = {};
Object.entries(source as Record<string, unknown>).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<string>();
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 = { export const authFilesApi = {
list: () => apiClient.get<AuthFilesResponse>('/auth-files'), list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
@@ -22,8 +59,7 @@ export const authFilesApi = {
// OAuth 排除模型 // OAuth 排除模型
async getOauthExcludedModels(): Promise<Record<string, string[]>> { async getOauthExcludedModels(): Promise<Record<string, string[]>> {
const data = await apiClient.get('/oauth-excluded-models'); const data = await apiClient.get('/oauth-excluded-models');
const payload = (data && (data['oauth-excluded-models'] ?? data.items ?? data)) as any; return normalizeOauthExcludedModels(data);
return payload && typeof payload === 'object' ? payload : {};
}, },
saveOauthExcludedModels: (provider: string, models: string[]) => saveOauthExcludedModels: (provider: string, models: string[]) =>
@@ -32,6 +68,9 @@ export const authFilesApi = {
deleteOauthExcludedEntry: (provider: string) => deleteOauthExcludedEntry: (provider: string) =>
apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`), apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`),
replaceOauthExcludedModels: (map: Record<string, string[]>) =>
apiClient.put('/oauth-excluded-models', normalizeOauthExcludedModels(map)),
// OAuth 模型映射 // OAuth 模型映射
async getOauthModelMappings(): Promise<Record<string, OAuthModelMappingEntry[]>> { async getOauthModelMappings(): Promise<Record<string, OAuthModelMappingEntry[]>> {
const data = await apiClient.get('/oauth-model-mappings'); const data = await apiClient.get('/oauth-model-mappings');