From 3ca4bfe7f617ca736cf08e55e5e4eef53a2e36d6 Mon Sep 17 00:00:00 2001 From: Chiron <598621670@qq.com> Date: Sat, 10 Jan 2026 22:27:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Flow=20Monitor=20?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=86=85=E5=AE=B9=E6=98=BE=E7=A4=BA=E5=92=8C?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=95=86=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复凭证池模式下非流式响应的 content 和 body 字段未保存问题 - 从响应 JSON 中提取 content、tool_calls 和 usage 信息 - 添加 provider_id 字段用于显示实际的提供商名称 - 优化清理完成弹窗样式,使用自定义 Modal 组件 - 美化排序下拉框,使用 Radix UI Select 组件 - 添加详细的调试日志用于追踪 Flow 显示问题 - 修复所有测试代码中缺少 provider_id 字段的问题 --- src-tauri/src/commands/flow_monitor_cmd.rs | 1 + src-tauri/src/flow_monitor/diff.rs | 1 + src-tauri/src/flow_monitor/exporter.rs | 2 + src-tauri/src/flow_monitor/memory_store.rs | 11 +- src-tauri/src/flow_monitor/models.rs | 5 + src-tauri/src/flow_monitor/monitor.rs | 37 ++- src-tauri/src/flow_monitor/query_service.rs | 17 +- src-tauri/src/server/handlers/api.rs | 234 ++++++++++++++---- src-tauri/tests/end_to_end_tests.rs | 1 + src/components/flow-monitor/CleanupDialog.tsx | 123 ++++++++- src/components/flow-monitor/FlowDetail.tsx | 5 +- src/components/flow-monitor/FlowList.tsx | 61 ++++- src/lib/api/flowMonitor.ts | 1 + 13 files changed, 431 insertions(+), 68 deletions(-) diff --git a/src-tauri/src/commands/flow_monitor_cmd.rs b/src-tauri/src/commands/flow_monitor_cmd.rs index 3c21fb1ee..9bf46c413 100644 --- a/src-tauri/src/commands/flow_monitor_cmd.rs +++ b/src-tauri/src/commands/flow_monitor_cmd.rs @@ -791,6 +791,7 @@ pub async fn create_test_flows( // 创建测试元数据 let metadata = FlowMetadata { provider: ProviderType::OpenAI, + provider_id: Some("openai".to_string()), credential_id: Some(format!("test-cred-{}", i)), credential_name: Some(format!("测试凭证 {}", i)), retry_count: 0, diff --git a/src-tauri/src/flow_monitor/diff.rs b/src-tauri/src/flow_monitor/diff.rs index 59b77a166..53ba7e960 100644 --- a/src-tauri/src/flow_monitor/diff.rs +++ b/src-tauri/src/flow_monitor/diff.rs @@ -1269,6 +1269,7 @@ mod property_tests { fn arb_flow_metadata() -> impl Strategy { arb_provider_type().prop_map(|provider| FlowMetadata { provider, + provider_id: None, credential_id: None, credential_name: None, retry_count: 0, diff --git a/src-tauri/src/flow_monitor/exporter.rs b/src-tauri/src/flow_monitor/exporter.rs index ddb5e9a72..c94160f7f 100644 --- a/src-tauri/src/flow_monitor/exporter.rs +++ b/src-tauri/src/flow_monitor/exporter.rs @@ -1618,6 +1618,7 @@ mod property_tests { fn arb_flow_metadata() -> impl Strategy { arb_provider_type().prop_map(|provider| FlowMetadata { provider, + provider_id: None, credential_id: None, credential_name: None, retry_count: 0, @@ -1969,6 +1970,7 @@ mod redaction_property_tests { let metadata = FlowMetadata { provider, + provider_id: None, credential_id: None, credential_name: None, retry_count: 0, diff --git a/src-tauri/src/flow_monitor/memory_store.rs b/src-tauri/src/flow_monitor/memory_store.rs index 2ce901a7c..93d98c9b3 100644 --- a/src-tauri/src/flow_monitor/memory_store.rs +++ b/src-tauri/src/flow_monitor/memory_store.rs @@ -394,19 +394,28 @@ impl FlowMemoryStore { pub fn add(&mut self, flow: LLMFlow) { let id = flow.id.clone(); + eprintln!( + "[MEMORY_STORE] 添加 Flow: id={}, model={}, state={:?}", + id, flow.request.model, flow.state + ); + // 如果已存在,先移除旧的 if self.flows.contains_key(&id) { self.ordered_ids.retain(|i| i != &id); + eprintln!("[MEMORY_STORE] 移除旧的 Flow: id={}", id); } // 检查是否需要驱逐 while self.flows.len() >= self.max_size { + eprintln!("[MEMORY_STORE] 缓存已满,驱逐最旧的 Flow"); self.evict_oldest(); } // 添加新 Flow self.flows.insert(id.clone(), Arc::new(RwLock::new(flow))); - self.ordered_ids.push_back(id); + self.ordered_ids.push_back(id.clone()); + + eprintln!("[MEMORY_STORE] Flow 已添加,当前数量: {}", self.flows.len()); } /// 获取 Flow diff --git a/src-tauri/src/flow_monitor/models.rs b/src-tauri/src/flow_monitor/models.rs index 25d22745c..f66523a1e 100644 --- a/src-tauri/src/flow_monitor/models.rs +++ b/src-tauri/src/flow_monitor/models.rs @@ -539,6 +539,9 @@ pub struct ToolCallDelta { pub struct FlowMetadata { /// 提供商类型 pub provider: ProviderType, + /// 提供商 ID(实际的 provider ID,如 "deepseek", "moonshot" 等) + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_id: Option, /// 凭证 ID #[serde(skip_serializing_if = "Option::is_none")] pub credential_id: Option, @@ -564,6 +567,7 @@ impl Default for FlowMetadata { fn default() -> Self { Self { provider: ProviderType::Kiro, + provider_id: None, credential_id: None, credential_name: None, retry_count: 0, @@ -1063,6 +1067,7 @@ mod property_tests { ) .prop_map(|(provider, credential_id, credential_name)| FlowMetadata { provider, + provider_id: None, credential_id, credential_name, retry_count: 0, diff --git a/src-tauri/src/flow_monitor/monitor.rs b/src-tauri/src/flow_monitor/monitor.rs index 174f6ab49..9306b0bdc 100644 --- a/src-tauri/src/flow_monitor/monitor.rs +++ b/src-tauri/src/flow_monitor/monitor.rs @@ -1052,6 +1052,10 @@ impl FlowMonitor { // 检查是否应该监控 if !config.should_monitor(&request.model, &request.path) { + eprintln!( + "[FLOW_MONITOR] 跳过监控: model={}, path={}", + request.model, request.path + ); return None; } @@ -1064,6 +1068,11 @@ impl FlowMonitor { // 生成唯一 ID let flow_id = Uuid::new_v4().to_string(); + eprintln!( + "[FLOW_MONITOR] 创建新 Flow: id={}, model={}, provider={:?}", + flow_id, request.model, metadata.provider + ); + // 确定 Flow 类型 let flow_type = Self::determine_flow_type(&request.path); @@ -1081,6 +1090,7 @@ impl FlowMonitor { { let mut active = self.active_flows.write().await; active.insert(flow_id.clone(), active_flow); + eprintln!("[FLOW_MONITOR] 活跃 Flow 数量: {}", active.len()); } // 发送事件 @@ -1172,6 +1182,12 @@ impl FlowMonitor { /// - `flow_id`: Flow ID /// - `response`: LLM 响应(如果是非流式响应) pub async fn complete_flow(&self, flow_id: &str, response: Option) { + eprintln!( + "[FLOW_MONITOR] 准备完成 Flow: id={}, has_response={}", + flow_id, + response.is_some() + ); + let mut active = self.active_flows.write().await; if let Some(mut active_flow) = active.remove(flow_id) { @@ -1185,12 +1201,17 @@ impl FlowMonitor { }; // 更新 Flow - active_flow.flow.response = final_response; + active_flow.flow.response = final_response.clone(); active_flow.flow.state = FlowState::Completed; active_flow.flow.timestamps.response_end = Some(now); active_flow.flow.timestamps.calculate_duration(); active_flow.flow.timestamps.calculate_ttfb(); + eprintln!( + "[FLOW_MONITOR] Flow 状态更新: id={}, state={:?}, duration_ms={}", + flow_id, active_flow.flow.state, active_flow.flow.timestamps.duration_ms + ); + // 检查阈值 let threshold_result = self.check_threshold(&active_flow.flow).await; @@ -1198,13 +1219,23 @@ impl FlowMonitor { { let mut store = self.memory_store.write().await; store.add(active_flow.flow.clone()); + eprintln!( + "[FLOW_MONITOR] 已保存到内存存储: id={}, 内存中 Flow 数量={}", + flow_id, + store.len() + ); } // 保存到文件存储 if let Some(ref file_store) = self.file_store { if let Err(e) = file_store.write(&active_flow.flow) { tracing::error!("保存 Flow 到文件失败: {}", e); + eprintln!("[FLOW_MONITOR] 保存到文件失败: id={}, error={}", flow_id, e); + } else { + eprintln!("[FLOW_MONITOR] 已保存到文件存储: id={}", flow_id); } + } else { + eprintln!("[FLOW_MONITOR] 文件存储未启用"); } // 发送完成事件 @@ -1225,6 +1256,10 @@ impl FlowMonitor { self.check_threshold_notifications(&active_flow.flow, &threshold_result) .await; } + + eprintln!("[FLOW_MONITOR] Flow 完成处理完毕: id={}", flow_id); + } else { + eprintln!("[FLOW_MONITOR] 警告: 未找到活跃 Flow: id={}", flow_id); } } diff --git a/src-tauri/src/flow_monitor/query_service.rs b/src-tauri/src/flow_monitor/query_service.rs index a232eddf2..caa18e9b6 100644 --- a/src-tauri/src/flow_monitor/query_service.rs +++ b/src-tauri/src/flow_monitor/query_service.rs @@ -212,10 +212,17 @@ impl FlowQueryService { page: usize, page_size: usize, ) -> Result { + eprintln!( + "[QUERY_SERVICE] 开始查询: filter={:?}, sort_by={:?}, page={}, page_size={}", + filter, sort_by, page, page_size + ); + // 先从内存获取 let memory_flows = { let store = self.memory_store.read().await; - store.query(&filter) + let flows = store.query(&filter); + eprintln!("[QUERY_SERVICE] 内存中查询到 {} 条记录", flows.len()); + flows }; // 再从文件获取(如果需要更多数据) @@ -228,8 +235,14 @@ impl FlowQueryService { let needed = page * page_size; if memory_count < needed { + eprintln!( + "[QUERY_SERVICE] 内存数据不足,从文件补充: memory_count={}, needed={}", + memory_count, needed + ); + // 从文件存储获取更多数据 let file_flows = self.file_store.query(&filter, needed * 2, 0)?; + eprintln!("[QUERY_SERVICE] 文件中查询到 {} 条记录", file_flows.len()); // 合并并去重(以 ID 为准) let memory_ids: std::collections::HashSet<_> = @@ -242,6 +255,8 @@ impl FlowQueryService { } } + eprintln!("[QUERY_SERVICE] 合并后总记录数: {}", all_flows.len()); + // 排序 Self::sort_flows(&mut all_flows, sort_by, sort_desc); diff --git a/src-tauri/src/server/handlers/api.rs b/src-tauri/src/server/handlers/api.rs index 3777f4a44..6d4ee3512 100644 --- a/src-tauri/src/server/handlers/api.rs +++ b/src-tauri/src/server/handlers/api.rs @@ -268,6 +268,7 @@ fn build_llm_request_from_anthropic( /// 构建 FlowMetadata fn build_flow_metadata( provider: ProviderType, + provider_id: Option<&str>, credential_id: Option<&str>, credential_name: Option<&str>, headers: &HeaderMap, @@ -287,6 +288,7 @@ fn build_flow_metadata( FlowMetadata { provider, + provider_id: provider_id.map(|s| s.to_string()), credential_id: credential_id.map(|s| s.to_string()), credential_name: credential_name.map(|s| s.to_string()), retry_count: 0, @@ -972,13 +974,25 @@ pub async fn chat_completions( let llm_request = build_llm_request_from_openai(&request, "/v1/chat/completions", &headers); // 尝试将 selected_provider 解析为 ProviderType - // 如果是自定义 provider ID,则使用 OpenAI 作为默认值(因为大多数自定义 provider 使用 OpenAI 协议) + // 构建 Flow Metadata,同时保存 provider_type 和实际的 provider_id let provider_type = selected_provider .parse::() .unwrap_or(ProviderType::OpenAI); + // 从凭证名称中提取 Provider 显示名称 + // 凭证名称格式:Some("[降级] DeepSeek") 或 Some("DeepSeek") + let provider_display_name = cred.name.as_ref().and_then(|name| { + // 去掉 "[降级] " 前缀 + if name.starts_with("[降级] ") { + Some(&name[9..]) // "[降级] " 是 9 个字节 + } else { + Some(name.as_str()) + } + }); + let flow_metadata = build_flow_metadata( provider_type, + provider_display_name, // 使用 Provider 显示名称(如 "DeepSeek") Some(&cred.uuid), cred.name.as_deref(), &headers, @@ -1026,6 +1040,7 @@ pub async fn chat_completions( // 记录请求统计 let is_success = response.status().is_success(); + let status_code = response.status().as_u16(); let status = if is_success { crate::telemetry::RequestStatus::Success } else { @@ -1033,38 +1048,99 @@ pub async fn chat_completions( }; record_request_telemetry(&state, &ctx, status, None); - // 如果成功,记录估算的 Token 使用量 - let estimated_input_tokens = request - .messages - .iter() - .map(|m| { - let content_len = match &m.content { - Some(c) => message_content_len(c), - None => 0, - }; - content_len / 4 - }) - .sum::() as u32; - let estimated_output_tokens = if is_success { 100u32 } else { 0u32 }; + // 如果成功且需要 Flow 捕获,提取响应体内容和响应头 + // 注意:非流式响应需要读取 body,所以必须在这里处理 + if is_success && flow_id.is_some() && !request.stream { + // 将 Response 转换为 bytes + let (parts, body) = response.into_parts(); - if is_success { - record_token_usage( - &state, - &ctx, - Some(estimated_input_tokens), - Some(estimated_output_tokens), - ); - } + // 提取响应头 + let mut response_headers = HashMap::new(); + for (name, value) in parts.headers.iter() { + if let Ok(v) = value.to_str() { + response_headers.insert(name.as_str().to_string(), v.to_string()); + } + } - // 完成 Flow 捕获并检查响应拦截 - // **Validates: Requirements 2.1, 2.5** - if let Some(fid) = flow_id { - if is_success { - let llm_response = build_llm_response( - 200, - "", // 内容在 provider_calls 中处理 - Some((estimated_input_tokens, estimated_output_tokens)), - ); + let body_bytes = match axum::body::to_bytes(body, usize::MAX).await { + Ok(bytes) => bytes, + Err(e) => { + eprintln!("[CHAT_COMPLETIONS] 读取响应体失败: {}", e); + // 如果读取失败,返回错误 + if let Some(fid) = flow_id { + let error = FlowError::new(FlowErrorType::Network, &e.to_string()); + state.flow_monitor.fail_flow(&fid, error).await; + } + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": {"message": format!("Failed to read response body: {}", e)}})), + ) + .into_response(); + } + }; + + // 解析响应体 + let response_json: serde_json::Value = match serde_json::from_slice(&body_bytes) { + Ok(json) => json, + Err(e) => { + eprintln!("[CHAT_COMPLETIONS] 解析响应体失败: {}", e); + // 如果解析失败,仍然返回原始响应 + if let Some(fid) = flow_id { + let error = FlowError::new( + FlowErrorType::Other, + &format!("Failed to parse response: {}", e), + ); + state.flow_monitor.fail_flow(&fid, error).await; + } + // 重新构建响应 + let response = Response::from_parts(parts, Body::from(body_bytes)); + return response; + } + }; + + // 提取内容和 token 使用量 + // 优先从 content 字段提取,如果为空则尝试从 tool_calls 提取 + let mut content = response_json["choices"][0]["message"]["content"] + .as_str() + .unwrap_or("") + .to_string(); + + // 如果 content 为空,检查是否有 tool_calls + if content.is_empty() { + if let Some(tool_calls) = + response_json["choices"][0]["message"]["tool_calls"].as_array() + { + if !tool_calls.is_empty() { + // 从第一个 tool_call 的 arguments 中提取内容 + if let Some(arguments) = tool_calls[0]["function"]["arguments"].as_str() { + content = arguments.to_string(); + eprintln!("[CHAT_COMPLETIONS] 从 tool_calls 中提取内容"); + } + } + } + } + + let input_tokens = response_json["usage"]["prompt_tokens"] + .as_u64() + .unwrap_or(0) as u32; + let output_tokens = response_json["usage"]["completion_tokens"] + .as_u64() + .unwrap_or(0) as u32; + + eprintln!("[CHAT_COMPLETIONS] 提取响应内容: content_len={}, input_tokens={}, output_tokens={}", + content.len(), input_tokens, output_tokens); + + // 记录 Token 使用量 + record_token_usage(&state, &ctx, Some(input_tokens), Some(output_tokens)); + + // 完成 Flow 捕获并检查响应拦截 + // **Validates: Requirements 2.1, 2.5** + if let Some(fid) = flow_id { + // 构建 LLMResponse,包含完整的响应体和响应头 + let mut llm_response = + build_llm_response(200, &content, Some((input_tokens, output_tokens))); + llm_response.body = response_json.clone(); + llm_response.headers = response_headers; // 设置响应头 // 检查是否需要拦截响应 if let Some(modified_response) = check_response_intercept( @@ -1090,7 +1166,6 @@ pub async fn chat_completions( .await; // 构建修改后的 HTTP 响应 - // 注意:这里简化处理,实际应该根据修改后的内容重新构建完整响应 return ( StatusCode::OK, Json(serde_json::json!({ @@ -1119,21 +1194,59 @@ pub async fn chat_completions( .into_response(); } + eprintln!("[FLOW_DEBUG] 准备完成 Flow: flow_id={}, content_len={}, input_tokens={}, output_tokens={}", + fid, llm_response.content.len(), llm_response.usage.input_tokens, llm_response.usage.output_tokens); + state .flow_monitor .complete_flow(&fid, Some(llm_response)) .await; - } else { - let error = FlowError::new( - FlowErrorType::from_status_code(response.status().as_u16()), - "Request failed", - ) - .with_status_code(response.status().as_u16()); - state.flow_monitor.fail_flow(&fid, error).await; - } - } - return response; + eprintln!("[FLOW_DEBUG] Flow 已完成: flow_id={}", fid); + } + + // 重新构建响应返回给客户端 + let response = Response::from_parts(parts, Body::from(body_bytes)); + return response; + } else { + // 流式响应或没有 Flow 捕获,直接返回 + // 估算 Token 使用量(用于统计) + let estimated_input_tokens = request + .messages + .iter() + .map(|m| { + let content_len = match &m.content { + Some(c) => message_content_len(c), + None => 0, + }; + content_len / 4 + }) + .sum::() as u32; + let estimated_output_tokens = if is_success { 100u32 } else { 0u32 }; + + if is_success { + record_token_usage( + &state, + &ctx, + Some(estimated_input_tokens), + Some(estimated_output_tokens), + ); + } + + // 如果失败,标记 Flow 失败 + if let Some(fid) = flow_id { + if !is_success { + let error = FlowError::new( + FlowErrorType::from_status_code(status_code), + "Request failed", + ) + .with_status_code(status_code); + state.flow_monitor.fail_flow(&fid, error).await; + } + } + + return response; + } } // 回退到旧的单凭证模式(仅当选择的 Provider 是 Kiro 时) @@ -1171,13 +1284,19 @@ pub async fn chat_completions( // 启动 Flow 捕获(legacy mode) let llm_request = build_llm_request_from_openai(&request, "/v1/chat/completions", &headers); - // 尝试将 selected_provider 解析为 ProviderType - // 如果是自定义 provider ID,则使用 OpenAI 作为默认值 + // 使用实际的 provider ID 构建 Flow Metadata let provider_type = selected_provider .parse::() .unwrap_or(ProviderType::OpenAI); - let flow_metadata = build_flow_metadata(provider_type, None, None, &headers, &ctx.request_id); + let flow_metadata = build_flow_metadata( + provider_type, + Some(&selected_provider), + None, + None, + &headers, + &ctx.request_id, + ); let flow_id = state .flow_monitor .start_flow(llm_request.clone(), flow_metadata.clone()) @@ -1934,12 +2053,25 @@ pub async fn anthropic_messages( // 尝试将 selected_provider 解析为 ProviderType // 如果是自定义 provider ID,则使用 OpenAI 作为默认值 + // 使用实际的 provider ID 构建 Flow Metadata let provider_type = selected_provider .parse::() .unwrap_or(ProviderType::OpenAI); + // 从凭证名称中提取 Provider 显示名称 + // 凭证名称格式:Some("[降级] DeepSeek") 或 Some("DeepSeek") + let provider_display_name = cred.name.as_ref().and_then(|name| { + // 去掉 "[降级] " 前缀 + if name.starts_with("[降级] ") { + Some(&name[9..]) // "[降级] " 是 9 个字节 + } else { + Some(name.as_str()) + } + }); + let flow_metadata = build_flow_metadata( provider_type, + provider_display_name, // 使用 Provider 显示名称(如 "DeepSeek") Some(&cred.uuid), cred.name.as_deref(), &headers, @@ -2129,13 +2261,19 @@ pub async fn anthropic_messages( // 启动 Flow 捕获(legacy mode) let llm_request = build_llm_request_from_anthropic(&request, "/v1/messages", &headers); - // 尝试将 selected_provider 解析为 ProviderType - // 如果是自定义 provider ID,则使用 OpenAI 作为默认值 + // 使用实际的 provider ID 构建 Flow Metadata let provider_type = selected_provider .parse::() .unwrap_or(ProviderType::OpenAI); - let flow_metadata = build_flow_metadata(provider_type, None, None, &headers, &ctx.request_id); + let flow_metadata = build_flow_metadata( + provider_type, + Some(&selected_provider), + None, + None, + &headers, + &ctx.request_id, + ); let flow_id = state .flow_monitor .start_flow(llm_request.clone(), flow_metadata.clone()) diff --git a/src-tauri/tests/end_to_end_tests.rs b/src-tauri/tests/end_to_end_tests.rs index 8eaa556f9..6438fef36 100644 --- a/src-tauri/tests/end_to_end_tests.rs +++ b/src-tauri/tests/end_to_end_tests.rs @@ -128,6 +128,7 @@ impl E2ETestContext { error: None, metadata: FlowMetadata { provider: provider, + provider_id: None, credential_name: Some("test-cred".to_string()), credential_id: Some("test-cred-id".to_string()), retry_count: 0, diff --git a/src/components/flow-monitor/CleanupDialog.tsx b/src/components/flow-monitor/CleanupDialog.tsx index 60a1d19eb..4ffe64e09 100644 --- a/src/components/flow-monitor/CleanupDialog.tsx +++ b/src/components/flow-monitor/CleanupDialog.tsx @@ -35,6 +35,12 @@ export function CleanupDialog({ const [cleanupType, setCleanupType] = useState("ByTime"); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [showSuccess, setShowSuccess] = useState(false); + const [cleanupResult, setCleanupResult] = useState<{ + cleaned_count: number; + cleaned_files: number; + freed_bytes: number; + } | null>(null); // 时间清理选项 const [retentionDays, setRetentionDays] = useState(7); @@ -105,14 +111,9 @@ export function CleanupDialog({ 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(); + // 保存清理结果并显示成功提示 + setCleanupResult(result); + setShowSuccess(true); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { @@ -120,6 +121,13 @@ export function CleanupDialog({ } }; + const handleSuccessClose = () => { + setShowSuccess(false); + setCleanupResult(null); + onSuccess(); + onClose(); + }; + const formatBytes = (bytes: number): string => { if (bytes === 0) return "0 B"; const k = 1024; @@ -149,6 +157,105 @@ export function CleanupDialog({ "IFlow", ]; + // 如果显示成功提示,渲染成功对话框 + if (showSuccess && cleanupResult) { + return ( + +
+ {/* 成功图标 */} +
+
+
+ + + +
+ {/* 动画圆环 */} +
+
+
+ + {/* 标题 */} +

清理完成!

+

+ 已成功清理日志数据 +

+ + {/* 清理统计 */} +
+
+
+
+ +
+ 删除记录 +
+ + {cleanupResult.cleaned_count} 条 + +
+ +
+
+
+ + + +
+ 清理文件 +
+ + {cleanupResult.cleaned_files} 个 + +
+ +
+
+
+ +
+ + 释放空间 + +
+ + {formatBytes(cleanupResult.freed_bytes)} + +
+
+ + {/* 确认按钮 */} + +
+
+ ); + } + return ( {/* 标题 */} diff --git a/src/components/flow-monitor/FlowDetail.tsx b/src/components/flow-monitor/FlowDetail.tsx index 11ace05f8..82d241895 100644 --- a/src/components/flow-monitor/FlowDetail.tsx +++ b/src/components/flow-monitor/FlowDetail.tsx @@ -383,7 +383,10 @@ function FlowDetailHeader({
- + )} - + + + + + + + + + + + + + + 按时间 + + + + + + 按耗时 + + + + + + 按 Token + + + + + + 按模型 + + + + +