fix(web): 修复话题重命名预填字段与写入字段不一致

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
This commit is contained in:
oudecheng 2026-08-04 23:57:04 +08:00
parent c2cc072b2e
commit adada2652c
2 changed files with 48 additions and 8 deletions

View File

@ -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();

View File

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