Compare commits

...

1 Commits

3 changed files with 92 additions and 32 deletions
@@ -2,7 +2,14 @@ import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/Button';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { IconBot, IconCheck, IconCode, IconDownload, IconInfo, IconTrash2 } from '@/components/ui/icons';
import {
IconBot,
IconCheck,
IconCode,
IconDownload,
IconInfo,
IconTrash2,
} from '@/components/ui/icons';
import { ProviderStatusBar } from '@/components/providers/ProviderStatusBar';
import type { AuthFileItem } from '@/types';
import { resolveAuthProvider } from '@/utils/quota';
@@ -16,7 +23,7 @@ import {
isRuntimeOnlyAuthFile,
resolveAuthFileStats,
type QuotaProviderType,
type ResolvedTheme
type ResolvedTheme,
} from '@/features/authFiles/constants';
import type { AuthFileStatusBarData } from '@/features/authFiles/hooks/useAuthFilesStatusBarCache';
import { AuthFileQuotaSection } from '@/features/authFiles/components/AuthFileQuotaSection';
@@ -37,7 +44,7 @@ export type AuthFileCardProps = {
onShowModels: (file: AuthFileItem) => void;
onShowDetails: (file: AuthFileItem) => void;
onDownload: (name: string) => void;
onOpenPrefixProxyEditor: (name: string) => void;
onOpenPrefixProxyEditor: (file: AuthFileItem) => void;
onDelete: (name: string) => void;
onToggleStatus: (file: AuthFileItem, enabled: boolean) => void;
onToggleSelect: (name: string) => void;
@@ -67,7 +74,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
onOpenPrefixProxyEditor,
onDelete,
onToggleStatus,
onToggleSelect
onToggleSelect,
} = props;
const fileStats = resolveAuthFileStats(file, keyStats);
@@ -110,7 +117,9 @@ export function AuthFileCard(props: AuthFileCardProps) {
type="button"
className={`${styles.selectionToggle} ${selected ? styles.selectionToggleActive : ''}`}
onClick={() => onToggleSelect(file.name)}
aria-label={selected ? t('auth_files.batch_deselect') : t('auth_files.batch_select_all')}
aria-label={
selected ? t('auth_files.batch_deselect') : t('auth_files.batch_select_all')
}
aria-pressed={selected}
title={selected ? t('auth_files.batch_deselect') : t('auth_files.batch_select_all')}
>
@@ -122,7 +131,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
style={{
backgroundColor: typeColor.bg,
color: typeColor.text,
...(typeColor.border ? { border: typeColor.border } : {})
...(typeColor.border ? { border: typeColor.border } : {}),
}}
>
{getTypeLabel(t, file.type || 'unknown')}
@@ -157,7 +166,11 @@ export function AuthFileCard(props: AuthFileCardProps) {
<ProviderStatusBar statusData={statusData} styles={styles} />
{showQuotaLayout && quotaType && (
<AuthFileQuotaSection file={file} quotaType={quotaType} disableControls={disableControls} />
<AuthFileQuotaSection
file={file}
quotaType={quotaType}
disableControls={disableControls}
/>
)}
<div className={styles.cardActions}>
@@ -198,7 +211,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
<Button
variant="secondary"
size="sm"
onClick={() => onOpenPrefixProxyEditor(file.name)}
onClick={() => onOpenPrefixProxyEditor(file)}
className={styles.iconButton}
title={t('auth_files.prefix_proxy_button')}
disabled={disableControls}
@@ -232,7 +245,9 @@ export function AuthFileCard(props: AuthFileCardProps) {
</div>
)}
{isRuntimeOnly && (
<div className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</div>
<div className={styles.virtualBadge}>
{t('auth_files.type_virtual') || '虚拟认证文件'}
</div>
)}
</div>
</div>
@@ -3,9 +3,11 @@ 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,
PrefixProxyEditorState
PrefixProxyEditorFieldValue,
PrefixProxyEditorState,
} from '@/features/authFiles/hooks/useAuthFilesPrefixProxyEditor';
import styles from '@/pages/AuthFilesPage.module.scss';
@@ -16,7 +18,7 @@ export type AuthFilesPrefixProxyEditorModalProps = {
dirty: boolean;
onClose: () => void;
onSave: () => void;
onChange: (field: PrefixProxyEditorField, value: string) => void;
onChange: (field: PrefixProxyEditorField, value: PrefixProxyEditorFieldValue) => void;
};
export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEditorModalProps) {
@@ -42,9 +44,7 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
<Button
onClick={onSave}
loading={editor?.saving === true}
disabled={
disableControls || editor?.saving === true || !dirty || !editor?.json
}
disabled={disableControls || editor?.saving === true || !dirty || !editor?.json}
>
{t('common.save')}
</Button>
@@ -114,6 +114,18 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('disableCooling', e.target.value)}
/>
{editor.isCodexFile && (
<div className="form-group">
<label>{t('ai_providers.codex_websockets_label')}</label>
<ToggleSwitch
checked={Boolean(editor.websocket)}
disabled={disableControls || editor.saving || !editor.json}
ariaLabel={t('ai_providers.codex_websockets_label')}
onChange={(value) => onChange('websocket', value)}
/>
<div className="hint">{t('ai_providers.codex_websockets_hint')}</div>
</div>
)}
</div>
</>
)}
@@ -122,4 +134,3 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
</Modal>
);
}
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { authFilesApi } 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';
@@ -8,7 +9,7 @@ import {
normalizeExcludedModels,
parseDisableCoolingValue,
parseExcludedModelsText,
parsePriorityValue
parsePriorityValue,
} from '@/features/authFiles/constants';
export type PrefixProxyEditorField =
@@ -16,10 +17,14 @@ export type PrefixProxyEditorField =
| 'proxyUrl'
| 'priority'
| 'excludedModelsText'
| 'disableCooling';
| 'disableCooling'
| 'websocket';
export type PrefixProxyEditorFieldValue = string | boolean;
export type PrefixProxyEditorState = {
fileName: string;
isCodexFile: boolean;
loading: boolean;
saving: boolean;
error: string | null;
@@ -31,6 +36,7 @@ export type PrefixProxyEditorState = {
priority: string;
excludedModelsText: string;
disableCooling: string;
websocket: boolean;
};
export type UseAuthFilesPrefixProxyEditorOptions = {
@@ -43,9 +49,12 @@ export type UseAuthFilesPrefixProxyEditorResult = {
prefixProxyEditor: PrefixProxyEditorState | null;
prefixProxyUpdatedText: string;
prefixProxyDirty: boolean;
openPrefixProxyEditor: (name: string) => Promise<void>;
openPrefixProxyEditor: (file: Pick<AuthFileItem, 'name' | 'type' | 'provider'>) => Promise<void>;
closePrefixProxyEditor: () => void;
handlePrefixProxyChange: (field: PrefixProxyEditorField, value: string) => void;
handlePrefixProxyChange: (
field: PrefixProxyEditorField,
value: PrefixProxyEditorFieldValue
) => void;
handlePrefixProxySave: () => Promise<void>;
};
@@ -80,6 +89,10 @@ const buildPrefixProxyUpdatedText = (editor: PrefixProxyEditorState | null): str
delete next.disable_cooling;
}
if (editor.isCodexFile) {
next.websocket = editor.websocket;
}
return JSON.stringify(next);
};
@@ -102,7 +115,16 @@ export function useAuthFilesPrefixProxyEditor(
setPrefixProxyEditor(null);
};
const openPrefixProxyEditor = async (name: string) => {
const openPrefixProxyEditor = async (file: Pick<AuthFileItem, 'name' | 'type' | 'provider'>) => {
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) {
setPrefixProxyEditor(null);
@@ -111,6 +133,7 @@ export function useAuthFilesPrefixProxyEditor(
setPrefixProxyEditor({
fileName: name,
isCodexFile,
loading: true,
saving: false,
error: null,
@@ -121,7 +144,8 @@ export function useAuthFilesPrefixProxyEditor(
proxyUrl: '',
priority: '',
excludedModelsText: '',
disableCooling: ''
disableCooling: '',
websocket: false,
});
try {
@@ -139,7 +163,7 @@ export function useAuthFilesPrefixProxyEditor(
loading: false,
error: t('auth_files.prefix_proxy_invalid_json'),
rawText: trimmed,
originalText: trimmed
originalText: trimmed,
};
});
return;
@@ -153,19 +177,24 @@ export function useAuthFilesPrefixProxyEditor(
loading: false,
error: t('auth_files.prefix_proxy_invalid_json'),
rawText: trimmed,
originalText: trimmed
originalText: trimmed,
};
});
return;
}
const json = parsed as Record<string, unknown>;
const json = { ...(parsed as Record<string, unknown>) };
if (isCodexFile) {
const websocketValue = parseDisableCoolingValue(json.websocket);
json.websocket = websocketValue ?? false;
}
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 websocketValue = parseDisableCoolingValue(json.websocket);
setPrefixProxyEditor((prev) => {
if (!prev || prev.fileName !== name) return prev;
@@ -181,7 +210,8 @@ export function useAuthFilesPrefixProxyEditor(
excludedModelsText: excludedModels.join('\n'),
disableCooling:
disableCoolingValue === undefined ? '' : disableCoolingValue ? 'true' : 'false',
error: null
websocket: websocketValue ?? false,
error: null,
};
});
} catch (err: unknown) {
@@ -194,14 +224,18 @@ export function useAuthFilesPrefixProxyEditor(
}
};
const handlePrefixProxyChange = (field: PrefixProxyEditorField, value: string) => {
const handlePrefixProxyChange = (
field: PrefixProxyEditorField,
value: PrefixProxyEditorFieldValue
) => {
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 };
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) };
return { ...prev, websocket: Boolean(value) };
});
};
@@ -249,6 +283,6 @@ export function useAuthFilesPrefixProxyEditor(
openPrefixProxyEditor,
closePrefixProxyEditor,
handlePrefixProxyChange,
handlePrefixProxySave
handlePrefixProxySave,
};
}