优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)

This commit is contained in:
2026-06-18 22:57:19 +08:00
parent 0ca5d9805f
commit a2871a66e0
87 changed files with 5720 additions and 3012 deletions

View File

@@ -6,10 +6,11 @@ use tauri::{AppHandle, Emitter};
use tokio::sync::Mutex;
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
// F-01 阶段5: 标题生成路由 — TaskRequirements(Lite + Low 成本,无需工具)。
// 标题是短文本摘要任务,选便宜 Lite 模型;池空/无匹配兜底 default_model(行为不变)。
// F-01 阶段5: 标题生成路由 — TaskRequirements(无需工具)。
// 标题是短文本摘要任务;池空/无匹配兜底 default_model(行为不变)。
// 注:路由已解耦(B-260618-03),原 Lite/Low 智力成本约束已删除,纯 weight 选模型。
use df_ai::router::{
select_model_id, CostTier, IntelligenceTier, Modality, TaskRequirements,
select_model_id, Modality, TaskRequirements,
};
use df_storage::crud::AiConversationRepo;
use df_storage::db::Database;
@@ -56,6 +57,7 @@ pub(crate) async fn ensure_conversation_title(
model: None,
status: None,
reasoning_content: m.reasoning_content.clone(),
timestamp: m.timestamp,
})
.collect();
(summary, session.messages.all_messages_clone())
@@ -64,35 +66,55 @@ pub(crate) async fn ensure_conversation_title(
return;
}
// B-260617-17 修复:进入即 extract_title 兜底落库 + emit,保证侧栏即时有非"新对话"标题。
// 原实现仅在 LLM 返回 None(失败)或 provider 构建失败时才落 extract——但 LLM 卡住
// (generate_title_via_llm 的 llm_concurrency 信号量 acquire 阻塞 / 网络挂起 / 后台
// spawn 未跑完即刷新)时,既不返回 None 也不落库 → 标题长期停留"新对话"。现先落 extract
// 兜底,下方 LLM 生成成功后覆盖;LLM 卡住/失败/超时则保留兜底,无论如何不再"新对话"。
let fallback_title = extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string());
if let Err(e) = conv_repo.update_field(conv_id, "title", &fallback_title).await {
tracing::error!("对话标题兜底落库失败(conv_id={}, title={}): {}", conv_id, fallback_title, e);
}
let _ = app_handle.emit("ai-conversation-changed", ());
// 标题生成是独立一次 LLM 调用,自建 provider便于后台 spawn不借主 loop 的 &dyn LlmProvider
// build_provider_for 含空 key 早失败:Err(如 keyring 读不到)→ 跳过 LLM,extract_title 兜底
let provider: Box<dyn LlmProvider> = match super::secret::build_provider_for(provider_config) {
Ok(p) => p,
Err(e) => {
tracing::warn!("标题生成跳过(provider 密钥不可用,走 extract_title 兜底): {}", e);
let title = extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string());
let _ = conv_repo.update_field(conv_id, "title", &title).await;
let _ = app_handle.emit("ai-conversation-changed", ());
// 兜底已落库 + emit,LLM 跳过(密钥不可用等)
tracing::warn!("标题 LLM 生成跳过(provider 密钥不可用,保留 extract_title 兜底): {}", e);
return;
}
};
// F-01 阶段5: 标题生成路由 — 选便宜 Lite 模型(Low 成本上限,无工具)
// select_model_id None(池空/无匹配)→ 兜底 default_model。
// F-01 阶段5: 标题生成路由 — select_model_id None(池空/无匹配)→ 兜底 default_model
let title_req = TaskRequirements {
modalities: vec![Modality::Text],
needs_tool_use: false,
min_intelligence: IntelligenceTier::Lite,
max_cost: Some(CostTier::Low),
estimated_context: 0,
};
let title_model = select_model_id(&title_req, &provider_config.model_configs)
.unwrap_or_else(|| provider_config.default_model.clone());
let title = match generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency).await {
Some(t) => t,
None => extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string()),
// B-260617-17: LLM 调用包 20s 超时,防信号量/网络挂起致后台 task 永久堆积;
// 超时或返回 None 均保留上方已落的 extract 兜底,不覆盖。
let title = match tokio::time::timeout(
std::time::Duration::from_secs(20),
generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency),
).await {
Ok(Some(t)) => t,
Ok(None) => {
tracing::warn!("标题 LLM 生成返回空,保留 extract_title 兜底(conv_id={})", conv_id);
return;
}
Err(_) => {
tracing::warn!("标题 LLM 生成超时(20s),保留 extract_title 兜底(conv_id={})", conv_id);
return;
}
};
let _ = conv_repo.update_field(conv_id, "title", &title).await;
// LLM 生成成功 → 覆盖 extract 兜底
if let Err(e) = conv_repo.update_field(conv_id, "title", &title).await {
tracing::error!("对话标题 LLM 落库失败(conv_id={}, title={}): {}", conv_id, title, e);
}
let _ = app_handle.emit("ai-conversation-changed", ());
}