feat: 添加主题描述生成和更新功能,优化会话信息展示

This commit is contained in:
ooodc 2026-05-24 08:32:34 +08:00
parent 5e04832f20
commit 0732b31e6b
7 changed files with 110 additions and 3 deletions

View File

@ -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,

View File

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

View File

@ -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<MessageBus>,
session_manager: SessionManager,
semaphore: Arc<Semaphore>,
_provider_config: LLMProviderConfig,
provider_config: LLMProviderConfig,
command_router: Arc<CommandRouter>,
}
@ -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");

View File

@ -18,4 +18,5 @@ pub mod scheduler;
pub mod skills;
pub mod storage;
pub mod text;
pub mod topic_description;
pub mod tools;

View File

@ -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<String, serde_json::Value>,
}
impl From<LLMProviderConfig> 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,

View File

@ -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 会被设为 NULLON DELETE SET NULL

27
src/topic_description.rs Normal file
View File

@ -0,0 +1,27 @@
use crate::providers::{ChatCompletionRequest, LLMProvider, Message};
pub async fn generate_topic_description(
provider: &dyn LLMProvider,
first_user_message: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
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())
}
}