ForcePilot/backend/package/yuxi/channel/bootstrap.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

250 lines
9.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.

"""多渠道网关启动与关闭封装。
将 `lifespan.py` 中分散的渠道组件初始化、组装与销毁逻辑集中到此处,
降低应用启动器与渠道子系统内部实现的耦合。
"""
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from yuxi.channel.config import ChannelConfigManager
from yuxi.channel.lifecycle import ChannelLifecycleManager
from yuxi.channel.message.dedupe import MessageDeduper
from yuxi.channel.message.dispatcher import ChannelGateway
from yuxi.channel.middlewares.inbound import (
CreateRunMiddleware,
DedupeMiddleware,
RouteMiddleware,
SecurityMiddleware,
SessionMiddleware,
TransactionMiddleware,
)
from yuxi.channel.middlewares.outbound import (
BuildMessageMiddleware,
ChunkMiddleware,
DowngradeMiddleware,
EnrichMiddleware,
FormatMiddleware,
MediaUploadMiddleware,
SendMiddleware,
StatusUpdateMiddleware,
)
from yuxi.channel.middlewares.registry import (
InboundMiddlewareRegistry,
OutboundMiddlewareRegistry,
)
from yuxi.channel.outbound import OutboundDispatcher
from yuxi.channel.outbound.retry import compensate_channel_messages
from yuxi.channel.plugins.loader import load_plugins
from yuxi.channel.plugins.registry import get_registry
from yuxi.channel.routing.router import BindingRouter
from yuxi.channel.security.allowlist import AllowlistChecker
from yuxi.channel.security.bot_loop import BotLoopDetector
from yuxi.channel.security.identity import IdentityLinkResolver
from yuxi.channel.security.pairing import PairingChecker, PairingManager
from yuxi.channel.security.policy import SecurityPolicy
from yuxi.channel.security.rate_limit import RateLimiter
from yuxi.channel.security.registry import SecurityCheckerRegistry
from yuxi.channel.session.manager import SessionManager
from yuxi.services.run_queue_service import get_redis_client
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from fastapi import FastAPI
class ChannelGatewayBootstrap:
"""多渠道网关启动器:负责加载插件、组装网关、启停生命周期与后台补偿任务。"""
def __init__(self) -> None:
self._lifecycle_manager: ChannelLifecycleManager | None = None
self._outbound_dispatcher: OutboundDispatcher | None = None
self._gateway: ChannelGateway | None = None
self._security_checker_registry: SecurityCheckerRegistry | None = None
self._inbound_registry: InboundMiddlewareRegistry | None = None
self._outbound_registry: OutboundMiddlewareRegistry | None = None
self._compensate_task: asyncio.Task[None] | None = None
self._started: bool = False
async def start(self, app: FastAPI) -> None:
"""加载插件、组装并启动多渠道网关,将关键组件挂载到 ``app.state``。
启动过程中任意步骤失败都会触发回滚(``stop``),避免组件泄漏。
重复调用会被忽略。
"""
if self._started:
logger.warning("Channel gateway already started, ignoring duplicate start")
return
load_plugins()
registry = get_registry()
config_manager = ChannelConfigManager()
outbound_registry = OutboundMiddlewareRegistry()
self._outbound_registry = outbound_registry
dispatcher = OutboundDispatcher(
registry=registry,
config_manager=config_manager,
outbound_registry=outbound_registry,
)
self._outbound_dispatcher = dispatcher
outbound_registry.register(BuildMessageMiddleware())
outbound_registry.register(MediaUploadMiddleware())
outbound_registry.register(DowngradeMiddleware())
outbound_registry.register(ChunkMiddleware())
outbound_registry.register(FormatMiddleware())
outbound_registry.register(EnrichMiddleware())
outbound_registry.register(SendMiddleware())
outbound_registry.register(
StatusUpdateMiddleware(update_status=dispatcher._finalize_dispatch)
)
inbound_registry = InboundMiddlewareRegistry()
self._inbound_registry = inbound_registry
router = BindingRouter()
session_manager = SessionManager(router)
redis = await get_redis_client()
identity_resolver = await IdentityLinkResolver.create()
security_checker_registry = SecurityCheckerRegistry()
security_checker_registry.register(AllowlistChecker())
security_checker_registry.register(PairingChecker(PairingManager()))
security_checker_registry.register(BotLoopDetector())
security_checker_registry.register(RateLimiter(redis))
self._security_checker_registry = security_checker_registry
security_policy = SecurityPolicy(security_checker_registry, identity_resolver)
lifecycle_manager = ChannelLifecycleManager(registry, config_manager)
self._lifecycle_manager = lifecycle_manager
dedupe = MessageDeduper()
gateway = ChannelGateway(
registry=registry,
config_manager=config_manager,
session_manager=session_manager,
binding_router=router,
outbound_dispatcher=dispatcher,
security_policy=security_policy,
lifecycle_manager=lifecycle_manager,
inbound_registry=inbound_registry,
dedupe=dedupe,
)
self._gateway = gateway
inbound_registry.register(DedupeMiddleware(dedupe))
inbound_registry.register(SecurityMiddleware(security_policy, dedupe))
inbound_registry.register(TransactionMiddleware(dedupe))
inbound_registry.register(SessionMiddleware(session_manager))
inbound_registry.register(RouteMiddleware(router))
inbound_registry.register(
CreateRunMiddleware(
create_run=gateway.create_agent_run,
record_message=session_manager.record_message,
)
)
# 必须在 lifecycle 启动前注册 handler否则 WebSocket/Polling 类渠道
# 在 start_all() 与 set_message_handler() 之间收到的消息会丢失。
lifecycle_manager.set_message_handler(lambda raw, ct, aid: gateway.on_transport_message(raw, ct, aid))
try:
await security_checker_registry.start_all()
await inbound_registry.start_all()
await lifecycle_manager.start_all()
await dispatcher.start()
await gateway.start_config_change_listener()
app.state.channel_lifecycle_manager = lifecycle_manager
app.state.channel_outbound_dispatcher = dispatcher
app.state.channel_gateway = gateway
self._compensate_task = asyncio.create_task(self._compensate_loop())
self._started = True
except Exception:
logger.exception("Failed to start channel gateway, rolling back")
await self.stop()
raise
async def stop(self) -> None:
"""按依赖顺序关闭多渠道网关相关组件与后台任务。
重复调用或 ``start`` 未成功时调用均为安全空操作;
若 ``start`` 中途失败,也会按实际已创建的组件进行清理。
"""
if (
not self._started
and self._lifecycle_manager is None
and self._gateway is None
and self._outbound_dispatcher is None
and self._security_checker_registry is None
):
return
self._started = False
if self._security_checker_registry is not None:
try:
await self._security_checker_registry.stop_all()
except Exception:
logger.exception("Failed to stop security checker registry")
finally:
self._security_checker_registry = None
if self._inbound_registry is not None:
try:
await self._inbound_registry.stop_all()
except Exception:
logger.exception("Failed to stop inbound middleware registry")
finally:
self._inbound_registry = None
if self._compensate_task is not None:
try:
self._compensate_task.cancel()
try:
await self._compensate_task
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Failed to stop channel compensate task")
finally:
self._compensate_task = None
# gateway.stop() 内部已会关闭 lifecycle避免重复停止。
if self._gateway is not None:
try:
await self._gateway.stop()
except Exception:
logger.exception("Failed to stop channel gateway")
elif self._lifecycle_manager is not None:
try:
await self._lifecycle_manager.stop_all()
except Exception:
logger.exception("Failed to stop channel lifecycle manager")
if self._outbound_dispatcher is not None:
try:
await self._outbound_dispatcher.stop()
except Exception:
logger.exception("Failed to stop channel outbound dispatcher")
self._gateway = None
self._lifecycle_manager = None
self._outbound_dispatcher = None
async def _compensate_loop(self) -> None:
while True:
await asyncio.sleep(60)
try:
await compensate_channel_messages()
except Exception:
logger.exception("Channel compensate loop error")