mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: 修复 Flow Monitor 响应内容显示和提供商显示问题 (#75)
- 修复凭证池模式下非流式响应的 content 和 body 字段未保存问题 - 从响应 JSON 中提取 content、tool_calls 和 usage 信息 - 添加 provider_id 字段用于显示实际的提供商名称 - 优化清理完成弹窗样式,使用自定义 Modal 组件 - 美化排序下拉框,使用 Radix UI Select 组件 - 添加详细的调试日志用于追踪 Flow 显示问题 - 修复所有测试代码中缺少 provider_id 字段的问题
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -1269,6 +1269,7 @@ mod property_tests {
|
||||
fn arb_flow_metadata() -> impl Strategy<Value = FlowMetadata> {
|
||||
arb_provider_type().prop_map(|provider| FlowMetadata {
|
||||
provider,
|
||||
provider_id: None,
|
||||
credential_id: None,
|
||||
credential_name: None,
|
||||
retry_count: 0,
|
||||
|
||||
@@ -1618,6 +1618,7 @@ mod property_tests {
|
||||
fn arb_flow_metadata() -> impl Strategy<Value = FlowMetadata> {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>,
|
||||
/// 凭证 ID
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub credential_id: Option<String>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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<LLMResponse>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,10 +212,17 @@ impl FlowQueryService {
|
||||
page: usize,
|
||||
page_size: usize,
|
||||
) -> Result<FlowQueryResult, FileStoreError> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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::<ProviderType>()
|
||||
.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::<usize>() 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::<usize>() 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::<ProviderType>()
|
||||
.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::<ProviderType>()
|
||||
.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::<ProviderType>()
|
||||
.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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -35,6 +35,12 @@ export function CleanupDialog({
|
||||
const [cleanupType, setCleanupType] = useState<CleanupType>("ByTime");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<Modal isOpen={isOpen} onClose={handleSuccessClose} maxWidth="max-w-md">
|
||||
<div className="px-6 py-8">
|
||||
{/* 成功图标 */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="relative">
|
||||
<div className="w-20 h-20 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
||||
<svg
|
||||
className="w-10 h-10 text-green-600 dark:text-green-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/* 动画圆环 */}
|
||||
<div className="absolute inset-0 w-20 h-20 border-4 border-green-200 dark:border-green-800 rounded-full animate-ping opacity-75"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标题 */}
|
||||
<h3 className="text-2xl font-bold text-center mb-2">清理完成!</h3>
|
||||
<p className="text-center text-muted-foreground mb-6">
|
||||
已成功清理日志数据
|
||||
</p>
|
||||
|
||||
{/* 清理统计 */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center">
|
||||
<Hash className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">删除记录</span>
|
||||
</div>
|
||||
<span className="text-lg font-semibold">
|
||||
{cleanupResult.cleaned_count} 条
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-5 h-5 text-purple-600 dark:text-purple-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">清理文件</span>
|
||||
</div>
|
||||
<span className="text-lg font-semibold">
|
||||
{cleanupResult.cleaned_files} 个
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center">
|
||||
<HardDrive className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-green-900 dark:text-green-100">
|
||||
释放空间
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-green-600 dark:text-green-400">
|
||||
{formatBytes(cleanupResult.freed_bytes)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 确认按钮 */}
|
||||
<button
|
||||
onClick={handleSuccessClose}
|
||||
className="w-full py-3 bg-primary hover:bg-primary/90 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} maxWidth="max-w-2xl">
|
||||
{/* 标题 */}
|
||||
|
||||
@@ -383,7 +383,10 @@ function FlowDetailHeader({
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<InfoItem label="模型" value={flow.request.model} />
|
||||
<InfoItem label="提供商" value={flow.metadata.provider} />
|
||||
<InfoItem
|
||||
label="提供商"
|
||||
value={flow.metadata.provider_id || flow.metadata.provider}
|
||||
/>
|
||||
<InfoItem label="类型" value={formatFlowType(flow.flow_type)} />
|
||||
<InfoItem
|
||||
label="创建时间"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import * as Select from "@radix-ui/react-select";
|
||||
import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
Activity,
|
||||
Bell,
|
||||
BellOff,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
flowMonitorApi,
|
||||
@@ -570,16 +572,59 @@ export function FlowList({
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<select
|
||||
className="rounded border bg-background px-2 py-1 text-sm"
|
||||
<Select.Root
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as FlowSortBy)}
|
||||
onValueChange={(value) => setSortBy(value as FlowSortBy)}
|
||||
>
|
||||
<option value="created_at">按时间</option>
|
||||
<option value="duration">按耗时</option>
|
||||
<option value="total_tokens">按 Token</option>
|
||||
<option value="model">按模型</option>
|
||||
</select>
|
||||
<Select.Trigger className="inline-flex min-w-[120px] items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-1.5 text-sm shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<Select.Value />
|
||||
<Select.Icon>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content className="relative z-50 min-w-[120px] overflow-hidden rounded-md border border-border bg-white dark:bg-gray-900 text-foreground shadow-lg animate-in fade-in-80 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2">
|
||||
<Select.Viewport className="p-1 bg-white dark:bg-gray-900">
|
||||
<Select.Item
|
||||
value="created_at"
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>按时间</Select.ItemText>
|
||||
</Select.Item>
|
||||
<Select.Item
|
||||
value="duration"
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>按耗时</Select.ItemText>
|
||||
</Select.Item>
|
||||
<Select.Item
|
||||
value="total_tokens"
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>按 Token</Select.ItemText>
|
||||
</Select.Item>
|
||||
<Select.Item
|
||||
value="model"
|
||||
className="relative flex cursor-pointer select-none items-center rounded-sm px-8 py-2 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
>
|
||||
<Select.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>按模型</Select.ItemText>
|
||||
</Select.Item>
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
<button
|
||||
onClick={() => setSortDesc(!sortDesc)}
|
||||
className="rounded border px-2 py-1 text-sm hover:bg-muted"
|
||||
|
||||
@@ -333,6 +333,7 @@ export interface RoutingInfo {
|
||||
*/
|
||||
export interface FlowMetadata {
|
||||
provider: ProviderType;
|
||||
provider_id?: string; // 实际的 provider ID(如 "deepseek", "moonshot" 等)
|
||||
credential_id?: string;
|
||||
credential_name?: string;
|
||||
retry_count: number;
|
||||
|
||||
Reference in New Issue
Block a user