From de342f9195ba79acd3519c1c237bf6d7738ce6ab Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 19 May 2026 11:33:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20Day=207=20=E2=80=94=20=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=8C=81=E4=B9=85=E5=8C=96=20Entity=20+=20Service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 3 个 SeaORM Entity: ai_chat_session / ai_chat_message / ai_tool_call_log - ChatSessionService: create / list / get / close / rename - ChatMessageService: save_message / list_messages / save_tool_call_log - 参数封装为 SaveMessageParams / SaveToolCallLogParams 避免 clippy too_many_arguments - AiState 注册 chat_session + chat_message 服务 - erp-server main.rs 初始化注入 --- crates/erp-ai/src/entity/ai_chat_message.rs | 41 ++++++++ crates/erp-ai/src/entity/ai_chat_session.rs | 37 +++++++ crates/erp-ai/src/entity/ai_tool_call_log.rs | 37 +++++++ crates/erp-ai/src/entity/mod.rs | 3 + crates/erp-ai/src/service/chat_message.rs | 105 +++++++++++++++++++ crates/erp-ai/src/service/chat_session.rs | 101 ++++++++++++++++++ crates/erp-ai/src/service/mod.rs | 2 + crates/erp-ai/src/state.rs | 4 + crates/erp-server/src/main.rs | 6 ++ 9 files changed, 336 insertions(+) create mode 100644 crates/erp-ai/src/entity/ai_chat_message.rs create mode 100644 crates/erp-ai/src/entity/ai_chat_session.rs create mode 100644 crates/erp-ai/src/entity/ai_tool_call_log.rs create mode 100644 crates/erp-ai/src/service/chat_message.rs create mode 100644 crates/erp-ai/src/service/chat_session.rs diff --git a/crates/erp-ai/src/entity/ai_chat_message.rs b/crates/erp-ai/src/entity/ai_chat_message.rs new file mode 100644 index 0000000..d8e03ba --- /dev/null +++ b/crates/erp-ai/src/entity/ai_chat_message.rs @@ -0,0 +1,41 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "ai_chat_messages")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub session_id: Uuid, + pub role: String, + pub content: Option, + pub tool_calls: Option, + pub tool_call_id: Option, + pub token_count: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Option, + pub updated_by: Option, + pub deleted_at: Option, + #[sea_orm(default_value = 1)] + pub version_lock: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::ai_chat_session::Entity", + from = "Column::SessionId", + to = "super::ai_chat_session::Column::Id" + )] + Session, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-ai/src/entity/ai_chat_session.rs b/crates/erp-ai/src/entity/ai_chat_session.rs new file mode 100644 index 0000000..a0f5419 --- /dev/null +++ b/crates/erp-ai/src/entity/ai_chat_session.rs @@ -0,0 +1,37 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "ai_chat_sessions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub user_id: Uuid, + pub patient_id: Option, + pub title: Option, + #[sea_orm(default_value = "active")] + pub status: String, + pub metadata: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Option, + pub updated_by: Option, + pub deleted_at: Option, + #[sea_orm(default_value = 1)] + pub version_lock: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::ai_chat_message::Entity")] + Messages, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Messages.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-ai/src/entity/ai_tool_call_log.rs b/crates/erp-ai/src/entity/ai_tool_call_log.rs new file mode 100644 index 0000000..2e50312 --- /dev/null +++ b/crates/erp-ai/src/entity/ai_tool_call_log.rs @@ -0,0 +1,37 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "ai_tool_call_logs")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub session_id: Uuid, + pub message_id: Uuid, + pub tool_name: String, + pub parameters: Option, + pub result_summary: Option, + pub execution_ms: Option, + pub success: bool, + pub created_at: DateTimeUtc, + pub created_by: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::ai_chat_session::Entity", + from = "Column::SessionId", + to = "super::ai_chat_session::Column::Id" + )] + Session, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-ai/src/entity/mod.rs b/crates/erp-ai/src/entity/mod.rs index fc3ccbe..09dbdb1 100644 --- a/crates/erp-ai/src/entity/mod.rs +++ b/crates/erp-ai/src/entity/mod.rs @@ -1,5 +1,7 @@ pub mod ai_analysis; pub mod ai_analysis_queue; +pub mod ai_chat_message; +pub mod ai_chat_session; pub mod ai_feature_flags; pub mod ai_knowledge_guides; pub mod ai_knowledge_references; @@ -9,6 +11,7 @@ pub mod ai_risk_threshold; pub mod ai_suggestion; pub mod ai_suggestion_feedback; pub mod ai_tenant_config; +pub mod ai_tool_call_log; pub mod ai_usage; pub mod ai_usage_daily; pub mod copilot_chat_logs; diff --git a/crates/erp-ai/src/service/chat_message.rs b/crates/erp-ai/src/service/chat_message.rs new file mode 100644 index 0000000..5a0055d --- /dev/null +++ b/crates/erp-ai/src/service/chat_message.rs @@ -0,0 +1,105 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, + QuerySelect, Set, +}; +use uuid::Uuid; + +use crate::entity::ai_chat_message; +use crate::entity::ai_tool_call_log; + +pub struct ChatMessageService { + db: DatabaseConnection, +} + +pub struct SaveMessageParams { + pub tenant_id: Uuid, + pub session_id: Uuid, + pub role: String, + pub content: Option, + pub tool_calls: Option, + pub tool_call_id: Option, + pub token_count: Option, + pub user_id: Uuid, +} + +pub struct SaveToolCallLogParams { + pub tenant_id: Uuid, + pub session_id: Uuid, + pub message_id: Uuid, + pub tool_name: String, + pub parameters: Option, + pub result_summary: Option, + pub execution_ms: i32, + pub success: bool, + pub user_id: Uuid, +} + +impl ChatMessageService { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } + + pub async fn save_message( + &self, + params: SaveMessageParams, + ) -> Result { + let id = Uuid::now_v7(); + let now = chrono::Utc::now(); + let model = ai_chat_message::ActiveModel { + id: Set(id), + tenant_id: Set(params.tenant_id), + session_id: Set(params.session_id), + role: Set(params.role), + content: Set(params.content), + tool_calls: Set(params.tool_calls), + tool_call_id: Set(params.tool_call_id), + token_count: Set(params.token_count), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(Some(params.user_id)), + updated_by: Set(Some(params.user_id)), + deleted_at: Set(None), + version_lock: Set(1), + }; + let result = model.insert(&self.db).await?; + Ok(result) + } + + pub async fn list_messages( + &self, + tenant_id: Uuid, + session_id: Uuid, + limit: u64, + ) -> Result, sea_orm::DbErr> { + ai_chat_message::Entity::find() + .filter(ai_chat_message::Column::TenantId.eq(tenant_id)) + .filter(ai_chat_message::Column::SessionId.eq(session_id)) + .filter(ai_chat_message::Column::DeletedAt.is_null()) + .order_by_asc(ai_chat_message::Column::CreatedAt) + .limit(limit) + .all(&self.db) + .await + } + + pub async fn save_tool_call_log( + &self, + params: SaveToolCallLogParams, + ) -> Result { + let id = Uuid::now_v7(); + let model = ai_tool_call_log::ActiveModel { + id: Set(id), + tenant_id: Set(params.tenant_id), + session_id: Set(params.session_id), + message_id: Set(params.message_id), + tool_name: Set(params.tool_name), + parameters: Set(params.parameters), + result_summary: Set(params.result_summary), + execution_ms: Set(Some(params.execution_ms)), + success: Set(params.success), + created_at: Set(chrono::Utc::now()), + created_by: Set(Some(params.user_id)), + }; + let result = model.insert(&self.db).await?; + Ok(result) + } +} diff --git a/crates/erp-ai/src/service/chat_session.rs b/crates/erp-ai/src/service/chat_session.rs new file mode 100644 index 0000000..54a943a --- /dev/null +++ b/crates/erp-ai/src/service/chat_session.rs @@ -0,0 +1,101 @@ +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, Set, +}; +use uuid::Uuid; + +use crate::entity::ai_chat_session; + +pub struct ChatSessionService { + db: DatabaseConnection, +} + +impl ChatSessionService { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } + + pub async fn create( + &self, + tenant_id: Uuid, + user_id: Uuid, + patient_id: Option, + title: Option, + ) -> Result { + let id = Uuid::now_v7(); + let now = chrono::Utc::now(); + let model = ai_chat_session::ActiveModel { + id: Set(id), + tenant_id: Set(tenant_id), + user_id: Set(user_id), + patient_id: Set(patient_id), + title: Set(title), + status: Set("active".to_string()), + metadata: Set(None), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(Some(user_id)), + updated_by: Set(Some(user_id)), + deleted_at: Set(None), + version_lock: Set(1), + }; + let result = model.insert(&self.db).await?; + Ok(result) + } + + pub async fn list( + &self, + tenant_id: Uuid, + user_id: Uuid, + ) -> Result, sea_orm::DbErr> { + ai_chat_session::Entity::find() + .filter(ai_chat_session::Column::TenantId.eq(tenant_id)) + .filter(ai_chat_session::Column::UserId.eq(user_id)) + .filter(ai_chat_session::Column::DeletedAt.is_null()) + .filter(ai_chat_session::Column::Status.ne("closed")) + .order_by_desc(ai_chat_session::Column::UpdatedAt) + .all(&self.db) + .await + } + + pub async fn get( + &self, + tenant_id: Uuid, + session_id: Uuid, + ) -> Result, sea_orm::DbErr> { + ai_chat_session::Entity::find() + .filter(ai_chat_session::Column::TenantId.eq(tenant_id)) + .filter(ai_chat_session::Column::Id.eq(session_id)) + .filter(ai_chat_session::Column::DeletedAt.is_null()) + .one(&self.db) + .await + } + + pub async fn close(&self, tenant_id: Uuid, session_id: Uuid) -> Result { + let session = self.get(tenant_id, session_id).await?; + let Some(session) = session else { + return Ok(false); + }; + let mut active: ai_chat_session::ActiveModel = session.into(); + active.status = Set("closed".to_string()); + active.updated_at = Set(chrono::Utc::now()); + active.update(&self.db).await?; + Ok(true) + } + + pub async fn rename( + &self, + tenant_id: Uuid, + session_id: Uuid, + new_title: String, + ) -> Result { + let session = self.get(tenant_id, session_id).await?; + let Some(session) = session else { + return Ok(false); + }; + let mut active: ai_chat_session::ActiveModel = session.into(); + active.title = Set(Some(new_title)); + active.updated_at = Set(chrono::Utc::now()); + active.update(&self.db).await?; + Ok(true) + } +} diff --git a/crates/erp-ai/src/service/mod.rs b/crates/erp-ai/src/service/mod.rs index 2814964..a051225 100644 --- a/crates/erp-ai/src/service/mod.rs +++ b/crates/erp-ai/src/service/mod.rs @@ -2,6 +2,8 @@ pub mod analysis; pub mod analysis_queue; pub mod auto_analysis; pub mod cache; +pub mod chat_message; +pub mod chat_session; pub mod comparison; pub mod cost; pub mod dialysis_risk_scorer; diff --git a/crates/erp-ai/src/state.rs b/crates/erp-ai/src/state.rs index 1503df9..e06dc4a 100644 --- a/crates/erp-ai/src/state.rs +++ b/crates/erp-ai/src/state.rs @@ -7,6 +7,8 @@ use sea_orm::DatabaseConnection; use crate::provider::registry::ProviderRegistry; use crate::service::analysis::AnalysisService; use crate::service::cache::CacheService; +use crate::service::chat_message::ChatMessageService; +use crate::service::chat_session::ChatSessionService; use crate::service::feature_flag_service::FeatureFlagService; use crate::service::insight_service::InsightService; use crate::service::knowledge::KnowledgeService; @@ -32,4 +34,6 @@ pub struct AiState { pub insight_service: Arc, pub feature_flags: Arc, pub knowledge: Arc, + pub chat_session: Arc, + pub chat_message: Arc, } diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index 99af28b..4b44bf6 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -615,6 +615,12 @@ async fn main() -> anyhow::Result<()> { erp_ai::service::embedding::EmbeddingService::from_settings(&db).await, ), )), + chat_session: std::sync::Arc::new( + erp_ai::service::chat_session::ChatSessionService::new(db.clone()), + ), + chat_message: std::sync::Arc::new( + erp_ai::service::chat_message::ChatMessageService::new(db.clone()), + ), } };