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(() => {