mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
4c94e70f97
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
223 lines
7.9 KiB
TypeScript
223 lines
7.9 KiB
TypeScript
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>
|
||
);
|
||
}
|