gitdataai/libs/transport/handler/sse.rs
ZhenYi 14f6e1e500 feat(core): initialize project with access control and AI integration
- Add gitignore and prettier configuration files for project scaffolding
- Implement room access control service with project member verification
- Create user access key management with CRUD operations and activity logging
- Add accordion UI component for frontend expandable sections
- Implement room AI configuration with list, upsert, and delete operations
- Add AI event types for agent join/leave/status change tracking
- Create streaming AI processing services for mode and react patterns
- Build room AI service with model detection and idempotency handling
- Integrate chat service orchestration for AI message processing
- Add typing indicators and stream cancellation for AI interactions
- Implement mention parsing and context extraction for AI agents
2026-05-03 06:04:31 +08:00

77 lines
2.8 KiB
Rust

use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::web::Bytes;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
use uuid::Uuid;
use service::AppService;
use queue::RoomMessageStreamChunkEvent;
/// SSE endpoint: GET /ws/ai-stream/{room_id}/{message_id}
pub async fn ws_ai_stream(
service: web::Data<AppService>,
req: HttpRequest,
path: web::Path<(Uuid, Uuid)>,
) -> Result<HttpResponse, actix_web::Error> {
let (room_id, message_id) = path.into_inner();
let user_id = if let Some(token) = req.uri().query().and_then(|q| {
q.split('&').find(|p| p.starts_with("token=")).and_then(|p| p.split('=').nth(1))
}) {
match service.ws_token.validate_token(token).await {
Ok(uid) => uid,
Err(_) => return Err(actix_web::error::ErrorUnauthorized("invalid token")),
}
} else {
return Err(actix_web::error::ErrorUnauthorized("no auth provided"));
};
if let Err(e) = service.room.check_room_access(room_id, user_id).await {
tracing::warn!(user_id = %user_id, room_id = %room_id, error = ?e, "AI SSE: access denied");
return Err(actix_web::error::ErrorForbidden("access denied").into());
}
tracing::info!(user_id = %user_id, room_id = %room_id, message_id = %message_id, "AI SSE stream opened");
let manager = service.room.room_manager.clone();
let stream_rx = manager.subscribe_stream(message_id).await;
let stream_rx = match stream_rx {
Some(rx) => rx,
None => return Err(actix_web::error::ErrorNotFound("stream not found").into()),
};
let sse_stream = BroadcastStream::new(stream_rx)
.map(|result| match result {
Ok(chunk) => {
let data = format_sse_chunk(&chunk);
if chunk.done {
Ok::<Bytes, actix_web::Error>(Bytes::from(format!("{}event: done\ndata: \n\n", data)))
} else {
Ok::<Bytes, actix_web::Error>(Bytes::from(data))
}
}
Err(_) => Ok::<Bytes, actix_web::Error>(Bytes::from(": keepalive\n\n")),
});
Ok(HttpResponse::Ok()
.content_type("text/event-stream")
.append_header(("Cache-Control", "no-cache"))
.append_header(("Connection", "keep-alive"))
.append_header(("X-Accel-Buffering", "no"))
.streaming(sse_stream))
}
fn format_sse_chunk(chunk: &RoomMessageStreamChunkEvent) -> String {
let json = serde_json::json!({
"message_id": chunk.message_id,
"room_id": chunk.room_id,
"seq": chunk.seq,
"content": chunk.content,
"done": chunk.done,
"error": chunk.error,
"display_name": chunk.display_name,
"chunk_type": chunk.chunk_type,
});
format!("event: chunk\ndata: {}\n\n", json)
}