refactor: 重大界面重构 - 删除仪表盘,重组导航结构

## 主要变更

### 1. 删除仪表盘功能
- 删除 Dashboard.tsx 组件和相关前端代码
- 删除后端 get_dashboard_data 命令和 DashboardData 结构
- 删除 getDashboardData API 和相关类型定义
- 删除不再使用的 MonitoringPage 组件

### 2. 重新设计导航结构
- API Server 现在是默认首页(替代仪表盘)
- 左侧导航栏顺序:API Server、凭证池、配置管理、Flow Monitor、设置
- 移除仪表盘菜单项,简化导航体验

### 3. 设置页面功能整合
- 将扩展功能(MCP、Prompts、Skills、Plugins)移到设置页面的"扩展"标签
- 将路由管理功能移到设置页面的"路由管理 (实验)"标签,标注为实验功能
- 创建 ExtensionsSettings.tsx 和 RoutingSettings.tsx 组件
- 保持所有功能完整性,只是重新组织了访问路径

### 4. 代码清理
- 清理前后端所有仪表盘相关代码
- 移除未使用的导入和组件引用
- 修复 TypeScript 类型错误

## 新的界面结构

```
左侧导航:
├─ API Server (首页)
├─ 凭证池
├─ 配置管理
├─ Flow Monitor
└─ 设置
   ├─ 通用
   ├─ 代理服务
   ├─ 安全
   ├─ 高级
   ├─ 扩展 (新增)
   ├─ 路由管理 (实验) (新增)
   └─ 关于
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
coso
2025-12-24 22:28:54 +08:00
co-authored by Claude Sonnet 4
parent e88c55da20
commit ca73d0aa1c
14 changed files with 153 additions and 1337 deletions
-52
View File
@@ -284,55 +284,3 @@ pub async fn get_token_stats_by_day(
let tokens = state.tokens.read();
Ok(tokens.by_day(days.unwrap_or(7)))
}
// ========== 仪表盘数据命令 ==========
/// 仪表盘数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardData {
/// 统计摘要
pub stats: StatsSummary,
/// Token 摘要
pub tokens: TokenStatsSummary,
/// 按 Provider 统计
pub by_provider: HashMap<String, ProviderStats>,
/// 最近请求
pub recent_logs: Vec<RequestLog>,
}
/// 获取仪表盘数据
#[tauri::command]
pub async fn get_dashboard_data(
state: tauri::State<'_, TelemetryState>,
) -> Result<DashboardData, String> {
// 获取最近 24 小时的统计
let range = Some(TimeRange::last_hours(24));
let stats_guard = state.stats.read();
let stats = stats_guard.summary(range);
let by_provider: HashMap<String, ProviderStats> = stats_guard
.by_provider(range)
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
drop(stats_guard);
let tokens_guard = state.tokens.read();
let tokens = tokens_guard.summary(
Some(Utc::now() - chrono::Duration::hours(24)),
Some(Utc::now()),
);
drop(tokens_guard);
// 获取最近 20 条日志
let mut recent_logs = state.logger.get_all();
recent_logs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
recent_logs.truncate(20);
Ok(DashboardData {
stats,
tokens,
by_provider,
recent_logs,
})
}
-1
View File
@@ -2042,7 +2042,6 @@ pub fn run() {
commands::telemetry_cmd::get_token_stats_by_provider,
commands::telemetry_cmd::get_token_stats_by_model,
commands::telemetry_cmd::get_token_stats_by_day,
commands::telemetry_cmd::get_dashboard_data,
// Injection commands
commands::injection_cmd::get_injection_config,
commands::injection_cmd::set_injection_enabled,
+2 -14
View File
@@ -1,27 +1,21 @@
import { useState, useEffect } from "react";
import { Sidebar } from "./components/Sidebar";
import { Dashboard } from "./components/Dashboard";
import { SettingsPage } from "./components/settings";
import { ApiServerPage } from "./components/api-server/ApiServerPage";
import { ProviderPoolPage } from "./components/provider-pool";
import { RoutingManagementPage } from "./components/routing/RoutingManagementPage";
import { ConfigManagementPage } from "./components/config/ConfigManagementPage";
import { ExtensionsPage } from "./components/extensions";
import { FlowMonitorPage } from "./pages";
import { flowEventManager } from "./lib/flowEventManager";
type Page =
| "dashboard"
| "provider-pool"
| "routing-management"
| "config-management"
| "extensions"
| "api-server"
| "flow-monitor"
| "settings";
function App() {
const [currentPage, setCurrentPage] = useState<Page>("dashboard");
const [currentPage, setCurrentPage] = useState<Page>("api-server");
// 在应用启动时初始化 Flow 事件订阅
useEffect(() => {
@@ -31,16 +25,10 @@ function App() {
const renderPage = () => {
switch (currentPage) {
case "dashboard":
return <Dashboard />;
case "provider-pool":
return <ProviderPoolPage />;
case "routing-management":
return <RoutingManagementPage />;
case "config-management":
return <ConfigManagementPage />;
case "extensions":
return <ExtensionsPage />;
case "api-server":
return <ApiServerPage />;
case "flow-monitor":
@@ -48,7 +36,7 @@ function App() {
case "settings":
return <SettingsPage />;
default:
return <Dashboard />;
return <ApiServerPage />;
}
};
-379
View File
@@ -1,379 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import {
Server,
Zap,
Clock,
Key,
Monitor,
Globe,
AlertCircle,
Coins,
RefreshCw,
LayoutDashboard,
Activity,
CheckCircle2,
} from "lucide-react";
import {
getServerStatus,
getConfig,
ServerStatus,
Config,
getDefaultProvider,
} from "@/hooks/useTauri";
import { useAllOAuthCredentials } from "@/hooks/useOAuthCredentials";
import { useProviderPool } from "@/hooks/useProviderPool";
import { TokenStats } from "./monitoring/TokenStats";
import {
getTokenStatsByDay,
getTokenStatsByProvider,
type PeriodTokenStats,
type ProviderTokenStats,
} from "@/lib/api/telemetry";
type TabType = "overview" | "tokens";
export function Dashboard() {
const [activeTab, setActiveTab] = useState<TabType>("overview");
const [status, setStatus] = useState<ServerStatus | null>(null);
const [config, setConfig] = useState<Config | null>(null);
const [defaultProvider, setDefaultProvider] = useState<string>("");
const { reload: reloadCredentials } = useAllOAuthCredentials();
const { refresh: refreshProviderPool } = useProviderPool();
// Token 数据状态
const [tokensByDay, setTokensByDay] = useState<PeriodTokenStats[]>([]);
const [tokensByProvider, setTokensByProvider] = useState<
Record<string, ProviderTokenStats>
>({});
const [monitoringLoading, setMonitoringLoading] = useState(false);
// 获取基础数据
useEffect(() => {
const fetchData = async () => {
try {
const [s, c, dp] = await Promise.all([
getServerStatus(),
getConfig(),
getDefaultProvider(),
]);
setStatus(s);
setConfig(c);
setDefaultProvider(dp);
} catch (e) {
console.error("Failed to fetch data:", e);
}
};
fetchData();
reloadCredentials();
refreshProviderPool();
const interval = setInterval(fetchData, 5000);
return () => clearInterval(interval);
}, [reloadCredentials, refreshProviderPool]);
// 获取Token数据
const fetchMonitoringData = useCallback(async () => {
try {
setMonitoringLoading(true);
const [dayStats, providerStats] = await Promise.all([
getTokenStatsByDay(7),
getTokenStatsByProvider({ preset: "7d" }),
]);
setTokensByDay(dayStats);
setTokensByProvider(providerStats);
} catch (e) {
console.error("Failed to fetch token data:", e);
} finally {
setMonitoringLoading(false);
}
}, []);
// 切换到Token标签时加载数据
useEffect(() => {
if (activeTab === "tokens") {
fetchMonitoringData();
}
}, [activeTab, fetchMonitoringData]);
// 定时刷新Token数据
useEffect(() => {
if (activeTab === "tokens") {
const interval = setInterval(fetchMonitoringData, 30000);
return () => clearInterval(interval);
}
}, [activeTab, fetchMonitoringData]);
const formatUptime = (secs: number) => {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
return `${h}h ${m}m`;
};
const serverUrl = status
? `http://${status.host}:${status.port}`
: "http://localhost:8999";
const getProviderName = (id: string) => {
switch (id) {
case "kiro":
return "Kiro Claude";
case "gemini":
return "Gemini CLI";
case "qwen":
return "通义千问";
case "openai":
return "OpenAI 自定义";
case "claude":
return "Claude 自定义";
default:
return id;
}
};
const tabs: { id: TabType; label: string; icon: React.ElementType }[] = [
{ id: "overview", label: "概览", icon: LayoutDashboard },
{ id: "tokens", label: "Token", icon: Coins },
];
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">仪表盘</h2>
<p className="text-muted-foreground">系统状态与监控</p>
</div>
<div className="flex gap-2">
{activeTab === "tokens" && (
<button
onClick={fetchMonitoringData}
disabled={monitoringLoading}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
>
<RefreshCw
className={`h-4 w-4 ${monitoringLoading ? "animate-spin" : ""}`}
/>
刷新
</button>
)}
</div>
</div>
{/* 标签页 */}
<div className="flex gap-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* 内容区域 */}
{activeTab === "overview" && (
<OverviewTab
status={status}
config={config}
defaultProvider={defaultProvider}
serverUrl={serverUrl}
formatUptime={formatUptime}
getProviderName={getProviderName}
/>
)}
{activeTab === "tokens" && (
<TokenStats
summary={{
total_input_tokens: 0,
total_output_tokens: 0,
total_tokens: 0,
record_count: 0,
actual_count: 0,
estimated_count: 0,
avg_input_tokens: 0,
avg_output_tokens: 0,
}}
byProvider={tokensByProvider}
byDay={tokensByDay}
/>
)}
{/* 加载状态 */}
{activeTab === "tokens" &&
monitoringLoading &&
Object.keys(tokensByProvider).length === 0 && (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)}
</div>
);
}
// 概览标签页内容
function OverviewTab({
status,
config,
defaultProvider,
serverUrl,
formatUptime,
getProviderName,
}: {
status: ServerStatus | null;
config: Config | null;
defaultProvider: string;
serverUrl: string;
formatUptime: (secs: number) => string;
getProviderName: (id: string) => string;
}) {
return (
<div className="space-y-6">
{/* Server Status Cards */}
<div className="grid grid-cols-4 gap-4">
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">服务状态</span>
</div>
<div className="mt-2 flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${status?.running ? "bg-green-500" : "bg-red-500"}`}
/>
<span className="font-medium">
{status?.running ? "运行中" : "已停止"}
</span>
</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2">
<Zap className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">请求数</span>
</div>
<div className="mt-2 text-2xl font-bold">{status?.requests || 0}</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">运行时间</span>
</div>
<div className="mt-2 font-medium">
{formatUptime(status?.uptime_secs || 0)}
</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2">
<Server className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">默认 Provider</span>
</div>
<div className="mt-2 font-medium">
{defaultProvider ? getProviderName(defaultProvider) : "加载中..."}
</div>
</div>
</div>
{/* Quick Links */}
<div className="grid grid-cols-3 gap-4">
<QuickLinkCard
icon={Key}
title="凭证管理"
description="管理 OAuth 凭证"
status="info"
statusText="Provider Pool 管理"
/>
<QuickLinkCard
icon={Monitor}
title="配置切换"
description="一键切换 Claude Code/Codex/Gemini CLI 的 API 配置"
status="info"
statusText="管理 Provider 配置"
/>
<QuickLinkCard
icon={Globe}
title="API Server"
description={`${serverUrl}`}
status={status?.running ? "success" : "warning"}
statusText={status?.running ? "运行中" : "已停止"}
/>
</div>
{/* Server Info */}
{config && (
<div className="rounded-lg border bg-card p-6">
<h3 className="mb-4 font-semibold">服务器信息</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">API 地址:</span>
<code className="ml-2 rounded bg-muted px-2 py-1">
{serverUrl}
</code>
</div>
<div>
<span className="text-muted-foreground">API Key:</span>
<code className="ml-2 rounded bg-muted px-2 py-1">
{config.server.api_key.length > 8
? `${config.server.api_key.slice(0, 4)}****${config.server.api_key.slice(-4)}`
: "****"}
</code>
</div>
</div>
</div>
)}
</div>
);
}
function QuickLinkCard({
icon: Icon,
title,
description,
status,
statusText,
}: {
icon: React.ElementType;
title: string;
description: string;
status: "success" | "warning" | "error" | "info";
statusText: string;
}) {
const statusColors = {
success: "text-green-600",
warning: "text-yellow-600",
error: "text-red-600",
info: "text-blue-600",
};
const StatusIcon = status === "success" ? CheckCircle2 : AlertCircle;
return (
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-3 mb-2">
<div className="rounded-lg bg-primary/10 p-2">
<Icon className="h-5 w-5 text-primary" />
</div>
<div>
<h4 className="font-medium">{title}</h4>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
<div
className={`flex items-center gap-1 text-xs ${statusColors[status]}`}
>
<StatusIcon className="h-3 w-3" />
{statusText}
</div>
</div>
);
}
+3 -18
View File
@@ -1,21 +1,9 @@
import {
LayoutDashboard,
Settings,
Globe,
Database,
Route,
FileCode,
Puzzle,
Activity,
} from "lucide-react";
import { Settings, Globe, Database, FileCode, Activity } from "lucide-react";
import { cn } from "@/lib/utils";
type Page =
| "dashboard"
| "provider-pool"
| "routing-management"
| "config-management"
| "extensions"
| "api-server"
| "flow-monitor"
| "settings";
@@ -26,12 +14,9 @@ interface SidebarProps {
}
const navItems = [
{ id: "dashboard" as Page, label: "仪表盘", icon: LayoutDashboard },
{ id: "provider-pool" as Page, label: "凭证池", icon: Database },
{ id: "routing-management" as Page, label: "路由管理", icon: Route },
{ id: "config-management" as Page, label: "配置管理", icon: FileCode },
{ id: "extensions" as Page, label: "扩展", icon: Puzzle },
{ id: "api-server" as Page, label: "API Server", icon: Globe },
{ id: "provider-pool" as Page, label: "凭证池", icon: Database },
{ id: "config-management" as Page, label: "配置管理", icon: FileCode },
{ id: "flow-monitor" as Page, label: "Flow Monitor", icon: Activity },
{ id: "settings" as Page, label: "设置", icon: Settings },
];
-278
View File
@@ -1,278 +0,0 @@
import React, { useState } from "react";
import {
FileText,
CheckCircle2,
XCircle,
Clock,
AlertTriangle,
ChevronDown,
ChevronRight,
Trash2,
Filter,
} from "lucide-react";
import type { RequestLog, RequestStatus } from "@/lib/api/telemetry";
interface LogViewerProps {
logs: RequestLog[];
onClear: () => void;
onFilter?: (filter: { provider?: string; status?: RequestStatus }) => void;
}
export function LogViewer({ logs, onClear, onFilter }: LogViewerProps) {
const [expandedId, setExpandedId] = useState<string | null>(null);
const [filterProvider, setFilterProvider] = useState<string>("");
const [filterStatus, setFilterStatus] = useState<string>("");
const getStatusIcon = (status: RequestStatus) => {
switch (status) {
case "success":
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case "failed":
return <XCircle className="h-4 w-4 text-red-500" />;
case "timeout":
return <Clock className="h-4 w-4 text-yellow-500" />;
case "retrying":
return <AlertTriangle className="h-4 w-4 text-orange-500" />;
case "cancelled":
return <XCircle className="h-4 w-4 text-gray-500" />;
}
};
const getStatusText = (status: RequestStatus) => {
const texts: Record<RequestStatus, string> = {
success: "成功",
failed: "失败",
timeout: "超时",
retrying: "重试中",
cancelled: "已取消",
};
return texts[status];
};
const getProviderName = (id: string) => {
const names: Record<string, string> = {
kiro: "Kiro",
gemini: "Gemini",
qwen: "Qwen",
openai: "OpenAI",
claude: "Claude",
antigravity: "Antigravity",
};
return names[id] || id;
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
return date.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
const formatDuration = (ms: number) => {
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(2)}s`;
};
const handleFilterChange = () => {
onFilter?.({
provider: filterProvider || undefined,
status: (filterStatus as RequestStatus) || undefined,
});
};
// 获取唯一的 providers
const providers = [...new Set(logs.map((l) => l.provider))];
return (
<div className="space-y-4">
{/* 过滤器和操作 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<select
className="rounded border bg-background px-2 py-1 text-sm"
value={filterProvider}
onChange={(e) => {
setFilterProvider(e.target.value);
handleFilterChange();
}}
>
<option value="">所有 Provider</option>
{providers.map((p) => (
<option key={p} value={p}>
{getProviderName(p)}
</option>
))}
</select>
<select
className="rounded border bg-background px-2 py-1 text-sm"
value={filterStatus}
onChange={(e) => {
setFilterStatus(e.target.value);
handleFilterChange();
}}
>
<option value="">所有状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
<option value="timeout">超时</option>
</select>
</div>
<button
onClick={onClear}
className="flex items-center gap-1 rounded px-2 py-1 text-sm text-muted-foreground hover:bg-muted"
>
<Trash2 className="h-3 w-3" />
清空日志
</button>
</div>
{/* 日志列表 */}
<div className="rounded-lg border bg-card">
<div className="border-b px-4 py-2 flex items-center gap-2">
<FileText className="h-4 w-4" />
<span className="font-medium">请求日志</span>
<span className="text-sm text-muted-foreground">
({logs.length} 条)
</span>
</div>
{logs.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
暂无请求日志
</div>
) : (
<div className="divide-y max-h-[500px] overflow-y-auto">
{logs.map((log) => (
<LogEntry
key={log.id}
log={log}
expanded={expandedId === log.id}
onToggle={() =>
setExpandedId(expandedId === log.id ? null : log.id)
}
getStatusIcon={getStatusIcon}
getStatusText={getStatusText}
getProviderName={getProviderName}
formatTime={formatTime}
formatDuration={formatDuration}
/>
))}
</div>
)}
</div>
</div>
);
}
function LogEntry({
log,
expanded,
onToggle,
getStatusIcon,
getStatusText,
getProviderName,
formatTime,
formatDuration,
}: {
log: RequestLog;
expanded: boolean;
onToggle: () => void;
getStatusIcon: (status: RequestStatus) => React.ReactNode;
getStatusText: (status: RequestStatus) => string;
getProviderName: (id: string) => string;
formatTime: (timestamp: string) => string;
formatDuration: (ms: number) => string;
}) {
return (
<div className="hover:bg-muted/50">
<div
className="flex items-center gap-3 px-4 py-2 cursor-pointer"
onClick={onToggle}
>
{expanded ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
{getStatusIcon(log.status)}
<span className="text-xs text-muted-foreground w-32">
{formatTime(log.timestamp)}
</span>
<span className="font-medium w-20">
{getProviderName(log.provider)}
</span>
<span className="text-sm flex-1 truncate">{log.model}</span>
<span className="text-sm text-muted-foreground w-16 text-right">
{formatDuration(log.duration_ms)}
</span>
{log.total_tokens && (
<span className="text-xs text-muted-foreground w-20 text-right">
{log.total_tokens} tokens
</span>
)}
</div>
{expanded && (
<div className="px-4 pb-3 pl-12 space-y-2 text-sm">
<div className="grid grid-cols-2 gap-4 rounded bg-muted/50 p-3">
<div>
<span className="text-muted-foreground">状态:</span>{" "}
<span
className={
log.status === "success" ? "text-green-600" : "text-red-600"
}
>
{getStatusText(log.status)}
</span>
{log.http_status && (
<span className="ml-2 text-muted-foreground">
(HTTP {log.http_status})
</span>
)}
</div>
<div>
<span className="text-muted-foreground">流式:</span>{" "}
{log.is_streaming ? "是" : "否"}
</div>
{log.input_tokens !== undefined && (
<div>
<span className="text-muted-foreground">输入 Token:</span>{" "}
{log.input_tokens}
</div>
)}
{log.output_tokens !== undefined && (
<div>
<span className="text-muted-foreground">输出 Token:</span>{" "}
{log.output_tokens}
</div>
)}
{log.retry_count > 0 && (
<div>
<span className="text-muted-foreground">重试次数:</span>{" "}
{log.retry_count}
</div>
)}
{log.credential_id && (
<div>
<span className="text-muted-foreground">凭证 ID:</span>{" "}
<code className="text-xs">
{log.credential_id.slice(0, 8)}...
</code>
</div>
)}
</div>
{log.error_message && (
<div className="rounded bg-red-50 dark:bg-red-950/20 p-2 text-red-600 dark:text-red-400">
<span className="font-medium">错误:</span> {log.error_message}
</div>
)}
</div>
)}
</div>
);
}
@@ -1,173 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import { Activity, FileText, Coins, RefreshCw } from "lucide-react";
import { StatsOverview } from "./StatsOverview";
import { LogViewer } from "./LogViewer";
import { TokenStats } from "./TokenStats";
import {
getDashboardData,
getRequestLogs,
clearRequestLogs,
getTokenStatsByDay,
getTokenStatsByProvider,
type DashboardData,
type RequestLog,
type PeriodTokenStats,
type ProviderTokenStats,
type RequestStatus,
} from "@/lib/api/telemetry";
type TabType = "overview" | "logs" | "tokens";
export function MonitoringPage() {
const [activeTab, setActiveTab] = useState<TabType>("overview");
const [dashboardData, setDashboardData] = useState<DashboardData | null>(
null,
);
const [logs, setLogs] = useState<RequestLog[]>([]);
const [tokensByDay, setTokensByDay] = useState<PeriodTokenStats[]>([]);
const [tokensByProvider, setTokensByProvider] = useState<
Record<string, ProviderTokenStats>
>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const [dashboard, logsData, dayStats, providerStats] = await Promise.all([
getDashboardData(),
getRequestLogs({ limit: 100 }),
getTokenStatsByDay(7),
getTokenStatsByProvider({ preset: "7d" }),
]);
setDashboardData(dashboard);
setLogs(logsData);
setTokensByDay(dayStats);
setTokensByProvider(providerStats);
} catch (e) {
console.error("Failed to fetch monitoring data:", e);
setError(e instanceof Error ? e.message : "加载数据失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
// 每 30 秒刷新一次
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [fetchData]);
const handleClearLogs = async () => {
try {
await clearRequestLogs();
setLogs([]);
} catch (e) {
console.error("Failed to clear logs:", e);
}
};
const handleFilterLogs = async (filter: {
provider?: string;
status?: RequestStatus;
}) => {
try {
const filteredLogs = await getRequestLogs({
...filter,
limit: 100,
});
setLogs(filteredLogs);
} catch (e) {
console.error("Failed to filter logs:", e);
}
};
const tabs: { id: TabType; label: string; icon: React.ElementType }[] = [
{ id: "overview", label: "统计概览", icon: Activity },
{ id: "logs", label: "请求日志", icon: FileText },
{ id: "tokens", label: "Token 统计", icon: Coins },
];
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">监控中心</h2>
<p className="text-muted-foreground">
请求统计、日志和 Token 使用情况
</p>
</div>
<button
onClick={fetchData}
disabled={loading}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
刷新
</button>
</div>
{/* 标签页 */}
<div className="flex gap-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* 错误提示 */}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 dark:bg-red-950/20 p-4 text-red-600 dark:text-red-400">
{error}
</div>
)}
{/* 内容区域 */}
{loading && !dashboardData ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
{activeTab === "overview" && dashboardData && (
<StatsOverview
stats={dashboardData.stats}
byProvider={dashboardData.by_provider}
/>
)}
{activeTab === "logs" && (
<LogViewer
logs={logs}
onClear={handleClearLogs}
onFilter={handleFilterLogs}
/>
)}
{activeTab === "tokens" && dashboardData && (
<TokenStats
summary={dashboardData.tokens}
byProvider={tokensByProvider}
byDay={tokensByDay}
/>
)}
</>
)}
</div>
);
}
-178
View File
@@ -1,178 +0,0 @@
import React from "react";
import {
Activity,
CheckCircle2,
XCircle,
Clock,
Zap,
TrendingUp,
} from "lucide-react";
import type { StatsSummary, ProviderStats } from "@/lib/api/telemetry";
interface StatsOverviewProps {
stats: StatsSummary;
byProvider: Record<string, ProviderStats>;
}
export function StatsOverview({ stats, byProvider }: StatsOverviewProps) {
const formatLatency = (ms: number) => {
if (ms < 1000) return `${Math.round(ms)}ms`;
return `${(ms / 1000).toFixed(2)}s`;
};
const formatRate = (rate: number) => `${(rate * 100).toFixed(1)}%`;
const getProviderName = (id: string) => {
const names: Record<string, string> = {
kiro: "Kiro Claude",
gemini: "Gemini",
qwen: "通义千问",
openai: "OpenAI",
claude: "Claude",
antigravity: "Antigravity",
};
return names[id] || id;
};
return (
<div className="space-y-6">
{/* 总体统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<StatCard
icon={Activity}
label="总请求数"
value={stats.total_requests.toString()}
subValue={`成功 ${stats.successful_requests}`}
/>
<StatCard
icon={CheckCircle2}
label="成功率"
value={formatRate(stats.success_rate)}
subValue={`失败 ${stats.failed_requests}`}
valueColor={
stats.success_rate >= 0.9
? "text-green-600"
: stats.success_rate >= 0.7
? "text-yellow-600"
: "text-red-600"
}
/>
<StatCard
icon={Clock}
label="平均延迟"
value={formatLatency(stats.avg_latency_ms)}
subValue={
stats.max_latency_ms
? `最大 ${formatLatency(stats.max_latency_ms)}`
: undefined
}
/>
<StatCard
icon={Zap}
label="总 Token"
value={formatNumber(stats.total_tokens)}
subValue={`输入 ${formatNumber(stats.total_input_tokens)} / 输出 ${formatNumber(stats.total_output_tokens)}`}
/>
</div>
{/* 按 Provider 统计 */}
{Object.keys(byProvider).length > 0 && (
<div className="rounded-lg border bg-card p-4">
<h3 className="mb-4 font-semibold flex items-center gap-2">
<TrendingUp className="h-4 w-4" />按 Provider 统计
</h3>
<div className="space-y-3">
{Object.entries(byProvider).map(([provider, providerStats]) => (
<ProviderStatRow
key={provider}
name={getProviderName(provider)}
stats={providerStats}
/>
))}
</div>
</div>
)}
</div>
);
}
function StatCard({
icon: Icon,
label,
value,
subValue,
valueColor = "text-foreground",
}: {
icon: React.ElementType;
label: string;
value: string;
subValue?: string;
valueColor?: string;
}) {
return (
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{label}</span>
</div>
<div className={`mt-2 text-2xl font-bold ${valueColor}`}>{value}</div>
{subValue && (
<div className="mt-1 text-xs text-muted-foreground">{subValue}</div>
)}
</div>
);
}
function ProviderStatRow({
name,
stats,
}: {
name: string;
stats: ProviderStats;
}) {
const successRate = stats.success_rate * 100;
return (
<div className="flex items-center justify-between rounded-lg bg-muted/50 p-3">
<div className="flex items-center gap-3">
<div className="font-medium">{name}</div>
<div className="text-sm text-muted-foreground">
{stats.total_requests} 请求
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<CheckCircle2 className="h-3 w-3 text-green-500" />
<span className="text-sm">{stats.successful_requests}</span>
</div>
<div className="flex items-center gap-1">
<XCircle className="h-3 w-3 text-red-500" />
<span className="text-sm">{stats.failed_requests}</span>
</div>
<div className="w-20">
<div className="h-2 rounded-full bg-muted">
<div
className={`h-2 rounded-full ${
successRate >= 90
? "bg-green-500"
: successRate >= 70
? "bg-yellow-500"
: "bg-red-500"
}`}
style={{ width: `${successRate}%` }}
/>
</div>
</div>
<span className="text-sm font-medium w-12 text-right">
{successRate.toFixed(0)}%
</span>
</div>
</div>
);
}
function formatNumber(num: number): string {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num.toString();
}
-226
View File
@@ -1,226 +0,0 @@
import React from "react";
import { Coins, TrendingUp, BarChart3 } from "lucide-react";
import type {
TokenStatsSummary,
ProviderTokenStats,
PeriodTokenStats,
} from "@/lib/api/telemetry";
interface TokenStatsProps {
summary: TokenStatsSummary;
byProvider: Record<string, ProviderTokenStats>;
byDay: PeriodTokenStats[];
}
export function TokenStats({ summary, byProvider, byDay }: TokenStatsProps) {
const getProviderName = (id: string) => {
const names: Record<string, string> = {
kiro: "Kiro Claude",
gemini: "Gemini",
qwen: "通义千问",
openai: "OpenAI",
claude: "Claude",
antigravity: "Antigravity",
};
return names[id] || id;
};
const formatNumber = (num: number): string => {
if (num >= 1000000) return `${(num / 1000000).toFixed(2)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num.toString();
};
const formatDate = (dateStr?: string) => {
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleDateString("zh-CN", {
month: "2-digit",
day: "2-digit",
});
};
// 计算每日最大值用于图表缩放
const maxDayTokens = Math.max(...byDay.map((d) => d.total_tokens), 1);
return (
<div className="space-y-6">
{/* Token 总览 */}
<div className="grid grid-cols-4 gap-4">
<TokenCard
icon={Coins}
label="总 Token"
value={formatNumber(summary.total_tokens)}
subValue={`${summary.record_count} 条记录`}
/>
<TokenCard
icon={TrendingUp}
label="输入 Token"
value={formatNumber(summary.total_input_tokens)}
subValue={`平均 ${Math.round(summary.avg_input_tokens)}/请求`}
/>
<TokenCard
icon={TrendingUp}
label="输出 Token"
value={formatNumber(summary.total_output_tokens)}
subValue={`平均 ${Math.round(summary.avg_output_tokens)}/请求`}
/>
<TokenCard
icon={BarChart3}
label="数据来源"
value={`${summary.actual_count}/${summary.record_count}`}
subValue={`实际值 / 估算值 ${summary.estimated_count}`}
/>
</div>
{/* 按 Provider 统计 */}
{Object.keys(byProvider).length > 0 && (
<div className="rounded-lg border bg-card p-4">
<h3 className="mb-4 font-semibold flex items-center gap-2">
<Coins className="h-4 w-4" />按 Provider Token 使用
</h3>
<div className="space-y-3">
{Object.entries(byProvider).map(([provider, stats]) => (
<ProviderTokenRow
key={provider}
name={getProviderName(provider)}
stats={stats}
totalTokens={summary.total_tokens}
/>
))}
</div>
</div>
)}
{/* 每日趋势 */}
{byDay.length > 0 && (
<div className="rounded-lg border bg-card p-4">
<h3 className="mb-4 font-semibold flex items-center gap-2">
<BarChart3 className="h-4 w-4" />
每日 Token 使用趋势
</h3>
<div className="flex items-end gap-1 h-32">
{byDay
.slice()
.reverse()
.map((day, index) => (
<DayBar
key={index}
date={formatDate(day.period_start)}
tokens={day.total_tokens}
maxTokens={maxDayTokens}
/>
))}
</div>
</div>
)}
</div>
);
}
function TokenCard({
icon: Icon,
label,
value,
subValue,
}: {
icon: React.ElementType;
label: string;
value: string;
subValue?: string;
}) {
return (
<div className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{label}</span>
</div>
<div className="mt-2 text-2xl font-bold">{value}</div>
{subValue && (
<div className="mt-1 text-xs text-muted-foreground">{subValue}</div>
)}
</div>
);
}
function ProviderTokenRow({
name,
stats,
totalTokens,
}: {
name: string;
stats: ProviderTokenStats;
totalTokens: number;
}) {
const percentage =
totalTokens > 0 ? (stats.total_tokens / totalTokens) * 100 : 0;
const formatNumber = (num: number): string => {
if (num >= 1000000) return `${(num / 1000000).toFixed(2)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num.toString();
};
return (
<div className="flex items-center justify-between rounded-lg bg-muted/50 p-3">
<div className="flex items-center gap-3">
<div className="font-medium">{name}</div>
<div className="text-sm text-muted-foreground">
{stats.record_count} 条记录
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-sm">
<span className="text-muted-foreground">输入:</span>{" "}
{formatNumber(stats.total_input_tokens)}
</div>
<div className="text-sm">
<span className="text-muted-foreground">输出:</span>{" "}
{formatNumber(stats.total_output_tokens)}
</div>
<div className="w-24">
<div className="h-2 rounded-full bg-muted">
<div
className="h-2 rounded-full bg-primary"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
<span className="text-sm font-medium w-16 text-right">
{formatNumber(stats.total_tokens)}
</span>
</div>
</div>
);
}
function DayBar({
date,
tokens,
maxTokens,
}: {
date: string;
tokens: number;
maxTokens: number;
}) {
const height = maxTokens > 0 ? (tokens / maxTokens) * 100 : 0;
const formatNumber = (num: number): string => {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
return num.toString();
};
return (
<div className="flex-1 flex flex-col items-center gap-1">
<div className="w-full flex items-end justify-center h-24">
<div
className="w-full max-w-8 bg-primary/80 rounded-t hover:bg-primary transition-colors"
style={{ height: `${Math.max(height, 2)}%` }}
title={`${formatNumber(tokens)} tokens`}
/>
</div>
<span className="text-xs text-muted-foreground">{date}</span>
</div>
);
}
-4
View File
@@ -1,4 +0,0 @@
export { MonitoringPage } from "./MonitoringPage";
export { StatsOverview } from "./StatsOverview";
export { LogViewer } from "./LogViewer";
export { TokenStats } from "./TokenStats";
@@ -0,0 +1,58 @@
import { useState } from "react";
import { Plug, MessageSquare, Boxes, Puzzle } from "lucide-react";
import { cn } from "@/lib/utils";
import { McpPage } from "../mcp/McpPage";
import { PromptsPage } from "../prompts/PromptsPage";
import { SkillsPage } from "../skills/SkillsPage";
import { PluginManager } from "../plugins/PluginManager";
type Tab = "mcp" | "prompts" | "skills" | "plugins";
const tabs = [
{ id: "mcp" as Tab, label: "MCP", icon: Plug },
{ id: "prompts" as Tab, label: "Prompts", icon: MessageSquare },
{ id: "skills" as Tab, label: "Skills", icon: Boxes },
{ id: "plugins" as Tab, label: "Plugins", icon: Puzzle },
];
export function ExtensionsSettings() {
const [activeTab, setActiveTab] = useState<Tab>("mcp");
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold">扩展管理</h3>
<p className="text-muted-foreground">
管理 MCP 服务器、Prompts 和 Skills
</p>
</div>
{/* Tab 切换 */}
<div className="flex gap-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Tab 内容 */}
<div className="pt-2">
{activeTab === "mcp" && <McpPage hideHeader />}
{activeTab === "prompts" && <PromptsPage hideHeader />}
{activeTab === "skills" && <SkillsPage hideHeader />}
{activeTab === "plugins" && <PluginManager />}
</div>
</div>
);
}
@@ -0,0 +1,76 @@
import { useState } from "react";
import { Route, Shield, AlertTriangle } from "lucide-react";
import { cn } from "@/lib/utils";
import { RoutingPage } from "../routing/RoutingPage";
import { ResiliencePage } from "../resilience/ResiliencePage";
type Tab = "routing" | "resilience";
const tabs = [
{ id: "routing" as Tab, label: "智能路由", icon: Route },
{ id: "resilience" as Tab, label: "容错配置", icon: Shield },
];
export function RoutingSettings() {
const [activeTab, setActiveTab] = useState<Tab>("routing");
return (
<div className="space-y-6">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold">路由管理</h3>
<div className="flex items-center gap-1 px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-md">
<AlertTriangle className="h-3 w-3" />
实验功能
</div>
</div>
<p className="text-muted-foreground">
配置智能路由规则和容错策略(实验性功能,可能不稳定)
</p>
</div>
{/* Tab 切换 */}
<div className="flex gap-1 border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === tab.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Tab 内容 */}
<div className="pt-2">
{activeTab === "routing" && <RoutingPageContent />}
{activeTab === "resilience" && <ResiliencePageContent />}
</div>
</div>
);
}
// 路由页面内容(去掉标题)
function RoutingPageContent() {
return (
<div className="routing-content">
<RoutingPage hideHeader />
</div>
);
}
// 容错页面内容(去掉标题)
function ResiliencePageContent() {
return (
<div className="resilience-content">
<ResiliencePage hideHeader />
</div>
);
}
+14 -1
View File
@@ -7,14 +7,25 @@ import { AboutSection } from "./AboutSection";
import { TlsSettings } from "./TlsSettings";
import { QuotaSettings } from "./QuotaSettings";
import { RemoteManagementSettings } from "./RemoteManagementSettings";
import { ExtensionsSettings } from "./ExtensionsSettings";
import { RoutingSettings } from "./RoutingSettings";
type SettingsTab = "general" | "proxy" | "security" | "advanced" | "about";
type SettingsTab =
| "general"
| "proxy"
| "security"
| "advanced"
| "extensions"
| "routing"
| "about";
const tabs: { id: SettingsTab; label: string }[] = [
{ id: "general", label: "通用" },
{ id: "proxy", label: "代理服务" },
{ id: "security", label: "安全" },
{ id: "advanced", label: "高级" },
{ id: "extensions", label: "扩展" },
{ id: "routing", label: "路由管理 (实验)" },
{ id: "about", label: "关于" },
];
@@ -65,6 +76,8 @@ export function SettingsPage() {
<QuotaSettings />
</div>
)}
{activeTab === "extensions" && <ExtensionsSettings />}
{activeTab === "routing" && <RoutingSettings />}
{activeTab === "about" && <AboutSection />}
</div>
</div>
-13
View File
@@ -124,13 +124,6 @@ export interface TimeRangeParam {
preset?: "1h" | "24h" | "7d" | "30d";
}
export interface DashboardData {
stats: StatsSummary;
tokens: TokenStatsSummary;
by_provider: Record<string, ProviderStats>;
recent_logs: RequestLog[];
}
// ========== 请求日志 API ==========
export async function getRequestLogs(params?: {
@@ -197,9 +190,3 @@ export async function getTokenStatsByDay(
): Promise<PeriodTokenStats[]> {
return invoke("get_token_stats_by_day", { days });
}
// ========== 仪表盘 API ==========
export async function getDashboardData(): Promise<DashboardData> {
return invoke("get_dashboard_data");
}