Files
MoviePilot/app/application/messaging/router.py
jxxghp dd38c16400 refactor(messaging): 拆分用户交互模块到 application/messaging 层
- 新增 application/messaging 交互层:router.py 统一会话优先级与回调分发,
  site/subscribe/skill/media/plugin 各交互状态与视图从 Chain 迁出
- MessageChain 改为通过 InteractionRouter 派发文本会话与按钮回调,
  新增结构化 callback_data 通道(兼容 CALLBACK: 文本前缀)
- Transfer 失败重试/AI 接管回调归入 TransferChain
- MediaInteractionChain 拆出为 app/chain/interaction.py(旧路径保留兼容别名)
- WebAgent Endpoint 去重,统一使用 agent.py 回调协议函数
- 删除 app/chain/skills.py(交互逻辑并入 SkillInteractionHandler)
- 同步更新架构文档与测试,全量 4476 通过
2026-08-15 16:36:39 +08:00

122 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
交互路由层:统一选择活动文本会话,并按固定顺序派发按钮回调。
文本会话候选覆盖 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,
)
)