Compare commits

...

1 Commits

Author SHA1 Message Date
SaladDay 483256ad92 refactor: remove redundant proxy query paths 2026-07-30 15:08:42 +00:00
9 changed files with 89 additions and 273 deletions
+5 -3
View File
@@ -29,7 +29,7 @@ import {
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { Provider, VisibleApps } from "@/types";
import type { EnvConflict } from "@/types/env";
import { useProvidersQuery, useSettingsQuery } from "@/lib/query";
import { proxyKeys, useProvidersQuery, useSettingsQuery } from "@/lib/query";
import {
providersApi,
settingsApi,
@@ -389,8 +389,10 @@ function App() {
await queryClient.invalidateQueries({ queryKey: ["profiles"] });
await queryClient.invalidateQueries({ queryKey: ["mcp", "all"] });
await queryClient.invalidateQueries({ queryKey: ["skills"] });
await queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
await queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
await queryClient.invalidateQueries({
queryKey: proxyKeys.takeoverStatus,
});
await queryClient.invalidateQueries({ queryKey: proxyKeys.status });
await queryClient.invalidateQueries({
queryKey: ["providers", "claude-desktop"],
});
+3 -2
View File
@@ -15,12 +15,12 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { ToggleRow } from "@/components/ui/toggle-row";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { toast } from "sonner";
import { useFailoverQueue } from "@/lib/query/failover";
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
import { useProviderHealth } from "@/lib/query/failover";
import {
useProxyStatusQuery,
useProxyTakeoverStatus,
useSetProxyTakeoverForApp,
useGlobalProxyConfig,
@@ -45,7 +45,8 @@ export function ProxyPanel({
isProxyPending,
}: ProxyPanelProps) {
const { t } = useTranslation();
const { status, isRunning } = useProxyStatus();
const { data: status } = useProxyStatusQuery();
const isRunning = status?.running ?? false;
// 获取应用接管状态
const { data: takeoverStatus } = useProxyTakeoverStatus();
+19 -88
View File
@@ -2,15 +2,15 @@
* 代理服务状态管理 Hook
*/
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import type {
ProxyStatus,
ProxyServerInfo,
ProxyTakeoverStatus,
} from "@/types/proxy";
import { proxyApi } from "@/lib/api/proxy";
import {
proxyKeys,
useProxyStatusQuery,
useProxyTakeoverStatus,
} from "@/lib/query/proxy";
import { extractErrorMessage } from "@/utils/errorUtils";
/**
@@ -21,25 +21,14 @@ export function useProxyStatus() {
const { t } = useTranslation();
// 查询状态(自动轮询)
const { data: status, isLoading } = useQuery({
queryKey: ["proxyStatus"],
queryFn: () => invoke<ProxyStatus>("get_proxy_status"),
// 仅在服务运行时轮询
refetchInterval: (query) => (query.state.data?.running ? 2000 : false),
// 保持之前的数据,避免闪烁
placeholderData: (previousData) => previousData,
});
const { data: status } = useProxyStatusQuery();
// 查询各应用接管状态
const { data: takeoverStatus } = useQuery({
queryKey: ["proxyTakeoverStatus"],
queryFn: () => invoke<ProxyTakeoverStatus>("get_proxy_takeover_status"),
placeholderData: (previousData) => previousData,
});
const { data: takeoverStatus } = useProxyTakeoverStatus(false);
// 启动服务器(总开关:仅启动服务,不接管)
const startProxyServerMutation = useMutation({
mutationFn: () => invoke<ProxyServerInfo>("start_proxy_server"),
mutationFn: () => proxyApi.startProxyServer(),
onSuccess: (info) => {
toast.success(
t("proxy.server.started", {
@@ -49,7 +38,7 @@ export function useProxyStatus() {
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
},
onError: (error: Error) => {
const detail =
@@ -66,7 +55,7 @@ export function useProxyStatus() {
// 停止服务器(仅停止服务,不改写/恢复其它应用接管状态)
const stopProxyServerMutation = useMutation({
mutationFn: () => invoke("stop_proxy_server"),
mutationFn: () => proxyApi.stopProxyServer(),
onSuccess: () => {
toast.success(
t("proxy.server.stopped", {
@@ -74,7 +63,7 @@ export function useProxyStatus() {
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
},
onError: (error: Error) => {
const detail =
@@ -91,7 +80,7 @@ export function useProxyStatus() {
// 停止服务器(总开关关闭:强制恢复所有已接管的 Live 配置)
const stopWithRestoreMutation = useMutation({
mutationFn: () => invoke("stop_proxy_with_restore"),
mutationFn: () => proxyApi.stopProxyWithRestore(),
onSuccess: () => {
toast.success(
t("proxy.stoppedWithRestore", {
@@ -99,8 +88,8 @@ export function useProxyStatus() {
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
queryClient.invalidateQueries({ queryKey: proxyKeys.takeoverStatus });
// 彻底删除所有供应商健康状态缓存(后端已清空数据库记录)
queryClient.removeQueries({ queryKey: ["providerHealth"] });
// 彻底删除所有熔断器统计缓存(代理停止后熔断器状态已重置)
@@ -123,7 +112,7 @@ export function useProxyStatus() {
// 按应用开启/关闭接管
const setTakeoverForAppMutation = useMutation({
mutationFn: ({ appType, enabled }: { appType: string; enabled: boolean }) =>
invoke("set_proxy_takeover_for_app", { appType, enabled }),
proxyApi.setProxyTakeoverForApp(appType, enabled),
onSuccess: (_data, variables) => {
const appLabel =
variables.appType === "claude"
@@ -149,8 +138,8 @@ export function useProxyStatus() {
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
queryClient.invalidateQueries({ queryKey: proxyKeys.takeoverStatus });
},
onError: (error: Error) => {
const detail =
@@ -165,60 +154,10 @@ export function useProxyStatus() {
},
});
// 代理模式切换供应商(热切换)
const switchProxyProviderMutation = useMutation({
mutationFn: ({
appType,
providerId,
}: {
appType: string;
providerId: string;
}) => invoke("switch_proxy_provider", { appType, providerId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
},
onError: (error: Error) => {
const detail =
extractErrorMessage(error) ||
t("common.unknown", { defaultValue: "未知错误" });
toast.error(
t("proxy.switchFailed", {
error: detail,
defaultValue: `切换失败: ${detail}`,
}),
);
},
});
// 检查是否运行中
const checkRunning = async () => {
try {
return await invoke<boolean>("is_proxy_running");
} catch {
return false;
}
};
// 检查接管状态
const checkTakeoverActive = async () => {
try {
return await invoke<boolean>("is_live_takeover_active");
} catch {
return false;
}
};
return {
status,
isLoading,
isRunning: status?.running || false,
takeoverStatus,
isTakeoverActive:
takeoverStatus?.claude ||
takeoverStatus?.codex ||
takeoverStatus?.gemini ||
takeoverStatus?.grokbuild ||
false,
// 启动/停止(总开关)
startProxyServer: startProxyServerMutation.mutateAsync,
@@ -228,17 +167,9 @@ export function useProxyStatus() {
// 按应用接管开关
setTakeoverForApp: setTakeoverForAppMutation.mutateAsync,
// 代理模式下切换供应商
switchProxyProvider: switchProxyProviderMutation.mutateAsync,
// 状态检查
checkRunning,
checkTakeoverActive,
// 加载状态
isStarting: startProxyServerMutation.isPending,
isStoppingServer: stopProxyServerMutation.isPending,
isStopping: stopWithRestoreMutation.isPending,
isPending:
startProxyServerMutation.isPending ||
stopProxyServerMutation.isPending ||
+5 -31
View File
@@ -1,6 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import type {
ProxyConfig,
ProxyStatus,
ProxyServerInfo,
ProxyTakeoverStatus,
@@ -16,6 +15,11 @@ export const proxyApi = {
return invoke("start_proxy_server");
},
// 停止代理服务器(不恢复已接管配置)
async stopProxyServer(): Promise<void> {
return invoke("stop_proxy_server");
},
// 停止代理服务器并恢复配置
async stopProxyWithRestore(): Promise<void> {
return invoke("stop_proxy_with_restore");
@@ -26,24 +30,6 @@ export const proxyApi = {
return invoke("get_proxy_status");
},
// 检查代理服务器是否正在运行
async isProxyRunning(): Promise<boolean> {
return invoke("is_proxy_running");
},
// 检查是否处于接管模式
async isLiveTakeoverActive(): Promise<boolean> {
return invoke("is_live_takeover_active");
},
// 代理模式下切换供应商
async switchProxyProvider(
appType: string,
providerId: string,
): Promise<void> {
return invoke("switch_proxy_provider", { appType, providerId });
},
// ========== 接管状态 API ==========
// 获取各应用接管状态
@@ -59,18 +45,6 @@ export const proxyApi = {
return invoke("set_proxy_takeover_for_app", { appType, enabled });
},
// ========== Legacy 代理配置 API (兼容) ==========
// 获取代理配置(旧版 v2 兼容接口)
async getProxyConfig(): Promise<ProxyConfig> {
return invoke("get_proxy_config");
},
// 更新代理配置(旧版 v2 兼容接口)
async updateProxyConfig(config: ProxyConfig): Promise<void> {
return invoke("update_proxy_config", { config });
},
// ========== v3+ 全局/应用级配置 API ==========
// 获取全局代理配置
+3 -2
View File
@@ -3,6 +3,7 @@ import { failoverApi } from "@/lib/api/failover";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { extractErrorMessage } from "@/utils/errorUtils";
import { proxyKeys } from "@/lib/query/proxy";
// ========== 熔断器 Hooks ==========
@@ -47,7 +48,7 @@ export function useResetCircuitBreaker() {
});
// 刷新代理状态(更新 active_targets
queryClient.invalidateQueries({
queryKey: ["proxyStatus"],
queryKey: proxyKeys.status,
});
},
});
@@ -283,7 +284,7 @@ export function useSetAutoFailoverEnabled() {
queryKey: ["providers", variables.appType],
});
queryClient.invalidateQueries({
queryKey: ["proxyStatus"],
queryKey: proxyKeys.status,
});
},
});
+2 -1
View File
@@ -9,6 +9,7 @@ import { extractErrorMessage } from "@/utils/errorUtils";
import { generateUUID } from "@/utils/uuid";
import { openclawKeys } from "@/hooks/useOpenClaw";
import { invalidateHermesProviderCaches } from "@/hooks/useHermes";
import { proxyKeys } from "@/lib/query/proxy";
import { usageKeys } from "@/lib/query/usage";
import {
CODEX_OFFICIAL_PROVIDER_ID,
@@ -286,7 +287,7 @@ export const useSwitchProviderMutation = (appId: AppId) => {
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
if (appId === "claude-desktop") {
await queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
await queryClient.invalidateQueries({ queryKey: proxyKeys.status });
await queryClient.invalidateQueries({
queryKey: ["claudeDesktopStatus"],
});
+34 -136
View File
@@ -2,90 +2,54 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { proxyApi } from "@/lib/api/proxy";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import type { GlobalProxyConfig, AppProxyConfig } from "@/types/proxy";
import type {
GlobalProxyConfig,
AppProxyConfig,
ProxyTakeoverStatus,
} from "@/types/proxy";
export const proxyKeys = {
status: ["proxyStatus"] as const,
takeoverStatus: ["proxyTakeoverStatus"] as const,
globalConfig: ["globalProxyConfig"] as const,
appConfig: (appType: string) => ["appProxyConfig", appType] as const,
};
// ========== 代理服务器状态 Hooks ==========
/**
* 获取代理服务器状态
*/
export function useProxyStatus() {
export function useProxyStatusQuery() {
return useQuery({
queryKey: ["proxyStatus"],
queryKey: proxyKeys.status,
queryFn: () => proxyApi.getProxyStatus(),
refetchInterval: 5000, // 每 5 秒刷新一次
});
}
/**
* 检查代理服务器是否运行
*/
export function useIsProxyRunning() {
return useQuery({
queryKey: ["proxyRunning"],
queryFn: () => proxyApi.isProxyRunning(),
refetchInterval: 2000,
});
}
/**
* 检查是否处于接管模式
*/
export function useIsLiveTakeoverActive() {
return useQuery({
queryKey: ["liveTakeoverActive"],
queryFn: () => proxyApi.isLiveTakeoverActive(),
refetchInterval: 2000,
// 仅在服务运行时轮询
refetchInterval: (query) => (query.state.data?.running ? 2000 : false),
// 保持之前的数据,避免闪烁
placeholderData: (previousData) => previousData,
});
}
/**
* 获取各应用接管状态
*/
export function useProxyTakeoverStatus() {
export function useProxyTakeoverStatus(poll = true) {
return useQuery({
queryKey: ["proxyTakeoverStatus"],
queryKey: proxyKeys.takeoverStatus,
queryFn: () => proxyApi.getProxyTakeoverStatus(),
refetchInterval: 2000,
refetchInterval: poll ? 2000 : false,
...(poll
? {}
: {
placeholderData: (previousData: ProxyTakeoverStatus | undefined) =>
previousData,
}),
});
}
// ========== 代理服务器控制 Hooks ==========
/**
* 启动代理服务器
*/
export function useStartProxyServer() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => proxyApi.startProxyServer(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyRunning"] });
queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
},
});
}
/**
* 停止代理服务器
*/
export function useStopProxyServer() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => proxyApi.stopProxyWithRestore(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyRunning"] });
queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
},
});
}
/**
* 设置应用接管状态
*/
@@ -96,75 +60,11 @@ export function useSetProxyTakeoverForApp() {
mutationFn: ({ appType, enabled }: { appType: string; enabled: boolean }) =>
proxyApi.setProxyTakeoverForApp(appType, enabled),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.takeoverStatus });
},
});
}
/**
* 代理模式下切换供应商
*/
export function useSwitchProxyProvider() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: ({
appType,
providerId,
}: {
appType: string;
providerId: string;
}) => proxyApi.switchProxyProvider(appType, providerId),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({
queryKey: ["providers", variables.appType],
});
},
onError: (error: Error) => {
toast.error(t("proxy.switchFailed", { error: error.message }));
},
});
}
// ========== Legacy 代理配置 Hooks (兼容) ==========
/**
* 获取代理配置(旧版)
*/
export function useProxyConfig() {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { data: config, isLoading } = useQuery({
queryKey: ["proxyConfig"],
queryFn: () => proxyApi.getProxyConfig(),
});
const updateMutation = useMutation({
mutationFn: proxyApi.updateProxyConfig,
onSuccess: () => {
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
},
onError: (error: Error) => {
toast.error(
t("proxy.settings.toast.saveFailed", { error: error.message }),
);
},
});
return {
config,
isLoading,
updateConfig: updateMutation.mutateAsync,
isUpdating: updateMutation.isPending,
};
}
// ========== v3+ 全局/应用级配置 Hooks ==========
/**
@@ -172,7 +72,7 @@ export function useProxyConfig() {
*/
export function useGlobalProxyConfig() {
return useQuery({
queryKey: ["globalProxyConfig"],
queryKey: proxyKeys.globalConfig,
queryFn: () => proxyApi.getGlobalProxyConfig(),
});
}
@@ -189,9 +89,8 @@ export function useUpdateGlobalProxyConfig() {
proxyApi.updateGlobalProxyConfig(config),
onSuccess: () => {
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
queryClient.invalidateQueries({ queryKey: ["globalProxyConfig"] });
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.globalConfig });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
},
onError: (error: Error) => {
toast.error(
@@ -206,7 +105,7 @@ export function useUpdateGlobalProxyConfig() {
*/
export function useAppProxyConfig(appType: string) {
return useQuery({
queryKey: ["appProxyConfig", appType],
queryKey: proxyKeys.appConfig(appType),
queryFn: () => proxyApi.getProxyConfigForApp(appType),
enabled: !!appType,
});
@@ -225,14 +124,13 @@ export function useUpdateAppProxyConfig() {
onSuccess: (_, variables) => {
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
queryClient.invalidateQueries({
queryKey: ["appProxyConfig", variables.appType],
queryKey: proxyKeys.appConfig(variables.appType),
});
queryClient.invalidateQueries({
queryKey: ["autoFailoverEnabled", variables.appType],
});
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
queryClient.invalidateQueries({ queryKey: ["circuitBreakerConfig"] });
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: proxyKeys.status });
},
onError: (error: Error) => {
toast.error(
+2 -9
View File
@@ -22,17 +22,10 @@ vi.mock("react-i18next", () => ({
vi.mock("@/hooks/useProxyStatus", () => ({
useProxyStatus: () => ({
status: null,
isLoading: false,
isRunning: false,
isTakeoverActive: false,
startWithTakeover: vi.fn(),
takeoverStatus: null,
startProxyServer: vi.fn(),
stopWithRestore: vi.fn(),
switchProxyProvider: vi.fn(),
checkRunning: vi.fn(),
checkTakeoverActive: vi.fn(),
isStarting: false,
isStopping: false,
isPending: false,
}),
}));
+16 -1
View File
@@ -3,6 +3,7 @@ import { renderHook, act, waitFor } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { proxyKeys } from "@/lib/query/proxy";
import { createTestQueryClient } from "../utils/testQueryClient";
const toastSuccessMock = vi.fn();
@@ -99,12 +100,19 @@ describe("useProxyStatus", () => {
});
});
it("keeps the established proxy query key shapes", () => {
expect(proxyKeys.status).toEqual(["proxyStatus"]);
expect(proxyKeys.takeoverStatus).toEqual(["proxyTakeoverStatus"]);
expect(proxyKeys.globalConfig).toEqual(["globalProxyConfig"]);
expect(proxyKeys.appConfig("claude")).toEqual(["appProxyConfig", "claude"]);
});
it("shows interpolated address and port after proxy server starts", async () => {
const { wrapper } = createWrapper();
const { result } = renderHook(() => useProxyStatus(), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.status).toBeDefined();
});
await act(async () => {
@@ -115,5 +123,12 @@ describe("useProxyStatus", () => {
"代理服务已启动 - 127.0.0.1:15721",
{ closeButton: true },
);
await act(async () => {
await result.current.stopProxyServer();
});
expect(invokeMock).toHaveBeenCalledWith("start_proxy_server");
expect(invokeMock).toHaveBeenCalledWith("stop_proxy_server");
});
});