Files
CC-Switch/src/components/proxy/ClaudeDesktopRouteToggle.tsx
T
Jason 8b3ad9caf9 feat(claude-desktop): add 3P provider switching with proxy gateway
Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
inference profile under configLibrary/, sharing _meta.json with other
launchers (Ollama-compatible) so cc-switch can coexist with them.

Two switch modes:
- direct: provider already exposes claude-* / anthropic/claude-* model
  ids on Anthropic Messages, Claude Desktop connects to it directly.
- proxy: cc-switch's local proxy acts as the inference gateway,
  presenting only claude-* route names to Claude Desktop and mapping
  them to real upstream models. Required after Anthropic restricted
  Claude Desktop to claude-family ids.

Backend:
- New module claude_desktop_config with snapshot/rollback, official seed
  bypass, /claude-desktop/v1/{models,messages} routes, and a single
  source of truth for default proxy routes.
- Gateway token persisted in SQLite, validated on every proxied request.
- get_claude_desktop_status surfaces drift signals (stale models,
  missing routes, proxy stopped, base URL mismatch, missing token).

Frontend:
- Slim ClaudeDesktopProviderForm independent from ProviderForm,
  controlled by a top-level appId guard.
- ProviderList banner consumes the status query (5s polling) and
  renders actionable diagnostics.
- ClaudeDesktopRouteToggle in the header to start/stop the local
  gateway without touching takeover state.
- Three-locale i18n synchronised.
2026-05-08 22:34:46 +08:00

97 lines
2.6 KiB
TypeScript

import { Loader2, Radio } from "lucide-react";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { Switch } from "@/components/ui/switch";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { cn } from "@/lib/utils";
interface ClaudeDesktopRouteToggleProps {
className?: string;
}
export function ClaudeDesktopRouteToggle({
className,
}: ClaudeDesktopRouteToggleProps) {
const { t } = useTranslation();
const {
isRunning,
status,
takeoverStatus,
startProxyServer,
stopProxyServer,
isStarting,
isStoppingServer,
} = useProxyStatus();
const isBusy = isStarting || isStoppingServer;
const otherTakeoverActive = Boolean(
takeoverStatus?.claude || takeoverStatus?.codex || takeoverStatus?.gemini,
);
const routeAddress = status?.address ?? "127.0.0.1";
const routePort = status?.port ?? 15721;
const handleToggle = async (checked: boolean) => {
try {
if (checked) {
await startProxyServer();
return;
}
if (otherTakeoverActive) {
toast.warning(
t("claudeDesktop.route.stopBlockedByTakeover", {
defaultValue:
"其它应用正在使用代理接管。请先在设置中关闭对应应用接管,再停止本地路由。",
}),
{ duration: 5000 },
);
return;
}
await stopProxyServer();
} catch (error) {
console.error("[ClaudeDesktopRouteToggle] Toggle route failed:", error);
}
};
const tooltipText = isRunning
? t("claudeDesktop.route.tooltip.active", {
address: routeAddress,
port: routePort,
defaultValue: `Claude Desktop 本地路由已开启 - ${routeAddress}:${routePort}`,
})
: t("claudeDesktop.route.tooltip.inactive", {
address: routeAddress,
port: routePort,
defaultValue: `开启 Claude Desktop 本地路由,用于需要模型映射或格式转换的供应商。当前配置地址:${routeAddress}:${routePort}`,
});
return (
<div
className={cn(
"flex items-center gap-1 px-1.5 h-8 rounded-lg bg-muted/50 transition-all",
className,
)}
title={tooltipText}
>
{isBusy ? (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
) : (
<Radio
className={cn(
"h-4 w-4 transition-colors",
isRunning
? "text-emerald-500 animate-pulse"
: "text-muted-foreground",
)}
/>
)}
<Switch
checked={isRunning}
onCheckedChange={handleToggle}
disabled={isBusy}
/>
</div>
);
}