ForcePilot/backend/package/yuxi/channels/adapters/qqbot/interaction.py
Kris 552aef767c feat(qqbot): 实现QQ机器人适配器完整功能模块
新增QQ Bot适配器完整代码栈,包含:
1. 基础适配器入口与工具类封装
2. 会话管理、重试队列与流量控制
3. 命令系统与内置指令(ping/help/status等)
4. 富媒体消息处理与格式转换
5. 引用存储与审批管理
6. 凭证备份与会话持久化
7. 健康检查与交互回调系统
2026-05-12 00:48:04 +08:00

186 lines
5.3 KiB
Python

from __future__ import annotations
import asyncio
import hashlib
import logging
import time
from collections.abc import Callable, Awaitable
from dataclasses import dataclass, field
from typing import Any
logger = logging.getLogger(__name__)
@dataclass
class InteractionContext:
interaction_id: str
interaction_type: str
chat_type: str
chat_id: str
user_id: str
user_name: str
data: dict[str, Any]
msg_id: str = ""
timestamp: float = field(default_factory=time.time)
InteractionCallback = Callable[[InteractionContext], Awaitable[bool]]
class InteractionRegistry:
def __init__(self):
self._callbacks: dict[str, InteractionCallback] = {}
def register(self, action_id: str, callback: InteractionCallback) -> None:
self._callbacks[action_id] = callback
logger.debug("InteractionRegistry: registered %s", action_id)
def unregister(self, action_id: str) -> None:
self._callbacks.pop(action_id, None)
async def dispatch(self, ctx: InteractionContext) -> bool:
if not ctx.interaction_id:
return False
for action_id, callback in self._callbacks.items():
if ctx.interaction_id == action_id or ctx.interaction_id.startswith(action_id):
try:
return await callback(ctx)
except Exception:
logger.exception("InteractionRegistry: callback failed for %s", action_id)
return False
class InteractionBuilder:
@staticmethod
def make_confirm_button(
action_id: str,
label: str = "确认",
style: int = 1,
) -> dict:
return {
"type": 2,
"style": style,
"label": label,
"data": action_id,
}
@staticmethod
def make_action_row(buttons: list[dict]) -> dict:
return {"type": 1, "components": buttons}
@staticmethod
def make_select_menu(
action_id: str,
placeholder: str = "请选择",
options: list[dict] | None = None,
min_values: int = 1,
max_values: int = 1,
) -> dict:
return {
"type": 3,
"custom_id": action_id,
"placeholder": placeholder,
"options": options or [],
"min_values": min_values,
"max_values": max_values,
}
@staticmethod
def make_modal(
action_id: str,
title: str,
fields: list[dict],
) -> dict:
return {
"type": 4,
"custom_id": action_id,
"title": title,
"components": fields,
}
@dataclass
class InteractionHandler:
interaction_id: str
handler_type: str
chat_id: str
user_id: str
created_at: float = field(default_factory=time.time)
expires_at: float = 300.0
resolved: bool = False
result: Any = None
def __post_init__(self):
self.expires_at = self.created_at + 300.0
class InteractionSessionManager:
def __init__(self, max_sessions: int = 1000):
self._sessions: dict[str, InteractionHandler] = {}
self._max_sessions = max_sessions
self._lock = asyncio.Lock()
async def create(
self,
handler_type: str,
chat_id: str,
user_id: str,
ttl: float = 300.0,
) -> InteractionHandler:
async with self._lock:
self._cleanup_expired()
if len(self._sessions) >= self._max_sessions:
oldest = min(
self._sessions.values(),
key=lambda h: h.created_at,
default=None,
)
if oldest:
self._sessions.pop(oldest.interaction_id, None)
iid = hashlib.sha256(f"{chat_id}:{user_id}:{handler_type}:{time.time()}".encode()).hexdigest()[:16]
handler = InteractionHandler(
interaction_id=iid,
handler_type=handler_type,
chat_id=chat_id,
user_id=user_id,
)
handler.expires_at = time.time() + ttl
self._sessions[iid] = handler
return handler
async def get(self, interaction_id: str) -> InteractionHandler | None:
async with self._lock:
handler = self._sessions.get(interaction_id)
if handler is None:
return None
if time.time() >= handler.expires_at:
self._sessions.pop(interaction_id, None)
return None
return handler
async def resolve(self, interaction_id: str, result: Any = None) -> bool:
async with self._lock:
handler = self._sessions.get(interaction_id)
if handler is None:
return False
handler.resolved = True
handler.result = result
self._sessions.pop(interaction_id, None)
return True
async def cancel(self, interaction_id: str) -> bool:
async with self._lock:
if interaction_id in self._sessions:
self._sessions.pop(interaction_id, None)
return True
return False
def _cleanup_expired(self) -> None:
now = time.time()
expired = [iid for iid, h in self._sessions.items() if now >= h.expires_at]
for iid in expired:
self._sessions.pop(iid, None)