From adada2652c1eba869a0ff22c2f9d6069d1bfda4d Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Tue, 4 Aug 2026 23:57:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E4=BF=AE=E5=A4=8D=E8=AF=9D?= =?UTF-8?q?=E9=A2=98=E9=87=8D=E5=91=BD=E5=90=8D=E9=A2=84=E5=A1=AB=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E4=B8=8E=E5=86=99=E5=85=A5=E5=AD=97=E6=AE=B5=E4=B8=8D?= =?UTF-8?q?=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: 点击铅笔编辑时,输入框显示的是 topic.title(系统生成的 id+时间), 而不是用户看到的 topic.description(AI 生成的摘要)。且后端 rename 写入 title, 但列表显示优先 description,导致改名后视觉上无变化。 根因:显示用 'description || title',编辑预填用 'title',写入也改 'title', 三者不一致。 修复: - 前端 startEdit: 预填 'description || title'(与显示逻辑一致) - 后端 rename_topic: 改用 update_topic_description 写入显示字段, 保留 title 作为内部标识;'未变化'比较也基于当前显示值 - 新增测试: 验证有 description 时比较逻辑基于 description 而非 title --- src/command/handlers/rename_topic.rs | 53 ++++++++++++++++++++---- web/src/components/Sidebar/TopicList.tsx | 3 +- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/command/handlers/rename_topic.rs b/src/command/handlers/rename_topic.rs index 0db12b7..6d83d4f 100644 --- a/src/command/handlers/rename_topic.rs +++ b/src/command/handlers/rename_topic.rs @@ -75,10 +75,15 @@ async fn handle_rename_topic( CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)) })?; - let old_title = topic.title.clone(); + let old_display = topic + .description + .as_deref() + .filter(|s| !s.is_empty()) + .unwrap_or(&topic.title) + .to_string(); // 标题未变化时直接返回当前列表,避免无意义写入 - if old_title == trimmed_title { + if old_display == trimmed_title { let topics = handler .store .list_topics(session_id) @@ -98,10 +103,10 @@ async fn handle_rename_topic( .with_metadata("session_id", session_id)); } - // 执行重命名(存储层方法已存在) + // 执行重命名:更新 description(显示字段),保留 title 作为内部标识 handler .store - .update_topic_title(&topic_id, trimmed_title) + .update_topic_description(&topic_id, trimmed_title) .map_err(|e| CommandError::new("RENAME_TOPIC_ERROR", e.to_string()))?; // 查询更新后的话题列表,返回给前端刷新侧边栏 @@ -114,7 +119,7 @@ async fn handle_rename_topic( let topic_summaries_json = serde_json::to_string(&topic_summaries) .map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?; - let message = format!("✓ 已重命名话题: {} → {}", old_title, trimmed_title); + let message = format!("✓ 已重命名话题: {} → {}", old_display, trimmed_title); Ok(CommandResponse::success(ctx.request_id) .with_message(MessageKind::Notification, &message) @@ -165,9 +170,10 @@ mod tests { ); assert!(resp.metadata.contains_key("topics")); - // 验证存储层已更新 + // 验证存储层已更新 description(显示字段),title 保持原内部标识 let updated = store.get_topic(&topic.id).unwrap().unwrap(); - assert_eq!(updated.title, "new title"); + assert_eq!(updated.title, "old title"); + assert_eq!(updated.description.as_deref(), Some("new title")); } #[tokio::test] @@ -222,6 +228,7 @@ mod tests { let store = handler.store.clone(); let session = store.create_session("test_channel", Some("test")).unwrap(); + // description=None,当前显示值 fallback 到 title let topic = store.create_topic(&session.id, "same title", None).unwrap(); let original_updated_at = store.get_topic(&topic.id).unwrap().unwrap().updated_at; @@ -244,6 +251,38 @@ mod tests { assert_eq!(after.updated_at, original_updated_at); } + #[tokio::test] + async fn test_rename_topic_compares_against_description_when_present() { + // 有 description 时,"未变化"比较应基于 description 而非 title + let handler = create_test_handler(); + let store = handler.store.clone(); + + let session = store.create_session("test_channel", Some("test")).unwrap(); + // title 是内部标识,description 是显示值 + let topic = store + .create_topic(&session.id, "topic_internal_id", Some("AI 生成的摘要")) + .unwrap(); + let original_updated_at = store.get_topic(&topic.id).unwrap().unwrap().updated_at; + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let ctx = CommandContext::new("test", "test_channel") + .with_session_id(&session.id) + .with_chat_id(&session.id); + // 提交与 description 相同的值(不是 title),应跳过写入 + let cmd = Command::RenameTopic { + topic_id: topic.id.clone(), + title: "AI 生成的摘要".to_string(), + }; + + let result = handler.handle(cmd, ctx).await; + assert!(result.is_ok()); + + let after = store.get_topic(&topic.id).unwrap().unwrap(); + assert_eq!(after.updated_at, original_updated_at); + assert_eq!(after.title, "topic_internal_id"); + } + #[test] fn test_can_handle() { let handler = create_test_handler(); diff --git a/web/src/components/Sidebar/TopicList.tsx b/web/src/components/Sidebar/TopicList.tsx index e954df2..752b033 100644 --- a/web/src/components/Sidebar/TopicList.tsx +++ b/web/src/components/Sidebar/TopicList.tsx @@ -93,7 +93,8 @@ export function TopicList({ const startEdit = useCallback((topic: Topic) => { setConfirmDeleteId(null); setEditingTopicId(topic.id); - setEditingTitle(topic.title); + // 预填当前显示值(description 优先,与列表显示逻辑一致) + setEditingTitle(topic.description || topic.title); }, []); const cancelEdit = useCallback(() => {