import json import time from django.conf import settings from django.http import StreamingHttpResponse from ninja import Router from ninja.errors import HttpError from apps.accounts.api import require_permission from apps.tickets.models import TicketAutomationRun router = Router(tags=["ticket-automation-runs"]) def _cursor(request, after: int | None) -> int: raw = request.headers.get("Last-Event-ID") or after or 0 try: return max(int(raw), 0) except (TypeError, ValueError) as error: raise HttpError(400, "事件游标无效") from error def _format_event(event) -> str: payload = { "sequence": event.sequence, "event_type": event.event_type, "payload": event.payload, "created_at": event.created_at.isoformat(), } return ( f"id: {event.sequence}\n" f"event: {event.event_type}\n" f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" ) @router.get("/{public_id}/events") def stream_ticket_automation_events(request, public_id: str, after: int | None = None): require_permission(request, "ticket.view") run = TicketAutomationRun.objects.filter(public_id=public_id).first() if run is None: raise HttpError(404, "工单自动化运行不存在") cursor = _cursor(request, after) terminal = {"completed", "failed", "cancelled"} max_seconds = getattr(settings, "AGENT_RUN_SSE_MAX_SECONDS", 60) def event_stream(): current = cursor started = time.monotonic() heartbeat = started while time.monotonic() - started < max_seconds: events = list(run.events.filter(sequence__gt=current)[:100]) for event in events: current = event.sequence yield _format_event(event) if events and events[-1].event_type in terminal: return now = time.monotonic() if now - heartbeat >= 15: yield ": keep-alive\n\n" heartbeat = now time.sleep(0.5) response = StreamingHttpResponse(event_stream(), content_type="text/event-stream") response["Cache-Control"] = "no-cache" response["X-Accel-Buffering"] = "no" return response