feat(ai): Day 7 — 会话持久化 Entity + Service

- 新增 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 初始化注入
This commit is contained in:
iven
2026-05-19 11:33:37 +08:00
parent b03ea47fed
commit de342f9195
9 changed files with 336 additions and 0 deletions

View File

@@ -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<String>,
pub tool_calls: Option<serde_json::Value>,
pub tool_call_id: Option<String>,
pub token_count: Option<i32>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub created_by: Option<Uuid>,
pub updated_by: Option<Uuid>,
pub deleted_at: Option<DateTimeUtc>,
#[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<super::ai_chat_session::Entity> for Entity {
fn to() -> RelationDef {
Relation::Session.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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<Uuid>,
pub title: Option<String>,
#[sea_orm(default_value = "active")]
pub status: String,
pub metadata: Option<serde_json::Value>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub created_by: Option<Uuid>,
pub updated_by: Option<Uuid>,
pub deleted_at: Option<DateTimeUtc>,
#[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<super::ai_chat_message::Entity> for Entity {
fn to() -> RelationDef {
Relation::Messages.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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<serde_json::Value>,
pub result_summary: Option<String>,
pub execution_ms: Option<i32>,
pub success: bool,
pub created_at: DateTimeUtc,
pub created_by: Option<Uuid>,
}
#[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<super::ai_chat_session::Entity> for Entity {
fn to() -> RelationDef {
Relation::Session.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

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

View File

@@ -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<String>,
pub tool_calls: Option<serde_json::Value>,
pub tool_call_id: Option<String>,
pub token_count: Option<i32>,
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<serde_json::Value>,
pub result_summary: Option<String>,
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<ai_chat_message::Model, sea_orm::DbErr> {
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<Vec<ai_chat_message::Model>, 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<ai_tool_call_log::Model, sea_orm::DbErr> {
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)
}
}

View File

@@ -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<Uuid>,
title: Option<String>,
) -> Result<ai_chat_session::Model, sea_orm::DbErr> {
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<Vec<ai_chat_session::Model>, 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<Option<ai_chat_session::Model>, 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<bool, sea_orm::DbErr> {
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<bool, sea_orm::DbErr> {
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)
}
}

View File

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

View File

@@ -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<InsightService>,
pub feature_flags: Arc<FeatureFlagService>,
pub knowledge: Arc<KnowledgeService>,
pub chat_session: Arc<ChatSessionService>,
pub chat_message: Arc<ChatMessageService>,
}

View File

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