mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(auth-files): enhance error handling for invalid JSON and refactor file upload logic
This commit is contained in:
@@ -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<HTMLInputElement | null>(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<string>();
|
||||
const confirmedDisabled = new Map<string, boolean>();
|
||||
|
||||
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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<string, unknown> => {
|
||||
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<string, unknown>) };
|
||||
};
|
||||
|
||||
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<string, string[]> => {
|
||||
if (!payload || typeof payload !== 'object') return {};
|
||||
|
||||
@@ -105,8 +132,18 @@ const OAUTH_MODEL_ALIAS_ENDPOINT = '/oauth-model-alias';
|
||||
export const authFilesApi = {
|
||||
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
||||
|
||||
setStatus: (name: string, disabled: boolean) =>
|
||||
apiClient.patch<AuthFileStatusResponse>('/auth-files/status', { name, disabled }),
|
||||
async setStatus(name: string, disabled: boolean): Promise<AuthFileStatusResponse> {
|
||||
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<Record<string, unknown>> {
|
||||
const rawText = await authFilesApi.downloadText(name);
|
||||
return parseAuthFileJsonObject(rawText);
|
||||
},
|
||||
|
||||
saveText: (name: string, text: string) => saveAuthFileText(name, text),
|
||||
|
||||
saveJsonObject: (name: string, json: Record<string, unknown>) =>
|
||||
saveAuthFileText(name, JSON.stringify(json)),
|
||||
|
||||
// OAuth 排除模型
|
||||
async getOauthExcludedModels(): Promise<Record<string, string[]>> {
|
||||
const data = await apiClient.get('/oauth-excluded-models');
|
||||
|
||||
Reference in New Issue
Block a user