mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
41267135f5
* feat(db): add circuit breaker config table and provider proxy target APIs Add database support for auto-failover feature: - Add circuit_breaker_config table for storing failover thresholds - Add get/update_circuit_breaker_config methods in proxy DAO - Add reset_provider_health method for manual recovery - Add set_proxy_target and get_proxy_targets methods in providers DAO for managing multi-provider failover configuration * feat(proxy): implement circuit breaker and provider router for auto-failover Add core failover logic: - CircuitBreaker: Tracks provider health with three states: - Closed: Normal operation, requests pass through - Open: Circuit broken after consecutive failures, skip provider - HalfOpen: Testing recovery with limited requests - ProviderRouter: Routes requests across multiple providers with: - Health tracking and automatic failover - Configurable failure/success thresholds - Auto-disable proxy target after reaching failure threshold - Support for manual circuit breaker reset - Export new types in proxy module * feat(proxy): add failover Tauri commands and integrate with forwarder Expose failover functionality to frontend: - Add Tauri commands: get_proxy_targets, set_proxy_target, get_provider_health, reset_circuit_breaker, get/update_circuit_breaker_config, get_circuit_breaker_stats - Register all new commands in lib.rs invoke handler - Update forwarder with improved error handling and logging - Integrate ProviderRouter with proxy server startup - Add provider health tracking in request handlers * feat(frontend): add failover API layer and TanStack Query hooks Add frontend data layer for failover management: - Add failover.ts API: Tauri invoke wrappers for all failover commands - Add failover.ts query hooks: TanStack Query mutations and queries - useProxyTargets, useProviderHealth queries - useSetProxyTarget, useResetCircuitBreaker mutations - useCircuitBreakerConfig query and mutation - Update queries.ts with provider health query key - Update mutations.ts to invalidate health on provider changes - Add CircuitBreakerConfig and ProviderHealth types * feat(ui): add auto-failover configuration UI and provider health display Add comprehensive UI for failover management: Components: - ProviderHealthBadge: Display provider health status with color coding - CircuitBreakerConfigPanel: Configure failure/success thresholds, timeout duration, and error rate limits - AutoFailoverConfigPanel: Manage proxy targets with drag-and-drop priority ordering and individual enable/disable controls - ProxyPanel: Integrate failover tabs for unified proxy management Provider enhancements: - ProviderCard: Show health badge and proxy target indicator - ProviderActions: Add "Set as Proxy Target" action - EditProviderDialog: Add is_proxy_target toggle - ProviderList: Support proxy target filtering mode Other: - Update App.tsx routing for settings integration - Update useProviderActions hook with proxy target mutation - Fix ProviderList tests for updated component API * fix(usage): stabilize date range to prevent infinite re-renders * feat(backend): add tool version check command Add get_tool_versions command to check local and latest versions of Claude, Codex, and Gemini CLI tools: - Detect local installed versions via command line execution - Fetch latest versions from npm registry (Claude, Gemini) and GitHub releases API (Codex) - Return comprehensive version info including error details for uninstalled tools - Register command in Tauri invoke handler * style(ui): format accordion component code style Apply consistent code formatting to accordion component: - Convert double quotes to semicolons at line endings - Adjust indentation to 2-space standard - Align with project code style conventions * refactor(providers): update provider card styling to use theme tokens Replace hardcoded color classes with semantic design tokens: - Use bg-card, border-border, text-card-foreground instead of glass-card - Replace gray/white color literals with muted/foreground tokens - Change proxy target indicator color from purple to green - Improve hover states with border-border-active - Ensure consistent dark mode support via CSS variables * refactor(proxy): simplify auto-failover config panel structure Restructure AutoFailoverConfigPanel for better integration: - Remove internal Card wrapper and expansion toggle (now handled by parent) - Extract enabled state to props for external control - Simplify loading state display - Clean up redundant CardHeader/CardContent wrappers - ProxyPanel: reduce complexity by delegating to parent components * feat(settings): enhance settings page with accordion layout and tool versions Major settings page improvements: AboutSection: - Add local tool version detection (Claude, Codex, Gemini) - Display installed vs latest version comparison with visual indicators - Show update availability badges and environment check cards SettingsPage: - Reorganize advanced settings into collapsible accordion sections - Add proxy control panel with inline status toggle - Integrate auto-failover configuration with accordion UI - Add database and cost calculation config sections DirectorySettings & WindowSettings: - Minor styling adjustments for consistency settings.ts API: - Add getToolVersions() wrapper for new backend command * refactor(usage): restructure usage dashboard components Comprehensive usage statistics panel refactoring: UsageDashboard: - Reorganize layout with improved section headers - Add better loading states and empty state handling ModelStatsTable & ProviderStatsTable: - Minor styling updates for consistency ModelTestConfigPanel & PricingConfigPanel: - Simplify component structure - Remove redundant Card wrappers - Improve form field organization RequestLogTable: - Enhance table layout with better column sizing - Improve pagination controls UsageSummaryCards: - Update card styling with semantic tokens - Better responsive grid layout UsageTrendChart: - Refine chart container styling - Improve legend and tooltip display * chore(deps): add accordion and animation dependencies Package updates: - Add @radix-ui/react-accordion for collapsible sections - Add cmdk for command palette support - Add framer-motion for enhanced animations Tailwind config: - Add accordion-up/accordion-down animations - Update darkMode config to support both selector and class - Reorganize color and keyframe definitions for clarity * style(app): update header and app switcher styling App.tsx: - Replace glass-header with explicit bg-background/80 backdrop-blur - Update navigation button container to use bg-muted AppSwitcher: - Replace hardcoded gray colors with semantic muted/foreground tokens - Ensure consistent dark mode support via CSS variables - Add group class for better hover state transitions
195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import {
|
|
AreaChart,
|
|
Area,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
Legend,
|
|
} from "recharts";
|
|
import { useUsageTrends } from "@/lib/query/usage";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
interface UsageTrendChartProps {
|
|
days: number;
|
|
}
|
|
|
|
export function UsageTrendChart({ days }: UsageTrendChartProps) {
|
|
const { t } = useTranslation();
|
|
const { data: trends, isLoading } = useUsageTrends(days);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-[350px] items-center justify-center rounded-xl bg-card/40 border border-border/50">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground/30" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isToday = days === 1;
|
|
const chartData =
|
|
trends?.map((stat) => {
|
|
const pointDate = new Date(stat.date);
|
|
return {
|
|
rawDate: stat.date,
|
|
label: isToday
|
|
? pointDate.toLocaleTimeString("zh-CN", { hour: "2-digit" })
|
|
: pointDate.toLocaleDateString("zh-CN", {
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
}),
|
|
hour: pointDate.getHours(),
|
|
inputTokens: stat.totalInputTokens,
|
|
outputTokens: stat.totalOutputTokens,
|
|
cost: parseFloat(stat.totalCost),
|
|
};
|
|
}) || [];
|
|
|
|
const hourlyData = (() => {
|
|
if (!isToday) return chartData;
|
|
const map = new Map<number, (typeof chartData)[number]>();
|
|
chartData.forEach((point) => {
|
|
map.set(point.hour ?? 0, point);
|
|
});
|
|
return Array.from({ length: 24 }, (_, hour) => {
|
|
const bucket = map.get(hour);
|
|
return {
|
|
label: `${hour.toString().padStart(2, "0")}:00`,
|
|
inputTokens: bucket?.inputTokens ?? 0,
|
|
outputTokens: bucket?.outputTokens ?? 0,
|
|
cost: bucket?.cost ?? 0,
|
|
};
|
|
});
|
|
})();
|
|
|
|
const displayData = isToday ? hourlyData : chartData;
|
|
|
|
const CustomTooltip = ({ active, payload, label }: any) => {
|
|
if (active && payload && payload.length) {
|
|
return (
|
|
<div className="rounded-lg border bg-background/95 p-3 shadow-lg backdrop-blur-md">
|
|
<p className="mb-2 font-medium">{label}</p>
|
|
{payload.map((entry: any, index: number) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center gap-2 text-sm"
|
|
style={{ color: entry.color }}
|
|
>
|
|
<div
|
|
className="h-2 w-2 rounded-full"
|
|
style={{ backgroundColor: entry.color }}
|
|
/>
|
|
<span className="font-medium">{entry.name}:</span>
|
|
<span>
|
|
{entry.name.includes(t("usage.cost", "成本"))
|
|
? `$${typeof entry.value === "number" ? entry.value.toFixed(6) : entry.value}`
|
|
: entry.value.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-xl border border-border/50 bg-card/40 p-6 backdrop-blur-sm">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold">
|
|
{t("usage.trends", "使用趋势")}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{isToday
|
|
? t("usage.rangeToday", "今天 (按小时)")
|
|
: days === 7
|
|
? t("usage.rangeLast7Days", "过去 7 天")
|
|
: t("usage.rangeLast30Days", "过去 30 天")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="h-[350px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart
|
|
data={displayData}
|
|
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
|
>
|
|
<defs>
|
|
<linearGradient id="colorInput" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.2} />
|
|
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
|
</linearGradient>
|
|
<linearGradient id="colorOutput" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.2} />
|
|
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid
|
|
strokeDasharray="3 3"
|
|
vertical={false}
|
|
stroke="hsl(var(--border))"
|
|
opacity={0.4}
|
|
/>
|
|
<XAxis
|
|
dataKey="label"
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 12 }}
|
|
dy={10}
|
|
/>
|
|
<YAxis
|
|
yAxisId="tokens"
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 12 }}
|
|
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
|
|
/>
|
|
<YAxis
|
|
yAxisId="cost"
|
|
orientation="right"
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 12 }}
|
|
tickFormatter={(value) => `$${value}`}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Legend />
|
|
<Area
|
|
yAxisId="tokens"
|
|
type="monotone"
|
|
dataKey="inputTokens"
|
|
name={t("usage.inputTokens", "输入 Tokens")}
|
|
stroke="#3b82f6"
|
|
fillOpacity={1}
|
|
fill="url(#colorInput)"
|
|
strokeWidth={2}
|
|
/>
|
|
<Area
|
|
yAxisId="tokens"
|
|
type="monotone"
|
|
dataKey="outputTokens"
|
|
name={t("usage.outputTokens", "输出 Tokens")}
|
|
stroke="#22c55e"
|
|
fillOpacity={1}
|
|
fill="url(#colorOutput)"
|
|
strokeWidth={2}
|
|
/>
|
|
<Area
|
|
yAxisId="cost"
|
|
type="monotone"
|
|
dataKey="cost"
|
|
name={t("usage.cost", "成本")}
|
|
stroke="#f43f5e"
|
|
fill="none"
|
|
strokeWidth={2}
|
|
strokeDasharray="4 4"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|