- FastAPI + WebSocket 백엔드 (ws_relay, webhook, messages 라우터) - GUARDiA-Bot: @bot 명령 응답 + 선제적 맥락 분석 (DB 지연, 디스크, 장애 감지) - 승인 워크플로우: 재기동/배포 SR → 승인/반려 인터랙티브 버튼 - 다크 테마 Slack형 프론트엔드 (5개 채널, 실시간 메시지) - 채널: 일반/배포/운영/PM관리/알림 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
3.3 KiB
Python
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)
|