From cb8208e0f73debfdcc79bda2cecf378c6cb55a1f Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 7 Mar 2026 16:06:23 +0800 Subject: [PATCH] feat(auth-files): enhance error handling for invalid JSON and refactor file upload logic --- .../authFiles/hooks/useAuthFilesData.ts | 57 +++++++++++-------- .../hooks/useAuthFilesPrefixProxyEditor.ts | 3 +- src/services/api/authFiles.ts | 51 ++++++++++++++++- 3 files changed, 82 insertions(+), 29 deletions(-) diff --git a/src/features/authFiles/hooks/useAuthFilesData.ts b/src/features/authFiles/hooks/useAuthFilesData.ts index a3d2c08..34d5a9f 100644 --- a/src/features/authFiles/hooks/useAuthFilesData.ts +++ b/src/features/authFiles/hooks/useAuthFilesData.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState, type ChangeEvent, type RefObject } from 'react'; import { useTranslation } from 'react-i18next'; -import { authFilesApi } from '@/services/api'; +import { authFilesApi, isAuthFileInvalidJsonObjectError } from '@/services/api'; import { apiClient } from '@/services/api/client'; import { useNotificationStore } from '@/stores'; import type { AuthFileItem } from '@/types'; @@ -60,6 +60,16 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles const fileInputRef = useRef(null); const selectionCount = selectedFiles.size; + const resolveStatusUpdateErrorMessage = useCallback( + (err: unknown) => { + if (isAuthFileInvalidJsonObjectError(err)) { + return t('auth_files.prefix_proxy_invalid_json'); + } + return err instanceof Error ? err.message : ''; + }, + [t] + ); + const toggleSelect = useCallback((name: string) => { setSelectedFiles((prev) => { const next = new Set(prev); @@ -337,10 +347,9 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: nextDisabled } : f))); try { - const res = await authFilesApi.setStatus(name, nextDisabled); - setFiles((prev) => - prev.map((f) => (f.name === name ? { ...f, disabled: res.disabled } : f)) - ); + await authFilesApi.setStatus(name, nextDisabled); + await loadFiles(); + void refreshKeyStats().catch(() => {}); showNotification( enabled ? t('auth_files.status_enabled_success', { name }) @@ -348,7 +357,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles 'success' ); } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; + const errorMessage = resolveStatusUpdateErrorMessage(err); setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: previousDisabled } : f)) ); @@ -362,7 +371,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles }); } }, - [showNotification, t] + [loadFiles, refreshKeyStats, resolveStatusUpdateErrorMessage, showNotification, t] ); const batchSetStatus = useCallback( @@ -372,6 +381,9 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles const targetNames = new Set(uniqueNames); const nextDisabled = !enabled; + const previousDisabled = new Map( + files.map((file) => [file.name, file.disabled === true] as const) + ); setFiles((prev) => prev.map((file) => @@ -385,31 +397,26 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles let successCount = 0; let failCount = 0; - const failedNames = new Set(); - const confirmedDisabled = new Map(); - results.forEach((result, index) => { - const name = uniqueNames[index]; + results.forEach((result) => { if (result.status === 'fulfilled') { successCount++; - confirmedDisabled.set(name, result.value.disabled); } else { failCount++; - failedNames.add(name); } }); - setFiles((prev) => - prev.map((file) => { - if (failedNames.has(file.name)) { - return { ...file, disabled: !nextDisabled }; - } - if (confirmedDisabled.has(file.name)) { - return { ...file, disabled: confirmedDisabled.get(file.name) }; - } - return file; - }) - ); + if (successCount > 0) { + await loadFiles(); + void refreshKeyStats().catch(() => {}); + } else { + setFiles((prev) => + prev.map((file) => { + if (!targetNames.has(file.name)) return file; + return { ...file, disabled: previousDisabled.get(file.name) === true }; + }) + ); + } if (failCount === 0) { showNotification(t('auth_files.batch_status_success', { count: successCount }), 'success'); @@ -422,7 +429,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles deselectAll(); }, - [deselectAll, showNotification, t] + [deselectAll, files, loadFiles, refreshKeyStats, showNotification, t] ); const batchDelete = useCallback( diff --git a/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts b/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts index 5a6232a..e99c534 100644 --- a/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts +++ b/src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts @@ -260,8 +260,7 @@ export function useAuthFilesPrefixProxyEditor( }); try { - const file = new File([payload], name, { type: 'application/json' }); - await authFilesApi.upload(file); + await authFilesApi.saveText(name, payload); showNotification(t('auth_files.prefix_proxy_saved_success', { name }), 'success'); await loadFiles(); await loadKeyStats(); diff --git a/src/services/api/authFiles.ts b/src/services/api/authFiles.ts index 8d165ba..ad53779 100644 --- a/src/services/api/authFiles.ts +++ b/src/services/api/authFiles.ts @@ -9,12 +9,39 @@ import type { OAuthModelAliasEntry } from '@/types'; type StatusError = { status?: number }; type AuthFileStatusResponse = { status: string; disabled: boolean }; +export const AUTH_FILE_INVALID_JSON_OBJECT_ERROR = 'AUTH_FILE_INVALID_JSON_OBJECT'; + const getStatusCode = (err: unknown): number | undefined => { if (!err || typeof err !== 'object') return undefined; if ('status' in err) return (err as StatusError).status; return undefined; }; +const parseAuthFileJsonObject = (rawText: string): Record => { + const trimmed = rawText.trim(); + + let parsed: unknown; + try { + parsed = JSON.parse(trimmed) as unknown; + } catch { + throw new Error(AUTH_FILE_INVALID_JSON_OBJECT_ERROR); + } + + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error(AUTH_FILE_INVALID_JSON_OBJECT_ERROR); + } + + return { ...(parsed as Record) }; +}; + +const saveAuthFileText = async (name: string, text: string) => { + const file = new File([text], name, { type: 'application/json' }); + await authFilesApi.upload(file); +}; + +export const isAuthFileInvalidJsonObjectError = (err: unknown): boolean => + err instanceof Error && err.message === AUTH_FILE_INVALID_JSON_OBJECT_ERROR; + const normalizeOauthExcludedModels = (payload: unknown): Record => { if (!payload || typeof payload !== 'object') return {}; @@ -105,8 +132,18 @@ const OAUTH_MODEL_ALIAS_ENDPOINT = '/oauth-model-alias'; export const authFilesApi = { list: () => apiClient.get('/auth-files'), - setStatus: (name: string, disabled: boolean) => - apiClient.patch('/auth-files/status', { name, disabled }), + async setStatus(name: string, disabled: boolean): Promise { + const json = await authFilesApi.downloadJsonObject(name); + + if (disabled) { + json.disabled = true; + } else { + delete json.disabled; + } + + await authFilesApi.saveJsonObject(name, json); + return { status: disabled ? 'disabled' : 'enabled', disabled }; + }, upload: (file: File) => { const formData = new FormData(); @@ -126,6 +163,16 @@ export const authFilesApi = { return blob.text(); }, + async downloadJsonObject(name: string): Promise> { + const rawText = await authFilesApi.downloadText(name); + return parseAuthFileJsonObject(rawText); + }, + + saveText: (name: string, text: string) => saveAuthFileText(name, text), + + saveJsonObject: (name: string, json: Record) => + saveAuthFileText(name, JSON.stringify(json)), + // OAuth 排除模型 async getOauthExcludedModels(): Promise> { const data = await apiClient.get('/oauth-excluded-models');