ForcePilot/backend/package/yuxi/channel/interfaces/websocket/manager.py
Kris 9a8a27bf36 feat(channel): 新增渠道网关模块完整实现
本次提交新增了完整的多渠道消息网关系统,包括:
1. 支持飞书、钉钉、Web、Hook 四种渠道的适配器与配置
2. 领域模型层:消息、会话、绑定、出箱等核心实体
3. 应用服务层:管道、中间件、DTO 与业务逻辑
4. 基础设施层:持久化、过滤器、队列等端口实现
5. 接口层:REST API、SSE、WebSocket 通信端点
6. 前端页面与路由配置,添加渠道管理菜单
7. 新增相关依赖包与 docker-compose 部署配置
2026-05-30 21:53:09 +08:00

76 lines
2.7 KiB
Python

from __future__ import annotations
import asyncio
import logging
from uuid import uuid4
from yuxi.channel.application.service.inbound_service import InboundService
from yuxi.channel.domain.port.channel_adapter_port import ChannelAdapterPort
from yuxi.channel.domain.port.metrics_port import MetricsPort
from yuxi.channel.domain.port.ws_connection_port import WsConnectionPort
logger = logging.getLogger(__name__)
class WsConnectionManager:
def __init__(self, metrics: MetricsPort | None = None) -> None:
self._connections: dict[str, WsConnectionPort] = {}
self._adapters: dict[str, ChannelAdapterPort] = {}
self._loop: asyncio.AbstractEventLoop | None = None
self._inbound_service: InboundService | None = None
self._metrics = metrics
def register(self, connection: WsConnectionPort) -> None:
self._connections[connection.channel_type] = connection
def set_adapters(self, adapters: dict[str, ChannelAdapterPort]) -> None:
self._adapters = adapters
async def start_all(
self,
loop: asyncio.AbstractEventLoop,
inbound_service: InboundService,
) -> None:
self._loop = loop
self._inbound_service = inbound_service
if self._connections:
await asyncio.gather(
*(conn.start(loop, self._on_message) for conn in self._connections.values()),
return_exceptions=True,
)
async def stop_all(self) -> None:
for conn in self._connections.values():
try:
await conn.stop()
except Exception:
logger.warning("ws stop failed for %s", conn.channel_type)
async def _on_message(self, raw: dict) -> None:
channel_type = raw.get("channel_type", "")
adapter = self._adapters.get(channel_type)
if not adapter:
logger.warning("no adapter for ws message from %s", channel_type)
return
try:
message = await adapter.receive_message(raw)
except Exception:
logger.exception("ws receive_message failed for %s", channel_type)
return
trace_id = raw.get("header", {}).get("event_id", str(uuid4()))
message.metadata["trace_id"] = trace_id
message.metadata["source"] = "websocket"
message.metadata["idempotency_key"] = trace_id
if self._inbound_service:
try:
await self._inbound_service.submit(message, channel_type=channel_type, trace_id=trace_id)
except Exception:
logger.exception("ws inbound submit failed for %s", channel_type)
@property
def connections_status(self) -> dict[str, bool]:
return {ct: conn.is_connected for ct, conn in self._connections.items()}