"""全局一炁事件 SSE 端点(非 run-scoped)。 供前端 useGlobalEventStream hook 订阅全局广播事件: - ``proactive.session`` —— 叩心枢新建的主动会话 - ``proactive.message`` —— 叩心枢追加到已有会话的消息 """ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator from typing import Any from fastapi import APIRouter, Request from sse_starlette.sse import EventSourceResponse from app.core.config import settings from app.services.event_store import DbEventStore router = APIRouter(prefix="/events", tags=["events"]) def _get_event_store(request: Request) -> DbEventStore: return request.app.state.event_store @router.get("") async def global_event_stream( request: Request, ) -> EventSourceResponse: """全局事件 SSE 流:通过 EventStore.subscribe_by_pattern 监听所有 proactive.* 事件,推送给前端。""" if not settings.enable_sse: from app.core.errors import AppError raise AppError("invalid_request", "SSE 当前未启用。") event_store = _get_event_store(request) return EventSourceResponse(_event_generator(event_store)) async def _event_generator( event_store: DbEventStore, ) -> AsyncGenerator[dict[str, Any], None]: queue = event_store.subscribe_by_pattern("proactive.*") try: while True: try: event = await asyncio.wait_for(queue.get(), timeout=settings.sse_heartbeat_seconds) yield { "id": event.id, "event": event.type, "data": event.model_dump_json(), } except TimeoutError: yield { "event": "heartbeat", "data": '{"type":"heartbeat"}', } except asyncio.CancelledError: pass finally: event_store.unsubscribe_by_pattern("proactive.*", queue)