# -*- coding: utf-8 -*- """事件模型 —— 把一次求解的中间态结构化,经 SSE 推给前端。 设计要点: * 前端只认**一种信封**:`{"seq", "ts", "type", "data"}`,分支全在 `type` 上。 * `type` 的取值集中在下面的 `EVT_*` 常量里,`runner.py` 与前端 `app.js` 共用同一套名字; 新增事件类型时**只改这里 + 前端一处**。 * 事件是**追加式**的:每个 job 维护一个列表,客户端可带 `since=` 续传 —— 断线重连、或任务跑到一半才打开页面,都能补齐历史(SSE 的 `Last-Event-ID` 走同一套)。 """ from __future__ import annotations import threading import time from dataclasses import dataclass, field from typing import Any, Dict # ---- 事件类型 ------------------------------------------------------------- # EVT_STARTED = "job.started" # data: {target, kind, mode, params} EVT_CHALLENGE = "challenge" # data: {kind, instruction, bg_url, prompt_url, subcapclass} EVT_SOLVE = "solve" # data: {points, off_px, annotated_url, note} EVT_TELEMETRY = "telemetry" # data: {source, collect_len, segments, traj_points} EVT_VERIFY = "verify" # data: {errorCode, ticket, randstr, elapsed_ms, success} EVT_LOG = "log" # data: {level, logger, message} EVT_FINISHED = "job.finished" # data: {success, errorCode, ticket, elapsed_ms, error, stats} EVT_ERROR = "job.error" # data: {message} #: 一次任务最终会落到哪些终态(前端据此停止重连) TERMINAL_TYPES = frozenset({EVT_FINISHED, EVT_ERROR}) @dataclass class Event: """一条事件。SSE 的 `data:` 就是 `to_dict()` 的 JSON。""" seq: int type: str data: Dict[str, Any] = field(default_factory=dict) ts: float = field(default_factory=time.time) def to_dict(self) -> Dict[str, Any]: return {"seq": self.seq, "ts": round(self.ts, 3), "type": self.type, "data": self.data} class EventLog: """一个 job 的事件缓冲区:追加 + 按 seq 续传。 注意它是**只追加**的:`since` 语义是"给我 seq > since 的全部", 这样 SSE 断开重连不会丢事件,也不会重复推送。 带锁:写在工作线程(求解),读在 HTTP 线程(SSE), 且 `seq = len + 1` 与 `append` 必须是原子的,否则会撞出重复 seq。 """ def __init__(self) -> None: self._items: list[Event] = [] self._lock = threading.Lock() def append(self, type_: str, **data: Any) -> Event: with self._lock: evt = Event(seq=len(self._items) + 1, type=type_, data=dict(data)) self._items.append(evt) return evt def since(self, seq: int) -> list[Event]: with self._lock: return [e for e in self._items if e.seq > seq] def __len__(self) -> int: # pragma: no cover - 方便调试 with self._lock: return len(self._items) @property def last_seq(self) -> int: with self._lock: return len(self._items)