ForcePilot/backend/package/yuxi/channel/extensions/bluebubbles/tts.py
Kris b018e3eda4 feat(bluebubbles): 新增BlueBubbles(iMessage)渠道插件完整实现
该提交新增了完整的BlueBubbles渠道插件,支持通过BlueBubbles Server集成iMessage功能,包含以下核心能力:
1.  支持私聊和群聊会话管理,自动区分会话类型
2.  完整的消息收发支持,包括文本、图片、语音、文件、视频消息
3.  支持消息反应、已读回执、消息编辑与撤回
4.  内置去重、防抖处理机制
5.  支持Webhook和WebSocket两种事件接收方式
6.  完善的权限与安全校验机制
7.  历史消息同步与抓包功能
8.  TTS语音合成与发送支持
9.  群组管理能力,包括重命名、修改头像、增减成员等
2026-05-21 10:40:33 +08:00

74 lines
2.1 KiB
Python

import logging
import os
from pathlib import Path
from yuxi.channel.extensions.bluebubbles.client import BlueBubblesClient
from yuxi.channel.extensions.bluebubbles.outbound import send_attachment
logger = logging.getLogger(__name__)
SUPPORTED_AUDIO_FORMATS = frozenset({"caf", "mp3", "m4a", "wav", "aac"})
PREFERRED_FORMAT = "caf"
MAX_VOICE_MEMO_MB = 5.0
VOICE_MEMO_MIME = "audio/x-caf"
def _audio_file_ext(file_path: str) -> str:
return Path(file_path).suffix.lower().lstrip(".")
def _audio_file_size_mb(file_path: str) -> float:
try:
return os.path.getsize(file_path) / (1024 * 1024)
except OSError:
return 0.0
def validate_voice_input(file_path: str) -> None:
if not os.path.isfile(file_path):
raise FileNotFoundError(f"Voice memo file not found: {file_path}")
ext = _audio_file_ext(file_path)
if ext not in SUPPORTED_AUDIO_FORMATS:
raise ValueError(f"Unsupported audio format '.{ext}'. Supported: {', '.join(sorted(SUPPORTED_AUDIO_FORMATS))}")
size_mb = _audio_file_size_mb(file_path)
if size_mb > MAX_VOICE_MEMO_MB:
raise ValueError(f"Voice memo too large: {size_mb:.1f}MB (max {MAX_VOICE_MEMO_MB}MB)")
if ext != PREFERRED_FORMAT:
logger.warning(
"Voice memo format is '.%s', prefer '.%s' to avoid BlueBubbles Server CAF->MP3 race condition",
ext,
PREFERRED_FORMAT,
)
async def send_voice_memo(
client: BlueBubblesClient,
chat_guid: str,
audio_file_path: str,
*,
private_api_available: bool = False,
) -> dict:
validate_voice_input(audio_file_path)
ext = _audio_file_ext(audio_file_path)
mime_type = "audio/x-caf" if ext == "caf" else f"audio/{ext}"
logger.info(
"Sending iMessage voice memo: chat=%s format=%s size=%.1fMB",
chat_guid,
ext,
_audio_file_size_mb(audio_file_path),
)
return await send_attachment(
client,
chat_guid,
audio_file_path,
mime_type=mime_type,
as_voice=True,
private_api_available=private_api_available,
)