""" 交互路由层:统一选择活动文本会话,并按固定顺序派发按钮回调。 文本会话候选覆盖 Site、Subscribe、Skill、Media 四类, 按会话创建时间选择最近激活的一条,避免旧会话抢占新会话的输入。 """ from dataclasses import dataclass from typing import Any, Callable, Optional, Sequence, Union from app.application.messaging.interaction import ( InteractionContext, InteractionDispatch, ) from app.application.messaging.media import media_interaction_manager from app.application.messaging.site import site_interaction_manager from app.application.messaging.skill import skill_interaction_manager from app.application.messaging.subscribe import subscribe_interaction_manager @dataclass(frozen=True, slots=True) class SessionRoute: """描述一种可继续接收文本的交互会话。""" # 会话名称,与对应 Slash 命令一致 name: str # 返回用户当前待处理会话对象(含 created_at),无会话返回 None get_pending: Callable[[Union[str, int]], Optional[Any]] # 将一条文本派发给该会话,返回是否已消费 handle_text: Callable[[InteractionContext, str], bool] @dataclass(frozen=True, slots=True) class CallbackRoute: """描述一种按钮回调的匹配和处理方式。""" # 路由名称,用于日志和排查 name: str # 按回调内容判断是否归本路由处理 matches: Callable[[str], bool] # 执行回调处理并返回派发结果 dispatch: Callable[[str, InteractionContext], InteractionDispatch] class InteractionRouter: """统一选择活动文本会话并按顺序派发按钮回调。""" def __init__( self, session_routes: Sequence[SessionRoute], callback_routes: Sequence[CallbackRoute], ): """按注册顺序保存会话路由和回调路由,回调顺序即优先级。""" self._session_routes = list(session_routes) self._callback_routes = list(callback_routes) def latest_session(self, user_id: Union[str, int]) -> Optional[SessionRoute]: """返回最近创建的待处理文本会话路由,无会话返回 None。""" best_route: Optional[SessionRoute] = None best_created_at = None for route in self._session_routes: pending = route.get_pending(user_id) if pending is None: continue created_at = getattr(pending, "created_at", None) # 缺少时间戳的会话视为最早,保证有时间戳的新会话优先 if best_route is None or ( created_at is not None and (best_created_at is None or created_at > best_created_at) ): best_route = route best_created_at = created_at return best_route def has_pending(self, user_id: Union[str, int]) -> bool: """判断用户是否存在任意待处理文本会话。""" return any( route.get_pending(user_id) is not None for route in self._session_routes ) def dispatch_active_text( self, context: InteractionContext, text: str, ) -> bool: """把文本派发给最近活动的会话,返回是否被消费。""" route = self.latest_session(context.user_id) if route is None: return False return route.handle_text(context, text) def dispatch_callback( self, context: InteractionContext, callback_data: str, ) -> InteractionDispatch: """按注册顺序匹配并派发按钮回调,均不匹配时返回未处理。 匹配到的路由未消费回调(handled=False)时继续尝试后续路由, 与既有顺序式派发的行为保持一致。 """ for route in self._callback_routes: if route.matches(callback_data): result = route.dispatch(callback_data, context) if result.handled: return result return InteractionDispatch(handled=False) def has_pending_interaction(user_id: Union[str, int]) -> bool: """供 WebAgent 判断用户是否处于传统交互会话。""" return any( manager.get_by_user(user_id) is not None for manager in ( site_interaction_manager, subscribe_interaction_manager, skill_interaction_manager, media_interaction_manager, ) )