diff --git a/package.json b/package.json index 271c3f4e6..c50034dbf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.17.0", + "version": "0.17.1", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4763ddac6..1a91f4991 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3367,7 +3367,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.17.0" +version = "0.17.1" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index aa3f42769..e8c388559 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.17.0" +version = "0.17.1" 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 2577036f3..72001eea4 100644 --- a/src-tauri/src/commands/flow_monitor_cmd.rs +++ b/src-tauri/src/commands/flow_monitor_cmd.rs @@ -552,6 +552,51 @@ pub async fn get_flow_monitor_status( }) } +/// 获取 Flow Monitor 状态(调试用) +/// +/// **Validates: Requirements 10.1** +/// +/// # Arguments +/// * `monitor` - Flow 监控服务状态 +/// * `query_service` - 查询服务状态 +/// +/// # Returns +/// * `Ok(FlowMonitorDebugInfo)` - 成功时返回调试信息 +/// * `Err(String)` - 失败时返回错误消息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FlowMonitorDebugInfo { + /// 是否启用 + pub enabled: bool, + /// 活跃 Flow 数量 + pub active_flow_count: usize, + /// 内存中的 Flow 数量 + pub memory_flow_count: usize, + /// 最大内存 Flow 数量 + pub max_memory_flows: usize, + /// 内存中的 Flow ID 列表(最多显示10个) + pub memory_flow_ids: Vec, + /// 配置信息 + pub config_enabled: bool, +} + +#[tauri::command] +pub async fn get_flow_monitor_debug_info( + monitor: State<'_, FlowMonitorState>, + query_service: State<'_, FlowQueryServiceState>, +) -> Result { + let config = monitor.0.config().await; + let recent_flows = query_service.0.get_recent(10).await; + + Ok(FlowMonitorDebugInfo { + enabled: monitor.0.is_enabled().await, + active_flow_count: monitor.0.active_flow_count().await, + memory_flow_count: monitor.0.memory_flow_count().await, + max_memory_flows: config.max_memory_flows, + memory_flow_ids: recent_flows.into_iter().map(|f| f.id).collect(), + config_enabled: config.enabled, + }) +} + /// 启用 Flow Monitor /// /// **Validates: Requirements 10.1** @@ -568,6 +613,120 @@ pub async fn enable_flow_monitor(monitor: State<'_, FlowMonitorState>) -> Result Ok(()) } +/// 创建测试 Flow 数据(仅用于调试) +/// +/// **Validates: Requirements 10.1** +/// +/// # Arguments +/// * `count` - 要创建的测试 Flow 数量 +/// * `monitor` - Flow 监控服务状态 +/// +/// # Returns +/// * `Ok(usize)` - 成功创建的 Flow 数量 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn create_test_flows( + count: Option, + monitor: State<'_, FlowMonitorState>, +) -> Result { + use crate::flow_monitor::{ + ClientInfo, FlowMetadata, LLMRequest, Message, MessageRole, ProviderType, + RequestParameters, RoutingInfo, + }; + use chrono::Utc; + + let count = count.unwrap_or(5); + let mut created = 0; + + for i in 0..count { + // 创建测试请求 + let request = LLMRequest { + method: "POST".to_string(), + path: "/v1/chat/completions".to_string(), + headers: std::collections::HashMap::new(), + body: serde_json::json!({ + "model": format!("gpt-4-test-{}", i), + "messages": [{"role": "user", "content": format!("测试消息 {}", i)}] + }), + messages: vec![Message { + role: MessageRole::User, + content: crate::flow_monitor::MessageContent::Text(format!("测试消息 {}", i)), + tool_calls: None, + tool_result: None, + name: None, + }], + system_prompt: None, + tools: None, + model: format!("gpt-4-test-{}", i), + original_model: None, + parameters: RequestParameters { + temperature: Some(0.7), + top_p: Some(1.0), + max_tokens: Some(1000), + stop: None, + stream: false, + extra: std::collections::HashMap::new(), + }, + size_bytes: 100 + i * 10, + timestamp: Utc::now(), + }; + + // 创建测试元数据 + let metadata = FlowMetadata { + provider: ProviderType::OpenAI, + credential_id: Some(format!("test-cred-{}", i)), + credential_name: Some(format!("测试凭证 {}", i)), + retry_count: 0, + client_info: ClientInfo { + ip: Some("127.0.0.1".to_string()), + user_agent: Some("test-agent".to_string()), + request_id: Some(format!("test-req-{}", i)), + }, + routing_info: RoutingInfo { + target_url: Some("https://api.openai.com".to_string()), + route_rule: None, + load_balance_strategy: None, + }, + injected_params: None, + context_usage_percentage: Some(50.0), + }; + + // 启动 Flow + if let Some(flow_id) = monitor.0.start_flow(request, metadata).await { + // 模拟完成 Flow + let response = crate::flow_monitor::LLMResponse { + status_code: 200, + status_text: "OK".to_string(), + headers: std::collections::HashMap::new(), + body: serde_json::json!({ + "choices": [{"message": {"role": "assistant", "content": format!("测试响应 {}", i)}}] + }), + content: format!("测试响应 {}", i), + thinking: None, + tool_calls: Vec::new(), + usage: crate::flow_monitor::TokenUsage { + input_tokens: 10 + i as u32, + output_tokens: 20 + i as u32, + cache_read_tokens: None, + cache_write_tokens: None, + thinking_tokens: None, + total_tokens: 30 + i as u32 * 2, + }, + stop_reason: Some(crate::flow_monitor::StopReason::Stop), + size_bytes: 200 + i * 15, + timestamp_start: Utc::now(), + timestamp_end: Utc::now(), + stream_info: None, + }; + + monitor.0.complete_flow(&flow_id, Some(response)).await; + created += 1; + } + } + + Ok(created) +} + /// 禁用 Flow Monitor /// /// **Validates: Requirements 10.1** diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 0d9c41c28..d69d0a0c9 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -15,3 +15,4 @@ pub mod telemetry_cmd; pub mod tray_cmd; pub mod usage_cmd; pub mod websocket_cmd; +pub mod window_cmd; diff --git a/src-tauri/src/commands/window_cmd.rs b/src-tauri/src/commands/window_cmd.rs new file mode 100644 index 000000000..6286c8603 --- /dev/null +++ b/src-tauri/src/commands/window_cmd.rs @@ -0,0 +1,396 @@ +//! 窗口控制命令 +//! +//! 提供窗口大小调整、位置控制等功能 + +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Manager, PhysicalSize, Window}; + +/// 窗口大小预设 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowSize { + pub width: u32, + pub height: u32, +} + +/// 预定义的窗口大小 +impl WindowSize { + /// 默认窗口大小 + pub fn default() -> Self { + Self { + width: 1200, + height: 800, + } + } + + /// Flow Monitor 优化大小(更宽更高,适合数据展示) + pub fn flow_monitor() -> Self { + Self { + width: 1600, + height: 1000, + } + } + + /// 紧凑模式 + pub fn compact() -> Self { + Self { + width: 1000, + height: 700, + } + } + + /// 大屏模式 + pub fn large() -> Self { + Self { + width: 1920, + height: 1200, + } + } + + /// 超大屏模式 + pub fn extra_large() -> Self { + Self { + width: 2560, + height: 1440, + } + } + + /// 4K 模式 + pub fn ultra_wide() -> Self { + Self { + width: 3440, + height: 1440, + } + } + + /// 4K 标准模式 + pub fn four_k() -> Self { + Self { + width: 3840, + height: 2160, + } + } +} + +/// 窗口大小选项 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowSizeOption { + pub id: String, + pub name: String, + pub description: String, + pub size: WindowSize, +} + +impl WindowSizeOption { + /// 获取所有可用的窗口大小选项 + pub fn all_options() -> Vec { + vec![ + Self { + id: "compact".to_string(), + name: "紧凑模式".to_string(), + description: "1000×700 - 节省屏幕空间".to_string(), + size: WindowSize::compact(), + }, + Self { + id: "default".to_string(), + name: "默认大小".to_string(), + description: "1200×800 - 日常使用".to_string(), + size: WindowSize::default(), + }, + Self { + id: "flow_monitor".to_string(), + name: "Flow Monitor".to_string(), + description: "1600×1000 - 数据展示优化".to_string(), + size: WindowSize::flow_monitor(), + }, + Self { + id: "large".to_string(), + name: "大屏模式".to_string(), + description: "1920×1200 - 大屏幕显示".to_string(), + size: WindowSize::large(), + }, + Self { + id: "extra_large".to_string(), + name: "超大屏模式".to_string(), + description: "2560×1440 - 超大屏幕".to_string(), + size: WindowSize::extra_large(), + }, + Self { + id: "ultra_wide".to_string(), + name: "超宽屏模式".to_string(), + description: "3440×1440 - 超宽屏显示".to_string(), + size: WindowSize::ultra_wide(), + }, + Self { + id: "four_k".to_string(), + name: "4K 模式".to_string(), + description: "3840×2160 - 4K 显示器".to_string(), + size: WindowSize::four_k(), + }, + ] + } +} + +/// 获取所有可用的窗口大小选项 +/// +/// # Returns +/// * `Vec` - 所有可用的窗口大小选项 +#[tauri::command] +pub async fn get_window_size_options() -> Vec { + WindowSizeOption::all_options() +} + +/// 设置窗口为指定的预设大小 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// * `option_id` - 窗口大小选项 ID +/// +/// # Returns +/// * `Ok(WindowSize)` - 成功时返回之前的窗口大小 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn set_window_size_by_option( + app: AppHandle, + option_id: String, +) -> Result { + // 获取当前大小 + let current_size = get_window_size(app.clone()).await?; + + // 查找对应的窗口大小选项 + let options = WindowSizeOption::all_options(); + let option = options + .iter() + .find(|opt| opt.id == option_id) + .ok_or_else(|| format!("未找到窗口大小选项: {}", option_id))?; + + // 设置新的窗口大小 + set_window_size(app.clone(), option.size.clone()).await?; + + // 居中窗口 + center_window(app).await?; + + Ok(current_size) +} + +/// 切换全屏模式 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// +/// # Returns +/// * `Ok(bool)` - 成功时返回是否进入了全屏模式 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn toggle_fullscreen(app: AppHandle) -> Result { + let window = app.get_webview_window("main").ok_or("无法获取主窗口")?; + + let is_fullscreen = window + .is_fullscreen() + .map_err(|e| format!("获取全屏状态失败: {}", e))?; + + window + .set_fullscreen(!is_fullscreen) + .map_err(|e| format!("切换全屏模式失败: {}", e))?; + + Ok(!is_fullscreen) +} + +/// 检查是否处于全屏模式 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// +/// # Returns +/// * `Ok(bool)` - 成功时返回是否处于全屏模式 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn is_fullscreen(app: AppHandle) -> Result { + let window = app.get_webview_window("main").ok_or("无法获取主窗口")?; + + window + .is_fullscreen() + .map_err(|e| format!("获取全屏状态失败: {}", e)) +} + +/// 获取当前窗口大小 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// +/// # Returns +/// * `Ok(WindowSize)` - 成功时返回当前窗口大小 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn get_window_size(app: AppHandle) -> Result { + let window = app.get_webview_window("main").ok_or("无法获取主窗口")?; + + let size = window + .inner_size() + .map_err(|e| format!("获取窗口大小失败: {}", e))?; + + Ok(WindowSize { + width: size.width, + height: size.height, + }) +} + +/// 设置窗口大小 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// * `size` - 新的窗口大小 +/// +/// # Returns +/// * `Ok(())` - 成功 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn set_window_size(app: AppHandle, size: WindowSize) -> Result<(), String> { + let window = app.get_webview_window("main").ok_or("无法获取主窗口")?; + + let physical_size = PhysicalSize::new(size.width, size.height); + + window + .set_size(physical_size) + .map_err(|e| format!("设置窗口大小失败: {}", e))?; + + Ok(()) +} + +/// 切换到 Flow Monitor 优化大小 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// +/// # Returns +/// * `Ok(WindowSize)` - 成功时返回之前的窗口大小 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn resize_for_flow_monitor(app: AppHandle) -> Result { + // 先获取当前大小,用于恢复 + let current_size = get_window_size(app.clone()).await?; + + // 设置为 Flow Monitor 优化大小 + let flow_monitor_size = WindowSize::flow_monitor(); + set_window_size(app, flow_monitor_size).await?; + + Ok(current_size) +} + +/// 恢复窗口到指定大小 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// * `size` - 要恢复的窗口大小 +/// +/// # Returns +/// * `Ok(())` - 成功 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn restore_window_size(app: AppHandle, size: WindowSize) -> Result<(), String> { + set_window_size(app, size).await +} + +/// 切换窗口大小(在默认大小和 Flow Monitor 大小之间切换) +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// +/// # Returns +/// * `Ok(bool)` - 成功时返回是否切换到了 Flow Monitor 大小 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn toggle_window_size(app: AppHandle) -> Result { + let current_size = get_window_size(app.clone()).await?; + let flow_monitor_size = WindowSize::flow_monitor(); + let default_size = WindowSize::default(); + + // 判断当前是否接近 Flow Monitor 大小(允许一些误差) + let is_flow_monitor_size = (current_size.width as i32 - flow_monitor_size.width as i32).abs() + < 50 + && (current_size.height as i32 - flow_monitor_size.height as i32).abs() < 50; + + if is_flow_monitor_size { + // 当前是 Flow Monitor 大小,切换到默认大小 + set_window_size(app, default_size).await?; + Ok(false) + } else { + // 当前不是 Flow Monitor 大小,切换到 Flow Monitor 大小 + set_window_size(app, flow_monitor_size).await?; + Ok(true) + } +} + +/// 居中窗口 +/// +/// # Arguments +/// * `app` - Tauri AppHandle +/// +/// # Returns +/// * `Ok(())` - 成功 +/// * `Err(String)` - 失败时返回错误消息 +#[tauri::command] +pub async fn center_window(app: AppHandle) -> Result<(), String> { + let window = app.get_webview_window("main").ok_or("无法获取主窗口")?; + + window + .center() + .map_err(|e| format!("居中窗口失败: {}", e))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_window_size_presets() { + let default = WindowSize::default(); + assert_eq!(default.width, 1200); + assert_eq!(default.height, 800); + + let flow_monitor = WindowSize::flow_monitor(); + assert_eq!(flow_monitor.width, 1600); + assert_eq!(flow_monitor.height, 1000); + + let compact = WindowSize::compact(); + assert_eq!(compact.width, 1000); + assert_eq!(compact.height, 700); + + let large = WindowSize::large(); + assert_eq!(large.width, 1920); + assert_eq!(large.height, 1200); + + let extra_large = WindowSize::extra_large(); + assert_eq!(extra_large.width, 2560); + assert_eq!(extra_large.height, 1440); + + let ultra_wide = WindowSize::ultra_wide(); + assert_eq!(ultra_wide.width, 3440); + assert_eq!(ultra_wide.height, 1440); + + let four_k = WindowSize::four_k(); + assert_eq!(four_k.width, 3840); + assert_eq!(four_k.height, 2160); + } + + #[test] + fn test_window_size_options() { + let options = WindowSizeOption::all_options(); + assert_eq!(options.len(), 7); + + // 验证每个选项都有有效的 ID 和名称 + for option in &options { + assert!(!option.id.is_empty()); + assert!(!option.name.is_empty()); + assert!(!option.description.is_empty()); + assert!(option.size.width > 0); + assert!(option.size.height > 0); + } + + // 验证特定选项 + let default_option = options.iter().find(|opt| opt.id == "default").unwrap(); + assert_eq!(default_option.size.width, 1200); + assert_eq!(default_option.size.height, 800); + } +} diff --git a/src-tauri/src/flow_monitor/file_store.rs b/src-tauri/src/flow_monitor/file_store.rs index b74034977..7898bb4b0 100644 --- a/src-tauri/src/flow_monitor/file_store.rs +++ b/src-tauri/src/flow_monitor/file_store.rs @@ -578,7 +578,7 @@ impl FlowFileStore { return Ok(None); } - let flow: LLMFlow = serde_json::from_str(&line)?; + let mut flow: LLMFlow = serde_json::from_str(&line)?; Ok(Some(flow)) } diff --git a/src-tauri/src/flow_monitor/memory_store.rs b/src-tauri/src/flow_monitor/memory_store.rs index 56e652b88..46bd598c1 100644 --- a/src-tauri/src/flow_monitor/memory_store.rs +++ b/src-tauri/src/flow_monitor/memory_store.rs @@ -206,13 +206,26 @@ impl FlowFilter { } } - // 内容搜索 + // 内容搜索(搜索响应内容、模型名称、提供商名称) if let Some(ref search) = self.content_search { + let search_lower = search.to_lowercase(); + + // 搜索响应内容 let content = flow .response .as_ref() .map_or(String::new(), |r| r.content.clone()); - if !content.to_lowercase().contains(&search.to_lowercase()) { + let content_matches = content.to_lowercase().contains(&search_lower); + + // 搜索模型名称 + let model_matches = flow.request.model.to_lowercase().contains(&search_lower); + + // 搜索提供商名称 + let provider_name = format!("{:?}", flow.metadata.provider).to_lowercase(); + let provider_matches = provider_name.contains(&search_lower); + + // 任一匹配即可 + if !content_matches && !model_matches && !provider_matches { return false; } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 739be3b8c..56d11d4a5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1904,6 +1904,8 @@ pub fn run() { commands::flow_monitor_cmd::cleanup_flows, commands::flow_monitor_cmd::get_recent_flows, commands::flow_monitor_cmd::get_flow_monitor_status, + commands::flow_monitor_cmd::get_flow_monitor_debug_info, + commands::flow_monitor_cmd::create_test_flows, commands::flow_monitor_cmd::enable_flow_monitor, commands::flow_monitor_cmd::disable_flow_monitor, commands::flow_monitor_cmd::subscribe_flow_events, @@ -1997,6 +1999,17 @@ pub fn run() { commands::flow_monitor_cmd::batch_export_flows, commands::flow_monitor_cmd::batch_delete_flows, commands::flow_monitor_cmd::batch_add_to_session, + // Window control commands + commands::window_cmd::get_window_size, + commands::window_cmd::set_window_size, + commands::window_cmd::resize_for_flow_monitor, + commands::window_cmd::restore_window_size, + commands::window_cmd::toggle_window_size, + commands::window_cmd::center_window, + commands::window_cmd::get_window_size_options, + commands::window_cmd::set_window_size_by_option, + commands::window_cmd::toggle_fullscreen, + commands::window_cmd::is_fullscreen, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a8fd2a727..08c61984e 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.0", + "version": "0.17.1", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/flow-monitor/FlowDetail.tsx b/src/components/flow-monitor/FlowDetail.tsx index 8c71bcd49..ad87ea577 100644 --- a/src/components/flow-monitor/FlowDetail.tsx +++ b/src/components/flow-monitor/FlowDetail.tsx @@ -62,14 +62,7 @@ export function FlowDetail({ flowId, onBack, onExport }: FlowDetailProps) { const [codeMode, setCodeMode] = useState(false); // 使用 Flow 操作 Hook - const { - copyText, - copyFlowContent, - copyRequest, - copyResponse, - exportFlow, - exporting, - } = useFlowActions(); + const { copyText, copyFlowContent, exportFlow, exporting } = useFlowActions(); useEffect(() => { const loadFlowDetail = async () => { @@ -236,47 +229,6 @@ export function FlowDetail({ flowId, onBack, onExport }: FlowDetailProps) { > 时间线 - - {/* 快捷复制按钮 */} -
- - - -
{/* 内容区域 */} diff --git a/src/components/flow-monitor/FlowFilters.tsx b/src/components/flow-monitor/FlowFilters.tsx index 1c227c890..67f1d1f26 100644 --- a/src/components/flow-monitor/FlowFilters.tsx +++ b/src/components/flow-monitor/FlowFilters.tsx @@ -316,14 +316,25 @@ export function FlowFilters({ filter, onChange }: FlowFiltersProps) { {/* 表达式模式 */} {filterMode === "expression" ? (
- setShowHelp(!showHelp)} - /> +
+
+ setShowHelp(!showHelp)} + /> +
+ +
{/* 帮助面板 */} {showHelp && ( diff --git a/src/components/flow-monitor/FlowStats.tsx b/src/components/flow-monitor/FlowStats.tsx index 12a01388e..7c2cc5422 100644 --- a/src/components/flow-monitor/FlowStats.tsx +++ b/src/components/flow-monitor/FlowStats.tsx @@ -77,6 +77,8 @@ export function FlowStats({ setLoading(true); setError(null); + console.log("正在获取统计数据,过滤条件:", filter); + const [basicStats, enhanced] = await Promise.all([ flowMonitorApi.getFlowStats(filter), showEnhanced @@ -84,6 +86,9 @@ export function FlowStats({ : Promise.resolve(null), ]); + console.log("获取到的基础统计数据:", basicStats); + console.log("获取到的增强统计数据:", enhanced); + setStats(basicStats); setEnhancedStats(enhanced); setLastUpdated(new Date()); @@ -161,6 +166,30 @@ export function FlowStats({ 统计仪表板
+ {/* 调试按钮 */} + + {/* 时间范围选择 */}