ForcePilot/backend/package/yuxi/channel/extensions/rocketchat/gateway.py
Kris 043e75d787 feat(channel): 添加 RocketChat 渠道扩展
新增 RocketChat 渠道扩展,支持在 Yuxi 平台中集成 RocketChat 团队协作平台。

包含以下功能模块:
- client: RocketChat API 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- websocket: WebSocket 实时连接
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedup: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- gating: 门控管理
- threading: 线程管理
- reactions: 表情反应
- types: 类型定义
2026-05-21 11:39:24 +08:00

184 lines
7.2 KiB
Python

from __future__ import annotations
import asyncio
import logging
import random
from yuxi.channel.extensions.rocketchat.client import RocketChatClient
from yuxi.channel.extensions.rocketchat.config import (
RocketChatConfigAdapter,
normalize_rocketchat_server_url,
)
from yuxi.channel.extensions.rocketchat.errors import RocketChatError, RocketChatAuthError
from yuxi.channel.extensions.rocketchat.monitor import RocketChatMonitor
from yuxi.channel.extensions.rocketchat.status import RocketChatStatusAdapter
from yuxi.channel.extensions.rocketchat.websocket import RocketChatDDPClient
logger = logging.getLogger(__name__)
GATEWAY_AUTH_BYPASS_PATHS = [
"/api/channels/rocketchat/webhook",
]
class RocketChatGatewayAdapter:
def __init__(self, config_adapter: RocketChatConfigAdapter):
self.config_adapter = config_adapter
self._clients: dict[str, RocketChatClient] = {}
self._ddp_clients: dict[str, RocketChatDDPClient] = {}
self._monitors: dict[str, RocketChatMonitor] = {}
self._tasks: dict[str, asyncio.Task] = {}
self._abort_events: dict[str, asyncio.Event] = {}
self._status_adapters: dict[str, RocketChatStatusAdapter] = {}
self.bot_info: dict[str, dict] = {}
async def start(self, ctx: object) -> object:
account_id = getattr(ctx, "account_id", "default") if ctx else "default"
account = await self.config_adapter.resolve_account(account_id)
if not account.get("auth_token") or not account.get("user_id") or not account.get("server_url"):
logger.warning("Rocket.Chat account %s not configured, skipping", account_id)
return {"status": "not_configured", "account_id": account_id}
server_url = normalize_rocketchat_server_url(account["server_url"])
client = RocketChatClient(
server_url=server_url,
user_id=account["user_id"],
auth_token=account["auth_token"],
)
try:
me = await client.fetch_me()
self.bot_info[account_id] = me
logger.info(
"Rocket.Chat bot %s (%s) connected to %s",
me.get("username", "unknown"),
me.get("_id", "unknown"),
server_url,
)
except RocketChatAuthError as e:
logger.error("Rocket.Chat auth failed for account %s: %s", account_id, e)
await client.close()
return {"status": "auth_failed", "account_id": account_id}
except RocketChatError as e:
logger.error("Rocket.Chat connection failed for account %s: %s", account_id, e)
await client.close()
return {"status": "connection_failed", "account_id": account_id}
self._clients[account_id] = client
self._status_adapters[account_id] = RocketChatStatusAdapter(client, account_id)
monitor = RocketChatMonitor(client, account_id, account)
monitor.bot_user_id = me.get("_id", "")
monitor.bot_username = me.get("username", "")
self._monitors[account_id] = monitor
abort_event = asyncio.Event()
self._abort_events[account_id] = abort_event
async def ddp_connect_loop():
ddp_client = RocketChatDDPClient(
server_url=server_url,
user_id=account["user_id"],
auth_token=account["auth_token"],
on_message=monitor.handle_message,
)
self._ddp_clients[account_id] = ddp_client
delay = 2000
max_delay = 60000
while not abort_event.is_set():
try:
await ddp_client.connect()
delay = 2000
try:
await client.set_user_status("online", "Bot is ready")
logger.info("Rocket.Chat bot status set to online for account %s", account_id)
except RocketChatError as e:
logger.warning("Failed to set bot status to online: %s", e)
await ddp_client.listen(abort_event)
except asyncio.CancelledError:
break
except Exception as e:
if abort_event.is_set():
break
logger.warning("Rocket.Chat DDP connection error: %s. Reconnecting...", e)
delay = min(delay * 2, max_delay)
spread = delay * 0.2
offset = random.uniform(-spread, spread)
jittered = max(1, round(delay + offset))
logger.info("Reconnecting in %d ms", jittered)
await asyncio.sleep(jittered / 1000)
task = asyncio.create_task(ddp_connect_loop())
self._tasks[account_id] = task
return {
"status": "started",
"account_id": account_id,
"server_url": server_url,
"bot_user_id": me.get("_id", ""),
"bot_username": me.get("username", ""),
}
async def stop(self, ctx: object) -> None:
account_id = getattr(ctx, "account_id", "default") if ctx else "default"
abort = self._abort_events.pop(account_id, None)
if abort:
abort.set()
task = self._tasks.pop(account_id, None)
if task:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
ddp = self._ddp_clients.pop(account_id, None)
if ddp:
await ddp.disconnect()
self._monitors.pop(account_id, None)
client = self._clients.pop(account_id, None)
if client:
try:
await client.set_user_status("offline", "Bot is offline")
except Exception:
pass
await client.close()
self._status_adapters.pop(account_id, None)
self.bot_info.pop(account_id, None)
logger.info("Rocket.Chat gateway stopped for account %s", account_id)
def resolve_gateway_auth_bypass_paths(self, config: dict) -> list[str]:
return GATEWAY_AUTH_BYPASS_PATHS
def get_client(self, account_id: str = "default") -> RocketChatClient | None:
return self._clients.get(account_id)
def get_ddp_client(self, account_id: str = "default") -> RocketChatDDPClient | None:
return self._ddp_clients.get(account_id)
def get_monitor(self, account_id: str = "default") -> RocketChatMonitor | None:
return self._monitors.get(account_id)
def get_bot_user_id(self, account_id: str = "default") -> str:
info = self.bot_info.get(account_id, {})
return info.get("_id", "")
def get_bot_username(self, account_id: str = "default") -> str:
info = self.bot_info.get(account_id, {})
return info.get("username", "")
async def handle_webhook(self, request_data: bytes, headers: dict) -> dict | None:
from yuxi.channel.extensions.rocketchat.webhook import handle_rocketchat_webhook
account_id = "default"
account = await self.config_adapter.resolve_account(account_id)
webhook_secret = account.get("webhook_secret", "")
return await handle_rocketchat_webhook(request_data, headers, webhook_secret)