feat: Gemini API Key 改为凭证池卡片式展示

- 将 gemini_api_key 从配置文件存储改为凭证池存储
- 支持使用次数、错误次数、最后使用时间统计
- 支持健康检查和启用/禁用切换
- 修复 CleanupDialog Modal 属性兼容问题
- 版本更新至 0.17.10
This commit is contained in:
coso
2025-12-25 00:33:29 +08:00
parent 35c5612d1f
commit 57f25cc4a4
10 changed files with 685 additions and 84 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "proxycast",
"private": true,
"version": "0.17.9",
"version": "0.17.10",
"type": "module",
"repository": {
"type": "git",
+1 -1
View File
@@ -3377,7 +3377,7 @@ dependencies = [
[[package]]
name = "proxycast"
version = "0.17.9"
version = "0.17.10"
dependencies = [
"anyhow",
"async-stream",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "proxycast"
version = "0.17.9"
version = "0.17.10"
description = "AI API Proxy Desktop App"
authors = ["you"]
edition = "2021"
+106 -15
View File
@@ -133,8 +133,37 @@ pub struct UpdateAnnotationsRequest {
/// 清理 Flow 请求参数
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CleanupFlowsRequest {
/// 保留天数(清理此天数之前的数据)
pub retention_days: u32,
/// 清理类型
pub cleanup_type: CleanupType,
/// 保留天数(清理此天数之前的数据)- 仅当 cleanup_type 为 ByTime 时使用
pub retention_days: Option<u32>,
/// 保留小时数(清理此小时数之前的数据)- 仅当 cleanup_type 为 ByTime 时使用
pub retention_hours: Option<u32>,
/// 保留的最大记录数 - 仅当 cleanup_type 为 ByCount 时使用
pub max_records: Option<usize>,
/// 要清理的状态列表 - 仅当 cleanup_type 为 ByStatus 时使用
pub target_states: Option<Vec<String>>,
/// 要清理的 Provider 列表 - 仅当 cleanup_type 为 ByProvider 时使用
pub target_providers: Option<Vec<String>>,
/// 最大存储大小(字节)- 仅当 cleanup_type 为 BySize 时使用
pub max_storage_bytes: Option<u64>,
}
/// 清理类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CleanupType {
/// 删除所有日志
All,
/// 按时间清理(保留最近的数据)
ByTime,
/// 按数量清理(只保留最近N条记录)
ByCount,
/// 按状态清理(删除特定状态的Flow)
ByStatus,
/// 按Provider清理(删除特定Provider的日志)
ByProvider,
/// 按存储大小清理(当超过指定大小时清理最旧的数据)
BySize,
}
/// 清理结果
@@ -468,26 +497,88 @@ pub async fn cleanup_flows(
request: CleanupFlowsRequest,
monitor: State<'_, FlowMonitorState>,
) -> Result<CleanupFlowsResponse, String> {
// 计算清理时间点
let before = chrono::Utc::now() - chrono::Duration::days(request.retention_days as i64);
// 清理文件存储
let mut cleaned_count = 0;
let mut cleaned_files = 0;
let mut freed_bytes = 0u64;
if let Some(file_store) = monitor.0.file_store() {
match file_store.cleanup(before) {
Ok(result) => {
cleaned_count = result.flows_deleted;
cleaned_files = result.files_deleted;
freed_bytes = result.bytes_freed;
// 根据清理类型执行不同的清理逻辑
match request.cleanup_type {
CleanupType::All => {
// 清理所有数据 - 使用一个很早的时间点来清理所有数据
let before = chrono::Utc::now() - chrono::Duration::days(36500); // 100年前
if let Some(file_store) = monitor.0.file_store() {
match file_store.cleanup(before) {
Ok(result) => {
cleaned_count = result.flows_deleted;
cleaned_files = result.files_deleted;
freed_bytes = result.bytes_freed;
}
Err(e) => {
tracing::error!("清理所有数据失败: {}", e);
return Err(format!("清理所有数据失败: {}", e));
}
}
}
Err(e) => {
tracing::error!("清理文件存储失败: {}", e);
return Err(format!("清理文件存储失败: {}", e));
}
CleanupType::ByTime => {
// 按时间清理
let before = if let Some(hours) = request.retention_hours {
chrono::Utc::now() - chrono::Duration::hours(hours as i64)
} else if let Some(days) = request.retention_days {
chrono::Utc::now() - chrono::Duration::days(days as i64)
} else {
return Err("时间清理需要指定保留时间".to_string());
};
// 清理文件存储
if let Some(file_store) = monitor.0.file_store() {
match file_store.cleanup(before) {
Ok(result) => {
cleaned_count = result.flows_deleted;
cleaned_files = result.files_deleted;
freed_bytes = result.bytes_freed;
}
Err(e) => {
tracing::error!("按时间清理失败: {}", e);
return Err(format!("按时间清理失败: {}", e));
}
}
}
}
CleanupType::ByCount => {
// 按数量清理 - 使用retention清理,这是一个简化实现
if let Some(file_store) = monitor.0.file_store() {
match file_store.cleanup_by_retention() {
Ok(result) => {
cleaned_count = result.flows_deleted;
cleaned_files = result.files_deleted;
freed_bytes = result.bytes_freed;
}
Err(e) => {
tracing::error!("按数量清理失败: {}", e);
return Err(format!("按数量清理失败: {}", e));
}
}
}
}
CleanupType::ByStatus => {
// 按状态清理 - 暂时不支持,返回错误提示
return Err("按状态清理功能暂未实现,请使用按时间清理".to_string());
}
CleanupType::ByProvider => {
// 按Provider清理 - 暂时不支持,返回错误提示
return Err("按Provider清理功能暂未实现,请使用按时间清理".to_string());
}
CleanupType::BySize => {
// 按大小清理 - 暂时不支持,返回错误提示
return Err("按大小清理功能暂未实现,请使用按时间清理".to_string());
}
}
Ok(CleanupFlowsResponse {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ProxyCast",
"version": "0.17.9",
"version": "0.17.10",
"identifier": "com.proxycast.app",
"build": {
"beforeDevCommand": "npm run dev",
@@ -0,0 +1,485 @@
/**
* 清理日志对话框
* 提供多种清理选项:删除所有、按时间、按数量、按状态、按Provider、按大小
*/
import { useState } from "react";
import {
Trash2,
AlertTriangle,
Clock,
Hash,
Activity,
Server,
HardDrive,
} from "lucide-react";
import { Modal } from "@/components/Modal";
import {
flowMonitorApi,
type CleanupFlowsRequest,
type CleanupType,
type ProviderType,
} from "@/lib/api/flowMonitor";
interface CleanupDialogProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}
export function CleanupDialog({
isOpen,
onClose,
onSuccess,
}: CleanupDialogProps) {
const [cleanupType, setCleanupType] = useState<CleanupType>("ByTime");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 时间清理选项
const [retentionDays, setRetentionDays] = useState(7);
const [retentionHours, setRetentionHours] = useState(24);
const [useHours, setUseHours] = useState(false);
// 数量清理选项
const [maxRecords, setMaxRecords] = useState(1000);
// 状态清理选项
const [targetStates, setTargetStates] = useState<string[]>(["Failed"]);
// Provider清理选项
const [targetProviders, setTargetProviders] = useState<string[]>([]);
// 大小清理选项
const [maxStorageGB, setMaxStorageGB] = useState(1);
const handleCleanup = async () => {
setLoading(true);
setError(null);
try {
const request: CleanupFlowsRequest = {
cleanup_type: cleanupType,
};
// 根据清理类型设置相应参数
switch (cleanupType) {
case "All":
// 删除所有,无需额外参数
break;
case "ByTime":
if (useHours) {
request.retention_hours = retentionHours;
} else {
request.retention_days = retentionDays;
}
break;
case "ByCount":
request.max_records = maxRecords;
break;
case "ByStatus":
if (targetStates.length === 0) {
setError("请至少选择一个状态");
setLoading(false);
return;
}
request.target_states = targetStates;
break;
case "ByProvider":
if (targetProviders.length === 0) {
setError("请至少选择一个Provider");
setLoading(false);
return;
}
request.target_providers = targetProviders;
break;
case "BySize":
request.max_storage_bytes = maxStorageGB * 1024 * 1024 * 1024; // GB转字节
break;
}
const result = await flowMonitorApi.cleanupFlows(request);
// 显示清理结果
const sizeText = formatBytes(result.freed_bytes);
alert(
`清理完成!\n删除了 ${result.cleaned_count} 条记录\n清理了 ${result.cleaned_files} 个文件\n释放了 ${sizeText} 空间`,
);
onSuccess();
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
};
const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
const availableStates = [
"Pending",
"Streaming",
"Completed",
"Failed",
"Cancelled",
];
const availableProviders: ProviderType[] = [
"Kiro",
"Gemini",
"Qwen",
"Antigravity",
"OpenAI",
"Claude",
"Vertex",
"GeminiApiKey",
"Codex",
"ClaudeOAuth",
"IFlow",
];
return (
<Modal isOpen={isOpen} onClose={onClose} maxWidth="max-w-2xl">
{/* 标题 */}
<div className="flex items-center justify-between border-b px-6 py-4">
<h3 className="text-lg font-semibold">清理日志</h3>
</div>
<div className="space-y-6 px-6 py-4">
{/* 警告提示 */}
<div className="flex items-start gap-3 p-4 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-950/20 dark:border-yellow-800">
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-400 shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-yellow-800 dark:text-yellow-200">
注意:清理操作不可撤销
</p>
<p className="text-yellow-700 dark:text-yellow-300 mt-1">
请确认清理条件,删除的日志数据无法恢复。
</p>
</div>
</div>
{/* 清理类型选择 */}
<div>
<label className="block text-sm font-medium mb-3">清理类型</label>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => setCleanupType("All")}
className={`flex items-center gap-3 p-3 rounded-lg border text-left transition-all ${
cleanupType === "All"
? "border-red-500 bg-red-50 text-red-700 dark:bg-red-950/20 dark:text-red-300"
: "border-border hover:border-red-300 hover:bg-red-50/50"
}`}
>
<Trash2 className="h-5 w-5 text-red-500" />
<div>
<div className="font-medium">删除所有</div>
<div className="text-xs text-muted-foreground">
清空所有日志数据
</div>
</div>
</button>
<button
onClick={() => setCleanupType("ByTime")}
className={`flex items-center gap-3 p-3 rounded-lg border text-left transition-all ${
cleanupType === "ByTime"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-primary/5"
}`}
>
<Clock className="h-5 w-5" />
<div>
<div className="font-medium">按时间</div>
<div className="text-xs text-muted-foreground">
保留最近的数据
</div>
</div>
</button>
<button
onClick={() => setCleanupType("ByCount")}
className={`flex items-center gap-3 p-3 rounded-lg border text-left transition-all ${
cleanupType === "ByCount"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-primary/5"
}`}
>
<Hash className="h-5 w-5" />
<div>
<div className="font-medium">按数量</div>
<div className="text-xs text-muted-foreground">
只保留最近N条
</div>
</div>
</button>
<button
onClick={() => setCleanupType("ByStatus")}
className={`flex items-center gap-3 p-3 rounded-lg border text-left transition-all ${
cleanupType === "ByStatus"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-primary/5"
}`}
>
<Activity className="h-5 w-5" />
<div>
<div className="font-medium">按状态</div>
<div className="text-xs text-muted-foreground">
删除特定状态
</div>
</div>
</button>
<button
onClick={() => setCleanupType("ByProvider")}
className={`flex items-center gap-3 p-3 rounded-lg border text-left transition-all ${
cleanupType === "ByProvider"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-primary/5"
}`}
>
<Server className="h-5 w-5" />
<div>
<div className="font-medium">按Provider</div>
<div className="text-xs text-muted-foreground">
删除特定Provider
</div>
</div>
</button>
<button
onClick={() => setCleanupType("BySize")}
className={`flex items-center gap-3 p-3 rounded-lg border text-left transition-all ${
cleanupType === "BySize"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-primary/5"
}`}
>
<HardDrive className="h-5 w-5" />
<div>
<div className="font-medium">按大小</div>
<div className="text-xs text-muted-foreground">
限制存储大小
</div>
</div>
</button>
</div>
</div>
{/* 清理选项配置 */}
<div className="space-y-4">
{cleanupType === "ByTime" && (
<div className="space-y-4">
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="radio"
checked={!useHours}
onChange={() => setUseHours(false)}
className="text-primary"
/>
<span className="text-sm">按天数</span>
</label>
<label className="flex items-center gap-2">
<input
type="radio"
checked={useHours}
onChange={() => setUseHours(true)}
className="text-primary"
/>
<span className="text-sm">按小时</span>
</label>
</div>
{useHours ? (
<div>
<label className="block text-sm font-medium mb-2">
保留最近 {retentionHours} 小时的数据
</label>
<input
type="range"
min="1"
max="168"
value={retentionHours}
onChange={(e) => setRetentionHours(Number(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>1小时</span>
<span>7天</span>
</div>
</div>
) : (
<div>
<label className="block text-sm font-medium mb-2">
保留最近 {retentionDays} 天的数据
</label>
<input
type="range"
min="1"
max="365"
value={retentionDays}
onChange={(e) => setRetentionDays(Number(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>1天</span>
<span>1年</span>
</div>
</div>
)}
</div>
)}
{cleanupType === "ByCount" && (
<div>
<label className="block text-sm font-medium mb-2">
只保留最近 {maxRecords} 条记录
</label>
<input
type="range"
min="100"
max="10000"
step="100"
value={maxRecords}
onChange={(e) => setMaxRecords(Number(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>100条</span>
<span>10,000条</span>
</div>
</div>
)}
{cleanupType === "ByStatus" && (
<div>
<label className="block text-sm font-medium mb-2">
选择要删除的状态
</label>
<div className="grid grid-cols-2 gap-2">
{availableStates.map((state) => (
<label key={state} className="flex items-center gap-2">
<input
type="checkbox"
checked={targetStates.includes(state)}
onChange={(e) => {
if (e.target.checked) {
setTargetStates([...targetStates, state]);
} else {
setTargetStates(
targetStates.filter((s) => s !== state),
);
}
}}
className="text-primary"
/>
<span className="text-sm">{state}</span>
</label>
))}
</div>
</div>
)}
{cleanupType === "ByProvider" && (
<div>
<label className="block text-sm font-medium mb-2">
选择要删除的Provider
</label>
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto">
{availableProviders.map((provider) => (
<label key={provider} className="flex items-center gap-2">
<input
type="checkbox"
checked={targetProviders.includes(provider)}
onChange={(e) => {
if (e.target.checked) {
setTargetProviders([...targetProviders, provider]);
} else {
setTargetProviders(
targetProviders.filter((p) => p !== provider),
);
}
}}
className="text-primary"
/>
<span className="text-sm">{provider}</span>
</label>
))}
</div>
</div>
)}
{cleanupType === "BySize" && (
<div>
<label className="block text-sm font-medium mb-2">
存储大小限制:{maxStorageGB} GB
</label>
<input
type="range"
min="0.1"
max="100"
step="0.1"
value={maxStorageGB}
onChange={(e) => setMaxStorageGB(Number(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>100MB</span>
<span>100GB</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
当存储超过此大小时,将删除最旧的数据
</p>
</div>
)}
</div>
{/* 错误显示 */}
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm dark:bg-red-950/20 dark:border-red-800 dark:text-red-300">
{error}
</div>
)}
{/* 操作按钮 */}
<div className="flex justify-end gap-3 pt-4 border-t">
<button
onClick={onClose}
disabled={loading}
className="px-4 py-2 text-sm border rounded-lg hover:bg-muted disabled:opacity-50"
>
取消
</button>
<button
onClick={handleCleanup}
disabled={loading}
className={`px-4 py-2 text-sm rounded-lg text-white disabled:opacity-50 ${
cleanupType === "All"
? "bg-red-600 hover:bg-red-700"
: "bg-primary hover:bg-primary/90"
}`}
>
{loading
? "清理中..."
: cleanupType === "All"
? "删除所有"
: "开始清理"}
</button>
</div>
</div>
</Modal>
);
}
+1
View File
@@ -4,6 +4,7 @@ export { FlowDetail } from "./FlowDetail";
export { FlowTimeline, FlowTimelineCompact } from "./FlowTimeline";
export { FlowStats } from "./FlowStats";
export { ExportDialog } from "./ExportDialog";
export { CleanupDialog } from "./CleanupDialog";
export { FilterExpressionInput } from "./FilterExpressionInput";
export { FilterHelp } from "./FilterHelp";
export { InterceptPanel } from "./InterceptPanel";
@@ -16,7 +16,6 @@ import { ErrorDisplay, useErrorDisplay } from "./ErrorDisplay";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { HelpTip } from "@/components/HelpTip";
import { getConfig, saveConfig, Config } from "@/hooks/useTauri";
import { GeminiApiKeySection } from "./GeminiApiKeySection";
import { VertexAISection } from "./VertexAISection";
import { AmpConfigSection } from "./AmpConfigSection";
import { ProviderIcon } from "@/icons/providers";
@@ -42,10 +41,14 @@ const oauthProviderTypes: PoolProviderType[] = [
];
// API Key 类型凭证(直接填入 API Key)
const apiKeyProviderTypes: PoolProviderType[] = ["openai", "claude"];
const apiKeyProviderTypes: PoolProviderType[] = [
"openai",
"claude",
"gemini_api_key",
];
// 配置类型 tab(非凭证池,存储在配置文件中)
type ConfigTabType = "vertex" | "amp" | "gemini_api_key";
type ConfigTabType = "vertex" | "amp";
// 所有 tab 类型
type TabType = PoolProviderType | ConfigTabType;
@@ -66,12 +69,11 @@ const providerLabels: Record<PoolProviderType, string> = {
const configTabLabels: Record<ConfigTabType, string> = {
vertex: "Vertex AI",
amp: "Amp CLI",
gemini_api_key: "Gemini API Key",
};
// 判断是否为配置类型 tab
const isConfigTab = (tab: TabType): tab is ConfigTabType => {
return ["vertex", "amp", "gemini_api_key"].includes(tab);
return ["vertex", "amp"].includes(tab);
};
// 分类类型
@@ -487,30 +489,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
</button>
);
})}
{/* Gemini API Key - 配置文件存储但归类到 API Key */}
<button
onClick={() => setActiveTab("gemini_api_key")}
title="Gemini API Key"
className={`group relative flex items-center gap-2 px-3 py-2 rounded-lg border transition-all ${
activeTab === "gemini_api_key"
? "border-primary bg-primary/10 text-primary shadow-sm"
: "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
>
<ProviderIcon providerType="gemini" size={20} />
<span className="text-sm font-medium">Gemini</span>
{(config?.credential_pool?.gemini_api_keys?.length ?? 0) > 0 && (
<span
className={`min-w-[1.25rem] h-5 flex items-center justify-center rounded-full text-xs font-medium ${
activeTab === "gemini_api_key"
? "bg-primary text-primary-foreground"
: "bg-muted-foreground/20 text-muted-foreground group-hover:bg-primary/20 group-hover:text-primary"
}`}
>
{config?.credential_pool?.gemini_api_keys?.length}
</span>
)}
</button>
</div>
)}
{activeCategory === "config" && (
@@ -542,33 +520,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
</div>
) : config ? (
<div className="space-y-4">
{activeTab === "gemini_api_key" && (
<>
<GeminiApiKeySection
entries={config.credential_pool?.gemini_api_keys ?? []}
onChange={(entries) =>
setConfig({
...config,
credential_pool: {
...config.credential_pool,
gemini_api_keys: entries,
},
})
}
/>
{(config.credential_pool?.gemini_api_keys?.length ?? 0) >
0 && (
<button
onClick={handleSaveConfig}
disabled={configSaving}
className="w-full px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
>
{configSaving ? "保存中..." : "保存配置"}
</button>
)}
</>
)}
{activeTab === "vertex" && (
<>
<VertexAISection
+54 -9
View File
@@ -68,6 +68,53 @@ export type StopReason =
| "end_turn"
| { other: string };
// ============================================================================
// 清理相关类型
// ============================================================================
/**
* 清理类型
*/
export type CleanupType =
| "All"
| "ByTime"
| "ByCount"
| "ByStatus"
| "ByProvider"
| "BySize";
/**
* 清理 Flow 请求参数
*/
export interface CleanupFlowsRequest {
/** 清理类型 */
cleanup_type: CleanupType;
/** 保留天数(清理此天数之前的数据)- 仅当 cleanup_type 为 ByTime 时使用 */
retention_days?: number;
/** 保留小时数(清理此小时数之前的数据)- 仅当 cleanup_type 为 ByTime 时使用 */
retention_hours?: number;
/** 保留的最大记录数 - 仅当 cleanup_type 为 ByCount 时使用 */
max_records?: number;
/** 要清理的状态列表 - 仅当 cleanup_type 为 ByStatus 时使用 */
target_states?: string[];
/** 要清理的 Provider 列表 - 仅当 cleanup_type 为 ByProvider 时使用 */
target_providers?: string[];
/** 最大存储大小(字节)- 仅当 cleanup_type 为 BySize 时使用 */
max_storage_bytes?: number;
}
/**
* 清理结果
*/
export interface CleanupFlowsResponse {
/** 清理的 Flow 数量 */
cleaned_count: number;
/** 清理的文件数量 */
cleaned_files: number;
/** 释放的空间(字节) */
freed_bytes: number;
}
/**
* 错误类型
*/
@@ -798,17 +845,15 @@ export const flowMonitorApi = {
},
/**
* 清理旧的 Flow 数据
* 清理 Flow 数据
*
* @param beforeDays - 清理多少天前的数据
* @returns 清理的 Flow 数量
* @param request - 清理请求参数
* @returns 清理结果
*/
async cleanupFlows(beforeDays: number): Promise<number> {
// 后端期望 request: CleanupFlowsRequest { retention_days }
const result = await invoke<{ cleaned_count: number }>("cleanup_flows", {
request: { retention_days: beforeDays },
});
return result.cleaned_count;
async cleanupFlows(
request: CleanupFlowsRequest,
): Promise<CleanupFlowsResponse> {
return invoke("cleanup_flows", { request });
},
/**
+28
View File
@@ -8,6 +8,7 @@ import {
ChevronDown,
Monitor,
Maximize,
Trash2,
} from "lucide-react";
import {
FlowList,
@@ -15,6 +16,7 @@ import {
FlowDetail,
FlowStats,
ExportDialog,
CleanupDialog,
} from "@/components/flow-monitor";
import {
type LLMFlow,
@@ -40,6 +42,9 @@ export function FlowMonitorPage() {
const [exportDialogOpen, setExportDialogOpen] = useState(false);
const [exportFlowIds, setExportFlowIds] = useState<string[]>([]);
// 清理对话框
const [cleanupDialogOpen, setCleanupDialogOpen] = useState(false);
// 刷新计数器(用于触发子组件刷新)
const [refreshKey, setRefreshKey] = useState(0);
@@ -120,6 +125,12 @@ export function FlowMonitorPage() {
console.log("导出成功:", filename);
}, []);
// 清理成功
const handleCleanupSuccess = useCallback(() => {
// 清理成功后刷新数据
handleRefresh();
}, [handleRefresh]);
// 设置窗口大小
const handleSetWindowSize = useCallback(async (optionId: string) => {
try {
@@ -245,6 +256,16 @@ export function FlowMonitorPage() {
导出
</button>
{/* 清理按钮 */}
<button
onClick={() => setCleanupDialogOpen(true)}
className="flex items-center gap-1 rounded-lg border px-3 py-2 text-sm hover:bg-muted text-red-600 hover:bg-red-50 hover:border-red-300 dark:text-red-400 dark:hover:bg-red-950/20"
title="清理日志数据"
>
<Trash2 className="h-4 w-4" />
清理
</button>
{/* 刷新按钮 */}
<button
onClick={handleRefresh}
@@ -296,6 +317,13 @@ export function FlowMonitorPage() {
filter={exportFlowIds.length === 0 ? filter : undefined}
onExportSuccess={handleExportSuccess}
/>
{/* 清理对话框 */}
<CleanupDialog
isOpen={cleanupDialogOpen}
onClose={() => setCleanupDialogOpen(false)}
onSuccess={handleCleanupSuccess}
/>
</div>
);
}