ForcePilot/backend/package/yuxi/channel/extensions/bluebubbles/debounce.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.6 KiB
Python

import asyncio
from collections import defaultdict
from yuxi.channel.message.models import UnifiedMessage
class DebounceManager:
def __init__(self, window_ms: int = 2500, coalesce_dms: bool = False):
self.window_ms = window_ms
self.coalesce_dms = coalesce_dms
self._pending: dict[str, list[UnifiedMessage]] = defaultdict(list)
self._timers: dict[str, asyncio.Task] = {}
self._dm_buffers: dict[str, tuple[float, list[UnifiedMessage]]] = {}
self._lock = asyncio.Lock()
def _dm_key(self, msg: UnifiedMessage) -> str:
return f"dm:{msg.sender.id}"
def _group_key(self, msg: UnifiedMessage) -> str:
gid = msg.group.id if msg.group else "unknown"
return f"group:{gid}:{msg.sender.id}"
def _debounce_key(self, msg: UnifiedMessage) -> str:
if self.coalesce_dms and not msg.group:
return self._dm_key(msg)
return self._group_key(msg)
async def enqueue(self, msg: UnifiedMessage, on_dispatch: callable):
key = self._debounce_key(msg)
async with self._lock:
self._pending[key].append(msg)
if key in self._timers and not self._timers[key].done():
self._timers[key].cancel()
self._timers[key] = asyncio.create_task(self._flush_after_delay(key, on_dispatch))
async def _flush_after_delay(self, key: str, on_dispatch: callable):
await asyncio.sleep(self.window_ms / 1000.0)
await self.flush_key(key, on_dispatch)
async def flush_key(self, key: str, on_dispatch: callable):
async with self._lock:
messages = self._pending.pop(key, [])
if key in self._timers:
self._timers.pop(key, None)
if not messages:
return
if self.coalesce_dms and key.startswith("dm:"):
merged = self._coalesce_dm_messages(messages)
await on_dispatch(merged)
else:
last = messages[-1]
await on_dispatch(last)
def _coalesce_dm_messages(self, messages: list[UnifiedMessage]) -> UnifiedMessage:
if len(messages) <= 1:
return messages[0]
combined = " ".join(m.content for m in messages if m.content)
all_urls = []
for m in messages:
all_urls.extend(m.media_urls)
result = messages[-1]
result.content = combined
result.media_urls = all_urls
return result
async def flush_all(self, on_dispatch: callable):
keys = list(self._pending.keys())
for key in keys:
await self.flush_key(key, on_dispatch)