mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
v0.17.1: Flow Monitor 增强 - 窗口大小调整、搜索优化
- 添加窗口大小调整功能(紧凑、默认、大屏、超大屏、超宽屏、4K) - 添加全屏切换功能 - 优化简单过滤搜索,支持搜索模型名称和提供商 - 修复下拉菜单背景透明问题 - 添加表达式模式搜索按钮
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"private": true,
|
||||
"version": "0.17.0",
|
||||
"version": "0.17.1",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Generated
+1
-1
@@ -3367,7 +3367,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxycast"
|
||||
version = "0.17.0"
|
||||
version = "0.17.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<String>,
|
||||
/// 配置信息
|
||||
pub config_enabled: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_flow_monitor_debug_info(
|
||||
monitor: State<'_, FlowMonitorState>,
|
||||
query_service: State<'_, FlowQueryServiceState>,
|
||||
) -> Result<FlowMonitorDebugInfo, String> {
|
||||
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<usize>,
|
||||
monitor: State<'_, FlowMonitorState>,
|
||||
) -> Result<usize, String> {
|
||||
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**
|
||||
|
||||
@@ -15,3 +15,4 @@ pub mod telemetry_cmd;
|
||||
pub mod tray_cmd;
|
||||
pub mod usage_cmd;
|
||||
pub mod websocket_cmd;
|
||||
pub mod window_cmd;
|
||||
|
||||
@@ -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<Self> {
|
||||
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<WindowSizeOption>` - 所有可用的窗口大小选项
|
||||
#[tauri::command]
|
||||
pub async fn get_window_size_options() -> Vec<WindowSizeOption> {
|
||||
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<WindowSize, String> {
|
||||
// 获取当前大小
|
||||
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<bool, String> {
|
||||
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<bool, String> {
|
||||
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<WindowSize, String> {
|
||||
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<WindowSize, String> {
|
||||
// 先获取当前大小,用于恢复
|
||||
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<bool, String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
>
|
||||
时间线
|
||||
</TabButton>
|
||||
|
||||
{/* 快捷复制按钮 */}
|
||||
<div className="ml-auto flex items-center gap-1 px-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
console.log("复制请求按钮被点击");
|
||||
const result = await copyRequest(flow);
|
||||
console.log("复制结果:", result);
|
||||
}}
|
||||
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
title="复制请求"
|
||||
type="button"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
console.log("复制响应按钮被点击");
|
||||
const result = await copyResponse(flow);
|
||||
console.log("复制结果:", result);
|
||||
}}
|
||||
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="复制响应"
|
||||
disabled={!flow.response}
|
||||
type="button"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
console.log("复制完整 JSON 按钮被点击");
|
||||
const result = await copyFlowContent(flow);
|
||||
console.log("复制结果:", result);
|
||||
}}
|
||||
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
title="复制完整 JSON"
|
||||
type="button"
|
||||
>
|
||||
<FileJson className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
|
||||
@@ -316,14 +316,25 @@ export function FlowFilters({ filter, onChange }: FlowFiltersProps) {
|
||||
{/* 表达式模式 */}
|
||||
{filterMode === "expression" ? (
|
||||
<div className="space-y-3">
|
||||
<FilterExpressionInput
|
||||
value={expressionValue}
|
||||
onChange={setExpressionValue}
|
||||
onSubmit={handleExpressionSubmit}
|
||||
onValidationChange={handleExpressionValidation}
|
||||
showHelp={showHelp}
|
||||
onHelpToggle={() => setShowHelp(!showHelp)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<FilterExpressionInput
|
||||
value={expressionValue}
|
||||
onChange={setExpressionValue}
|
||||
onSubmit={handleExpressionSubmit}
|
||||
onValidationChange={handleExpressionValidation}
|
||||
showHelp={showHelp}
|
||||
onHelpToggle={() => setShowHelp(!showHelp)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleExpressionSubmit(expressionValue)}
|
||||
disabled={!expressionValid || !expressionValue.trim()}
|
||||
className="rounded-lg border px-4 py-2 text-sm hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 帮助面板 */}
|
||||
{showHelp && (
|
||||
|
||||
@@ -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({
|
||||
统计仪表板
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 调试按钮 */}
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const debugInfo =
|
||||
await flowMonitorApi.getFlowMonitorDebugInfo();
|
||||
console.log("Flow Monitor 调试信息:", debugInfo);
|
||||
|
||||
if (debugInfo.memory_flow_count === 0) {
|
||||
console.log("内存中没有 Flow 数据,创建测试数据...");
|
||||
const created = await flowMonitorApi.createTestFlows(5);
|
||||
console.log(`已创建 ${created} 个测试 Flow`);
|
||||
// 重新获取统计数据
|
||||
fetchStats();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("调试失败:", e);
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
调试
|
||||
</button>
|
||||
|
||||
{/* 时间范围选择 */}
|
||||
<select
|
||||
value={timeRangeHours}
|
||||
|
||||
+38
-20
@@ -675,10 +675,25 @@ export const flowMonitorApi = {
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
): Promise<FlowQueryResult> {
|
||||
// 后端期望一个 request 对象,包含 filter, sort_by, sort_desc, page, page_size
|
||||
// 如果有 filter_expression,使用表达式查询
|
||||
if (filter.filter_expression) {
|
||||
return invoke("query_flows_with_expression", {
|
||||
request: {
|
||||
filter_expr: filter.filter_expression,
|
||||
sort_by: sortBy,
|
||||
sort_desc: sortDesc,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 否则使用普通查询
|
||||
// 移除 filter_expression 字段,因为后端不支持
|
||||
const { filter_expression: _filterExpr, ...cleanFilter } = filter;
|
||||
return invoke("query_flows", {
|
||||
request: {
|
||||
filter,
|
||||
filter: cleanFilter,
|
||||
sort_by: sortBy,
|
||||
sort_desc: sortDesc,
|
||||
page,
|
||||
@@ -953,26 +968,29 @@ export const flowMonitorApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 将 Flow 导出为代码
|
||||
* 获取 Flow Monitor 调试信息
|
||||
*
|
||||
* @param flowId - Flow ID
|
||||
* @param format - 代码格式 (curl, python, typescript, javascript)
|
||||
* @returns 导出的代码
|
||||
* @returns Flow Monitor 调试信息
|
||||
*/
|
||||
async exportFlowAsCode(
|
||||
flowId: string,
|
||||
format: CodeExportFormat,
|
||||
): Promise<string> {
|
||||
const response = await invoke<{ code: string; format: CodeExportFormat }>(
|
||||
"export_flow_as_code",
|
||||
{
|
||||
request: {
|
||||
flow_id: flowId,
|
||||
format,
|
||||
},
|
||||
},
|
||||
);
|
||||
return response.code;
|
||||
async getFlowMonitorDebugInfo(): Promise<{
|
||||
enabled: boolean;
|
||||
active_flow_count: number;
|
||||
memory_flow_count: number;
|
||||
max_memory_flows: number;
|
||||
memory_flow_ids: string[];
|
||||
config_enabled: boolean;
|
||||
}> {
|
||||
return invoke("get_flow_monitor_debug_info");
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建测试 Flow 数据(仅用于调试)
|
||||
*
|
||||
* @param count - 要创建的测试 Flow 数量
|
||||
* @returns 成功创建的 Flow 数量
|
||||
*/
|
||||
async createTestFlows(count?: number): Promise<number> {
|
||||
return invoke("create_test_flows", { count });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 窗口控制 API
|
||||
*
|
||||
* 提供窗口大小调整、位置控制等功能
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
/**
|
||||
* 窗口大小
|
||||
*/
|
||||
export interface WindowSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗口大小选项
|
||||
*/
|
||||
export interface WindowSizeOption {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
size: WindowSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗口控制 API
|
||||
*/
|
||||
export const windowApi = {
|
||||
/**
|
||||
* 获取当前窗口大小
|
||||
*
|
||||
* @returns 当前窗口大小
|
||||
*/
|
||||
async getWindowSize(): Promise<WindowSize> {
|
||||
return invoke("get_window_size");
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置窗口大小
|
||||
*
|
||||
* @param size - 新的窗口大小
|
||||
*/
|
||||
async setWindowSize(size: WindowSize): Promise<void> {
|
||||
return invoke("set_window_size", { size });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有可用的窗口大小选项
|
||||
*
|
||||
* @returns 窗口大小选项列表
|
||||
*/
|
||||
async getWindowSizeOptions(): Promise<WindowSizeOption[]> {
|
||||
return invoke("get_window_size_options");
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置窗口为指定的预设大小
|
||||
*
|
||||
* @param optionId - 窗口大小选项 ID
|
||||
* @returns 之前的窗口大小(用于恢复)
|
||||
*/
|
||||
async setWindowSizeByOption(optionId: string): Promise<WindowSize> {
|
||||
return invoke("set_window_size_by_option", { optionId });
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换全屏模式
|
||||
*
|
||||
* @returns 是否进入了全屏模式
|
||||
*/
|
||||
async toggleFullscreen(): Promise<boolean> {
|
||||
return invoke("toggle_fullscreen");
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否处于全屏模式
|
||||
*
|
||||
* @returns 是否处于全屏模式
|
||||
*/
|
||||
async isFullscreen(): Promise<boolean> {
|
||||
return invoke("is_fullscreen");
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换到 Flow Monitor 优化大小
|
||||
*
|
||||
* @returns 之前的窗口大小(用于恢复)
|
||||
*/
|
||||
async resizeForFlowMonitor(): Promise<WindowSize> {
|
||||
return invoke("resize_for_flow_monitor");
|
||||
},
|
||||
|
||||
/**
|
||||
* 恢复窗口到指定大小
|
||||
*
|
||||
* @param size - 要恢复的窗口大小
|
||||
*/
|
||||
async restoreWindowSize(size: WindowSize): Promise<void> {
|
||||
return invoke("restore_window_size", { size });
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换窗口大小(在默认大小和 Flow Monitor 大小之间切换)
|
||||
*
|
||||
* @returns 是否切换到了 Flow Monitor 大小
|
||||
*/
|
||||
async toggleWindowSize(): Promise<boolean> {
|
||||
return invoke("toggle_window_size");
|
||||
},
|
||||
|
||||
/**
|
||||
* 居中窗口
|
||||
*/
|
||||
async centerWindow(): Promise<void> {
|
||||
return invoke("center_window");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 预定义的窗口大小
|
||||
*/
|
||||
export const WindowSizes = {
|
||||
/** 紧凑模式 */
|
||||
compact: { width: 1000, height: 700 } as WindowSize,
|
||||
|
||||
/** 默认窗口大小 */
|
||||
default: { width: 1200, height: 800 } as WindowSize,
|
||||
|
||||
/** Flow Monitor 优化大小 */
|
||||
flowMonitor: { width: 1600, height: 1000 } as WindowSize,
|
||||
|
||||
/** 大屏模式 */
|
||||
large: { width: 1920, height: 1200 } as WindowSize,
|
||||
|
||||
/** 超大屏模式 */
|
||||
extraLarge: { width: 2560, height: 1440 } as WindowSize,
|
||||
|
||||
/** 超宽屏模式 */
|
||||
ultraWide: { width: 3440, height: 1440 } as WindowSize,
|
||||
|
||||
/** 4K 模式 */
|
||||
fourK: { width: 3840, height: 2160 } as WindowSize,
|
||||
};
|
||||
|
||||
export default windowApi;
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { Activity, RefreshCw, Download, BarChart3, List } from "lucide-react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import {
|
||||
Activity,
|
||||
RefreshCw,
|
||||
Download,
|
||||
BarChart3,
|
||||
List,
|
||||
ChevronDown,
|
||||
Monitor,
|
||||
Maximize,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
FlowList,
|
||||
FlowFilters,
|
||||
@@ -12,6 +21,7 @@ import {
|
||||
type FlowFilter,
|
||||
type ExportFormat,
|
||||
} from "@/lib/api/flowMonitor";
|
||||
import { windowApi, type WindowSizeOption } from "@/lib/api/window";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ViewMode = "list" | "stats";
|
||||
@@ -33,6 +43,48 @@ export function FlowMonitorPage() {
|
||||
// 刷新计数器(用于触发子组件刷新)
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// 窗口大小状态
|
||||
const [windowSizeOptions, setWindowSizeOptions] = useState<
|
||||
WindowSizeOption[]
|
||||
>([]);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showWindowMenu, setShowWindowMenu] = useState(false);
|
||||
|
||||
// 初始化窗口大小选项
|
||||
useEffect(() => {
|
||||
const loadWindowOptions = async () => {
|
||||
try {
|
||||
const options = await windowApi.getWindowSizeOptions();
|
||||
setWindowSizeOptions(options);
|
||||
|
||||
// 检查是否处于全屏模式
|
||||
const fullscreen = await windowApi.isFullscreen();
|
||||
setIsFullscreen(fullscreen);
|
||||
} catch (error) {
|
||||
console.error("加载窗口选项失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadWindowOptions();
|
||||
}, []);
|
||||
|
||||
// 点击外部关闭窗口菜单
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (showWindowMenu) {
|
||||
const target = event.target as Element;
|
||||
if (!target.closest(".window-menu-container")) {
|
||||
setShowWindowMenu(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [showWindowMenu]);
|
||||
|
||||
// 处理 Flow 选择
|
||||
const handleFlowSelect = useCallback((flow: LLMFlow) => {
|
||||
setSelectedFlow(flow);
|
||||
@@ -68,6 +120,27 @@ export function FlowMonitorPage() {
|
||||
console.log("导出成功:", filename);
|
||||
}, []);
|
||||
|
||||
// 设置窗口大小
|
||||
const handleSetWindowSize = useCallback(async (optionId: string) => {
|
||||
try {
|
||||
await windowApi.setWindowSizeByOption(optionId);
|
||||
setShowWindowMenu(false);
|
||||
} catch (error) {
|
||||
console.error("设置窗口大小失败:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 切换全屏模式
|
||||
const handleToggleFullscreen = useCallback(async () => {
|
||||
try {
|
||||
const newFullscreenState = await windowApi.toggleFullscreen();
|
||||
setIsFullscreen(newFullscreenState);
|
||||
setShowWindowMenu(false);
|
||||
} catch (error) {
|
||||
console.error("切换全屏模式失败:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面头部 */}
|
||||
@@ -110,6 +183,59 @@ export function FlowMonitorPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 窗口大小调整下拉菜单 */}
|
||||
<div className="relative window-menu-container">
|
||||
<button
|
||||
onClick={() => setShowWindowMenu(!showWindowMenu)}
|
||||
className="flex items-center gap-1 rounded-lg border px-3 py-2 text-sm hover:bg-muted"
|
||||
title="调整窗口大小"
|
||||
>
|
||||
<Monitor className="h-4 w-4" />
|
||||
窗口
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
{showWindowMenu && (
|
||||
<div className="absolute right-0 top-full mt-1 z-[100] min-w-[200px] rounded-lg border bg-background p-1 shadow-xl">
|
||||
{/* 窗口大小选项 */}
|
||||
<div className="px-2 py-1 text-xs font-medium text-muted-foreground">
|
||||
窗口大小
|
||||
</div>
|
||||
{windowSizeOptions.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
onClick={() => handleSetWindowSize(option.id)}
|
||||
className="w-full text-left px-2 py-1.5 text-sm rounded hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<div className="font-medium">{option.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{option.description}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* 分隔线 */}
|
||||
<div className="my-1 h-px bg-border" />
|
||||
|
||||
{/* 全屏选项 */}
|
||||
<button
|
||||
onClick={handleToggleFullscreen}
|
||||
className="w-full text-left px-2 py-1.5 text-sm rounded hover:bg-accent hover:text-accent-foreground flex items-center gap-2"
|
||||
>
|
||||
<Maximize className="h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{isFullscreen ? "退出全屏" : "全屏模式"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{isFullscreen ? "返回窗口模式" : "使用整个屏幕"}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 导出按钮 */}
|
||||
<button
|
||||
onClick={handleBatchExport}
|
||||
|
||||
Reference in New Issue
Block a user