ForcePilot/backend/package/yuxi/channels/adapters/qqbot/rate_limiter.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

88 lines
2.9 KiB
Python

from __future__ import annotations
import asyncio
import logging
import time
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class TokenBucket:
rate: float
burst: int
_tokens: float = field(default=0, init=False)
_last_refill: float = field(default_factory=time.monotonic, init=False)
_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False)
def __post_init__(self):
self._tokens = float(self.burst)
def _refill(self) -> None:
now = time.monotonic()
elapsed = now - self._last_refill
self._tokens = min(self._tokens + elapsed * self.rate, float(self.burst))
self._last_refill = now
async def acquire(self, tokens: float = 1.0) -> None:
while True:
async with self._lock:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return
wait = (tokens - self._tokens) / self.rate if self.rate > 0 else 0.1
await asyncio.sleep(max(wait, 0.01))
def try_acquire(self, tokens: float = 1.0) -> bool:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return True
return False
@dataclass
class RouteRateLimiter:
defaults: dict[str, tuple[float, int]] = field(
default_factory=lambda: {
"send_message": (5.0, 10),
"send_media": (1.0, 3),
"upload_media": (0.5, 2),
"default": (10.0, 20),
}
)
_buckets: dict[str, TokenBucket] = field(default_factory=dict, init=False)
_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False)
async def acquire(self, route: str = "default", tokens: float = 1.0) -> None:
bucket = await self._get_or_create_bucket(route)
await bucket.acquire(tokens)
def try_acquire(self, route: str = "default", tokens: float = 1.0) -> bool:
bucket = self._get_or_create_bucket_sync(route)
return bucket.try_acquire(tokens)
async def _get_or_create_bucket(self, route: str) -> TokenBucket:
async with self._lock:
if route not in self._buckets:
rate, burst = self.defaults.get(route, self.defaults["default"])
self._buckets[route] = TokenBucket(rate=rate, burst=burst)
return self._buckets[route]
def _get_or_create_bucket_sync(self, route: str) -> TokenBucket:
if route not in self._buckets:
rate, burst = self.defaults.get(route, self.defaults["default"])
self._buckets.setdefault(route, TokenBucket(rate=rate, burst=burst))
return self._buckets[route]
def get_stats(self) -> dict[str, dict]:
return {
route: {"tokens": bucket._tokens, "rate": bucket.rate, "burst": bucket.burst}
for route, bucket in self._buckets.items()
}