跳转到内容

行为日志

行为日志记录机制与 handler 编写约定

迪拉熊Bot 通过 behavior_logging 装饰器记录用户对各项功能的使用情况,用于使用量统计与活跃度分析。该机制仅记录成功执行的调用——即 handler 确实向用户交付了有意义数据的调用。

源码位于 util/database/behavior_log.py

@behavior_logging.regist(command_type, subcommand) 装饰的 handler 有三种结束路径,每种对应不同的记录行为:

路径 写法 装饰器视角 是否记录
成功 .send() 后正常返回(或函数自然结束) 函数正常返回 ✅ 记录
失败 .finish() 发送错误消息 FinishedException 向上逃逸 ❌ 不记录
早退 raise SkipLog 装饰器捕获 SkipLog ❌ 不记录

handler 完成了用户请求的操作,并向用户交付了有意义的数据(成绩卡片、查询结果、操作确认等)。使用 UniMessage.send() 发送消息后正常返回即可,装饰器会在函数返回后自动记录。

@foo.handle()
@behavior_logging.regist("module", "foo")
async def _(event: MessageEvent):
# ... 业务逻辑 ...
await UniMessage.text(result).send(at_sender=True)
# 函数自然结束 → 装饰器记录本次调用

handler 无法完成用户请求(未找到数据、无权限、未配置、结果过多、数据源报错等),向用户发送错误提示。使用 UniMessage.finish() 发送消息——.finish() 会抛出 FinishedException,装饰器不会捕获该异常,因此不会记录。

if not data:
msg = (
"迪拉熊没有找到相关信息mai~",
Image(path=resource_path.REACTIONS / "struggle.png", sticker=True),
)
await UniMessage(msg).finish(at_sender=True)
# .finish() 抛出 FinishedException → 不记录

handler 因前置条件不满足而根本未执行实质工作(正则未匹配、参数缺失、非目标事件类型等)。此时应抛出 SkipLog 异常,装饰器捕获后跳过记录。

from util.database.behavior_log import behavior_logging, SkipLog
@foo.handle()
@behavior_logging.regist("module", "foo")
async def _(event: MessageEvent):
match = re.fullmatch(r"...", event.get_plaintext())
if not match:
raise SkipLog # 正则未匹配,非有效调用 → 不记录
# ... 后续业务逻辑 ...

判定一个路径属于“成功”还是“失败”的核心标准:handler 是否向用户交付了有具体意义的数据

  • 交付了数据(哪怕是不完全的结果列表)→ 成功 → .send() + 返回
  • 未交付数据(没找到、没权限、没配置、太多无法展示、数据源报错)→ 失败 → .finish()
  • 根本未执行实质工作(正则没匹配、参数缺失)→ 早退 → raise SkipLog

SkipLog 是行为日志机制专属的控制流异常,定义在 util/database/behavior_log.py

class SkipLog(Exception):
"""抛出此异常以跳过行为日志记录。仅用于被 regist 装饰的 handler 中
表达"本次调用未执行实质工作,不应记为有效调用"。"""
  • 继承自 Exception(非 NoneBotException),因为它是项目级关注点而非框架控制流。
  • 仅被 regist 装饰器的 wrapper 捕获,不会逃逸到 NoneBot。
  • 若意外逃逸(装饰器 bug),会被当作普通异常上报,可见于错误日志。

regist 装饰器在函数执行记录日志,仅当函数正常返回时记录:

def regist(self, command_type, subcommand):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except SkipLog:
return # 早退 → 不记录
# 正常返回 → 记录
event = self._extract_event(args, kwargs)
if event is not None and hasattr(event, "get_user_id"):
try:
# ... add_log ...
except Exception as e:
logger.warning(f"记录行为日志失败: {e!r}")
return wrapper
return decorator

这意味着:

  • 成功(.send() + 返回)→ 记录 ✅
  • 失败(.finish()FinishedException)→ 异常逃逸,不记录 ✅
  • 早退(raise SkipLog)→ 捕获,不记录 ✅
  • 崩溃(意外异常)→ 异常逃逸,不记录 ✅

.finish() 会抛出异常,其后的代码原本不可达。若将成功路径的 .finish() 改为 .send()必须补上 return,否则会 fall through 到原本不可达的代码,导致 TypeError 等错误。

# ❌ 错误:缺少 return,会 fall through
match song_info:
case set():
msg = f"迪拉熊找到了这些乐曲——\r\n{'\r\n'.join(song_info)}"
await UniMessage.text(msg).send()
case None:
...
await UniMessage(msg).finish()
# ↓ song_info 是 set,访问 ["id"] 会 TypeError
song_info["id"]
# ✅ 正确:补上 return
match song_info:
case set():
msg = f"迪拉熊找到了这些乐曲——\r\n{'\r\n'.join(song_info)}"
await UniMessage.text(msg).send()
return
case None:
...
await UniMessage(msg).finish()

regist 装饰的 handler 中,如果因前置条件不满足需要提前退出,必须用 raise SkipLog 而非裸 return。裸 return 会被装饰器当作正常返回,从而误记为一次有效调用。

# ❌ 错误:裸 return 会被记录为有效调用
if not match:
return
# ✅ 正确:raise SkipLog 跳过记录
if not match:
raise SkipLog

.finish() 会抛出 FinishedException,装饰器不会捕获它,因此 .finish() 路径一律不记录。成功路径不得使用 .finish(),否则该次成功调用不会被记录。成功路径应使用 .send() + 正常返回(或函数自然结束)。