新增 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: 类型定义
134 lines
5.1 KiB
Python
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", "")
|