mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
refactor(proxy): remove is_proxy_target in favor of failover_queue
- Remove `is_proxy_target` field from Provider struct (Rust & TypeScript) - Remove related DAO methods: get_proxy_target_provider, set_proxy_target - Remove deprecated Tauri commands: get_proxy_targets, set_proxy_target - Add `is_available()` method to CircuitBreaker for availability checks without consuming HalfOpen probe permits (used in select_providers) - Keep `allow_request()` for actual request gating with permit tracking - Update stream_check to use failover_queue instead of is_proxy_target - Clean up commented-out reset circuit breaker button in ProviderActions - Remove unused useProxyTargets and useSetProxyTarget hooks
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
Play,
|
||||
TestTube2,
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -23,9 +22,6 @@ interface ProviderActionsProps {
|
||||
onTest?: () => void;
|
||||
onConfigureUsage: () => void;
|
||||
onDelete: () => void;
|
||||
onResetCircuitBreaker?: () => void;
|
||||
isProxyTarget?: boolean;
|
||||
consecutiveFailures?: number;
|
||||
}
|
||||
|
||||
export function ProviderActions({
|
||||
@@ -38,9 +34,6 @@ export function ProviderActions({
|
||||
onTest,
|
||||
onConfigureUsage,
|
||||
onDelete,
|
||||
onResetCircuitBreaker,
|
||||
isProxyTarget,
|
||||
consecutiveFailures = 0,
|
||||
}: ProviderActionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const iconButtonClass = "h-8 w-8 p-1";
|
||||
@@ -123,33 +116,6 @@ export function ProviderActions({
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* 重置熔断器按钮 - 代理目标启用时显示 */}
|
||||
{/* TODO: 暂时隐藏,后续根据故障转移功能启用 */}
|
||||
{/* {onResetCircuitBreaker && isProxyTarget && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={onResetCircuitBreaker}
|
||||
disabled={consecutiveFailures === 0}
|
||||
title={
|
||||
consecutiveFailures > 0
|
||||
? t("provider.resetCircuitBreaker", {
|
||||
defaultValue: "重置熔断器",
|
||||
})
|
||||
: t("provider.noFailures", {
|
||||
defaultValue: "当前无失败记录",
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
iconButtonClass,
|
||||
consecutiveFailures > 0 &&
|
||||
"hover:text-orange-500 dark:hover:text-orange-400",
|
||||
)}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
)} */}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
||||
@@ -12,11 +12,7 @@ import { ProviderActions } from "@/components/providers/ProviderActions";
|
||||
import { ProviderIcon } from "@/components/ProviderIcon";
|
||||
import UsageFooter from "@/components/UsageFooter";
|
||||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||||
import {
|
||||
useProviderHealth,
|
||||
useResetCircuitBreaker,
|
||||
} from "@/lib/query/failover";
|
||||
import { toast } from "sonner";
|
||||
import { useProviderHealth } from "@/lib/query/failover";
|
||||
import { useUsageQuery } from "@/lib/query/queries";
|
||||
|
||||
interface DragHandleProps {
|
||||
@@ -98,32 +94,6 @@ export function ProviderCard({
|
||||
// 获取供应商健康状态
|
||||
const { data: health } = useProviderHealth(provider.id, appId);
|
||||
|
||||
// 重置熔断器
|
||||
const resetCircuitBreaker = useResetCircuitBreaker();
|
||||
|
||||
const handleResetCircuitBreaker = async () => {
|
||||
try {
|
||||
await resetCircuitBreaker.mutateAsync({
|
||||
providerId: provider.id,
|
||||
appType: appId,
|
||||
});
|
||||
toast.success(
|
||||
t("provider.circuitBreakerReset", {
|
||||
defaultValue: "熔断器已重置",
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t("provider.circuitBreakerResetFailed", {
|
||||
defaultValue: "重置失败",
|
||||
}) +
|
||||
": " +
|
||||
String(error),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackUrlText = t("provider.notConfigured", {
|
||||
defaultValue: "未配置接口地址",
|
||||
});
|
||||
@@ -338,13 +308,6 @@ export function ProviderCard({
|
||||
onTest={onTest ? () => onTest(provider) : undefined}
|
||||
onConfigureUsage={() => onConfigureUsage(provider)}
|
||||
onDelete={() => onDelete(provider)}
|
||||
onResetCircuitBreaker={
|
||||
isProxyRunning && provider.isProxyTarget
|
||||
? handleResetCircuitBreaker
|
||||
: undefined
|
||||
}
|
||||
isProxyTarget={provider.isProxyTarget}
|
||||
consecutiveFailures={health?.consecutive_failures ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ export function AutoFailoverConfigPanel({
|
||||
<AlertDescription className="text-sm">
|
||||
{t(
|
||||
"proxy.autoFailover.info",
|
||||
"当启用多个代理目标时,系统会按优先级顺序依次尝试。当某个供应商连续失败达到阈值时,熔断器会自动打开,跳过该供应商。",
|
||||
"当故障转移队列中配置了多个供应商时,系统会在请求失败时按优先级顺序依次尝试。当某个供应商连续失败达到阈值时,熔断器会打开并在一段时间内跳过该供应商。",
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { ProxySettingsDialog } from "./ProxySettingsDialog";
|
||||
import { toast } from "sonner";
|
||||
import { useProxyTargets } from "@/lib/query/failover";
|
||||
import { useFailoverQueue } from "@/lib/query/failover";
|
||||
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
|
||||
import { useProviderHealth } from "@/lib/query/failover";
|
||||
import type { ProxyStatus } from "@/types/proxy";
|
||||
@@ -13,10 +13,11 @@ export function ProxyPanel() {
|
||||
const { status, isRunning } = useProxyStatus();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
// 获取所有三个应用类型的代理目标列表
|
||||
const { data: claudeTargets = [] } = useProxyTargets("claude");
|
||||
const { data: codexTargets = [] } = useProxyTargets("codex");
|
||||
const { data: geminiTargets = [] } = useProxyTargets("gemini");
|
||||
// 获取所有三个应用类型的故障转移队列(不包含当前供应商)
|
||||
// 当前供应商始终优先,队列仅用于失败后的备用顺序
|
||||
const { data: claudeQueue = [] } = useFailoverQueue("claude");
|
||||
const { data: codexQueue = [] } = useFailoverQueue("codex");
|
||||
const { data: geminiQueue = [] } = useFailoverQueue("gemini");
|
||||
|
||||
const formatUptime = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
@@ -95,9 +96,9 @@ export function ProxyPanel() {
|
||||
</div>
|
||||
|
||||
{/* 供应商队列 - 按应用类型分组展示 */}
|
||||
{(claudeTargets.length > 0 ||
|
||||
codexTargets.length > 0 ||
|
||||
geminiTargets.length > 0) && (
|
||||
{(claudeQueue.length > 0 ||
|
||||
codexQueue.length > 0 ||
|
||||
geminiQueue.length > 0) && (
|
||||
<div className="pt-3 border-t border-border space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ListOrdered className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
@@ -107,31 +108,49 @@ export function ProxyPanel() {
|
||||
</div>
|
||||
|
||||
{/* Claude 队列 */}
|
||||
{claudeTargets.length > 0 && (
|
||||
{claudeQueue.length > 0 && (
|
||||
<ProviderQueueGroup
|
||||
appType="claude"
|
||||
appLabel="Claude"
|
||||
targets={claudeTargets}
|
||||
targets={claudeQueue
|
||||
.filter((item) => item.enabled)
|
||||
.sort((a, b) => a.queueOrder - b.queueOrder)
|
||||
.map((item) => ({
|
||||
id: item.providerId,
|
||||
name: item.providerName,
|
||||
}))}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Codex 队列 */}
|
||||
{codexTargets.length > 0 && (
|
||||
{codexQueue.length > 0 && (
|
||||
<ProviderQueueGroup
|
||||
appType="codex"
|
||||
appLabel="Codex"
|
||||
targets={codexTargets}
|
||||
targets={codexQueue
|
||||
.filter((item) => item.enabled)
|
||||
.sort((a, b) => a.queueOrder - b.queueOrder)
|
||||
.map((item) => ({
|
||||
id: item.providerId,
|
||||
name: item.providerName,
|
||||
}))}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Gemini 队列 */}
|
||||
{geminiTargets.length > 0 && (
|
||||
{geminiQueue.length > 0 && (
|
||||
<ProviderQueueGroup
|
||||
appType="gemini"
|
||||
appLabel="Gemini"
|
||||
targets={geminiTargets}
|
||||
targets={geminiQueue
|
||||
.filter((item) => item.enabled)
|
||||
.sort((a, b) => a.queueOrder - b.queueOrder)
|
||||
.map((item) => ({
|
||||
id: item.providerId,
|
||||
name: item.providerName,
|
||||
}))}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
Coins,
|
||||
Database,
|
||||
Server,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -279,10 +281,10 @@ export function SettingsPage({
|
||||
|
||||
<AccordionItem
|
||||
value="proxy"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
className="rounded-xl glass-card overflow-hidden [&[data-state=open]>.accordion-header]:bg-muted/50"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex flex-1 items-center justify-between pr-4">
|
||||
<AccordionPrimitive.Header className="accordion-header flex items-center justify-between px-6 py-4 hover:bg-muted/50">
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center justify-between hover:no-underline [&[data-state=open]>svg]:rotate-180">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-green-500" />
|
||||
<div className="text-left">
|
||||
@@ -294,27 +296,26 @@ export function SettingsPage({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
|
||||
<div className="flex items-center gap-4 pl-4">
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5 h-6"
|
||||
>
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5 h-6"
|
||||
>
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
/>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="px-6 pb-6 pt-0 border-t border-border/50">
|
||||
<ProxyPanel />
|
||||
</AccordionContent>
|
||||
@@ -344,10 +345,10 @@ export function SettingsPage({
|
||||
|
||||
<AccordionItem
|
||||
value="failover"
|
||||
className="rounded-xl glass-card overflow-hidden"
|
||||
className="rounded-xl glass-card overflow-hidden [&[data-state=open]>.accordion-header]:bg-muted/50"
|
||||
>
|
||||
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
|
||||
<div className="flex flex-1 items-center justify-between pr-4">
|
||||
<AccordionPrimitive.Header className="accordion-header flex items-center justify-between px-6 py-4 hover:bg-muted/50">
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center justify-between hover:no-underline [&[data-state=open]>svg]:rotate-180">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-orange-500" />
|
||||
<div className="text-left">
|
||||
@@ -359,20 +360,16 @@ export function SettingsPage({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Removed status text as requested */}
|
||||
<Switch
|
||||
checked={failoverEnabled}
|
||||
onCheckedChange={setFailoverEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
|
||||
<div className="flex items-center gap-2 pl-4">
|
||||
<Switch
|
||||
checked={failoverEnabled}
|
||||
onCheckedChange={setFailoverEnabled}
|
||||
/>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
|
||||
<div className="space-y-6">
|
||||
{/* 故障转移队列管理 */}
|
||||
|
||||
@@ -864,7 +864,7 @@
|
||||
"toggleFailed": "Failed to update status"
|
||||
},
|
||||
"autoFailover": {
|
||||
"info": "When multiple proxy targets are enabled, the system will try them in priority order. When a provider fails consecutively reaching the threshold, the circuit breaker will open and skip that provider.",
|
||||
"info": "When the failover queue has multiple providers, the system will try them in priority order when requests fail. When a provider reaches the consecutive failure threshold, the circuit breaker will open and skip it temporarily.",
|
||||
"configSaved": "Auto failover config saved",
|
||||
"configSaveFailed": "Failed to save",
|
||||
"retrySettings": "Retry & Timeout Settings",
|
||||
|
||||
@@ -864,7 +864,7 @@
|
||||
"toggleFailed": "状态更新失败"
|
||||
},
|
||||
"autoFailover": {
|
||||
"info": "当启用多个代理目标时,系统会按优先级顺序依次尝试。当某个供应商连续失败达到阈值时,熔断器会自动打开,跳过该供应商。",
|
||||
"info": "当故障转移队列中配置了多个供应商时,系统会在请求失败时按优先级顺序依次尝试。当某个供应商连续失败达到阈值时,熔断器会打开并在一段时间内跳过该供应商。",
|
||||
"configSaved": "自动故障转移配置已保存",
|
||||
"configSaveFailed": "保存失败",
|
||||
"retrySettings": "重试与超时设置",
|
||||
|
||||
@@ -18,26 +18,9 @@ export interface Provider {
|
||||
meta?: unknown;
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
isProxyTarget?: boolean;
|
||||
}
|
||||
|
||||
export const failoverApi = {
|
||||
// ========== 旧版代理目标 API(保留向后兼容)==========
|
||||
|
||||
// 获取代理目标列表
|
||||
async getProxyTargets(appType: string): Promise<Provider[]> {
|
||||
return invoke("get_proxy_targets", { appType });
|
||||
},
|
||||
|
||||
// 设置代理目标
|
||||
async setProxyTarget(
|
||||
providerId: string,
|
||||
appType: string,
|
||||
enabled: boolean,
|
||||
): Promise<void> {
|
||||
return invoke("set_proxy_target", { providerId, appType, enabled });
|
||||
},
|
||||
|
||||
// ========== 熔断器 API ==========
|
||||
|
||||
// 获取供应商健康状态
|
||||
|
||||
@@ -38,10 +38,6 @@ export const providersApi = {
|
||||
return await invoke("switch_provider", { id, app: appId });
|
||||
},
|
||||
|
||||
async setProxyTarget(id: string, appId: AppId): Promise<boolean> {
|
||||
return await invoke("set_proxy_target_provider", { id, app: appId });
|
||||
},
|
||||
|
||||
async importDefault(appId: AppId): Promise<boolean> {
|
||||
return await invoke("import_default_config", { app: appId });
|
||||
},
|
||||
|
||||
@@ -1,50 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { failoverApi } from "@/lib/api/failover";
|
||||
|
||||
// ========== 旧版代理目标 Hooks(保留向后兼容)==========
|
||||
|
||||
/**
|
||||
* 获取代理目标列表
|
||||
*/
|
||||
export function useProxyTargets(appType: string) {
|
||||
return useQuery({
|
||||
queryKey: ["proxyTargets", appType],
|
||||
queryFn: () => failoverApi.getProxyTargets(appType),
|
||||
enabled: !!appType,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置代理目标
|
||||
*/
|
||||
export function useSetProxyTarget() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
providerId,
|
||||
appType,
|
||||
enabled,
|
||||
}: {
|
||||
providerId: string;
|
||||
appType: string;
|
||||
enabled: boolean;
|
||||
}) => failoverApi.setProxyTarget(providerId, appType, enabled),
|
||||
onSuccess: (_, variables) => {
|
||||
// 刷新代理目标列表
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["proxyTargets", variables.appType],
|
||||
});
|
||||
// 刷新供应商列表
|
||||
queryClient.invalidateQueries({ queryKey: ["providers"] });
|
||||
// 刷新健康状态
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["providerHealth", variables.providerId, variables.appType],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 熔断器 Hooks ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -180,35 +180,6 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useSetProxyTargetMutation = (appId: AppId) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (providerId: string) => {
|
||||
return await providersApi.setProxyTarget(providerId, appId);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||
toast.success(
|
||||
t("notifications.proxyTargetSet", {
|
||||
defaultValue: "已设置代理目标",
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||
toast.error(
|
||||
t("notifications.setProxyTargetFailed", {
|
||||
defaultValue: "设置代理目标失败: {{error}}",
|
||||
error: detail,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useSaveSettingsMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ export interface Provider {
|
||||
// 图标配置
|
||||
icon?: string; // 图标名称(如 "openai", "anthropic")
|
||||
iconColor?: string; // 图标颜色(Hex 格式,如 "#00A67E")
|
||||
// 新增:是否为代理目标
|
||||
isProxyTarget?: boolean;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
|
||||
Reference in New Issue
Block a user