Files
mai-bot/src/platform_io/manager.py
DrSmoothl e26b27c287 refactor: update message gateway handling and remove adapter references
- Changed the message sending method to return DeliveryBatch instead of DeliveryReceipt in integration.py.
- Removed AdapterDeclarationPayload and related references from envelope.py, replacing them with MessageGatewayStateUpdatePayload and MessageGatewayStateUpdateResultPayload.
- Updated runner_main.py to remove adapter-related logic and methods, focusing on message gateway functionality.
- Added tests for message gateway runtime state synchronization and action bridge functionality in test files.
2026-03-24 12:14:41 +08:00

563 lines
20 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.
"""提供 Platform IO 层的中心 Broker 管理器。"""
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional
from src.common.logger import get_logger
from src.platform_io.drivers.base import PlatformIODriver
from .dedupe import MessageDeduplicator
from .outbound_tracker import OutboundTracker
from .route_key_factory import RouteKeyFactory
from .registry import DriverRegistry
from .routing import RouteTable
from .types import DeliveryBatch, DeliveryReceipt, DeliveryStatus, InboundMessageEnvelope, RouteBinding, RouteKey
if TYPE_CHECKING:
from src.chat.message_receive.message import SessionMessage
logger = get_logger("platform_io.manager")
InboundDispatcher = Callable[[InboundMessageEnvelope], Awaitable[None]]
class PlatformIOManager:
"""统一协调平台消息 IO 的路由、去重与状态跟踪。
与旧实现不同,这个管理器不再负责“多条链路谁该接管平台”的裁决,
只维护发送表和接收表两张轻量路由表:
- 发送时:解析所有命中的发送绑定并全部投递。
- 接收时:只校验当前驱动是否已登记为可接收链路,然后全部放行给上层。
- 去重时:仅对单条链路做技术性重放抑制,不做跨链路语义去重。
"""
def __init__(self) -> None:
"""初始化 Broker 管理器及其内存状态。"""
self._driver_registry = DriverRegistry()
self._send_route_table = RouteTable()
self._receive_route_table = RouteTable()
self._deduplicator = MessageDeduplicator()
self._outbound_tracker = OutboundTracker()
self._inbound_dispatcher: Optional[InboundDispatcher] = None
self._started = False
@property
def is_started(self) -> bool:
"""返回 Broker 当前是否已进入运行态。
Returns:
bool: 若 Broker 已启动则返回 ``True``。
"""
return self._started
async def start(self) -> None:
"""启动 Broker并依次启动当前已注册的全部驱动。
Raises:
Exception: 当某个驱动启动失败时,异常会继续上抛;已成功启动的驱动
会被自动回滚停止。
"""
if self._started:
return
started_drivers: List[PlatformIODriver] = []
try:
for driver in self._driver_registry.list():
await driver.start()
started_drivers.append(driver)
except Exception:
for driver in reversed(started_drivers):
try:
await driver.stop()
except Exception:
logger.exception("回滚驱动停止失败: driver_id=%s", driver.driver_id)
raise
self._started = True
async def stop(self) -> None:
"""停止 Broker并按逆序停止全部已注册驱动。
停止完成后,会同步清空仅对当前运行周期有效的去重缓存和出站跟踪状态,
避免下一次启动时继续沿用上一个运行周期的瞬时内存数据。
Raises:
RuntimeError: 当一个或多个驱动停止失败时抛出汇总异常。
"""
if not self._started:
return
stop_errors: List[str] = []
for driver in reversed(self._driver_registry.list()):
try:
await driver.stop()
except Exception as exc:
stop_errors.append(f"{driver.driver_id}: {exc}")
logger.exception("驱动停止失败: driver_id=%s", driver.driver_id)
self._started = False
self._deduplicator.clear()
self._outbound_tracker.clear()
if stop_errors:
raise RuntimeError(f"部分驱动停止失败: {'; '.join(stop_errors)}")
async def add_driver(self, driver: PlatformIODriver) -> None:
"""向运行中的 Broker 注册并启动一个驱动。
如果 Broker 尚未启动,则该方法等价于 ``register_driver()``。
Args:
driver: 要添加的驱动实例。
Raises:
Exception: 当驱动启动失败时,注册会自动回滚,异常继续上抛。
"""
self._register_driver_internal(driver)
if not self._started:
return
try:
await driver.start()
except Exception:
self._unregister_driver_internal(driver.driver_id)
raise
async def remove_driver(self, driver_id: str) -> Optional[PlatformIODriver]:
"""从运行中的 Broker 停止并移除一个驱动。
如果 Broker 尚未启动,则该方法等价于 ``unregister_driver()``。
Args:
driver_id: 要移除的驱动 ID。
Returns:
Optional[PlatformIODriver]: 若驱动存在,则返回被移除的驱动实例。
Raises:
Exception: 当 Broker 运行中且驱动停止失败时,异常会继续上抛。
"""
if not self._started:
return self.unregister_driver(driver_id)
driver = self._driver_registry.get(driver_id)
if driver is None:
return None
await driver.stop()
return self._unregister_driver_internal(driver_id)
@property
def driver_registry(self) -> DriverRegistry:
"""返回管理器持有的驱动注册表。
Returns:
DriverRegistry: 用于保存全部已注册驱动的注册表。
"""
return self._driver_registry
@property
def send_route_table(self) -> RouteTable:
"""返回发送路由表。"""
return self._send_route_table
@property
def receive_route_table(self) -> RouteTable:
"""返回接收路由表。"""
return self._receive_route_table
@property
def route_table(self) -> RouteTable:
"""兼容旧接口,返回发送路由表。"""
return self._send_route_table
@property
def deduplicator(self) -> MessageDeduplicator:
"""返回管理器持有的入站去重器。
Returns:
MessageDeduplicator: 用于抑制重复入站的去重器。
"""
return self._deduplicator
@property
def outbound_tracker(self) -> OutboundTracker:
"""返回管理器持有的出站跟踪器。
Returns:
OutboundTracker: 用于记录出站 pending 状态与回执的跟踪器。
"""
return self._outbound_tracker
def set_inbound_dispatcher(self, dispatcher: InboundDispatcher) -> None:
"""设置统一的入站分发回调。
Args:
dispatcher: 接收已通过 Broker 审核的入站封装,并继续送入
Core 下一处理阶段的异步回调。
"""
self._inbound_dispatcher = dispatcher
def clear_inbound_dispatcher(self) -> None:
"""清除当前的入站分发回调。"""
self._inbound_dispatcher = None
@property
def has_inbound_dispatcher(self) -> bool:
"""返回当前是否已经配置入站分发回调。
Returns:
bool: 若已经配置入站分发回调则返回 ``True``。
"""
return self._inbound_dispatcher is not None
def register_driver(self, driver: PlatformIODriver) -> None:
"""注册驱动,并把它的入站回调挂到 Broker。
Args:
driver: 要注册的驱动实例。
Raises:
RuntimeError: 当 Broker 已经处于运行态时抛出。此时应改用
``add_driver()`` 以保证驱动生命周期和注册状态一致。
"""
if self._started:
raise RuntimeError("Broker 运行中不允许直接 register_driver请改用 add_driver()")
self._register_driver_internal(driver)
def _register_driver_internal(self, driver: PlatformIODriver) -> None:
"""执行不带运行态限制的内部驱动注册。
Args:
driver: 要注册的驱动实例。
"""
driver.set_inbound_handler(self.accept_inbound)
self._driver_registry.register(driver)
def unregister_driver(self, driver_id: str) -> Optional[PlatformIODriver]:
"""从 Broker 注销一个驱动。
Args:
driver_id: 要移除的驱动 ID。
Returns:
Optional[PlatformIODriver]: 若驱动存在,则返回被移除的驱动实例。
Raises:
RuntimeError: 当 Broker 已经处于运行态时抛出。此时应改用
``remove_driver()``,避免驱动停止与路由解绑脱节。
"""
if self._started:
raise RuntimeError("Broker 运行中不允许直接 unregister_driver请改用 remove_driver()")
return self._unregister_driver_internal(driver_id)
def _unregister_driver_internal(self, driver_id: str) -> Optional[PlatformIODriver]:
"""执行不带运行态限制的内部驱动注销。
Args:
driver_id: 要移除的驱动 ID。
Returns:
Optional[PlatformIODriver]: 若驱动存在,则返回被移除的驱动实例。
"""
removed_driver = self._driver_registry.unregister(driver_id)
if removed_driver is None:
return None
removed_driver.clear_inbound_handler()
self._send_route_table.remove_bindings_by_driver(driver_id)
self._receive_route_table.remove_bindings_by_driver(driver_id)
return removed_driver
def bind_send_route(self, binding: RouteBinding) -> None:
"""为某个路由键绑定发送驱动。
Args:
binding: 要保存的路由绑定。
Raises:
ValueError: 当绑定引用了不存在的驱动,或者绑定与驱动描述不一致时抛出。
"""
driver = self._driver_registry.get(binding.driver_id)
if driver is None:
raise ValueError(f"驱动 {binding.driver_id} 未注册,无法绑定路由")
self._validate_binding_against_driver(binding, driver)
self._send_route_table.bind(binding)
def bind_receive_route(self, binding: RouteBinding) -> None:
"""为某个路由键绑定接收驱动。
Args:
binding: 要保存的路由绑定。
Raises:
ValueError: 当绑定引用了不存在的驱动,或者绑定与驱动描述不一致时抛出。
"""
driver = self._driver_registry.get(binding.driver_id)
if driver is None:
raise ValueError(f"驱动 {binding.driver_id} 未注册,无法绑定路由")
self._validate_binding_against_driver(binding, driver)
self._receive_route_table.bind(binding)
def bind_route(self, binding: RouteBinding) -> None:
"""兼容旧接口,默认同时绑定发送表和接收表。"""
self.bind_send_route(binding)
self.bind_receive_route(binding)
def unbind_send_route(self, route_key: RouteKey, driver_id: Optional[str] = None) -> None:
"""移除发送路由绑定。
Args:
route_key: 要移除绑定的路由键。
driver_id: 可选的特定驱动 ID。
"""
self._send_route_table.unbind(route_key, driver_id)
def unbind_receive_route(self, route_key: RouteKey, driver_id: Optional[str] = None) -> None:
"""移除接收路由绑定。
Args:
route_key: 要移除绑定的路由键。
driver_id: 可选的特定驱动 ID。
"""
self._receive_route_table.unbind(route_key, driver_id)
def unbind_route(self, route_key: RouteKey, driver_id: Optional[str] = None) -> None:
"""兼容旧接口,默认同时从发送表和接收表解绑。"""
self.unbind_send_route(route_key, driver_id)
self.unbind_receive_route(route_key, driver_id)
def resolve_drivers(self, route_key: RouteKey) -> List[PlatformIODriver]:
"""解析某个路由键当前命中的全部发送驱动。
Args:
route_key: 要解析的路由键。
Returns:
List[PlatformIODriver]: 当前命中的全部发送驱动。
"""
drivers: List[PlatformIODriver] = []
for binding in self._send_route_table.resolve_bindings(route_key):
driver = self._driver_registry.get(binding.driver_id)
if driver is not None:
drivers.append(driver)
return drivers
def resolve_driver(self, route_key: RouteKey) -> Optional[PlatformIODriver]:
"""兼容旧接口,返回首个命中的发送驱动。"""
drivers = self.resolve_drivers(route_key)
return drivers[0] if drivers else None
@staticmethod
def build_route_key_from_message(message: "SessionMessage") -> RouteKey:
"""根据 ``SessionMessage`` 构造路由键。
Args:
message: 内部会话消息对象。
Returns:
RouteKey: 由消息内容提取出的规范化路由键。
"""
return RouteKeyFactory.from_session_message(message)
@staticmethod
def build_route_key_from_message_dict(message_dict: Dict[str, Any]) -> RouteKey:
"""根据消息字典构造路由键。
Args:
message_dict: Host 与插件之间传输的消息字典。
Returns:
RouteKey: 由消息字典提取出的规范化路由键。
"""
return RouteKeyFactory.from_message_dict(message_dict)
async def accept_inbound(self, envelope: InboundMessageEnvelope) -> bool:
"""处理一条由驱动上报的入站封装。
Args:
envelope: 由传输驱动产出的入站封装。
Returns:
bool: 若消息被接受并继续转发给入站分发器,则返回 ``True``
否则返回 ``False``。
"""
if not self._receive_route_table.has_binding_for_driver(envelope.route_key, envelope.driver_id):
logger.info(
"忽略未登记到接收路由表的入站消息: route=%s driver=%s",
envelope.route_key,
envelope.driver_id,
)
return False
if self._inbound_dispatcher is None:
logger.debug("PlatformIOManager 尚未配置 inbound dispatcher暂不继续分发")
return False
dedupe_key = self._build_inbound_dedupe_key(envelope)
if dedupe_key is not None:
if not self._deduplicator.mark_seen(dedupe_key):
logger.info("忽略重复入站消息: dedupe_key=%s", dedupe_key)
return False
await self._inbound_dispatcher(envelope)
return True
async def send_message(
self,
message: "SessionMessage",
route_key: RouteKey,
metadata: Optional[Dict[str, Any]] = None,
) -> DeliveryBatch:
"""通过 Broker 选中的全部发送驱动广播一条消息。
Args:
message: 要投递的内部会话消息。
route_key: 本次出站投递选择的路由键。
metadata: 可选的额外 Broker 侧元数据。
Returns:
DeliveryBatch: 规范化后的批量出站回执。
"""
drivers = self.resolve_drivers(route_key)
if not drivers:
return DeliveryBatch(internal_message_id=message.message_id, route_key=route_key)
receipts: List[DeliveryReceipt] = []
for driver in drivers:
try:
self._outbound_tracker.begin_tracking(
internal_message_id=message.message_id,
route_key=route_key,
driver_id=driver.driver_id,
metadata=metadata,
)
except ValueError as exc:
receipts.append(
DeliveryReceipt(
internal_message_id=message.message_id,
route_key=route_key,
status=DeliveryStatus.FAILED,
driver_id=driver.driver_id,
driver_kind=driver.descriptor.kind,
error=str(exc),
)
)
continue
try:
receipt = await driver.send_message(message=message, route_key=route_key, metadata=metadata)
except Exception as exc:
receipt = DeliveryReceipt(
internal_message_id=message.message_id,
route_key=route_key,
status=DeliveryStatus.FAILED,
driver_id=driver.driver_id,
driver_kind=driver.descriptor.kind,
error=str(exc),
)
self._outbound_tracker.finish_tracking(receipt)
receipts.append(receipt)
return DeliveryBatch(
internal_message_id=message.message_id,
route_key=route_key,
receipts=receipts,
)
@staticmethod
def _build_inbound_dedupe_key(envelope: InboundMessageEnvelope) -> Optional[str]:
"""构造用于入站抑制的去重键。
Args:
envelope: 当前正在处理的入站封装。
Returns:
Optional[str]: 若可以构造稳定去重键则返回该键,否则返回 ``None``。
Notes:
这里仅接受上游显式提供的稳定消息身份,例如 ``dedupe_key``、
平台侧 ``external_message_id`` 或已经完成规范化的
``session_message.message_id``。Broker 不再根据 ``payload`` 内容
猜测语义去重键,避免把“短时间内两条内容刚好完全相同”的合法消息
误判为重复入站。
"""
raw_dedupe_key = envelope.dedupe_key or envelope.external_message_id
if raw_dedupe_key is None and envelope.session_message is not None:
raw_dedupe_key = envelope.session_message.message_id
if raw_dedupe_key is None:
return None
normalized_dedupe_key = str(raw_dedupe_key).strip()
if not normalized_dedupe_key:
return None
return f"{envelope.driver_id}:{normalized_dedupe_key}"
@staticmethod
def _validate_binding_against_driver(binding: RouteBinding, driver: PlatformIODriver) -> None:
"""校验路由绑定与驱动描述是否一致。
Args:
binding: 待校验的路由绑定。
driver: 被绑定的驱动实例。
Raises:
ValueError: 当绑定类型、平台或更细粒度路由维度与驱动描述冲突时抛出。
"""
descriptor = driver.descriptor
if binding.driver_kind != descriptor.kind:
raise ValueError(
f"路由绑定的 driver_kind={binding.driver_kind} 与驱动 {driver.driver_id} 的类型 "
f"{descriptor.kind} 不一致"
)
if binding.route_key.platform != descriptor.platform:
raise ValueError(
f"路由绑定的平台 {binding.route_key.platform} 与驱动 {driver.driver_id} 的平台 "
f"{descriptor.platform} 不一致"
)
if descriptor.account_id is not None and binding.route_key.account_id not in (None, descriptor.account_id):
raise ValueError(
f"路由绑定的 account_id={binding.route_key.account_id} 与驱动 {driver.driver_id}"
f"account_id={descriptor.account_id} 冲突"
)
if descriptor.scope is not None and binding.route_key.scope not in (None, descriptor.scope):
raise ValueError(
f"路由绑定的 scope={binding.route_key.scope} 与驱动 {driver.driver_id}"
f"scope={descriptor.scope} 冲突"
)
_platform_io_manager: Optional[PlatformIOManager] = None
def get_platform_io_manager() -> PlatformIOManager:
"""返回全局 ``PlatformIOManager`` 单例。
Returns:
PlatformIOManager: 进程级共享的 Broker 管理器实例。
"""
global _platform_io_manager
if _platform_io_manager is None:
_platform_io_manager = PlatformIOManager()
return _platform_io_manager