mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 04:02:02 +08:00
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:
+1
-1
@@ -491,7 +491,7 @@ function App() {
|
||||
)}
|
||||
{currentView === "providers" && (
|
||||
<>
|
||||
<ProxyToggle />
|
||||
<ProxyToggle activeApp={activeApp} />
|
||||
|
||||
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
||||
|
||||
|
||||
@@ -9,40 +9,54 @@ import { Radio, Loader2 } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AppId } from "@/lib/api";
|
||||
|
||||
interface ProxyToggleProps {
|
||||
className?: string;
|
||||
activeApp: AppId;
|
||||
}
|
||||
|
||||
export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
export function ProxyToggle({ className, activeApp }: ProxyToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isRunning,
|
||||
isTakeoverActive,
|
||||
startWithTakeover,
|
||||
stopWithRestore,
|
||||
takeoverStatus,
|
||||
setTakeoverForApp,
|
||||
isPending,
|
||||
status,
|
||||
} = useProxyStatus();
|
||||
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
if (checked) {
|
||||
await startWithTakeover();
|
||||
} else {
|
||||
await stopWithRestore();
|
||||
}
|
||||
await setTakeoverForApp({ appType: activeApp, enabled: checked });
|
||||
};
|
||||
|
||||
const isActive = isRunning && isTakeoverActive;
|
||||
const takeoverEnabled = takeoverStatus?.[activeApp] || false;
|
||||
|
||||
const tooltipText = isActive
|
||||
? `代理模式运行中 - ${status?.address}:${status?.port}\n切换供应商为热切换`
|
||||
: "开启代理模式\n启用后自动接管 Live 配置";
|
||||
const appLabel =
|
||||
activeApp === "claude"
|
||||
? "Claude"
|
||||
: activeApp === "codex"
|
||||
? "Codex"
|
||||
: "Gemini";
|
||||
|
||||
const tooltipText = takeoverEnabled
|
||||
? isRunning
|
||||
? t("proxy.takeover.tooltip.active", {
|
||||
defaultValue: `${appLabel} 已接管 - ${status?.address}:${status?.port}\n切换该应用供应商为热切换`,
|
||||
})
|
||||
: t("proxy.takeover.tooltip.broken", {
|
||||
defaultValue: `${appLabel} 已接管,但代理服务未运行`,
|
||||
})
|
||||
: t("proxy.takeover.tooltip.inactive", {
|
||||
defaultValue: `接管 ${appLabel} 的 Live 配置,让该应用请求走本地代理`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all cursor-default",
|
||||
isActive
|
||||
takeoverEnabled
|
||||
? "bg-emerald-500/10 border border-emerald-500/30"
|
||||
: "bg-muted/50 hover:bg-muted",
|
||||
className,
|
||||
@@ -55,7 +69,7 @@ export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
<Radio
|
||||
className={cn(
|
||||
"h-4 w-4 transition-colors",
|
||||
isActive
|
||||
takeoverEnabled
|
||||
? "text-emerald-500 animate-pulse"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
@@ -64,7 +78,7 @@ export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium transition-colors select-none",
|
||||
isActive
|
||||
takeoverEnabled
|
||||
? "text-emerald-600 dark:text-emerald-400"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
@@ -72,7 +86,7 @@ export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
Proxy
|
||||
</span>
|
||||
<Switch
|
||||
checked={isActive}
|
||||
checked={takeoverEnabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="ml-1"
|
||||
|
||||
@@ -177,8 +177,8 @@ export function SettingsPage({
|
||||
|
||||
const {
|
||||
isRunning,
|
||||
startWithTakeover: startProxy,
|
||||
stopWithRestore: stopProxy,
|
||||
startProxyServer,
|
||||
stopWithRestore,
|
||||
isPending: isProxyPending,
|
||||
} = useProxyStatus();
|
||||
const [failoverEnabled, setFailoverEnabled] = useState(true);
|
||||
@@ -186,9 +186,9 @@ export function SettingsPage({
|
||||
const handleToggleProxy = async (checked: boolean) => {
|
||||
try {
|
||||
if (!checked) {
|
||||
await stopProxy();
|
||||
await stopWithRestore();
|
||||
} else {
|
||||
await startProxy();
|
||||
await startProxyServer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Toggle proxy failed:", error);
|
||||
|
||||
+72
-21
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ export interface ProxyServerInfo {
|
||||
started_at: string;
|
||||
}
|
||||
|
||||
export interface ProxyTakeoverStatus {
|
||||
claude: boolean;
|
||||
codex: boolean;
|
||||
gemini: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderHealth {
|
||||
provider_id: string;
|
||||
app_type: string;
|
||||
|
||||
Reference in New Issue
Block a user