mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +08:00
feat(proxy): implement local HTTP proxy server with multi-provider failover
Add a complete HTTP proxy server implementation built on Axum framework, enabling local API request forwarding with automatic provider failover and load balancing capabilities. Backend Implementation (Rust): - Add proxy server module with 7 core components: * server.rs: Axum HTTP server lifecycle management (start/stop/status) * router.rs: API routing configuration for Claude/OpenAI/Gemini endpoints * handlers.rs: Request/response handling and transformation * forwarder.rs: Upstream forwarding logic with retry mechanism (652 lines) * error.rs: Comprehensive error handling and HTTP status mapping * types.rs: Shared types (ProxyConfig, ProxyStatus, ProxyServerInfo) * health.rs: Provider health check infrastructure Service Layer: - Add ProxyService (services/proxy.rs, 157 lines): * Manage proxy server lifecycle * Handle configuration updates * Track runtime status and metrics Database Layer: - Add proxy configuration DAO (dao/proxy.rs, 242 lines): * Persist proxy settings (listen address, port, timeout) * Store provider priority and availability flags - Update schema with proxy_config table (schema.rs): * Support runtime configuration persistence Tauri Commands: - Add 6 command endpoints (commands/proxy.rs): * start_proxy_server: Launch proxy server * stop_proxy_server: Gracefully shutdown server * get_proxy_status: Query runtime status * get_proxy_config: Retrieve current configuration * update_proxy_config: Modify settings without restart * is_proxy_running: Check server state Frontend Implementation (React + TypeScript): - Add ProxyPanel component (222 lines): * Real-time server status display * Start/stop controls * Provider availability monitoring - Add ProxySettingsDialog component (420 lines): * Configuration editor (address, port, timeout) * Provider priority management * Settings validation - Add React hooks: * useProxyConfig: Manage proxy configuration state * useProxyStatus: Poll and display server status - Add TypeScript types (types/proxy.ts): * Define ProxyConfig, ProxyStatus interfaces Provider Integration: - Extend Provider model with availability field (providers.rs): * Track provider health for failover logic - Update ProviderCard UI to display proxy status - Integrate proxy controls in Settings page Dependencies: - Add Axum 0.7 (async web framework) - Add Tower 0.4 (middleware and service abstractions) - Add Tower-HTTP (CORS layer) - Add Tokio sync primitives (oneshot, RwLock) Technical Details: - Graceful shutdown via oneshot channel - Shared state with Arc<RwLock<T>> for thread-safe config updates - CORS enabled for cross-origin frontend access - Request/response streaming support - Automatic retry with exponential backoff (forwarder) - API key extraction from multiple config formats (Claude/Codex/Gemini) File Statistics: - 41 files changed - 3491 insertions(+), 41 deletions(-) - Core modules: 1393 lines (server + forwarder + handlers) - Frontend UI: 642 lines (ProxyPanel + ProxySettingsDialog) - Database/DAO: 326 lines This implementation provides the foundation for advanced features like: - Multi-provider load balancing - Automatic failover on provider errors - Request logging and analytics - Usage tracking and cost monitoring
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { useState } from "react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { Settings, Activity, Clock, TrendingUp, Server } from "lucide-react";
|
||||
import { ProxySettingsDialog } from "./ProxySettingsDialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ProxyPanel() {
|
||||
const { status, isRunning, start, stop, isPending } = useProxyStatus();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const handleToggle = async () => {
|
||||
try {
|
||||
if (isRunning) {
|
||||
await stop();
|
||||
} else {
|
||||
await start();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Toggle proxy failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
} else {
|
||||
return `${secs}s`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="space-y-6 rounded-xl border border-white/10 glass-card p-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||
<Server className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
本地代理服务
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isRunning
|
||||
? `运行中 · ${status?.address}:${status?.port}`
|
||||
: "已停止"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge
|
||||
variant={isRunning ? "default" : "secondary"}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Activity
|
||||
className={`h-3 w-3 ${isRunning ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowSettings(true)}
|
||||
disabled={isPending}
|
||||
aria-label="打开代理设置"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
aria-label={isRunning ? "停止代理服务" : "启动代理服务"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isRunning && status ? (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg border border-white/10 bg-muted/40 p-4 space-y-4">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
服务地址
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<code className="flex-1 text-sm bg-background px-3 py-2 rounded border border-border/60">
|
||||
http://{status.address}:{status.port}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`http://${status.address}:${status.port}`,
|
||||
);
|
||||
toast.success("地址已复制");
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-white/10 space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
当前代理
|
||||
</p>
|
||||
{status.active_targets && status.active_targets.length > 0 ? (
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{status.active_targets.map((target) => (
|
||||
<div
|
||||
key={target.app_type}
|
||||
className="flex items-center justify-between rounded-md border border-white/10 bg-background/60 px-2 py-1.5 text-xs"
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{target.app_type}
|
||||
</span>
|
||||
<span
|
||||
className="ml-2 font-medium truncate text-foreground"
|
||||
title={target.provider_name}
|
||||
>
|
||||
{target.provider_name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : status.current_provider ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
当前 Provider:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{status.current_provider}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
当前 Provider:等待首次请求…
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<StatCard
|
||||
icon={<Activity className="h-4 w-4" />}
|
||||
label="活跃连接"
|
||||
value={status.active_connections}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<TrendingUp className="h-4 w-4" />}
|
||||
label="总请求数"
|
||||
value={status.total_requests}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Clock className="h-4 w-4" />}
|
||||
label="成功率"
|
||||
value={`${status.success_rate.toFixed(1)}%`}
|
||||
variant={status.success_rate > 90 ? "success" : "warning"}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Clock className="h-4 w-4" />}
|
||||
label="运行时间"
|
||||
value={formatUptime(status.uptime_seconds)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10 text-muted-foreground">
|
||||
<div className="mx-auto w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||
<Server className="h-8 w-8" />
|
||||
</div>
|
||||
<p className="text-base font-medium text-foreground mb-1">
|
||||
代理服务已停止
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
使用右上角开关即可启动服务
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<ProxySettingsDialog open={showSettings} onOpenChange={setShowSettings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
variant?: "default" | "success" | "warning";
|
||||
}
|
||||
|
||||
function StatCard({ icon, label, value, variant = "default" }: StatCardProps) {
|
||||
const variantStyles = {
|
||||
default: "",
|
||||
success: "border-green-500/40 bg-green-500/5",
|
||||
warning: "border-yellow-500/40 bg-yellow-500/5",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg border border-white/10 bg-white/70 p-4 text-sm text-muted-foreground dark:bg-white/5 ${variantStyles[variant]}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-2">
|
||||
{icon}
|
||||
<span className="text-xs font-medium uppercase tracking-wide">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-foreground">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user