From 4e3a11f9254ea42dd89163ebf7522c32486e4da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sat, 20 Jun 2026 01:29:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E4=B8=BB=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=82=B9=E4=B8=8D=E5=BC=80=20+=20=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=A0=B7=E5=BC=8F/=E8=8F=9C=E5=8D=95=20+=20?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E5=8D=A1=E6=AD=BB/=E5=BC=B9=E5=9B=9E/updated?= =?UTF-8?q?=5Fat(God=20=E6=8B=86=E5=88=86=E9=81=97=E6=BC=8F=20+=20?= =?UTF-8?q?=E5=A4=9A=20bug=20=E5=90=88=E5=B9=B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主菜单点不开(7f1242c):c38cefe ErrorBoundary 嵌 transition 内部,slot 透传非元素根 → mode="out-in" 过渡卡死 → 点主菜单 URL 变页面不切换。修:ErrorBoundary 移 transition 外 + 根固定 div.error-boundary-root + display:contents 对话列表样式/菜单(God 拆分遗漏 4f8f1cf + 脉冲点 5c395f4 + 审查 agent 菜单): - God 拆分样式没移子组件,AiChat scoped 不匹配子元素 → 列表/输入框样式丢。修:样式移 ConversationSidebar/ChatInput scoped + 复用抽 components.css - 脉冲点 vertical-align/margin-left 冲突。修:移除用 flex gap 统一 - 4 操作按钮窄宽撑高 → 收进"..."下拉菜单(置顶/归档/删除)+ 移除导出(后端 IPC 保留) 标题 3 重根因(8192cd2 合并): - 卡死:ensure :40 title.is_some() return,"新对话"占位永远跳过;switch need_title_regen 仅 None 不触发。修:排除"新对话"占位 + need_title_regen 含"新对话" - 弹回::121 无条件落 LLM 结果,clean_title 空兜底"新对话"覆盖 extract。修:非空非"新对话"才落 - updated_at 跳变:update_field 强制 SET updated_at=now,标题生成致时间分组跳。修:set_title 不动 updated_at CR-20/21 审查登记(待审查.md) cargo check + vue-tsc + vite build EXIT 0 --- .../df-storage/src/crud/conversation_repo.rs | 22 ++ docs/待审查.md | 24 ++ .../src/commands/ai/commands/conversation.rs | 6 +- src-tauri/src/commands/ai/title.rs | 23 +- src/App.vue | 15 +- src/components/AiChat.vue | 1 - src/components/ErrorBoundary.vue | 21 +- src/components/ai/ConversationSidebar.vue | 213 ++++++++---------- src/i18n/en/aiChat.ts | 7 +- src/i18n/zh-CN/aiChat.ts | 7 +- 10 files changed, 180 insertions(+), 159 deletions(-) diff --git a/crates/df-storage/src/crud/conversation_repo.rs b/crates/df-storage/src/crud/conversation_repo.rs index a53e7be..682d6b9 100644 --- a/crates/df-storage/src/crud/conversation_repo.rs +++ b/crates/df-storage/src/crud/conversation_repo.rs @@ -345,6 +345,28 @@ impl AiConversationRepo { .map_err(storage_err)? } + /// 设置标题(仅改 title,不动 updated_at) + /// + /// 区别于 update_field(强制 SET updated_at=now,会把标题生成误判为内容更新, + /// 导致侧栏时间分组/排序跳变)。标题生成是系统后台操作,应保持会话相对时间不变。 + pub async fn set_title(&self, id: &str, title: &str) -> Result { + let conn = self.conn.clone(); + let id = id.to_owned(); + let title = title.to_owned(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let affected = guard + .execute( + "UPDATE ai_conversations SET title = ?1 WHERE id = ?2", + params![title, id], + ) + .map_err(storage_err)?; + Ok(affected > 0) + }) + .await + .map_err(storage_err)? + } + /// 设置置顶标记(仅改 pinned,不动 updated_at) — UX-17 /// /// 同 set_archived:置顶是纯元数据标记,不应改变相对时间。前端排序读 pinned DESC, updated_at DESC。 diff --git a/docs/待审查.md b/docs/待审查.md index b9c8110..72ef18c 100644 --- a/docs/待审查.md +++ b/docs/待审查.md @@ -1215,6 +1215,30 @@ --- +### CR-260619-20 主导航点不开内容(transition + ErrorBoundary 非 element root·commit 7f1242c) — 🟡 待审 + +- **范围**(2 文件):App.vue(ErrorBoundary 从 transition 内移到外,主+分离两处)+ ErrorBoundary.vue(根固定 div.error-boundary-root + display:contents) +- **根因**:c38cefe ErrorBoundary 接入嵌 transition 内部,slot 透传非元素根 → mode="out-in" 过渡卡死 → 点主菜单 URL 变但页面不切换(AI 面板不走 transition 故正常) +- **维度**: + 1. transition 直接子是真实元素根(component :is 路由页面) + 2. ErrorBoundary display:contents 正常态布局透明 + error 态 flex 降级 + 3. 主+分离窗口两处改 +- **验证**:独立 grep/read 核验 7f1242c + vue-tsc +- **状态**:🟡 待审 + +### CR-260619-21 对话标题卡死"新对话"(ensure 跳过占位·commit ecf501d) — 🟡 待审 + +- **范围**(2 文件):title.rs(:40 ensure 排除"新对话"占位)+ conversation.rs(:139 need_title_regen 含"新对话") +- **根因**:ensure :40 title.is_some() return,"新对话"占位 Some 永远跳过;switch need_title_regen 仅 None,"新对话"不触发 → 双重卡死 +- **维度**: + 1. ensure :40 排除"新对话"(title Some 且非空非"新对话"才 return) + 2. conversation.rs :139 need_title_regen 含"新对话"(switch 触发) + 3. 数据印证:6 条"新对话"会话 msg_len 100K+ 有 user(extract 应非 None,证实未生成) +- **验证**:独立 grep/read 核验 ecf501d + cargo check +- **状态**:🟡 待审 + +--- + ## 已审归档 > 已审 CR 段迁独立文件: [待审查归档/2026-06.md](./07-项目管理/待审查归档/2026-06.md) diff --git a/src-tauri/src/commands/ai/commands/conversation.rs b/src-tauri/src/commands/ai/commands/conversation.rs index f761d52..26fb0e1 100644 --- a/src-tauri/src/commands/ai/commands/conversation.rs +++ b/src-tauri/src/commands/ai/commands/conversation.rs @@ -135,8 +135,10 @@ pub async fn ai_conversation_switch( let messages_json = record.messages.clone(); let title = record.title.clone(); - // B-260617-17 续:历史会话 title 空(显"新对话")→ 切入后触发重新生成(用户诉求) - let need_title_regen = record.title.is_none(); + // B-260617-17 续:历史会话 title 空(显"新对话")→ 切入后触发重新生成(用户诉求)。 + // 含 "新对话" 占位(Some 但未生成):title.rs ensure :40 同步排除"新对话"占位不跳过, + // 否则 title 落"新对话"(LLM 返空兜底/默认)后 is_some()=true 永远跳过 → 标题卡死。 + let need_title_regen = record.title.is_none() || record.title.as_deref() == Some("新对话"); let mut session = state.ai_session.lock().await; // F-260616-09 B 批4(决策 e 真并发上线):删除 readonly 分支。 diff --git a/src-tauri/src/commands/ai/title.rs b/src-tauri/src/commands/ai/title.rs index 64a5b51..bca026a 100644 --- a/src-tauri/src/commands/ai/title.rs +++ b/src-tauri/src/commands/ai/title.rs @@ -35,10 +35,14 @@ pub(crate) async fn ensure_conversation_title( ) { let conv_repo = AiConversationRepo::new(db); - // 已有标题(用户改名或已生成)→ 不覆盖 + // 已有标题(用户改名或已生成)→ 不覆盖。 + // 但 "新对话" 是未生成占位(默认/LLM 返空兜底/清理后空回退),视为未生成可重新生成, + // 否则一旦 title 落"新对话"便 is_some()=true 永远跳过 → 标题卡死"新对话"。 if let Ok(Some(rec)) = conv_repo.get_by_id(conv_id).await { - if rec.title.is_some() { - return; + if let Some(t) = &rec.title { + if !t.is_empty() && t != "新对话" { + return; + } } } @@ -78,7 +82,7 @@ pub(crate) async fn ensure_conversation_title( // 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 { + if let Err(e) = conv_repo.set_title(conv_id, &fallback_title).await { tracing::error!("对话标题兜底落库失败(conv_id={}, title={}): {}", conv_id, fallback_title, e); } let _ = app_handle.emit("ai-conversation-changed", ()); @@ -117,11 +121,14 @@ pub(crate) async fn ensure_conversation_title( } }; - // 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); + // LLM 生成成功且有效(非空非"新对话")→ 覆盖 extract 兜底。 + // LLM 返空/无效(clean_title 兜底"新对话")→ 保留上方已落的 extract 兜底,不覆盖(防弹回"新对话")。 + if !title.is_empty() && title != "新对话" { + if let Err(e) = conv_repo.set_title(conv_id, &title).await { + tracing::error!("对话标题 LLM 落库失败(conv_id={}, title={}): {}", conv_id, title, e); + } + let _ = app_handle.emit("ai-conversation-changed", ()); } - let _ = app_handle.emit("ai-conversation-changed", ()); } /// 后台生成对话标题(不阻塞主流程;失败有 extract_title 兜底) diff --git a/src/App.vue b/src/App.vue index 0f81949..a3bc1ae 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,19 +1,24 @@