mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
refactor: register event delivery contracts
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""事件 payload、可见性、投递与可靠性契约清单。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
import app.schemas.event as event_schemas
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
|
||||
|
||||
class EventDelivery(StrEnum):
|
||||
"""描述事件当前的交付保证。"""
|
||||
|
||||
EPHEMERAL = "ephemeral"
|
||||
DURABLE_REQUIRED = "durable_required"
|
||||
|
||||
|
||||
class EventVisibility(StrEnum):
|
||||
"""描述事件是否属于插件公开扩展面。"""
|
||||
|
||||
HOST_ONLY = "host_only"
|
||||
PLUGIN_PUBLIC = "plugin_public"
|
||||
TARGET_PLUGIN = "target_plugin"
|
||||
|
||||
|
||||
class EventErrorBehavior(StrEnum):
|
||||
"""描述 handler 异常后的传播规则。"""
|
||||
|
||||
ISOLATE = "isolate"
|
||||
STOP_CHAIN = "stop_chain"
|
||||
NOTIFY = "notify"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EventContract:
|
||||
"""冻结一个事件的 payload 与运行语义。"""
|
||||
|
||||
event_name: str
|
||||
payload_model: type[BaseModel] | None
|
||||
payload_contract: str
|
||||
mode: str
|
||||
visibility: EventVisibility
|
||||
delivery: EventDelivery
|
||||
error_behavior: EventErrorBehavior
|
||||
ordering: str
|
||||
sensitive_fields: tuple[str, ...] = ()
|
||||
legacy_reason: str | None = None
|
||||
|
||||
|
||||
_PAYLOAD_MODELS: dict[EventType | ChainEventType, type[BaseModel]] = {
|
||||
EventType.ConfigChanged: event_schemas.ConfigChangeEventData,
|
||||
EventType.AgentTokensUsage: event_schemas.AgentTokensUsageEventData,
|
||||
EventType.SubscribeModified: event_schemas.SubscribeModifiedEventData,
|
||||
ChainEventType.PluginDataReset: event_schemas.PluginDataResetEventData,
|
||||
ChainEventType.AuthVerification: event_schemas.AuthCredentials,
|
||||
ChainEventType.AuthIntercept: event_schemas.AuthInterceptCredentials,
|
||||
ChainEventType.CommandRegister: event_schemas.CommandRegisterEventData,
|
||||
ChainEventType.TransferRename: event_schemas.TransferRenameEventData,
|
||||
ChainEventType.TransferRenameBuild: event_schemas.TransferRenameBuildEventData,
|
||||
ChainEventType.TransferIntercept: event_schemas.TransferInterceptEventData,
|
||||
ChainEventType.TransferOverwriteCheck: event_schemas.TransferOverwriteCheckEventData,
|
||||
ChainEventType.ResourceSelection: event_schemas.ResourceSelectionEventData,
|
||||
ChainEventType.ResourceDownload: event_schemas.ResourceDownloadEventData,
|
||||
ChainEventType.DiscoverSource: event_schemas.DiscoverSourceEventData,
|
||||
ChainEventType.MediaRecognizeConvert: event_schemas.MediaRecognizeConvertEventData,
|
||||
ChainEventType.RecommendSource: event_schemas.RecommendSourceEventData,
|
||||
ChainEventType.StorageOperSelection: event_schemas.StorageOperSelectionEventData,
|
||||
ChainEventType.AgentLLMProvider: event_schemas.AgentLLMProviderEventData,
|
||||
ChainEventType.SubscribeEpisodesRefresh: event_schemas.SubscribeEpisodesRefreshEventData,
|
||||
ChainEventType.SubscribeCompletionCheck: event_schemas.SubscribeCompletionCheckEventData,
|
||||
}
|
||||
|
||||
_DURABLE_REQUIRED = {
|
||||
EventType.SubscribeAdded,
|
||||
EventType.SubscribeModified,
|
||||
EventType.SubscribeDeleted,
|
||||
EventType.DownloadAdded,
|
||||
EventType.TransferComplete,
|
||||
EventType.TransferFailed,
|
||||
}
|
||||
_TARGET_PLUGIN = {EventType.PluginAction, EventType.PluginTriggered}
|
||||
_HOST_ONLY = {EventType.SystemError, EventType.ConfigChanged, EventType.ModuleReload}
|
||||
_SENSITIVE_FIELDS = {
|
||||
ChainEventType.AuthVerification: ("password", "token", "mfa_code"),
|
||||
ChainEventType.AuthIntercept: ("token",),
|
||||
ChainEventType.AgentLLMProvider: ("api_key",),
|
||||
}
|
||||
|
||||
|
||||
def _build_contract(event_type: EventType | ChainEventType) -> EventContract:
|
||||
"""按 enum 类别和首批 model 映射构造完整契约。"""
|
||||
payload_model = _PAYLOAD_MODELS.get(event_type)
|
||||
is_chain = isinstance(event_type, ChainEventType)
|
||||
visibility = EventVisibility.PLUGIN_PUBLIC
|
||||
if event_type in _TARGET_PLUGIN:
|
||||
visibility = EventVisibility.TARGET_PLUGIN
|
||||
elif event_type in _HOST_ONLY:
|
||||
visibility = EventVisibility.HOST_ONLY
|
||||
return EventContract(
|
||||
event_name=f"{event_type.__class__.__name__}.{event_type.name}",
|
||||
payload_model=payload_model,
|
||||
payload_contract=payload_model.__name__ if payload_model else "legacy_dict",
|
||||
mode="chain" if is_chain else "broadcast",
|
||||
visibility=visibility,
|
||||
delivery=(
|
||||
EventDelivery.DURABLE_REQUIRED
|
||||
if event_type in _DURABLE_REQUIRED
|
||||
else EventDelivery.EPHEMERAL
|
||||
),
|
||||
error_behavior=(
|
||||
EventErrorBehavior.STOP_CHAIN if is_chain else EventErrorBehavior.NOTIFY
|
||||
),
|
||||
ordering="priority_serial" if is_chain else "priority_queue",
|
||||
sensitive_fields=_SENSITIVE_FIELDS.get(event_type, ()),
|
||||
legacy_reason=(
|
||||
None
|
||||
if payload_model
|
||||
else "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
EVENT_CONTRACTS = {
|
||||
event_type: _build_contract(event_type)
|
||||
for event_type in (*tuple(EventType), *tuple(ChainEventType))
|
||||
}
|
||||
|
||||
|
||||
def get_event_contract(event_type: EventType | ChainEventType) -> EventContract:
|
||||
"""返回 enum 事件的完整登记契约。"""
|
||||
return EVENT_CONTRACTS[event_type]
|
||||
|
||||
|
||||
def validate_event_payload(
|
||||
event_type: EventType | ChainEventType,
|
||||
payload: Any,
|
||||
) -> tuple[str, ...]:
|
||||
"""在发送边界诊断首批 typed payload,保持原对象和插件 dict 形状不变。"""
|
||||
model = get_event_contract(event_type).payload_model
|
||||
if model is None or payload is None:
|
||||
return ()
|
||||
if isinstance(payload, model):
|
||||
return ()
|
||||
try:
|
||||
model.model_validate(payload)
|
||||
except (ValidationError, TypeError, ValueError) as error:
|
||||
return (str(error),)
|
||||
return ()
|
||||
@@ -20,6 +20,7 @@ from app.runtime.event.binding import (
|
||||
from app.runtime.event.dispatch import EventDispatcher
|
||||
from app.runtime.event.errors import EventErrorNotifier, EventErrorPolicy
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
from app.runtime.event.contracts import validate_event_payload
|
||||
|
||||
DEFAULT_EVENT_PRIORITY = 10 # 事件的默认优先级
|
||||
MIN_EVENT_CONSUMER_THREADS = 1 # 最小事件消费者线程数
|
||||
@@ -40,6 +41,13 @@ class Event:
|
||||
:param event_data: 可选,事件携带的数据,默认为空字典
|
||||
:param priority: 可选,事件的优先级,默认为 10
|
||||
"""
|
||||
payload_problems = validate_event_payload(event_type, event_data)
|
||||
if payload_problems:
|
||||
logger.warning(
|
||||
"事件 %s payload 与登记契约不一致:%s;当前保留旧 payload 继续投递",
|
||||
event_type.value,
|
||||
"; ".join(payload_problems),
|
||||
)
|
||||
self.event_id = str(uuid.uuid4()) # 事件ID
|
||||
self.event_type = event_type # 事件类型
|
||||
self.event_data = event_data or {} # 事件数据
|
||||
|
||||
@@ -590,6 +590,17 @@ ModuleMethodSpec(
|
||||
5. 报告“宿主无 consumer”时区分插件公开事件、预留事件和真正死事件。
|
||||
6. `SystemError` 递归保护继续保留并补 contract;错误通知不能再次构造无限错误链。
|
||||
|
||||
**实施记录(2026-08-21)**:
|
||||
|
||||
- 新增 `app/runtime/event/contracts.py`,53 个 `EventType` / `ChainEventType` 全量登记 payload、
|
||||
broadcast/chain、可见性、顺序、错误策略、敏感字段和 ephemeral/durable-required 语义;尚未模型化的
|
||||
事件显式记录 legacy dict 原因,不把“未登记”当成兼容策略。
|
||||
- 首批 20 个已有 Pydantic payload 的配置、订阅、整理、资源、认证、插件和 Agent 事件绑定具体 model。
|
||||
`Event` 创建边界对 dict/model 做诊断校验,但继续投递原对象,因此插件 dict 形状和链式原地修改语义不变。
|
||||
- 订阅变更、下载添加、整理成功/失败等用户副作用标记为 `durable_required`,只表达完成语义要求;
|
||||
在 ARCH-251 pilot 完成前不虚构当前已具备持久投递。SystemError 仍沿用既有递归保护和异常通知路径。
|
||||
- runtime contract baseline 新增稳定 `event_specs`,后续 enum 新增必须同步登记,且不比较源码行号。
|
||||
|
||||
#### ARCH-242:Module/Integration 质量清单
|
||||
|
||||
**目标**:借鉴 Home Assistant Integration Quality Scale,为 `app/modules` 建立可检查但渐进的质量视图。
|
||||
|
||||
@@ -803,6 +803,7 @@ def collect_runtime_baseline() -> dict[str, Any]:
|
||||
"run_module": collect_run_module_contracts(),
|
||||
"module_method_specs": collect_module_method_specs(),
|
||||
"events": collect_event_contracts(),
|
||||
"event_specs": collect_event_specs(),
|
||||
"sdk_exports": collect_sdk_exports(),
|
||||
"compat_manifest": collect_compat_manifest(),
|
||||
}
|
||||
@@ -824,6 +825,33 @@ def collect_module_method_specs() -> dict[str, Any]:
|
||||
sys.modules.pop(spec.name, None)
|
||||
|
||||
|
||||
def collect_event_specs() -> dict[str, Any]:
|
||||
"""收集全部 enum 事件的稳定 payload、可见性与可靠性登记。"""
|
||||
project_root = str(PROJECT_ROOT)
|
||||
inserted = project_root not in sys.path
|
||||
if inserted:
|
||||
sys.path.insert(0, project_root)
|
||||
try:
|
||||
from app.runtime.event.contracts import EVENT_CONTRACTS
|
||||
finally:
|
||||
if inserted:
|
||||
sys.path.remove(project_root)
|
||||
|
||||
return {
|
||||
contract.event_name: {
|
||||
"payload_contract": contract.payload_contract,
|
||||
"mode": contract.mode,
|
||||
"visibility": contract.visibility.value,
|
||||
"delivery": contract.delivery.value,
|
||||
"error_behavior": contract.error_behavior.value,
|
||||
"ordering": contract.ordering,
|
||||
"sensitive_fields": list(contract.sensitive_fields),
|
||||
"legacy_reason": contract.legacy_reason,
|
||||
}
|
||||
for contract in EVENT_CONTRACTS.values()
|
||||
}
|
||||
|
||||
|
||||
def collect_runtime_diagnostics() -> dict[str, Any]:
|
||||
"""生成带当前源码行号的运行契约诊断视图,不写入语义 fixture。"""
|
||||
return {
|
||||
|
||||
+8
-3
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6204,
|
||||
"edge_sha256": "9a988284502bb26fae5266321765fbf0e10b9fe608fd448c6868e67a824ea862",
|
||||
"edge_count": 6208,
|
||||
"edge_sha256": "a91776a358fc4820d1d6f6dc300b3432491dc6b87a0423a99acd0e6c1366526b",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -5345,6 +5345,9 @@
|
||||
"app.runtime.event.binding -> app.runtime.event",
|
||||
"app.runtime.event.binding -> app.runtime.event.registry",
|
||||
"app.runtime.event.binding -> app.runtime.log",
|
||||
"app.runtime.event.contracts -> app.schemas",
|
||||
"app.runtime.event.contracts -> app.schemas.event",
|
||||
"app.runtime.event.contracts -> app.schemas.types",
|
||||
"app.runtime.event.dispatch -> app.runtime",
|
||||
"app.runtime.event.dispatch -> app.runtime.event",
|
||||
"app.runtime.event.dispatch -> app.runtime.event.binding",
|
||||
@@ -5367,6 +5370,7 @@
|
||||
"app.runtime.events -> app.runtime.config",
|
||||
"app.runtime.events -> app.runtime.event",
|
||||
"app.runtime.events -> app.runtime.event.binding",
|
||||
"app.runtime.events -> app.runtime.event.contracts",
|
||||
"app.runtime.events -> app.runtime.event.dispatch",
|
||||
"app.runtime.events -> app.runtime.event.errors",
|
||||
"app.runtime.events -> app.runtime.event.registry",
|
||||
@@ -6221,7 +6225,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 771,
|
||||
"module_count": 772,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -6852,6 +6856,7 @@
|
||||
"app.runtime.debounce",
|
||||
"app.runtime.event",
|
||||
"app.runtime.event.binding",
|
||||
"app.runtime.event.contracts",
|
||||
"app.runtime.event.dispatch",
|
||||
"app.runtime.event.errors",
|
||||
"app.runtime.event.registry",
|
||||
|
||||
@@ -1159,6 +1159,546 @@
|
||||
"app.utils"
|
||||
]
|
||||
},
|
||||
"event_specs": {
|
||||
"ChainEventType.AgentLLMProvider": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "AgentLLMProviderEventData",
|
||||
"sensitive_fields": [
|
||||
"api_key"
|
||||
],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.AuthIntercept": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "AuthInterceptCredentials",
|
||||
"sensitive_fields": [
|
||||
"token"
|
||||
],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.AuthVerification": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "AuthCredentials",
|
||||
"sensitive_fields": [
|
||||
"password",
|
||||
"token",
|
||||
"mfa_code"
|
||||
],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.CommandRegister": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "CommandRegisterEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.DiscoverSource": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "DiscoverSourceEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.MediaRecognize": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.MediaRecognizeConvert": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "MediaRecognizeConvertEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.MusicMediaRecognize": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.MusicNameRecognize": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.NameRecognize": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.PluginDataReset": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "PluginDataResetEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.RecommendSource": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "RecommendSourceEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.ResourceDownload": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "ResourceDownloadEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.ResourceSelection": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "ResourceSelectionEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.StorageOperSelection": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "StorageOperSelectionEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.SubscribeCompletionCheck": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "SubscribeCompletionCheckEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.SubscribeEpisodesRefresh": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "SubscribeEpisodesRefreshEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.TransferIntercept": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "TransferInterceptEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.TransferOverwriteCheck": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "TransferOverwriteCheckEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.TransferRename": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "TransferRenameEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.TransferRenameBuild": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": null,
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "TransferRenameBuildEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"ChainEventType.WorkflowExecution": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "stop_chain",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "chain",
|
||||
"ordering": "priority_serial",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.AgentTokensUsage": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": null,
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "AgentTokensUsageEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.AudioTransferComplete": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.AudioTransferFailed": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.CommandExcute": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.ConfigChanged": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": null,
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "ConfigChangeEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "host_only"
|
||||
},
|
||||
"EventType.DownloadAdded": {
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.DownloadDeleted": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.DownloadFileDeleted": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.HistoryDeleted": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.MessageAction": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.MetadataScrape": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.ModuleReload": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "host_only"
|
||||
},
|
||||
"EventType.NoticeMessage": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.PluginAction": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "target_plugin"
|
||||
},
|
||||
"EventType.PluginReload": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.PluginTriggered": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "target_plugin"
|
||||
},
|
||||
"EventType.SiteDeleted": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SiteRefreshed": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SiteUpdated": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubscribeAdded": {
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubscribeComplete": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubscribeDeleted": {
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubscribeModified": {
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": null,
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "SubscribeModifiedEventData",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubtitleTransferComplete": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SubtitleTransferFailed": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.SystemError": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "host_only"
|
||||
},
|
||||
"EventType.TransferComplete": {
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.TransferFailed": {
|
||||
"delivery": "durable_required",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.UserMessage": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.WebhookMessage": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
},
|
||||
"EventType.WorkflowExecute": {
|
||||
"delivery": "ephemeral",
|
||||
"error_behavior": "notify",
|
||||
"legacy_reason": "现有插件 payload 尚未收敛为稳定 model,保留原始 dict ABI",
|
||||
"mode": "broadcast",
|
||||
"ordering": "priority_queue",
|
||||
"payload_contract": "legacy_dict",
|
||||
"sensitive_fields": [],
|
||||
"visibility": "plugin_public"
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"consumer_count": 15,
|
||||
"dynamic_consumers": [
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Event Contract Registry 完整性和兼容校验测试。"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.runtime.event.contracts import (
|
||||
EVENT_CONTRACTS,
|
||||
EventDelivery,
|
||||
get_event_contract,
|
||||
validate_event_payload,
|
||||
)
|
||||
from app.runtime.events import Event
|
||||
from app.schemas.event import ConfigChangeEventData
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
|
||||
|
||||
def test_every_event_enum_has_complete_contract() -> None:
|
||||
"""53 个广播/链式事件必须全部登记且 legacy 项必须解释原因。"""
|
||||
expected = {*EventType, *ChainEventType}
|
||||
|
||||
assert set(EVENT_CONTRACTS) == expected
|
||||
assert len(EVENT_CONTRACTS) == 53
|
||||
for contract in EVENT_CONTRACTS.values():
|
||||
assert contract.mode in {"broadcast", "chain"}
|
||||
assert contract.payload_contract
|
||||
if contract.payload_contract == "legacy_dict":
|
||||
assert contract.legacy_reason
|
||||
|
||||
|
||||
def test_typed_payload_is_validated_without_changing_public_shape() -> None:
|
||||
"""旧 dict 通过 model 校验后仍以同一个 dict 对象投递给插件。"""
|
||||
payload = {"key": "PROXY_HOST", "value": "http://proxy"}
|
||||
|
||||
assert validate_event_payload(EventType.ConfigChanged, payload) == ()
|
||||
event = Event(EventType.ConfigChanged, payload)
|
||||
|
||||
assert event.event_data is payload
|
||||
assert isinstance(event.event_data, dict)
|
||||
|
||||
|
||||
def test_invalid_typed_payload_is_diagnostic_only() -> None:
|
||||
"""兼容阶段坏 payload 产生诊断但不阻断既有投递。"""
|
||||
payload = {"value": "missing-key"}
|
||||
|
||||
with patch("app.runtime.events.logger.warning") as warning:
|
||||
event = Event(EventType.ConfigChanged, payload)
|
||||
|
||||
assert event.event_data is payload
|
||||
warning.assert_called_once()
|
||||
|
||||
|
||||
def test_selected_user_side_effects_are_marked_durable_required() -> None:
|
||||
"""订阅、下载和整理完成事件必须明确暴露后续 durable pilot 要求。"""
|
||||
for event_type in (
|
||||
EventType.SubscribeAdded,
|
||||
EventType.SubscribeModified,
|
||||
EventType.SubscribeDeleted,
|
||||
EventType.DownloadAdded,
|
||||
EventType.TransferComplete,
|
||||
EventType.TransferFailed,
|
||||
):
|
||||
assert get_event_contract(event_type).delivery is EventDelivery.DURABLE_REQUIRED
|
||||
|
||||
|
||||
def test_model_instance_remains_mutable_chain_payload() -> None:
|
||||
"""链式处理器继续接收原 model 实例,确保输出字段可原地接力。"""
|
||||
payload = ConfigChangeEventData(key={"PROXY_HOST"})
|
||||
event = Event(EventType.ConfigChanged, payload)
|
||||
|
||||
assert event.event_data is payload
|
||||
Reference in New Issue
Block a user