ForcePilot/backend/package/yuxi/channels/adapters/qqbot/interaction.py
Kris ef5483dc1a refactor(qqbot): 重构QQ机器人适配器代码,优化多项功能与结构
主要变更:
1. 修复速率限流器使用setdefault替代重复创建令牌桶
2. 重构交互注册表匹配逻辑,优化精确匹配查找
3. 重构去重缓存逻辑,移到适配器实例方法
4. 重构发送URL解析,增加合法性校验并拆分公共方法
5. 优化流式消息处理逻辑,简化flush_controller调用
6. 重构群聊类型判断代码,简化语法
7. 修复重连管理器对None类型关闭分类的处理
8. 新增消息缓存、线程模拟器、发送初始化模块
9. 重构凭证备份与会话存储逻辑,支持环境变量指定状态目录
10. 新增配置提示与向导二维码绑定功能
11. 优化媒体上传逻辑,增加重试机制与缓存
12. 新增审批键盘模板构建函数
13. 重构消息格式处理,修正媒体发送字段与长度限制
14. 修复令牌过期时间计算,使用time.time替代monotonic
15. 新增群组激活缓冲区与用户追踪器增强功能
16. 修复换行符问题,统一文件结尾格式
2026-05-13 16:13:48 +08:00

186 lines
5.2 KiB
Python

from __future__ import annotations
import asyncio
import hashlib
import logging
import time
from collections.abc import Awaitable, Callable
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
callback = self._callbacks.get(ctx.interaction_id)
if callback:
try:
return await callback(ctx)
except Exception:
logger.exception("InteractionRegistry: callback failed for %s", ctx.interaction_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)