mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 14:35:22 +08:00
b642ef0633
- Forwarder buffers non-streaming bodies and primes streaming first chunk before signaling success, so body timeouts and SSE first-chunk failures route through the circuit breaker instead of being recorded as success on response-header arrival - Atomic enable-failover: switch to P1 before persisting the flag, and roll back auto-added queue entries when the switch is rejected (e.g. official providers) - Hot-reload circuit breaker config on per-app proxy config change instead of waiting for a proxy restart - FailoverToggle / FailoverQueueManager / AutoFailoverConfigPanel require proxy takeover for the active app; the backend command also rejects enabling when takeover is off - ProviderHealthBadge consumes the backend is_healthy flag instead of hardcoding the 5-failure threshold Cleanup: - impl From<&AppProxyConfig> for CircuitBreakerConfig and use it from the command layer - Collapse three identical TabsContent blocks into a single map
86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
/**
|
|
* 故障转移切换开关组件
|
|
*
|
|
* 放置在主界面头部,用于一键启用/关闭自动故障转移
|
|
*/
|
|
|
|
import { Shuffle, Loader2 } from "lucide-react";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
useAutoFailoverEnabled,
|
|
useSetAutoFailoverEnabled,
|
|
} from "@/lib/query/failover";
|
|
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
|
import { cn } from "@/lib/utils";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { AppId } from "@/lib/api";
|
|
|
|
interface FailoverToggleProps {
|
|
className?: string;
|
|
activeApp: AppId;
|
|
}
|
|
|
|
export function FailoverToggle({ className, activeApp }: FailoverToggleProps) {
|
|
const { t } = useTranslation();
|
|
const { data: isEnabled = false, isLoading } =
|
|
useAutoFailoverEnabled(activeApp);
|
|
const setEnabled = useSetAutoFailoverEnabled();
|
|
const { takeoverStatus } = useProxyStatus();
|
|
const takeoverEnabled = takeoverStatus?.[activeApp] ?? false;
|
|
|
|
const handleToggle = (checked: boolean) => {
|
|
if (checked && !takeoverEnabled) return;
|
|
setEnabled.mutate({ appType: activeApp, enabled: checked });
|
|
};
|
|
|
|
const appLabel =
|
|
activeApp === "claude"
|
|
? "Claude"
|
|
: activeApp === "codex"
|
|
? "Codex"
|
|
: "Gemini";
|
|
|
|
const tooltipText = !takeoverEnabled
|
|
? t("failover.tooltip.takeoverRequired", {
|
|
app: appLabel,
|
|
defaultValue: `请先接管 ${appLabel},再启用故障转移`,
|
|
})
|
|
: isEnabled
|
|
? t("failover.tooltip.enabled", {
|
|
app: appLabel,
|
|
defaultValue: `${appLabel} 故障转移已启用\n按队列优先级(P1→P2→...)选择供应商`,
|
|
})
|
|
: t("failover.tooltip.disabled", {
|
|
app: appLabel,
|
|
defaultValue: `启用 ${appLabel} 故障转移\n将立即切换到队列 P1,并在失败时自动切换到下一个`,
|
|
});
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-1 px-1.5 h-8 rounded-lg bg-muted/50 transition-all",
|
|
className,
|
|
)}
|
|
title={tooltipText}
|
|
>
|
|
{setEnabled.isPending || isLoading ? (
|
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
) : (
|
|
<Shuffle
|
|
className={cn(
|
|
"h-4 w-4 transition-colors",
|
|
isEnabled
|
|
? "text-emerald-500 animate-pulse"
|
|
: "text-muted-foreground",
|
|
)}
|
|
/>
|
|
)}
|
|
<Switch
|
|
checked={isEnabled}
|
|
onCheckedChange={handleToggle}
|
|
disabled={setEnabled.isPending || isLoading || !takeoverEnabled}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|