from __future__ import annotations from dataclasses import dataclass, field from enum import StrEnum from typing import Any from yuxi.utils.logging_config import logger class DispatchAction(StrEnum): MESSAGE = "message" COMMAND = "command" CARD_ACTION = "card_action" BOT_MENU = "bot_menu" REACTION = "reaction" MEMBER_EVENT = "member_event" READ_RECEIPT = "read_receipt" SYSTEM_EVENT = "system_event" UNKNOWN = "unknown" @dataclass class CardAction: action_id: str = "" action_type: str = "" button_id: str = "" value: str = "" user_id: str = "" chat_id: str = "" raw_data: dict[str, Any] = field(default_factory=dict) @classmethod def from_event(cls, raw_event: dict) -> CardAction: return cls( action_id=raw_event.get("action_id", ""), action_type=raw_event.get("type", ""), button_id=raw_event.get("button_id", raw_event.get("button", {}).get("id", "")), value=raw_event.get("value", ""), user_id=raw_event.get("open_id", ""), chat_id=raw_event.get("group_open_id", raw_event.get("open_id", "")), raw_data=raw_event, ) @dataclass class BotMenuAction: menu_id: str = "" menu_name: str = "" user_id: str = "" chat_id: str = "" raw_data: dict[str, Any] = field(default_factory=dict) @classmethod def from_event(cls, raw_event: dict) -> BotMenuAction: return cls( menu_id=raw_event.get("menu_id", ""), menu_name=raw_event.get("menu_name", raw_event.get("content", "")), user_id=raw_event.get("open_id", ""), chat_id=raw_event.get("group_open_id", raw_event.get("open_id", "")), raw_data=raw_event, ) @dataclass class DispatchContext: action: DispatchAction raw_event: dict[str, Any] channel_message: Any = None metadata: dict[str, Any] = field(default_factory=dict) @dataclass class DispatchResult: handled: bool = False action: DispatchAction = DispatchAction.UNKNOWN response: Any = None metadata: dict[str, Any] = field(default_factory=dict) class InteractiveDispatcher: def __init__(self): self._handlers: dict[DispatchAction, list[callable]] = {} self._fallback_handler: callable | None = None def register(self, action: DispatchAction, handler: callable) -> None: if action not in self._handlers: self._handlers[action] = [] self._handlers[action].append(handler) def set_fallback(self, handler: callable) -> None: self._fallback_handler = handler async def dispatch(self, ctx: DispatchContext) -> DispatchResult: handlers = self._handlers.get(ctx.action, []) for handler in handlers: try: result = await handler(ctx) if result is not None: return result except Exception as e: logger.error( f"[Yuanbao] Dispatch handler error for {ctx.action}: {e}", exc_info=True, ) if self._fallback_handler: try: return await self._fallback_handler(ctx) or DispatchResult( handled=False, action=ctx.action, ) except Exception as e: logger.error(f"[Yuanbao] Fallback handler error: {e}", exc_info=True) return DispatchResult(handled=False, action=ctx.action) @staticmethod def classify_event(raw_event: dict[str, Any]) -> DispatchAction: event_type = raw_event.get("type", "") if event_type == "message": content = raw_event.get("content", "") if content and content.startswith("/"): return DispatchAction.COMMAND return DispatchAction.MESSAGE elif event_type == "edited_message": return DispatchAction.MESSAGE elif event_type in ("reaction_added", "reaction_removed"): return DispatchAction.REACTION elif event_type == "bot_menu": return DispatchAction.BOT_MENU elif event_type in ("member_joined", "member_left", "bot_added", "bot_removed"): return DispatchAction.MEMBER_EVENT elif event_type in ("card_action", "card_click"): return DispatchAction.CARD_ACTION elif event_type == "read_receipt": return DispatchAction.READ_RECEIPT elif event_type in ("system_event", "typing"): return DispatchAction.SYSTEM_EVENT return DispatchAction.UNKNOWN