"""流程事件:stdout NDJSON / 文件 / HTTP webhook。""" from __future__ import annotations import json import time import urllib.error import urllib.request from pathlib import Path from typing import Any, Dict, List, Optional, Union def _now_iso() -> str: return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) def _http_json_post(url: str, body: Dict[str, Any], timeout: float = 30.0) -> None: data = json.dumps(body, ensure_ascii=False).encode("utf-8") request = urllib.request.Request( url, data=data, headers={"Accept": "application/json", "Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(request, timeout=timeout) as resp: resp.read() class CallbackHub: """向 stdout / 文件 / HTTP webhook 广播流程事件。""" def __init__( self, *, callback_url: Optional[str] = None, callback_file: Optional[Union[str, Path]] = None, quiet: bool = False, ) -> None: self.callback_url = callback_url self.callback_file = Path(callback_file) if callback_file else None self.quiet = quiet self.events: List[Dict[str, Any]] = [] def emit(self, event: str, **payload: Any) -> Dict[str, Any]: record = { "event": event, "ts": _now_iso(), **payload, } self.events.append(record) if not self.quiet: print(json.dumps(record, ensure_ascii=False), flush=True) if self.callback_file: self.callback_file.parent.mkdir(parents=True, exist_ok=True) self.callback_file.write_text( json.dumps( {"latest": record, "history": self.events}, ensure_ascii=False, indent=2, ), encoding="utf-8", ) if self.callback_url: try: _http_json_post(self.callback_url, record, timeout=30.0) except Exception as exc: # noqa: BLE001 if not self.quiet: warn = { "event": "callback_error", "ts": _now_iso(), "error": str(exc), "callback_url": self.callback_url, } print(json.dumps(warn, ensure_ascii=False), flush=True) return record