gitdataai/libs/room/src/service/mod.rs
2026-05-14 10:02:21 +08:00

410 lines
13 KiB
Rust

mod access;
mod access_write;
mod ai_common;
mod ai_mode_streaming;
mod ai_mode_streaming_post;
mod ai_mode_streaming_steps;
mod ai_nonstreaming;
mod ai_react_nonstreaming;
mod ai_react_streaming;
mod ai_react_streaming_post;
mod ai_react_streaming_steps;
mod ai_service;
mod ai_streaming;
mod history;
mod mentions;
mod notifications;
mod patterns;
mod process_ai;
mod sequence;
mod type_convert;
mod validation;
mod workers;
mod workers_spawn;
pub use patterns::{mention_bracket_re, mention_tag_re, user_mention_re};
pub use access::{
check_project_member, check_room_access, find_room_or_404, get_or_create_room_user_state,
is_project_admin, is_room_admin, require_project_admin, require_room_access,
require_room_admin,
};
pub use ai_common::create_and_publish_ai_message;
pub use ai_nonstreaming::process_message_ai_nonstreaming;
pub use ai_react_nonstreaming::process_message_ai_react_nonstreaming;
pub use ai_react_streaming::process_message_ai_react_streaming;
pub use ai_service::RoomAiService;
pub use ai_streaming::process_message_ai_streaming;
pub use history::{extract_mention_context, get_room_ai_config, get_room_history, get_user_names};
pub use mentions::extract_mentions;
pub use notifications::{notify_project_members, publish_room_event};
pub use sequence::next_room_message_seq_internal;
pub use workers::{PushNotificationFn, start_workers};
pub use workers_spawn::{spawn_agent_task, spawn_room_workers, unmark_room_spawned};
use std::sync::Arc;
use chrono::Utc;
use db::cache::AppCache;
use db::database::AppDatabase;
use models::rooms::room;
use models::rooms::room_ai;
use queue::{MessageProducer, ProjectRoomEvent};
use sea_orm::{ColumnTrait, QueryFilter};
use uuid::Uuid;
use crate::connection::{DedupCache, RoomConnectionManager};
use crate::error::RoomError;
use crate::presence::PresenceStore;
use agent::TaskService;
use agent::chat::ChatService;
use agent::embed::EmbedService;
use models::agent_task::AgentType;
const DEFAULT_MAX_CONCURRENT_WORKERS: usize = 1024;
#[derive(Clone)]
pub struct RoomService {
pub db: AppDatabase,
pub cache: AppCache,
pub config: config::AppConfig,
pub room_manager: Arc<RoomConnectionManager>,
pub queue: MessageProducer,
pub nats: Option<Arc<queue::NatsClient>>,
pub chat_service: Option<Arc<ChatService>>,
pub task_service: Option<Arc<TaskService>>,
pub embed_service: Option<Arc<EmbedService>>,
pub push_fn: Option<workers::PushNotificationFn>,
pub ai_service: RoomAiService,
pub presence: PresenceStore,
worker_semaphore: Arc<tokio::sync::Semaphore>,
dedup_cache: DedupCache,
}
impl RoomService {
pub fn new(
db: AppDatabase,
cache: AppCache,
config: config::AppConfig,
queue: MessageProducer,
room_manager: Arc<RoomConnectionManager>,
nats: Option<Arc<queue::NatsClient>>,
chat_service: Option<Arc<ChatService>>,
task_service: Option<Arc<TaskService>>,
max_concurrent_workers: Option<usize>,
push_fn: Option<workers::PushNotificationFn>,
embed_service: Option<Arc<EmbedService>>,
) -> Self {
let dedup_cache: DedupCache = Arc::new(dashmap::DashMap::with_capacity_and_hasher(
10000,
Default::default(),
));
let ai_service = RoomAiService::new(
db.clone(),
cache.clone(),
config.clone(),
queue.clone(),
room_manager.clone(),
chat_service.clone(),
);
Self {
db,
cache,
config,
room_manager,
queue,
nats,
chat_service,
task_service,
embed_service,
ai_service,
worker_semaphore: Arc::new(tokio::sync::Semaphore::new(
max_concurrent_workers.unwrap_or(DEFAULT_MAX_CONCURRENT_WORKERS),
)),
dedup_cache,
push_fn,
presence: PresenceStore::new(),
}
}
pub async fn start_workers(
&self,
shutdown_rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<()> {
workers::start_workers(
self.db.clone(),
self.cache.clone(),
self.room_manager.clone(),
self.queue.clone(),
self.nats.clone(),
self.dedup_cache.clone(),
self.task_service.clone(),
None, // max_concurrent_workers handled by semaphore
shutdown_rx,
self.embed_service.clone(),
)
.await
}
pub async fn spawn_agent_task<F, Fut>(
&self,
project_id: Uuid,
agent_type: AgentType,
input: String,
_title: Option<String>,
execute: F,
) -> anyhow::Result<i64>
where
F: FnOnce(i64, Arc<TaskService>) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<String, String>> + Send,
{
let task_service = match &self.task_service {
Some(ts) => ts.clone(),
None => return Err(anyhow::anyhow!("task service not configured")),
};
workers_spawn::spawn_agent_task(
project_id,
agent_type,
input,
task_service,
self.queue.clone(),
self.room_manager.clone(),
self.worker_semaphore.clone(),
execute,
)
.await
}
pub fn spawn_room_workers(&self, room_id: uuid::Uuid) {
workers_spawn::spawn_room_workers(
room_id,
self.db.clone(),
self.room_manager.clone(),
self.queue.clone(),
self.nats.clone(),
self.worker_semaphore.clone(),
self.embed_service.clone(),
);
}
pub async fn publish_room_event(
&self,
project_id: uuid::Uuid,
event_type: super::RoomEventType,
room_id: Option<uuid::Uuid>,
category_id: Option<uuid::Uuid>,
message_id: Option<uuid::Uuid>,
seq: Option<i64>,
) {
let event = ProjectRoomEvent {
event_type: event_type.as_str().into(),
project_id,
room_id,
category_id,
message_id,
seq,
timestamp: Utc::now(),
};
self.queue
.publish_project_room_event(project_id, event)
.await;
}
pub fn notify_project_members(
&self,
project_id: uuid::Uuid,
notification_type: super::NotificationType,
title: String,
content: Option<String>,
related_room_id: Option<uuid::Uuid>,
) {
notifications::notify_project_members(
self.db.clone(),
project_id,
notification_type,
title,
content,
related_room_id,
);
}
pub fn extract_mentions(content: &str) -> Vec<Uuid> {
mentions::extract_mentions(content)
}
pub async fn resolve_mentions(&self, content: &str) -> Vec<Uuid> {
use models::users::User;
use sea_orm::EntityTrait;
let mut resolved = Vec::new();
let mut seen_usernames = Vec::new();
for cap in mention_bracket_re().captures_iter(content) {
if let (Some(type_m), Some(id_m)) = (cap.get(1), cap.get(2)) {
if type_m.as_str() == "user" {
let id = id_m.as_str().trim();
if let Ok(uuid) = Uuid::parse_str(id) {
if !resolved.contains(&uuid) {
resolved.push(uuid);
}
} else if let Some(label_m) = cap.get(3) {
let label = label_m.as_str().trim();
if !label.is_empty() {
let label_lower = label.to_lowercase();
if seen_usernames.contains(&label_lower) {
continue;
}
seen_usernames.push(label_lower.clone());
if let Some(user) = User::find()
.filter(models::users::user::Column::Username.ilike(&label_lower))
.one(&self.db)
.await
.ok()
.flatten()
{
if !resolved.contains(&user.uid) {
resolved.push(user.uid);
}
}
}
}
}
}
}
resolved
}
pub async fn check_room_access(&self, room_id: Uuid, user_id: Uuid) -> Result<(), RoomError> {
access::check_room_access(&self.db, room_id, user_id).await
}
pub async fn check_project_member(
&self,
project_id: Uuid,
user_id: Uuid,
) -> Result<(), RoomError> {
access::check_project_member(&self.db, project_id, user_id).await
}
pub async fn should_ai_respond(&self, room_id: Uuid, content: &str) -> Result<bool, RoomError> {
self.ai_service.should_respond(room_id, content).await
}
pub async fn get_room_ai_config(
&self,
room_id: Uuid,
) -> Result<Option<room_ai::Model>, RoomError> {
history::get_room_ai_config(&self.db, room_id).await
}
pub async fn get_user_names(
&self,
user_ids: &[Uuid],
) -> std::collections::HashMap<Uuid, String> {
history::get_user_names(&self.db, user_ids).await
}
pub async fn require_room_access(&self, room_id: Uuid, user_id: Uuid) -> Result<(), RoomError> {
access::require_room_access(&self.db, room_id, user_id).await
}
pub async fn require_room_admin(&self, room_id: Uuid, user_id: Uuid) -> Result<(), RoomError> {
access::require_room_admin(&self.db, room_id, user_id).await
}
pub async fn is_room_admin(&self, room_id: Uuid, user_id: Uuid) -> bool {
access::is_room_admin(&self.db, room_id, user_id).await
}
pub async fn require_project_admin(
&self,
project_id: Uuid,
user_id: Uuid,
) -> Result<(), RoomError> {
access::require_project_admin(&self.db, project_id, user_id).await
}
pub async fn find_room_or_404(&self, room_id: Uuid) -> Result<room::Model, RoomError> {
access::find_room_or_404(&self.db, room_id).await
}
// ─── Presence Methods ─────────────────────────────────────────────────────
/// Set user presence in a project context and broadcast to project subscribers.
/// Returns the local PresenceChanged type - caller is responsible for conversion.
pub async fn set_user_presence(
&self,
user_id: Uuid,
project_id: Option<Uuid>,
status: crate::presence::PresenceStatus,
) -> Option<crate::presence::PresenceChanged> {
let event = self.presence.set_presence(user_id, project_id, status);
if event.is_some() {
// Broadcast to project subscribers
if let Some(pid) = project_id {
self.room_manager
.broadcast_project(
pid,
queue::ProjectRoomEvent {
event_type: "presence_changed".into(),
project_id: pid,
room_id: None,
category_id: None,
message_id: None,
seq: None,
timestamp: chrono::Utc::now(),
},
)
.await;
}
}
event
}
/// Set user custom status (emoji, text).
pub fn set_custom_status(
&self,
user_id: Uuid,
emoji: Option<String>,
text: Option<String>,
expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Option<crate::presence::CustomStatusChanged> {
self.presence
.set_custom_status(user_id, emoji, text, expires_at)
}
/// Get all presence entries for a project.
pub fn get_project_presence(&self, project_id: Uuid) -> Vec<crate::presence::PresenceChanged> {
self.presence.get_project_presence(project_id)
}
/// Remove user presence when they disconnect.
pub async fn remove_user_presence(
&self,
user_id: Uuid,
project_id: Option<Uuid>,
) -> Option<crate::presence::PresenceChanged> {
let event = self.presence.remove_presence(user_id, project_id);
if event.is_some() {
// Broadcast to project subscribers
if let Some(pid) = project_id {
self.room_manager
.broadcast_project(
pid,
queue::ProjectRoomEvent {
event_type: "presence_changed".into(),
project_id: pid,
room_id: None,
category_id: None,
message_id: None,
seq: None,
timestamp: chrono::Utc::now(),
},
)
.await;
}
}
event
}
}