mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: Gemini API Key 改为凭证池卡片式展示
- 将 gemini_api_key 从配置文件存储改为凭证池存储 - 支持使用次数、错误次数、最后使用时间统计 - 支持健康检查和启用/禁用切换 - 修复 CleanupDialog Modal 属性兼容问题 - 版本更新至 0.17.10
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"private": true,
|
||||
"version": "0.17.9",
|
||||
"version": "0.17.10",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Generated
+1
-1
@@ -3377,7 +3377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxycast"
|
||||
version = "0.17.9"
|
||||
version = "0.17.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user