from __future__ import annotations import warnings from dataclasses import MISSING, dataclass, field, fields from typing import ClassVar from yuxi.channel.config.defaults import ( BLOCK_STREAMING_BREAK, BLOCK_STREAMING_CHUNK_BREAK_PREFERENCE, BLOCK_STREAMING_CHUNK_MAX_CHARS, BLOCK_STREAMING_CHUNK_MIN_CHARS, BLOCK_STREAMING_COALESCE_IDLE_MS, BLOCK_STREAMING_COALESCE_MAX_CHARS, BLOCK_STREAMING_COALESCE_MIN_CHARS, SSE_MAX_REASONING_CHARS, STREAMING_PREVIEW_MIN_INITIAL_CHARS, STREAMING_PREVIEW_THROTTLE_MS, ) # 哨兵对象,用于标记字段尚未被显式设置 _UNSET = object() # 支持的流式传输模式 STREAMING_MODES: tuple[str, ...] = ("card_kit", "block", "c2c_stream_api", "raw") # 块级流式传输的分割断点类型 BLOCK_STREAMING_BREAKS: tuple[str, ...] = ("text_end", "paragraph", "sentence", "token") @dataclass class ChannelCapabilities: """定义通道(Channel)支持的各种能力配置。""" # 聊天类型,默认支持私聊和群聊 chat_types: list[str] = field(default_factory=lambda: ["direct", "group"]) # 消息类型,默认仅支持文本 message_types: list[str] = field(default_factory=lambda: ["text"]) # 交互方式列表 interactions: list[str] = field(default_factory=list) # 以下为各类功能开关,默认使用 _UNSET 表示未显式设置 reactions: bool = field(default=_UNSET) # 是否支持消息表情反应 typing_indicator: bool = field(default=_UNSET) # 是否支持输入状态指示 threads: bool = field(default=_UNSET) # 是否支持话题线程 edit: bool = field(default=_UNSET) # 是否支持编辑消息 unsend: bool = field(default=_UNSET) # 是否支持撤回消息 reply: bool = field(default=_UNSET) # 是否支持回复消息 media: bool = field(default=_UNSET) # 是否支持发送媒体 effects: bool = field(default=_UNSET) # 是否支持消息特效 native_commands: bool = field(default=_UNSET) # 是否支持原生命令 polls: bool = field(default=_UNSET) # 是否支持投票 pins: bool = field(default=_UNSET) # 是否支持置顶消息 group_management: bool = field(default=_UNSET) # 是否支持群组管理 adaptive_cards: bool = field(default=_UNSET) # 是否支持自适应卡片 tasks: bool = field(default=_UNSET) # 是否支持任务 events: bool = field(default=_UNSET) # 是否支持事件 notes: bool = field(default=_UNSET) # 是否支持笔记 task_modules: bool = field(default=_UNSET) # 是否支持任务模块 messaging_extensions: bool = field(default=_UNSET) # 是否支持消息扩展 incoming_webhooks: bool = field(default=_UNSET) # 是否支持传入的 Webhook bot_state: bool = field(default=_UNSET) # 是否支持机器人状态 # 流式传输相关配置 streaming: bool = field(default=_UNSET) # 是否启用流式传输 streaming_mode: str | None = None # 流式传输模式 preview_stream_throttle_ms: int = field(default=_UNSET) # 预览流节流间隔(毫秒) preview_min_initial_chars: int = field(default=_UNSET) # 预览最小初始字符数 # 块级流式传输配置 block_streaming: bool = field(default=_UNSET) # 是否启用块级流式传输 block_streaming_break: str = field(default=_UNSET) # 块级流式分割断点 block_streaming_chunk_min_chars: int = field(default=_UNSET) # 块最小字符数 block_streaming_chunk_max_chars: int = field(default=_UNSET) # 块最大字符数 block_streaming_chunk_break_preference: str = field(default=_UNSET) # 块分割偏好 block_streaming_coalesce_min_chars: int | None = None # 合并块最小字符数 block_streaming_coalesce_max_chars: int | None = None # 合并块最大字符数 block_streaming_coalesce_idle_ms: int = field(default=_UNSET) # 合并空闲等待时间(毫秒) # SSE 与轮询配置 sse_support: bool = field(default=_UNSET) # 是否支持 SSE sse_max_reasoning_chars: int = field(default=_UNSET) # SSE 最大推理字符数 polling_fallback: bool = field(default=_UNSET) # 是否启用轮询降级 # TTS(文本转语音)配置 tts: dict | None = None # 类变量:已警告的冻结字段集合 _WARNED_FROZEN_FIELDS: ClassVar[frozenset[str]] = frozenset() def __post_init__(self) -> None: """初始化后处理:记录原始值并对未设置的字段应用默认值。""" self._raw_values = dict(self.__dict__) # 布尔功能开关默认值均为 False if self.reactions is _UNSET: self.reactions = False if self.typing_indicator is _UNSET: self.typing_indicator = False if self.threads is _UNSET: self.threads = False if self.edit is _UNSET: self.edit = False if self.unsend is _UNSET: self.unsend = False if self.reply is _UNSET: self.reply = False if self.media is _UNSET: self.media = False if self.effects is _UNSET: self.effects = False if self.native_commands is _UNSET: self.native_commands = False if self.polls is _UNSET: self.polls = False if self.pins is _UNSET: self.pins = False if self.group_management is _UNSET: self.group_management = False if self.adaptive_cards is _UNSET: self.adaptive_cards = False if self.tasks is _UNSET: self.tasks = False if self.events is _UNSET: self.events = False if self.notes is _UNSET: self.notes = False if self.task_modules is _UNSET: self.task_modules = False if self.messaging_extensions is _UNSET: self.messaging_extensions = False if self.incoming_webhooks is _UNSET: self.incoming_webhooks = False if self.bot_state is _UNSET: self.bot_state = False if self.streaming is _UNSET: self.streaming = False if self.block_streaming is _UNSET: self.block_streaming = False if self.sse_support is _UNSET: self.sse_support = False if self.polling_fallback is _UNSET: self.polling_fallback = False # 数值类型字段默认值(来源:config/defaults.py) if self.preview_stream_throttle_ms is _UNSET: self.preview_stream_throttle_ms = STREAMING_PREVIEW_THROTTLE_MS if self.preview_min_initial_chars is _UNSET: self.preview_min_initial_chars = STREAMING_PREVIEW_MIN_INITIAL_CHARS if self.block_streaming_break is _UNSET: self.block_streaming_break = BLOCK_STREAMING_BREAK if self.block_streaming_chunk_min_chars is _UNSET: self.block_streaming_chunk_min_chars = BLOCK_STREAMING_CHUNK_MIN_CHARS if self.block_streaming_chunk_max_chars is _UNSET: self.block_streaming_chunk_max_chars = BLOCK_STREAMING_CHUNK_MAX_CHARS if self.block_streaming_chunk_break_preference is _UNSET: self.block_streaming_chunk_break_preference = BLOCK_STREAMING_CHUNK_BREAK_PREFERENCE if self.block_streaming_coalesce_idle_ms is _UNSET: self.block_streaming_coalesce_idle_ms = BLOCK_STREAMING_COALESCE_IDLE_MS if self.sse_max_reasoning_chars is _UNSET: self.sse_max_reasoning_chars = SSE_MAX_REASONING_CHARS # 校验流式传输模式是否合法 if self.streaming_mode is not None and self.streaming_mode not in STREAMING_MODES: warnings.warn( f"Unknown streaming_mode: {self.streaming_mode!r}. Known modes: {STREAMING_MODES}", stacklevel=3, ) # 校验块级流式分割断点是否合法 if self.block_streaming_break not in BLOCK_STREAMING_BREAKS: warnings.warn( f"Unknown block_streaming_break: {self.block_streaming_break!r}. " f"Known breaks: {BLOCK_STREAMING_BREAKS}", stacklevel=3, ) # 校验块字符数范围合理性 if self.block_streaming_chunk_min_chars > self.block_streaming_chunk_max_chars: raise ValueError( f"block_streaming_chunk_min_chars ({self.block_streaming_chunk_min_chars}) " f"must be <= block_streaming_chunk_max_chars ({self.block_streaming_chunk_max_chars})" ) # 块级流式传输仅在流式传输开启时生效 if self.block_streaming and not self.streaming: warnings.warn( "block_streaming=True but streaming=False, block_streaming has no effect", stacklevel=3, ) # 校验合并块字符数范围合理性 if self.block_streaming_coalesce_min_chars is not None and self.block_streaming_coalesce_max_chars is not None: if self.block_streaming_coalesce_min_chars > self.block_streaming_coalesce_max_chars: raise ValueError( f"block_streaming_coalesce_min_chars ({self.block_streaming_coalesce_min_chars}) " f"must be <= block_streaming_coalesce_max_chars ({self.block_streaming_coalesce_max_chars})" ) def to_dict(self) -> dict: """将能力配置转换为字典格式。""" return { "chat_types": list(self.chat_types), "message_types": list(self.message_types), "interactions": list(self.interactions), "reactions": self.reactions, "typing_indicator": self.typing_indicator, "threads": self.threads, "edit": self.edit, "unsend": self.unsend, "reply": self.reply, "media": self.media, "effects": self.effects, "native_commands": self.native_commands, "polls": self.polls, "pins": self.pins, "group_management": self.group_management, "adaptive_cards": self.adaptive_cards, "tasks": self.tasks, "events": self.events, "notes": self.notes, "task_modules": self.task_modules, "messaging_extensions": self.messaging_extensions, "incoming_webhooks": self.incoming_webhooks, "bot_state": self.bot_state, "streaming": self.streaming, "streaming_mode": self.streaming_mode, "preview_stream_throttle_ms": self.preview_stream_throttle_ms, "preview_min_initial_chars": self.preview_min_initial_chars, "block_streaming": self.block_streaming, "block_streaming_break": self.block_streaming_break, "block_streaming_chunk_min_chars": self.block_streaming_chunk_min_chars, "block_streaming_chunk_max_chars": self.block_streaming_chunk_max_chars, "block_streaming_chunk_break_preference": self.block_streaming_chunk_break_preference, "block_streaming_coalesce_min_chars": self.block_streaming_coalesce_min_chars, "block_streaming_coalesce_max_chars": self.block_streaming_coalesce_max_chars, "block_streaming_coalesce_idle_ms": self.block_streaming_coalesce_idle_ms, "sse_support": self.sse_support, "sse_max_reasoning_chars": self.sse_max_reasoning_chars, "polling_fallback": self.polling_fallback, "tts": dict(self.tts) if self.tts is not None else None, } def override_with(self, overrides: ChannelCapabilities | dict) -> ChannelCapabilities: """使用另一组能力配置覆盖当前配置,返回新的 ChannelCapabilities 实例。""" if isinstance(overrides, dict): override_dict: dict | None = overrides overrides = ChannelCapabilities(**overrides) else: override_dict = None if not isinstance(overrides, ChannelCapabilities): raise TypeError(f"Expected ChannelCapabilities or dict, got {type(overrides).__name__}") def _merge_list(base: list[str], override: list[str]) -> list[str]: """合并两个列表并去重,保持原有顺序。""" return list(dict.fromkeys(base + override)) def _should_override(name: str) -> bool: """判断指定字段是否需要被覆盖。""" if override_dict is not None: return name in override_dict raw = getattr(overrides, "_raw_values", None) if raw is None: return True raw_value = raw.get(name) if raw_value is _UNSET: return False field_default = _FIELD_DEFAULTS.get(name) if field_default is not _UNSET: return raw_value != field_default return True return ChannelCapabilities( chat_types=_merge_list(self.chat_types, overrides.chat_types), message_types=_merge_list(self.message_types, overrides.message_types), interactions=_merge_list(self.interactions, overrides.interactions), reactions=overrides.reactions if _should_override("reactions") else self.reactions, typing_indicator=overrides.typing_indicator if _should_override("typing_indicator") else self.typing_indicator, threads=overrides.threads if _should_override("threads") else self.threads, edit=overrides.edit if _should_override("edit") else self.edit, unsend=overrides.unsend if _should_override("unsend") else self.unsend, reply=overrides.reply if _should_override("reply") else self.reply, media=overrides.media if _should_override("media") else self.media, effects=overrides.effects if _should_override("effects") else self.effects, native_commands=overrides.native_commands if _should_override("native_commands") else self.native_commands, polls=overrides.polls if _should_override("polls") else self.polls, pins=overrides.pins if _should_override("pins") else self.pins, group_management=overrides.group_management if _should_override("group_management") else self.group_management, adaptive_cards=overrides.adaptive_cards if _should_override("adaptive_cards") else self.adaptive_cards, tasks=overrides.tasks if _should_override("tasks") else self.tasks, events=overrides.events if _should_override("events") else self.events, notes=overrides.notes if _should_override("notes") else self.notes, task_modules=overrides.task_modules if _should_override("task_modules") else self.task_modules, messaging_extensions=overrides.messaging_extensions if _should_override("messaging_extensions") else self.messaging_extensions, incoming_webhooks=overrides.incoming_webhooks if _should_override("incoming_webhooks") else self.incoming_webhooks, bot_state=overrides.bot_state if _should_override("bot_state") else self.bot_state, streaming=overrides.streaming if _should_override("streaming") else self.streaming, streaming_mode=overrides.streaming_mode if _should_override("streaming_mode") else self.streaming_mode, preview_stream_throttle_ms=overrides.preview_stream_throttle_ms if _should_override("preview_stream_throttle_ms") else self.preview_stream_throttle_ms, preview_min_initial_chars=overrides.preview_min_initial_chars if _should_override("preview_min_initial_chars") else self.preview_min_initial_chars, block_streaming=overrides.block_streaming if _should_override("block_streaming") else self.block_streaming, block_streaming_break=overrides.block_streaming_break if _should_override("block_streaming_break") else self.block_streaming_break, block_streaming_chunk_min_chars=overrides.block_streaming_chunk_min_chars if _should_override("block_streaming_chunk_min_chars") else self.block_streaming_chunk_min_chars, block_streaming_chunk_max_chars=overrides.block_streaming_chunk_max_chars if _should_override("block_streaming_chunk_max_chars") else self.block_streaming_chunk_max_chars, block_streaming_chunk_break_preference=overrides.block_streaming_chunk_break_preference if _should_override("block_streaming_chunk_break_preference") else self.block_streaming_chunk_break_preference, block_streaming_coalesce_min_chars=overrides.block_streaming_coalesce_min_chars if _should_override("block_streaming_coalesce_min_chars") else self.block_streaming_coalesce_min_chars, block_streaming_coalesce_max_chars=overrides.block_streaming_coalesce_max_chars if _should_override("block_streaming_coalesce_max_chars") else self.block_streaming_coalesce_max_chars, block_streaming_coalesce_idle_ms=overrides.block_streaming_coalesce_idle_ms if _should_override("block_streaming_coalesce_idle_ms") else self.block_streaming_coalesce_idle_ms, sse_support=overrides.sse_support if _should_override("sse_support") else self.sse_support, sse_max_reasoning_chars=overrides.sse_max_reasoning_chars if _should_override("sse_max_reasoning_chars") else self.sse_max_reasoning_chars, polling_fallback=overrides.polling_fallback if _should_override("polling_fallback") else self.polling_fallback, tts=overrides.tts if _should_override("tts") else self.tts, ) def supports(self, feature: str) -> bool: """检查是否支持指定功能。""" return bool(getattr(self, feature, False)) def get_action_registry(self, plugin: object) -> MessageActionRegistry: """根据当前能力配置生成消息动作注册表。""" from yuxi.channel.sdk.actions import MessageAction, MessageActionRegistry registry = MessageActionRegistry() base_actions = {MessageAction.SEND, MessageAction.SEND_MEDIA} if self.reply: base_actions.add(MessageAction.REPLY) if self.edit: base_actions.add(MessageAction.EDIT) if self.unsend: base_actions.add(MessageAction.UNSEND) if self.reactions: base_actions.add(MessageAction.REACT) base_actions.add(MessageAction.REACT_REMOVE) base_actions.add(MessageAction.REACTIONS) if self.pins: base_actions.add(MessageAction.PIN) base_actions.add(MessageAction.UNPIN) if self.polls: base_actions.add(MessageAction.POLL) base_actions.add(MessageAction.POLL_VOTE) if self.group_management: base_actions |= { MessageAction.RENAME_GROUP, MessageAction.ADD_PARTICIPANT, MessageAction.REMOVE_PARTICIPANT, MessageAction.MEMBER_INFO, MessageAction.PERMISSIONS, } if self.threads: base_actions |= { MessageAction.THREAD_CREATE, MessageAction.THREAD_LIST, MessageAction.THREAD_REPLY, MessageAction.TOPIC_CREATE, MessageAction.TOPIC_EDIT, } for action in base_actions: registry.register(action, lambda **kwargs: {"success": True}) return registry def derive_from_adapter(self, adapter) -> ChannelCapabilities: """从适配器推导能力配置。""" actions = {a.value for a in adapter.list_actions()} return ChannelCapabilities( chat_types=self.chat_types, message_types=self.message_types, reactions="react" in actions or "react_remove" in actions, edit="edit" in actions, unsend="unsend" in actions, polls="poll" in actions or "poll_vote" in actions, pins="pin" in actions or "unpin" in actions, group_management=( "add_participant" in actions or "remove_participant" in actions or "rename_group" in actions ), threads="thread_reply" in actions or "thread_create" in actions or "thread_list" in actions, media="send_media" in actions, reply="reply" in actions, ) def get_effective_streaming_mode(self) -> str: """获取实际生效的流式传输模式。""" if not self.streaming: return "none" if self.streaming_mode: return self.streaming_mode return "preview" def get_chunk_limit(self) -> int: """获取分块大小限制。""" return 4096 def get_effective_block_config(self) -> dict: """获取实际生效的块级流式传输配置。""" text_limit = self.get_chunk_limit() chunk_min = max(1, self.block_streaming_chunk_min_chars) chunk_max = max(chunk_min, min(self.block_streaming_chunk_max_chars, text_limit)) coalesce_min = self.block_streaming_coalesce_min_chars or chunk_min coalesce_max = self.block_streaming_coalesce_max_chars or chunk_max coalesce_min = min(coalesce_min, coalesce_max) coalesce_max = min(coalesce_max, chunk_max) coalesce_idle_ms = max(0, min(self.block_streaming_coalesce_idle_ms, 5000)) return { "chunk": { "min_chars": chunk_min, "max_chars": chunk_max, "break_preference": self.block_streaming_chunk_break_preference, }, "coalesce": { "min_chars": coalesce_min, "max_chars": coalesce_max, "idle_ms": coalesce_idle_ms, }, "text_limit": text_limit, } # 构建字段默认值映射表,用于 override_with 中的覆盖判断 _FIELD_DEFAULTS: dict[str, object] = {} for _f in fields(ChannelCapabilities): if _f.default is not MISSING: _FIELD_DEFAULTS[_f.name] = _f.default elif _f.default_factory is not MISSING: _FIELD_DEFAULTS[_f.name] = _f.default_factory()