v0.17.1: Flow Monitor 增强 - 窗口大小调整、搜索优化

- 添加窗口大小调整功能(紧凑、默认、大屏、超大屏、超宽屏、4K)
- 添加全屏切换功能
- 优化简单过滤搜索,支持搜索模型名称和提供商
- 修复下拉菜单背景透明问题
- 添加表达式模式搜索按钮
This commit is contained in:
coso
2025-12-22 19:44:55 +08:00
parent 32620e2d8d
commit 0859a01ca8
17 changed files with 951 additions and 86 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "proxycast",
"private": true,
"version": "0.17.0",
"version": "0.17.1",
"type": "module",
"repository": {
"type": "git",
+1 -1
View File
@@ -3367,7 +3367,7 @@ dependencies = [
[[package]]
name = "proxycast"
version = "0.17.0"
version = "0.17.1"
dependencies = [
"anyhow",
"async-stream",
+1 -1
View File
@@ -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"
+159
View File
@@ -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**
+1
View File
@@ -15,3 +15,4 @@ pub mod telemetry_cmd;
pub mod tray_cmd;
pub mod usage_cmd;
pub mod websocket_cmd;
pub mod window_cmd;
+396
View File
@@ -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);
}
}
+1 -1
View File
@@ -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))
}
+15 -2
View File
@@ -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;
}
}
+13
View File
@@ -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 -1
View File
@@ -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",
+1 -49
View File
@@ -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>
{/* 内容区域 */}
+19 -8
View File
@@ -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 && (
+29
View File
@@ -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}
View File
+38 -20
View File
@@ -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 });
},
};
+147
View File
@@ -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;
+128 -2
View File
@@ -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}