迪拉熊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
Section titled “SkipLog”SkipLog 是行为日志机制专属的控制流异常,定义在 util/database/behavior_log.py:
class SkipLog(Exception): """抛出此异常以跳过行为日志记录。仅用于被 regist 装饰的 handler 中 表达"本次调用未执行实质工作,不应记为有效调用"。"""- 继承自
Exception(非NoneBotException),因为它是项目级关注点而非框架控制流。 - 仅被
regist装饰器的 wrapper 捕获,不会逃逸到 NoneBot。 - 若意外逃逸(装饰器 bug),会被当作普通异常上报,可见于错误日志。
装饰器工作原理
Section titled “装饰器工作原理”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() 改 .send() 时必须加 return
Section titled “.finish() 改 .send() 时必须加 return”.finish() 会抛出异常,其后的代码原本不可达。若将成功路径的 .finish() 改为 .send(),必须补上 return,否则会 fall through 到原本不可达的代码,导致 TypeError 等错误。
# ❌ 错误:缺少 return,会 fall throughmatch 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"] 会 TypeErrorsong_info["id"]
# ✅ 正确:补上 returnmatch 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()不要用裸 return 做早退
Section titled “不要用裸 return 做早退”被 regist 装饰的 handler 中,如果因前置条件不满足需要提前退出,必须用 raise SkipLog 而非裸 return。裸 return 会被装饰器当作正常返回,从而误记为一次有效调用。
# ❌ 错误:裸 return 会被记录为有效调用if not match: return
# ✅ 正确:raise SkipLog 跳过记录if not match: raise SkipLog.finish() 仅用于失败
Section titled “.finish() 仅用于失败”.finish() 会抛出 FinishedException,装饰器不会捕获它,因此 .finish() 路径一律不记录。成功路径不得使用 .finish(),否则该次成功调用不会被记录。成功路径应使用 .send() + 正常返回(或函数自然结束)。