diff --git a/src/components/proxy/AutoFailoverConfigPanel.tsx b/src/components/proxy/AutoFailoverConfigPanel.tsx index 43aa71bbc..9e8710f28 100644 --- a/src/components/proxy/AutoFailoverConfigPanel.tsx +++ b/src/components/proxy/AutoFailoverConfigPanel.tsx @@ -1,37 +1,30 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { ChevronDown, ChevronRight, Save, Loader2, Info } from "lucide-react"; +import { Save, Loader2, Info } from "lucide-react"; import { toast } from "sonner"; import { useCircuitBreakerConfig, useUpdateCircuitBreakerConfig, } from "@/lib/query/failover"; -/** - * 自动故障转移配置面板 - * 放置在高级设置页面,模型测试配置下方 - */ -export function AutoFailoverConfigPanel() { - const { t } = useTranslation(); - const [isExpanded, setIsExpanded] = useState(false); +export interface AutoFailoverConfigPanelProps { + enabled: boolean; + onEnabledChange: (enabled: boolean) => void; +} +export function AutoFailoverConfigPanel({ + enabled, + onEnabledChange, +}: AutoFailoverConfigPanelProps) { + const { t } = useTranslation(); const { data: config, isLoading, error } = useCircuitBreakerConfig(); const updateConfig = useUpdateCircuitBreakerConfig(); const [formData, setFormData] = useState({ - enabled: true, // 自动故障转移总开关 failureThreshold: 5, successThreshold: 2, timeoutSeconds: 60, @@ -42,7 +35,6 @@ export function AutoFailoverConfigPanel() { useEffect(() => { if (config) { setFormData({ - enabled: true, // 默认开启,后续可以从数据库读取 ...config, }); } @@ -70,7 +62,6 @@ export function AutoFailoverConfigPanel() { const handleReset = () => { if (config) { setFormData({ - enabled: formData.enabled, ...config, }); } @@ -78,326 +69,268 @@ export function AutoFailoverConfigPanel() { if (isLoading) { return ( - - setIsExpanded(!isExpanded)} - > -
- - - {t("proxy.autoFailover.title", "自动故障转移配置")} - -
-
-
+
+ +
); } return ( - - setIsExpanded(!isExpanded)} - > -
- {isExpanded ? ( - - ) : ( - - )} -
-
- - {t("proxy.autoFailover.title", "自动故障转移配置")} - - {/* 总开关 - 点击时阻止折叠/展开 */} -
e.stopPropagation()} - className="flex items-center gap-2" - > - - setFormData({ ...formData, enabled: checked }) - } - className="scale-90" - /> - -
-
- {!isExpanded && ( - - {t( - "proxy.autoFailover.description", - "配置多个代理目标之间的自动切换规则和熔断策略", - )} - +
+ {/* 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). + */} + +
+ {error && ( + + {String(error)} + + )} + + + + + {t( + "proxy.autoFailover.info", + "当启用多个代理目标时,系统会按优先级顺序依次尝试。当某个供应商连续失败达到阈值时,熔断器会自动打开,跳过该供应商。", )} + + + + {/* 重试与超时配置 */} +
+

+ {t("proxy.autoFailover.retrySettings", "重试与超时设置")} +

+ +
+
+ + + setFormData({ + ...formData, + failureThreshold: parseInt(e.target.value) || 5, + }) + } + disabled={!enabled} + /> +

+ {t( + "proxy.autoFailover.failureThresholdHint", + "连续失败多少次后打开熔断器(建议: 3-10)", + )} +

+
+ +
+ + + setFormData({ + ...formData, + timeoutSeconds: parseInt(e.target.value) || 60, + }) + } + disabled={!enabled} + /> +

+ {t( + "proxy.autoFailover.timeoutHint", + "熔断器打开后,等待多久后尝试恢复(建议: 30-120)", + )} +

+
- - {isExpanded && ( - - {error && ( - - {String(error)} - - )} + {/* 熔断器高级配置 */} +
+

+ {t("proxy.autoFailover.circuitBreakerSettings", "熔断器高级设置")} +

- - - +
+
+ + + setFormData({ + ...formData, + successThreshold: parseInt(e.target.value) || 2, + }) + } + disabled={!enabled} + /> +

+ {t( + "proxy.autoFailover.successThresholdHint", + "半开状态下成功多少次后关闭熔断器", + )} +

+
+ +
+ + + setFormData({ + ...formData, + errorRateThreshold: (parseInt(e.target.value) || 50) / 100, + }) + } + disabled={!enabled} + /> +

+ {t( + "proxy.autoFailover.errorRateHint", + "错误率超过此值时打开熔断器", + )} +

+
+ +
+ + + setFormData({ + ...formData, + minRequests: parseInt(e.target.value) || 10, + }) + } + disabled={!enabled} + /> +

+ {t( + "proxy.autoFailover.minRequestsHint", + "计算错误率前的最小请求数", + )} +

+
+
+
+ + {/* 操作按钮 */} +
+ + +
+ + {/* 说明信息 */} +
+

+ {t("proxy.autoFailover.explanationTitle", "工作原理")} +

+
    +
  • + •{" "} + + {t("proxy.autoFailover.failureThresholdLabel", "失败阈值")} + + : {t( - "proxy.autoFailover.info", - "当启用多个代理目标时,系统会按优先级顺序依次尝试。当某个供应商连续失败达到阈值时,熔断器会自动打开,跳过该供应商。", + "proxy.autoFailover.failureThresholdExplain", + "连续失败达到此次数时,熔断器打开,该供应商暂时不可用", )} - - - - {/* 重试与超时配置 */} -
    -

    - {t("proxy.autoFailover.retrySettings", "重试与超时设置")} -

    - -
    -
    - - - setFormData({ - ...formData, - failureThreshold: parseInt(e.target.value) || 5, - }) - } - disabled={!formData.enabled} - /> -

    - {t( - "proxy.autoFailover.failureThresholdHint", - "连续失败多少次后打开熔断器(建议: 3-10)", - )} -

    -
    - -
    - - - setFormData({ - ...formData, - timeoutSeconds: parseInt(e.target.value) || 60, - }) - } - disabled={!formData.enabled} - /> -

    - {t( - "proxy.autoFailover.timeoutHint", - "熔断器打开后,等待多久后尝试恢复(建议: 30-120)", - )} -

    -
    -
    -
    - - {/* 熔断器高级配置 */} -
    -

    - {t("proxy.autoFailover.circuitBreakerSettings", "熔断器高级设置")} -

    - -
    -
    - - - setFormData({ - ...formData, - successThreshold: parseInt(e.target.value) || 2, - }) - } - disabled={!formData.enabled} - /> -

    - {t( - "proxy.autoFailover.successThresholdHint", - "半开状态下成功多少次后关闭熔断器", - )} -

    -
    - -
    - - - setFormData({ - ...formData, - errorRateThreshold: - (parseInt(e.target.value) || 50) / 100, - }) - } - disabled={!formData.enabled} - /> -

    - {t( - "proxy.autoFailover.errorRateHint", - "错误率超过此值时打开熔断器", - )} -

    -
    - -
    - - - setFormData({ - ...formData, - minRequests: parseInt(e.target.value) || 10, - }) - } - disabled={!formData.enabled} - /> -

    - {t( - "proxy.autoFailover.minRequestsHint", - "计算错误率前的最小请求数", - )} -

    -
    -
    -
    - - {/* 操作按钮 */} -
    - -
  • +
  • + •{" "} + + {t("proxy.autoFailover.timeoutLabel", "恢复等待时间")} + + : + {t( + "proxy.autoFailover.timeoutExplain", + "熔断器打开后,等待此时间后尝试半开状态", )} - -
- - {/* 说明信息 */} -
-

- {t("proxy.autoFailover.explanationTitle", "工作原理")} -

-
    -
  • - •{" "} - - {t("proxy.autoFailover.failureThresholdLabel", "失败阈值")} - - : - {t( - "proxy.autoFailover.failureThresholdExplain", - "连续失败达到此次数时,熔断器打开,该供应商暂时不可用", - )} -
  • -
  • - •{" "} - - {t("proxy.autoFailover.timeoutLabel", "恢复等待时间")} - - : - {t( - "proxy.autoFailover.timeoutExplain", - "熔断器打开后,等待此时间后尝试半开状态", - )} -
  • -
  • - •{" "} - - {t( - "proxy.autoFailover.successThresholdLabel", - "恢复成功阈值", - )} - - : - {t( - "proxy.autoFailover.successThresholdExplain", - "半开状态下,成功达到此次数时关闭熔断器,供应商恢复可用", - )} -
  • -
  • - •{" "} - - {t("proxy.autoFailover.errorRateLabel", "错误率阈值")} - - : - {t( - "proxy.autoFailover.errorRateExplain", - "错误率超过此值时,即使未达到失败阈值也会打开熔断器", - )} -
  • -
-
-
- )} - + +
  • + •{" "} + + {t("proxy.autoFailover.successThresholdLabel", "恢复成功阈值")} + + : + {t( + "proxy.autoFailover.successThresholdExplain", + "半开状态下,成功达到此次数时关闭熔断器,供应商恢复可用", + )} +
  • +
  • + •{" "} + + {t("proxy.autoFailover.errorRateLabel", "错误率阈值")} + + : + {t( + "proxy.autoFailover.errorRateExplain", + "错误率超过此值时,即使未达到失败阈值也会打开熔断器", + )} +
  • + +
    +
    +
    ); } diff --git a/src/components/proxy/ProxyPanel.tsx b/src/components/proxy/ProxyPanel.tsx index f4c4777fc..fab2fb54f 100644 --- a/src/components/proxy/ProxyPanel.tsx +++ b/src/components/proxy/ProxyPanel.tsx @@ -1,16 +1,7 @@ import { useState } from "react"; -import { Switch } from "@/components/ui/switch"; -import { Badge } from "@/components/ui/badge"; +import { Activity, Clock, TrendingUp, Server, ListOrdered } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useProxyStatus } from "@/hooks/useProxyStatus"; -import { - Settings, - Activity, - Clock, - TrendingUp, - Server, - ListOrdered, -} from "lucide-react"; import { ProxySettingsDialog } from "./ProxySettingsDialog"; import { toast } from "sonner"; import { useProxyTargets } from "@/lib/query/failover"; @@ -19,7 +10,7 @@ import { useProviderHealth } from "@/lib/query/failover"; import type { ProxyStatus } from "@/types/proxy"; export function ProxyPanel() { - const { status, isRunning, start, stop, isPending } = useProxyStatus(); + const { status, isRunning } = useProxyStatus(); const [showSettings, setShowSettings] = useState(false); // 获取所有三个应用类型的代理目标列表 @@ -27,18 +18,6 @@ export function ProxyPanel() { const { data: codexTargets = [] } = useProxyTargets("codex"); const { data: geminiTargets = [] } = useProxyTargets("gemini"); - const handleToggle = async () => { - try { - if (isRunning) { - await stop(); - } else { - await start(); - } - } catch (error) { - console.error("Toggle proxy failed:", error); - } - }; - const formatUptime = (seconds: number): string => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); @@ -55,58 +34,12 @@ export function ProxyPanel() { return ( <> -
    -
    -
    -
    - -
    -
    -

    - 本地代理服务 -

    -

    - {isRunning - ? `运行中 · ${status?.address}:${status?.port}` - : "已停止"} -

    -
    -
    -
    - - - {isRunning ? "运行中" : "已停止"} - - - -
    -
    - +
    {isRunning && status ? (
    -
    +
    -

    - 服务地址 -

    +

    服务地址

    http://{status.address}:{status.port} @@ -126,16 +59,14 @@ export function ProxyPanel() {
    -
    -

    - 使用中 -

    +
    +

    使用中

    {status.active_targets && status.active_targets.length > 0 ? (
    {status.active_targets.map((target) => (
    {target.app_type} @@ -167,10 +98,10 @@ export function ProxyPanel() { {(claudeTargets.length > 0 || codexTargets.length > 0 || geminiTargets.length > 0) && ( -
    +
    -

    +

    故障转移队列

    @@ -268,13 +199,11 @@ function StatCard({ icon, label, value, variant = "default" }: StatCardProps) { return (
    {icon} - - {label} - + {label}

    {value}

    @@ -351,7 +280,7 @@ function ProviderQueueItem({ className={`flex items-center justify-between rounded-md border px-3 py-2 text-sm transition-colors ${ isCurrent ? "border-primary/40 bg-primary/10 text-primary font-medium" - : "border-white/10 bg-background/60" + : "border-border bg-background/60" }`} >