From 0732b31e6bc8ba7a42c6f9cf0d72586f45faa2a2 Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sun, 24 May 2026 08:32:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E7=94=9F=E6=88=90=E5=92=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/command/handlers/get_current.rs | 13 +++++++++- src/command/handlers/list_sessions.rs | 7 +++++ src/gateway/processor.rs | 37 +++++++++++++++++++++++++-- src/lib.rs | 1 + src/providers/traits.rs | 18 +++++++++++++ src/storage/mod.rs | 10 ++++++++ src/topic_description.rs | 27 +++++++++++++++++++ 7 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/topic_description.rs diff --git a/src/command/handlers/get_current.rs b/src/command/handlers/get_current.rs index 9b44fa0..b1d1884 100644 --- a/src/command/handlers/get_current.rs +++ b/src/command/handlers/get_current.rs @@ -114,10 +114,21 @@ async fn handle_get_current_session( let last_active = format_time_ago(topic.last_active_at); let created_at = format_time_ago(topic.created_at); + let description_line = if let Some(ref desc) = topic.description { + if !desc.is_empty() { + format!("\n Description: {}", desc) + } else { + String::new() + } + } else { + String::new() + }; + let message = format!( - "Current Topic:\n\n Topic ID: {}\n Title: {}\n Messages: {}\n Tokens: ~{} (系统提示词: ~{}, 用户消息: ~{})\n Created: {}\n Last Active: {}", + "Current Topic:\n\n Topic ID: {}\n Title: {}{}\n Messages: {}\n Tokens: ~{} (系统提示词: ~{}, 用户消息: ~{})\n Created: {}\n Last Active: {}", topic.id, topic.title, + description_line, actual_message_count, total_tokens, system_prompt_tokens, diff --git a/src/command/handlers/list_sessions.rs b/src/command/handlers/list_sessions.rs index ccdbb46..d7bf0dc 100644 --- a/src/command/handlers/list_sessions.rs +++ b/src/command/handlers/list_sessions.rs @@ -80,6 +80,13 @@ async fn handle_list_sessions( "{}. {}{} ({})", num, topic.title, marker, msg_count )); + + // 显示描述(如果有) + if let Some(ref desc) = topic.description { + if !desc.is_empty() { + lines.push(format!(" {}", desc)); + } + } } lines.push(String::new()); diff --git a/src/gateway/processor.rs b/src/gateway/processor.rs index 7e65b56..8f25e7f 100644 --- a/src/gateway/processor.rs +++ b/src/gateway/processor.rs @@ -17,8 +17,10 @@ use crate::command::handlers::session::SessionCommandHandler; use crate::command::handlers::switch_session::SwitchSessionCommandHandler; use crate::config::LLMProviderConfig; use crate::gateway::agent_prompt_provider::AgentPromptProvider; +use crate::providers::{create_provider, ProviderRuntimeConfig}; use crate::skills::SkillPromptProvider; use crate::storage::persistent_session_id; +use crate::topic_description::generate_topic_description; use super::session::{BusToolCallEmitter, SessionManager}; @@ -27,7 +29,7 @@ pub struct InboundProcessor { bus: Arc, session_manager: SessionManager, semaphore: Arc, - _provider_config: LLMProviderConfig, + provider_config: LLMProviderConfig, command_router: Arc, } @@ -99,7 +101,7 @@ impl InboundProcessor { bus, session_manager, semaphore, - _provider_config: provider_config, + provider_config, command_router: Arc::new(command_router), } } @@ -243,6 +245,37 @@ impl InboundProcessor { tracing::error!(error = %error, "Failed to publish outbound"); } } + + // 异步生成 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(); + + 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"); + } + } + Err(e) => { + tracing::error!(error = %e, topic_id = %topic_id_clone, "Failed to generate topic description"); + } + } + } + }); + } + } + } } Err(error) => { tracing::error!(error = %error, "Failed to handle message"); diff --git a/src/lib.rs b/src/lib.rs index 6e3c3a2..c0d6d5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,4 +18,5 @@ pub mod scheduler; pub mod skills; pub mod storage; pub mod text; +pub mod topic_description; pub mod tools; diff --git a/src/providers/traits.rs b/src/providers/traits.rs index 28b757c..4526f67 100644 --- a/src/providers/traits.rs +++ b/src/providers/traits.rs @@ -1,5 +1,6 @@ use crate::domain::messages::{ContentBlock, ToolCall}; use crate::domain::tools::Tool; +use crate::config::LLMProviderConfig; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -18,6 +19,23 @@ pub struct ProviderRuntimeConfig { pub model_extra: HashMap, } +impl From for ProviderRuntimeConfig { + fn from(config: LLMProviderConfig) -> Self { + Self { + provider_type: config.provider_type, + name: config.name, + base_url: config.base_url, + api_key: config.api_key, + extra_headers: config.extra_headers, + llm_timeout_secs: config.llm_timeout_secs, + model_id: config.model_id, + temperature: config.temperature, + max_tokens: config.max_tokens, + model_extra: config.model_extra, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Message { pub role: String, diff --git a/src/storage/mod.rs b/src/storage/mod.rs index b22a0b2..2927ac7 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -462,6 +462,16 @@ impl SessionStore { Ok(()) } + pub fn update_topic_description(&self, topic_id: &str, description: &str) -> Result<(), StorageError> { + let now = current_timestamp(); + let conn = self.conn.lock().expect("session db mutex poisoned"); + conn.execute( + "UPDATE topics SET description = ?2, updated_at = ?3 WHERE id = ?1", + params![topic_id, description, now], + )?; + Ok(()) + } + pub fn delete_topic(&self, topic_id: &str) -> Result<(), StorageError> { let conn = self.conn.lock().expect("session db mutex poisoned"); // Messages 的 topic_id 会被设为 NULL(ON DELETE SET NULL) diff --git a/src/topic_description.rs b/src/topic_description.rs new file mode 100644 index 0000000..ea69365 --- /dev/null +++ b/src/topic_description.rs @@ -0,0 +1,27 @@ +use crate::providers::{ChatCompletionRequest, LLMProvider, Message}; + +pub async fn generate_topic_description( + provider: &dyn LLMProvider, + first_user_message: &str, +) -> Result> { + let prompt = format!( + "请根据用户的第一句话,用简短的词语(不超过15字)描述这个对话的主题或意图。只输出描述内容,不要其他解释。\n\n用户消息:{}", + first_user_message + ); + + let request = ChatCompletionRequest { + messages: vec![Message::user(prompt)], + temperature: Some(0.3), + max_tokens: Some(50), + tools: None, + }; + + let response = provider.chat(request).await?; + let description = response.content.trim(); + + if description.len() > 50 { + Ok(description.chars().take(50).collect()) + } else { + Ok(description.to_string()) + } +} \ No newline at end of file