✨ feat(limit, message): 引入声明式限流系统并增强消息格式化功能 (#1978)

- 新增 Cooldown、RateLimit、ConcurrencyLimit 三种限流依赖
- MessageUtils 支持动态格式化字符串 (format_args 参数)
- 插件CD限制消息显示精确剩余时间

- 重构限流逻辑至 utils/limiters.py,新增时间工具模块
- 整合时间工具函数并优化时区处理
- 新增 limiter_hook 自动释放资源,CooldownError 优化异常处理

- 冷却提示从固定文本改为动态显示剩余时间
- 示例:总结功能冷却中,请等待 1分30秒 后再试~

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: HibiKier <45528451+HibiKier@users.noreply.github.com>
This commit is contained in:
Rumio
2025-07-15 17:13:33 +08:00
committed by GitHub
co-authored by webjoin111 HibiKier
parent d218c569d4
commit b993450a23
9 changed files with 463 additions and 107 deletions
+169 -2
View File
@@ -1,13 +1,181 @@
from typing import Any
from typing import Any, Literal
from nonebot.adapters import Bot, Event
from nonebot.internal.params import Depends
from nonebot.matcher import Matcher
from nonebot.params import Command
from nonebot.permission import SUPERUSER
from nonebot_plugin_session import EventSession
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
from zhenxun.utils.limiters import ConcurrencyLimiter, FreqLimiter, RateLimiter
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.time_utils import TimeUtils
_coolers: dict[str, FreqLimiter] = {}
_rate_limiters: dict[str, RateLimiter] = {}
_concurrency_limiters: dict[str, ConcurrencyLimiter] = {}
def _create_limiter_dependency(
limiter_class: type,
limiter_storage: dict,
limiter_init_args: dict[str, Any],
scope: Literal["user", "group", "global"],
prompt: str,
**kwargs,
):
"""
一个高阶函数,用于创建不同类型的限制器依赖。
参数:
limiter_class: 限制器类 (FreqLimiter, RateLimiter, etc.).
limiter_storage: 用于存储限制器实例的字典.
limiter_init_args: 限制器类的初始化参数.
scope: 限制作用域.
prompt: 触发限制时的提示信息.
**kwargs: 传递给特定限制器逻辑的额外参数.
"""
async def dependency(
matcher: Matcher, session: EventSession, bot: Bot, event: Event
) -> bool:
if await SUPERUSER(bot, event):
return True
handler_id = (
f"{matcher.plugin_name}:{matcher.handlers[0].call.__code__.co_firstlineno}"
)
key: str | None = None
if scope == "user":
key = session.id1
elif scope == "group":
key = session.id3 or session.id2 or session.id1
elif scope == "global":
key = f"global_{handler_id}"
if not key:
return True
if handler_id not in limiter_storage:
limiter_storage[handler_id] = limiter_class(**limiter_init_args)
limiter = limiter_storage[handler_id]
if isinstance(limiter, ConcurrencyLimiter):
await limiter.acquire(key)
matcher.state["_concurrency_limiter_info"] = {
"limiter": limiter,
"key": key,
}
return True
else:
if limiter.check(key):
if isinstance(limiter, FreqLimiter):
limiter.start_cd(
key, kwargs.get("duration_sec", limiter.default_cd)
)
return True
else:
left_time = limiter.left_time(key)
format_kwargs = {
"cd_str": TimeUtils.format_duration(left_time),
**(kwargs.get("prompt_format_kwargs", {})),
}
message = prompt.format(**format_kwargs)
await matcher.finish(message)
return Depends(dependency)
def Cooldown(
duration: str,
*,
scope: Literal["user", "group", "global"] = "user",
prompt: str = "操作过于频繁,请等待 {cd_str}",
) -> bool:
"""声明式冷却检查依赖,限制用户操作频率
参数:
duration: 冷却时间字符串 (e.g., "30s", "10m", "1h")
scope: 冷却作用域
prompt: 自定义的冷却提示消息,可使用 {cd_str} 占位符
返回:
bool: 是否允许执行
"""
try:
parsed_seconds = TimeUtils.parse_time_string(duration)
except ValueError as e:
raise ValueError(f"Cooldown装饰器中的duration格式错误: {e}")
return _create_limiter_dependency(
limiter_class=FreqLimiter,
limiter_storage=_coolers,
limiter_init_args={"default_cd_seconds": parsed_seconds},
scope=scope,
prompt=prompt,
duration_sec=parsed_seconds,
)
def RateLimit(
count: int,
duration: str,
*,
scope: Literal["user", "group", "global"] = "user",
prompt: str = "太快了,在 {duration_str} 内只能触发{limit}次,请等待 {cd_str}",
) -> bool:
"""声明式速率限制依赖,在指定时间窗口内限制操作次数
参数:
count: 在时间窗口内允许的最大调用次数
duration: 时间窗口字符串 (e.g., "1m", "1h")
scope: 限制作用域
prompt: 自定义的提示消息,可使用 {cd_str}, {duration_str}, {limit} 占位符
返回:
bool: 是否允许执行
"""
try:
parsed_seconds = TimeUtils.parse_time_string(duration)
except ValueError as e:
raise ValueError(f"RateLimit装饰器中的duration格式错误: {e}")
return _create_limiter_dependency(
limiter_class=RateLimiter,
limiter_storage=_rate_limiters,
limiter_init_args={"max_calls": count, "time_window": parsed_seconds},
scope=scope,
prompt=prompt,
prompt_format_kwargs={"duration_str": duration, "limit": count},
)
def ConcurrencyLimit(
count: int,
*,
scope: Literal["user", "group", "global"] = "global",
prompt: str | None = "当前功能繁忙,请稍后再试...",
) -> bool:
"""声明式并发数限制依赖,控制某个功能同时执行的实例数量
参数:
count: 最大并发数
scope: 限制作用域
prompt: 提示消息(暂未使用,主要用于未来扩展超时功能)
返回:
bool: 是否允许执行
"""
return _create_limiter_dependency(
limiter_class=ConcurrencyLimiter,
limiter_storage=_concurrency_limiters,
limiter_init_args={"max_concurrent": count},
scope=scope,
prompt=prompt or "",
)
def CheckUg(check_user: bool = True, check_group: bool = True):
@@ -75,7 +243,6 @@ def GetConfig(
if module_:
value = Config.get_config(module_, config, default_value)
if value is None and prompt:
# await matcher.finish(prompt or f"配置项 {config} 未填写!")
await matcher.finish(prompt)
return value