From e093164b8dd47cb724fb750cfbc530f14a3b69ba Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 8 Dec 2025 11:09:19 +0800 Subject: [PATCH] feat(frontend): add failover API layer and TanStack Query hooks Add frontend data layer for failover management: - Add failover.ts API: Tauri invoke wrappers for all failover commands - Add failover.ts query hooks: TanStack Query mutations and queries - useProxyTargets, useProviderHealth queries - useSetProxyTarget, useResetCircuitBreaker mutations - useCircuitBreakerConfig query and mutation - Update queries.ts with provider health query key - Update mutations.ts to invalidate health on provider changes - Add CircuitBreakerConfig and ProviderHealth types --- src/lib/api/failover.ts | 73 +++++++++++++++++++++++ src/lib/query/failover.ts | 116 +++++++++++++++++++++++++++++++++++++ src/lib/query/mutations.ts | 28 +++++---- src/lib/query/queries.ts | 10 ++++ src/types/proxy.ts | 33 +++++++++++ 5 files changed, 248 insertions(+), 12 deletions(-) create mode 100644 src/lib/api/failover.ts create mode 100644 src/lib/query/failover.ts diff --git a/src/lib/api/failover.ts b/src/lib/api/failover.ts new file mode 100644 index 000000000..aed49451a --- /dev/null +++ b/src/lib/api/failover.ts @@ -0,0 +1,73 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { + ProviderHealth, + CircuitBreakerConfig, + CircuitBreakerStats, +} from "@/types/proxy"; + +export interface Provider { + id: string; + name: string; + settingsConfig: unknown; + websiteUrl?: string; + category?: string; + createdAt?: number; + sortIndex?: number; + notes?: string; + meta?: unknown; + icon?: string; + iconColor?: string; + isProxyTarget?: boolean; +} + +export const failoverApi = { + // 获取代理目标列表 + async getProxyTargets(appType: string): Promise { + return invoke("get_proxy_targets", { appType }); + }, + + // 设置代理目标 + async setProxyTarget( + providerId: string, + appType: string, + enabled: boolean, + ): Promise { + return invoke("set_proxy_target", { providerId, appType, enabled }); + }, + + // 获取供应商健康状态 + async getProviderHealth( + providerId: string, + appType: string, + ): Promise { + return invoke("get_provider_health", { providerId, appType }); + }, + + // 重置熔断器 + async resetCircuitBreaker( + providerId: string, + appType: string, + ): Promise { + return invoke("reset_circuit_breaker", { providerId, appType }); + }, + + // 获取熔断器配置 + async getCircuitBreakerConfig(): Promise { + return invoke("get_circuit_breaker_config"); + }, + + // 更新熔断器配置 + async updateCircuitBreakerConfig( + config: CircuitBreakerConfig, + ): Promise { + return invoke("update_circuit_breaker_config", { config }); + }, + + // 获取熔断器统计信息 + async getCircuitBreakerStats( + providerId: string, + appType: string, + ): Promise { + return invoke("get_circuit_breaker_stats", { providerId, appType }); + }, +}; diff --git a/src/lib/query/failover.ts b/src/lib/query/failover.ts new file mode 100644 index 000000000..7c9aea6c3 --- /dev/null +++ b/src/lib/query/failover.ts @@ -0,0 +1,116 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { failoverApi } from "@/lib/api/failover"; + +/** + * 获取代理目标列表 + */ +export function useProxyTargets(appType: string) { + return useQuery({ + queryKey: ["proxyTargets", appType], + queryFn: () => failoverApi.getProxyTargets(appType), + enabled: !!appType, + }); +} + +/** + * 获取供应商健康状态 + */ +export function useProviderHealth(providerId: string, appType: string) { + return useQuery({ + queryKey: ["providerHealth", providerId, appType], + queryFn: () => failoverApi.getProviderHealth(providerId, appType), + enabled: !!providerId && !!appType, + refetchInterval: 5000, // 每 5 秒刷新一次 + retry: false, + }); +} + +/** + * 设置代理目标 + */ +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], + }); + }, + }); +} + +/** + * 重置熔断器 + */ +export function useResetCircuitBreaker() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + providerId, + appType, + }: { + providerId: string; + appType: string; + }) => failoverApi.resetCircuitBreaker(providerId, appType), + onSuccess: (_, variables) => { + // 刷新健康状态 + queryClient.invalidateQueries({ + queryKey: ["providerHealth", variables.providerId, variables.appType], + }); + }, + }); +} + +/** + * 获取熔断器配置 + */ +export function useCircuitBreakerConfig() { + return useQuery({ + queryKey: ["circuitBreakerConfig"], + queryFn: () => failoverApi.getCircuitBreakerConfig(), + }); +} + +/** + * 更新熔断器配置 + */ +export function useUpdateCircuitBreakerConfig() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: failoverApi.updateCircuitBreakerConfig, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["circuitBreakerConfig"] }); + }, + }); +} + +/** + * 获取熔断器统计信息 + */ +export function useCircuitBreakerStats(providerId: string, appType: string) { + return useQuery({ + queryKey: ["circuitBreakerStats", providerId, appType], + queryFn: () => failoverApi.getCircuitBreakerStats(providerId, appType), + enabled: !!providerId && !!appType, + refetchInterval: 5000, // 每 5 秒刷新一次 + }); +} diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index 8cdddacf9..00f0b54ea 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -36,9 +36,10 @@ export const useAddProviderMutation = (appId: AppId) => { toast.success( t("notifications.providerAdded", { defaultValue: "供应商已添加", - }), { - closeButton: true - } + }), + { + closeButton: true, + }, ); }, onError: (error: Error) => { @@ -66,9 +67,10 @@ export const useUpdateProviderMutation = (appId: AppId) => { toast.success( t("notifications.updateSuccess", { defaultValue: "供应商更新成功", - }), { - closeButton: true - } + }), + { + closeButton: true, + }, ); }, onError: (error: Error) => { @@ -106,9 +108,10 @@ export const useDeleteProviderMutation = (appId: AppId) => { toast.success( t("notifications.deleteSuccess", { defaultValue: "供应商已删除", - }), { - closeButton: true - } + }), + { + closeButton: true, + }, ); }, onError: (error: Error) => { @@ -147,9 +150,10 @@ export const useSwitchProviderMutation = (appId: AppId) => { t("notifications.switchSuccess", { defaultValue: "切换供应商成功", appName: t(`apps.${appId}`, { defaultValue: appId }), - }), { - closeButton: true - } + }), + { + closeButton: true, + }, ); }, onError: (error: Error) => { diff --git a/src/lib/query/queries.ts b/src/lib/query/queries.ts index e1beb6ae3..47a404166 100644 --- a/src/lib/query/queries.ts +++ b/src/lib/query/queries.ts @@ -34,12 +34,22 @@ export interface ProvidersQueryData { currentProviderId: string; } +export interface UseProvidersQueryOptions { + isProxyRunning?: boolean; // 代理服务是否运行中 +} + export const useProvidersQuery = ( appId: AppId, + options?: UseProvidersQueryOptions, ): UseQueryResult => { + const { isProxyRunning = false } = options || {}; + return useQuery({ queryKey: ["providers", appId], placeholderData: keepPreviousData, + // 当代理服务运行时,每 10 秒刷新一次供应商列表 + // 这样可以自动反映后端熔断器自动禁用代理目标的变更 + refetchInterval: isProxyRunning ? 10000 : false, queryFn: async () => { let providers: Record = {}; let currentProviderId = ""; diff --git a/src/types/proxy.ts b/src/types/proxy.ts index 83762c413..8d4d4dc54 100644 --- a/src/types/proxy.ts +++ b/src/types/proxy.ts @@ -48,6 +48,39 @@ export interface ProviderHealth { updated_at: string; } +// 熔断器相关类型 +export interface CircuitBreakerConfig { + failureThreshold: number; + successThreshold: number; + timeoutSeconds: number; + errorRateThreshold: number; + minRequests: number; +} + +export type CircuitState = "closed" | "open" | "half_open"; + +export interface CircuitBreakerStats { + state: CircuitState; + consecutiveFailures: number; + consecutiveSuccesses: number; + totalRequests: number; + failedRequests: number; +} + +// 供应商健康状态枚举 +export enum ProviderHealthStatus { + Healthy = "healthy", + Degraded = "degraded", + Failed = "failed", + Unknown = "unknown", +} + +// 扩展 ProviderHealth 以包含前端计算的状态 +export interface ProviderHealthWithStatus extends ProviderHealth { + status: ProviderHealthStatus; + circuitState?: CircuitState; +} + export interface ProxyUsageRecord { provider_id: string; app_type: string;