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"
- />
-
- {formData.enabled
- ? t("proxy.autoFailover.enabled", "已启用")
- : t("proxy.autoFailover.disabled", "已禁用")}
-
-
-
- {!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", "重试与超时设置")}
+
+
+
+
+
+ {t("proxy.autoFailover.failureThreshold", "失败阈值")}
+
+
+ setFormData({
+ ...formData,
+ failureThreshold: parseInt(e.target.value) || 5,
+ })
+ }
+ disabled={!enabled}
+ />
+
+ {t(
+ "proxy.autoFailover.failureThresholdHint",
+ "连续失败多少次后打开熔断器(建议: 3-10)",
+ )}
+
+
+
+
+
+ {t("proxy.autoFailover.timeout", "恢复等待时间(秒)")}
+
+
+ setFormData({
+ ...formData,
+ timeoutSeconds: parseInt(e.target.value) || 60,
+ })
+ }
+ disabled={!enabled}
+ />
+
+ {t(
+ "proxy.autoFailover.timeoutHint",
+ "熔断器打开后,等待多久后尝试恢复(建议: 30-120)",
+ )}
+
+
-
- {isExpanded && (
-
- {error && (
-
- {String(error)}
-
- )}
+ {/* 熔断器高级配置 */}
+
+
+ {t("proxy.autoFailover.circuitBreakerSettings", "熔断器高级设置")}
+
-
-
-
+
+
+
+ {t("proxy.autoFailover.successThreshold", "恢复成功阈值")}
+
+
+ setFormData({
+ ...formData,
+ successThreshold: parseInt(e.target.value) || 2,
+ })
+ }
+ disabled={!enabled}
+ />
+
+ {t(
+ "proxy.autoFailover.successThresholdHint",
+ "半开状态下成功多少次后关闭熔断器",
+ )}
+
+
+
+
+
+ {t("proxy.autoFailover.errorRate", "错误率阈值 (%)")}
+
+
+ setFormData({
+ ...formData,
+ errorRateThreshold: (parseInt(e.target.value) || 50) / 100,
+ })
+ }
+ disabled={!enabled}
+ />
+
+ {t(
+ "proxy.autoFailover.errorRateHint",
+ "错误率超过此值时打开熔断器",
+ )}
+
+
+
+
+
+ {t("proxy.autoFailover.minRequests", "最小请求数")}
+
+
+ setFormData({
+ ...formData,
+ minRequests: parseInt(e.target.value) || 10,
+ })
+ }
+ disabled={!enabled}
+ />
+
+ {t(
+ "proxy.autoFailover.minRequestsHint",
+ "计算错误率前的最小请求数",
+ )}
+
+
+
+
+
+ {/* 操作按钮 */}
+
+
+ {t("common.reset", "重置")}
+
+
+ {updateConfig.isPending ? (
+ <>
+
+ {t("common.saving", "保存中...")}
+ >
+ ) : (
+ <>
+
+ {t("common.save", "保存")}
+ >
+ )}
+
+
+
+ {/* 说明信息 */}
+
+
+ {t("proxy.autoFailover.explanationTitle", "工作原理")}
+
+
+
+ •{" "}
+
+ {t("proxy.autoFailover.failureThresholdLabel", "失败阈值")}
+
+ :
{t(
- "proxy.autoFailover.info",
- "当启用多个代理目标时,系统会按优先级顺序依次尝试。当某个供应商连续失败达到阈值时,熔断器会自动打开,跳过该供应商。",
+ "proxy.autoFailover.failureThresholdExplain",
+ "连续失败达到此次数时,熔断器打开,该供应商暂时不可用",
)}
-
-
-
- {/* 重试与超时配置 */}
-
-
- {t("proxy.autoFailover.retrySettings", "重试与超时设置")}
-
-
-
-
-
- {t("proxy.autoFailover.failureThreshold", "失败阈值")}
-
-
- setFormData({
- ...formData,
- failureThreshold: parseInt(e.target.value) || 5,
- })
- }
- disabled={!formData.enabled}
- />
-
- {t(
- "proxy.autoFailover.failureThresholdHint",
- "连续失败多少次后打开熔断器(建议: 3-10)",
- )}
-
-
-
-
-
- {t("proxy.autoFailover.timeout", "恢复等待时间(秒)")}
-
-
- setFormData({
- ...formData,
- timeoutSeconds: parseInt(e.target.value) || 60,
- })
- }
- disabled={!formData.enabled}
- />
-
- {t(
- "proxy.autoFailover.timeoutHint",
- "熔断器打开后,等待多久后尝试恢复(建议: 30-120)",
- )}
-
-
-
-
-
- {/* 熔断器高级配置 */}
-
-
- {t("proxy.autoFailover.circuitBreakerSettings", "熔断器高级设置")}
-
-
-
-
-
- {t("proxy.autoFailover.successThreshold", "恢复成功阈值")}
-
-
- setFormData({
- ...formData,
- successThreshold: parseInt(e.target.value) || 2,
- })
- }
- disabled={!formData.enabled}
- />
-
- {t(
- "proxy.autoFailover.successThresholdHint",
- "半开状态下成功多少次后关闭熔断器",
- )}
-
-
-
-
-
- {t("proxy.autoFailover.errorRate", "错误率阈值 (%)")}
-
-
- setFormData({
- ...formData,
- errorRateThreshold:
- (parseInt(e.target.value) || 50) / 100,
- })
- }
- disabled={!formData.enabled}
- />
-
- {t(
- "proxy.autoFailover.errorRateHint",
- "错误率超过此值时打开熔断器",
- )}
-
-
-
-
-
- {t("proxy.autoFailover.minRequests", "最小请求数")}
-
-
- setFormData({
- ...formData,
- minRequests: parseInt(e.target.value) || 10,
- })
- }
- disabled={!formData.enabled}
- />
-
- {t(
- "proxy.autoFailover.minRequestsHint",
- "计算错误率前的最小请求数",
- )}
-
-
-
-
-
- {/* 操作按钮 */}
-
-
- {t("common.reset", "重置")}
-
-
- {updateConfig.isPending ? (
- <>
-
- {t("common.saving", "保存中...")}
- >
- ) : (
- <>
-
- {t("common.save", "保存")}
- >
+
+
+ •{" "}
+
+ {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 ? "运行中" : "已停止"}
-
-
setShowSettings(true)}
- disabled={isPending}
- aria-label="打开代理设置"
- >
-
-
-
-
-
-
+
{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"
}`}
>