mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
refactor(ui): redesign proxy panel with inline config controls
Replace ProxySettingsDialog with inline controls in ProxyPanel. Add per-app takeover switches and global address/port settings. Simplify AutoFailoverConfigPanel by removing timeout settings.
This commit is contained in:
@@ -6,51 +6,67 @@ import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Save, Loader2, Info } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useCircuitBreakerConfig,
|
||||
useUpdateCircuitBreakerConfig,
|
||||
} from "@/lib/query/failover";
|
||||
import { useAppProxyConfig, useUpdateAppProxyConfig } from "@/lib/query/proxy";
|
||||
|
||||
export interface AutoFailoverConfigPanelProps {
|
||||
enabled?: boolean;
|
||||
onEnabledChange?: (enabled: boolean) => void;
|
||||
appType: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function AutoFailoverConfigPanel({
|
||||
enabled = true,
|
||||
onEnabledChange: _onEnabledChange,
|
||||
}: AutoFailoverConfigPanelProps = {}) {
|
||||
// Note: onEnabledChange is currently unused but kept in the interface
|
||||
// for potential future use by parent components
|
||||
void _onEnabledChange;
|
||||
appType,
|
||||
disabled = false,
|
||||
}: AutoFailoverConfigPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: config, isLoading, error } = useCircuitBreakerConfig();
|
||||
const updateConfig = useUpdateCircuitBreakerConfig();
|
||||
const { data: config, isLoading, error } = useAppProxyConfig(appType);
|
||||
const updateConfig = useUpdateAppProxyConfig();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
failureThreshold: 5,
|
||||
successThreshold: 2,
|
||||
timeoutSeconds: 60,
|
||||
errorRateThreshold: 0.5,
|
||||
minRequests: 10,
|
||||
autoFailoverEnabled: false,
|
||||
maxRetries: 3,
|
||||
streamingFirstByteTimeout: 30,
|
||||
streamingIdleTimeout: 60,
|
||||
nonStreamingTimeout: 300,
|
||||
circuitFailureThreshold: 5,
|
||||
circuitSuccessThreshold: 2,
|
||||
circuitTimeoutSeconds: 60,
|
||||
circuitErrorRateThreshold: 0.5,
|
||||
circuitMinRequests: 10,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setFormData({
|
||||
...config,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!config) return;
|
||||
try {
|
||||
await updateConfig.mutateAsync({
|
||||
failureThreshold: formData.failureThreshold,
|
||||
successThreshold: formData.successThreshold,
|
||||
timeoutSeconds: formData.timeoutSeconds,
|
||||
errorRateThreshold: formData.errorRateThreshold,
|
||||
minRequests: formData.minRequests,
|
||||
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,
|
||||
});
|
||||
toast.success(
|
||||
t("proxy.autoFailover.configSaved", "自动故障转移配置已保存"),
|
||||
@@ -66,7 +82,16 @@ export function AutoFailoverConfigPanel({
|
||||
const handleReset = () => {
|
||||
if (config) {
|
||||
setFormData({
|
||||
...config,
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -79,16 +104,10 @@ export function AutoFailoverConfigPanel({
|
||||
);
|
||||
}
|
||||
|
||||
const isDisabled = disabled || updateConfig.isPending;
|
||||
|
||||
return (
|
||||
<div className="border-0 rounded-none shadow-none bg-transparent">
|
||||
{/* Header Switch moved to parent accordion logic or kept here absolutely positioned if styling permits.
|
||||
Since we need it in the accordion header, and this component is inside the content, we can use a portal or
|
||||
absolute positioning trick similar to ProxyPanel, OR cleaner, just duplicate the switch logic in SettingsPage
|
||||
and pass it down. But for now, let's use the absolute positioning trick to "lift" it visually.
|
||||
Better yet, let's just render the content directly without the wrapping Card header/collapse logic
|
||||
since the user requested "click to expand is detailed info, no need to fold again" (implying the accordion handles folding).
|
||||
*/}
|
||||
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
@@ -114,22 +133,48 @@ export function AutoFailoverConfigPanel({
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="failureThreshold">
|
||||
{t("proxy.autoFailover.failureThreshold", "失败阈值")}
|
||||
<Label htmlFor={`maxRetries-${appType}`}>
|
||||
{t("proxy.autoFailover.maxRetries", "最大重试次数")}
|
||||
</Label>
|
||||
<Input
|
||||
id="failureThreshold"
|
||||
id={`maxRetries-${appType}`}
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
value={formData.failureThreshold}
|
||||
min="0"
|
||||
max="10"
|
||||
value={formData.maxRetries}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
failureThreshold: parseInt(e.target.value) || 5,
|
||||
maxRetries: parseInt(e.target.value) || 3,
|
||||
})
|
||||
}
|
||||
disabled={!enabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"proxy.autoFailover.maxRetriesHint",
|
||||
"请求失败时的重试次数(0-10)",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`failureThreshold-${appType}`}>
|
||||
{t("proxy.autoFailover.failureThreshold", "失败阈值")}
|
||||
</Label>
|
||||
<Input
|
||||
id={`failureThreshold-${appType}`}
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
value={formData.circuitFailureThreshold}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
circuitFailureThreshold: parseInt(e.target.value) || 5,
|
||||
})
|
||||
}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
@@ -138,59 +183,123 @@ export function AutoFailoverConfigPanel({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 超时配置 */}
|
||||
<div className="space-y-4 rounded-lg border border-white/10 bg-muted/30 p-4">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.autoFailover.timeoutSettings", "超时配置")}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timeoutSeconds">
|
||||
{t("proxy.autoFailover.timeout", "恢复等待时间(秒)")}
|
||||
<Label htmlFor={`streamingFirstByte-${appType}`}>
|
||||
{t(
|
||||
"proxy.autoFailover.streamingFirstByte",
|
||||
"流式首字节超时(秒)",
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id="timeoutSeconds"
|
||||
id={`streamingFirstByte-${appType}`}
|
||||
type="number"
|
||||
min="10"
|
||||
max="300"
|
||||
value={formData.timeoutSeconds}
|
||||
min="0"
|
||||
max="180"
|
||||
value={formData.streamingFirstByteTimeout}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
timeoutSeconds: parseInt(e.target.value) || 60,
|
||||
streamingFirstByteTimeout: parseInt(e.target.value) || 30,
|
||||
})
|
||||
}
|
||||
disabled={!enabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"proxy.autoFailover.timeoutHint",
|
||||
"熔断器打开后,等待多久后尝试恢复(建议: 30-120)",
|
||||
"proxy.autoFailover.streamingFirstByteHint",
|
||||
"等待首个数据块的最大时间",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`streamingIdle-${appType}`}>
|
||||
{t("proxy.autoFailover.streamingIdle", "流式静默超时(秒)")}
|
||||
</Label>
|
||||
<Input
|
||||
id={`streamingIdle-${appType}`}
|
||||
type="number"
|
||||
min="0"
|
||||
max="600"
|
||||
value={formData.streamingIdleTimeout}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
streamingIdleTimeout: parseInt(e.target.value) || 60,
|
||||
})
|
||||
}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"proxy.autoFailover.streamingIdleHint",
|
||||
"数据块之间的最大间隔",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`nonStreaming-${appType}`}>
|
||||
{t("proxy.autoFailover.nonStreaming", "非流式超时(秒)")}
|
||||
</Label>
|
||||
<Input
|
||||
id={`nonStreaming-${appType}`}
|
||||
type="number"
|
||||
min="0"
|
||||
max="1800"
|
||||
value={formData.nonStreamingTimeout}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
nonStreamingTimeout: parseInt(e.target.value) || 300,
|
||||
})
|
||||
}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"proxy.autoFailover.nonStreamingHint",
|
||||
"非流式请求的总超时时间",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 熔断器高级配置 */}
|
||||
{/* 熔断器配置 */}
|
||||
<div className="space-y-4 rounded-lg border border-white/10 bg-muted/30 p-4">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.autoFailover.circuitBreakerSettings", "熔断器高级设置")}
|
||||
{t("proxy.autoFailover.circuitBreakerSettings", "熔断器配置")}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="successThreshold">
|
||||
<Label htmlFor={`successThreshold-${appType}`}>
|
||||
{t("proxy.autoFailover.successThreshold", "恢复成功阈值")}
|
||||
</Label>
|
||||
<Input
|
||||
id="successThreshold"
|
||||
id={`successThreshold-${appType}`}
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={formData.successThreshold}
|
||||
value={formData.circuitSuccessThreshold}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
successThreshold: parseInt(e.target.value) || 2,
|
||||
circuitSuccessThreshold: parseInt(e.target.value) || 2,
|
||||
})
|
||||
}
|
||||
disabled={!enabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
@@ -201,23 +310,50 @@ export function AutoFailoverConfigPanel({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="errorRateThreshold">
|
||||
<Label htmlFor={`timeoutSeconds-${appType}`}>
|
||||
{t("proxy.autoFailover.timeout", "恢复等待时间(秒)")}
|
||||
</Label>
|
||||
<Input
|
||||
id={`timeoutSeconds-${appType}`}
|
||||
type="number"
|
||||
min="10"
|
||||
max="300"
|
||||
value={formData.circuitTimeoutSeconds}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
circuitTimeoutSeconds: parseInt(e.target.value) || 60,
|
||||
})
|
||||
}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"proxy.autoFailover.timeoutHint",
|
||||
"熔断器打开后,等待多久后尝试恢复(建议: 30-120)",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`errorRateThreshold-${appType}`}>
|
||||
{t("proxy.autoFailover.errorRate", "错误率阈值 (%)")}
|
||||
</Label>
|
||||
<Input
|
||||
id="errorRateThreshold"
|
||||
id={`errorRateThreshold-${appType}`}
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
value={Math.round(formData.errorRateThreshold * 100)}
|
||||
value={Math.round(formData.circuitErrorRateThreshold * 100)}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
errorRateThreshold: (parseInt(e.target.value) || 50) / 100,
|
||||
circuitErrorRateThreshold:
|
||||
(parseInt(e.target.value) || 50) / 100,
|
||||
})
|
||||
}
|
||||
disabled={!enabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
@@ -228,22 +364,22 @@ export function AutoFailoverConfigPanel({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="minRequests">
|
||||
<Label htmlFor={`minRequests-${appType}`}>
|
||||
{t("proxy.autoFailover.minRequests", "最小请求数")}
|
||||
</Label>
|
||||
<Input
|
||||
id="minRequests"
|
||||
id={`minRequests-${appType}`}
|
||||
type="number"
|
||||
min="5"
|
||||
max="100"
|
||||
value={formData.minRequests}
|
||||
value={formData.circuitMinRequests}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
minRequests: parseInt(e.target.value) || 10,
|
||||
circuitMinRequests: parseInt(e.target.value) || 10,
|
||||
})
|
||||
}
|
||||
disabled={!enabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
@@ -257,17 +393,10 @@ export function AutoFailoverConfigPanel({
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
disabled={updateConfig.isPending || !enabled}
|
||||
>
|
||||
<Button variant="outline" onClick={handleReset} disabled={isDisabled}>
|
||||
{t("common.reset", "重置")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={updateConfig.isPending || !enabled}
|
||||
>
|
||||
<Button onClick={handleSave} disabled={isDisabled}>
|
||||
{updateConfig.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
@@ -281,59 +410,6 @@ export function AutoFailoverConfigPanel({
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 说明信息 */}
|
||||
<div className="p-4 bg-muted/50 rounded-lg space-y-2 text-sm">
|
||||
<h4 className="font-medium">
|
||||
{t("proxy.autoFailover.explanationTitle", "工作原理")}
|
||||
</h4>
|
||||
<ul className="space-y-1 text-muted-foreground">
|
||||
<li>
|
||||
•{" "}
|
||||
<strong>
|
||||
{t("proxy.autoFailover.failureThresholdLabel", "失败阈值")}
|
||||
</strong>
|
||||
:
|
||||
{t(
|
||||
"proxy.autoFailover.failureThresholdExplain",
|
||||
"连续失败达到此次数时,熔断器打开,该供应商暂时不可用",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<strong>
|
||||
{t("proxy.autoFailover.timeoutLabel", "恢复等待时间")}
|
||||
</strong>
|
||||
:
|
||||
{t(
|
||||
"proxy.autoFailover.timeoutExplain",
|
||||
"熔断器打开后,等待此时间后尝试半开状态",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<strong>
|
||||
{t("proxy.autoFailover.successThresholdLabel", "恢复成功阈值")}
|
||||
</strong>
|
||||
:
|
||||
{t(
|
||||
"proxy.autoFailover.successThresholdExplain",
|
||||
"半开状态下,成功达到此次数时关闭熔断器,供应商恢复可用",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
•{" "}
|
||||
<strong>
|
||||
{t("proxy.autoFailover.errorRateLabel", "错误率阈值")}
|
||||
</strong>
|
||||
:
|
||||
{t(
|
||||
"proxy.autoFailover.errorRateExplain",
|
||||
"错误率超过此值时,即使未达到失败阈值也会打开熔断器",
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,54 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Activity,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
Server,
|
||||
ListOrdered,
|
||||
Settings,
|
||||
Save,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { ProxySettingsDialog } from "./ProxySettingsDialog";
|
||||
import { toast } from "sonner";
|
||||
import { useFailoverQueue } from "@/lib/query/failover";
|
||||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||||
import { useProviderHealth } from "@/lib/query/failover";
|
||||
import {
|
||||
useProxyTakeoverStatus,
|
||||
useSetProxyTakeoverForApp,
|
||||
useGlobalProxyConfig,
|
||||
useUpdateGlobalProxyConfig,
|
||||
} from "@/lib/query/proxy";
|
||||
import type { ProxyStatus } from "@/types/proxy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ProxyPanel() {
|
||||
const { t } = useTranslation();
|
||||
const { status, isRunning } = useProxyStatus();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
// 获取应用接管状态
|
||||
const { data: takeoverStatus } = useProxyTakeoverStatus();
|
||||
const setTakeoverForApp = useSetProxyTakeoverForApp();
|
||||
|
||||
// 获取全局代理配置
|
||||
const { data: globalConfig } = useGlobalProxyConfig();
|
||||
const updateGlobalConfig = useUpdateGlobalProxyConfig();
|
||||
|
||||
// 监听地址/端口的本地状态
|
||||
const [listenAddress, setListenAddress] = useState("127.0.0.1");
|
||||
const [listenPort, setListenPort] = useState(5000);
|
||||
|
||||
// 同步全局配置到本地状态
|
||||
useEffect(() => {
|
||||
if (globalConfig) {
|
||||
setListenAddress(globalConfig.listenAddress);
|
||||
setListenPort(globalConfig.listenPort);
|
||||
}
|
||||
}, [globalConfig]);
|
||||
|
||||
// 获取所有三个应用类型的故障转移队列(不包含当前供应商)
|
||||
// 当前供应商始终优先,队列仅用于失败后的备用顺序
|
||||
@@ -28,6 +56,69 @@ export function ProxyPanel() {
|
||||
const { data: codexQueue = [] } = useFailoverQueue("codex");
|
||||
const { data: geminiQueue = [] } = useFailoverQueue("gemini");
|
||||
|
||||
const handleTakeoverChange = async (appType: string, enabled: boolean) => {
|
||||
try {
|
||||
await setTakeoverForApp.mutateAsync({ appType, enabled });
|
||||
toast.success(
|
||||
enabled
|
||||
? t("proxy.takeover.enabled", {
|
||||
app: appType,
|
||||
defaultValue: `${appType} 接管已启用`,
|
||||
})
|
||||
: t("proxy.takeover.disabled", {
|
||||
app: appType,
|
||||
defaultValue: `${appType} 接管已关闭`,
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t("proxy.takeover.failed", {
|
||||
defaultValue: "切换接管状态失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoggingChange = async (enabled: boolean) => {
|
||||
if (!globalConfig) return;
|
||||
try {
|
||||
await updateGlobalConfig.mutateAsync({
|
||||
...globalConfig,
|
||||
enableLogging: enabled,
|
||||
});
|
||||
toast.success(
|
||||
enabled
|
||||
? t("proxy.logging.enabled", { defaultValue: "日志记录已启用" })
|
||||
: t("proxy.logging.disabled", { defaultValue: "日志记录已关闭" }),
|
||||
{ closeButton: true },
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t("proxy.logging.failed", { defaultValue: "切换日志状态失败" }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveBasicConfig = async () => {
|
||||
if (!globalConfig) return;
|
||||
try {
|
||||
await updateGlobalConfig.mutateAsync({
|
||||
...globalConfig,
|
||||
listenAddress,
|
||||
listenPort,
|
||||
});
|
||||
toast.success(
|
||||
t("proxy.settings.configSaved", { defaultValue: "代理配置已保存" }),
|
||||
{ closeButton: true },
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t("proxy.settings.configSaveFailed", { defaultValue: "保存配置失败" }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
@@ -49,22 +140,11 @@ export function ProxyPanel() {
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.panel.serviceAddress", {
|
||||
defaultValue: "服务地址",
|
||||
})}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="h-7 gap-1.5 text-xs"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
{t("common.settings")}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
{t("proxy.panel.serviceAddress", {
|
||||
defaultValue: "服务地址",
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<code className="flex-1 text-sm bg-background px-3 py-2 rounded border border-border/60">
|
||||
http://{status.address}:{status.port}
|
||||
@@ -87,6 +167,11 @@ export function ProxyPanel() {
|
||||
{t("common.copy")}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{t("proxy.settings.restartRequired", {
|
||||
defaultValue: "修改监听地址/端口需要先停止代理服务",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-border space-y-2">
|
||||
@@ -130,6 +215,63 @@ export function ProxyPanel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 应用接管开关 */}
|
||||
<div className="pt-3 border-t border-border space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxyConfig.appTakeover", {
|
||||
defaultValue: "应用接管",
|
||||
})}
|
||||
</p>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{(["claude", "codex", "gemini"] as const).map((appType) => {
|
||||
const isEnabled =
|
||||
takeoverStatus?.[
|
||||
appType as keyof typeof takeoverStatus
|
||||
] ?? false;
|
||||
return (
|
||||
<div
|
||||
key={appType}
|
||||
className="flex items-center justify-between rounded-md border border-border bg-background/60 px-3 py-2"
|
||||
>
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{appType}
|
||||
</span>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
handleTakeoverChange(appType, checked)
|
||||
}
|
||||
disabled={setTakeoverForApp.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 日志记录开关 */}
|
||||
<div className="pt-3 border-t border-border">
|
||||
<div className="flex items-center justify-between rounded-md border border-border bg-background/60 px-3 py-2">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-sm font-medium">
|
||||
{t("proxy.settings.fields.enableLogging.label", {
|
||||
defaultValue: "启用日志记录",
|
||||
})}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.settings.fields.enableLogging.description", {
|
||||
defaultValue: "记录所有代理请求,便于排查问题",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={globalConfig?.enableLogging ?? true}
|
||||
onCheckedChange={handleLoggingChange}
|
||||
disabled={updateGlobalConfig.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 供应商队列 - 按应用类型分组展示 */}
|
||||
{(claudeQueue.length > 0 ||
|
||||
codexQueue.length > 0 ||
|
||||
@@ -217,36 +359,109 @@ export function ProxyPanel() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10 text-muted-foreground">
|
||||
<div className="mx-auto w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||
<Server className="h-8 w-8" />
|
||||
<div className="space-y-6">
|
||||
{/* 空白区域避免冲突 */}
|
||||
<div className="h-4"></div>
|
||||
|
||||
{/* 基础设置 - 监听地址/端口 */}
|
||||
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t("proxy.settings.basic.title", {
|
||||
defaultValue: "基础设置",
|
||||
})}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.settings.basic.description", {
|
||||
defaultValue: "配置代理服务监听的地址与端口。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="listen-address">
|
||||
{t("proxy.settings.fields.listenAddress.label", {
|
||||
defaultValue: "监听地址",
|
||||
})}
|
||||
</Label>
|
||||
<Input
|
||||
id="listen-address"
|
||||
value={listenAddress}
|
||||
onChange={(e) => setListenAddress(e.target.value)}
|
||||
placeholder="127.0.0.1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.settings.fields.listenAddress.description", {
|
||||
defaultValue:
|
||||
"代理服务器监听的 IP 地址(推荐 127.0.0.1)",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="listen-port">
|
||||
{t("proxy.settings.fields.listenPort.label", {
|
||||
defaultValue: "监听端口",
|
||||
})}
|
||||
</Label>
|
||||
<Input
|
||||
id="listen-port"
|
||||
type="number"
|
||||
value={listenPort}
|
||||
onChange={(e) =>
|
||||
setListenPort(parseInt(e.target.value) || 5000)
|
||||
}
|
||||
placeholder="5000"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.settings.fields.listenPort.description", {
|
||||
defaultValue: "代理服务器监听的端口号(1024 ~ 65535)",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSaveBasicConfig}
|
||||
disabled={updateGlobalConfig.isPending}
|
||||
>
|
||||
{updateGlobalConfig.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t("common.saving", { defaultValue: "保存中..." })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{t("common.save", { defaultValue: "保存" })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 代理服务已停止提示 */}
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<div className="mx-auto w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||
<Server className="h-8 w-8" />
|
||||
</div>
|
||||
<p className="text-base font-medium text-foreground mb-1">
|
||||
{t("proxy.panel.stoppedTitle", {
|
||||
defaultValue: "代理服务已停止",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proxy.panel.stoppedDescription", {
|
||||
defaultValue: "使用右上角开关即可启动服务",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-base font-medium text-foreground mb-1">
|
||||
{t("proxy.panel.stoppedTitle", {
|
||||
defaultValue: "代理服务已停止",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t("proxy.panel.stoppedDescription", {
|
||||
defaultValue: "使用右上角开关即可启动服务",
|
||||
})}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
{t("proxy.panel.openSettings", {
|
||||
defaultValue: "配置代理服务",
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<ProxySettingsDialog open={showSettings} onOpenChange={setShowSettings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,553 +0,0 @@
|
||||
/**
|
||||
* 代理服务设置对话框
|
||||
*/
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useProxyConfig } from "@/hooks/useProxyConfig";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import type { ProxyConfig } from "@/types/proxy";
|
||||
|
||||
// 表单数据类型(仅包含可编辑字段)
|
||||
type ProxyConfigForm = Pick<
|
||||
ProxyConfig,
|
||||
| "listen_address"
|
||||
| "listen_port"
|
||||
| "max_retries"
|
||||
| "request_timeout"
|
||||
| "enable_logging"
|
||||
| "streaming_first_byte_timeout"
|
||||
| "streaming_idle_timeout"
|
||||
| "non_streaming_timeout"
|
||||
>;
|
||||
|
||||
const createProxyConfigSchema = (t: TFunction) => {
|
||||
// 流式首字节超时:0 或 1-180 秒
|
||||
const streamingFirstByteSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.refine((val) => val === 0 || (val >= 1 && val <= 180), {
|
||||
message: t("proxy.settings.validation.streamingFirstByteRange", {
|
||||
defaultValue: "请输入 0 或 1-180 之间的数值",
|
||||
}),
|
||||
});
|
||||
|
||||
// 流式静默期超时:0 或 60-600 秒
|
||||
const streamingIdleSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.refine((val) => val === 0 || (val >= 60 && val <= 600), {
|
||||
message: t("proxy.settings.validation.streamingIdleRange", {
|
||||
defaultValue: "请输入 0 或 60-600 之间的数值",
|
||||
}),
|
||||
});
|
||||
|
||||
// 非流式总超时:0 或 60-1800 秒
|
||||
const nonStreamingSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.refine((val) => val === 0 || (val >= 60 && val <= 1800), {
|
||||
message: t("proxy.settings.validation.nonStreamingRange", {
|
||||
defaultValue: "请输入 0 或 60-1800 之间的数值",
|
||||
}),
|
||||
});
|
||||
|
||||
// 旧版请求超时(兼容)
|
||||
const requestTimeoutSchema = z.number().min(0);
|
||||
|
||||
return z.object({
|
||||
listen_address: z.string().regex(
|
||||
/^(\d{1,3}\.){3}\d{1,3}$/,
|
||||
t("proxy.settings.validation.addressInvalid", {
|
||||
defaultValue: "请输入有效的IP地址",
|
||||
}),
|
||||
),
|
||||
listen_port: z
|
||||
.number()
|
||||
.min(
|
||||
1024,
|
||||
t("proxy.settings.validation.portMin", {
|
||||
defaultValue: "端口必须大于1024",
|
||||
}),
|
||||
)
|
||||
.max(
|
||||
65535,
|
||||
t("proxy.settings.validation.portMax", {
|
||||
defaultValue: "端口必须小于65535",
|
||||
}),
|
||||
),
|
||||
max_retries: z
|
||||
.number()
|
||||
.min(
|
||||
0,
|
||||
t("proxy.settings.validation.retryMin", {
|
||||
defaultValue: "重试次数不能为负",
|
||||
}),
|
||||
)
|
||||
.max(
|
||||
10,
|
||||
t("proxy.settings.validation.retryMax", {
|
||||
defaultValue: "重试次数不能超过10",
|
||||
}),
|
||||
),
|
||||
request_timeout: requestTimeoutSchema,
|
||||
enable_logging: z.boolean(),
|
||||
streaming_first_byte_timeout: streamingFirstByteSchema,
|
||||
streaming_idle_timeout: streamingIdleSchema,
|
||||
non_streaming_timeout: nonStreamingSchema,
|
||||
});
|
||||
};
|
||||
|
||||
interface ProxySettingsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function ProxySettingsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ProxySettingsDialogProps) {
|
||||
const { config, isLoading, updateConfig, isUpdating } = useProxyConfig();
|
||||
const { t } = useTranslation();
|
||||
const schema = useMemo(() => createProxyConfigSchema(t), [t]);
|
||||
|
||||
const closePanel = () => onOpenChange(false);
|
||||
|
||||
const form = useForm<ProxyConfigForm>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
listen_address: "127.0.0.1",
|
||||
listen_port: 5000,
|
||||
max_retries: 3,
|
||||
request_timeout: 300,
|
||||
enable_logging: true,
|
||||
streaming_first_byte_timeout: 30,
|
||||
streaming_idle_timeout: 60,
|
||||
non_streaming_timeout: 600,
|
||||
},
|
||||
});
|
||||
|
||||
// 当配置加载完成后更新表单
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
form.reset({
|
||||
listen_address: config.listen_address,
|
||||
listen_port: config.listen_port,
|
||||
max_retries: config.max_retries,
|
||||
request_timeout: config.request_timeout,
|
||||
enable_logging: config.enable_logging,
|
||||
streaming_first_byte_timeout: config.streaming_first_byte_timeout ?? 30,
|
||||
streaming_idle_timeout: config.streaming_idle_timeout ?? 60,
|
||||
non_streaming_timeout: config.non_streaming_timeout ?? 300,
|
||||
});
|
||||
}
|
||||
}, [config, form]);
|
||||
|
||||
const onSubmit = async (data: ProxyConfigForm) => {
|
||||
try {
|
||||
await updateConfig(data);
|
||||
closePanel();
|
||||
} catch (error) {
|
||||
console.error("Save config failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const formId = "proxy-settings-form";
|
||||
|
||||
return (
|
||||
<FullScreenPanel
|
||||
isOpen={open}
|
||||
title={t("proxy.settings.title", { defaultValue: "代理服务设置" })}
|
||||
onClose={closePanel}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={closePanel}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{t("common.cancel", { defaultValue: "取消" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={formId}
|
||||
disabled={isUpdating || isLoading}
|
||||
>
|
||||
{isUpdating
|
||||
? t("common.saving", { defaultValue: "保存中..." })
|
||||
: t("proxy.settings.actions.save", { defaultValue: "保存配置" })}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proxy.settings.description", {
|
||||
defaultValue:
|
||||
"配置本地代理服务器的监听地址、端口和运行参数,保存后立即生效。",
|
||||
})}
|
||||
</p>
|
||||
<Alert className="border-emerald-500/40 bg-emerald-500/10">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
{t("proxy.settings.alert.autoApply", {
|
||||
defaultValue:
|
||||
"保存后将自动同步到正在运行的代理服务,无需手动重启。",
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<section className="space-y-4 rounded-xl border border-white/10 glass-card p-6">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
{t("proxy.settings.basic.title", {
|
||||
defaultValue: "基础设置",
|
||||
})}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proxy.settings.basic.description", {
|
||||
defaultValue: "配置代理服务监听的地址与端口。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="listen_address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.listenAddress.label", {
|
||||
defaultValue: "监听地址",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"proxy.settings.fields.listenAddress.placeholder",
|
||||
{ defaultValue: "127.0.0.1" },
|
||||
)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("proxy.settings.fields.listenAddress.description", {
|
||||
defaultValue:
|
||||
"代理服务器监听的 IP 地址(推荐 127.0.0.1)",
|
||||
})}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="listen_port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.listenPort.label", {
|
||||
defaultValue: "监听端口",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
placeholder={t(
|
||||
"proxy.settings.fields.listenPort.placeholder",
|
||||
{ defaultValue: "5000" },
|
||||
)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("proxy.settings.fields.listenPort.description", {
|
||||
defaultValue:
|
||||
"代理服务器监听的端口号(1024 ~ 65535)",
|
||||
})}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4 rounded-xl border border-white/10 glass-card p-6">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
{t("proxy.settings.advanced.title", {
|
||||
defaultValue: "高级参数",
|
||||
})}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proxy.settings.advanced.description", {
|
||||
defaultValue: "控制请求的稳定性和日志记录。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="max_retries"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.maxRetries.label", {
|
||||
defaultValue: "最大重试次数",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
placeholder={t(
|
||||
"proxy.settings.fields.maxRetries.placeholder",
|
||||
{ defaultValue: "3" },
|
||||
)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("proxy.settings.fields.maxRetries.description", {
|
||||
defaultValue: "请求失败时的重试次数(0 ~ 10)",
|
||||
})}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="request_timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.requestTimeout.label", {
|
||||
defaultValue: "请求超时(秒)",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
placeholder={t(
|
||||
"proxy.settings.fields.requestTimeout.placeholder",
|
||||
{ defaultValue: "0(不限)或 300" },
|
||||
)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("proxy.settings.fields.requestTimeout.description", {
|
||||
defaultValue:
|
||||
"单个请求的最大等待时间(0 表示不限制,或设置 10 ~ 600 秒)",
|
||||
})}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enable_logging"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border border-white/10 bg-background/60 p-4">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.enableLogging.label", {
|
||||
defaultValue: "启用日志记录",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t("proxy.settings.fields.enableLogging.description", {
|
||||
defaultValue: "记录所有代理请求,便于排查问题",
|
||||
})}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4 rounded-xl border border-white/10 glass-card p-6">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
{t("proxy.settings.timeout.title", {
|
||||
defaultValue: "超时设置",
|
||||
})}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proxy.settings.timeout.description", {
|
||||
defaultValue: "配置流式和非流式请求的超时时间。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="streaming_first_byte_timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"proxy.settings.fields.streamingFirstByteTimeout.label",
|
||||
{
|
||||
defaultValue: "流式首字超时(秒)",
|
||||
},
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
placeholder="30"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"proxy.settings.fields.streamingFirstByteTimeout.description",
|
||||
{
|
||||
defaultValue: "等待首个数据块的最大时间",
|
||||
},
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="streaming_idle_timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.streamingIdleTimeout.label", {
|
||||
defaultValue: "流式静默超时(秒)",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
placeholder="60"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"proxy.settings.fields.streamingIdleTimeout.description",
|
||||
{
|
||||
defaultValue: "数据块之间的最大间隔",
|
||||
},
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="non_streaming_timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("proxy.settings.fields.nonStreamingTimeout.label", {
|
||||
defaultValue: "非流式超时(秒)",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value, 10) || 0)
|
||||
}
|
||||
placeholder="300"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"proxy.settings.fields.nonStreamingTimeout.description",
|
||||
{
|
||||
defaultValue: "非流式请求的总超时时间",
|
||||
},
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</FullScreenPanel>
|
||||
);
|
||||
}
|
||||
@@ -3,4 +3,3 @@
|
||||
*/
|
||||
|
||||
export { ProxyPanel } from "./ProxyPanel";
|
||||
export { ProxySettingsDialog } from "./ProxySettingsDialog";
|
||||
|
||||
Reference in New Issue
Block a user