ForcePilot/backend/package/yuxi/channels/adapters/qqbot/interaction.py
Kris 1f78c44b03 refactor: 整理并清理项目中的冗余代码与格式问题
这是一个批量整理提交,包含以下主要改动:
1.  删除多处冗余的空行和未使用的导入
2.  修复文件末尾缺少换行符的问题
3.  调整部分模块的导入顺序与代码排版
4.  修复部分配置默认值与策略逻辑
5.  新增多个功能模块与辅助工具
6.  完善异常处理与日志记录
7.  修复速率限制、消息缓存、权限校验等逻辑bug
8.  废弃部分旧有API与配置项并添加警告提示
2026-05-12 14:51:53 +08:00

186 lines
5.3 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
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)