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

包含以下功能模块:
- client: Mattermost API 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- websocket: WebSocket 实时连接
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedup: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- interactions: 交互处理
- slash_commands: 斜杠指令
- actions: 动作处理
- approval: 审批流程
- delivery: 消息送达确认
- directory: 目录管理
- threading: 线程管理
- gating: 门控管理
- reconnect: 重连机制
- reactions: 表情反应
- media: 媒体资源处理
- model_picker: 模型选择
- types: 类型定义
2026-05-21 11:22:43 +08:00

134 lines
5.1 KiB
Python

from __future__ import annotations
import asyncio
import logging
from yuxi.channel.extensions.mattermost.client import MattermostClient
from yuxi.channel.extensions.mattermost.config import MattermostConfigAdapter, normalize_mattermost_base_url
from yuxi.channel.extensions.mattermost.errors import MattermostError, MattermostAuthError
from yuxi.channel.extensions.mattermost.reconnect import ReconnectManager
from yuxi.channel.extensions.mattermost.status import MattermostStatusAdapter
from yuxi.channel.extensions.mattermost.websocket import MattermostWebSocketMonitor
logger = logging.getLogger(__name__)
GATEWAY_AUTH_BYPASS_PATHS = [
"/api/channels/mattermost/command",
"/api/channels/mattermost/interactions",
]
class MattermostGatewayAdapter:
def __init__(self, config_adapter: MattermostConfigAdapter):
self.config_adapter = config_adapter
self._clients: dict[str, MattermostClient] = {}
self._monitors: dict[str, MattermostWebSocketMonitor] = {}
self._tasks: dict[str, asyncio.Task] = {}
self._abort_events: dict[str, asyncio.Event] = {}
self._status_adapters: dict[str, MattermostStatusAdapter] = {}
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("bot_token") or not account.get("base_url"):
logger.warning("Mattermost account %s not configured, skipping", account_id)
return {"status": "not_configured", "account_id": account_id}
base_url = normalize_mattermost_base_url(account["base_url"])
client = MattermostClient(
base_url=base_url,
bot_token=account["bot_token"],
allow_private_network=account.get("dangerously_allow_private_network", False),
)
try:
me = await client.fetch_me()
self.bot_info[account_id] = me
logger.info(
"Mattermost bot %s (%s) connected to %s",
me.get("username", "unknown"),
me.get("id", "unknown"),
base_url,
)
except MattermostAuthError as e:
logger.error("Mattermost auth failed for account %s: %s", account_id, e)
await client.close()
return {"status": "auth_failed", "account_id": account_id}
except MattermostError as e:
logger.error("Mattermost 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] = MattermostStatusAdapter(client, account_id)
abort_event = asyncio.Event()
self._abort_events[account_id] = abort_event
reconnect_mgr = ReconnectManager(initial_delay_ms=2000, max_delay_ms=60000)
async def ws_connect_loop():
monitor = MattermostWebSocketMonitor(client, account_id)
self._monitors[account_id] = monitor
monitor.bot_user_id = self.bot_info[account_id].get("id", "")
monitor.bot_last_update_at = self.bot_info[account_id].get("update_at", 0)
self._monitors[account_id] = monitor
await reconnect_mgr.run_with_reconnect(
connect_fn=monitor.connect,
abort_event=abort_event,
)
task = asyncio.create_task(ws_connect_loop())
self._tasks[account_id] = task
return {
"status": "started",
"account_id": account_id,
"base_url": base_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
monitor = self._monitors.pop(account_id, None)
if monitor:
await monitor.disconnect()
client = self._clients.pop(account_id, None)
if client:
await client.close()
self._status_adapters.pop(account_id, None)
self.bot_info.pop(account_id, None)
logger.info("Mattermost 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") -> MattermostClient | None:
return self._clients.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", "")