From 3a623cc8a3975f377a21fa6aa236c085edcc7fe1 Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sun, 7 Jun 2026 16:52:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=AF=9D=E9=A2=98?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91=E7=9A=84?= =?UTF-8?q?=E7=AB=9E=E6=80=81=E6=9D=A1=E4=BB=B6=E5=92=8C=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=8C=E5=89=8D=E7=AB=AF=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - topic_description.rs: LLM 返回空字符串时返回 Err 而非 Ok(""),防止空值写回 DB 触发循环生成 - processor.rs: 添加 Arc> 生成中守卫防止重复触发,改用 DB 中真正第一条用户消息生成描述 - useChat.ts: assistant_response 时检测当前话题描述为空则递增刷新信号 - App.tsx: 监听刷新信号,500ms 防抖后自动发送 list_topics 获取新描述 --- src/gateway/processor.rs | 74 +++++++++++++++++++++++++++++----------- src/topic_description.rs | 8 +++-- web/src/App.tsx | 16 +++++++++ web/src/hooks/useChat.ts | 19 +++++++++++ 4 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/gateway/processor.rs b/src/gateway/processor.rs index 8d1d841..036c2f8 100644 --- a/src/gateway/processor.rs +++ b/src/gateway/processor.rs @@ -1,4 +1,5 @@ -use std::sync::Arc; +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; use tokio::sync::Semaphore; @@ -35,6 +36,7 @@ pub struct InboundProcessor { provider_config: LLMProviderConfig, command_router: Arc, cancel_manager: CancelManager, + description_generation_in_flight: Arc>>, } impl InboundProcessor { @@ -120,6 +122,7 @@ impl InboundProcessor { provider_config, command_router: Arc::new(command_router), cancel_manager, + description_generation_in_flight: Arc::new(Mutex::new(HashSet::new())), } } @@ -285,33 +288,64 @@ impl InboundProcessor { } } - // 异步生成 topic 描述(仅第一条消息后触发一次) + // 异步生成 topic 描述(仅当描述为空且没有正在进行的生成任务时触发) if let Some(ref topic_id) = current_topic { let store = self.session_manager.store(); if let Ok(Some(topic)) = store.get_topic(topic_id) { if topic.description.is_none() || topic.description.as_ref().map(|d| d.is_empty()).unwrap_or(true) { - let provider_config = self.provider_config.clone(); - let topic_id_clone = topic_id.clone(); - let first_message = inbound.content.clone(); - let store_clone = store.clone(); + // 检查并设置"生成中"守卫,防止竞态条件导致重复生成 + let should_generate = { + let mut in_flight = self.description_generation_in_flight.lock().unwrap(); + if in_flight.contains(topic_id) { + false + } else { + in_flight.insert(topic_id.clone()); + true + } + }; - tokio::spawn(async move { - let runtime_config: ProviderRuntimeConfig = provider_config.into(); - if let Ok(provider) = create_provider(runtime_config) { - match generate_topic_description(provider.as_ref(), &first_message).await { - Ok(description) => { - if let Err(e) = store_clone.update_topic_description(&topic_id_clone, &description) { - tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to update topic description"); - } else { - tracing::info!(topic_id = %topic_id_clone, description = %description, "Topic description generated"); + if should_generate { + let provider_config = self.provider_config.clone(); + let topic_id_clone = topic_id.clone(); + let store_clone = store.clone(); + let in_flight = self.description_generation_in_flight.clone(); + + tokio::spawn(async move { + // 从 DB 查询该 topic 的第一条用户消息作为描述生成的依据 + let first_user_message = store_clone + .load_messages_for_topic(&topic_id_clone) + .ok() + .and_then(|msgs| msgs.into_iter().find(|m| m.role == "user")) + .map(|m| m.content); + + let message_content = match first_user_message { + Some(content) => content, + None => { + tracing::warn!(topic_id = %topic_id_clone, "No user message found for topic, skipping description generation"); + in_flight.lock().unwrap().remove(&topic_id_clone); + return; + } + }; + + let runtime_config: ProviderRuntimeConfig = provider_config.into(); + if let Ok(provider) = create_provider(runtime_config) { + match generate_topic_description(provider.as_ref(), &message_content).await { + Ok(description) => { + if let Err(e) = store_clone.update_topic_description(&topic_id_clone, &description) { + tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to update topic description"); + } else { + tracing::info!(topic_id = %topic_id_clone, description = %description, "Topic description generated"); + } + } + Err(e) => { + tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to generate topic description"); } } - Err(e) => { - tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to generate topic description"); - } } - } - }); + // 无论成功失败,释放生成守卫 + in_flight.lock().unwrap().remove(&topic_id_clone); + }); + } } } } diff --git a/src/topic_description.rs b/src/topic_description.rs index ea69365..6d15666 100644 --- a/src/topic_description.rs +++ b/src/topic_description.rs @@ -17,11 +17,15 @@ pub async fn generate_topic_description( }; let response = provider.chat(request).await?; - let description = response.content.trim(); + let description = response.content.trim().to_string(); + + if description.is_empty() { + return Err("LLM returned empty description".into()); + } if description.len() > 50 { Ok(description.chars().take(50).collect()) } else { - Ok(description.to_string()) + Ok(description) } } \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index b6b2bbc..12f680f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -60,6 +60,7 @@ function App() { deleteTopic, requestSessionList, requestTopicList, + topicRefreshTrigger, enterSubAgentView, exitSubAgentView, handleStop, @@ -125,6 +126,21 @@ function App() { } }, [sessionId, status, handleCommand, sendMessage, requestTopicList]) + // 话题描述异步生成后自动刷新话题列表 + useEffect(() => { + if (topicRefreshTrigger === 0) return + if (status !== 'connected') return + const topicCmd = requestTopicList() + if (!topicCmd) return + + const timer = setTimeout(() => { + handleCommand(topicCmd) + sendMessage({ type: 'command', payload: JSON.stringify(topicCmd) }) + }, 500) + + return () => clearTimeout(timer) + }, [topicRefreshTrigger]) + // Topics 加载后,自动选择第一个并通知后端切换,以便加载历史消息 useEffect(() => { if (topics.length === 0 || status !== 'connected') { diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index e288f25..4a190d6 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -68,6 +68,7 @@ interface UseChatReturn { // 初始化方法 requestSessionList: () => Command requestTopicList: () => Command | null + topicRefreshTrigger: number requestChannelList: () => Command selectChannel: (channelId: string) => void selectSession: (sessionId: string) => void @@ -118,6 +119,7 @@ export function useChat(): UseChatReturn { const [connectionId, setConnectionId] = useState(null) const [topics, setTopics] = useState([]) const [selectedTopic, setSelectedTopic] = useState(null) + const [topicRefreshTrigger, setTopicRefreshTrigger] = useState(0) const [sessions, setSessions] = useState([]) const [selectedSessionId, setSelectedSessionId] = useState(null) const [subAgentView, setSubAgentView] = useState(null) @@ -137,6 +139,8 @@ export function useChat(): UseChatReturn { // Ref to track subAgentView and schedulerView for use in callbacks const subAgentViewRef = useRef(null) const schedulerViewRef = useRef(null) + const topicsRef = useRef([]) + const selectedTopicRef = useRef(null) const isConnected = useMemo(() => connectionId !== null, [connectionId]) const selectedSession = useMemo( @@ -401,6 +405,12 @@ export function useChat(): UseChatReturn { }, ]) setIsLoading(false) + + // 当前话题无描述时,可能刚触发了异步生成,标记需要刷新 + const currentTopic = topicsRef.current.find(t => t.id === selectedTopicRef.current) + if (currentTopic && !currentTopic.description) { + setTopicRefreshTrigger(n => n + 1) + } break } @@ -617,6 +627,14 @@ export function useChat(): UseChatReturn { schedulerViewRef.current = schedulerView }, [schedulerView]) + useEffect(() => { + topicsRef.current = topics + }, [topics]) + + useEffect(() => { + selectedTopicRef.current = selectedTopic + }, [selectedTopic]) + const enterSubAgentView = useCallback((taskId: string, description: string): Command => { const newView: SubAgentView = { taskId, @@ -714,6 +732,7 @@ export function useChat(): UseChatReturn { deleteTopic, requestSessionList, requestTopicList, + topicRefreshTrigger, requestChannelList, selectChannel, selectSession,