修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
This commit is contained in:
@@ -82,15 +82,10 @@ pub(crate) async fn ensure_conversation_title(
|
||||
// 在传给 LLM 前合并连续同 role(拼接 content),不影响其他逻辑(本函数 summary 派生数据)。
|
||||
let summary_msgs = merge_consecutive_roles(summary_msgs);
|
||||
|
||||
// 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.set_title(conv_id, &fallback_title).await {
|
||||
tracing::error!("对话标题兜底落库失败(conv_id={}, title={}): {}", conv_id, fallback_title, e);
|
||||
}
|
||||
// B-260617-17 修复:先用 extract 兜底 emit 让侧栏即时更新(不落库),
|
||||
// 下方 LLM 生成成功后覆盖。这样 LLM 失败时有重试机会,
|
||||
// 不会永久锁定一个机械截取的差标题。
|
||||
let _fallback_title = extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string());
|
||||
let _ = app_handle.emit("ai-conversation-changed", ());
|
||||
|
||||
// 标题生成是独立一次 LLM 调用,自建 provider(便于后台 spawn,不借主 loop 的 &dyn LlmProvider)
|
||||
@@ -204,15 +199,43 @@ fn clean_title(raw: &str) -> String {
|
||||
if cleaned.is_empty() { "新对话".to_string() } else { cleaned }
|
||||
}
|
||||
|
||||
/// 从消息历史中提取对话标题(取第一条用户消息前 30 字)
|
||||
/// 从消息历史中提取对话标题。策略:取前 5 条 user 消息中字符数最多的那条。
|
||||
///
|
||||
/// 开头常出现测试/问候("1"、"你好"等),取最长消息自然跳过这些噪音。
|
||||
/// 极端情况全部过短时(仅测试消息),用第一条非空 user 兜底。
|
||||
pub(crate) fn extract_title(messages: &[ChatMessage]) -> Option<String> {
|
||||
messages.iter()
|
||||
.find(|m| matches!(m.role, MessageRole::User))
|
||||
.map(|m| {
|
||||
let mut chars = m.content.chars();
|
||||
let t: String = chars.by_ref().take(30).collect();
|
||||
if chars.next().is_some() { format!("{}...", t) } else { t }
|
||||
})
|
||||
const MAX_TITLE_CHARS: usize = 60;
|
||||
const SCAN_LIMIT: usize = 5;
|
||||
|
||||
let user_msgs: Vec<&str> = messages.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::User))
|
||||
.take(SCAN_LIMIT)
|
||||
.map(|m| m.content.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
// 取字符数最多的
|
||||
let best = user_msgs.iter()
|
||||
.max_by_key(|s| s.chars().count())
|
||||
.copied();
|
||||
|
||||
match best {
|
||||
Some(text) => {
|
||||
let mut chars = text.chars();
|
||||
let t: String = chars.by_ref().take(MAX_TITLE_CHARS).collect();
|
||||
if chars.next().is_some() { Some(format!("{}...", t)) } else { Some(t) }
|
||||
}
|
||||
None => {
|
||||
// 极端情况全空 → 第一条非空 user(兜底)
|
||||
messages.iter()
|
||||
.find(|m| matches!(m.role, MessageRole::User) && !m.content.trim().is_empty())
|
||||
.map(|m| {
|
||||
let mut chars = m.content.trim().chars();
|
||||
let t: String = chars.by_ref().take(MAX_TITLE_CHARS).collect();
|
||||
if chars.next().is_some() { format!("{}...", t) } else { t }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 合并相邻同 role 消息,拼接 content(防 Anthropic 1214 连续同 role 错误)。
|
||||
|
||||
Reference in New Issue
Block a user