ForcePilot/backend/package/yuxi/channel/extensions/qqbot/message_queue.py
Kris 2ab65f153f feat(channel): 添加 QQ Bot 渠道扩展
新增 QQ Bot 渠道扩展,支持在 Yuxi 平台中集成 QQ 机器人渠道。

包含以下功能模块:
- api_client: QQ API 客户端封装
- api_routes: API 路由管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- websocket: WebSocket 实时连接
- credentials: 凭证管理
- token: Token 管理
- outbound: 外发消息管理
- outbound_media: 媒体外发
- streaming: 流式消息处理
- streaming_media: 媒体流处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- pipeline: 消息管道
- pipeline_stages: 管道阶段
- commands: 指令处理
- commands_builtin: 内置指令
- interaction: 交互处理
- approval: 审批流程
- ark: ARK 消息
- audio: 音频处理
- media: 媒体资源
- media_chunked: 分块媒体
- media_tags: 媒体标签
- message_queue: 消息队列
- delivery: 消息送达确认
- reconnect: 重连机制
- typing_keepalive: 输入状态保活
- group_activation: 群激活
- group_gating: 群门控
- group_history: 群历史
- known_users: 已知用户
- ref_index: 引用索引
- tools: Agent 工具集成
- types: 类型定义
2026-05-21 11:35:12 +08:00

173 lines
5.6 KiB
Python

from __future__ import annotations
import asyncio
import logging
from collections import defaultdict
from typing import Any
from yuxi.channel.extensions.qqbot.types import QQBotChatType, QQBotEventType, QueuedMessage
logger = logging.getLogger(__name__)
class QQBotMessageQueue:
GLOBAL_QUEUE_SIZE = 1000
PER_PEER_QUEUE_SIZE = 20
GROUP_QUEUE_SIZE = 50
MAX_CONCURRENT_USERS = 10
def __init__(self):
self._global_queue: asyncio.Queue[QueuedMessage] = asyncio.Queue(maxsize=self.GLOBAL_QUEUE_SIZE)
self._peer_queues: dict[str, list[QueuedMessage]] = defaultdict(list)
self._active_peers: set[str] = set()
self._semaphore = asyncio.Semaphore(self.MAX_CONCURRENT_USERS)
self._consumer_task: asyncio.Task | None = None
self._handler: Any | None = None
self._cancel_event = asyncio.Event()
def set_handler(self, handler: Any) -> None:
self._handler = handler
async def enqueue(self, msg: QueuedMessage) -> None:
peer_id = self._get_peer_id(msg)
peer_queue = self._peer_queues[peer_id]
max_size = self.GROUP_QUEUE_SIZE if msg.chat_type == QQBotChatType.GROUP else self.PER_PEER_QUEUE_SIZE
if len(peer_queue) >= max_size:
if msg.chat_type == QQBotChatType.GROUP:
bot_msgs = [i for i, m in enumerate(peer_queue) if m.sender_is_bot]
if bot_msgs:
peer_queue.pop(bot_msgs[0])
else:
logger.warning("Group queue full for peer=%s, dropping", peer_id)
return
else:
peer_queue.pop(0)
peer_queue.append(msg)
async def start_consumer(self) -> None:
self._cancel_event.clear()
self._consumer_task = asyncio.create_task(self._consume_loop(), name="qqbot-msg-queue-consumer")
async def stop_consumer(self) -> None:
self._cancel_event.set()
if self._consumer_task and not self._consumer_task.done():
self._consumer_task.cancel()
try:
await self._consumer_task
except asyncio.CancelledError:
pass
self._consumer_task = None
async def _consume_loop(self) -> None:
while not self._cancel_event.is_set():
try:
await self._drain_next_peer()
except Exception:
logger.exception("Message queue consumer error")
async def _drain_next_peer(self) -> None:
ready_peers = [p for p in self._peer_queues if p not in self._active_peers and self._peer_queues[p]]
if not ready_peers:
await asyncio.sleep(0.1)
return
for peer_id in ready_peers:
if peer_id in self._active_peers:
continue
async with self._semaphore:
self._active_peers.add(peer_id)
try:
await self._drain_peer(peer_id)
finally:
self._active_peers.discard(peer_id)
return
await asyncio.sleep(0.1)
async def _drain_peer(self, peer_id: str) -> None:
queue = self._peer_queues.get(peer_id, [])
if not queue:
return
messages: list[QueuedMessage] = []
while queue:
msg = queue.pop(0)
messages.append(msg)
if not messages:
return
if len(messages) > 1 and any(m.chat_type == QQBotChatType.GROUP for m in messages):
messages = self._merge_group_messages(messages)
for msg in messages:
if self._handler:
try:
await self._handler(msg)
except Exception:
logger.exception("Message handler error for msg_id=%s", msg.msg_id)
def _merge_group_messages(self, messages: list[QueuedMessage]) -> list[QueuedMessage]:
control_msgs = []
normal_msgs = []
bot_msgs = []
for msg in messages:
if msg.content.startswith("/"):
control_msgs.append(msg)
elif msg.sender_is_bot:
bot_msgs.append(msg)
else:
normal_msgs.append(msg)
result = control_msgs.copy()
if normal_msgs:
merged = self._merge_messages(normal_msgs)
result.append(merged)
if bot_msgs:
bot_merged = self._merge_messages(bot_msgs)
result.append(bot_merged)
return result
def _merge_messages(self, messages: list[QueuedMessage]) -> QueuedMessage:
if len(messages) == 1:
return messages[0]
base = messages[-1]
contents = []
all_mentions: list[str] = []
all_attachments = []
for m in messages:
name = m.sender_name or m.sender_id
contents.append(f"[{name}]: {m.content}")
all_mentions.extend(m.mentions)
all_attachments.extend(m.attachments)
base.content = "\n".join(contents)
base.mentions = list(dict.fromkeys(all_mentions))
base.attachments = all_attachments
base.sender_is_bot = all(m.sender_is_bot for m in messages)
is_at = any(m.event_type == QQBotEventType.GROUP_AT_MESSAGE_CREATE for m in messages)
if is_at:
base.event_type = QQBotEventType.GROUP_AT_MESSAGE_CREATE
base.merge = {"count": len(messages)}
return base
def _get_peer_id(self, msg: QueuedMessage) -> str:
if msg.chat_type == QQBotChatType.GUILD:
return f"guild:{msg.channel_id}"
if msg.chat_type == QQBotChatType.GROUP:
return f"group:{msg.group_openid}"
return f"dm:{msg.sender_id}"