mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-02 22:17:08 +08:00
refactor: complete host module and event contracts
This commit is contained in:
@@ -9,7 +9,9 @@ from typing import Any
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
import app.schemas.event as event_schemas
|
||||
from app.schemas.mediaserver import WebhookEventInfo
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
from app.schemas.workflow import ActionContext
|
||||
|
||||
|
||||
class EventDelivery(StrEnum):
|
||||
@@ -53,6 +55,13 @@ class EventContract:
|
||||
|
||||
_PAYLOAD_MODELS: dict[EventType | ChainEventType, type[BaseModel]] = {
|
||||
EventType.ConfigChanged: event_schemas.ConfigChangeEventData,
|
||||
EventType.PluginReload: event_schemas.PluginReloadEventData,
|
||||
EventType.PluginAction: event_schemas.PluginActionEventData,
|
||||
EventType.PluginTriggered: event_schemas.PluginTriggeredEventData,
|
||||
EventType.CommandExcute: event_schemas.CommandExecuteEventData,
|
||||
EventType.SiteDeleted: event_schemas.SiteEventData,
|
||||
EventType.SiteUpdated: event_schemas.SiteEventData,
|
||||
EventType.SiteRefreshed: event_schemas.SiteEventData,
|
||||
EventType.AgentTokensUsage: event_schemas.AgentTokensUsageEventData,
|
||||
EventType.SubscribeAdded: event_schemas.SubscribeAddedEventData,
|
||||
EventType.SubscribeDeleted: event_schemas.SubscribeDeletedEventData,
|
||||
@@ -60,6 +69,22 @@ _PAYLOAD_MODELS: dict[EventType | ChainEventType, type[BaseModel]] = {
|
||||
EventType.DownloadAdded: event_schemas.DownloadAddedEventData,
|
||||
EventType.TransferComplete: event_schemas.TransferResultEventData,
|
||||
EventType.TransferFailed: event_schemas.TransferResultEventData,
|
||||
EventType.SubtitleTransferComplete: event_schemas.TransferResultEventData,
|
||||
EventType.SubtitleTransferFailed: event_schemas.TransferResultEventData,
|
||||
EventType.AudioTransferComplete: event_schemas.TransferResultEventData,
|
||||
EventType.AudioTransferFailed: event_schemas.TransferResultEventData,
|
||||
EventType.HistoryDeleted: event_schemas.HistoryDeletedEventData,
|
||||
EventType.DownloadFileDeleted: event_schemas.DownloadFileDeletedEventData,
|
||||
EventType.DownloadDeleted: event_schemas.DownloadDeletedEventData,
|
||||
EventType.UserMessage: event_schemas.UserMessageEventData,
|
||||
EventType.WebhookMessage: WebhookEventInfo,
|
||||
EventType.NoticeMessage: event_schemas.NoticeMessageEventData,
|
||||
EventType.SubscribeComplete: event_schemas.SubscribeCompleteEventData,
|
||||
EventType.SystemError: event_schemas.SystemErrorEventData,
|
||||
EventType.MetadataScrape: event_schemas.MetadataScrapeEventData,
|
||||
EventType.ModuleReload: event_schemas.EmptyEventData,
|
||||
EventType.MessageAction: event_schemas.MessageActionEventData,
|
||||
EventType.WorkflowExecute: event_schemas.WorkflowExecuteEventData,
|
||||
ChainEventType.PluginDataReset: event_schemas.PluginDataResetEventData,
|
||||
ChainEventType.AuthVerification: event_schemas.AuthCredentials,
|
||||
ChainEventType.AuthIntercept: event_schemas.AuthInterceptCredentials,
|
||||
@@ -77,6 +102,11 @@ _PAYLOAD_MODELS: dict[EventType | ChainEventType, type[BaseModel]] = {
|
||||
ChainEventType.AgentLLMProvider: event_schemas.AgentLLMProviderEventData,
|
||||
ChainEventType.SubscribeEpisodesRefresh: event_schemas.SubscribeEpisodesRefreshEventData,
|
||||
ChainEventType.SubscribeCompletionCheck: event_schemas.SubscribeCompletionCheckEventData,
|
||||
ChainEventType.NameRecognize: event_schemas.NameRecognizeEventData,
|
||||
ChainEventType.MusicNameRecognize: event_schemas.MusicNameRecognizeEventData,
|
||||
ChainEventType.MediaRecognize: event_schemas.MediaRecognizeEventData,
|
||||
ChainEventType.MusicMediaRecognize: event_schemas.MusicMediaRecognizeEventData,
|
||||
ChainEventType.WorkflowExecution: ActionContext,
|
||||
}
|
||||
|
||||
_DURABLE_REQUIRED = {
|
||||
|
||||
@@ -83,7 +83,7 @@ _METHOD_CONTRACTS = {
|
||||
"get_folder": ModuleMethodContract(family="storage", input_contract="StorageFolderRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path")),
|
||||
"get_parent_item": ModuleMethodContract(family="storage", input_contract="StorageParentRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem",)),
|
||||
"rename_file": ModuleMethodContract(family="storage", input_contract="StorageRenameRequest", result_contract="bool | FileItem", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "name")),
|
||||
"storage_manage": ModuleMethodContract(family="storage", input_contract="StorageManageRequest", result_contract="Any", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "action")),
|
||||
"storage_manage": ModuleMethodContract(family="storage", input_contract="StorageManageRequest", result_contract="StorageProviderResult", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "action")),
|
||||
"snapshot_storage": ModuleMethodContract(family="storage", input_contract="StorageSnapshotRequest", result_contract="dict[str, dict] | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path", "last_snapshot_time", "max_depth", "previous_snapshot")),
|
||||
"send_message": ModuleMethodContract(family="messaging", input_contract="MessageSendRequest", result_contract="Message | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY),
|
||||
"finalize_message": ModuleMethodContract(family="messaging", input_contract="MessageFinalizeRequest", result_contract="Message | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("response",)),
|
||||
@@ -107,6 +107,318 @@ _PREFIX_CONTRACTS = (
|
||||
)
|
||||
|
||||
|
||||
# 宿主静态扫描到的全部字符串能力名。第三方插件仍可声明未在这里出现的自定义方法,
|
||||
# 自定义方法继续走开放的 legacy contract;宿主新增调用则必须先进入本清单。
|
||||
_OBSERVED_HOST_METHODS = (
|
||||
'anilist_credits',
|
||||
'anilist_discover',
|
||||
'anilist_info',
|
||||
'anilist_person_credits',
|
||||
'anilist_person_detail',
|
||||
'anilist_popular_this_season',
|
||||
'anilist_recommendations',
|
||||
'anilist_trending',
|
||||
'any_files',
|
||||
'async_anilist_credits',
|
||||
'async_anilist_discover',
|
||||
'async_anilist_info',
|
||||
'async_anilist_person_credits',
|
||||
'async_anilist_person_detail',
|
||||
'async_anilist_popular_this_season',
|
||||
'async_anilist_recommendations',
|
||||
'async_anilist_trending',
|
||||
'async_bangumi_calendar',
|
||||
'async_bangumi_credits',
|
||||
'async_bangumi_discover',
|
||||
'async_bangumi_info',
|
||||
'async_bangumi_person_credits',
|
||||
'async_bangumi_person_detail',
|
||||
'async_bangumi_recommend',
|
||||
'async_douban_discover',
|
||||
'async_douban_info',
|
||||
'async_douban_movie_credits',
|
||||
'async_douban_movie_recommend',
|
||||
'async_douban_person_credits',
|
||||
'async_douban_person_detail',
|
||||
'async_douban_tv_credits',
|
||||
'async_douban_tv_recommend',
|
||||
'async_identify_music_by_fingerprint',
|
||||
'async_match_doubaninfo',
|
||||
'async_match_music_album',
|
||||
'async_match_tmdbinfo',
|
||||
'async_movie_hot',
|
||||
'async_movie_showing',
|
||||
'async_movie_top250',
|
||||
'async_obtain_images',
|
||||
'async_recognize_media',
|
||||
'async_refresh_torrents',
|
||||
'async_search_collections',
|
||||
'async_search_medias',
|
||||
'async_search_persons',
|
||||
'async_search_subtitles',
|
||||
'async_search_torrents',
|
||||
'async_tmdb_collection',
|
||||
'async_tmdb_discover',
|
||||
'async_tmdb_episodes',
|
||||
'async_tmdb_group_seasons',
|
||||
'async_tmdb_info',
|
||||
'async_tmdb_movie_credits',
|
||||
'async_tmdb_movie_recommend',
|
||||
'async_tmdb_movie_similar',
|
||||
'async_tmdb_person_credits',
|
||||
'async_tmdb_person_detail',
|
||||
'async_tmdb_seasons',
|
||||
'async_tmdb_trending',
|
||||
'async_tmdb_tv_credits',
|
||||
'async_tmdb_tv_recommend',
|
||||
'async_tmdb_tv_similar',
|
||||
'async_tv_animation',
|
||||
'async_tv_hot',
|
||||
'async_tv_weekly_chinese',
|
||||
'async_tv_weekly_global',
|
||||
'async_update_recognize_cache',
|
||||
'bangumi_calendar',
|
||||
'bangumi_credits',
|
||||
'bangumi_discover',
|
||||
'bangumi_info',
|
||||
'bangumi_person_credits',
|
||||
'bangumi_person_detail',
|
||||
'bangumi_recommend',
|
||||
'channel_manage',
|
||||
'clear_cache',
|
||||
'create_folder',
|
||||
'delete_file',
|
||||
'delete_message',
|
||||
'douban_discover',
|
||||
'douban_info',
|
||||
'douban_movie_credits',
|
||||
'douban_movie_recommend',
|
||||
'douban_person_credits',
|
||||
'douban_person_detail',
|
||||
'douban_tv_credits',
|
||||
'douban_tv_recommend',
|
||||
'download',
|
||||
'download_added',
|
||||
'download_discord_file_bytes',
|
||||
'download_feishu_file_bytes',
|
||||
'download_feishu_image_to_data_url',
|
||||
'download_file',
|
||||
'download_qq_file_bytes',
|
||||
'download_slack_file_bytes',
|
||||
'download_slack_file_to_data_url',
|
||||
'download_synologychat_file_bytes',
|
||||
'download_telegram_file_bytes',
|
||||
'download_telegram_file_to_base64',
|
||||
'download_vocechat_file_bytes',
|
||||
'download_vocechat_image_to_data_url',
|
||||
'download_wechat_image_to_data_url',
|
||||
'download_wechat_media_bytes',
|
||||
'downloader_info',
|
||||
'edit_message',
|
||||
'filter_torrents',
|
||||
'finalize_message',
|
||||
'get_file_item',
|
||||
'get_folder',
|
||||
'get_parent_item',
|
||||
'get_search_page_size',
|
||||
'get_torrent_trackers',
|
||||
'identify_music_by_fingerprint',
|
||||
'list_files',
|
||||
'list_torrents',
|
||||
'load_category_config',
|
||||
'mark_message_processing_finished',
|
||||
'mark_message_processing_started',
|
||||
'match_doubaninfo',
|
||||
'match_music_album',
|
||||
'match_tmdbinfo',
|
||||
'media_category',
|
||||
'media_exists',
|
||||
'media_files',
|
||||
'media_statistic',
|
||||
'mediaserver_image_cookies',
|
||||
'mediaserver_iteminfo',
|
||||
'mediaserver_items',
|
||||
'mediaserver_items_count',
|
||||
'mediaserver_latest',
|
||||
'mediaserver_latest_images',
|
||||
'mediaserver_librarys',
|
||||
'mediaserver_play_url',
|
||||
'mediaserver_playing',
|
||||
'mediaserver_season_episode_ids',
|
||||
'mediaserver_tv_episodes',
|
||||
'message_parser',
|
||||
'metadata_img',
|
||||
'metadata_nfo',
|
||||
'movie_hot',
|
||||
'movie_showing',
|
||||
'movie_top250',
|
||||
'music_album',
|
||||
'music_album_related',
|
||||
'music_artist',
|
||||
'music_artist_albums',
|
||||
'music_artist_related',
|
||||
'music_cache_clear',
|
||||
'music_cache_delete',
|
||||
'music_cache_items',
|
||||
'music_chart',
|
||||
'music_discover',
|
||||
'music_fresh_releases',
|
||||
'music_lyrics',
|
||||
'obtain_images',
|
||||
'obtain_specific_image',
|
||||
'recognize_media',
|
||||
'recommend_name',
|
||||
'refresh_torrents',
|
||||
'refresh_userdata',
|
||||
'register_commands',
|
||||
'remove_torrents',
|
||||
'rename_file',
|
||||
'save_category_config',
|
||||
'scheduler_job',
|
||||
'search_collections',
|
||||
'search_medias',
|
||||
'search_music',
|
||||
'search_persons',
|
||||
'search_subtitles',
|
||||
'search_torrents',
|
||||
'search_tvdb',
|
||||
'send_direct_message',
|
||||
'set_torrents_tag',
|
||||
'site_subtitle_links',
|
||||
'snapshot_storage',
|
||||
'start_torrents',
|
||||
'stop_torrents',
|
||||
'storage_manage',
|
||||
'tmdb_cache_clear',
|
||||
'tmdb_cache_delete',
|
||||
'tmdb_cache_items',
|
||||
'tmdb_collection',
|
||||
'tmdb_discover',
|
||||
'tmdb_episodes',
|
||||
'tmdb_group_seasons',
|
||||
'tmdb_info',
|
||||
'tmdb_movie_credits',
|
||||
'tmdb_movie_recommend',
|
||||
'tmdb_movie_similar',
|
||||
'tmdb_person_credits',
|
||||
'tmdb_person_detail',
|
||||
'tmdb_seasons',
|
||||
'tmdb_trending',
|
||||
'tmdb_tv_credits',
|
||||
'tmdb_tv_recommend',
|
||||
'tmdb_tv_similar',
|
||||
'torrent_files',
|
||||
'transfer',
|
||||
'transfer_completed',
|
||||
'tv_animation',
|
||||
'tv_hot',
|
||||
'tv_weekly_chinese',
|
||||
'tv_weekly_global',
|
||||
'tvdb_info',
|
||||
'tvdb_slug',
|
||||
'update_recognize_cache',
|
||||
'update_torrent',
|
||||
'upload_file',
|
||||
'user_authenticate',
|
||||
'webhook_parser',
|
||||
)
|
||||
|
||||
_FAMILY_IO_CONTRACTS = {
|
||||
"anilist": ("AniListKeywordArguments", "AniListProviderResult"),
|
||||
"authentication": ("AuthenticationKeywordArguments", "AuthenticationResult"),
|
||||
"bangumi": ("BangumiKeywordArguments", "BangumiProviderResult"),
|
||||
"category": ("CategoryKeywordArguments", "CategoryProviderResult"),
|
||||
"douban": ("DoubanKeywordArguments", "DoubanProviderResult"),
|
||||
"downloader": ("DownloaderKeywordArguments", "DownloaderProviderResult"),
|
||||
"integration": ("IntegrationKeywordArguments", "IntegrationProviderResult"),
|
||||
"media-discovery": ("MediaDiscoveryKeywordArguments", "MediaDiscoveryProviderResult"),
|
||||
"media-recognition": ("MediaRecognitionKeywordArguments", "MediaRecognitionProviderResult"),
|
||||
"media-server": ("MediaServerKeywordArguments", "MediaServerProviderResult"),
|
||||
"messaging": ("MessagingKeywordArguments", "MessagingProviderResult"),
|
||||
"metadata": ("MetadataKeywordArguments", "MetadataProviderResult"),
|
||||
"music": ("MusicKeywordArguments", "MusicProviderResult"),
|
||||
"site": ("SiteKeywordArguments", "SiteProviderResult"),
|
||||
"storage": ("StorageKeywordArguments", "StorageProviderResult"),
|
||||
"tmdb": ("TmdbKeywordArguments", "TmdbProviderResult"),
|
||||
"tvdb": ("TvdbKeywordArguments", "TvdbProviderResult"),
|
||||
}
|
||||
|
||||
|
||||
def _infer_observed_family(method: str) -> str:
|
||||
"""按稳定能力前缀把已观察宿主方法归入可审计的输入/结果族。"""
|
||||
for prefix, contract in _PREFIX_CONTRACTS:
|
||||
if method.startswith(prefix):
|
||||
return contract.family
|
||||
if method.startswith(("mediaserver_", "media_exists", "media_statistic")):
|
||||
return "media-server"
|
||||
if method.startswith((
|
||||
"download", "torrent_", "list_torrents", "refresh_torrents",
|
||||
"remove_torrents", "start_torrents", "stop_torrents",
|
||||
"set_torrents_tag", "update_torrent", "get_torrent_trackers",
|
||||
"downloader_info", "filter_torrents", "transfer_completed",
|
||||
)):
|
||||
return "downloader"
|
||||
if method.startswith((
|
||||
"channel_", "delete_message", "edit_message", "finalize_message",
|
||||
"mark_message_", "message_parser", "register_commands",
|
||||
"send_direct_message", "send_message",
|
||||
)):
|
||||
return "messaging"
|
||||
if method.startswith((
|
||||
"any_files", "create_folder", "delete_file", "get_file_item",
|
||||
"get_folder", "get_parent_item", "list_files", "media_files",
|
||||
"rename_file", "snapshot_storage", "storage_manage", "transfer",
|
||||
"upload_file",
|
||||
)):
|
||||
return "storage"
|
||||
if method.startswith((
|
||||
"metadata_", "obtain_specific_image", "recommend_name",
|
||||
)):
|
||||
return "metadata"
|
||||
if method.startswith((
|
||||
"async_identify_music", "async_match_music", "identify_music",
|
||||
"match_music", "search_music",
|
||||
)):
|
||||
return "music"
|
||||
if method.startswith((
|
||||
"async_match_", "async_obtain_images", "async_recognize_media",
|
||||
"async_update_recognize_cache", "match_", "obtain_images",
|
||||
"recognize_media", "update_recognize_cache",
|
||||
)):
|
||||
return "media-recognition"
|
||||
if method.startswith((
|
||||
"async_movie_", "async_search_", "async_tv_", "movie_",
|
||||
"search_collections", "search_medias", "search_persons",
|
||||
"search_subtitles", "search_torrents", "tv_",
|
||||
)):
|
||||
return "media-discovery"
|
||||
if method in {"clear_cache", "load_category_config", "save_category_config"}:
|
||||
return "category"
|
||||
if method in {"get_search_page_size", "refresh_userdata", "site_subtitle_links"}:
|
||||
return "site"
|
||||
if method == "user_authenticate":
|
||||
return "authentication"
|
||||
return "integration"
|
||||
|
||||
|
||||
def _register_observed_host_contracts() -> None:
|
||||
"""为全部宿主字符串调用登记完整 V2 字段,保留未知插件方法的 legacy fallback。"""
|
||||
for method in _OBSERVED_HOST_METHODS:
|
||||
if method in _METHOD_CONTRACTS:
|
||||
continue
|
||||
family = _infer_observed_family(method)
|
||||
input_contract, result_contract = _FAMILY_IO_CONTRACTS[family]
|
||||
_METHOD_CONTRACTS[method] = ModuleMethodContract(
|
||||
family=family,
|
||||
input_contract=input_contract,
|
||||
result_contract=result_contract,
|
||||
)
|
||||
|
||||
|
||||
_register_observed_host_contracts()
|
||||
|
||||
|
||||
|
||||
def get_module_method_contract(method: str) -> ModuleMethodContract:
|
||||
"""返回方法的显式能力族契约,未知方法保持既有 legacy 协议。"""
|
||||
if contract := _METHOD_CONTRACTS.get(method):
|
||||
|
||||
+185
-1
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Optional, Dict, Any, List, Set, Callable
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.schemas.common import JsonData
|
||||
from app.schemas.types import MediaType, NotificationChannel
|
||||
@@ -29,6 +29,190 @@ class BaseEventData(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class ExtensibleEventData(BaseEventData):
|
||||
"""允许第三方插件附加字段的类型化事件载荷基类。"""
|
||||
|
||||
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class EmptyEventData(ExtensibleEventData):
|
||||
"""当前没有固定字段、但仍需进入 typed registry 的事件载荷。"""
|
||||
|
||||
|
||||
class PluginReloadEventData(ExtensibleEventData):
|
||||
"""插件重载广播事件载荷。"""
|
||||
|
||||
plugin_id: str = Field(description="重载的插件 ID")
|
||||
|
||||
|
||||
class PluginActionEventData(ExtensibleEventData):
|
||||
"""插件命令动作载荷;具体动作参数由目标插件扩展。"""
|
||||
|
||||
plugin_id: Optional[str] = Field(default=None, description="目标插件 ID")
|
||||
action: Optional[str] = Field(default=None, description="插件动作名称")
|
||||
channel: Optional[str] = Field(default=None, description="消息渠道")
|
||||
source: Optional[str] = Field(default=None, description="消息来源")
|
||||
user: Optional[Any] = Field(default=None, description="发起用户")
|
||||
|
||||
|
||||
class PluginTriggeredEventData(ExtensibleEventData):
|
||||
"""插件主动发布的跨插件事件载荷。"""
|
||||
|
||||
plugin_id: str = Field(description="发布事件的插件 ID")
|
||||
event_name: str = Field(description="插件定义的稳定事件名")
|
||||
data: Any = Field(default=None, description="插件定义的事件数据")
|
||||
|
||||
|
||||
class CommandExecuteEventData(ExtensibleEventData):
|
||||
"""斜杠命令执行事件载荷。"""
|
||||
|
||||
cmd: str = Field(description="包含参数的完整命令文本")
|
||||
user: Optional[Any] = Field(default=None, description="发起用户")
|
||||
channel: Optional[str] = Field(default=None, description="消息渠道")
|
||||
source: Optional[str] = Field(default=None, description="消息来源")
|
||||
processing_status: Optional[Any] = Field(default=None, description="交互处理状态")
|
||||
|
||||
|
||||
class SiteEventData(ExtensibleEventData):
|
||||
"""站点新增、更新、删除或数据刷新事件载荷。"""
|
||||
|
||||
site_id: Optional[int | str] = Field(default=None, description="站点 ID 或通配符")
|
||||
domain: Optional[str] = Field(default=None, description="站点域名")
|
||||
name: Optional[str] = Field(default=None, description="站点名称")
|
||||
site_url: Optional[str] = Field(default=None, description="站点地址")
|
||||
|
||||
|
||||
class HistoryDeletedEventData(ExtensibleEventData):
|
||||
"""历史记录删除事件的兼容载荷。"""
|
||||
|
||||
history_id: Optional[int] = Field(default=None, description="历史记录 ID")
|
||||
src: Optional[str] = Field(default=None, description="关联源路径")
|
||||
|
||||
|
||||
class DownloadFileDeletedEventData(ExtensibleEventData):
|
||||
"""下载源文件删除事件载荷。"""
|
||||
|
||||
src: Optional[str] = Field(default=None, description="已删除的下载源路径")
|
||||
hash: Optional[str] = Field(default=None, description="下载任务 hash")
|
||||
|
||||
|
||||
class DownloadDeletedEventData(ExtensibleEventData):
|
||||
"""下载任务删除事件载荷。"""
|
||||
|
||||
hash: str = Field(description="下载任务 hash")
|
||||
torrents: List[Dict[str, Any]] = Field(default_factory=list, description="删除前任务快照")
|
||||
|
||||
|
||||
class UserMessageEventData(ExtensibleEventData):
|
||||
"""未被宿主命令或交互消费的用户文本消息载荷。"""
|
||||
|
||||
text: str = Field(description="用户文本")
|
||||
userid: Optional[Any] = Field(default=None, description="用户 ID")
|
||||
channel: Optional[str] = Field(default=None, description="消息渠道")
|
||||
source: Optional[str] = Field(default=None, description="消息来源")
|
||||
chat_id: Optional[Any] = Field(default=None, description="会话 ID")
|
||||
reply_to_message_id: Optional[Any] = Field(default=None, description="回复消息 ID")
|
||||
|
||||
|
||||
class NoticeMessageEventData(ExtensibleEventData):
|
||||
"""宿主向消息模块发送的通知事件载荷。"""
|
||||
|
||||
type: Optional[Any] = Field(default=None, description="兼容消息类型")
|
||||
title: Optional[str] = Field(default=None, description="消息标题")
|
||||
text: Optional[str] = Field(default=None, description="消息正文")
|
||||
userid: Optional[Any] = Field(default=None, description="目标用户 ID")
|
||||
channel: Optional[Any] = Field(default=None, description="目标消息渠道")
|
||||
source: Optional[str] = Field(default=None, description="消息来源")
|
||||
|
||||
|
||||
class SubscribeCompleteEventData(ExtensibleEventData):
|
||||
"""订阅完成广播事件载荷。"""
|
||||
|
||||
subscribe_id: int = Field(description="已完成订阅 ID")
|
||||
subscribe_info: Dict[str, Any] = Field(default_factory=dict, description="订阅快照")
|
||||
mediainfo: Dict[str, Any] = Field(default_factory=dict, description="媒体信息快照")
|
||||
|
||||
|
||||
class SystemErrorEventData(ExtensibleEventData):
|
||||
"""事件、模块、插件或调度器错误的宿主诊断载荷。"""
|
||||
|
||||
type: str = Field(description="错误来源类别")
|
||||
error: str = Field(description="错误摘要")
|
||||
traceback: Optional[str] = Field(default=None, description="错误堆栈")
|
||||
|
||||
|
||||
class MetadataScrapeEventData(ExtensibleEventData):
|
||||
"""媒体文件元数据刮削事件载荷。"""
|
||||
|
||||
fileitem: FileItem = Field(description="待刮削目录或文件项")
|
||||
file_list: List[str] = Field(default_factory=list, description="待刮削文件清单")
|
||||
meta: Any = Field(default=None, description="文件名解析对象")
|
||||
mediainfo: Any = Field(default=None, description="媒体信息对象")
|
||||
overwrite: bool = Field(default=False, description="是否覆盖已有元数据")
|
||||
file_contexts: List[Any] = Field(default_factory=list, description="逐文件上下文")
|
||||
|
||||
|
||||
class MessageActionEventData(ExtensibleEventData):
|
||||
"""定向插件消息交互动作载荷。"""
|
||||
|
||||
plugin_id: Optional[str] = Field(default=None, description="目标插件 ID")
|
||||
text: Optional[str] = Field(default=None, description="兼容动作文本")
|
||||
input_text: Optional[str] = Field(default=None, description="用户输入文本")
|
||||
userid: Optional[Any] = Field(default=None, description="用户 ID")
|
||||
channel: Optional[str] = Field(default=None, description="消息渠道")
|
||||
source: Optional[str] = Field(default=None, description="消息来源")
|
||||
input_session_id: Optional[str] = Field(default=None, description="输入会话 ID")
|
||||
payload: Any = Field(default=None, description="插件自定义交互数据")
|
||||
|
||||
|
||||
class WorkflowExecuteEventData(ExtensibleEventData):
|
||||
"""请求执行指定工作流的事件载荷。"""
|
||||
|
||||
workflow_id: int = Field(description="工作流 ID")
|
||||
|
||||
|
||||
class NameRecognizeEventData(ExtensibleEventData):
|
||||
"""影视名称辅助识别的输入和插件回写字段。"""
|
||||
|
||||
title: str = Field(description="待识别标题")
|
||||
name: Optional[str] = Field(default=None, description="插件识别后的名称")
|
||||
year: Optional[Any] = Field(default=None, description="年份")
|
||||
season: Optional[Any] = Field(default=None, description="季号")
|
||||
episode: Optional[Any] = Field(default=None, description="集号")
|
||||
|
||||
|
||||
class MusicNameRecognizeEventData(ExtensibleEventData):
|
||||
"""音乐名称辅助识别的输入和插件回写字段。"""
|
||||
|
||||
title: str = Field(description="待识别曲名")
|
||||
artist: Optional[str] = Field(default=None, description="艺术家")
|
||||
album: Optional[str] = Field(default=None, description="专辑")
|
||||
year: Optional[Any] = Field(default=None, description="年份")
|
||||
duration: Optional[Any] = Field(default=None, description="时长")
|
||||
name: Optional[str] = Field(default=None, description="插件识别后的曲名")
|
||||
|
||||
|
||||
class MediaRecognizeEventData(ExtensibleEventData):
|
||||
"""影视媒体身份补充识别的输入和插件回写字段。"""
|
||||
|
||||
title: Optional[str] = Field(default=None, description="待识别标题")
|
||||
year: Optional[Any] = Field(default=None, description="年份")
|
||||
season: Optional[Any] = Field(default=None, description="季号")
|
||||
type: Optional[Any] = Field(default=None, description="媒体类型")
|
||||
media_source: Optional[Any] = Field(default=None, description="媒体数据源")
|
||||
media_id: Optional[Any] = Field(default=None, description="数据源原生 ID")
|
||||
mediainfo: Optional[Dict[str, Any]] = Field(default=None, description="插件回写媒体信息")
|
||||
|
||||
|
||||
class MusicMediaRecognizeEventData(MediaRecognizeEventData):
|
||||
"""音乐媒体身份补充识别的输入和插件回写字段。"""
|
||||
|
||||
artists: List[str] = Field(default_factory=list, description="艺术家列表")
|
||||
album: Optional[str] = Field(default=None, description="专辑")
|
||||
isrc: Optional[str] = Field(default=None, description="ISRC")
|
||||
music_type: Optional[str] = Field(default=None, description="音乐实体类型")
|
||||
|
||||
|
||||
class ConfigChangeEventData(BaseEventData):
|
||||
"""
|
||||
ConfigChange 事件的数据模型
|
||||
|
||||
+22
-1
@@ -65,6 +65,7 @@ SCHEMA_EXPORTS = {
|
||||
'ChannelCapability': ('app.schemas.notification', 'ChannelCapability'),
|
||||
'ChannelCapabilityManager': ('app.schemas.notification', 'ChannelCapabilityManager'),
|
||||
'ClassVar': ('app.schemas.subscribe', 'ClassVar'),
|
||||
'CommandExecuteEventData': ('app.schemas.event', 'CommandExecuteEventData'),
|
||||
'CommandRegisterEventData': ('app.schemas.event', 'CommandRegisterEventData'),
|
||||
'ConfigChangeEventData': ('app.schemas.event', 'ConfigChangeEventData'),
|
||||
'ConfigDict': ('app.schemas.workflow', 'ConfigDict'),
|
||||
@@ -86,7 +87,9 @@ SCHEMA_EXPORTS = {
|
||||
'Discriminator': ('app.schemas.context', 'Discriminator'),
|
||||
'DownloadAddedData': ('app.schemas.download', 'DownloadAddedData'),
|
||||
'DownloadAddedEventData': ('app.schemas.event', 'DownloadAddedEventData'),
|
||||
'DownloadDeletedEventData': ('app.schemas.event', 'DownloadDeletedEventData'),
|
||||
'DownloadDirectory': ('app.schemas.download', 'DownloadDirectory'),
|
||||
'DownloadFileDeletedEventData': ('app.schemas.event', 'DownloadFileDeletedEventData'),
|
||||
'DownloadHistory': ('app.schemas.history', 'DownloadHistory'),
|
||||
'DownloadTask': ('app.schemas.workflow', 'DownloadTask'),
|
||||
'DownloadTaskMedia': ('app.schemas.transfer', 'DownloadTaskMedia'),
|
||||
@@ -94,6 +97,7 @@ SCHEMA_EXPORTS = {
|
||||
'DownloaderInfo': ('app.schemas.dashboard', 'DownloaderInfo'),
|
||||
'DownloaderTorrent': ('app.schemas.transfer', 'DownloaderTorrent'),
|
||||
'DownloadingTorrent': ('app.schemas.transfer', 'DownloadingTorrent'),
|
||||
'EmptyEventData': ('app.schemas.event', 'EmptyEventData'),
|
||||
'EndpointStats': ('app.schemas.monitoring', 'EndpointStats'),
|
||||
'Enum': ('app.schemas.notification', 'Enum'),
|
||||
'EpisodeFormat': ('app.schemas.transfer', 'EpisodeFormat'),
|
||||
@@ -103,12 +107,14 @@ SCHEMA_EXPORTS = {
|
||||
'ErrorRequest': ('app.schemas.monitoring', 'ErrorRequest'),
|
||||
'Event': ('app.schemas.event', 'Event'),
|
||||
'ExistMediaInfo': ('app.schemas.mediaserver', 'ExistMediaInfo'),
|
||||
'ExtensibleEventData': ('app.schemas.event', 'ExtensibleEventData'),
|
||||
'Field': ('app.schemas.mcp', 'Field'),
|
||||
'FileItem': ('app.schemas.workflow', 'FileItem'),
|
||||
'FileNameData': ('app.schemas.common', 'FileNameData'),
|
||||
'FileURI': ('app.schemas.file', 'FileURI'),
|
||||
'FilterRuleGroup': ('app.schemas.system', 'FilterRuleGroup'),
|
||||
'Generic': ('app.schemas.response', 'Generic'),
|
||||
'HistoryDeletedEventData': ('app.schemas.event', 'HistoryDeletedEventData'),
|
||||
'IdData': ('app.schemas.common', 'IdData'),
|
||||
'ImmediateException': ('app.schemas.exception', 'ImmediateException'),
|
||||
'IncomingMessage': ('app.schemas.message', 'IncomingMessage'),
|
||||
@@ -173,6 +179,7 @@ SCHEMA_EXPORTS = {
|
||||
'MediaLanguage': ('app.schemas.context', 'MediaLanguage'),
|
||||
'MediaPerson': ('app.schemas.context', 'MediaPerson'),
|
||||
'MediaRecognizeConvertEventData': ('app.schemas.event', 'MediaRecognizeConvertEventData'),
|
||||
'MediaRecognizeEventData': ('app.schemas.event', 'MediaRecognizeEventData'),
|
||||
'MediaReleaseDate': ('app.schemas.context', 'MediaReleaseDate'),
|
||||
'MediaSearchResult': ('app.schemas.context', 'MediaSearchResult'),
|
||||
'MediaSearchResults': ('app.schemas.context', 'MediaSearchResults'),
|
||||
@@ -190,6 +197,7 @@ SCHEMA_EXPORTS = {
|
||||
'MediaSourceInfo': ('app.schemas.event', 'MediaSourceInfo'),
|
||||
'MediaType': ('app.schemas.subscribe', 'MediaType'),
|
||||
'Message': ('app.schemas.message', 'Message'),
|
||||
'MessageActionEventData': ('app.schemas.event', 'MessageActionEventData'),
|
||||
'MessageClearBefore': ('app.schemas.message', 'MessageClearBefore'),
|
||||
'MessageClearData': ('app.schemas.message', 'MessageClearData'),
|
||||
'MessageClearScope': ('app.schemas.message', 'MessageClearScope'),
|
||||
@@ -197,6 +205,7 @@ SCHEMA_EXPORTS = {
|
||||
'MessageResponse': ('app.schemas.message', 'MessageResponse'),
|
||||
'MessageType': ('app.schemas.message', 'MessageType'),
|
||||
'MetaInfo': ('app.schemas.transfer', 'MetaInfo'),
|
||||
'MetadataScrapeEventData': ('app.schemas.event', 'MetadataScrapeEventData'),
|
||||
'MfaChallenge': ('app.schemas.token', 'MfaChallenge'),
|
||||
'MfaStatusData': ('app.schemas.mfa', 'MfaStatusData'),
|
||||
'MonitoringConfig': ('app.schemas.monitoring', 'MonitoringConfig'),
|
||||
@@ -205,16 +214,20 @@ SCHEMA_EXPORTS = {
|
||||
'MusicArtistInfo': ('app.schemas.music', 'MusicArtistInfo'),
|
||||
'MusicEntityType': ('app.schemas.music', 'MusicEntityType'),
|
||||
'MusicInfo': ('app.schemas.transfer', 'MusicInfo'),
|
||||
'MusicMediaRecognizeEventData': ('app.schemas.event', 'MusicMediaRecognizeEventData'),
|
||||
'MusicMeta': ('app.schemas.transfer', 'MusicMeta'),
|
||||
'MusicNameRecognizeEventData': ('app.schemas.event', 'MusicNameRecognizeEventData'),
|
||||
'MusicRecognitionCacheData': ('app.schemas.music', 'MusicRecognitionCacheData'),
|
||||
'MusicRecognitionCacheItem': ('app.schemas.music', 'MusicRecognitionCacheItem'),
|
||||
'MusicRecognizeRequest': ('app.schemas.music', 'MusicRecognizeRequest'),
|
||||
'MusicRelease': ('app.schemas.music', 'MusicRelease'),
|
||||
'MusicTargetEntityType': ('app.schemas.transfer', 'MusicTargetEntityType'),
|
||||
'NameData': ('app.schemas.common', 'NameData'),
|
||||
'NameRecognizeEventData': ('app.schemas.event', 'NameRecognizeEventData'),
|
||||
'NameValueOption': ('app.schemas.workflow', 'NameValueOption'),
|
||||
'NetTestTarget': ('app.schemas.system', 'NetTestTarget'),
|
||||
'NotExistMediaInfo': ('app.schemas.mediaserver', 'NotExistMediaInfo'),
|
||||
'NoticeMessageEventData': ('app.schemas.event', 'NoticeMessageEventData'),
|
||||
'NotificationChannel': ('app.schemas.notification', 'NotificationChannel'),
|
||||
'NotificationConf': ('app.schemas.system', 'NotificationConf'),
|
||||
'NotificationSwitch': ('app.schemas.message', 'NotificationSwitch'),
|
||||
@@ -246,6 +259,7 @@ SCHEMA_EXPORTS = {
|
||||
'Path': ('app.schemas.transfer', 'Path'),
|
||||
'PerformanceSnapshot': ('app.schemas.monitoring', 'PerformanceSnapshot'),
|
||||
'Plugin': ('app.schemas.plugin', 'Plugin'),
|
||||
'PluginActionEventData': ('app.schemas.event', 'PluginActionEventData'),
|
||||
'PluginCloneRequest': ('app.schemas.plugin', 'PluginCloneRequest'),
|
||||
'PluginDashboard': ('app.schemas.plugin', 'PluginDashboard'),
|
||||
'PluginDashboardMetaItem': ('app.schemas.plugin', 'PluginDashboardMetaItem'),
|
||||
@@ -261,10 +275,12 @@ SCHEMA_EXPORTS = {
|
||||
'PluginRatingRequest': ('app.schemas.plugin', 'PluginRatingRequest'),
|
||||
'PluginReleaseData': ('app.schemas.plugin', 'PluginReleaseData'),
|
||||
'PluginReleaseItem': ('app.schemas.plugin', 'PluginReleaseItem'),
|
||||
'PluginReloadEventData': ('app.schemas.event', 'PluginReloadEventData'),
|
||||
'PluginRemoteInfo': ('app.schemas.plugin', 'PluginRemoteInfo'),
|
||||
'PluginRuntimeStatus': ('app.schemas.plugin', 'PluginRuntimeStatus'),
|
||||
'PluginRuntimeSummary': ('app.schemas.plugin', 'PluginRuntimeSummary'),
|
||||
'PluginSidebarNavItem': ('app.schemas.plugin', 'PluginSidebarNavItem'),
|
||||
'PluginTriggeredEventData': ('app.schemas.event', 'PluginTriggeredEventData'),
|
||||
'PluginWorkflowActionGroup': ('app.schemas.workflow', 'PluginWorkflowActionGroup'),
|
||||
'ProcessInfo': ('app.schemas.dashboard', 'ProcessInfo'),
|
||||
'ProgressKeyData': ('app.schemas.common', 'ProgressKeyData'),
|
||||
@@ -304,6 +320,7 @@ SCHEMA_EXPORTS = {
|
||||
'SiteAuth': ('app.schemas.site', 'SiteAuth'),
|
||||
'SiteCategory': ('app.schemas.site', 'SiteCategory'),
|
||||
'SiteCookieUpdate': ('app.schemas.site', 'SiteCookieUpdate'),
|
||||
'SiteEventData': ('app.schemas.event', 'SiteEventData'),
|
||||
'SiteIconData': ('app.schemas.site', 'SiteIconData'),
|
||||
'SiteMappingData': ('app.schemas.site', 'SiteMappingData'),
|
||||
'SiteStatistic': ('app.schemas.site', 'SiteStatistic'),
|
||||
@@ -327,6 +344,7 @@ SCHEMA_EXPORTS = {
|
||||
'SubscrbieInfo': ('app.schemas.subscribe', 'SubscrbieInfo'),
|
||||
'Subscribe': ('app.schemas.workflow', 'Subscribe'),
|
||||
'SubscribeAddedEventData': ('app.schemas.event', 'SubscribeAddedEventData'),
|
||||
'SubscribeCompleteEventData': ('app.schemas.event', 'SubscribeCompleteEventData'),
|
||||
'SubscribeCompletionCheckEventData': ('app.schemas.event', 'SubscribeCompletionCheckEventData'),
|
||||
'SubscribeDeletedEventData': ('app.schemas.event', 'SubscribeDeletedEventData'),
|
||||
'SubscribeDownloadFileInfo': ('app.schemas.subscribe', 'SubscribeDownloadFileInfo'),
|
||||
@@ -341,6 +359,7 @@ SCHEMA_EXPORTS = {
|
||||
'SubtitleDownloadData': ('app.schemas.download', 'SubtitleDownloadData'),
|
||||
'SubtitleInfo': ('app.schemas.search', 'SubtitleInfo'),
|
||||
'SystemEnvironmentUpdateData': ('app.schemas.system', 'SystemEnvironmentUpdateData'),
|
||||
'SystemErrorEventData': ('app.schemas.event', 'SystemErrorEventData'),
|
||||
'SystemModuleInfo': ('app.schemas.system', 'SystemModuleInfo'),
|
||||
'SystemModuleListData': ('app.schemas.system', 'SystemModuleListData'),
|
||||
'TMDbException': ('app.schemas.exception', 'TMDbException'),
|
||||
@@ -386,6 +405,7 @@ SCHEMA_EXPORTS = {
|
||||
'UserInDB': ('app.schemas.user', 'UserInDB'),
|
||||
'UserInDBBase': ('app.schemas.user', 'UserInDBBase'),
|
||||
'UserMessage': ('app.schemas.agent', 'UserMessage'),
|
||||
'UserMessageEventData': ('app.schemas.event', 'UserMessageEventData'),
|
||||
'UserPermissions': ('app.schemas.user', 'UserPermissions'),
|
||||
'UserUpdate': ('app.schemas.user', 'UserUpdate'),
|
||||
'ValidationIssue': ('app.schemas.response', 'ValidationIssue'),
|
||||
@@ -397,6 +417,7 @@ SCHEMA_EXPORTS = {
|
||||
'WechatClawBotKnownTarget': ('app.schemas.notification', 'WechatClawBotKnownTarget'),
|
||||
'Workflow': ('app.schemas.workflow', 'Workflow'),
|
||||
'WorkflowActionDefinition': ('app.schemas.workflow', 'WorkflowActionDefinition'),
|
||||
'WorkflowExecuteEventData': ('app.schemas.event', 'WorkflowExecuteEventData'),
|
||||
'WorkflowExecutionConfig': ('app.schemas.workflow', 'WorkflowExecutionConfig'),
|
||||
'WorkflowExecutionState': ('app.schemas.workflow', 'WorkflowExecutionState'),
|
||||
'WorkflowNodeState': ('app.schemas.workflow', 'WorkflowNodeState'),
|
||||
@@ -415,7 +436,7 @@ SCHEMA_EXPORTS = {
|
||||
SCHEMA_CONFLICTS = {
|
||||
'Any': ['app.schemas.common', 'app.schemas.context', 'app.schemas.event', 'app.schemas.mediaserver', 'app.schemas.message', 'app.schemas.response', 'app.schemas.subscribe', 'app.schemas.system', 'app.schemas.workflow'],
|
||||
'BaseModel': ['app.schemas.agent', 'app.schemas.cache', 'app.schemas.category', 'app.schemas.common', 'app.schemas.context', 'app.schemas.dashboard', 'app.schemas.download', 'app.schemas.event', 'app.schemas.file', 'app.schemas.history', 'app.schemas.llm', 'app.schemas.mediaserver', 'app.schemas.message', 'app.schemas.mfa', 'app.schemas.music', 'app.schemas.monitoring', 'app.schemas.notification', 'app.schemas.plugin', 'app.schemas.response', 'app.schemas.rule', 'app.schemas.search', 'app.schemas.storage', 'app.schemas.openai', 'app.schemas.servarr', 'app.schemas.servcookie', 'app.schemas.site', 'app.schemas.subscribe', 'app.schemas.system', 'app.schemas.tmdb', 'app.schemas.token', 'app.schemas.transfer', 'app.schemas.user', 'app.schemas.workflow', 'app.schemas.mcp'],
|
||||
'ConfigDict': ['app.schemas.agent', 'app.schemas.category', 'app.schemas.history', 'app.schemas.mediaserver', 'app.schemas.response', 'app.schemas.openai', 'app.schemas.servcookie', 'app.schemas.site', 'app.schemas.subscribe', 'app.schemas.user', 'app.schemas.workflow'],
|
||||
'ConfigDict': ['app.schemas.agent', 'app.schemas.category', 'app.schemas.event', 'app.schemas.history', 'app.schemas.mediaserver', 'app.schemas.response', 'app.schemas.openai', 'app.schemas.servcookie', 'app.schemas.site', 'app.schemas.subscribe', 'app.schemas.user', 'app.schemas.workflow'],
|
||||
'Context': ['app.schemas.context', 'app.schemas.workflow'],
|
||||
'Dict': ['app.schemas.category', 'app.schemas.common', 'app.schemas.context', 'app.schemas.event', 'app.schemas.mediaserver', 'app.schemas.message', 'app.schemas.notification', 'app.schemas.plugin', 'app.schemas.openai', 'app.schemas.site', 'app.schemas.subscribe', 'app.schemas.mcp'],
|
||||
'DownloadTask': ['app.schemas.download', 'app.schemas.workflow'],
|
||||
|
||||
@@ -621,6 +621,9 @@ ModuleMethodSpec(
|
||||
`ModuleCapability` Protocol 为宿主和新插件提供静态声明入口,但不替换字符串 dispatcher ABI。
|
||||
- 22 个显式方法进一步登记宿主真实传入的 required parameter 名称,覆盖识别、搜索、媒体服务器、存储、
|
||||
消息收尾、命令注册和 webhook;dispatcher 仍只输出诊断 warning,不阻断缺少参数的旧插件或未知自定义方法。
|
||||
- 契约清单现覆盖静态扫描到的 211 个宿主字符串调用,并保留一个暂未被宿主调用的 `send_message` 公开能力,
|
||||
共 212 个显式 V2 spec。原先仅按 prefix 分类或落入默认 legacy 的宿主方法均获得稳定 family、输入合同、
|
||||
结果合同、执行、超时和错误语义;未知第三方自定义方法仍走开放 legacy fallback,不拒绝加载或执行。
|
||||
|
||||
#### ARCH-241:Event Contract Registry
|
||||
|
||||
@@ -658,6 +661,9 @@ ModuleMethodSpec(
|
||||
- 订阅变更、下载添加、整理成功/失败等用户副作用标记为 `durable_required`,只表达完成语义要求;
|
||||
在 ARCH-251 pilot 完成前不虚构当前已具备持久投递。SystemError 仍沿用既有递归保护和异常通知路径。
|
||||
- runtime contract baseline 新增稳定 `event_specs`,后续 enum 新增必须同步登记,且不比较源码行号。
|
||||
- 53 个事件现已全部绑定 typed payload,原有 28 个 `legacy_dict` 项归零。插件动作/触发等开放事件使用
|
||||
“公共字段类型化 + `extra=allow`”模型,Webhook 与 Workflow execution 复用既有 DTO;验证仍只诊断并投递
|
||||
同一个原始 dict/model,因此插件字段、对象引用和链式原地修改语义未改变。
|
||||
|
||||
#### ARCH-242:Module/Integration 质量清单
|
||||
|
||||
|
||||
+4
-2
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6367,
|
||||
"edge_sha256": "62c413857cd12dbaf4d31e4fad4c6b6c80f7dc747d0d5a74ecb61c44b4996822",
|
||||
"edge_count": 6369,
|
||||
"edge_sha256": "62638f6e334e7deb05902ca1e50cdcd03245a85c207dd2b4efaa03306cd28187",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -5436,7 +5436,9 @@
|
||||
"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.mediaserver",
|
||||
"app.runtime.event.contracts -> app.schemas.types",
|
||||
"app.runtime.event.contracts -> app.schemas.workflow",
|
||||
"app.runtime.event.dispatch -> app.runtime",
|
||||
"app.runtime.event.dispatch -> app.runtime.correlation",
|
||||
"app.runtime.event.dispatch -> app.runtime.event",
|
||||
|
||||
+2907
-57
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ from app.schemas.types import ChainEventType, EventType, MediaType
|
||||
|
||||
|
||||
def test_every_event_enum_has_complete_contract() -> None:
|
||||
"""53 个广播/链式事件必须全部登记且 legacy 项必须解释原因。"""
|
||||
"""53 个广播/链式事件必须全部登记并绑定 typed payload。"""
|
||||
expected = {*EventType, *ChainEventType}
|
||||
|
||||
assert set(EVENT_CONTRACTS) == expected
|
||||
@@ -26,8 +26,24 @@ def test_every_event_enum_has_complete_contract() -> None:
|
||||
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
|
||||
assert contract.payload_model is not None
|
||||
assert contract.payload_contract != "legacy_dict"
|
||||
assert contract.legacy_reason is None
|
||||
|
||||
|
||||
def test_extensible_plugin_payload_accepts_custom_fields_without_shape_change() -> None:
|
||||
"""插件动作 contract 只校验公共字段,插件自定义字段和原始 dict 均保持不变。"""
|
||||
payload = {
|
||||
"plugin_id": "DemoPlugin",
|
||||
"action": "refresh",
|
||||
"plugin_owned_field": {"value": 1},
|
||||
}
|
||||
|
||||
assert validate_event_payload(EventType.PluginAction, payload) == ()
|
||||
event = Event(EventType.PluginAction, payload)
|
||||
|
||||
assert event.event_data is payload
|
||||
assert event.event_data["plugin_owned_field"] == {"value": 1}
|
||||
|
||||
|
||||
def test_typed_payload_is_validated_without_changing_public_shape() -> None:
|
||||
|
||||
@@ -19,15 +19,19 @@ RUNTIME_BASELINE = (
|
||||
)
|
||||
|
||||
|
||||
def test_all_scanned_module_methods_resolve_a_contract() -> None:
|
||||
"""架构快照中的所有字符串方法都必须能解析到稳定聚合规则。"""
|
||||
def test_all_scanned_host_module_methods_have_explicit_v2_contracts() -> None:
|
||||
"""架构快照中的全部宿主字符串方法都必须显式登记 V2 契约。"""
|
||||
payload = json.loads(RUNTIME_BASELINE.read_text(encoding="utf-8"))
|
||||
methods = payload["run_module"]["methods"]
|
||||
methods = set(payload["run_module"]["methods"])
|
||||
contracts = list_explicit_module_contracts()
|
||||
|
||||
assert methods
|
||||
assert methods <= contracts.keys()
|
||||
for method in methods:
|
||||
contract = get_module_method_contract(method)
|
||||
contract = contracts[method]
|
||||
assert isinstance(contract.aggregation, ModuleResultAggregation)
|
||||
assert contract.input_contract != "legacy_args"
|
||||
assert contract.result_contract != "Any"
|
||||
assert contract.plugin_short_circuit is True
|
||||
|
||||
|
||||
@@ -59,11 +63,11 @@ def test_unknown_plugin_method_keeps_legacy_compatibility() -> None:
|
||||
assert contract.supports_async is True
|
||||
|
||||
|
||||
def test_contract_v2_freezes_at_least_twenty_high_value_methods() -> None:
|
||||
"""首批能力必须具备可生成文档和诊断的完整 V2 字段。"""
|
||||
def test_contract_v2_freezes_every_observed_host_method() -> None:
|
||||
"""全部已观察宿主能力必须具备可生成文档和诊断的完整 V2 字段。"""
|
||||
contracts = list_explicit_module_contracts()
|
||||
|
||||
assert len(contracts) >= 20
|
||||
assert len(contracts) >= 211
|
||||
for contract in contracts.values():
|
||||
assert contract.version == 1
|
||||
assert contract.input_contract != "legacy_args"
|
||||
|
||||
Reference in New Issue
Block a user