diff --git a/src/components/proxy/AutoFailoverConfigPanel.tsx b/src/components/proxy/AutoFailoverConfigPanel.tsx index 30f55e13..b80130fc 100644 --- a/src/components/proxy/AutoFailoverConfigPanel.tsx +++ b/src/components/proxy/AutoFailoverConfigPanel.tsx @@ -21,52 +21,64 @@ export function AutoFailoverConfigPanel({ const { data: config, isLoading, error } = useAppProxyConfig(appType); const updateConfig = useUpdateAppProxyConfig(); + // 使用字符串状态以支持完全清空数字输入框 const [formData, setFormData] = useState({ autoFailoverEnabled: false, - maxRetries: 3, - streamingFirstByteTimeout: 30, - streamingIdleTimeout: 60, - nonStreamingTimeout: 300, - circuitFailureThreshold: 5, - circuitSuccessThreshold: 2, - circuitTimeoutSeconds: 60, - circuitErrorRateThreshold: 0.5, - circuitMinRequests: 10, + maxRetries: "3", + streamingFirstByteTimeout: "30", + streamingIdleTimeout: "60", + nonStreamingTimeout: "300", + circuitFailureThreshold: "5", + circuitSuccessThreshold: "2", + circuitTimeoutSeconds: "60", + circuitErrorRateThreshold: "50", // 存储百分比值 + circuitMinRequests: "10", }); useEffect(() => { if (config) { setFormData({ autoFailoverEnabled: config.autoFailoverEnabled, - maxRetries: config.maxRetries, - streamingFirstByteTimeout: config.streamingFirstByteTimeout, - streamingIdleTimeout: config.streamingIdleTimeout, - nonStreamingTimeout: config.nonStreamingTimeout, - circuitFailureThreshold: config.circuitFailureThreshold, - circuitSuccessThreshold: config.circuitSuccessThreshold, - circuitTimeoutSeconds: config.circuitTimeoutSeconds, - circuitErrorRateThreshold: config.circuitErrorRateThreshold, - circuitMinRequests: config.circuitMinRequests, + maxRetries: String(config.maxRetries), + streamingFirstByteTimeout: String(config.streamingFirstByteTimeout), + streamingIdleTimeout: String(config.streamingIdleTimeout), + nonStreamingTimeout: String(config.nonStreamingTimeout), + circuitFailureThreshold: String(config.circuitFailureThreshold), + circuitSuccessThreshold: String(config.circuitSuccessThreshold), + circuitTimeoutSeconds: String(config.circuitTimeoutSeconds), + circuitErrorRateThreshold: String( + Math.round(config.circuitErrorRateThreshold * 100), + ), + circuitMinRequests: String(config.circuitMinRequests), }); } }, [config]); const handleSave = async () => { if (!config) return; + // 解析数字,空值使用默认值,0 是有效值 + const parseNum = (val: string, defaultVal: number) => { + const n = parseInt(val); + return isNaN(n) ? defaultVal : n; + }; try { await updateConfig.mutateAsync({ appType, enabled: config.enabled, autoFailoverEnabled: formData.autoFailoverEnabled, - maxRetries: formData.maxRetries, - streamingFirstByteTimeout: formData.streamingFirstByteTimeout, - streamingIdleTimeout: formData.streamingIdleTimeout, - nonStreamingTimeout: formData.nonStreamingTimeout, - circuitFailureThreshold: formData.circuitFailureThreshold, - circuitSuccessThreshold: formData.circuitSuccessThreshold, - circuitTimeoutSeconds: formData.circuitTimeoutSeconds, - circuitErrorRateThreshold: formData.circuitErrorRateThreshold, - circuitMinRequests: formData.circuitMinRequests, + maxRetries: parseNum(formData.maxRetries, 3), + streamingFirstByteTimeout: parseNum( + formData.streamingFirstByteTimeout, + 30, + ), + streamingIdleTimeout: parseNum(formData.streamingIdleTimeout, 60), + nonStreamingTimeout: parseNum(formData.nonStreamingTimeout, 300), + circuitFailureThreshold: parseNum(formData.circuitFailureThreshold, 5), + circuitSuccessThreshold: parseNum(formData.circuitSuccessThreshold, 2), + circuitTimeoutSeconds: parseNum(formData.circuitTimeoutSeconds, 60), + circuitErrorRateThreshold: + parseNum(formData.circuitErrorRateThreshold, 50) / 100, + circuitMinRequests: parseNum(formData.circuitMinRequests, 10), }); toast.success( t("proxy.autoFailover.configSaved", "自动故障转移配置已保存"), @@ -83,15 +95,17 @@ export function AutoFailoverConfigPanel({ if (config) { setFormData({ autoFailoverEnabled: config.autoFailoverEnabled, - maxRetries: config.maxRetries, - streamingFirstByteTimeout: config.streamingFirstByteTimeout, - streamingIdleTimeout: config.streamingIdleTimeout, - nonStreamingTimeout: config.nonStreamingTimeout, - circuitFailureThreshold: config.circuitFailureThreshold, - circuitSuccessThreshold: config.circuitSuccessThreshold, - circuitTimeoutSeconds: config.circuitTimeoutSeconds, - circuitErrorRateThreshold: config.circuitErrorRateThreshold, - circuitMinRequests: config.circuitMinRequests, + maxRetries: String(config.maxRetries), + streamingFirstByteTimeout: String(config.streamingFirstByteTimeout), + streamingIdleTimeout: String(config.streamingIdleTimeout), + nonStreamingTimeout: String(config.nonStreamingTimeout), + circuitFailureThreshold: String(config.circuitFailureThreshold), + circuitSuccessThreshold: String(config.circuitSuccessThreshold), + circuitTimeoutSeconds: String(config.circuitTimeoutSeconds), + circuitErrorRateThreshold: String( + Math.round(config.circuitErrorRateThreshold * 100), + ), + circuitMinRequests: String(config.circuitMinRequests), }); } }; @@ -142,13 +156,9 @@ export function AutoFailoverConfigPanel({ min="0" max="10" value={formData.maxRetries} - onChange={(e) => { - const val = parseInt(e.target.value); - setFormData({ - ...formData, - maxRetries: isNaN(val) ? 0 : val, - }); - }} + onChange={(e) => + setFormData({ ...formData, maxRetries: e.target.value }) + } disabled={isDisabled} />

@@ -169,13 +179,12 @@ export function AutoFailoverConfigPanel({ min="1" max="20" value={formData.circuitFailureThreshold} - onChange={(e) => { - const val = parseInt(e.target.value); + onChange={(e) => setFormData({ ...formData, - circuitFailureThreshold: isNaN(val) ? 1 : Math.max(1, val), - }); - }} + circuitFailureThreshold: e.target.value, + }) + } disabled={isDisabled} />

@@ -208,13 +217,12 @@ export function AutoFailoverConfigPanel({ min="0" max="180" value={formData.streamingFirstByteTimeout} - onChange={(e) => { - const val = parseInt(e.target.value); + onChange={(e) => setFormData({ ...formData, - streamingFirstByteTimeout: isNaN(val) ? 0 : val, - }); - }} + streamingFirstByteTimeout: e.target.value, + }) + } disabled={isDisabled} />

@@ -235,13 +243,12 @@ export function AutoFailoverConfigPanel({ min="0" max="600" value={formData.streamingIdleTimeout} - onChange={(e) => { - const val = parseInt(e.target.value); + onChange={(e) => setFormData({ ...formData, - streamingIdleTimeout: isNaN(val) ? 0 : val, - }); - }} + streamingIdleTimeout: e.target.value, + }) + } disabled={isDisabled} />

@@ -262,13 +269,12 @@ export function AutoFailoverConfigPanel({ min="0" max="1800" value={formData.nonStreamingTimeout} - onChange={(e) => { - const val = parseInt(e.target.value); + onChange={(e) => setFormData({ ...formData, - nonStreamingTimeout: isNaN(val) ? 0 : val, - }); - }} + nonStreamingTimeout: e.target.value, + }) + } disabled={isDisabled} />

@@ -298,13 +304,12 @@ export function AutoFailoverConfigPanel({ min="1" max="10" value={formData.circuitSuccessThreshold} - onChange={(e) => { - const val = parseInt(e.target.value); + onChange={(e) => setFormData({ ...formData, - circuitSuccessThreshold: isNaN(val) ? 1 : Math.max(1, val), - }); - }} + circuitSuccessThreshold: e.target.value, + }) + } disabled={isDisabled} />

@@ -325,13 +330,12 @@ export function AutoFailoverConfigPanel({ min="10" max="300" value={formData.circuitTimeoutSeconds} - onChange={(e) => { - const val = parseInt(e.target.value); + onChange={(e) => setFormData({ ...formData, - circuitTimeoutSeconds: isNaN(val) ? 10 : Math.max(10, val), - }); - }} + circuitTimeoutSeconds: e.target.value, + }) + } disabled={isDisabled} />

@@ -352,14 +356,13 @@ export function AutoFailoverConfigPanel({ min="0" max="100" step="5" - value={Math.round(formData.circuitErrorRateThreshold * 100)} - onChange={(e) => { - const val = parseInt(e.target.value); + value={formData.circuitErrorRateThreshold} + onChange={(e) => setFormData({ ...formData, - circuitErrorRateThreshold: isNaN(val) ? 0.5 : val / 100, - }); - }} + circuitErrorRateThreshold: e.target.value, + }) + } disabled={isDisabled} />

@@ -380,13 +383,12 @@ export function AutoFailoverConfigPanel({ min="5" max="100" value={formData.circuitMinRequests} - onChange={(e) => { - const val = parseInt(e.target.value); + onChange={(e) => setFormData({ ...formData, - circuitMinRequests: isNaN(val) ? 5 : Math.max(5, val), - }); - }} + circuitMinRequests: e.target.value, + }) + } disabled={isDisabled} />

diff --git a/src/components/proxy/CircuitBreakerConfigPanel.tsx b/src/components/proxy/CircuitBreakerConfigPanel.tsx index 93f48d59..5341df33 100644 --- a/src/components/proxy/CircuitBreakerConfigPanel.tsx +++ b/src/components/proxy/CircuitBreakerConfigPanel.tsx @@ -16,24 +16,43 @@ export function CircuitBreakerConfigPanel() { const { data: config, isLoading } = useCircuitBreakerConfig(); const updateConfig = useUpdateCircuitBreakerConfig(); + // 使用字符串状态以支持完全清空输入框 const [formData, setFormData] = useState({ - failureThreshold: 5, - successThreshold: 2, - timeoutSeconds: 60, - errorRateThreshold: 0.5, - minRequests: 10, + failureThreshold: "5", + successThreshold: "2", + timeoutSeconds: "60", + errorRateThreshold: "50", // 存储百分比值 + minRequests: "10", }); // 当配置加载完成时更新表单数据 useEffect(() => { if (config) { - setFormData(config); + setFormData({ + failureThreshold: String(config.failureThreshold), + successThreshold: String(config.successThreshold), + timeoutSeconds: String(config.timeoutSeconds), + errorRateThreshold: String(Math.round(config.errorRateThreshold * 100)), + minRequests: String(config.minRequests), + }); } }, [config]); const handleSave = async () => { + // 解析数字,空值使用默认值,0 是有效值 + const parseNum = (val: string, defaultVal: number) => { + const n = parseInt(val); + return isNaN(n) ? defaultVal : n; + }; try { - await updateConfig.mutateAsync(formData); + const parsed = { + failureThreshold: parseNum(formData.failureThreshold, 5), + successThreshold: parseNum(formData.successThreshold, 2), + timeoutSeconds: parseNum(formData.timeoutSeconds, 60), + errorRateThreshold: parseNum(formData.errorRateThreshold, 50) / 100, + minRequests: parseNum(formData.minRequests, 10), + }; + await updateConfig.mutateAsync(parsed); toast.success("熔断器配置已保存", { closeButton: true }); } catch (error) { toast.error("保存失败: " + String(error)); @@ -42,7 +61,13 @@ export function CircuitBreakerConfigPanel() { const handleReset = () => { if (config) { - setFormData(config); + setFormData({ + failureThreshold: String(config.failureThreshold), + successThreshold: String(config.successThreshold), + timeoutSeconds: String(config.timeoutSeconds), + errorRateThreshold: String(Math.round(config.errorRateThreshold * 100)), + minRequests: String(config.minRequests), + }); } }; @@ -72,10 +97,7 @@ export function CircuitBreakerConfigPanel() { max="20" value={formData.failureThreshold} onChange={(e) => - setFormData({ - ...formData, - failureThreshold: parseInt(e.target.value) || 5, - }) + setFormData({ ...formData, failureThreshold: e.target.value }) } />

@@ -93,10 +115,7 @@ export function CircuitBreakerConfigPanel() { max="300" value={formData.timeoutSeconds} onChange={(e) => - setFormData({ - ...formData, - timeoutSeconds: parseInt(e.target.value) || 60, - }) + setFormData({ ...formData, timeoutSeconds: e.target.value }) } />

@@ -114,10 +133,7 @@ export function CircuitBreakerConfigPanel() { max="10" value={formData.successThreshold} onChange={(e) => - setFormData({ - ...formData, - successThreshold: parseInt(e.target.value) || 2, - }) + setFormData({ ...formData, successThreshold: e.target.value }) } />

@@ -134,12 +150,9 @@ export function CircuitBreakerConfigPanel() { min="0" max="100" step="5" - value={Math.round(formData.errorRateThreshold * 100)} + value={formData.errorRateThreshold} onChange={(e) => - setFormData({ - ...formData, - errorRateThreshold: (parseInt(e.target.value) || 50) / 100, - }) + setFormData({ ...formData, errorRateThreshold: e.target.value }) } />

@@ -157,10 +170,7 @@ export function CircuitBreakerConfigPanel() { max="100" value={formData.minRequests} onChange={(e) => - setFormData({ - ...formData, - minRequests: parseInt(e.target.value) || 10, - }) + setFormData({ ...formData, minRequests: e.target.value }) } />

diff --git a/src/components/proxy/ProxyPanel.tsx b/src/components/proxy/ProxyPanel.tsx index be540832..1cf88a14 100644 --- a/src/components/proxy/ProxyPanel.tsx +++ b/src/components/proxy/ProxyPanel.tsx @@ -38,15 +38,15 @@ export function ProxyPanel() { const { data: globalConfig } = useGlobalProxyConfig(); const updateGlobalConfig = useUpdateGlobalProxyConfig(); - // 监听地址/端口的本地状态 + // 监听地址/端口的本地状态(端口用字符串以支持完全清空) const [listenAddress, setListenAddress] = useState("127.0.0.1"); - const [listenPort, setListenPort] = useState(15721); + const [listenPort, setListenPort] = useState("15721"); // 同步全局配置到本地状态 useEffect(() => { if (globalConfig) { setListenAddress(globalConfig.listenAddress); - setListenPort(globalConfig.listenPort); + setListenPort(String(globalConfig.listenPort)); } }, [globalConfig]); @@ -102,11 +102,13 @@ export function ProxyPanel() { const handleSaveBasicConfig = async () => { if (!globalConfig) return; + const port = parseInt(listenPort); + const validPort = isNaN(port) || port < 1024 || port > 65535 ? 15721 : port; try { await updateGlobalConfig.mutateAsync({ ...globalConfig, listenAddress, - listenPort, + listenPort: validPort, }); toast.success( t("proxy.settings.configSaved", { defaultValue: "代理配置已保存" }), @@ -414,9 +416,7 @@ export function ProxyPanel() { id="listen-port" type="number" value={listenPort} - onChange={(e) => - setListenPort(parseInt(e.target.value) || 15721) - } + onChange={(e) => setListenPort(e.target.value)} placeholder={t( "proxy.settings.fields.listenPort.placeholder", { diff --git a/src/components/usage/ModelTestConfigPanel.tsx b/src/components/usage/ModelTestConfigPanel.tsx index 213cf615..95a7e889 100644 --- a/src/components/usage/ModelTestConfigPanel.tsx +++ b/src/components/usage/ModelTestConfigPanel.tsx @@ -17,10 +17,11 @@ export function ModelTestConfigPanel() { const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); - const [config, setConfig] = useState({ - timeoutSecs: 45, - maxRetries: 2, - degradedThresholdMs: 6000, + // 使用字符串状态以支持完全清空数字输入框 + const [config, setConfig] = useState({ + timeoutSecs: "45", + maxRetries: "2", + degradedThresholdMs: "6000", claudeModel: "claude-haiku-4-5-20251001", codexModel: "gpt-5.1-codex@low", geminiModel: "gemini-3-pro-preview", @@ -35,7 +36,14 @@ export function ModelTestConfigPanel() { setIsLoading(true); setError(null); const data = await getStreamCheckConfig(); - setConfig(data); + setConfig({ + timeoutSecs: String(data.timeoutSecs), + maxRetries: String(data.maxRetries), + degradedThresholdMs: String(data.degradedThresholdMs), + claudeModel: data.claudeModel, + codexModel: data.codexModel, + geminiModel: data.geminiModel, + }); } catch (e) { setError(String(e)); } finally { @@ -44,9 +52,22 @@ export function ModelTestConfigPanel() { } async function handleSave() { + // 解析数字,空值使用默认值,0 是有效值 + const parseNum = (val: string, defaultVal: number) => { + const n = parseInt(val); + return isNaN(n) ? defaultVal : n; + }; try { setIsSaving(true); - await saveStreamCheckConfig(config); + const parsed: StreamCheckConfig = { + timeoutSecs: parseNum(config.timeoutSecs, 45), + maxRetries: parseNum(config.maxRetries, 2), + degradedThresholdMs: parseNum(config.degradedThresholdMs, 6000), + claudeModel: config.claudeModel, + codexModel: config.codexModel, + geminiModel: config.geminiModel, + }; + await saveStreamCheckConfig(parsed); toast.success(t("streamCheck.configSaved"), { closeButton: true, }); @@ -132,10 +153,7 @@ export function ModelTestConfigPanel() { max={120} value={config.timeoutSecs} onChange={(e) => - setConfig({ - ...config, - timeoutSecs: parseInt(e.target.value) || 45, - }) + setConfig({ ...config, timeoutSecs: e.target.value }) } /> @@ -149,10 +167,7 @@ export function ModelTestConfigPanel() { max={5} value={config.maxRetries} onChange={(e) => - setConfig({ - ...config, - maxRetries: parseInt(e.target.value) || 2, - }) + setConfig({ ...config, maxRetries: e.target.value }) } /> @@ -169,10 +184,7 @@ export function ModelTestConfigPanel() { step={1000} value={config.degradedThresholdMs} onChange={(e) => - setConfig({ - ...config, - degradedThresholdMs: parseInt(e.target.value) || 6000, - }) + setConfig({ ...config, degradedThresholdMs: e.target.value }) } />