ForcePilot/backend/package/yuxi/channel/extensions/mattermost/media.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

120 lines
3.5 KiB
Python

from __future__ import annotations
import logging
from pathlib import Path
import httpx
from yuxi.channel.extensions.mattermost.client import MattermostClient
from yuxi.channel.extensions.mattermost.errors import MattermostError
logger = logging.getLogger(__name__)
class MattermostMediaAdapter:
MAX_UPLOAD_MB = 50
def __init__(self, client: MattermostClient):
self.client = client
async def upload(
self,
channel_id: str,
file_path: str | Path,
mime_type: str | None = None,
filename: str | None = None,
) -> dict | None:
path = Path(file_path)
if not path.is_file():
logger.error("File not found: %s", file_path)
return None
if path.stat().st_size > self.MAX_UPLOAD_MB * 1024 * 1024:
logger.error("File too large: %s", file_path)
return None
data = path.read_bytes()
fname = filename or path.name
mt = mime_type or _guess_mime_type(path)
try:
return await self.client.upload_file(channel_id, data, fname, mt)
except MattermostError as e:
logger.error("Failed to upload file %s: %s", fname, e)
return None
async def upload_bytes(
self,
channel_id: str,
data: bytes,
filename: str,
mime_type: str = "application/octet-stream",
) -> dict | None:
if len(data) > self.MAX_UPLOAD_MB * 1024 * 1024:
logger.error("Data too large (%d bytes)", len(data))
return None
try:
return await self.client.upload_file(channel_id, data, filename, mime_type)
except MattermostError as e:
logger.error("Failed to upload: %s", e)
return None
async def download(self, file_id: str) -> bytes | None:
try:
return await self.client.get_file(file_id)
except MattermostError as e:
logger.error("Failed to download file %s: %s", file_id, e)
return None
async def download_to_path(self, file_id: str, dest_path: str | Path) -> bool:
data = await self.download(file_id)
if data is None:
return False
Path(dest_path).write_bytes(data)
return True
async def download_url(
self,
media_url: str,
timeout: float = 30.0,
) -> bytes | None:
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout)) as client:
response = await client.get(media_url)
response.raise_for_status()
return response.content
except Exception as e:
logger.error("Failed to download URL %s: %s", media_url, e)
return None
_MIME_MAP = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".svg": "image/svg+xml",
".pdf": "application/pdf",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".txt": "text/plain",
".csv": "text/csv",
".json": "application/json",
".xml": "application/xml",
".zip": "application/zip",
".mp4": "video/mp4",
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
}
def _guess_mime_type(path: Path) -> str:
suffix = path.suffix.lower()
return _MIME_MAP.get(suffix, "application/octet-stream")