fix(proxy): resolve 404 error and auto-setup proxy targets

Issues fixed:
1. Route prefix mismatch - Live config was set to use /claude, /codex,
   /gemini prefixes but server only had routes without prefixes
2. Proxy targets not auto-configured - start_with_takeover only modified
   Live config but didn't set is_proxy_target=true for current providers

Changes:
- Add prefixed routes (/claude/v1/messages, /codex/v1/*, /gemini/v1beta/*)
  to server.rs for backward compatibility
- Remove URL prefixes from takeover_live_configs() - server can route
  by API endpoint path alone (/v1/messages=Claude, /v1/chat/completions=Codex)
- Add setup_proxy_targets() method that automatically sets current providers
  as proxy targets when starting proxy with takeover
- Unify proxy toggle in SettingsPage to use takeover mode (same as main UI)
- Fix error message extraction in useProxyStatus hooks
- Fix provider switch logic to check both takeover flag AND proxy running state
This commit is contained in:
Jason
2025-12-10 19:58:43 +08:00
parent 3cdce2eced
commit 5cc864c6aa
17 changed files with 831 additions and 47 deletions
+5 -1
View File
@@ -33,6 +33,7 @@ import { ConfirmDialog } from "@/components/ConfirmDialog";
import { SettingsPage } from "@/components/settings/SettingsPage";
import { UpdateBadge } from "@/components/UpdateBadge";
import { EnvWarningBanner } from "@/components/env/EnvWarningBanner";
import { ProxyToggle } from "@/components/proxy/ProxyToggle";
import UsageScriptModal from "@/components/UsageScriptModal";
import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel";
import PromptPanel from "@/components/prompts/PromptPanel";
@@ -63,7 +64,7 @@ function App() {
"bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8";
// 获取代理服务状态
const { isRunning: isProxyRunning } = useProxyStatus();
const { isRunning: isProxyRunning, isTakeoverActive } = useProxyStatus();
// 获取供应商列表,当代理服务运行时自动刷新
const { data, isLoading, refetch } = useProvidersQuery(activeApp, {
@@ -321,6 +322,7 @@ function App() {
appId={activeApp}
isLoading={isLoading}
isProxyRunning={isProxyRunning}
isProxyTakeover={isProxyRunning && isTakeoverActive}
onSwitch={switchProvider}
onEdit={setEditingProvider}
onDelete={setConfirmDelete}
@@ -482,6 +484,8 @@ function App() {
)}
{currentView === "providers" && (
<>
<ProxyToggle />
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
<div className="bg-muted p-1 rounded-xl flex items-center gap-1">
+8 -1
View File
@@ -40,6 +40,7 @@ interface ProviderCardProps {
onTest?: (provider: Provider) => void;
isTesting?: boolean;
isProxyRunning: boolean;
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换)
proxyPriority?: number; // 代理目标的实际优先级 (1, 2, 3...)
allProviders?: Provider[]; // 所有供应商列表,用于计算开启后的优先级
dragHandleProps?: DragHandleProps;
@@ -93,6 +94,7 @@ export function ProviderCard({
onTest,
isTesting,
isProxyRunning,
isProxyTakeover = false,
proxyPriority,
allProviders,
dragHandleProps,
@@ -231,7 +233,12 @@ export function ProviderCard({
className={cn(
"relative overflow-hidden rounded-xl border border-border p-4 transition-all duration-300",
"bg-card text-card-foreground group hover:border-border-active",
isCurrent ? "border-primary/50 shadow-sm" : "hover:shadow-sm",
// 代理接管模式下当前供应商使用绿色边框
isProxyTakeover && isCurrent
? "border-emerald-500/60 shadow-sm shadow-emerald-500/10"
: isCurrent
? "border-primary/50 shadow-sm"
: "hover:shadow-sm",
dragHandleProps?.isDragging &&
"cursor-grabbing border-primary shadow-lg scale-105 z-10",
)}
@@ -27,6 +27,7 @@ interface ProviderListProps {
onCreate?: () => void;
isLoading?: boolean;
isProxyRunning?: boolean; // 代理服务运行状态
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管)
}
export function ProviderList({
@@ -42,6 +43,7 @@ export function ProviderList({
onCreate,
isLoading = false,
isProxyRunning = false, // 默认值为 false
isProxyTakeover = false, // 默认值为 false
}: ProviderListProps) {
const { sortedProviders, sensors, handleDragEnd } = useDragSort(
providers,
@@ -122,6 +124,7 @@ export function ProviderList({
onTest={handleTest}
isTesting={isTesting(provider.id)}
isProxyRunning={isProxyRunning}
isProxyTakeover={isProxyTakeover}
proxyPriority={proxyPriorityMap.get(provider.id)}
allProviders={sortedProviders}
/>
@@ -145,6 +148,7 @@ interface SortableProviderCardProps {
onTest: (provider: Provider) => void;
isTesting: boolean;
isProxyRunning: boolean;
isProxyTakeover: boolean;
proxyPriority?: number; // 代理目标的实际优先级 (1, 2, 3...)
allProviders?: Provider[]; // 所有供应商列表
}
@@ -162,6 +166,7 @@ function SortableProviderCard({
onTest,
isTesting,
isProxyRunning,
isProxyTakeover,
proxyPriority,
allProviders,
}: SortableProviderCardProps) {
@@ -196,6 +201,7 @@ function SortableProviderCard({
onTest={onTest}
isTesting={isTesting}
isProxyRunning={isProxyRunning}
isProxyTakeover={isProxyTakeover}
proxyPriority={proxyPriority}
allProviders={allProviders}
dragHandleProps={{
@@ -18,8 +18,11 @@ export interface AutoFailoverConfigPanelProps {
export function AutoFailoverConfigPanel({
enabled,
onEnabledChange,
onEnabledChange: _onEnabledChange,
}: AutoFailoverConfigPanelProps) {
// Note: onEnabledChange is currently unused but kept in the interface
// for potential future use by parent components
void _onEnabledChange;
const { t } = useTranslation();
const { data: config, isLoading, error } = useCircuitBreakerConfig();
const updateConfig = useUpdateCircuitBreakerConfig();
@@ -262,7 +265,7 @@ export function AutoFailoverConfigPanel({
</Button>
<Button
onClick={handleSave}
disabled={updateConfig.isPending || !formData.enabled}
disabled={updateConfig.isPending || !enabled}
>
{updateConfig.isPending ? (
<>
+82
View File
@@ -0,0 +1,82 @@
/**
* 代理模式切换开关组件
*
* 放置在主界面头部,用于一键启用/关闭代理模式
* 启用时自动接管 Live 配置,关闭时恢复原始配置
*/
import { Radio, Loader2 } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { cn } from "@/lib/utils";
interface ProxyToggleProps {
className?: string;
}
export function ProxyToggle({ className }: ProxyToggleProps) {
const {
isRunning,
isTakeoverActive,
startWithTakeover,
stopWithRestore,
isPending,
status,
} = useProxyStatus();
const handleToggle = async (checked: boolean) => {
if (checked) {
await startWithTakeover();
} else {
await stopWithRestore();
}
};
const isActive = isRunning && isTakeoverActive;
const tooltipText = isActive
? `代理模式运行中 - ${status?.address}:${status?.port}\n切换供应商为热切换`
: "开启代理模式\n启用后自动接管 Live 配置";
return (
<div
className={cn(
"flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all cursor-default",
isActive
? "bg-emerald-500/10 border border-emerald-500/30"
: "bg-muted/50 hover:bg-muted",
className,
)}
title={tooltipText}
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
) : (
<Radio
className={cn(
"h-4 w-4 transition-colors",
isActive
? "text-emerald-500 animate-pulse"
: "text-muted-foreground",
)}
/>
)}
<span
className={cn(
"text-sm font-medium transition-colors select-none",
isActive
? "text-emerald-600 dark:text-emerald-400"
: "text-muted-foreground",
)}
>
Proxy
</span>
<Switch
checked={isActive}
onCheckedChange={handleToggle}
disabled={isPending}
className="ml-1"
/>
</div>
);
}
+2 -2
View File
@@ -174,8 +174,8 @@ export function SettingsPage({
const {
isRunning,
start: startProxy,
stop: stopProxy,
startWithTakeover: startProxy,
stopWithRestore: stopProxy,
isPending: isProxyPending,
} = useProxyStatus();
const [failoverEnabled, setFailoverEnabled] = useState(true);
+108 -7
View File
@@ -5,13 +5,16 @@
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 { extractErrorMessage } from "@/utils/errorUtils";
/**
* 代理服务状态管理
*/
export function useProxyStatus() {
const queryClient = useQueryClient();
const { t } = useTranslation();
// 查询状态(自动轮询)
const { data: status, isLoading } = useQuery({
@@ -23,7 +26,13 @@ export function useProxyStatus() {
placeholderData: (previousData) => previousData,
});
// 启动服务器
// 查询接管状态
const { data: isTakeoverActive } = useQuery({
queryKey: ["proxyTakeoverActive"],
queryFn: () => invoke<boolean>("is_live_takeover_active"),
});
// 启动服务器(普通模式)
const startMutation = useMutation({
mutationFn: () => invoke<ProxyServerInfo>("start_proxy_server"),
onSuccess: (info) => {
@@ -31,11 +40,34 @@ export function useProxyStatus() {
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
},
onError: (error: Error) => {
toast.error(`启动失败: ${error.message}`);
const detail = extractErrorMessage(error) || "未知错误";
toast.error(`启动失败: ${detail}`);
},
});
// 停止服务器
// 启动服务器(带 Live 配置接管)
const startWithTakeoverMutation = useMutation({
mutationFn: () => invoke<ProxyServerInfo>("start_proxy_with_takeover"),
onSuccess: (info) => {
toast.success(
t("proxy.startedWithTakeover", {
defaultValue: `代理模式已启用 - ${info.address}:${info.port}`,
}),
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
},
onError: (error: Error) => {
const detail = extractErrorMessage(error) || "未知错误";
toast.error(
t("proxy.startWithTakeoverFailed", {
defaultValue: `启动失败: ${detail}`,
}),
);
},
});
// 停止服务器(普通模式)
const stopMutation = useMutation({
mutationFn: () => invoke("stop_proxy_server"),
onSuccess: () => {
@@ -43,7 +75,48 @@ export function useProxyStatus() {
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
},
onError: (error: Error) => {
toast.error(`停止失败: ${error.message}`);
const detail = extractErrorMessage(error) || "未知错误";
toast.error(`停止失败: ${detail}`);
},
});
// 停止服务器(恢复 Live 配置)
const stopWithRestoreMutation = useMutation({
mutationFn: () => invoke("stop_proxy_with_restore"),
onSuccess: () => {
toast.success(
t("proxy.stoppedWithRestore", {
defaultValue: "代理模式已关闭,配置已恢复",
}),
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
},
onError: (error: Error) => {
const detail = extractErrorMessage(error) || "未知错误";
toast.error(
t("proxy.stopWithRestoreFailed", {
defaultValue: `停止失败: ${detail}`,
}),
);
},
});
// 代理模式切换供应商(热切换)
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) || "未知错误";
toast.error(`切换失败: ${detail}`);
},
});
@@ -56,15 +129,43 @@ export function useProxyStatus() {
}
};
// 检查接管状态
const checkTakeoverActive = async () => {
try {
return await invoke<boolean>("is_live_takeover_active");
} catch {
return false;
}
};
return {
status,
isLoading,
isRunning: status?.running || false,
isTakeoverActive: isTakeoverActive || false,
// 普通模式
start: startMutation.mutateAsync,
stop: stopMutation.mutateAsync,
// 接管模式(推荐使用)
startWithTakeover: startWithTakeoverMutation.mutateAsync,
stopWithRestore: stopWithRestoreMutation.mutateAsync,
// 代理模式下切换供应商
switchProxyProvider: switchProxyProviderMutation.mutateAsync,
// 状态检查
checkRunning,
isStarting: startMutation.isPending,
isStopping: stopMutation.isPending,
isPending: startMutation.isPending || stopMutation.isPending,
checkTakeoverActive,
// 加载状态
isStarting: startMutation.isPending || startWithTakeoverMutation.isPending,
isStopping: stopMutation.isPending || stopWithRestoreMutation.isPending,
isPending:
startMutation.isPending ||
stopMutation.isPending ||
startWithTakeoverMutation.isPending ||
stopWithRestoreMutation.isPending,
};
}
+1
View File
@@ -5,6 +5,7 @@ export interface ProxyConfig {
max_retries: number;
request_timeout: number;
enable_logging: boolean;
live_takeover_active?: boolean;
}
export interface ProxyStatus {