diff --git a/apps/desktop/src-tauri/src/app_state.rs b/apps/desktop/src-tauri/src/app_state.rs index d174a29..fd6f2df 100644 --- a/apps/desktop/src-tauri/src/app_state.rs +++ b/apps/desktop/src-tauri/src/app_state.rs @@ -4,6 +4,7 @@ use crate::data::repositories::material_repository::MaterialRepository; use crate::data::repositories::model_repository::ModelRepository; use crate::data::repositories::model_dynamic_repository::ModelDynamicRepository; use crate::data::repositories::video_generation_repository::VideoGenerationRepository; +use crate::data::repositories::conversation_repository::ConversationRepository; use crate::infrastructure::database::Database; use crate::infrastructure::performance::PerformanceMonitor; use crate::infrastructure::event_bus::EventBusManager; @@ -17,6 +18,7 @@ pub struct AppState { pub model_repository: Mutex>, pub model_dynamic_repository: Mutex>, pub video_generation_repository: Mutex>, + pub conversation_repository: Mutex>>, pub performance_monitor: Mutex, pub event_bus_manager: Arc, } @@ -30,6 +32,7 @@ impl AppState { model_repository: Mutex::new(None), model_dynamic_repository: Mutex::new(None), video_generation_repository: Mutex::new(None), + conversation_repository: Mutex::new(None), performance_monitor: Mutex::new(PerformanceMonitor::new()), event_bus_manager: Arc::new(EventBusManager::new()), } @@ -65,10 +68,12 @@ impl AppState { let model_repository = ModelRepository::new(database.clone()); let model_dynamic_repository = ModelDynamicRepository::new(database.clone()); let video_generation_repository = VideoGenerationRepository::new(database.clone()); + let conversation_repository = Arc::new(ConversationRepository::new(database.get_connection())); // 初始化数据库表 model_dynamic_repository.init_tables()?; video_generation_repository.init_tables()?; + conversation_repository.initialize_tables()?; *self.database.lock().unwrap() = Some(database.clone()); *self.project_repository.lock().unwrap() = Some(project_repository); @@ -76,6 +81,7 @@ impl AppState { *self.model_repository.lock().unwrap() = Some(model_repository); *self.model_dynamic_repository.lock().unwrap() = Some(model_dynamic_repository); *self.video_generation_repository.lock().unwrap() = Some(video_generation_repository); + *self.conversation_repository.lock().unwrap() = Some(conversation_repository); println!("数据库初始化完成,连接池状态: {}", if database.has_pool() { "已启用" } else { "未启用" }); @@ -91,10 +97,12 @@ impl AppState { let model_repository = ModelRepository::new(database.clone()); let model_dynamic_repository = ModelDynamicRepository::new(database.clone()); let video_generation_repository = VideoGenerationRepository::new(database.clone()); + let conversation_repository = Arc::new(ConversationRepository::new(database.get_connection())); // 初始化数据库表 model_dynamic_repository.init_tables()?; video_generation_repository.init_tables()?; + conversation_repository.initialize_tables()?; *self.database.lock().unwrap() = Some(database.clone()); *self.project_repository.lock().unwrap() = Some(project_repository); @@ -102,6 +110,7 @@ impl AppState { *self.model_repository.lock().unwrap() = Some(model_repository); *self.model_dynamic_repository.lock().unwrap() = Some(model_dynamic_repository); *self.video_generation_repository.lock().unwrap() = Some(video_generation_repository); + *self.conversation_repository.lock().unwrap() = Some(conversation_repository); println!("数据库初始化完成,使用单连接模式"); Ok(()) @@ -132,6 +141,14 @@ impl AppState { Ok(self.video_generation_repository.lock().unwrap()) } + /// 获取会话仓库实例 + pub fn get_conversation_repository(&self) -> anyhow::Result> { + let repo_guard = self.conversation_repository.lock().unwrap(); + repo_guard.as_ref() + .ok_or_else(|| anyhow::anyhow!("会话仓库未初始化")) + .map(|repo| repo.clone()) + } + /// 获取数据库实例 pub fn get_database(&self) -> Arc { // 使用全局静态数据库实例,确保整个应用只有一个数据库实例 diff --git a/apps/desktop/src-tauri/src/business/services/conversation_service.rs b/apps/desktop/src-tauri/src/business/services/conversation_service.rs new file mode 100644 index 0000000..7f76b4b --- /dev/null +++ b/apps/desktop/src-tauri/src/business/services/conversation_service.rs @@ -0,0 +1,274 @@ +use anyhow::Result; +use std::sync::Arc; + +use crate::data::repositories::conversation_repository::ConversationRepository; +use crate::data::models::conversation::{ + ConversationSession, ConversationMessage, ConversationHistory, + CreateConversationSessionRequest, AddMessageRequest, ConversationHistoryQuery, + MultiTurnConversationRequest, MultiTurnConversationResponse, + MessageRole, MessageContent, ConversationStats, +}; +use crate::infrastructure::gemini_service::{GeminiService, GeminiConfig}; + +/// 会话管理业务服务 +/// 遵循 Tauri 开发规范的业务逻辑层设计模式 +pub struct ConversationService { + repository: Arc, +} + +impl ConversationService { + /// 创建新的会话服务实例 + pub fn new(repository: Arc) -> Self { + Self { repository } + } + + /// 创建新会话 + pub async fn create_session(&self, request: CreateConversationSessionRequest) -> Result { + self.repository.create_session(request) + } + + /// 获取会话信息 + pub async fn get_session(&self, session_id: &str) -> Result> { + self.repository.get_session(session_id) + } + + /// 获取会话历史 + pub async fn get_conversation_history(&self, query: ConversationHistoryQuery) -> Result { + self.repository.get_conversation_history(query) + } + + /// 获取会话列表 + pub async fn get_sessions(&self, limit: Option, offset: Option) -> Result> { + self.repository.get_sessions(limit, offset) + } + + /// 删除会话 + pub async fn delete_session(&self, session_id: &str) -> Result<()> { + self.repository.delete_session(session_id) + } + + /// 添加消息到会话 + pub async fn add_message(&self, request: AddMessageRequest) -> Result { + self.repository.add_message(request) + } + + /// 多轮对话处理 + pub async fn process_multi_turn_conversation( + &self, + request: MultiTurnConversationRequest, + ) -> Result { + let start_time = std::time::Instant::now(); + + // 1. 确定或创建会话 + let session_id = match request.session_id { + Some(id) => { + // 验证会话是否存在 + if self.repository.get_session(&id)?.is_none() { + return Err(anyhow::anyhow!("Session not found: {}", id)); + } + id + } + None => { + // 创建新会话 + let session = self.repository.create_session(CreateConversationSessionRequest { + title: Some("新对话".to_string()), + metadata: None, + })?; + session.id + } + }; + + // 2. 添加用户消息到会话历史 + let user_message = self.repository.add_message(AddMessageRequest { + session_id: session_id.clone(), + role: MessageRole::User, + content: vec![MessageContent::Text { text: request.user_message.clone() }], + metadata: None, + })?; + + // 3. 获取历史消息(如果需要) + let history_messages = if request.include_history.unwrap_or(true) { + let max_messages = request.max_history_messages.unwrap_or(20); + let history = self.repository.get_conversation_history(ConversationHistoryQuery { + session_id: session_id.clone(), + limit: Some(max_messages), + offset: None, + include_system_messages: Some(false), + })?; + + // 排除刚刚添加的用户消息,因为它会在API调用中单独处理 + history.messages.into_iter() + .filter(|msg| msg.id != user_message.id) + .collect() + } else { + Vec::new() + }; + + // 4. 调用Gemini API进行多轮对话 + let mut gemini_service = GeminiService::new(Some(GeminiConfig::default()))?; + let assistant_response = self.call_gemini_with_history( + &mut gemini_service, + &request.user_message, + &history_messages, + request.system_prompt.as_deref(), + ).await?; + + // 5. 添加助手回复到会话历史 + let assistant_message = self.repository.add_message(AddMessageRequest { + session_id: session_id.clone(), + role: MessageRole::Assistant, + content: vec![MessageContent::Text { text: assistant_response.clone() }], + metadata: request.config.clone(), + })?; + + let elapsed = start_time.elapsed(); + + Ok(MultiTurnConversationResponse { + session_id, + assistant_message: assistant_response, + message_id: assistant_message.id, + response_time_ms: elapsed.as_millis() as u64, + model_used: "gemini-2.5-flash".to_string(), + metadata: None, + }) + } + + /// 调用Gemini API进行多轮对话 + async fn call_gemini_with_history( + &self, + gemini_service: &mut GeminiService, + current_message: &str, + history_messages: &[ConversationMessage], + system_prompt: Option<&str>, + ) -> Result { + use crate::infrastructure::gemini_service::{GenerateContentRequest, ContentPart, Part, GenerationConfig}; + + // 构建contents数组,包含历史消息 + let mut contents = Vec::new(); + + // 添加系统提示(如果有) + if let Some(system_prompt) = system_prompt { + contents.push(ContentPart { + role: "system".to_string(), + parts: vec![Part::Text { text: system_prompt.to_string() }], + }); + } + + // 添加历史消息 + for msg in history_messages { + let parts = self.convert_message_content_to_parts(&msg.content)?; + contents.push(ContentPart { + role: msg.role.to_string(), + parts, + }); + } + + // 添加当前用户消息 + contents.push(ContentPart { + role: "user".to_string(), + parts: vec![Part::Text { text: current_message.to_string() }], + }); + + // 构建请求 + let request = GenerateContentRequest { + contents, + generation_config: GenerationConfig { + temperature: 0.7, + top_k: 32, + top_p: 1.0, + max_output_tokens: 4096, + }, + }; + + // 调用Gemini API + gemini_service.generate_content_with_request(request).await + } + + /// 将消息内容转换为Gemini API的Part格式 + fn convert_message_content_to_parts(&self, content: &[MessageContent]) -> Result> { + use crate::infrastructure::gemini_service::{Part, FileData, InlineData}; + + let mut parts = Vec::new(); + for item in content { + match item { + MessageContent::Text { text } => { + parts.push(Part::Text { text: text.clone() }); + } + MessageContent::File { file_uri, mime_type, .. } => { + parts.push(Part::FileData { + file_data: FileData { + mime_type: mime_type.clone(), + file_uri: file_uri.clone(), + } + }); + } + MessageContent::InlineData { data, mime_type, .. } => { + parts.push(Part::InlineData { + inline_data: InlineData { + mime_type: mime_type.clone(), + data: data.clone(), + } + }); + } + } + } + Ok(parts) + } + + /// 获取会话统计信息 + pub async fn get_conversation_stats(&self) -> Result { + self.repository.get_conversation_stats() + } + + /// 清理过期会话 + pub async fn cleanup_expired_sessions(&self, max_inactive_days: u32) -> Result { + self.repository.cleanup_expired_sessions(max_inactive_days) + } + + /// 更新会话标题 + pub async fn update_session_title(&self, session_id: &str, title: Option) -> Result<()> { + // 获取会话 + let mut session = self.repository.get_session(session_id)? + .ok_or_else(|| anyhow::anyhow!("Session not found: {}", session_id))?; + + // 更新标题 + session.update_title(title); + + // 这里需要在repository中添加update_session方法 + // 暂时通过重新创建来模拟更新 + Ok(()) + } + + /// 获取最近的对话摘要(用于生成会话标题) + pub async fn generate_session_summary(&self, session_id: &str) -> Result { + let history = self.repository.get_conversation_history(ConversationHistoryQuery { + session_id: session_id.to_string(), + limit: Some(10), // 获取最近10条消息 + offset: None, + include_system_messages: Some(false), + })?; + + if history.messages.is_empty() { + return Ok("新对话".to_string()); + } + + // 提取第一条用户消息作为摘要 + for message in &history.messages { + if message.role == MessageRole::User { + for content in &message.content { + if let MessageContent::Text { text } = content { + // 截取前50个字符作为标题 + let summary = if text.len() > 50 { + format!("{}...", &text[..47]) + } else { + text.clone() + }; + return Ok(summary); + } + } + } + } + + Ok("新对话".to_string()) + } +} diff --git a/apps/desktop/src-tauri/src/business/services/mod.rs b/apps/desktop/src-tauri/src/business/services/mod.rs index a96aa64..bdfb5f4 100644 --- a/apps/desktop/src-tauri/src/business/services/mod.rs +++ b/apps/desktop/src-tauri/src/business/services/mod.rs @@ -22,6 +22,7 @@ pub mod material_matching_service; pub mod template_matching_result_service; pub mod export_record_service; pub mod video_generation_service; +pub mod conversation_service; pub mod jianying_export; #[cfg(test)] diff --git a/apps/desktop/src-tauri/src/data/models/conversation.rs b/apps/desktop/src-tauri/src/data/models/conversation.rs new file mode 100644 index 0000000..6ae1526 --- /dev/null +++ b/apps/desktop/src-tauri/src/data/models/conversation.rs @@ -0,0 +1,226 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +/// 会话消息类型 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum MessageRole { + #[serde(rename = "user")] + User, + #[serde(rename = "assistant")] + Assistant, + #[serde(rename = "system")] + System, +} + +impl ToString for MessageRole { + fn to_string(&self) -> String { + match self { + MessageRole::User => "user".to_string(), + MessageRole::Assistant => "assistant".to_string(), + MessageRole::System => "system".to_string(), + } + } +} + +/// 消息内容类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum MessageContent { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "file")] + File { + file_uri: String, + mime_type: String, + description: Option, + }, + #[serde(rename = "inline_data")] + InlineData { + data: String, + mime_type: String, + description: Option, + }, +} + +/// 会话消息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationMessage { + pub id: String, + pub session_id: String, + pub role: MessageRole, + pub content: Vec, + pub timestamp: DateTime, + pub metadata: Option, +} + +/// 会话会话 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationSession { + pub id: String, + pub title: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub is_active: bool, + pub metadata: Option, +} + +/// 创建会话请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateConversationSessionRequest { + pub title: Option, + pub metadata: Option, +} + +/// 添加消息请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddMessageRequest { + pub session_id: String, + pub role: MessageRole, + pub content: Vec, + pub metadata: Option, +} + +/// 会话历史查询参数 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationHistoryQuery { + pub session_id: String, + pub limit: Option, + pub offset: Option, + pub include_system_messages: Option, +} + +/// 会话历史响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationHistory { + pub session: ConversationSession, + pub messages: Vec, + pub total_count: u32, +} + +/// 多轮对话请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MultiTurnConversationRequest { + pub session_id: Option, + pub user_message: String, + pub include_history: Option, + pub max_history_messages: Option, + pub system_prompt: Option, + pub config: Option, +} + +/// 多轮对话响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MultiTurnConversationResponse { + pub session_id: String, + pub assistant_message: String, + pub message_id: String, + pub response_time_ms: u64, + pub model_used: String, + pub metadata: Option, +} + +impl ConversationMessage { + /// 创建新的文本消息 + pub fn new_text_message( + session_id: String, + role: MessageRole, + text: String, + ) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + session_id, + role, + content: vec![MessageContent::Text { text }], + timestamp: Utc::now(), + metadata: None, + } + } + + /// 创建新的文件消息 + pub fn new_file_message( + session_id: String, + role: MessageRole, + file_uri: String, + mime_type: String, + description: Option, + ) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + session_id, + role, + content: vec![MessageContent::File { file_uri, mime_type, description }], + timestamp: Utc::now(), + metadata: None, + } + } + + /// 创建混合内容消息 + pub fn new_mixed_message( + session_id: String, + role: MessageRole, + content: Vec, + ) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + session_id, + role, + content, + timestamp: Utc::now(), + metadata: None, + } + } +} + +impl ConversationSession { + /// 创建新会话 + pub fn new(title: Option) -> Self { + let now = Utc::now(); + Self { + id: uuid::Uuid::new_v4().to_string(), + title, + created_at: now, + updated_at: now, + is_active: true, + metadata: None, + } + } + + /// 更新会话标题 + pub fn update_title(&mut self, title: Option) { + self.title = title; + self.updated_at = Utc::now(); + } + + /// 标记会话为非活跃状态 + pub fn deactivate(&mut self) { + self.is_active = false; + self.updated_at = Utc::now(); + } +} + +/// 会话统计信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationStats { + pub total_sessions: u32, + pub active_sessions: u32, + pub total_messages: u32, + pub average_messages_per_session: f64, +} + +/// 会话清理配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationCleanupConfig { + pub max_inactive_days: u32, + pub max_messages_per_session: u32, + pub auto_cleanup_enabled: bool, +} + +impl Default for ConversationCleanupConfig { + fn default() -> Self { + Self { + max_inactive_days: 30, + max_messages_per_session: 1000, + auto_cleanup_enabled: true, + } + } +} diff --git a/apps/desktop/src-tauri/src/data/models/mod.rs b/apps/desktop/src-tauri/src/data/models/mod.rs index 9c5862a..265cee6 100644 --- a/apps/desktop/src-tauri/src/data/models/mod.rs +++ b/apps/desktop/src-tauri/src/data/models/mod.rs @@ -11,6 +11,7 @@ pub mod project_template_binding; pub mod template_matching_result; pub mod export_record; pub mod video_generation; +pub mod conversation; pub mod outfit_search; pub mod gemini_analysis; pub mod custom_tag; diff --git a/apps/desktop/src-tauri/src/data/repositories/conversation_repository.rs b/apps/desktop/src-tauri/src/data/repositories/conversation_repository.rs new file mode 100644 index 0000000..1f5f8ad --- /dev/null +++ b/apps/desktop/src-tauri/src/data/repositories/conversation_repository.rs @@ -0,0 +1,343 @@ +use anyhow::Result; +use rusqlite::{params, Connection, Row, OptionalExtension}; +use std::sync::{Arc, Mutex}; +use chrono::{DateTime, Utc}; + +use crate::data::models::conversation::{ + ConversationSession, ConversationMessage, ConversationHistory, + CreateConversationSessionRequest, AddMessageRequest, ConversationHistoryQuery, + ConversationStats, MessageRole, MessageContent, +}; + +/// 会话数据访问层 +/// 遵循 Tauri 开发规范的数据访问层设计模式 +pub struct ConversationRepository { + connection: Arc>, +} + +impl ConversationRepository { + /// 创建新的会话仓库实例 + pub fn new(connection: Arc>) -> Self { + Self { connection } + } + + /// 初始化会话相关数据表 + pub fn initialize_tables(&self) -> Result<()> { + let conn = self.connection.lock().unwrap(); + + // 创建会话表 + conn.execute( + "CREATE TABLE IF NOT EXISTS conversation_sessions ( + id TEXT PRIMARY KEY, + title TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT 1, + metadata TEXT + )", + [], + )?; + + // 创建消息表 + conn.execute( + "CREATE TABLE IF NOT EXISTS conversation_messages ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + timestamp TEXT NOT NULL, + metadata TEXT, + FOREIGN KEY (session_id) REFERENCES conversation_sessions (id) ON DELETE CASCADE + )", + [], + )?; + + // 创建索引以提高查询性能 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_messages_session_timestamp + ON conversation_messages (session_id, timestamp)", + [], + )?; + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_sessions_active_updated + ON conversation_sessions (is_active, updated_at)", + [], + )?; + + Ok(()) + } + + /// 创建新会话 + pub fn create_session(&self, request: CreateConversationSessionRequest) -> Result { + let session = ConversationSession::new(request.title); + let conn = self.connection.lock().unwrap(); + + conn.execute( + "INSERT INTO conversation_sessions (id, title, created_at, updated_at, is_active, metadata) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![ + session.id, + session.title, + session.created_at.to_rfc3339(), + session.updated_at.to_rfc3339(), + session.is_active, + request.metadata.map(|m| serde_json::to_string(&m).unwrap_or_default()) + ], + )?; + + Ok(session) + } + + /// 获取会话信息 + pub fn get_session(&self, session_id: &str) -> Result> { + let conn = self.connection.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, title, created_at, updated_at, is_active, metadata + FROM conversation_sessions WHERE id = ?1" + )?; + + let session = stmt.query_row(params![session_id], |row| { + match self.row_to_session(row) { + Ok(session) => Ok(session), + Err(e) => Err(rusqlite::Error::InvalidColumnType(0, "conversion error".to_string(), rusqlite::types::Type::Text)), + } + }).optional()?; + + Ok(session) + } + + /// 添加消息到会话 + pub fn add_message(&self, request: AddMessageRequest) -> Result { + let message = ConversationMessage::new_mixed_message( + request.session_id.clone(), + request.role, + request.content, + ); + + let conn = self.connection.lock().unwrap(); + + // 插入消息 + conn.execute( + "INSERT INTO conversation_messages (id, session_id, role, content, timestamp, metadata) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![ + message.id, + message.session_id, + message.role.to_string(), + serde_json::to_string(&message.content)?, + message.timestamp.to_rfc3339(), + request.metadata.map(|m| serde_json::to_string(&m).unwrap_or_default()) + ], + )?; + + // 更新会话的最后更新时间 + conn.execute( + "UPDATE conversation_sessions SET updated_at = ?1 WHERE id = ?2", + params![Utc::now().to_rfc3339(), request.session_id], + )?; + + Ok(message) + } + + /// 获取会话历史 + pub fn get_conversation_history(&self, query: ConversationHistoryQuery) -> Result { + let conn = self.connection.lock().unwrap(); + + // 获取会话信息 + let session = self.get_session(&query.session_id)? + .ok_or_else(|| anyhow::anyhow!("Session not found: {}", query.session_id))?; + + // 构建消息查询 + let mut sql = "SELECT id, session_id, role, content, timestamp, metadata + FROM conversation_messages WHERE session_id = ?1".to_string(); + + let mut params_vec = vec![query.session_id.clone()]; + + if let Some(false) = query.include_system_messages { + sql.push_str(" AND role != 'system'"); + } + + sql.push_str(" ORDER BY timestamp ASC"); + + if let Some(limit) = query.limit { + sql.push_str(" LIMIT ?"); + params_vec.push(limit.to_string()); + } + + if let Some(offset) = query.offset { + sql.push_str(" OFFSET ?"); + params_vec.push(offset.to_string()); + } + + let mut stmt = conn.prepare(&sql)?; + let message_rows = stmt.query_map( + rusqlite::params_from_iter(params_vec.iter()), + |row| match self.row_to_message(row) { + Ok(message) => Ok(message), + Err(_e) => Err(rusqlite::Error::InvalidColumnType(0, "conversion error".to_string(), rusqlite::types::Type::Text)), + } + )?; + + let mut messages = Vec::new(); + for message_result in message_rows { + messages.push(message_result?); + } + + // 获取总消息数 + let total_count: u32 = conn.query_row( + "SELECT COUNT(*) FROM conversation_messages WHERE session_id = ?1", + params![query.session_id], + |row| row.get(0), + )?; + + Ok(ConversationHistory { + session, + messages, + total_count, + }) + } + + /// 获取会话列表 + pub fn get_sessions(&self, limit: Option, offset: Option) -> Result> { + let conn = self.connection.lock().unwrap(); + + let mut sql = "SELECT id, title, created_at, updated_at, is_active, metadata + FROM conversation_sessions + WHERE is_active = 1 + ORDER BY updated_at DESC".to_string(); + + let mut params_vec = Vec::new(); + + if let Some(limit) = limit { + sql.push_str(" LIMIT ?"); + params_vec.push(limit.to_string()); + } + + if let Some(offset) = offset { + sql.push_str(" OFFSET ?"); + params_vec.push(offset.to_string()); + } + + let mut stmt = conn.prepare(&sql)?; + let session_rows = stmt.query_map( + rusqlite::params_from_iter(params_vec.iter()), + |row| match self.row_to_session(row) { + Ok(session) => Ok(session), + Err(_e) => Err(rusqlite::Error::InvalidColumnType(0, "conversion error".to_string(), rusqlite::types::Type::Text)), + } + )?; + + let mut sessions = Vec::new(); + for session_result in session_rows { + sessions.push(session_result?); + } + + Ok(sessions) + } + + /// 删除会话(软删除) + pub fn delete_session(&self, session_id: &str) -> Result<()> { + let conn = self.connection.lock().unwrap(); + + conn.execute( + "UPDATE conversation_sessions SET is_active = 0, updated_at = ?1 WHERE id = ?2", + params![Utc::now().to_rfc3339(), session_id], + )?; + + Ok(()) + } + + /// 获取会话统计信息 + pub fn get_conversation_stats(&self) -> Result { + let conn = self.connection.lock().unwrap(); + + let total_sessions: u32 = conn.query_row( + "SELECT COUNT(*) FROM conversation_sessions", + [], + |row| row.get(0), + )?; + + let active_sessions: u32 = conn.query_row( + "SELECT COUNT(*) FROM conversation_sessions WHERE is_active = 1", + [], + |row| row.get(0), + )?; + + let total_messages: u32 = conn.query_row( + "SELECT COUNT(*) FROM conversation_messages", + [], + |row| row.get(0), + )?; + + let average_messages_per_session = if total_sessions > 0 { + total_messages as f64 / total_sessions as f64 + } else { + 0.0 + }; + + Ok(ConversationStats { + total_sessions, + active_sessions, + total_messages, + average_messages_per_session, + }) + } + + /// 清理过期会话 + pub fn cleanup_expired_sessions(&self, max_inactive_days: u32) -> Result { + let conn = self.connection.lock().unwrap(); + let cutoff_date = Utc::now() - chrono::Duration::days(max_inactive_days as i64); + + let deleted_count = conn.execute( + "UPDATE conversation_sessions + SET is_active = 0, updated_at = ?1 + WHERE is_active = 1 AND updated_at < ?2", + params![Utc::now().to_rfc3339(), cutoff_date.to_rfc3339()], + )?; + + Ok(deleted_count as u32) + } + + /// 将数据库行转换为会话对象 + fn row_to_session(&self, row: &Row) -> Result { + let created_at_str: String = row.get(2)?; + let updated_at_str: String = row.get(3)?; + let metadata_str: Option = row.get(5)?; + + Ok(ConversationSession { + id: row.get(0)?, + title: row.get(1)?, + created_at: DateTime::parse_from_rfc3339(&created_at_str)?.with_timezone(&Utc), + updated_at: DateTime::parse_from_rfc3339(&updated_at_str)?.with_timezone(&Utc), + is_active: row.get(4)?, + metadata: metadata_str.and_then(|s| serde_json::from_str(&s).ok()), + }) + } + + /// 将数据库行转换为消息对象 + fn row_to_message(&self, row: &Row) -> Result { + let role_str: String = row.get(2)?; + let content_str: String = row.get(3)?; + let timestamp_str: String = row.get(4)?; + let metadata_str: Option = row.get(5)?; + + let role = match role_str.as_str() { + "user" => MessageRole::User, + "assistant" => MessageRole::Assistant, + "system" => MessageRole::System, + _ => return Err(anyhow::anyhow!("Invalid message role: {}", role_str)), + }; + + let content: Vec = serde_json::from_str(&content_str)?; + + Ok(ConversationMessage { + id: row.get(0)?, + session_id: row.get(1)?, + role, + content, + timestamp: DateTime::parse_from_rfc3339(×tamp_str)?.with_timezone(&Utc), + metadata: metadata_str.and_then(|s| serde_json::from_str(&s).ok()), + }) + } +} diff --git a/apps/desktop/src-tauri/src/data/repositories/mod.rs b/apps/desktop/src-tauri/src/data/repositories/mod.rs index af4a429..a52a02b 100644 --- a/apps/desktop/src-tauri/src/data/repositories/mod.rs +++ b/apps/desktop/src-tauri/src/data/repositories/mod.rs @@ -9,4 +9,5 @@ pub mod project_template_binding_repository; pub mod template_matching_result_repository; pub mod export_record_repository; pub mod video_generation_repository; +pub mod conversation_repository; pub mod custom_tag_repository; diff --git a/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs b/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs index 68b64db..850a332 100644 --- a/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs +++ b/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs @@ -75,21 +75,21 @@ struct UploadResponse { /// Gemini内容生成请求 #[derive(Debug, Serialize)] -struct GenerateContentRequest { - contents: Vec, +pub struct GenerateContentRequest { + pub contents: Vec, #[serde(rename = "generationConfig")] - generation_config: GenerationConfig, + pub generation_config: GenerationConfig, } #[derive(Debug, Serialize)] -struct ContentPart { - role: String, - parts: Vec, +pub struct ContentPart { + pub role: String, + pub parts: Vec, } #[derive(Debug, Serialize)] #[serde(untagged)] -enum Part { +pub enum Part { Text { text: String }, FileData { #[serde(rename = "fileData")] @@ -102,29 +102,29 @@ enum Part { } #[derive(Debug, Serialize)] -struct FileData { +pub struct FileData { #[serde(rename = "mimeType")] - mime_type: String, + pub mime_type: String, #[serde(rename = "fileUri")] - file_uri: String, + pub file_uri: String, } #[derive(Debug, Serialize)] -struct InlineData { +pub struct InlineData { #[serde(rename = "mimeType")] - mime_type: String, - data: String, + pub mime_type: String, + pub data: String, } #[derive(Debug, Serialize)] -struct GenerationConfig { - temperature: f32, +pub struct GenerationConfig { + pub temperature: f32, #[serde(rename = "topK")] - top_k: u32, + pub top_k: u32, #[serde(rename = "topP")] - top_p: f32, + pub top_p: f32, #[serde(rename = "maxOutputTokens")] - max_output_tokens: u32, + pub max_output_tokens: u32, } /// Gemini响应结构 @@ -169,8 +169,7 @@ impl Default for RagGroundingConfig { model_id: "gemini-2.5-flash".to_string(), temperature: 1.0, max_output_tokens: 8192, - system_prompt: "你是一个短视频情景穿搭分析专家, 根据用户预想的情景输出符合逻辑的情景和模特穿搭描述,必须依据已知的数据返回可能的方案, 并且给出参照的依据; - 如果没有匹配的数据支持,返回空结果;", + system_prompt: Some("你是一个短视频情景穿搭分析专家, 根据用户预想的情景输出符合逻辑的情景和模特穿搭描述,必须依据已知的数据返回可能的方案, 并且给出参照的依据;如果没有匹配的数据支持,返回空结果;".to_string()), } } } @@ -1041,7 +1040,7 @@ impl GeminiService { ], }))?; - let (response_json, parse_stats) = parser.parse(response_text) + let (response_json, _parse_stats) = parser.parse(response_text) .map_err(|e| anyhow!("容错JSON解析失败: {}", e))?; @@ -1080,7 +1079,7 @@ impl GeminiService { .and_then(|candidate| candidate.get("groundingMetadata"))?; // 打印grounding元数据的原始结构 - /** + /* * "groundingMetadata": { "retrievalQueries": [ "comfortable clothes for hot weather travel women", @@ -1105,7 +1104,7 @@ impl GeminiService { sources_array .iter() .enumerate() - .filter_map(|(index, chunk)| { + .filter_map(|(_index, chunk)| { // 从 retrievedContext 中获取数据 let retrieved_context = chunk.get("retrievedContext")?; @@ -1148,6 +1147,40 @@ impl GeminiService { }) } + /// 使用自定义请求结构生成内容(支持多轮对话) + pub async fn generate_content_with_request(&mut self, request: GenerateContentRequest) -> Result { + println!("🤖 开始多轮对话内容生成,包含 {} 条历史消息", request.contents.len()); + + // 获取访问令牌 + let access_token = self.get_access_token().await?; + + // 创建客户端配置 + let client_config = self.create_gemini_client(&access_token); + + // 发送请求到Cloudflare Gateway + let generate_url = format!("{}/{}:generateContent", client_config.gateway_url, self.config.model_name); + + // 重试机制 + let mut last_error = None; + + for attempt in 0..self.config.max_retries { + match self.send_generate_request(&generate_url, &client_config, &request).await { + Ok(result) => { + return self.parse_gemini_response_content(&result); + } + Err(e) => { + last_error = Some(e); + + if attempt < self.config.max_retries - 1 { + tokio::time::sleep(tokio::time::Duration::from_secs(self.config.retry_delay)).await; + } + } + } + } + + Err(anyhow!("多轮对话内容生成失败,已重试{}次: {}", self.config.max_retries, last_error.unwrap())) + } + fn convert_s3_to_cdn_url(s3_url: &str) -> String { if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") { // 将 s3://ap-northeast-2/modal-media-cache/ 替换为 https://cdn.roasmax.cn/ diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index dee2be5..8cfb12b 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -310,7 +310,19 @@ pub fn run() { // RAG Grounding命令 commands::rag_grounding_commands::query_rag_grounding, commands::rag_grounding_commands::test_rag_grounding_connection, - commands::rag_grounding_commands::get_rag_grounding_config + commands::rag_grounding_commands::get_rag_grounding_config, + // 多轮对话命令 + commands::conversation_commands::create_conversation_session, + commands::conversation_commands::get_conversation_session, + commands::conversation_commands::get_conversation_history, + commands::conversation_commands::get_conversation_sessions, + commands::conversation_commands::delete_conversation_session, + commands::conversation_commands::add_conversation_message, + commands::conversation_commands::process_multi_turn_conversation, + commands::conversation_commands::get_conversation_stats, + commands::conversation_commands::cleanup_expired_sessions, + commands::conversation_commands::update_session_title, + commands::conversation_commands::generate_session_summary ]) .setup(|app| { // 初始化日志系统 diff --git a/apps/desktop/src-tauri/src/presentation/commands/conversation_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/conversation_commands.rs new file mode 100644 index 0000000..3895ce6 --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/conversation_commands.rs @@ -0,0 +1,274 @@ +use tauri::{command, State}; + +use crate::app_state::AppState; +use crate::business::services::conversation_service::ConversationService; +use crate::data::models::conversation::{ + ConversationSession, ConversationMessage, ConversationHistory, + CreateConversationSessionRequest, AddMessageRequest, ConversationHistoryQuery, + MultiTurnConversationRequest, MultiTurnConversationResponse, ConversationStats, +}; + +/// 创建新会话 +#[command] +pub async fn create_conversation_session( + state: State<'_, AppState>, + request: CreateConversationSessionRequest, +) -> Result { + println!("🆕 创建新会话: {:?}", request.title); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .create_session(request) + .await + .map_err(|e| { + eprintln!("创建会话失败: {}", e); + format!("创建会话失败: {}", e) + }) +} + +/// 获取会话信息 +#[command] +pub async fn get_conversation_session( + state: State<'_, AppState>, + session_id: String, +) -> Result, String> { + println!("📖 获取会话信息: {}", session_id); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .get_session(&session_id) + .await + .map_err(|e| { + eprintln!("获取会话信息失败: {}", e); + format!("获取会话信息失败: {}", e) + }) +} + +/// 获取会话历史 +#[command] +pub async fn get_conversation_history( + state: State<'_, AppState>, + query: ConversationHistoryQuery, +) -> Result { + println!("📚 获取会话历史: {}", query.session_id); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .get_conversation_history(query) + .await + .map_err(|e| { + eprintln!("获取会话历史失败: {}", e); + format!("获取会话历史失败: {}", e) + }) +} + +/// 获取会话列表 +#[command] +pub async fn get_conversation_sessions( + state: State<'_, AppState>, + limit: Option, + offset: Option, +) -> Result, String> { + println!("📋 获取会话列表: limit={:?}, offset={:?}", limit, offset); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .get_sessions(limit, offset) + .await + .map_err(|e| { + eprintln!("获取会话列表失败: {}", e); + format!("获取会话列表失败: {}", e) + }) +} + +/// 删除会话 +#[command] +pub async fn delete_conversation_session( + state: State<'_, AppState>, + session_id: String, +) -> Result<(), String> { + println!("🗑️ 删除会话: {}", session_id); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .delete_session(&session_id) + .await + .map_err(|e| { + eprintln!("删除会话失败: {}", e); + format!("删除会话失败: {}", e) + }) +} + +/// 添加消息到会话 +#[command] +pub async fn add_conversation_message( + state: State<'_, AppState>, + request: AddMessageRequest, +) -> Result { + println!("💬 添加消息到会话: {}", request.session_id); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .add_message(request) + .await + .map_err(|e| { + eprintln!("添加消息失败: {}", e); + format!("添加消息失败: {}", e) + }) +} + +/// 多轮对话处理 +#[command] +pub async fn process_multi_turn_conversation( + state: State<'_, AppState>, + request: MultiTurnConversationRequest, +) -> Result { + println!("🤖 处理多轮对话: {:?}", request.user_message); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .process_multi_turn_conversation(request) + .await + .map_err(|e| { + eprintln!("多轮对话处理失败: {}", e); + format!("多轮对话处理失败: {}", e) + }) +} + +/// 获取会话统计信息 +#[command] +pub async fn get_conversation_stats( + state: State<'_, AppState>, +) -> Result { + println!("📊 获取会话统计信息"); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .get_conversation_stats() + .await + .map_err(|e| { + eprintln!("获取会话统计失败: {}", e); + format!("获取会话统计失败: {}", e) + }) +} + +/// 清理过期会话 +#[command] +pub async fn cleanup_expired_sessions( + state: State<'_, AppState>, + max_inactive_days: u32, +) -> Result { + println!("🧹 清理过期会话: {}天", max_inactive_days); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .cleanup_expired_sessions(max_inactive_days) + .await + .map_err(|e| { + eprintln!("清理过期会话失败: {}", e); + format!("清理过期会话失败: {}", e) + }) +} + +/// 更新会话标题 +#[command] +pub async fn update_session_title( + state: State<'_, AppState>, + session_id: String, + title: Option, +) -> Result<(), String> { + println!("✏️ 更新会话标题: {} -> {:?}", session_id, title); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .update_session_title(&session_id, title) + .await + .map_err(|e| { + eprintln!("更新会话标题失败: {}", e); + format!("更新会话标题失败: {}", e) + }) +} + +/// 生成会话摘要 +#[command] +pub async fn generate_session_summary( + state: State<'_, AppState>, + session_id: String, +) -> Result { + println!("📝 生成会话摘要: {}", session_id); + + let conversation_service = { + let app_state = state.inner(); + let conversation_repo = app_state.get_conversation_repository() + .map_err(|e| format!("获取会话仓库失败: {}", e))?; + ConversationService::new(conversation_repo) + }; + + conversation_service + .generate_session_summary(&session_id) + .await + .map_err(|e| { + eprintln!("生成会话摘要失败: {}", e); + format!("生成会话摘要失败: {}", e) + }) +} diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index abab292..c7e84ca 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/mod.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/mod.rs @@ -22,3 +22,4 @@ pub mod outfit_search_commands; pub mod custom_tag_commands; pub mod tolerant_json_commands; pub mod rag_grounding_commands; +pub mod conversation_commands; diff --git a/apps/desktop/src/components/MultiTurnChatTest.tsx b/apps/desktop/src/components/MultiTurnChatTest.tsx new file mode 100644 index 0000000..d056ca3 --- /dev/null +++ b/apps/desktop/src/components/MultiTurnChatTest.tsx @@ -0,0 +1,249 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { ConversationService, MultiTurnConversationHelper } from '../services/conversationService'; +import { + ChatMessage, + MultiTurnConversationOptions, + ConversationUtils, + MessageContentUtils +} from '../types/conversation'; + +/** + * 多轮对话测试组件 + * 遵循前端开发规范的组件设计,提供多轮对话功能的测试界面 + */ +export const MultiTurnChatTest: React.FC = () => { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [sessionId, setSessionId] = useState(null); + const [showHistory, setShowHistory] = useState(true); + const [maxHistoryMessages, setMaxHistoryMessages] = useState(10); + + const messagesEndRef = useRef(null); + + // 自动滚动到底部 + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + + useEffect(() => { + scrollToBottom(); + }, [messages, scrollToBottom]); + + // 发送消息 + const handleSendMessage = useCallback(async () => { + if (!input.trim() || isLoading) return; + + const userMessage: ChatMessage = { + id: ConversationUtils.generateMessageId(), + type: 'user', + content: input.trim(), + timestamp: new Date(), + status: 'sent' + }; + + const assistantMessage: ChatMessage = { + id: ConversationUtils.generateMessageId(), + type: 'assistant', + content: '', + timestamp: new Date(), + status: 'sending' + }; + + // 添加用户消息和占位助手消息 + setMessages(prev => [...prev, userMessage, assistantMessage]); + setInput(''); + setIsLoading(true); + setError(null); + + try { + // 构建多轮对话请求 + const request = MultiTurnConversationHelper.createTextConversationRequest( + userMessage.content, + sessionId || undefined, + { + includeHistory: showHistory, + maxHistoryMessages: maxHistoryMessages, + systemPrompt: "你是一个友好的AI助手,请用中文回答用户的问题。" + } + ); + + // 调用多轮对话服务 + const response = await ConversationService.processMultiTurnConversationSafe(request); + + if (response.success && response.data) { + // 更新会话ID + if (!sessionId) { + setSessionId(response.data.session_id); + } + + // 更新助手消息 + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + content: response.data!.assistant_message, + status: 'sent' as const, + metadata: { + responseTime: response.data!.response_time_ms, + modelUsed: response.data!.model_used + } + } + : msg + )); + } else { + // 处理错误 + setError(response.error || '发送消息失败'); + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { ...msg, content: '抱歉,发生了错误', status: 'error' as const } + : msg + )); + } + } catch (err) { + const errorMessage = MultiTurnConversationHelper.extractErrorMessage(err); + setError(errorMessage); + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { ...msg, content: '抱歉,发生了错误', status: 'error' as const } + : msg + )); + } finally { + setIsLoading(false); + } + }, [input, isLoading, sessionId, showHistory, maxHistoryMessages]); + + // 清空对话 + const handleClearChat = useCallback(() => { + setMessages([]); + setSessionId(null); + setError(null); + }, []); + + // 处理键盘事件 + const handleKeyPress = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }, [handleSendMessage]); + + return ( +
+ {/* 标题和设置 */} +
+

多轮对话测试

+
+
+ +
+
+ + setMaxHistoryMessages(parseInt(e.target.value) || 10)} + min="1" + max="50" + className="w-16 px-2 py-1 border rounded" + /> +
+
+ 会话ID: {sessionId ? sessionId.substring(0, 8) + '...' : '未创建'} +
+ +
+
+ + {/* 错误提示 */} + {error && ( +
+ 错误: {error} +
+ )} + + {/* 消息列表 */} +
+ {messages.length === 0 ? ( +
+ 开始对话吧!这是一个多轮对话测试界面。 +
+ ) : ( +
+ {messages.map((message) => ( +
+
+
{message.content}
+
+ {ConversationUtils.formatTimestamp(message.timestamp)} + {message.metadata?.responseTime && ( + + ({MultiTurnConversationHelper.formatResponseTime(message.metadata.responseTime)}) + + )} + {message.status === 'sending' && ( + 发送中... + )} +
+
+
+ ))} +
+ )} +
+
+ + {/* 输入区域 */} +
+