新增Slack适配器全套核心模块,包括消息处理流水线、会话管理、配置适配、权限控制等完整功能: 1. 新增语音、视觉相关的TTS和图像分析导出接口 2. 实现消息预处理、路由、线程上下文处理的完整流水线 3. 新增账号管理、缓存机制、房间上下文提取功能 4. 支持Webhook和Socket Mode两种事件接收方式 5. 实现权限白名单、审批配对、自动状态管理功能 6. 新增配置迁移、作用域校验、重连策略等辅助模块
145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class SlackTTSConfig:
|
|
enabled: bool = False
|
|
provider: str = "openai"
|
|
voice: str = "alloy"
|
|
speed: float = 1.0
|
|
api_url: str = ""
|
|
api_key: str = ""
|
|
model: str = "tts-1"
|
|
response_format: str = "mp3"
|
|
visible_text_after_voice: bool = True
|
|
|
|
@classmethod
|
|
def from_config(cls, config: dict[str, Any] | None) -> SlackTTSConfig:
|
|
if not config:
|
|
return cls()
|
|
tts_cfg = config.get("tts", {}) or {}
|
|
return cls(
|
|
enabled=tts_cfg.get("enabled", False),
|
|
provider=tts_cfg.get("provider", "openai"),
|
|
voice=tts_cfg.get("voice", "alloy"),
|
|
speed=float(tts_cfg.get("speed", 1.0)),
|
|
api_url=tts_cfg.get("api_url", ""),
|
|
api_key=tts_cfg.get("api_key", ""),
|
|
model=tts_cfg.get("model", "tts-1"),
|
|
response_format=tts_cfg.get("response_format", "mp3"),
|
|
visible_text_after_voice=tts_cfg.get("visible_text_after_voice", True),
|
|
)
|
|
|
|
|
|
async def synthesize_slack_tts(text: str, tts_config: SlackTTSConfig) -> bytes | None:
|
|
if tts_config.provider == "openai":
|
|
return await _synthesize_openai(text, tts_config)
|
|
elif tts_config.provider == "azure":
|
|
return await _synthesize_azure(text, tts_config)
|
|
elif tts_config.provider == "custom":
|
|
return await _synthesize_custom(text, tts_config)
|
|
else:
|
|
logger.warning(f"[SlackTTS] Unknown TTS provider: {tts_config.provider}")
|
|
return None
|
|
|
|
|
|
async def _synthesize_openai(text: str, cfg: SlackTTSConfig) -> bytes | None:
|
|
import aiohttp
|
|
|
|
api_url = cfg.api_url or "https://api.openai.com/v1/audio/speech"
|
|
api_key = cfg.api_key
|
|
|
|
if not api_key:
|
|
logger.warning("[SlackTTS] OpenAI API key not configured")
|
|
return None
|
|
|
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
payload = {
|
|
"model": cfg.model,
|
|
"input": text,
|
|
"voice": cfg.voice,
|
|
"speed": cfg.speed,
|
|
"response_format": cfg.response_format,
|
|
}
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
api_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
return await resp.read()
|
|
logger.warning(f"[SlackTTS] OpenAI TTS returned status {resp.status}: {await resp.text()}")
|
|
return None
|
|
except Exception as e:
|
|
logger.warning(f"[SlackTTS] OpenAI TTS call failed: {e}")
|
|
return None
|
|
|
|
|
|
async def _synthesize_azure(text: str, cfg: SlackTTSConfig) -> bytes | None:
|
|
import aiohttp
|
|
|
|
region = cfg.api_url or "eastasia"
|
|
subscription_key = cfg.api_key
|
|
if not subscription_key:
|
|
logger.warning("[SlackTTS] Azure subscription key not configured")
|
|
return None
|
|
|
|
endpoint = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
|
|
ssml = (
|
|
f'<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="zh-CN">'
|
|
f'<voice name="{cfg.voice}">{text}</voice>'
|
|
f"</speak>"
|
|
)
|
|
headers = {
|
|
"Ocp-Apim-Subscription-Key": subscription_key,
|
|
"Content-Type": "application/ssml+xml",
|
|
"X-Microsoft-OutputFormat": "audio-16khz-128kbitrate-mono-mp3",
|
|
}
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
endpoint, headers=headers, data=ssml.encode(), timeout=aiohttp.ClientTimeout(total=60)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
return await resp.read()
|
|
logger.warning(f"[SlackTTS] Azure TTS returned status {resp.status}")
|
|
return None
|
|
except Exception as e:
|
|
logger.warning(f"[SlackTTS] Azure TTS call failed: {e}")
|
|
return None
|
|
|
|
|
|
async def _synthesize_custom(text: str, cfg: SlackTTSConfig) -> bytes | None:
|
|
import aiohttp
|
|
|
|
if not cfg.api_url:
|
|
logger.warning("[SlackTTS] Custom TTS API URL not configured")
|
|
return None
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
if cfg.api_key:
|
|
headers["Authorization"] = f"Bearer {cfg.api_key}"
|
|
|
|
payload = {"text": text, "voice": cfg.voice, "format": cfg.response_format}
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
cfg.api_url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)
|
|
) as resp:
|
|
if resp.status == 200:
|
|
return await resp.read()
|
|
logger.warning(f"[SlackTTS] Custom TTS returned status {resp.status}")
|
|
return None
|
|
except Exception as e:
|
|
logger.warning(f"[SlackTTS] Custom TTS call failed: {e}")
|
|
return None
|