Repository navigation
Description
操作系统
Windows 11(与操作系统无关,任何平台均可复现)
Python 版本
3.12.7
NoneBot 版本
2.5.0(PyPI 最新版;master 分支 nonebot/message.py L387 代码一致,问题同样存在)
适配器
无(直接调用 nonebot.message._check_matcher,与适配器无关)
协议端
无
描述问题
Matcher.new(expire_time=...) 的类型注解允许传入 datetime,带时区的 aware datetime 也是自然用法(例如 datetime.now(timezone.utc) + timedelta(minutes=5),或从数据库/API 读出的带 tzinfo 的过期时刻)。但 _check_matcher 的过期判断:
if Matcher.expire_time and datetime.now() > Matcher.expire_time: # message.py L387用 naive 的 datetime.now() 与 aware 的 expire_time 比较,直接抛出:
TypeError: can't compare offset-naive and offset-aware datetimes
三个后果叠加:
- 该 TypeError 位于任何 try 保护之外(下面的
check_perm/check_rule才有 try 包裹); - 异常沿
check_and_run_matcher→handle_event的任务组被捕获并记录,** matcher 不会被destroy()** —— 永久留在 matcher 列表中; - 临时响应器的 priority 为 0,
handle_event按 priority 从小到大检查,每个上报事件(不限群、不限类型)都会最先检查它并重复抛出同一 TypeError —— 日志持续刷屏,且该会话既不匹配也永不过期。
对照:同文件同一函数中,naive 的过期 matcher 会被正确 destroy();naive 的未过期 matcher 正常通过检查。即 naive 路径完全正常,仅 aware 输入崩溃。
复现步骤
完整最小脚本(无需适配器,pip install nonebot2[fastapi] 后即可运行):
import asyncio
from datetime import datetime, timedelta, timezone
import nonebot
nonebot.init()
from nonebot.matcher import Matcher, matchers
from nonebot.message import _check_matcher
def destroyed(m) -> bool:
return not any(m in lst for lst in matchers.values())
async def main():
# 对照组: naive expire_time —— 一切正常
naive_m = Matcher.new("message", expire_time=datetime.now() + timedelta(hours=1),
temp=True, priority=1, block=True)
await _check_matcher(Matcher=naive_m, bot=None, event=None, state={},
stack=None, dependency_cache=None)
print("naive 未过期: 销毁了吗?", destroyed(naive_m), "(False = 正常保留)")
# 实验组: aware expire_time —— 类型注解允许的输入
aware_m = Matcher.new("message",
expire_time=datetime.now(timezone.utc) + timedelta(hours=1),
temp=True, priority=0, block=True)
try:
await _check_matcher(Matcher=aware_m, bot=None, event=None, state={},
stack=None, dependency_cache=None)
except TypeError as e:
print("aware: TypeError:", e)
print("aware: matcher 已销毁?", destroyed(aware_m), "(True 才是预期)")
asyncio.run(main())实际输出(本机实测):
[naive] _check_matcher 正常返回 | matcher 已销毁(过期即删): False (False = 正常保留)
[aware] TypeError: can't compare offset-naive and offset-aware datetimes
[aware] matcher 已销毁? False <- 预期应销毁
再对照已过期的 naive matcher:destroy() 被正确调用(从 matcher 列表移除)——证明销毁逻辑本身正常,仅 aware 输入的_compare 抛错导致既不销毁也不匹配。
在真实进程中,handle_event 会在每个事件的任务组里捕获该 TypeError(表现为每个事件一条 "Error when checking Matcher." 错误日志),该临时 matcher(priority=0)永远占据检查首位。
期望的结果
aware datetime 应被正确处理——比较时统一时区(例如利用 expire_time 自身的 tzinfo):
if Matcher.expire_time and (
datetime.now(tz=Matcher.expire_time.tzinfo) > Matcher.expire_time
):tzinfo=None 时 datetime.now(tz=None) 即 naive now,与现有 naive 行为完全兼容;aware 时按其自身时区比较,两种输入都正确过期并销毁。
截图或日志
复现脚本实测输出(关键部分):
[naive] _check_matcher 正常返回 | matcher 已销毁(过期即删): False (False = 正常保留)
[aware] TypeError: can't compare offset-naive and offset-aware datetimes
[aware] matcher 已销毁? False <- 预期应销毁
[expired-naive] matcher 已销毁(过期即删): True
(附:matcher 已销毁(过期即删): False 一行中,naive 未过期的 False 是"正常保留";aware 行的 False 是"应销毁未销毁"。)
Activity