refactor: remove unused fields and streamline auth file editor logic

This commit is contained in:
Supra4E8C
2026-05-02 03:22:32 +08:00
Unverified
parent b25f722fef
commit 7d3c57092b
3 changed files with 175 additions and 140 deletions
@@ -3,7 +3,6 @@ import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { Input } from '@/components/ui/Input';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import type {
PrefixProxyEditorField,
PrefixProxyEditorFieldValue,
@@ -132,18 +131,6 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('priority', e.target.value)}
/>
<div className="form-group">
<label>{t('auth_files.excluded_models_label')}</label>
<textarea
className="input"
value={editor.excludedModelsText}
placeholder={t('auth_files.excluded_models_placeholder')}
rows={4}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('excludedModelsText', e.target.value)}
/>
<div className="hint">{t('auth_files.excluded_models_hint')}</div>
</div>
<div className="form-group">
<label>{t('auth_files.headers_label')}</label>
<textarea
@@ -158,14 +145,6 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
{editor.headersError && <div className="error-box">{editor.headersError}</div>}
<div className="hint">{t('auth_files.headers_hint')}</div>
</div>
<Input
label={t('auth_files.disable_cooling_label')}
value={editor.disableCooling}
placeholder={t('auth_files.disable_cooling_placeholder')}
hint={t('auth_files.disable_cooling_hint')}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('disableCooling', e.target.value)}
/>
<Input
label={t('auth_files.note_label')}
value={editor.note}
@@ -174,18 +153,6 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('note', e.target.value)}
/>
{editor.isCodexFile && (
<div className="form-group">
<label>{t('ai_providers.codex_websockets_label')}</label>
<ToggleSwitch
checked={Boolean(editor.websockets)}
disabled={disableControls || editor.saving || !editor.json}
ariaLabel={t('ai_providers.codex_websockets_label')}
onChange={(value) => onChange('websockets', value)}
/>
<div className="hint">{t('ai_providers.codex_websockets_hint')}</div>
</div>
)}
</div>
</>
)}
@@ -1,18 +1,9 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { authFilesApi } from '@/services/api';
import { authFilesApi, type AuthFileFieldsPatch } from '@/services/api';
import type { AuthFileItem } from '@/types';
import { useNotificationStore } from '@/stores';
import { formatFileSize } from '@/utils/format';
import { MAX_AUTH_FILE_SIZE } from '@/utils/constants';
import {
applyCodexAuthFileWebsockets,
normalizeExcludedModels,
parseDisableCoolingValue,
parseExcludedModelsText,
parsePriorityValue,
readCodexAuthFileWebsockets,
} from '@/features/authFiles/constants';
import { parsePriorityValue } from '@/features/authFiles/constants';
type AuthFileHeaders = Record<string, string>;
type AuthFileHeadersErrorKey =
@@ -20,22 +11,13 @@ type AuthFileHeadersErrorKey =
| 'auth_files.headers_invalid_object'
| 'auth_files.headers_invalid_value';
export type PrefixProxyEditorField =
| 'prefix'
| 'proxyUrl'
| 'priority'
| 'excludedModelsText'
| 'disableCooling'
| 'websockets'
| 'note'
| 'headersText';
export type PrefixProxyEditorField = 'prefix' | 'proxyUrl' | 'priority' | 'note' | 'headersText';
export type PrefixProxyEditorFieldValue = string | boolean;
export type PrefixProxyEditorFieldValue = string;
export type PrefixProxyEditorState = {
fileName: string;
fileInfoText: string;
isCodexFile: boolean;
loading: boolean;
saving: boolean;
error: string | null;
@@ -45,9 +27,6 @@ export type PrefixProxyEditorState = {
prefix: string;
proxyUrl: string;
priority: string;
excludedModelsText: string;
disableCooling: string;
websockets: boolean;
note: string;
noteTouched: boolean;
headersText: string;
@@ -108,46 +87,113 @@ const parseHeadersText = (
return { value: parsed as AuthFileHeaders, errorKey: null };
};
const buildPrefixProxyUpdatedText = (
editor: PrefixProxyEditorState | null,
const normalizeTextField = (value: unknown): string =>
typeof value === 'string' ? value.trim() : '';
const hasKeys = (value: Record<string, unknown> | AuthFileFieldsPatch | null): boolean =>
Boolean(value && Object.keys(value).length > 0);
const normalizeHeaders = (value: unknown): AuthFileHeaders => {
if (!isRecordObject(value)) return {};
return Object.entries(value).reduce<AuthFileHeaders>((result, [key, rawValue]) => {
if (typeof rawValue !== 'string') return result;
const name = key.trim();
const headerValue = rawValue.trim();
if (!name || !headerValue) return result;
result[name] = headerValue;
return result;
}, {});
};
const buildHeadersPatch = (
originalHeaders: AuthFileHeaders,
nextHeaders: AuthFileHeaders
): AuthFileHeaders | undefined => {
const patch: AuthFileHeaders = {};
const nextNames = new Set(Object.keys(nextHeaders));
Object.entries(nextHeaders).forEach(([name, value]) => {
if (originalHeaders[name] !== value) {
patch[name] = value;
}
});
Object.keys(originalHeaders).forEach((name) => {
if (!nextNames.has(name)) {
patch[name] = '';
}
});
return Object.keys(patch).length > 0 ? patch : undefined;
};
const applyHeadersPatch = (
value: Record<string, unknown>,
headersPatch: AuthFileHeaders | undefined
) => {
if (!headersPatch) return;
const nextHeaders = normalizeHeaders(value.headers);
Object.entries(headersPatch).forEach(([name, rawValue]) => {
const headerName = name.trim();
if (!headerName) return;
const headerValue = rawValue.trim();
if (!headerValue) {
delete nextHeaders[headerName];
return;
}
nextHeaders[headerName] = headerValue;
});
if (Object.keys(nextHeaders).length > 0) {
value.headers = nextHeaders;
} else {
delete value.headers;
}
};
const buildAuthFileFieldsPatch = (
editor: PrefixProxyEditorState,
resolveHeadersError: (key: AuthFileHeadersErrorKey) => string
): 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;
): AuthFileFieldsPatch => {
const original = editor.json ?? {};
const patch: AuthFileFieldsPatch = {};
const originalPrefix = normalizeTextField(original.prefix);
const nextPrefix = editor.prefix.trim();
if (nextPrefix !== originalPrefix) {
patch.prefix = nextPrefix;
}
const parsedPriority = parsePriorityValue(editor.priority);
if (parsedPriority !== undefined) {
next.priority = parsedPriority;
} else if ('priority' in next) {
delete next.priority;
const originalProxyURL = normalizeTextField(original.proxy_url);
const nextProxyURL = editor.proxyUrl.trim();
if (nextProxyURL !== originalProxyURL) {
patch.proxy_url = nextProxyURL;
}
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;
const originalPriority = parsePriorityValue(original.priority);
const priorityText = editor.priority.trim();
const nextPriority = parsePriorityValue(priorityText);
if (!priorityText) {
if (originalPriority !== undefined && originalPriority !== 0) {
patch.priority = 0;
}
} else if (nextPriority !== undefined) {
if (nextPriority === 0) {
if (originalPriority !== undefined && originalPriority !== 0) {
patch.priority = 0;
}
} else if (nextPriority !== originalPriority) {
patch.priority = nextPriority;
}
}
if (editor.noteTouched) {
const noteValue = editor.note.trim();
if (noteValue) {
next.note = editor.note;
} else if ('note' in next) {
delete next.note;
const originalNote = normalizeTextField(original.note);
const nextNote = editor.note.trim();
if (nextNote !== originalNote) {
patch.note = nextNote;
}
}
@@ -156,16 +202,59 @@ const buildPrefixProxyUpdatedText = (
if (errorKey) {
throw new Error(resolveHeadersError(errorKey));
}
if (parsedHeaders) {
next.headers = parsedHeaders;
} else {
delete next.headers;
const headersPatch = buildHeadersPatch(
normalizeHeaders(original.headers),
normalizeHeaders(parsedHeaders ?? {})
);
if (headersPatch) {
patch.headers = headersPatch;
}
}
return JSON.stringify(
editor.isCodexFile ? applyCodexAuthFileWebsockets(next, editor.websockets) : next
);
return patch;
};
const buildPrefixProxyUpdatedText = (
editor: PrefixProxyEditorState | null,
resolveHeadersError: (key: AuthFileHeadersErrorKey) => string
): string => {
if (!editor?.json) return editor?.rawText ?? '';
const patch = buildAuthFileFieldsPatch(editor, resolveHeadersError);
const next: Record<string, unknown> = { ...editor.json };
if (patch.prefix !== undefined) {
if (patch.prefix) {
next.prefix = patch.prefix;
} else {
delete next.prefix;
}
}
if (patch.proxy_url !== undefined) {
if (patch.proxy_url) {
next.proxy_url = patch.proxy_url;
} else {
delete next.proxy_url;
}
}
if (patch.priority !== undefined) {
if (patch.priority === 0) {
delete next.priority;
} else {
next.priority = patch.priority;
}
}
if (patch.note !== undefined) {
if (patch.note) {
next.note = patch.note;
} else if ('note' in next) {
delete next.note;
}
}
applyHeadersPatch(next, patch.headers);
return JSON.stringify(next);
};
export function useAuthFilesPrefixProxyEditor(
@@ -185,10 +274,12 @@ export function useAuthFilesPrefixProxyEditor(
? buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key))
: '';
const prefixProxyDirty =
Boolean(prefixProxyEditor?.json) &&
Boolean(prefixProxyEditor?.originalText) &&
(prefixProxyUpdatedText === '' || prefixProxyUpdatedText !== prefixProxyEditor?.originalText);
const prefixProxyPatch =
prefixProxyEditor?.json && !hasBlockingValidationError
? buildAuthFileFieldsPatch(prefixProxyEditor, (key) => t(key))
: null;
const prefixProxyDirty = hasKeys(prefixProxyPatch);
const closePrefixProxyEditor = () => {
setPrefixProxyEditor(null);
@@ -196,13 +287,6 @@ export function useAuthFilesPrefixProxyEditor(
const openPrefixProxyEditor = async (file: AuthFileItem) => {
const name = file.name;
const normalizedType = String(file.type ?? '')
.trim()
.toLowerCase();
const normalizedProvider = String(file.provider ?? '')
.trim()
.toLowerCase();
const isCodexFile = normalizedType === 'codex' || normalizedProvider === 'codex';
if (disableControls) return;
if (prefixProxyEditor?.fileName === name) {
@@ -213,7 +297,6 @@ export function useAuthFilesPrefixProxyEditor(
setPrefixProxyEditor({
fileName: name,
fileInfoText: JSON.stringify(file, null, 2),
isCodexFile,
loading: true,
saving: false,
error: null,
@@ -223,9 +306,6 @@ export function useAuthFilesPrefixProxyEditor(
prefix: '',
proxyUrl: '',
priority: '',
excludedModelsText: '',
disableCooling: '',
websockets: false,
note: '',
noteTouched: false,
headersText: '',
@@ -269,18 +349,10 @@ export function useAuthFilesPrefixProxyEditor(
}
const json = { ...(parsed as Record<string, unknown>) };
if (isCodexFile) {
const normalizedWebsockets = readCodexAuthFileWebsockets(json);
delete json.websocket;
json.websockets = normalizedWebsockets;
}
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);
const websocketsValue = readCodexAuthFileWebsockets(json);
const note = typeof json.note === 'string' ? json.note : '';
const headers = json.headers;
let headersText = '';
@@ -302,10 +374,6 @@ export function useAuthFilesPrefixProxyEditor(
prefix,
proxyUrl,
priority: priority !== undefined ? String(priority) : '',
excludedModelsText: excludedModels.join('\n'),
disableCooling:
disableCoolingValue === undefined ? '' : disableCoolingValue ? 'true' : 'false',
websockets: websocketsValue,
note,
noteTouched: false,
headersText,
@@ -333,8 +401,6 @@ export function useAuthFilesPrefixProxyEditor(
if (field === 'prefix') return { ...prev, prefix: String(value) };
if (field === 'proxyUrl') return { ...prev, proxyUrl: String(value) };
if (field === 'priority') return { ...prev, priority: String(value) };
if (field === 'excludedModelsText') return { ...prev, excludedModelsText: String(value) };
if (field === 'disableCooling') return { ...prev, disableCooling: String(value) };
if (field === 'note') return { ...prev, note: String(value), noteTouched: true };
if (field === 'headersText') {
const headersText = String(value);
@@ -346,7 +412,7 @@ export function useAuthFilesPrefixProxyEditor(
headersError: errorKey ? t(errorKey) : null,
};
}
return { ...prev, websockets: Boolean(value) };
return prev;
});
};
@@ -355,23 +421,15 @@ export function useAuthFilesPrefixProxyEditor(
if (!prefixProxyDirty) return;
const name = prefixProxyEditor.fileName;
let payload = '';
let payload: AuthFileFieldsPatch;
try {
payload = buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key));
payload = buildAuthFileFieldsPatch(prefixProxyEditor, (key) => t(key));
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Invalid format';
showNotification(errorMessage, 'error');
return;
}
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;
}
if (!hasKeys(payload)) return;
setPrefixProxyEditor((prev) => {
if (!prev || prev.fileName !== name) return prev;
@@ -379,13 +437,13 @@ export function useAuthFilesPrefixProxyEditor(
});
try {
await authFilesApi.saveText(name, payload);
await authFilesApi.patchFields(name, payload);
showNotification(t('auth_files.prefix_proxy_saved_success', { name }), 'success');
await loadFiles();
setPrefixProxyEditor(null);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : '';
showNotification(`${t('notification.upload_failed')}: ${errorMessage}`, 'error');
showNotification(`${t('notification.update_failed')}: ${errorMessage}`, 'error');
setPrefixProxyEditor((prev) => {
if (!prev || prev.fileName !== name) return prev;
return { ...prev, saving: false };
+10
View File
@@ -10,6 +10,13 @@ import { parseTimestampMs } from '@/utils/timestamp';
type StatusError = { status?: number };
type AuthFileStatusResponse = { status: string; disabled: boolean };
type AuthFileEntry = AuthFilesResponse['files'][number];
export type AuthFileFieldsPatch = {
prefix?: string;
proxy_url?: string;
headers?: Record<string, string>;
priority?: number;
note?: string;
};
type AuthFileBatchFailure = { name: string; error: string };
type AuthFileBatchUploadResponse = {
status?: string;
@@ -401,6 +408,9 @@ export const authFilesApi = {
setStatus: (name: string, disabled: boolean) =>
apiClient.patch<AuthFileStatusResponse>('/auth-files/status', { name, disabled }),
patchFields: (name: string, fields: AuthFileFieldsPatch) =>
apiClient.patch('/auth-files/fields', { name, ...fields }),
uploadFiles: async (files: File[]): Promise<AuthFileBatchUploadResult> => {
const requestedNames = files.map((file) => file.name);
if (requestedNames.length === 0) {