新增QQ Bot适配器完整代码栈,包含: 1. 基础适配器入口与工具类封装 2. 会话管理、重试队列与流量控制 3. 命令系统与内置指令(ping/help/status等) 4. 富媒体消息处理与格式转换 5. 引用存储与审批管理 6. 凭证备份与会话持久化 7. 健康检查与交互回调系统
186 lines
5.3 KiB
Python
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)
|