mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 11:10:49 +08:00
refactor(auth-files): split AuthFilesPage
This commit is contained in:
318
src/features/authFiles/hooks/useAuthFilesData.ts
Normal file
318
src/features/authFiles/hooks/useAuthFilesData.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { useCallback, useRef, useState, type ChangeEvent, type RefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authFilesApi } from '@/services/api';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
import { useNotificationStore } from '@/stores';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { formatFileSize } from '@/utils/format';
|
||||
import { MAX_AUTH_FILE_SIZE, getTypeLabel, isRuntimeOnlyAuthFile } from '@/features/authFiles/constants';
|
||||
|
||||
type DeleteAllOptions = {
|
||||
filter: string;
|
||||
onResetFilterToAll: () => void;
|
||||
};
|
||||
|
||||
export type UseAuthFilesDataResult = {
|
||||
files: AuthFileItem[];
|
||||
loading: boolean;
|
||||
error: string;
|
||||
uploading: boolean;
|
||||
deleting: string | null;
|
||||
deletingAll: boolean;
|
||||
statusUpdating: Record<string, boolean>;
|
||||
fileInputRef: RefObject<HTMLInputElement | null>;
|
||||
loadFiles: () => Promise<void>;
|
||||
handleUploadClick: () => void;
|
||||
handleFileChange: (event: ChangeEvent<HTMLInputElement>) => Promise<void>;
|
||||
handleDelete: (name: string) => void;
|
||||
handleDeleteAll: (options: DeleteAllOptions) => void;
|
||||
handleDownload: (name: string) => Promise<void>;
|
||||
handleStatusToggle: (item: AuthFileItem, enabled: boolean) => Promise<void>;
|
||||
};
|
||||
|
||||
export type UseAuthFilesDataOptions = {
|
||||
refreshKeyStats: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFilesDataResult {
|
||||
const { refreshKeyStats } = options;
|
||||
const { t } = useTranslation();
|
||||
const { showNotification, showConfirmation } = useNotificationStore();
|
||||
|
||||
const [files, setFiles] = useState<AuthFileItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
const [deletingAll, setDeletingAll] = useState(false);
|
||||
const [statusUpdating, setStatusUpdating] = useState<Record<string, boolean>>({});
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const loadFiles = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await authFilesApi.list();
|
||||
setFiles(data?.files || []);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed');
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const handleUploadClick = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const fileList = event.target.files;
|
||||
if (!fileList || fileList.length === 0) return;
|
||||
|
||||
const filesToUpload = Array.from(fileList);
|
||||
const validFiles: File[] = [];
|
||||
const invalidFiles: string[] = [];
|
||||
const oversizedFiles: string[] = [];
|
||||
|
||||
filesToUpload.forEach((file) => {
|
||||
if (!file.name.endsWith('.json')) {
|
||||
invalidFiles.push(file.name);
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_AUTH_FILE_SIZE) {
|
||||
oversizedFiles.push(file.name);
|
||||
return;
|
||||
}
|
||||
validFiles.push(file);
|
||||
});
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
showNotification(t('auth_files.upload_error_json'), 'error');
|
||||
}
|
||||
if (oversizedFiles.length > 0) {
|
||||
showNotification(
|
||||
t('auth_files.upload_error_size', { maxSize: formatFileSize(MAX_AUTH_FILE_SIZE) }),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
let successCount = 0;
|
||||
const failed: { name: string; message: string }[] = [];
|
||||
|
||||
for (const file of validFiles) {
|
||||
try {
|
||||
await authFilesApi.upload(file);
|
||||
successCount++;
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
failed.push({ name: file.name, message: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
const suffix = validFiles.length > 1 ? ` (${successCount}/${validFiles.length})` : '';
|
||||
showNotification(
|
||||
`${t('auth_files.upload_success')}${suffix}`,
|
||||
failed.length ? 'warning' : 'success'
|
||||
);
|
||||
await loadFiles();
|
||||
await refreshKeyStats();
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
const details = failed.map((item) => `${item.name}: ${item.message}`).join('; ');
|
||||
showNotification(`${t('notification.upload_failed')}: ${details}`, 'error');
|
||||
}
|
||||
|
||||
setUploading(false);
|
||||
event.target.value = '';
|
||||
},
|
||||
[loadFiles, refreshKeyStats, showNotification, t]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(name: string) => {
|
||||
showConfirmation({
|
||||
title: t('auth_files.delete_title', { defaultValue: 'Delete File' }),
|
||||
message: `${t('auth_files.delete_confirm')} "${name}" ?`,
|
||||
variant: 'danger',
|
||||
confirmText: t('common.confirm'),
|
||||
onConfirm: async () => {
|
||||
setDeleting(name);
|
||||
try {
|
||||
await authFilesApi.deleteFile(name);
|
||||
showNotification(t('auth_files.delete_success'), 'success');
|
||||
setFiles((prev) => prev.filter((item) => item.name !== name));
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error');
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[showConfirmation, showNotification, t]
|
||||
);
|
||||
|
||||
const handleDeleteAll = useCallback(
|
||||
(deleteAllOptions: DeleteAllOptions) => {
|
||||
const { filter, onResetFilterToAll } = deleteAllOptions;
|
||||
const isFiltered = filter !== 'all';
|
||||
const typeLabel = isFiltered ? getTypeLabel(t, filter) : t('auth_files.filter_all');
|
||||
const confirmMessage = isFiltered
|
||||
? t('auth_files.delete_filtered_confirm', { type: typeLabel })
|
||||
: t('auth_files.delete_all_confirm');
|
||||
|
||||
showConfirmation({
|
||||
title: t('auth_files.delete_all_title', { defaultValue: 'Delete All Files' }),
|
||||
message: confirmMessage,
|
||||
variant: 'danger',
|
||||
confirmText: t('common.confirm'),
|
||||
onConfirm: async () => {
|
||||
setDeletingAll(true);
|
||||
try {
|
||||
if (!isFiltered) {
|
||||
await authFilesApi.deleteAll();
|
||||
showNotification(t('auth_files.delete_all_success'), 'success');
|
||||
setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file)));
|
||||
} else {
|
||||
const filesToDelete = files.filter(
|
||||
(f) => f.type === filter && !isRuntimeOnlyAuthFile(f)
|
||||
);
|
||||
|
||||
if (filesToDelete.length === 0) {
|
||||
showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info');
|
||||
setDeletingAll(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
const deletedNames: string[] = [];
|
||||
|
||||
for (const file of filesToDelete) {
|
||||
try {
|
||||
await authFilesApi.deleteFile(file.name);
|
||||
success++;
|
||||
deletedNames.push(file.name);
|
||||
} catch {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name)));
|
||||
|
||||
if (failed === 0) {
|
||||
showNotification(
|
||||
t('auth_files.delete_filtered_success', { count: success, type: typeLabel }),
|
||||
'success'
|
||||
);
|
||||
} else {
|
||||
showNotification(
|
||||
t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }),
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
onResetFilterToAll();
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error');
|
||||
} finally {
|
||||
setDeletingAll(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[files, showConfirmation, showNotification, t]
|
||||
);
|
||||
|
||||
const handleDownload = useCallback(
|
||||
async (name: string) => {
|
||||
try {
|
||||
const response = await apiClient.getRaw(
|
||||
`/auth-files/download?name=${encodeURIComponent(name)}`,
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
showNotification(t('auth_files.download_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('notification.download_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
},
|
||||
[showNotification, t]
|
||||
);
|
||||
|
||||
const handleStatusToggle = useCallback(
|
||||
async (item: AuthFileItem, enabled: boolean) => {
|
||||
const name = item.name;
|
||||
const nextDisabled = !enabled;
|
||||
const previousDisabled = item.disabled === true;
|
||||
|
||||
setStatusUpdating((prev) => ({ ...prev, [name]: true }));
|
||||
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))
|
||||
);
|
||||
showNotification(
|
||||
enabled
|
||||
? t('auth_files.status_enabled_success', { name })
|
||||
: t('auth_files.status_disabled_success', { name }),
|
||||
'success'
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
setFiles((prev) =>
|
||||
prev.map((f) => (f.name === name ? { ...f, disabled: previousDisabled } : f))
|
||||
);
|
||||
showNotification(`${t('notification.update_failed')}: ${errorMessage}`, 'error');
|
||||
} finally {
|
||||
setStatusUpdating((prev) => {
|
||||
if (!prev[name]) return prev;
|
||||
const next = { ...prev };
|
||||
delete next[name];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
[showNotification, t]
|
||||
);
|
||||
|
||||
return {
|
||||
files,
|
||||
loading,
|
||||
error,
|
||||
uploading,
|
||||
deleting,
|
||||
deletingAll,
|
||||
statusUpdating,
|
||||
fileInputRef,
|
||||
loadFiles,
|
||||
handleUploadClick,
|
||||
handleFileChange,
|
||||
handleDelete,
|
||||
handleDeleteAll,
|
||||
handleDownload,
|
||||
handleStatusToggle
|
||||
};
|
||||
}
|
||||
86
src/features/authFiles/hooks/useAuthFilesModels.ts
Normal file
86
src/features/authFiles/hooks/useAuthFilesModels.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authFilesApi } from '@/services/api';
|
||||
import { useNotificationStore } from '@/stores';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import type { AuthFileModelItem } from '@/features/authFiles/constants';
|
||||
|
||||
type ModelsError = 'unsupported' | null;
|
||||
|
||||
export type UseAuthFilesModelsResult = {
|
||||
modelsModalOpen: boolean;
|
||||
modelsLoading: boolean;
|
||||
modelsList: AuthFileModelItem[];
|
||||
modelsFileName: string;
|
||||
modelsFileType: string;
|
||||
modelsError: ModelsError;
|
||||
showModels: (item: AuthFileItem) => Promise<void>;
|
||||
closeModelsModal: () => void;
|
||||
};
|
||||
|
||||
export function useAuthFilesModels(): UseAuthFilesModelsResult {
|
||||
const { t } = useTranslation();
|
||||
const showNotification = useNotificationStore((state) => state.showNotification);
|
||||
|
||||
const [modelsModalOpen, setModelsModalOpen] = useState(false);
|
||||
const [modelsLoading, setModelsLoading] = useState(false);
|
||||
const [modelsList, setModelsList] = useState<AuthFileModelItem[]>([]);
|
||||
const [modelsFileName, setModelsFileName] = useState('');
|
||||
const [modelsFileType, setModelsFileType] = useState('');
|
||||
const [modelsError, setModelsError] = useState<ModelsError>(null);
|
||||
const modelsCacheRef = useRef<Map<string, AuthFileModelItem[]>>(new Map());
|
||||
|
||||
const closeModelsModal = useCallback(() => {
|
||||
setModelsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const showModels = useCallback(
|
||||
async (item: AuthFileItem) => {
|
||||
setModelsFileName(item.name);
|
||||
setModelsFileType(item.type || '');
|
||||
setModelsList([]);
|
||||
setModelsError(null);
|
||||
setModelsModalOpen(true);
|
||||
|
||||
const cached = modelsCacheRef.current.get(item.name);
|
||||
if (cached) {
|
||||
setModelsList(cached);
|
||||
setModelsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setModelsLoading(true);
|
||||
try {
|
||||
const models = await authFilesApi.getModelsForAuthFile(item.name);
|
||||
modelsCacheRef.current.set(item.name, models);
|
||||
setModelsList(models);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
if (
|
||||
errorMessage.includes('404') ||
|
||||
errorMessage.includes('not found') ||
|
||||
errorMessage.includes('Not Found')
|
||||
) {
|
||||
setModelsError('unsupported');
|
||||
} else {
|
||||
showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
} finally {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
},
|
||||
[showNotification, t]
|
||||
);
|
||||
|
||||
return {
|
||||
modelsModalOpen,
|
||||
modelsLoading,
|
||||
modelsList,
|
||||
modelsFileName,
|
||||
modelsFileType,
|
||||
modelsError,
|
||||
showModels,
|
||||
closeModelsModal
|
||||
};
|
||||
}
|
||||
|
||||
504
src/features/authFiles/hooks/useAuthFilesOauth.tsx
Normal file
504
src/features/authFiles/hooks/useAuthFilesOauth.tsx
Normal file
@@ -0,0 +1,504 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { authFilesApi } from '@/services/api';
|
||||
import { useNotificationStore } from '@/stores';
|
||||
import type { AuthFileItem, OAuthModelAliasEntry } from '@/types';
|
||||
import type { AuthFileModelItem } from '@/features/authFiles/constants';
|
||||
import { normalizeProviderKey } from '@/features/authFiles/constants';
|
||||
|
||||
type UnsupportedError = 'unsupported' | null;
|
||||
type ViewMode = 'diagram' | 'list';
|
||||
|
||||
export type UseAuthFilesOauthResult = {
|
||||
excluded: Record<string, string[]>;
|
||||
excludedError: UnsupportedError;
|
||||
modelAlias: Record<string, OAuthModelAliasEntry[]>;
|
||||
modelAliasError: UnsupportedError;
|
||||
allProviderModels: Record<string, AuthFileModelItem[]>;
|
||||
providerList: string[];
|
||||
loadExcluded: () => Promise<void>;
|
||||
loadModelAlias: () => Promise<void>;
|
||||
deleteExcluded: (provider: string) => void;
|
||||
deleteModelAlias: (provider: string) => void;
|
||||
handleMappingUpdate: (provider: string, sourceModel: string, newAlias: string) => Promise<void>;
|
||||
handleDeleteLink: (provider: string, sourceModel: string, alias: string) => void;
|
||||
handleToggleFork: (
|
||||
provider: string,
|
||||
sourceModel: string,
|
||||
alias: string,
|
||||
fork: boolean
|
||||
) => Promise<void>;
|
||||
handleRenameAlias: (oldAlias: string, newAlias: string) => Promise<void>;
|
||||
handleDeleteAlias: (aliasName: string) => void;
|
||||
};
|
||||
|
||||
export type UseAuthFilesOauthOptions = {
|
||||
viewMode: ViewMode;
|
||||
files: AuthFileItem[];
|
||||
};
|
||||
|
||||
export function useAuthFilesOauth(options: UseAuthFilesOauthOptions): UseAuthFilesOauthResult {
|
||||
const { viewMode, files } = options;
|
||||
const { t } = useTranslation();
|
||||
const { showNotification, showConfirmation } = useNotificationStore();
|
||||
|
||||
const [excluded, setExcluded] = useState<Record<string, string[]>>({});
|
||||
const [excludedError, setExcludedError] = useState<UnsupportedError>(null);
|
||||
const [modelAlias, setModelAlias] = useState<Record<string, OAuthModelAliasEntry[]>>({});
|
||||
const [modelAliasError, setModelAliasError] = useState<UnsupportedError>(null);
|
||||
const [allProviderModels, setAllProviderModels] = useState<Record<string, AuthFileModelItem[]>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const excludedUnsupportedRef = useRef(false);
|
||||
const mappingsUnsupportedRef = useRef(false);
|
||||
|
||||
const providerList = useMemo(() => {
|
||||
const providers = new Set<string>();
|
||||
|
||||
Object.keys(modelAlias).forEach((provider) => {
|
||||
const key = provider.trim().toLowerCase();
|
||||
if (key) providers.add(key);
|
||||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
if (typeof file.type === 'string') {
|
||||
const key = file.type.trim().toLowerCase();
|
||||
if (key) providers.add(key);
|
||||
}
|
||||
if (typeof file.provider === 'string') {
|
||||
const key = file.provider.trim().toLowerCase();
|
||||
if (key) providers.add(key);
|
||||
}
|
||||
});
|
||||
return Array.from(providers);
|
||||
}, [files, modelAlias]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'diagram') return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadAllModels = async () => {
|
||||
if (providerList.length === 0) {
|
||||
if (!cancelled) setAllProviderModels({});
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
providerList.map(async (provider) => {
|
||||
try {
|
||||
const models = await authFilesApi.getModelDefinitions(provider);
|
||||
return { provider, models };
|
||||
} catch {
|
||||
return { provider, models: [] as AuthFileModelItem[] };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const nextModels: Record<string, AuthFileModelItem[]> = {};
|
||||
results.forEach(({ provider, models }) => {
|
||||
if (models.length > 0) {
|
||||
nextModels[provider] = models;
|
||||
}
|
||||
});
|
||||
|
||||
setAllProviderModels(nextModels);
|
||||
};
|
||||
|
||||
void loadAllModels();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [providerList, viewMode]);
|
||||
|
||||
const loadExcluded = useCallback(async () => {
|
||||
try {
|
||||
const res = await authFilesApi.getOauthExcludedModels();
|
||||
excludedUnsupportedRef.current = false;
|
||||
setExcluded(res || {});
|
||||
setExcludedError(null);
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
typeof err === 'object' && err !== null && 'status' in err
|
||||
? (err as { status?: unknown }).status
|
||||
: undefined;
|
||||
|
||||
if (status === 404) {
|
||||
setExcluded({});
|
||||
setExcludedError('unsupported');
|
||||
if (!excludedUnsupportedRef.current) {
|
||||
excludedUnsupportedRef.current = true;
|
||||
showNotification(t('oauth_excluded.upgrade_required'), 'warning');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 静默失败
|
||||
}
|
||||
}, [showNotification, t]);
|
||||
|
||||
const loadModelAlias = useCallback(async () => {
|
||||
try {
|
||||
const res = await authFilesApi.getOauthModelAlias();
|
||||
mappingsUnsupportedRef.current = false;
|
||||
setModelAlias(res || {});
|
||||
setModelAliasError(null);
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
typeof err === 'object' && err !== null && 'status' in err
|
||||
? (err as { status?: unknown }).status
|
||||
: undefined;
|
||||
|
||||
if (status === 404) {
|
||||
setModelAlias({});
|
||||
setModelAliasError('unsupported');
|
||||
if (!mappingsUnsupportedRef.current) {
|
||||
mappingsUnsupportedRef.current = true;
|
||||
showNotification(t('oauth_model_alias.upgrade_required'), 'warning');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 静默失败
|
||||
}
|
||||
}, [showNotification, t]);
|
||||
|
||||
const deleteExcluded = useCallback(
|
||||
(provider: string) => {
|
||||
const providerLabel = provider.trim() || provider;
|
||||
showConfirmation({
|
||||
title: t('oauth_excluded.delete_title', { defaultValue: 'Delete Exclusion' }),
|
||||
message: t('oauth_excluded.delete_confirm', { provider: providerLabel }),
|
||||
variant: 'danger',
|
||||
confirmText: t('common.confirm'),
|
||||
onConfirm: async () => {
|
||||
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<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');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[loadExcluded, showConfirmation, showNotification, t]
|
||||
);
|
||||
|
||||
const deleteModelAlias = useCallback(
|
||||
(provider: string) => {
|
||||
showConfirmation({
|
||||
title: t('oauth_model_alias.delete_title', { defaultValue: 'Delete Mappings' }),
|
||||
message: t('oauth_model_alias.delete_confirm', { provider }),
|
||||
variant: 'danger',
|
||||
confirmText: t('common.confirm'),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await authFilesApi.deleteOauthModelAlias(provider);
|
||||
await loadModelAlias();
|
||||
showNotification(t('oauth_model_alias.delete_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('oauth_model_alias.delete_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[loadModelAlias, showConfirmation, showNotification, t]
|
||||
);
|
||||
|
||||
const handleMappingUpdate = useCallback(
|
||||
async (provider: string, sourceModel: string, newAlias: string) => {
|
||||
if (!provider || !sourceModel || !newAlias) return;
|
||||
const normalizedProvider = normalizeProviderKey(provider);
|
||||
if (!normalizedProvider) return;
|
||||
|
||||
const providerKey = Object.keys(modelAlias).find(
|
||||
(key) => normalizeProviderKey(key) === normalizedProvider
|
||||
);
|
||||
const currentMappings = (providerKey ? modelAlias[providerKey] : null) ?? [];
|
||||
|
||||
const nameTrim = sourceModel.trim();
|
||||
const aliasTrim = newAlias.trim();
|
||||
const nameKey = nameTrim.toLowerCase();
|
||||
const aliasKey = aliasTrim.toLowerCase();
|
||||
|
||||
if (
|
||||
currentMappings.some(
|
||||
(m) =>
|
||||
(m.name ?? '').trim().toLowerCase() === nameKey &&
|
||||
(m.alias ?? '').trim().toLowerCase() === aliasKey
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextMappings: OAuthModelAliasEntry[] = [
|
||||
...currentMappings,
|
||||
{ name: nameTrim, alias: aliasTrim, fork: true }
|
||||
];
|
||||
|
||||
try {
|
||||
await authFilesApi.saveOauthModelAlias(normalizedProvider, nextMappings);
|
||||
await loadModelAlias();
|
||||
showNotification(t('oauth_model_alias.save_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
},
|
||||
[loadModelAlias, modelAlias, showNotification, t]
|
||||
);
|
||||
|
||||
const handleDeleteLink = useCallback(
|
||||
(provider: string, sourceModel: string, alias: string) => {
|
||||
const nameTrim = sourceModel.trim();
|
||||
const aliasTrim = alias.trim();
|
||||
if (!provider || !nameTrim || !aliasTrim) return;
|
||||
|
||||
showConfirmation({
|
||||
title: t('oauth_model_alias.delete_link_title', { defaultValue: 'Unlink mapping' }),
|
||||
message: (
|
||||
<Trans
|
||||
i18nKey="oauth_model_alias.delete_link_confirm"
|
||||
values={{ provider, sourceModel: nameTrim, alias: aliasTrim }}
|
||||
components={{ code: <code /> }}
|
||||
/>
|
||||
),
|
||||
variant: 'danger',
|
||||
confirmText: t('common.confirm'),
|
||||
onConfirm: async () => {
|
||||
const normalizedProvider = normalizeProviderKey(provider);
|
||||
const providerKey = Object.keys(modelAlias).find(
|
||||
(key) => normalizeProviderKey(key) === normalizedProvider
|
||||
);
|
||||
const currentMappings = (providerKey ? modelAlias[providerKey] : null) ?? [];
|
||||
const nameKey = nameTrim.toLowerCase();
|
||||
const aliasKey = aliasTrim.toLowerCase();
|
||||
const nextMappings = currentMappings.filter(
|
||||
(m) =>
|
||||
(m.name ?? '').trim().toLowerCase() !== nameKey ||
|
||||
(m.alias ?? '').trim().toLowerCase() !== aliasKey
|
||||
);
|
||||
if (nextMappings.length === currentMappings.length) return;
|
||||
|
||||
try {
|
||||
if (nextMappings.length === 0) {
|
||||
await authFilesApi.deleteOauthModelAlias(normalizedProvider);
|
||||
} else {
|
||||
await authFilesApi.saveOauthModelAlias(normalizedProvider, nextMappings);
|
||||
}
|
||||
await loadModelAlias();
|
||||
showNotification(t('oauth_model_alias.save_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[loadModelAlias, modelAlias, showConfirmation, showNotification, t]
|
||||
);
|
||||
|
||||
const handleToggleFork = useCallback(
|
||||
async (provider: string, sourceModel: string, alias: string, fork: boolean) => {
|
||||
const normalizedProvider = normalizeProviderKey(provider);
|
||||
if (!normalizedProvider) return;
|
||||
|
||||
const providerKey = Object.keys(modelAlias).find(
|
||||
(key) => normalizeProviderKey(key) === normalizedProvider
|
||||
);
|
||||
const currentMappings = (providerKey ? modelAlias[providerKey] : null) ?? [];
|
||||
const nameKey = sourceModel.trim().toLowerCase();
|
||||
const aliasKey = alias.trim().toLowerCase();
|
||||
let changed = false;
|
||||
|
||||
const nextMappings = currentMappings.map((m) => {
|
||||
const mName = (m.name ?? '').trim().toLowerCase();
|
||||
const mAlias = (m.alias ?? '').trim().toLowerCase();
|
||||
if (mName === nameKey && mAlias === aliasKey) {
|
||||
changed = true;
|
||||
return fork ? { ...m, fork: true } : { name: m.name, alias: m.alias };
|
||||
}
|
||||
return m;
|
||||
});
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
try {
|
||||
await authFilesApi.saveOauthModelAlias(normalizedProvider, nextMappings);
|
||||
await loadModelAlias();
|
||||
showNotification(t('oauth_model_alias.save_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
},
|
||||
[loadModelAlias, modelAlias, showNotification, t]
|
||||
);
|
||||
|
||||
const handleRenameAlias = useCallback(
|
||||
async (oldAlias: string, newAlias: string) => {
|
||||
const oldTrim = oldAlias.trim();
|
||||
const newTrim = newAlias.trim();
|
||||
if (!oldTrim || !newTrim || oldTrim === newTrim) return;
|
||||
|
||||
const oldKey = oldTrim.toLowerCase();
|
||||
const providersToUpdate = Object.entries(modelAlias).filter(([_, mappings]) =>
|
||||
mappings.some((m) => (m.alias ?? '').trim().toLowerCase() === oldKey)
|
||||
);
|
||||
|
||||
if (providersToUpdate.length === 0) return;
|
||||
|
||||
let hadFailure = false;
|
||||
let failureMessage = '';
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
providersToUpdate.map(([provider, mappings]) => {
|
||||
const nextMappings = mappings.map((m) =>
|
||||
(m.alias ?? '').trim().toLowerCase() === oldKey ? { ...m, alias: newTrim } : m
|
||||
);
|
||||
return authFilesApi.saveOauthModelAlias(provider, nextMappings);
|
||||
})
|
||||
);
|
||||
|
||||
const failures = results.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected'
|
||||
);
|
||||
|
||||
if (failures.length > 0) {
|
||||
hadFailure = true;
|
||||
const reason = failures[0].reason;
|
||||
failureMessage = reason instanceof Error ? reason.message : String(reason ?? '');
|
||||
}
|
||||
} finally {
|
||||
await loadModelAlias();
|
||||
}
|
||||
|
||||
if (hadFailure) {
|
||||
showNotification(
|
||||
failureMessage
|
||||
? `${t('oauth_model_alias.save_failed')}: ${failureMessage}`
|
||||
: t('oauth_model_alias.save_failed'),
|
||||
'error'
|
||||
);
|
||||
} else {
|
||||
showNotification(t('oauth_model_alias.save_success'), 'success');
|
||||
}
|
||||
},
|
||||
[loadModelAlias, modelAlias, showNotification, t]
|
||||
);
|
||||
|
||||
const handleDeleteAlias = useCallback(
|
||||
(aliasName: string) => {
|
||||
const aliasTrim = aliasName.trim();
|
||||
if (!aliasTrim) return;
|
||||
const aliasKey = aliasTrim.toLowerCase();
|
||||
const providersToUpdate = Object.entries(modelAlias).filter(([_, mappings]) =>
|
||||
mappings.some((m) => (m.alias ?? '').trim().toLowerCase() === aliasKey)
|
||||
);
|
||||
|
||||
if (providersToUpdate.length === 0) return;
|
||||
|
||||
showConfirmation({
|
||||
title: t('oauth_model_alias.delete_alias_title', { defaultValue: 'Delete Alias' }),
|
||||
message: (
|
||||
<Trans
|
||||
i18nKey="oauth_model_alias.delete_alias_confirm"
|
||||
values={{ alias: aliasTrim }}
|
||||
components={{ code: <code /> }}
|
||||
/>
|
||||
),
|
||||
variant: 'danger',
|
||||
confirmText: t('common.confirm'),
|
||||
onConfirm: async () => {
|
||||
let hadFailure = false;
|
||||
let failureMessage = '';
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
providersToUpdate.map(([provider, mappings]) => {
|
||||
const nextMappings = mappings.filter(
|
||||
(m) => (m.alias ?? '').trim().toLowerCase() !== aliasKey
|
||||
);
|
||||
if (nextMappings.length === 0) {
|
||||
return authFilesApi.deleteOauthModelAlias(provider);
|
||||
}
|
||||
return authFilesApi.saveOauthModelAlias(provider, nextMappings);
|
||||
})
|
||||
);
|
||||
|
||||
const failures = results.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected'
|
||||
);
|
||||
|
||||
if (failures.length > 0) {
|
||||
hadFailure = true;
|
||||
const reason = failures[0].reason;
|
||||
failureMessage = reason instanceof Error ? reason.message : String(reason ?? '');
|
||||
}
|
||||
} finally {
|
||||
await loadModelAlias();
|
||||
}
|
||||
|
||||
if (hadFailure) {
|
||||
showNotification(
|
||||
failureMessage
|
||||
? `${t('oauth_model_alias.delete_failed')}: ${failureMessage}`
|
||||
: t('oauth_model_alias.delete_failed'),
|
||||
'error'
|
||||
);
|
||||
} else {
|
||||
showNotification(t('oauth_model_alias.delete_success'), 'success');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[loadModelAlias, modelAlias, showConfirmation, showNotification, t]
|
||||
);
|
||||
|
||||
return {
|
||||
excluded,
|
||||
excludedError,
|
||||
modelAlias,
|
||||
modelAliasError,
|
||||
allProviderModels,
|
||||
providerList,
|
||||
loadExcluded,
|
||||
loadModelAlias,
|
||||
deleteExcluded,
|
||||
deleteModelAlias,
|
||||
handleMappingUpdate,
|
||||
handleDeleteLink,
|
||||
handleToggleFork,
|
||||
handleRenameAlias,
|
||||
handleDeleteAlias
|
||||
};
|
||||
}
|
||||
|
||||
254
src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts
Normal file
254
src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authFilesApi } from '@/services/api';
|
||||
import { useNotificationStore } from '@/stores';
|
||||
import { formatFileSize } from '@/utils/format';
|
||||
import {
|
||||
MAX_AUTH_FILE_SIZE,
|
||||
normalizeExcludedModels,
|
||||
parseDisableCoolingValue,
|
||||
parseExcludedModelsText,
|
||||
parsePriorityValue
|
||||
} from '@/features/authFiles/constants';
|
||||
|
||||
export type PrefixProxyEditorField =
|
||||
| 'prefix'
|
||||
| 'proxyUrl'
|
||||
| 'priority'
|
||||
| 'excludedModelsText'
|
||||
| 'disableCooling';
|
||||
|
||||
export type PrefixProxyEditorState = {
|
||||
fileName: string;
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
originalText: string;
|
||||
rawText: string;
|
||||
json: Record<string, unknown> | null;
|
||||
prefix: string;
|
||||
proxyUrl: string;
|
||||
priority: string;
|
||||
excludedModelsText: string;
|
||||
disableCooling: string;
|
||||
};
|
||||
|
||||
export type UseAuthFilesPrefixProxyEditorOptions = {
|
||||
disableControls: boolean;
|
||||
loadFiles: () => Promise<void>;
|
||||
loadKeyStats: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type UseAuthFilesPrefixProxyEditorResult = {
|
||||
prefixProxyEditor: PrefixProxyEditorState | null;
|
||||
prefixProxyUpdatedText: string;
|
||||
prefixProxyDirty: boolean;
|
||||
openPrefixProxyEditor: (name: string) => Promise<void>;
|
||||
closePrefixProxyEditor: () => void;
|
||||
handlePrefixProxyChange: (field: PrefixProxyEditorField, value: string) => void;
|
||||
handlePrefixProxySave: () => Promise<void>;
|
||||
};
|
||||
|
||||
const buildPrefixProxyUpdatedText = (editor: PrefixProxyEditorState | null): string => {
|
||||
if (!editor?.json) return editor?.rawText ?? '';
|
||||
const next: Record<string, unknown> = { ...editor.json };
|
||||
if ('prefix' in next || editor.prefix.trim()) {
|
||||
next.prefix = editor.prefix;
|
||||
}
|
||||
if ('proxy_url' in next || editor.proxyUrl.trim()) {
|
||||
next.proxy_url = editor.proxyUrl;
|
||||
}
|
||||
|
||||
const parsedPriority = parsePriorityValue(editor.priority);
|
||||
if (parsedPriority !== undefined) {
|
||||
next.priority = parsedPriority;
|
||||
} else if ('priority' in next) {
|
||||
delete next.priority;
|
||||
}
|
||||
|
||||
const excludedModels = parseExcludedModelsText(editor.excludedModelsText);
|
||||
if (excludedModels.length > 0) {
|
||||
next.excluded_models = excludedModels;
|
||||
} else if ('excluded_models' in next) {
|
||||
delete next.excluded_models;
|
||||
}
|
||||
|
||||
const parsedDisableCooling = parseDisableCoolingValue(editor.disableCooling);
|
||||
if (parsedDisableCooling !== undefined) {
|
||||
next.disable_cooling = parsedDisableCooling;
|
||||
} else if ('disable_cooling' in next) {
|
||||
delete next.disable_cooling;
|
||||
}
|
||||
|
||||
return JSON.stringify(next);
|
||||
};
|
||||
|
||||
export function useAuthFilesPrefixProxyEditor(
|
||||
options: UseAuthFilesPrefixProxyEditorOptions
|
||||
): UseAuthFilesPrefixProxyEditorResult {
|
||||
const { disableControls, loadFiles, loadKeyStats } = options;
|
||||
const { t } = useTranslation();
|
||||
const showNotification = useNotificationStore((state) => state.showNotification);
|
||||
|
||||
const [prefixProxyEditor, setPrefixProxyEditor] = useState<PrefixProxyEditorState | null>(null);
|
||||
|
||||
const prefixProxyUpdatedText = buildPrefixProxyUpdatedText(prefixProxyEditor);
|
||||
const prefixProxyDirty =
|
||||
Boolean(prefixProxyEditor?.json) &&
|
||||
Boolean(prefixProxyEditor?.originalText) &&
|
||||
prefixProxyUpdatedText !== prefixProxyEditor?.originalText;
|
||||
|
||||
const closePrefixProxyEditor = () => {
|
||||
setPrefixProxyEditor(null);
|
||||
};
|
||||
|
||||
const openPrefixProxyEditor = async (name: string) => {
|
||||
if (disableControls) return;
|
||||
if (prefixProxyEditor?.fileName === name) {
|
||||
setPrefixProxyEditor(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setPrefixProxyEditor({
|
||||
fileName: name,
|
||||
loading: true,
|
||||
saving: false,
|
||||
error: null,
|
||||
originalText: '',
|
||||
rawText: '',
|
||||
json: null,
|
||||
prefix: '',
|
||||
proxyUrl: '',
|
||||
priority: '',
|
||||
excludedModelsText: '',
|
||||
disableCooling: ''
|
||||
});
|
||||
|
||||
try {
|
||||
const rawText = await authFilesApi.downloadText(name);
|
||||
const trimmed = rawText.trim();
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed) as unknown;
|
||||
} catch {
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return {
|
||||
...prev,
|
||||
loading: false,
|
||||
error: t('auth_files.prefix_proxy_invalid_json'),
|
||||
rawText: trimmed,
|
||||
originalText: trimmed
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return {
|
||||
...prev,
|
||||
loading: false,
|
||||
error: t('auth_files.prefix_proxy_invalid_json'),
|
||||
rawText: trimmed,
|
||||
originalText: trimmed
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const json = parsed as Record<string, unknown>;
|
||||
const originalText = JSON.stringify(json);
|
||||
const prefix = typeof json.prefix === 'string' ? json.prefix : '';
|
||||
const proxyUrl = typeof json.proxy_url === 'string' ? json.proxy_url : '';
|
||||
const priority = parsePriorityValue(json.priority);
|
||||
const excludedModels = normalizeExcludedModels(json.excluded_models);
|
||||
const disableCoolingValue = parseDisableCoolingValue(json.disable_cooling);
|
||||
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return {
|
||||
...prev,
|
||||
loading: false,
|
||||
originalText,
|
||||
rawText: originalText,
|
||||
json,
|
||||
prefix,
|
||||
proxyUrl,
|
||||
priority: priority !== undefined ? String(priority) : '',
|
||||
excludedModelsText: excludedModels.join('\n'),
|
||||
disableCooling:
|
||||
disableCoolingValue === undefined ? '' : disableCoolingValue ? 'true' : 'false',
|
||||
error: null
|
||||
};
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : t('notification.download_failed');
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return { ...prev, loading: false, error: errorMessage, rawText: '' };
|
||||
});
|
||||
showNotification(`${t('notification.download_failed')}: ${errorMessage}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrefixProxyChange = (field: PrefixProxyEditorField, value: string) => {
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev) return prev;
|
||||
if (field === 'prefix') return { ...prev, prefix: value };
|
||||
if (field === 'proxyUrl') return { ...prev, proxyUrl: value };
|
||||
if (field === 'priority') return { ...prev, priority: value };
|
||||
if (field === 'excludedModelsText') return { ...prev, excludedModelsText: value };
|
||||
return { ...prev, disableCooling: value };
|
||||
});
|
||||
};
|
||||
|
||||
const handlePrefixProxySave = async () => {
|
||||
if (!prefixProxyEditor?.json) return;
|
||||
if (!prefixProxyDirty) return;
|
||||
|
||||
const name = prefixProxyEditor.fileName;
|
||||
const payload = prefixProxyUpdatedText;
|
||||
const fileSize = new Blob([payload]).size;
|
||||
if (fileSize > MAX_AUTH_FILE_SIZE) {
|
||||
showNotification(
|
||||
t('auth_files.upload_error_size', { maxSize: formatFileSize(MAX_AUTH_FILE_SIZE) }),
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return { ...prev, saving: true };
|
||||
});
|
||||
|
||||
try {
|
||||
const file = new File([payload], name, { type: 'application/json' });
|
||||
await authFilesApi.upload(file);
|
||||
showNotification(t('auth_files.prefix_proxy_saved_success', { name }), 'success');
|
||||
await loadFiles();
|
||||
await loadKeyStats();
|
||||
setPrefixProxyEditor(null);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
showNotification(`${t('notification.upload_failed')}: ${errorMessage}`, 'error');
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return { ...prev, saving: false };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
prefixProxyEditor,
|
||||
prefixProxyUpdatedText,
|
||||
prefixProxyDirty,
|
||||
openPrefixProxyEditor,
|
||||
closePrefixProxyEditor,
|
||||
handlePrefixProxyChange,
|
||||
handlePrefixProxySave
|
||||
};
|
||||
}
|
||||
35
src/features/authFiles/hooks/useAuthFilesStats.ts
Normal file
35
src/features/authFiles/hooks/useAuthFilesStats.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { usageApi } from '@/services/api';
|
||||
import { collectUsageDetails, type KeyStats, type UsageDetail } from '@/utils/usage';
|
||||
|
||||
export type UseAuthFilesStatsResult = {
|
||||
keyStats: KeyStats;
|
||||
usageDetails: UsageDetail[];
|
||||
loadKeyStats: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function useAuthFilesStats(): UseAuthFilesStatsResult {
|
||||
const [keyStats, setKeyStats] = useState<KeyStats>({ bySource: {}, byAuthIndex: {} });
|
||||
const [usageDetails, setUsageDetails] = useState<UsageDetail[]>([]);
|
||||
const loadingKeyStatsRef = useRef(false);
|
||||
|
||||
const loadKeyStats = useCallback(async () => {
|
||||
if (loadingKeyStatsRef.current) return;
|
||||
loadingKeyStatsRef.current = true;
|
||||
try {
|
||||
const usageResponse = await usageApi.getUsage();
|
||||
const usageData = usageResponse?.usage ?? usageResponse;
|
||||
const stats = await usageApi.getKeyStats(usageData);
|
||||
setKeyStats(stats);
|
||||
const details = collectUsageDetails(usageData);
|
||||
setUsageDetails(details);
|
||||
} catch {
|
||||
// 静默失败
|
||||
} finally {
|
||||
loadingKeyStatsRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { keyStats, usageDetails, loadKeyStats };
|
||||
}
|
||||
|
||||
28
src/features/authFiles/hooks/useAuthFilesStatusBarCache.ts
Normal file
28
src/features/authFiles/hooks/useAuthFilesStatusBarCache.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { calculateStatusBarData, type UsageDetail } from '@/utils/usage';
|
||||
import { normalizeAuthIndexValue } from '@/features/authFiles/constants';
|
||||
|
||||
export type AuthFileStatusBarData = ReturnType<typeof calculateStatusBarData>;
|
||||
|
||||
export function useAuthFilesStatusBarCache(files: AuthFileItem[], usageDetails: UsageDetail[]) {
|
||||
return useMemo(() => {
|
||||
const cache = new Map<string, AuthFileStatusBarData>();
|
||||
|
||||
files.forEach((file) => {
|
||||
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
|
||||
const authIndexKey = normalizeAuthIndexValue(rawAuthIndex);
|
||||
|
||||
if (authIndexKey) {
|
||||
const filteredDetails = usageDetails.filter((detail) => {
|
||||
const detailAuthIndex = normalizeAuthIndexValue(detail.auth_index);
|
||||
return detailAuthIndex !== null && detailAuthIndex === authIndexKey;
|
||||
});
|
||||
cache.set(authIndexKey, calculateStatusBarData(filteredDetails));
|
||||
}
|
||||
});
|
||||
|
||||
return cache;
|
||||
}, [files, usageDetails]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user