新增飞书机器人适配器全套功能,包括: - 基础适配器入口与工具导出 - 消息格式化、卡片渲染、回复调度逻辑 - 会话ID生成、模型覆盖策略 - 消息发送缓存、顺序队列管理 - 飞书签名验证、加解密webhook请求 - 审批权限校验、机器人菜单事件处理 - 文档评论、钉消息、语音转码处理 - 静态/动态目录管理、子代理生命周期管理 - 各类工具集:聊天、云盘、文档、知识库API封装
263 lines
7.7 KiB
Python
263 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
import lark_oapi
|
|
|
|
HAS_LARK_SDK = True
|
|
except ImportError:
|
|
HAS_LARK_SDK = False
|
|
lark_oapi = None # type: ignore
|
|
|
|
|
|
@dataclass
|
|
class FeishuTTSConfig:
|
|
enabled: bool = False
|
|
mode: str = "off"
|
|
provider: str = "openai"
|
|
voice: str = "alloy"
|
|
speed: float = 1.0
|
|
auto_tts: str = "off"
|
|
summary_model: str = ""
|
|
api_url: str = ""
|
|
api_key: str = ""
|
|
model: str = "tts-1"
|
|
response_format: str = "mp3"
|
|
|
|
@classmethod
|
|
def from_config(cls, config: dict[str, Any] | None) -> FeishuTTSConfig:
|
|
if not config:
|
|
return cls()
|
|
tts_cfg = config.get("tts", {}) or {}
|
|
return cls(
|
|
enabled=tts_cfg.get("enabled", False),
|
|
mode=tts_cfg.get("mode", "off"),
|
|
provider=tts_cfg.get("provider", "openai"),
|
|
voice=tts_cfg.get("voice", "alloy"),
|
|
speed=float(tts_cfg.get("speed", 1.0)),
|
|
auto_tts=tts_cfg.get("auto", "off"),
|
|
summary_model=tts_cfg.get("summaryModel", ""),
|
|
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"),
|
|
)
|
|
|
|
|
|
async def synthesize_tts(
|
|
text: str,
|
|
*,
|
|
voice: str = "alloy",
|
|
speed: float = 1.0,
|
|
provider: str = "openai",
|
|
api_url: str = "",
|
|
api_key: str = "",
|
|
model: str = "tts-1",
|
|
response_format: str = "mp3",
|
|
) -> bytes | None:
|
|
logger.info("[FeishuTTS] Synthesizing TTS: %d chars, voice=%s, provider=%s", len(text), voice, provider)
|
|
|
|
if provider == "openai":
|
|
return await _synthesize_openai(text, voice, speed, api_url, api_key, model, response_format)
|
|
elif provider == "azure":
|
|
return await _synthesize_azure(text, voice, api_url, api_key)
|
|
elif provider == "custom":
|
|
return await _synthesize_custom(text, voice, speed, api_url, api_key, response_format)
|
|
else:
|
|
logger.warning("[FeishuTTS] Unknown TTS provider: %s", provider)
|
|
return None
|
|
|
|
|
|
async def _synthesize_openai(
|
|
text: str,
|
|
voice: str,
|
|
speed: float,
|
|
api_url: str,
|
|
api_key: str,
|
|
model: str,
|
|
response_format: str,
|
|
) -> bytes | None:
|
|
import aiohttp
|
|
|
|
url = api_url or "https://api.openai.com/v1/audio/speech"
|
|
if not api_key:
|
|
logger.warning("[FeishuTTS] OpenAI API key not configured")
|
|
return None
|
|
|
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
payload = {
|
|
"model": model,
|
|
"input": text,
|
|
"voice": voice,
|
|
"speed": speed,
|
|
"response_format": response_format,
|
|
}
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
url,
|
|
json=payload,
|
|
headers=headers,
|
|
timeout=aiohttp.ClientTimeout(total=60),
|
|
) as resp:
|
|
if resp.status == 200:
|
|
return await resp.read()
|
|
logger.warning("[FeishuTTS] OpenAI TTS returned status %d: %s", resp.status, await resp.text())
|
|
return None
|
|
except Exception as e:
|
|
logger.warning("[FeishuTTS] OpenAI TTS call failed: %s", e)
|
|
return None
|
|
|
|
|
|
async def _synthesize_azure(
|
|
text: str,
|
|
voice: str,
|
|
api_url: str,
|
|
api_key: str,
|
|
) -> bytes | None:
|
|
import aiohttp
|
|
|
|
region = api_url or "eastasia"
|
|
if not api_key:
|
|
logger.warning("[FeishuTTS] 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="{voice}">{text}</voice>'
|
|
f"</speak>"
|
|
)
|
|
headers = {
|
|
"Ocp-Apim-Subscription-Key": api_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("[FeishuTTS] Azure TTS returned status %d", resp.status)
|
|
return None
|
|
except Exception as e:
|
|
logger.warning("[FeishuTTS] Azure TTS call failed: %s", e)
|
|
return None
|
|
|
|
|
|
async def _synthesize_custom(
|
|
text: str,
|
|
voice: str,
|
|
speed: float,
|
|
api_url: str,
|
|
api_key: str,
|
|
response_format: str,
|
|
) -> bytes | None:
|
|
import aiohttp
|
|
|
|
if not api_url:
|
|
logger.warning("[FeishuTTS] Custom TTS API URL not configured")
|
|
return None
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
if api_key:
|
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
|
|
payload = {"text": text, "voice": voice, "speed": speed, "format": 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("[FeishuTTS] Custom TTS returned status %d", resp.status)
|
|
return None
|
|
except Exception as e:
|
|
logger.warning("[FeishuTTS] Custom TTS call failed: %s", e)
|
|
return None
|
|
|
|
|
|
async def send_tts_audio(
|
|
client: Any,
|
|
chat_id: str,
|
|
text: str,
|
|
*,
|
|
tts_config: FeishuTTSConfig | None = None,
|
|
) -> bool:
|
|
cfg = tts_config or FeishuTTSConfig()
|
|
if not cfg.enabled:
|
|
return False
|
|
|
|
audio_data = await synthesize_tts(
|
|
text,
|
|
voice=cfg.voice,
|
|
speed=cfg.speed,
|
|
provider=cfg.provider,
|
|
api_url=cfg.api_url,
|
|
api_key=cfg.api_key,
|
|
model=cfg.model,
|
|
response_format=cfg.response_format,
|
|
)
|
|
if audio_data is None:
|
|
logger.warning("[FeishuTTS] TTS synthesis returned no data")
|
|
return False
|
|
|
|
try:
|
|
from .media import upload_file
|
|
|
|
format_ext = cfg.response_format or "mp3"
|
|
filename = f"tts_audio.{format_ext}"
|
|
file_key = await upload_file(client, audio_data, filename, file_type="opus")
|
|
if not file_key:
|
|
logger.warning("[FeishuTTS] Failed to upload TTS audio file")
|
|
return False
|
|
|
|
import json
|
|
|
|
content = json.dumps({"file_key": file_key}, ensure_ascii=False)
|
|
|
|
receive_id = chat_id
|
|
receive_id_type = "open_id" if chat_id.startswith("ou_") else "chat_id"
|
|
|
|
import lark_oapi as _lark
|
|
|
|
request = (
|
|
_lark.api.im.v1.CreateMessageRequest.builder()
|
|
.receive_id_type(receive_id_type)
|
|
.request_body(
|
|
_lark.api.im.v1.CreateMessageRequestBody.builder()
|
|
.receive_id(receive_id)
|
|
.msg_type("audio")
|
|
.content(content)
|
|
.build()
|
|
)
|
|
.build()
|
|
)
|
|
|
|
resp = await client.im.message.create(request)
|
|
if resp.success():
|
|
logger.info("[FeishuTTS] Audio message sent successfully, msg_id=%s", resp.data.get("message_id", ""))
|
|
return True
|
|
logger.warning("[FeishuTTS] Send audio message failed: %s", resp.msg)
|
|
return False
|
|
except Exception as e:
|
|
logger.warning("[FeishuTTS] Send audio failed: %s", e)
|
|
return False
|