Refine room AI streaming logic, update TOTP auth error handling, and adjust user 2FA migration order. Remove unused service exports.
261 lines
8.9 KiB
Rust
261 lines
8.9 KiB
Rust
use std::sync::Arc;
|
|
|
|
use ::agent::chat::ChatService;
|
|
use ::agent::client::AiClientConfig;
|
|
use ::agent::tool::ToolRegistry;
|
|
use ::agent::{new_embed_client, EmbedService, TaskService};
|
|
use avatar::AppAvatar;
|
|
use config::AppConfig;
|
|
use db::cache::AppCache;
|
|
use db::database::AppDatabase;
|
|
use email::AppEmail;
|
|
use queue::{
|
|
start_email_worker, EmailEnvelope, EmailSendFn, EmailSendFut, MessageProducer, NatsClient,
|
|
};
|
|
use room::metrics::RoomMetrics;
|
|
use room::RoomService;
|
|
use serde::{Deserialize, Serialize};
|
|
use utoipa::ToSchema;
|
|
use ws_token::WsTokenService;
|
|
|
|
pub mod storage;
|
|
pub use storage::AppStorage;
|
|
pub mod push;
|
|
pub use push::{PushPayload, WebPushService};
|
|
pub mod push_helper;
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppService {
|
|
pub db: AppDatabase,
|
|
pub config: AppConfig,
|
|
pub cache: AppCache,
|
|
pub email: AppEmail,
|
|
pub avatar: AppAvatar,
|
|
pub room: RoomService,
|
|
pub ws_token: Arc<WsTokenService>,
|
|
pub queue_producer: MessageProducer,
|
|
pub storage: Option<AppStorage>,
|
|
pub push: Option<Arc<WebPushService>>,
|
|
pub embed_service: Option<Arc<EmbedService>>,
|
|
pub nats: Option<Arc<NatsClient>>,
|
|
pub chat_service: Option<Arc<ChatService>>,
|
|
}
|
|
|
|
impl AppService {
|
|
pub fn send_push_to_user(&self, user_id: uuid::Uuid, payload: PushPayload) {
|
|
push_helper::spawn_push_notification(self.push.clone(), self.db.clone(), user_id, payload);
|
|
}
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn start_room_workers(
|
|
&self,
|
|
shutdown_rx: tokio::sync::broadcast::Receiver<()>,
|
|
) -> anyhow::Result<()> {
|
|
self.room.start_workers(shutdown_rx).await
|
|
}
|
|
|
|
pub async fn new(config: AppConfig) -> anyhow::Result<Self> {
|
|
let db = AppDatabase::init(&config).await?;
|
|
let cache = AppCache::init(&config).await?;
|
|
|
|
let email = AppEmail::init(&config).await?;
|
|
let avatar = AppAvatar::init(&config).await?;
|
|
let storage = match AppStorage::new(&config) {
|
|
Ok(s) => {
|
|
tracing::info!(path = %s.base_path.display(), "Storage initialized");
|
|
Some(s)
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "Storage not available");
|
|
None
|
|
}
|
|
};
|
|
|
|
let push = match (config.vapid_public_key(), config.vapid_private_key()) {
|
|
(Some(public_key), Some(private_key)) => {
|
|
match WebPushService::new(public_key, private_key, config.vapid_sender_email()) {
|
|
Ok(s) => {
|
|
tracing::info!("WebPush initialized");
|
|
Some(Arc::new(s))
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "WebPush not available");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
tracing::warn!("WebPush disabled - VAPID keys not configured");
|
|
None
|
|
}
|
|
};
|
|
|
|
// Redis connection getter — used by MessageProducer (for cache/seq) and WsTokenService
|
|
let get_redis: Arc<
|
|
dyn Fn() -> tokio::task::JoinHandle<anyhow::Result<deadpool_redis::cluster::Connection>>
|
|
+ Send
|
|
+ Sync,
|
|
> = Arc::new({
|
|
let pool = cache.redis_pool().clone();
|
|
move || {
|
|
let pool = pool.clone();
|
|
tokio::spawn(async move { pool.get().await.map_err(|e| anyhow::anyhow!("{}", e)) })
|
|
}
|
|
});
|
|
|
|
// Connect to NATS (if configured)
|
|
let nats: Option<Arc<NatsClient>> = match NatsClient::connect(&config).await {
|
|
Some(c) => Some(Arc::new(c)),
|
|
None => None,
|
|
};
|
|
|
|
// Build MessageProducer with NATS
|
|
let message_producer = MessageProducer::new(nats.clone(), get_redis.clone());
|
|
|
|
// Build RoomService
|
|
let task_service = Arc::new(TaskService::new(db.clone()));
|
|
let room_metrics = Arc::new(RoomMetrics::default());
|
|
let room_manager = Arc::new(room::connection::RoomConnectionManager::new(
|
|
room_metrics.clone(),
|
|
cache.clone(),
|
|
));
|
|
|
|
// Build EmbedService if Qdrant and embedding model are configured (graceful degradation)
|
|
let embed_service: Option<Arc<EmbedService>> = match new_embed_client(&config).await {
|
|
Ok(client) => {
|
|
let model_name = config
|
|
.get_embed_model_name()
|
|
.unwrap_or_else(|_| "text-embedding-3-small".into());
|
|
let dimensions = config.get_embed_model_dimensions().unwrap_or(1536);
|
|
let svc = EmbedService::new(client, db.writer().clone(), model_name, dimensions);
|
|
let _ = svc.ensure_collections().await;
|
|
tracing::info!("EmbedService initialized (Qdrant + embeddings)");
|
|
Some(Arc::new(svc))
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "EmbedService not available - vector search disabled");
|
|
None
|
|
}
|
|
};
|
|
|
|
let embed_service_for_app = embed_service.clone();
|
|
|
|
// Build ChatService if AI is configured; otherwise AI chat is disabled (graceful degradation)
|
|
let chat_service: Option<Arc<ChatService>> =
|
|
match (config.ai_api_key(), config.ai_basic_url()) {
|
|
(Ok(api_key), Ok(base_url)) => {
|
|
tracing::info!(url = %base_url, "AI chat enabled");
|
|
let ai_client_config = AiClientConfig::new(api_key).with_base_url(&base_url);
|
|
let mut registry = ToolRegistry::new();
|
|
fctool::git_tools::register_all(&mut registry);
|
|
fctool::file_tools::register_all(&mut registry);
|
|
fctool::project_tools::register_all(&mut registry);
|
|
fctool::chat_tools::register_all(&mut registry);
|
|
let mut chat_svc = ChatService::new()
|
|
.with_ai_client_config(ai_client_config)
|
|
.with_tool_registry(registry);
|
|
if let Some(ref es) = embed_service {
|
|
chat_svc = chat_svc.with_embed_service((**es).clone());
|
|
}
|
|
Some(Arc::new(chat_svc))
|
|
}
|
|
(Err(e), _) => {
|
|
tracing::warn!(error = %e, "AI chat disabled");
|
|
None
|
|
}
|
|
(_, Err(e)) => {
|
|
tracing::warn!(error = %e, "AI chat disabled");
|
|
None
|
|
}
|
|
};
|
|
|
|
// Build push notification callback for RoomService
|
|
let push_fn: Option<room::PushNotificationFn> = push
|
|
.as_ref()
|
|
.map(|push_svc| push_helper::create_push_notification_fn(push_svc.clone(), db.clone()));
|
|
|
|
let room = RoomService::new(
|
|
db.clone(),
|
|
cache.clone(),
|
|
config.clone(),
|
|
message_producer.clone(),
|
|
room_manager,
|
|
nats.clone(),
|
|
chat_service.clone(),
|
|
Some(task_service.clone()),
|
|
None,
|
|
push_fn,
|
|
embed_service,
|
|
);
|
|
|
|
// Build WsTokenService (still uses Redis for token cache)
|
|
let ws_token = Arc::new(WsTokenService::new(get_redis));
|
|
|
|
Ok(Self {
|
|
db,
|
|
config,
|
|
cache,
|
|
email,
|
|
avatar,
|
|
room,
|
|
ws_token,
|
|
queue_producer: message_producer,
|
|
storage,
|
|
push,
|
|
embed_service: embed_service_for_app,
|
|
nats,
|
|
chat_service,
|
|
})
|
|
}
|
|
|
|
pub async fn start_email_workers(
|
|
&self,
|
|
shutdown_rx: tokio::sync::broadcast::Receiver<()>,
|
|
) -> anyhow::Result<()> {
|
|
let email = self.email.clone();
|
|
let send_fn: EmailSendFn = Arc::new(move |envelopes: Vec<EmailEnvelope>| -> EmailSendFut {
|
|
let email = email.clone();
|
|
Box::pin(async move {
|
|
for envelope in envelopes {
|
|
let to = envelope.to.clone();
|
|
let msg = email::EmailMessage {
|
|
to: envelope.to,
|
|
subject: envelope.subject,
|
|
body: envelope.body,
|
|
};
|
|
if let Err(e) = email.send(msg).await {
|
|
tracing::error!(to = %to, error = %e, "email send failed");
|
|
}
|
|
}
|
|
Ok(())
|
|
})
|
|
});
|
|
|
|
let nats_for_email = self.nats.clone();
|
|
start_email_worker(nats_for_email, send_fn, shutdown_rx).await;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub mod agent;
|
|
pub mod auth;
|
|
pub mod chat;
|
|
pub mod error;
|
|
pub mod git;
|
|
pub mod issue;
|
|
pub mod project;
|
|
pub mod pull_request;
|
|
pub mod search;
|
|
pub mod skill;
|
|
pub mod user;
|
|
pub mod utils;
|
|
pub mod ws_token;
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
|
pub struct Pager {
|
|
pub page: i64,
|
|
#[serde(alias = "par_page")]
|
|
pub per_page: i64,
|
|
}
|