zioinfo-mail/messenger/routers/ws_relay.py
DESKTOP-TKLFCPR\ython 85e4901541 feat(messenger): Slack형 실시간 채팅 메신저 구현
- FastAPI + WebSocket 백엔드 (ws_relay, webhook, messages 라우터)
- GUARDiA-Bot: @bot 명령 응답 + 선제적 맥락 분석 (DB 지연, 디스크, 장애 감지)
- 승인 워크플로우: 재기동/배포 SR → 승인/반려 인터랙티브 버튼
- 다크 테마 Slack형 프론트엔드 (5개 채널, 실시간 메시지)
- 채널: 일반/배포/운영/PM관리/알림

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:04:03 +09:00

108 lines
3.3 KiB
Python

import asyncio
import json
from datetime import datetime
from typing import Dict, List
from uuid import uuid4
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from core.bot import (
ROOMS,
check_proactive_context,
get_bot_response,
message_store,
store_message,
)
router = APIRouter()
# room_id → list of connected WebSocket clients
room_channels: Dict[str, List[WebSocket]] = {}
def _new_msg_id() -> str:
return f"MSG-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:8].upper()}"
async def broadcast(room_id: str, payload: dict) -> None:
dead: List[WebSocket] = []
for ws in room_channels.get(room_id, []):
try:
await ws.send_text(json.dumps(payload, ensure_ascii=False))
except Exception:
dead.append(ws)
for ws in dead:
room_channels[room_id].remove(ws)
def _make_bot_msg(room_id: str, reply: dict) -> dict:
return {
"message_id": _new_msg_id(),
"timestamp": datetime.now().isoformat(),
"room_id": room_id,
"sender": "GUARDiA-Bot",
"sender_type": "BOT",
"msg_type": "CHAT",
"content": reply.get("content", ""),
"is_widget": reply.get("is_widget", False),
"interactive_action": reply.get("interactive_action"),
}
@router.websocket("/ws/chat/{room_id}/{client_id}")
async def chat_endpoint(ws: WebSocket, room_id: str, client_id: str):
await ws.accept()
room_channels.setdefault(room_id, []).append(ws)
# 접속 시 최근 메시지 + 방 목록 전달
history = message_store.get(room_id, [])[-50:]
await ws.send_text(json.dumps(
{"type": "init", "messages": history, "rooms": ROOMS},
ensure_ascii=False,
))
try:
while True:
raw = await ws.receive_text()
data = json.loads(raw)
text = data.get("content", "").strip()
if not text:
continue
# 사용자 메시지 브로드캐스트
user_msg = {
"message_id": _new_msg_id(),
"timestamp": datetime.now().isoformat(),
"room_id": room_id,
"sender": client_id,
"sender_type": "HUMAN",
"msg_type": "CHAT",
"content": text,
"is_widget": False,
"interactive_action": None,
}
store_message(room_id, user_msg)
await broadcast(room_id, user_msg)
# 봇 명령 응답
bot_reply = get_bot_response(text)
if bot_reply:
await asyncio.sleep(0.4)
bot_msg = _make_bot_msg(room_id, bot_reply)
store_message(room_id, bot_msg)
await broadcast(room_id, bot_msg)
continue
# 선제적 맥락 분석
proactive = check_proactive_context(text)
if proactive:
await asyncio.sleep(1.2)
pro_msg = _make_bot_msg(room_id, proactive)
store_message(room_id, pro_msg)
await broadcast(room_id, pro_msg)
except WebSocketDisconnect:
lst = room_channels.get(room_id, [])
if ws in lst:
lst.remove(ws)