Files
2026-08-21 16:00:13 +08:00

122 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""一炁事件模型与事件存储抽象。
字段对齐 docs/architecture/data-and-security-architecture.md §4。当前阶段不引入数据库(Alembic 迁移为独立工程),
EventStore 用 Protocol 抽象、提供内存实现;内存实现额外支持按 run 订阅,供 SSE 实时推送。
未来接入 SQLAlchemy + Alembic 时替换实现即可,调用方无需改动。
"""
from __future__ import annotations
import asyncio
import fnmatch
import uuid
from datetime import UTC, datetime
from typing import Any, Literal, Protocol
from pydantic import BaseModel, ConfigDict, Field
Visibility = Literal["public", "internal", "audit"]
Sensitivity = Literal["none", "low", "medium", "high"]
def new_event_id() -> str:
"""生成带 ``event_`` 前缀的事件 ID(契约字典 ID 前缀表)。"""
return f"event_{uuid.uuid4().hex}"
def now_iso() -> str:
return datetime.now(UTC).isoformat()
class Event(BaseModel):
"""一炁事件。"""
model_config = ConfigDict(extra="forbid")
id: str = Field(default_factory=new_event_id)
type: str
source: str
payload: dict[str, Any] = Field(default_factory=dict)
visibility: Visibility = "internal"
sensitivity: Sensitivity = "low"
run_id: str | None = None
character_id: str | None = None
trace_id: str | None = None
parent_id: str | None = None
sequence: int | None = None
evidence_ids: list[str] = Field(default_factory=list)
created_at: str = Field(default_factory=now_iso)
class EventStore(Protocol):
"""事件存储接口:append 写入、list_by_run 检索、subscribe/unsubscribe 订阅实时事件。"""
async def append(self, event: Event, **kwargs: Any) -> Event: ...
async def list_by_run(self, run_id: str) -> list[Event]: ...
def subscribe(self, run_id: str) -> asyncio.Queue[Event]: ...
def unsubscribe(self, run_id: str, queue: asyncio.Queue[Event]) -> None: ...
def subscribe_by_pattern(self, pattern: str) -> asyncio.Queue[Event]:
"""按 event.type 的 glob pattern 订阅(如 'time.*' / '*' / 'time.reunion')。
与 subscribe(run_id) 对称:append 时用 fnmatch 匹配命中 pattern 的订阅者并 put_nowait。
"""
...
def unsubscribe_by_pattern(self, pattern: str, queue: asyncio.Queue[Event]) -> None:
"""取消 pattern 订阅(与 subscribe_by_pattern 对称)。"""
...
class InMemoryEventStore:
"""内存事件存储,供本地与测试使用。支持按 run 订阅,append 时通知订阅者。"""
def __init__(self) -> None:
self._events: list[Event] = []
self._subscribers: dict[str, list[asyncio.Queue[Event]]] = {}
self._pattern_subscribers: dict[str, list[asyncio.Queue[Event]]] = {}
async def append(self, event: Event, **kwargs: Any) -> Event: # noqa: ARG002
# 为同一 run 的事件分配单调递增的 sequence(SSE 排序与断线对齐用,API §5.7)
if event.run_id and event.sequence is None:
count = sum(1 for e in self._events if e.run_id == event.run_id)
event = event.model_copy(update={"sequence": count + 1})
self._events.append(event)
for queue in self._subscribers.get(event.run_id or "", []):
queue.put_nowait(event)
# pattern 订阅:fnmatch 匹配 event.type,命中则 put_nowait(与 run_id 订阅对称的内存 pub/sub)
for pattern, queues in self._pattern_subscribers.items():
if fnmatch.fnmatch(event.type, pattern):
for queue in queues:
queue.put_nowait(event)
return event
async def list_by_run(self, run_id: str) -> list[Event]:
return [e for e in self._events if e.run_id == run_id]
def subscribe(self, run_id: str) -> asyncio.Queue[Event]:
queue: asyncio.Queue[Event] = asyncio.Queue()
self._subscribers.setdefault(run_id, []).append(queue)
return queue
def unsubscribe(self, run_id: str, queue: asyncio.Queue[Event]) -> None:
subscribers = self._subscribers.get(run_id)
if subscribers is not None:
self._subscribers[run_id] = [q for q in subscribers if q is not queue]
def subscribe_by_pattern(self, pattern: str) -> asyncio.Queue[Event]:
queue: asyncio.Queue[Event] = asyncio.Queue()
self._pattern_subscribers.setdefault(pattern, []).append(queue)
return queue
def unsubscribe_by_pattern(self, pattern: str, queue: asyncio.Queue[Event]) -> None:
subscribers = self._pattern_subscribers.get(pattern)
if subscribers is not None:
self._pattern_subscribers[pattern] = [q for q in subscribers if q is not queue]
def all(self) -> list[Event]:
return list(self._events)