"""SSE 内容协商(`docs/05` §3.2、§3.5)。 `Accept` 在文档里是**非必填**头,所以判定规则是"客户端是否**显式拒绝**了 `text/event-stream`",而不是"客户端是否显式接受"。 放在这里而不是写在某个 Controller 里,是因为平台有两个 SSE 端点 (`/api/v1/agent-runs/{run_id}/events` 与 `/api/v1/risk/daily-report/stream`):口径 一旦分叉,同一份客户端代码就会在一个端点上拿到 200、在另一个端点上拿到 406。 """ from __future__ import annotations SSE_MEDIA_TYPE = "text/event-stream" # `text/*` 与 `*/*` 都覆盖 `text/event-stream`,属于"可接受"。 ACCEPTABLE_SSE_TYPES = frozenset({SSE_MEDIA_TYPE, "text/*", "*/*"}) def accepts_event_stream(accept: str | None) -> bool: """`Accept` 是否接受 `text/event-stream`(文档 §3.2:该头**非必填**)。 - 未携带(`None` 或空串)→ 放行:文档写明"默认 `application/json`;SSE 为 `text/event-stream`",即由接口自身决定响应类型,不是客户端错误; - 携带 `text/event-stream`、`text/*` 或 `*/*` 且 `q != 0` → 放行; - 显式携带但只接受其他类型(如 `application/json`)→ 拒绝,由调用方转 `406 SSE_NOT_ACCEPTABLE`(文档 §3.5/§6.4)。 只做"是否可接受"的判定,不参与内容协商排序:SSE 端点只有一种表示。 """ if accept is None or not accept.strip(): return True for entry in accept.split(","): parts = entry.split(";") media_type = parts[0].strip().lower() if media_type not in ACCEPTABLE_SSE_TYPES: continue quality = 1.0 for parameter in parts[1:]: name, _, value = parameter.partition("=") if name.strip().lower() == "q": try: quality = float(value.strip()) except ValueError: quality = 0.0 if quality > 0: return True return False