mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
478 lines
18 KiB
TypeScript
478 lines
18 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Card } from '@/components/ui/Card';
|
|
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Input } from '@/components/ui/Input';
|
|
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
|
import { configApi } from '@/services/api';
|
|
import type { Config } from '@/types';
|
|
import styles from './Settings/Settings.module.scss';
|
|
|
|
type PendingKey =
|
|
| 'debug'
|
|
| 'proxy'
|
|
| 'retry'
|
|
| 'logsMaxSize'
|
|
| 'forceModelPrefix'
|
|
| 'routingStrategy'
|
|
| 'switchProject'
|
|
| 'switchPreview'
|
|
| 'usage'
|
|
| 'loggingToFile'
|
|
| 'wsAuth';
|
|
|
|
export function SettingsPage() {
|
|
const { t } = useTranslation();
|
|
const { showNotification } = useNotificationStore();
|
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
|
const config = useConfigStore((state) => state.config);
|
|
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
|
const updateConfigValue = useConfigStore((state) => state.updateConfigValue);
|
|
const clearCache = useConfigStore((state) => state.clearCache);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [proxyValue, setProxyValue] = useState('');
|
|
const [retryValue, setRetryValue] = useState(0);
|
|
const [logsMaxTotalSizeMb, setLogsMaxTotalSizeMb] = useState(0);
|
|
const [routingStrategy, setRoutingStrategy] = useState('round-robin');
|
|
const [pending, setPending] = useState<Record<PendingKey, boolean>>({} as Record<PendingKey, boolean>);
|
|
const [error, setError] = useState('');
|
|
|
|
const disableControls = connectionStatus !== 'connected';
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const [configResult, logsResult, prefixResult, routingResult] = await Promise.allSettled([
|
|
fetchConfig(),
|
|
configApi.getLogsMaxTotalSizeMb(),
|
|
configApi.getForceModelPrefix(),
|
|
configApi.getRoutingStrategy(),
|
|
]);
|
|
|
|
if (configResult.status !== 'fulfilled') {
|
|
throw configResult.reason;
|
|
}
|
|
|
|
const data = configResult.value as Config;
|
|
setProxyValue(data?.proxyUrl ?? '');
|
|
setRetryValue(typeof data?.requestRetry === 'number' ? data.requestRetry : 0);
|
|
|
|
if (logsResult.status === 'fulfilled' && Number.isFinite(logsResult.value)) {
|
|
setLogsMaxTotalSizeMb(Math.max(0, Number(logsResult.value)));
|
|
updateConfigValue('logs-max-total-size-mb', Math.max(0, Number(logsResult.value)));
|
|
}
|
|
|
|
if (prefixResult.status === 'fulfilled') {
|
|
updateConfigValue('force-model-prefix', Boolean(prefixResult.value));
|
|
}
|
|
|
|
if (routingResult.status === 'fulfilled' && routingResult.value) {
|
|
setRoutingStrategy(String(routingResult.value));
|
|
updateConfigValue('routing/strategy', String(routingResult.value));
|
|
}
|
|
} catch (err: any) {
|
|
setError(err?.message || t('notification.refresh_failed'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
load();
|
|
}, [fetchConfig, t, updateConfigValue]);
|
|
|
|
useEffect(() => {
|
|
if (config) {
|
|
setProxyValue(config.proxyUrl ?? '');
|
|
if (typeof config.requestRetry === 'number') {
|
|
setRetryValue(config.requestRetry);
|
|
}
|
|
if (typeof config.logsMaxTotalSizeMb === 'number') {
|
|
setLogsMaxTotalSizeMb(config.logsMaxTotalSizeMb);
|
|
}
|
|
if (config.routingStrategy) {
|
|
setRoutingStrategy(config.routingStrategy);
|
|
}
|
|
}
|
|
}, [config?.proxyUrl, config?.requestRetry, config?.logsMaxTotalSizeMb, config?.routingStrategy]);
|
|
|
|
const setPendingFlag = (key: PendingKey, value: boolean) => {
|
|
setPending((prev) => ({ ...prev, [key]: value }));
|
|
};
|
|
|
|
const toggleSetting = async (
|
|
section: PendingKey,
|
|
rawKey: 'debug' | 'usage-statistics-enabled' | 'logging-to-file' | 'ws-auth' | 'force-model-prefix',
|
|
value: boolean,
|
|
updater: (val: boolean) => Promise<any>,
|
|
successMessage: string
|
|
) => {
|
|
const previous = (() => {
|
|
switch (rawKey) {
|
|
case 'debug':
|
|
return config?.debug ?? false;
|
|
case 'usage-statistics-enabled':
|
|
return config?.usageStatisticsEnabled ?? false;
|
|
case 'logging-to-file':
|
|
return config?.loggingToFile ?? false;
|
|
case 'ws-auth':
|
|
return config?.wsAuth ?? false;
|
|
case 'force-model-prefix':
|
|
return config?.forceModelPrefix ?? false;
|
|
default:
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
setPendingFlag(section, true);
|
|
updateConfigValue(rawKey, value);
|
|
|
|
try {
|
|
await updater(value);
|
|
clearCache(rawKey);
|
|
showNotification(successMessage, 'success');
|
|
} catch (err: any) {
|
|
updateConfigValue(rawKey, previous);
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag(section, false);
|
|
}
|
|
};
|
|
|
|
const handleProxyUpdate = async () => {
|
|
const previous = config?.proxyUrl ?? '';
|
|
setPendingFlag('proxy', true);
|
|
updateConfigValue('proxy-url', proxyValue);
|
|
try {
|
|
await configApi.updateProxyUrl(proxyValue.trim());
|
|
clearCache('proxy-url');
|
|
showNotification(t('notification.proxy_updated'), 'success');
|
|
} catch (err: any) {
|
|
setProxyValue(previous);
|
|
updateConfigValue('proxy-url', previous);
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag('proxy', false);
|
|
}
|
|
};
|
|
|
|
const handleProxyClear = async () => {
|
|
const previous = config?.proxyUrl ?? '';
|
|
setPendingFlag('proxy', true);
|
|
updateConfigValue('proxy-url', '');
|
|
try {
|
|
await configApi.clearProxyUrl();
|
|
clearCache('proxy-url');
|
|
setProxyValue('');
|
|
showNotification(t('notification.proxy_cleared'), 'success');
|
|
} catch (err: any) {
|
|
setProxyValue(previous);
|
|
updateConfigValue('proxy-url', previous);
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag('proxy', false);
|
|
}
|
|
};
|
|
|
|
const handleRetryUpdate = async () => {
|
|
const previous = config?.requestRetry ?? 0;
|
|
const parsed = Number(retryValue);
|
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
showNotification(t('login.error_invalid'), 'error');
|
|
setRetryValue(previous);
|
|
return;
|
|
}
|
|
setPendingFlag('retry', true);
|
|
updateConfigValue('request-retry', parsed);
|
|
try {
|
|
await configApi.updateRequestRetry(parsed);
|
|
clearCache('request-retry');
|
|
showNotification(t('notification.retry_updated'), 'success');
|
|
} catch (err: any) {
|
|
setRetryValue(previous);
|
|
updateConfigValue('request-retry', previous);
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag('retry', false);
|
|
}
|
|
};
|
|
|
|
const handleLogsMaxTotalSizeUpdate = async () => {
|
|
const previous = config?.logsMaxTotalSizeMb ?? 0;
|
|
const parsed = Number(logsMaxTotalSizeMb);
|
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
showNotification(t('login.error_invalid'), 'error');
|
|
setLogsMaxTotalSizeMb(previous);
|
|
return;
|
|
}
|
|
const normalized = Math.max(0, parsed);
|
|
setPendingFlag('logsMaxSize', true);
|
|
updateConfigValue('logs-max-total-size-mb', normalized);
|
|
try {
|
|
await configApi.updateLogsMaxTotalSizeMb(normalized);
|
|
clearCache('logs-max-total-size-mb');
|
|
showNotification(t('notification.logs_max_total_size_updated'), 'success');
|
|
} catch (err: any) {
|
|
setLogsMaxTotalSizeMb(previous);
|
|
updateConfigValue('logs-max-total-size-mb', previous);
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag('logsMaxSize', false);
|
|
}
|
|
};
|
|
|
|
const handleRoutingStrategyUpdate = async () => {
|
|
const strategy = routingStrategy.trim();
|
|
if (!strategy) {
|
|
showNotification(t('login.error_invalid'), 'error');
|
|
return;
|
|
}
|
|
const previous = config?.routingStrategy ?? 'round-robin';
|
|
setPendingFlag('routingStrategy', true);
|
|
updateConfigValue('routing/strategy', strategy);
|
|
try {
|
|
await configApi.updateRoutingStrategy(strategy);
|
|
clearCache('routing/strategy');
|
|
showNotification(t('notification.routing_strategy_updated'), 'success');
|
|
} catch (err: any) {
|
|
setRoutingStrategy(previous);
|
|
updateConfigValue('routing/strategy', previous);
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag('routingStrategy', false);
|
|
}
|
|
};
|
|
|
|
const quotaSwitchProject = config?.quotaExceeded?.switchProject ?? false;
|
|
const quotaSwitchPreview = config?.quotaExceeded?.switchPreviewModel ?? false;
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<h1 className={styles.pageTitle}>{t('basic_settings.title')}</h1>
|
|
|
|
<div className={styles.grid}>
|
|
<Card>
|
|
{error && <div className="error-box">{error}</div>}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<ToggleSwitch
|
|
label={t('basic_settings.debug_enable')}
|
|
checked={config?.debug ?? false}
|
|
disabled={disableControls || pending.debug || loading}
|
|
onChange={(value) =>
|
|
toggleSetting('debug', 'debug', value, configApi.updateDebug, t('notification.debug_updated'))
|
|
}
|
|
/>
|
|
|
|
<ToggleSwitch
|
|
label={t('basic_settings.usage_statistics_enable')}
|
|
checked={config?.usageStatisticsEnabled ?? false}
|
|
disabled={disableControls || pending.usage || loading}
|
|
onChange={(value) =>
|
|
toggleSetting(
|
|
'usage',
|
|
'usage-statistics-enabled',
|
|
value,
|
|
configApi.updateUsageStatistics,
|
|
t('notification.usage_statistics_updated')
|
|
)
|
|
}
|
|
/>
|
|
|
|
<ToggleSwitch
|
|
label={t('basic_settings.logging_to_file_enable')}
|
|
checked={config?.loggingToFile ?? false}
|
|
disabled={disableControls || pending.loggingToFile || loading}
|
|
onChange={(value) =>
|
|
toggleSetting(
|
|
'loggingToFile',
|
|
'logging-to-file',
|
|
value,
|
|
configApi.updateLoggingToFile,
|
|
t('notification.logging_to_file_updated')
|
|
)
|
|
}
|
|
/>
|
|
|
|
<ToggleSwitch
|
|
label={t('basic_settings.ws_auth_enable')}
|
|
checked={config?.wsAuth ?? false}
|
|
disabled={disableControls || pending.wsAuth || loading}
|
|
onChange={(value) =>
|
|
toggleSetting(
|
|
'wsAuth',
|
|
'ws-auth',
|
|
value,
|
|
configApi.updateWsAuth,
|
|
t('notification.ws_auth_updated')
|
|
)
|
|
}
|
|
/>
|
|
|
|
<ToggleSwitch
|
|
label={t('basic_settings.force_model_prefix_enable')}
|
|
checked={config?.forceModelPrefix ?? false}
|
|
disabled={disableControls || pending.forceModelPrefix || loading}
|
|
onChange={(value) =>
|
|
toggleSetting(
|
|
'forceModelPrefix',
|
|
'force-model-prefix',
|
|
value,
|
|
configApi.updateForceModelPrefix,
|
|
t('notification.force_model_prefix_updated')
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title={t('basic_settings.proxy_title')}>
|
|
<Input
|
|
label={t('basic_settings.proxy_url_label')}
|
|
placeholder={t('basic_settings.proxy_url_placeholder')}
|
|
value={proxyValue}
|
|
onChange={(e) => setProxyValue(e.target.value)}
|
|
disabled={disableControls || loading}
|
|
/>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<Button variant="secondary" onClick={handleProxyClear} disabled={disableControls || pending.proxy || loading}>
|
|
{t('basic_settings.proxy_clear')}
|
|
</Button>
|
|
<Button onClick={handleProxyUpdate} loading={pending.proxy} disabled={disableControls || loading}>
|
|
{t('basic_settings.proxy_update')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title={t('basic_settings.retry_title')}>
|
|
<div className={styles.retryRow}>
|
|
<Input
|
|
label={t('basic_settings.retry_count_label')}
|
|
type="number"
|
|
inputMode="numeric"
|
|
min={0}
|
|
step={1}
|
|
value={retryValue}
|
|
onChange={(e) => setRetryValue(Number(e.target.value))}
|
|
disabled={disableControls || loading}
|
|
className={styles.retryInput}
|
|
/>
|
|
<Button
|
|
className={styles.retryButton}
|
|
onClick={handleRetryUpdate}
|
|
loading={pending.retry}
|
|
disabled={disableControls || loading}
|
|
>
|
|
{t('basic_settings.retry_update')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title={t('basic_settings.logs_max_total_size_title')}>
|
|
<div className={`${styles.retryRow} ${styles.retryRowAligned} ${styles.retryRowInputGrow}`}>
|
|
<Input
|
|
label={t('basic_settings.logs_max_total_size_label')}
|
|
hint={t('basic_settings.logs_max_total_size_hint')}
|
|
type="number"
|
|
inputMode="numeric"
|
|
min={0}
|
|
step={1}
|
|
value={logsMaxTotalSizeMb}
|
|
onChange={(e) => setLogsMaxTotalSizeMb(Number(e.target.value))}
|
|
disabled={disableControls || loading}
|
|
className={styles.retryInput}
|
|
/>
|
|
<Button
|
|
className={styles.retryButton}
|
|
onClick={handleLogsMaxTotalSizeUpdate}
|
|
loading={pending.logsMaxSize}
|
|
disabled={disableControls || loading}
|
|
>
|
|
{t('basic_settings.logs_max_total_size_update')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title={t('basic_settings.routing_title')}>
|
|
<div className={`${styles.retryRow} ${styles.retryRowAligned} ${styles.retryRowInputGrow}`}>
|
|
<div className="form-group">
|
|
<label>{t('basic_settings.routing_strategy_label')}</label>
|
|
<select
|
|
className="input"
|
|
value={routingStrategy}
|
|
onChange={(e) => setRoutingStrategy(e.target.value)}
|
|
disabled={disableControls || loading}
|
|
>
|
|
<option value="round-robin">{t('basic_settings.routing_strategy_round_robin')}</option>
|
|
<option value="fill-first">{t('basic_settings.routing_strategy_fill_first')}</option>
|
|
</select>
|
|
<div className="hint">{t('basic_settings.routing_strategy_hint')}</div>
|
|
</div>
|
|
<Button
|
|
className={styles.retryButton}
|
|
onClick={handleRoutingStrategyUpdate}
|
|
loading={pending.routingStrategy}
|
|
disabled={disableControls || loading}
|
|
>
|
|
{t('basic_settings.routing_strategy_update')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title={t('basic_settings.quota_title')}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<ToggleSwitch
|
|
label={t('basic_settings.quota_switch_project')}
|
|
checked={quotaSwitchProject}
|
|
disabled={disableControls || pending.switchProject || loading}
|
|
onChange={(value) =>
|
|
(async () => {
|
|
const previous = config?.quotaExceeded?.switchProject ?? false;
|
|
const nextQuota = { ...(config?.quotaExceeded || {}), switchProject: value };
|
|
setPendingFlag('switchProject', true);
|
|
updateConfigValue('quota-exceeded', nextQuota);
|
|
try {
|
|
await configApi.updateSwitchProject(value);
|
|
clearCache('quota-exceeded');
|
|
showNotification(t('notification.quota_switch_project_updated'), 'success');
|
|
} catch (err: any) {
|
|
updateConfigValue('quota-exceeded', { ...(config?.quotaExceeded || {}), switchProject: previous });
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag('switchProject', false);
|
|
}
|
|
})()
|
|
}
|
|
/>
|
|
<ToggleSwitch
|
|
label={t('basic_settings.quota_switch_preview')}
|
|
checked={quotaSwitchPreview}
|
|
disabled={disableControls || pending.switchPreview || loading}
|
|
onChange={(value) =>
|
|
(async () => {
|
|
const previous = config?.quotaExceeded?.switchPreviewModel ?? false;
|
|
const nextQuota = { ...(config?.quotaExceeded || {}), switchPreviewModel: value };
|
|
setPendingFlag('switchPreview', true);
|
|
updateConfigValue('quota-exceeded', nextQuota);
|
|
try {
|
|
await configApi.updateSwitchPreviewModel(value);
|
|
clearCache('quota-exceeded');
|
|
showNotification(t('notification.quota_switch_preview_updated'), 'success');
|
|
} catch (err: any) {
|
|
updateConfigValue('quota-exceeded', { ...(config?.quotaExceeded || {}), switchPreviewModel: previous });
|
|
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
|
} finally {
|
|
setPendingFlag('switchPreview', false);
|
|
}
|
|
})()
|
|
}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|