feat(proxy): implement per-app takeover mode

Replace global live takeover with granular per-app control:
- Add start_proxy_server command (start without takeover)
- Add get_proxy_takeover_status to query each app's state
- Add set_proxy_takeover_for_app for individual app control
- Use live backup existence as SSOT for takeover state
- Refactor sync_live_to_provider to eliminate code duplication
- Update ProxyToggle to show status per active app
This commit is contained in:
Jason
2025-12-18 11:28:10 +08:00
parent 0cd7d0756c
commit 18207771ad
12 changed files with 768 additions and 238 deletions
+72 -21
View File
@@ -6,7 +6,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import type { ProxyStatus, ProxyServerInfo } from "@/types/proxy";
import type { ProxyStatus, ProxyServerInfo, ProxyTakeoverStatus } from "@/types/proxy";
import { extractErrorMessage } from "@/utils/errorUtils";
/**
@@ -26,47 +26,47 @@ export function useProxyStatus() {
placeholderData: (previousData) => previousData,
});
// 查询接管状态
const { data: isTakeoverActive } = useQuery({
queryKey: ["proxyTakeoverActive"],
queryFn: () => invoke<boolean>("is_live_takeover_active"),
// 查询各应用接管状态
const { data: takeoverStatus } = useQuery({
queryKey: ["proxyTakeoverStatus"],
queryFn: () => invoke<ProxyTakeoverStatus>("get_proxy_takeover_status"),
placeholderData: (previousData) => previousData,
});
// 启动服务器(带 Live 配置接管)
const startWithTakeoverMutation = useMutation({
mutationFn: () => invoke<ProxyServerInfo>("start_proxy_with_takeover"),
// 启动服务器(总开关:仅启动服务,不接管)
const startProxyServerMutation = useMutation({
mutationFn: () => invoke<ProxyServerInfo>("start_proxy_server"),
onSuccess: (info) => {
toast.success(
t("proxy.startedWithTakeover", {
defaultValue: `代理模式已启 - ${info.address}:${info.port}`,
t("proxy.server.started", {
defaultValue: `代理服务已启 - ${info.address}:${info.port}`,
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
},
onError: (error: Error) => {
const detail = extractErrorMessage(error) || "未知错误";
toast.error(
t("proxy.startWithTakeoverFailed", {
defaultValue: `启动失败: ${detail}`,
t("proxy.server.startFailed", {
defaultValue: `启动代理服务失败: ${detail}`,
}),
);
},
});
// 停止服务器(恢复 Live 配置)
// 停止服务器(总开关关闭:强制恢复所有已接管的 Live 配置)
const stopWithRestoreMutation = useMutation({
mutationFn: () => invoke("stop_proxy_with_restore"),
onSuccess: () => {
toast.success(
t("proxy.stoppedWithRestore", {
defaultValue: "代理模式已关闭,配置已恢复",
defaultValue: "代理服务已关闭,已恢复所有接管配置",
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
// 清除所有供应商健康状态缓存(后端已清空数据库记录)
queryClient.invalidateQueries({ queryKey: ["providerHealth"] });
},
@@ -80,6 +80,47 @@ export function useProxyStatus() {
},
});
// 按应用开启/关闭接管
const setTakeoverForAppMutation = useMutation({
mutationFn: ({
appType,
enabled,
}: {
appType: string;
enabled: boolean;
}) => invoke("set_proxy_takeover_for_app", { appType, enabled }),
onSuccess: (_data, variables) => {
const appLabel =
variables.appType === "claude"
? "Claude"
: variables.appType === "codex"
? "Codex"
: "Gemini";
toast.success(
variables.enabled
? t("proxy.takeover.enabled", {
defaultValue: `已接管 ${appLabel} 配置(请求将走本地代理)`,
})
: t("proxy.takeover.disabled", {
defaultValue: `已恢复 ${appLabel} 配置`,
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
},
onError: (error: Error) => {
const detail = extractErrorMessage(error) || "未知错误";
toast.error(
t("proxy.takeover.failed", {
defaultValue: `操作失败: ${detail}`,
}),
);
},
});
// 代理模式切换供应商(热切换)
const switchProxyProviderMutation = useMutation({
mutationFn: ({
@@ -120,12 +161,20 @@ export function useProxyStatus() {
status,
isLoading,
isRunning: status?.running || false,
isTakeoverActive: isTakeoverActive || false,
takeoverStatus,
isTakeoverActive:
takeoverStatus?.claude ||
takeoverStatus?.codex ||
takeoverStatus?.gemini ||
false,
// 启动/停止(接管模式
startWithTakeover: startWithTakeoverMutation.mutateAsync,
// 启动/停止(总开关
startProxyServer: startProxyServerMutation.mutateAsync,
stopWithRestore: stopWithRestoreMutation.mutateAsync,
// 按应用接管开关
setTakeoverForApp: setTakeoverForAppMutation.mutateAsync,
// 代理模式下切换供应商
switchProxyProvider: switchProxyProviderMutation.mutateAsync,
@@ -134,9 +183,11 @@ export function useProxyStatus() {
checkTakeoverActive,
// 加载状态
isStarting: startWithTakeoverMutation.isPending,
isStarting: startProxyServerMutation.isPending,
isStopping: stopWithRestoreMutation.isPending,
isPending:
startWithTakeoverMutation.isPending || stopWithRestoreMutation.isPending,
startProxyServerMutation.isPending ||
stopWithRestoreMutation.isPending ||
setTakeoverForAppMutation.isPending,
};
}