From 57f25cc4a42f236801b9161f0fe121b810340238 Mon Sep 17 00:00:00 2001 From: coso Date: Thu, 25 Dec 2025 00:33:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Gemini=20API=20Key=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E6=B1=A0=E5=8D=A1=E7=89=87=E5=BC=8F=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 gemini_api_key 从配置文件存储改为凭证池存储 - 支持使用次数、错误次数、最后使用时间统计 - 支持健康检查和启用/禁用切换 - 修复 CleanupDialog Modal 属性兼容问题 - 版本更新至 0.17.10 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/flow_monitor_cmd.rs | 121 ++++- src-tauri/tauri.conf.json | 2 +- src/components/flow-monitor/CleanupDialog.tsx | 485 ++++++++++++++++++ src/components/flow-monitor/index.ts | 1 + .../provider-pool/ProviderPoolPage.tsx | 63 +-- src/lib/api/flowMonitor.ts | 63 ++- src/pages/FlowMonitorPage.tsx | 28 + 10 files changed, 685 insertions(+), 84 deletions(-) create mode 100644 src/components/flow-monitor/CleanupDialog.tsx diff --git a/package.json b/package.json index 30c746510..41e8311fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.17.9", + "version": "0.17.10", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 414173bf4..730aa0fae 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3377,7 +3377,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.17.9" +version = "0.17.10" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9cbecd011..352a271b9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/src/commands/flow_monitor_cmd.rs b/src-tauri/src/commands/flow_monitor_cmd.rs index 72001eea4..fe232b018 100644 --- a/src-tauri/src/commands/flow_monitor_cmd.rs +++ b/src-tauri/src/commands/flow_monitor_cmd.rs @@ -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, + /// 保留小时数(清理此小时数之前的数据)- 仅当 cleanup_type 为 ByTime 时使用 + pub retention_hours: Option, + /// 保留的最大记录数 - 仅当 cleanup_type 为 ByCount 时使用 + pub max_records: Option, + /// 要清理的状态列表 - 仅当 cleanup_type 为 ByStatus 时使用 + pub target_states: Option>, + /// 要清理的 Provider 列表 - 仅当 cleanup_type 为 ByProvider 时使用 + pub target_providers: Option>, + /// 最大存储大小(字节)- 仅当 cleanup_type 为 BySize 时使用 + pub max_storage_bytes: Option, +} + +/// 清理类型 +#[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 { - // 计算清理时间点 - 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 { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9129e5afe..223e65aa5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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", diff --git a/src/components/flow-monitor/CleanupDialog.tsx b/src/components/flow-monitor/CleanupDialog.tsx new file mode 100644 index 000000000..60a1d19eb --- /dev/null +++ b/src/components/flow-monitor/CleanupDialog.tsx @@ -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("ByTime"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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(["Failed"]); + + // Provider清理选项 + const [targetProviders, setTargetProviders] = useState([]); + + // 大小清理选项 + 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 ( + + {/* 标题 */} +
+

清理日志

+
+ +
+ {/* 警告提示 */} +
+ +
+

+ 注意:清理操作不可撤销 +

+

+ 请确认清理条件,删除的日志数据无法恢复。 +

+
+
+ + {/* 清理类型选择 */} +
+ +
+ + + + + + + + + + + +
+
+ + {/* 清理选项配置 */} +
+ {cleanupType === "ByTime" && ( +
+
+ + +
+ + {useHours ? ( +
+ + setRetentionHours(Number(e.target.value))} + className="w-full" + /> +
+ 1小时 + 7天 +
+
+ ) : ( +
+ + setRetentionDays(Number(e.target.value))} + className="w-full" + /> +
+ 1天 + 1年 +
+
+ )} +
+ )} + + {cleanupType === "ByCount" && ( +
+ + setMaxRecords(Number(e.target.value))} + className="w-full" + /> +
+ 100条 + 10,000条 +
+
+ )} + + {cleanupType === "ByStatus" && ( +
+ +
+ {availableStates.map((state) => ( + + ))} +
+
+ )} + + {cleanupType === "ByProvider" && ( +
+ +
+ {availableProviders.map((provider) => ( + + ))} +
+
+ )} + + {cleanupType === "BySize" && ( +
+ + setMaxStorageGB(Number(e.target.value))} + className="w-full" + /> +
+ 100MB + 100GB +
+

+ 当存储超过此大小时,将删除最旧的数据 +

+
+ )} +
+ + {/* 错误显示 */} + {error && ( +
+ {error} +
+ )} + + {/* 操作按钮 */} +
+ + +
+
+
+ ); +} diff --git a/src/components/flow-monitor/index.ts b/src/components/flow-monitor/index.ts index 1af759ca6..f800b28a7 100644 --- a/src/components/flow-monitor/index.ts +++ b/src/components/flow-monitor/index.ts @@ -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"; diff --git a/src/components/provider-pool/ProviderPoolPage.tsx b/src/components/provider-pool/ProviderPoolPage.tsx index 8116dea46..a6c097bc1 100644 --- a/src/components/provider-pool/ProviderPoolPage.tsx +++ b/src/components/provider-pool/ProviderPoolPage.tsx @@ -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 = { const configTabLabels: Record = { 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( ); })} - {/* Gemini API Key - 配置文件存储但归类到 API Key */} - )} {activeCategory === "config" && ( @@ -542,33 +520,6 @@ export const ProviderPoolPage = forwardRef( ) : config ? (
- {activeTab === "gemini_api_key" && ( - <> - - setConfig({ - ...config, - credential_pool: { - ...config.credential_pool, - gemini_api_keys: entries, - }, - }) - } - /> - {(config.credential_pool?.gemini_api_keys?.length ?? 0) > - 0 && ( - - )} - - )} - {activeTab === "vertex" && ( <> { - // 后端期望 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 { + return invoke("cleanup_flows", { request }); }, /** diff --git a/src/pages/FlowMonitorPage.tsx b/src/pages/FlowMonitorPage.tsx index f81fa8e98..363bc8fa5 100644 --- a/src/pages/FlowMonitorPage.tsx +++ b/src/pages/FlowMonitorPage.tsx @@ -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([]); + // 清理对话框 + 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() { 导出 + {/* 清理按钮 */} + + {/* 刷新按钮 */}
); }