[Refactor] 重构至 Nonebot#34
Conversation
Walkthrough将运行时从 botpy 迁移到 NoneBot:新增 NoneBot 及插件依赖,移除 botpy 客户端/命令/调度实现,改为 NoneBot 初始化、适配器注册与插件化模块(on_command),并引入统一的 reply/report_exception 消息层与 APScheduler 插件。 Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant NoneBot
participant Plugin as Plugin (maintain/cron/interact/...)
participant Adapter as OneBot/QQ Adapter
participant Scheduler as APScheduler
User->>NoneBot: 发送命令或消息
NoneBot->>Plugin: 触发 on_command 处理器
Plugin->>NoneBot: 调用 reply(...) / report_exception(...)
NoneBot->>Adapter: 通过适配器发送消息(群/私聊)
Scheduler->>Plugin: 定时触发任务(heart_beat / peeper_daily_update)
Plugin->>Adapter: 发送定时消息至目标(群/私聊)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~35 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
requirements.txt (1)
19-24: 可选:将“强固定 ==”改为“兼容固定 ~=”以便接收补丁更新当前全部依赖用
==强固定,有利于可重复构建,但增加了安全与兼容维护成本(无法自动获得兼容的补丁修复)。若你的发布流程没有 lock 文件(如 pip-tools/uv lock),建议在 NoneBot 生态依赖上使用~=放宽到补丁级别。示例变更如下(保持当前小版本主线,允许补丁升级):
-nonebot2[all]==2.4.3 -nonebot-adapter-qq==1.6.5 -nonebot-adapter-onebot==2.4.6 -nonebot-plugin-send-anything-anywhere==0.7.1 -nonebot-plugin-apscheduler==0.5.0 -nonebot-plugin-localstore==0.7.4 +nonebot2==2.4.3 +nonebot-adapter-qq~=1.6.5 +nonebot-adapter-onebot~=2.4.6 +nonebot-plugin-send-anything-anywhere~=0.7.1 +nonebot-plugin-apscheduler~=0.5.0 +nonebot-plugin-localstore~=0.7.4若你决定保留
==,建议引入 lock 工具(如 pip-tools/uv)统一锁定整个解析结果,减少隐式不一致。
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
requirements.txt(1 hunks)
🔇 Additional comments (3)
requirements.txt (3)
20-20: 避免对 qq-botpy 的“双重固定”造成的隐式冲突
nonebot-adapter-qq通常对qq-botpy有自己的版本范围。当前文件同时固定了qq-botpy==1.2.1(第 9 行,未在本段 diff 中)和nonebot-adapter-qq==1.6.5,一旦两者范围不一致会产生安装冲突或运行时问题。建议:
- 若项目未直接使用
qq-botpy,考虑移除顶层的qq-botpy==1.2.1让适配器决定具体版本;- 若确实直接使用,请确认该版本满足适配器要求。
我可以帮你在仓库内检索是否存在直接引用 botpy 的代码以决策是否移除顶层依赖。
18-18: imgkit 需要系统级依赖,请在部署说明中同步补充
imgkit依赖系统安装wkhtmltoimage/wkhtmltopdf可执行文件。仅在 Python 层固定版本并不足以保证功能正常。建议在 Dockerfile/部署文档中明确安装步骤与版本,避免运行时找不到可执行文件。我可以协助补充 README/Dockerfile 安装段落;请确认目标运行环境(OS/架构)后告知。
19-24: 确认 NoneBot 2.4.x 及各插件依赖兼容性
- nonebot2 2.4.3 与 nonebot-adapter-onebot 2.4.6 属于同期小版本,无已知冲突;请本地通过 pip show/pip check 核对 Pydantic 与 FastAPI/Starlette 的主版本一致性。
- nonebot-adapter-qq 1.6.5 要求 qq-botpy ≥1.2.0,<1.3.0,项目中固定的 qq-botpy==1.2.1 符合该范围。
- nonebot-plugin-send-anything-anywhere 0.7.1 的必选依赖为 anyio、filetype、nonebot2 (>=2.3.0,<3.0.0)、pydantic (!=2.5.0,!=2.5.1,>=1.10.0,<3.0.0) 及 strenum;nonebot-plugin-alconna 并非必装,仅在需 Alconna 集成功能时才引入。
- nonebot-plugin-apscheduler 0.5.0 要求 APScheduler ≥3.7.0,<4.0.0,APScheduler==3.10.4 在支持范围内。
- nonebot-plugin-localstore 0.7.4 无额外第三方依赖,仅需与 nonebot2 保持同一大版本即可。
依上述核对后,当前依赖版本组合合理,可放心安装。
+ cron.py 定时模块,包含原每日更新榜单和 (Onebot Only) 心跳模块 + maintain.py 杂项,主要用于腾讯官方机器人获取用户 ID 进行加权操作等。
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (15)
pyproject.toml (1)
1-17: 文件末尾缺少换行符根据 POSIX 标准,文本文件应以换行符结尾。建议在文件末尾添加一个换行符。
[tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" +src/module/cron.py (2)
18-18: SQLAlchemy 作业存储配置缺少空格
add_jobstore方法的参数格式不规范,建议在参数之间添加空格以提高可读性。-scheduler.add_jobstore("sqlalchemy",alias="default",url=f"sqlite:///{os.path.join(data_dir,'jobs.sqlite')}") +scheduler.add_jobstore("sqlalchemy", alias="default", url=f"sqlite:///{os.path.join(data_dir, 'jobs.sqlite')}")
24-27: 定时任务参数格式不一致这些定时任务装饰器的参数格式不一致,有些缺少空格。建议统一格式以提高可读性。
-@scheduler.scheduled_job("cron",hour=6,id="job_ping_6",kwargs={'content':"ping",'id':Constants.role_conf['heartbeat_id']}) -@scheduler.scheduled_job("cron",hour=12,id="job_ping_12",kwargs={'content':"pong",'id':Constants.role_conf['heartbeat_id']}) -@scheduler.scheduled_job("cron",hour=18,id="job_ping_18",kwargs={'content':"pingping",'id':Constants.role_conf['heartbeat_id']}) -@scheduler.scheduled_job("cron",hour=0,id="job_ping_0",kwargs={'content':"pongpong",'id':Constants.role_conf['heartbeat_id']}) +@scheduler.scheduled_job("cron", hour=6, id="job_ping_6", kwargs={'content': "ping", 'id': Constants.role_conf['heartbeat_id']}) +@scheduler.scheduled_job("cron", hour=12, id="job_ping_12", kwargs={'content': "pong", 'id': Constants.role_conf['heartbeat_id']}) +@scheduler.scheduled_job("cron", hour=18, id="job_ping_18", kwargs={'content': "pingping", 'id': Constants.role_conf['heartbeat_id']}) +@scheduler.scheduled_job("cron", hour=0, id="job_ping_0", kwargs={'content': "pongpong", 'id': Constants.role_conf['heartbeat_id']})src/core/bot/message.py (2)
11-11: 函数参数格式不一致参数列表中缺少空格,建议统一格式。
-async def reply(contents:list[str | bytes], event:Event, modal_words: bool = True, finish: bool = True): +async def reply(contents: list[str | bytes], event: Event, modal_words: bool = True, finish: bool = True):
22-23: 变量命名不一致使用
images作为列表名,但texts使用单数形式。建议保持一致性。- images:list[Image] = [] - texts = Text("") + images: list[Image] = [] + text = Text("")相应地更新后续使用
texts的地方为text。src/module/maintain.py (5)
10-10: 命令参数格式不一致建议在参数之间添加空格以提高可读性。
-reload_conf = on_command("reload_conf", aliases={"配置重载"}, permission=SUPERUSER, rule=to_me(),priority=0,block=True) +reload_conf = on_command("reload_conf", aliases={"配置重载"}, permission=SUPERUSER, rule=to_me(), priority=0, block=True)
12-12: 函数参数缺少空格-async def reply_reload_conf(event:Event): +async def reply_reload_conf(event: Event):
15-15: 参数格式不一致- await reply(["配置文件已重载"],event, modal_words=False, finish=True) + await reply(["配置文件已重载"], event, modal_words=False, finish=True)
17-17: 命令参数格式不一致-chat_scene_id = on_command("chat_scene_id", aliases={"对话场景ID"}, rule=to_me(),priority=5,block=True) +chat_scene_id = on_command("chat_scene_id", aliases={"对话场景ID"}, rule=to_me(), priority=5, block=True)
22-22: 命令参数格式不一致-my_id = on_command("my_id", aliases={"我的ID"}, rule=to_me(),priority=5,block=True) +my_id = on_command("my_id", aliases={"我的ID"}, rule=to_me(), priority=5, block=True)src/module/tool/how_to_cook.py (2)
1-14: 清理未使用的导入并统一风格(逗号后空格)Ruff/CodeQL 指出 os、AggregatedMessageFactory、Text、SAAImage 未使用;同时建议在 import 列表中逗号后加空格,提升一致性。
-import os import random @@ -from nonebot_plugin_saa import MessageFactory, AggregatedMessageFactory, Text -from nonebot_plugin_saa import Image as SAAImage +from nonebot_plugin_saa import MessageFactory @@ -from src.core.util.tools import png2jpg,fuzzy_matching +from src.core.util.tools import png2jpg, fuzzy_matching
47-47: 移除无占位符的 f 前缀f-string 未使用占位符,建议去掉 f 以消除静态检查告警。
- await reply([f"发送消息时发生错误,请联系管理员排障"], event, finish=True) + await reply(["发送消息时发生错误,请联系管理员排障"], event, finish=True)robot.py (1)
1-1: 文件疑似包含 UTF-8 BOM首行前存在 BOM 符号的迹象(零宽不可见字符)。建议以 UTF-8 无 BOM 保存,避免在某些环境中引发解析问题。
src/module/interact.py (2)
6-6: 移除未使用的导入:Command静态检查标注未使用。删除以保持整洁。
-from nonebot.params import CommandArg, Command +from nonebot.params import CommandArg
13-13: 移除未使用的导入:check_is_int未被使用,建议删除。
-from src.core.util.tools import png2jpg, get_simple_qrcode, check_intersect, get_today_timestamp_range, check_is_int +from src.core.util.tools import png2jpg, get_simple_qrcode, check_intersect, get_today_timestamp_range
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
pyproject.toml(1 hunks)robot.py(1 hunks)src/core/bot/interact.py(0 hunks)src/core/bot/message.py(1 hunks)src/core/constants.py(2 hunks)src/core/util/tools.py(2 hunks)src/module/cron.py(1 hunks)src/module/interact.py(1 hunks)src/module/maintain.py(1 hunks)src/module/tool/how_to_cook.py(1 hunks)
💤 Files with no reviewable changes (1)
- src/core/bot/interact.py
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/module/cron.py (2)
src/core/constants.py (1)
Constants(76-194)src/module/cp/peeper.py (1)
daily_update_job(103-111)
src/module/maintain.py (2)
src/core/bot/message.py (1)
reply(11-37)src/core/constants.py (2)
Constants(76-194)reload_conf(192-194)
robot.py (1)
src/core/constants.py (1)
Constants(76-194)
src/core/bot/message.py (1)
src/core/constants.py (1)
Constants(76-194)
src/module/tool/how_to_cook.py (6)
src/core/constants.py (1)
Constants(76-194)src/core/bot/message.py (1)
reply(11-37)src/core/util/tools.py (2)
png2jpg(213-221)fuzzy_matching(223-264)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/data/data_how_to_cook.py (1)
load_dishes(9-21)src/render/html/render_how_to_cook.py (1)
render_how_to_cook(22-42)
src/module/interact.py (16)
src/core/bot/decorator.py (1)
get_all_modules_info(69-78)src/core/bot/message.py (1)
reply(11-37)src/core/constants.py (1)
Constants(76-194)src/core/util/tools.py (5)
png2jpg(213-221)get_simple_qrcode(300-304)check_intersect(338-339)get_today_timestamp_range(292-293)check_is_int(171-176)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/platform/online/atcoder.py (1)
AtCoder(15-207)src/platform/online/codeforces.py (1)
Codeforces(23-683)src/platform/online/nowcoder.py (1)
NowCoder(13-316)src/render/pixie/render_about.py (1)
AboutRenderer(121-150)src/render/pixie/render_contest_list.py (1)
ContestListRenderer(244-282)src/render/pixie/render_help.py (1)
HelpRenderer(155-185)src/platform/model.py (1)
get_contest_list(89-108)src/core/bot/interact.py (1)
reply_recent_contests(127-184)src/module/cp/nk.py (1)
send_contest(45-53)src/module/cp/cf.py (1)
send_contest(177-185)src/module/cp/atc.py (2)
reply_atc_request(87-133)send_contest(75-83)
🪛 Ruff (0.12.2)
src/core/bot/message.py
3-3: nonebot.adapters.onebot.v11.PrivateMessageEvent imported but unused
Remove unused import
(F401)
3-3: nonebot.adapters.onebot.v11.GroupMessageEvent imported but unused
Remove unused import
(F401)
src/module/tool/how_to_cook.py
1-1: os imported but unused
Remove unused import: os
(F401)
9-9: nonebot_plugin_saa.AggregatedMessageFactory imported but unused
Remove unused import
(F401)
9-9: nonebot_plugin_saa.Text imported but unused
Remove unused import
(F401)
10-10: nonebot_plugin_saa.Image imported but unused
Remove unused import: nonebot_plugin_saa.Image
(F401)
23-23: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
47-47: f-string without any placeholders
Remove extraneous f prefix
(F541)
src/module/interact.py
6-6: nonebot.params.Command imported but unused
Remove unused import: nonebot.params.Command
(F401)
13-13: src.core.util.tools.check_is_int imported but unused
Remove unused import: src.core.util.tools.check_is_int
(F401)
32-32: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
38-38: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
99-99: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🪛 GitHub Check: CodeQL
src/core/bot/message.py
[notice] 3-3: Unused import
Import of 'PrivateMessageEvent' is not used.
Import of 'GroupMessageEvent' is not used.
src/module/tool/how_to_cook.py
[notice] 1-1: Unused import
Import of 'os' is not used.
[notice] 9-9: Unused import
Import of 'AggregatedMessageFactory' is not used.
Import of 'Text' is not used.
[notice] 10-10: Unused import
Import of 'SAAImage' is not used.
src/module/interact.py
[notice] 6-6: Unused import
Import of 'Command' is not used.
[notice] 13-13: Unused import
Import of 'check_is_int' is not used.
🔇 Additional comments (7)
src/core/constants.py (1)
6-6: 日志记录器迁移到 NoneBot 看起来正确从
botpy.logging迁移到nonebot.logger符合整体的 NoneBot 重构方向。src/core/util/tools.py (1)
22-22: 确认依赖 thefuzz 已声明
- 在
requirements*.txt第15行已声明thefuzz==0.22.1- 如需进一步提升性能,可考虑将依赖改为
thefuzz[speedup]==0.22.1并锁定版本,以避免运行时 ImportError 及确保一致性src/module/tool/how_to_cook.py (1)
23-23: 关于 CommandArg() 作为缺省参数的提示Ruff B008 在 NoneBot 插件中属误报。NoneBot 官方推荐以依赖注入的方式声明参数(示例:message: Message = CommandArg())。此处可忽略该告警。
如需压制,可在配置中对特定规则或路径进行忽略;也可在本文件顶部添加针对 B008 的逐文件忽略注释(不推荐全局忽略)。
robot.py (1)
7-7: 确保 superusers 始终为 set[str]:NoneBot 要求 superusers 为 set[str],如果直接传入字符串会被拆成单字符集合,或传入 int 会导致类型错误。建议在 robot.py 中对
Constants.role_conf['admin_id']做统一的类型检查和转换:-# 初始化 NoneBot -nonebot.init(superusers=Constants.role_conf['admin_id'], command_sep={' '}) +# 初始化 NoneBot +admins = Constants.role_conf.get('admin_id', []) +if isinstance(admins, str): + admins_set = {admins} +elif isinstance(admins, (list, tuple, set)): + admins_set = set(map(str, admins)) +else: + admins_set = {str(admins)} +nonebot.init(superusers=admins_set, command_sep={' '})请验证
config.json(或其它配置来源)中role.admin_id的实际类型,确保上述转换逻辑能够正确覆盖字符串、数字、列表等情况。
—src/module/interact.py (3)
32-32: 关于 CommandArg() 作为缺省参数的提示Ruff B008 在 NoneBot 插件中属误报。该写法符合 NoneBot 依赖注入用法,可忽略。
38-38: 关于 CommandArg() 作为缺省参数的提示同上,属误报,可忽略。
99-99: 关于 CommandArg() 作为缺省参数的提示同上,属误报,可忽略。
| def png2jpg(path: str, remove_origin: bool = True) -> bytes: | ||
| img = Image.open(path) | ||
| new_path = os.path.splitext(path)[0] + '.jpg' | ||
| img.convert('RGB').save(new_path) | ||
| if remove_origin: | ||
| os.remove(path) | ||
| return new_path | ||
| with open(new_path, 'rb') as f: | ||
| image_bytes = f.read() | ||
| return image_bytes | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
png2jpg 存在不必要的磁盘写入与资源释放问题,建议改为内存转换并确保关闭句柄
当前实现会写入临时 JPG 文件、再读回为 bytes,且 Image 未使用上下文管理器,可能导致资源未及时释放与磁盘膨胀。可直接在内存中完成 PNG->JPG 转换,并在需要时删除源文件。
建议替换为内存转换实现(并确保可选删除源文件):
-def png2jpg(path: str, remove_origin: bool = True) -> bytes:
- img = Image.open(path)
- new_path = os.path.splitext(path)[0] + '.jpg'
- img.convert('RGB').save(new_path)
- if remove_origin:
- os.remove(path)
- with open(new_path, 'rb') as f:
- image_bytes = f.read()
- return image_bytes
+def png2jpg(path: str, remove_origin: bool = True) -> bytes:
+ from io import BytesIO
+ with Image.open(path) as img:
+ buffer = BytesIO()
+ img.convert('RGB').save(buffer, format='JPEG', quality=85)
+ image_bytes = buffer.getvalue()
+ if remove_origin and os.path.exists(path):
+ os.remove(path)
+ return image_bytes需要在文件顶部增加依赖导入:
from io import BytesIO🤖 Prompt for AI Agents
In src/core/util/tools.py around lines 213 to 222, png2jpg currently writes a
temporary JPG to disk and doesn't use context managers for PIL Image, causing
unnecessary disk I/O and potential resource leaks; change it to open the source
with a with Image.open(path) as img: context, convert to RGB, write the JPG into
an in-memory BytesIO buffer (save buffer as 'JPEG'), call buffer.getvalue() to
return bytes, optionally os.remove(path) if remove_origin is True, and ensure
the BytesIO and image are closed; also add from io import BytesIO to the file
imports.
| def fuzzy_matching(query: list | dict, target: str, query_idx: int): | ||
| """ | ||
| 模糊匹配,支持下标查询 | ||
| :param query: 匹配列表 | ||
| :param target: 匹配目标文本 | ||
| :param query_idx: 匹配结果的第几个 | ||
| :return [匹配程度文本, 提示文本, 选取的对象] | ||
| """ | ||
| if len(query) == 0: | ||
| raise(ValueError("查询列表为空")) | ||
| if query_idx == 0: | ||
| raise(ValueError("查询从 1 开始编号")) | ||
|
|
||
| match_results = process.extract(target, query, limit=5) | ||
| if isinstance(query, dict): | ||
| # 传递 dict 时会返回 tuple(value, ratio, key) | ||
| picked_tuple = [(result[2], result[1]) for result in match_results if result[1] >= 20] # 相似度至少 20% | ||
| else: | ||
| picked_tuple = [(result[0], result[1]) for result in match_results if result[1] >= 20] | ||
| if len(picked_tuple) == 0: | ||
| raise(ValueError("没有找到满足条件的匹配项")) | ||
| if query_idx > len(picked_tuple): | ||
| raise(ValueError(f"查询编号多余候选数量,当前有 {len(picked_tuple)} 个候选项")) | ||
| picked, ratio = picked_tuple[query_idx - 1] | ||
| query_more_tip = f"\n标签匹配度 {ratio}%" | ||
| if query_idx != 1: | ||
| query_more_tip += f",在 {len(picked_tuple)} 个候选中排名第 {query_idx} " | ||
| else: | ||
| query_more_tip += f",共有 {len(picked_tuple)} 个候选" | ||
| if len(picked_tuple) > 1: | ||
| query_more_tip += ",可以在指令后追加编号参数查询更多" | ||
|
|
||
| if ratio >= 95: # 简单的评价反馈 | ||
| query_tag = "完美满足条件的" | ||
| elif ratio >= 60: | ||
| query_tag = "满足条件的" | ||
| elif ratio >= 35: | ||
| query_tag = "比较满足条件的" | ||
| else: | ||
| query_tag = "可能不太满足条件的" | ||
|
|
||
| return [query_tag, query_more_tip, picked] | ||
|
|
There was a problem hiding this comment.
修正 dict 输入场景下 thefuzz.process.extract 返回元组顺序的误用,及 query_idx 允许负数的问题
- thefuzz 在 choices 为 dict 时,返回 (key, score, value),当前代码按 (value, score, key) 解读,picked 将被错误地设为 value 而非 key。
- 允许 query_idx 为负数会导致 Python 负索引取末尾项,违背“从 1 开始编号”的设计。
建议如下修复(同时将 query_idx 校验调整为 < 1 即报错):
- if len(query) == 0:
- raise(ValueError("查询列表为空"))
- if query_idx == 0:
- raise(ValueError("查询从 1 开始编号"))
+ if len(query) == 0:
+ raise ValueError("查询列表为空")
+ if query_idx < 1:
+ raise ValueError("查询从 1 开始编号")
match_results = process.extract(target, query, limit=5)
if isinstance(query, dict):
- # 传递 dict 时会返回 tuple(value, ratio, key)
- picked_tuple = [(result[2], result[1]) for result in match_results if result[1] >= 20] # 相似度至少 20%
+ # dict 情况下返回 (key, ratio, value)
+ picked_tuple = [(result[0], result[1]) for result in match_results if result[1] >= 20] # 相似度至少 20%
else:
picked_tuple = [(result[0], result[1]) for result in match_results if result[1] >= 20]
if len(picked_tuple) == 0:
- raise(ValueError("没有找到满足条件的匹配项"))
+ raise ValueError("没有找到满足条件的匹配项")
if query_idx > len(picked_tuple):
- raise(ValueError(f"查询编号多余候选数量,当前有 {len(picked_tuple)} 个候选项"))
+ raise ValueError(f"查询编号多余候选数量,当前有 {len(picked_tuple)} 个候选项")如需进一步提升可读性与易用性,建议:
- 返回类型标注调整为 tuple[str, str, T](或为 list 保持当前结构,但统一 dict 的 picked 语义为 key),并在文档注明 dict 情况下返回的 picked 为 key。
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def fuzzy_matching(query: list | dict, target: str, query_idx: int): | |
| """ | |
| 模糊匹配,支持下标查询 | |
| :param query: 匹配列表 | |
| :param target: 匹配目标文本 | |
| :param query_idx: 匹配结果的第几个 | |
| :return [匹配程度文本, 提示文本, 选取的对象] | |
| """ | |
| if len(query) == 0: | |
| raise(ValueError("查询列表为空")) | |
| if query_idx == 0: | |
| raise(ValueError("查询从 1 开始编号")) | |
| match_results = process.extract(target, query, limit=5) | |
| if isinstance(query, dict): | |
| # 传递 dict 时会返回 tuple(value, ratio, key) | |
| picked_tuple = [(result[2], result[1]) for result in match_results if result[1] >= 20] # 相似度至少 20% | |
| else: | |
| picked_tuple = [(result[0], result[1]) for result in match_results if result[1] >= 20] | |
| if len(picked_tuple) == 0: | |
| raise(ValueError("没有找到满足条件的匹配项")) | |
| if query_idx > len(picked_tuple): | |
| raise(ValueError(f"查询编号多余候选数量,当前有 {len(picked_tuple)} 个候选项")) | |
| picked, ratio = picked_tuple[query_idx - 1] | |
| query_more_tip = f"\n标签匹配度 {ratio}%" | |
| if query_idx != 1: | |
| query_more_tip += f",在 {len(picked_tuple)} 个候选中排名第 {query_idx} " | |
| else: | |
| query_more_tip += f",共有 {len(picked_tuple)} 个候选" | |
| if len(picked_tuple) > 1: | |
| query_more_tip += ",可以在指令后追加编号参数查询更多" | |
| if ratio >= 95: # 简单的评价反馈 | |
| query_tag = "完美满足条件的" | |
| elif ratio >= 60: | |
| query_tag = "满足条件的" | |
| elif ratio >= 35: | |
| query_tag = "比较满足条件的" | |
| else: | |
| query_tag = "可能不太满足条件的" | |
| return [query_tag, query_more_tip, picked] | |
| def fuzzy_matching(query: list | dict, target: str, query_idx: int): | |
| """ | |
| 模糊匹配,支持下标查询 | |
| :param query: 匹配列表 | |
| :param target: 匹配目标文本 | |
| :param query_idx: 匹配结果的第几个 | |
| :return [匹配程度文本, 提示文本, 选取的对象] | |
| """ | |
| if len(query) == 0: | |
| raise ValueError("查询列表为空") | |
| if query_idx < 1: | |
| raise ValueError("查询从 1 开始编号") | |
| match_results = process.extract(target, query, limit=5) | |
| if isinstance(query, dict): | |
| # dict 情况下返回 (key, ratio, value) | |
| picked_tuple = [ | |
| (result[0], result[1]) | |
| for result in match_results | |
| if result[1] >= 20 | |
| ] # 相似度至少 20% | |
| else: | |
| picked_tuple = [ | |
| (result[0], result[1]) | |
| for result in match_results | |
| if result[1] >= 20 | |
| ] | |
| if len(picked_tuple) == 0: | |
| raise ValueError("没有找到满足条件的匹配项") | |
| if query_idx > len(picked_tuple): | |
| raise ValueError(f"查询编号多余候选数量,当前有 {len(picked_tuple)} 个候选项") | |
| picked, ratio = picked_tuple[query_idx - 1] | |
| query_more_tip = f"\n标签匹配度 {ratio}%" | |
| if query_idx != 1: | |
| query_more_tip += f",在 {len(picked_tuple)} 个候选中排名第 {query_idx} " | |
| else: | |
| query_more_tip += f",共有 {len(picked_tuple)} 个候选" | |
| if len(picked_tuple) > 1: | |
| query_more_tip += ",可以在指令后追加编号参数查询更多" | |
| if ratio >= 95: # 简单的评价反馈 | |
| query_tag = "完美满足条件的" | |
| elif ratio >= 60: | |
| query_tag = "满足条件的" | |
| elif ratio >= 35: | |
| query_tag = "比较满足条件的" | |
| else: | |
| query_tag = "可能不太满足条件的" | |
| return [query_tag, query_more_tip, picked] |
🤖 Prompt for AI Agents
In src/core/util/tools.py around lines 223-265, the fuzz matching logic
misinterprets thefuzz.process.extract tuple order for dict choices and allows
negative query_idx; change the dict branch to extract key=result[0] and
score=result[1] (so picked is the key, not the value), adjust the non-dict
branch to continue using result[0] and result[1], and tighten the index
validation to "if query_idx < 1" (reject zero and negatives) and keep the
existing upper-bound check; optionally update the function return/type docs to
clarify that when query is a dict the returned picked is the dict key.
| @scheduler.scheduled_job("cron", id="peeper_daily_update", hour=0, minute=0, second=1) | ||
| async def peeper_daily_update(): | ||
| daily_update_job() |
There was a problem hiding this comment.
🛠️ Refactor suggestion
异步函数调用同步函数可能导致阻塞
peeper_daily_update 是异步函数,但直接调用了同步函数 daily_update_job()。如果 daily_update_job() 执行时间较长,可能会阻塞事件循环。
建议使用 asyncio.to_thread 或 run_in_executor 来避免阻塞:
+import asyncio
+
@scheduler.scheduled_job("cron", id="peeper_daily_update", hour=0, minute=0, second=1)
async def peeper_daily_update():
- daily_update_job()
+ await asyncio.to_thread(daily_update_job)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @scheduler.scheduled_job("cron", id="peeper_daily_update", hour=0, minute=0, second=1) | |
| async def peeper_daily_update(): | |
| daily_update_job() | |
| import asyncio | |
| @scheduler.scheduled_job("cron", id="peeper_daily_update", hour=0, minute=0, second=1) | |
| async def peeper_daily_update(): | |
| await asyncio.to_thread(daily_update_job) |
🤖 Prompt for AI Agents
In src/module/cron.py around lines 20 to 22, the async scheduled function
peeper_daily_update directly calls the synchronous daily_update_job(), which can
block the event loop; change the call to run the sync work off the loop (e.g.
await asyncio.to_thread(daily_update_job) or use loop.run_in_executor) or
convert daily_update_job to an async function and await it; ensure the scheduler
function awaits the offloaded task and handles/propagates exceptions
appropriately.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (8)
src/core/bot/message.py (2)
3-5: 删除未使用的导入
PrivateMessageEvent和GroupMessageEvent被导入但未使用,应该删除以保持代码整洁。
40-43: 修复函数签名和异步调用问题函数存在两个问题:
- 函数签名与之前评论中指出的问题相同,
self参数不应存在- 异步函数
reply没有被 await,可能导致未处理的协程-def report_exception(event:Event,module_name: str, e: Exception): +async def report_exception(event:Event, module_name: str, e: Exception): Constants.log.warning(f"[obot-module] 操作失败,模块 {module_name} 出现异常") Constants.log.exception(f"[obot-module] {e}") - reply([handle_exception(e)], event, modal_words=False, finish=True) + await reply([handle_exception(e)], event, modal_words=False, finish=True)src/module/tool/how_to_cook.py (2)
24-26: 修复控制流问题当菜谱库为空时,代码发送回复但没有停止执行,导致后续代码仍会运行并可能抛出异常。
if len(dishes) == 0: await reply(["抱歉,菜谱库为空,请踢一踢管理员"], event, finish=True) + return
40-40: 优化缓存文件处理
png2jpg调用中传入False参数会保留源 PNG 文件,可能导致磁盘空间浪费。- await reply([f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}",png2jpg(f"{cached_prefix}.png",False)], event, finish=True) + await reply([f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}",png2jpg(f"{cached_prefix}.png")], event, finish=True)src/module/interact.py (4)
32-32: 修复函数参数默认值中的函数调用与
how_to_cook.py中的问题相同,不应在参数默认值中调用函数。但考虑到这是 NoneBot v2 的依赖注入模式,这个警告可能是框架要求的写法。Also applies to: 38-38, 99-99
66-71: 添加异常处理防止平台查询失败当前代码直接解包
get_contest_list()的返回值,如果某个平台返回 None 或抛出异常,会导致整个查询流程崩溃。running_contests, upcoming_contests, finished_contests = [], [], [] for platform in queries: - running, upcoming, finished = platform.get_contest_list() - running_contests.extend(running) - upcoming_contests.extend(upcoming) - finished_contests.extend(finished) + try: + result = platform.get_contest_list() + except Exception as e: + Constants.log.exception(f"[contest] 获取 {platform.platform_name} 列表失败: {e}") + continue + if not result: + Constants.log.warning(f"[contest] {platform.platform_name} 返回空结果,已跳过") + continue + running, upcoming, finished = result + running_contests.extend(running) + upcoming_contests.extend(upcoming) + finished_contests.extend(finished)
101-107: 修复二维码生成的控制流问题当内容为空时,代码发送提示但继续执行,会生成空内容的二维码。
if len(content) == 0: - await reply(["请提供需要生成二维码的内容"], event) + await reply(["请提供需要生成二维码的内容"], event, finish=True) + return
128-131: 修复睡觉命令的逻辑错误当前代码无条件发送"早上好",与注释描述的"可能延时发送"不符。应该只在回复"晚安"时才发送后续消息。
chosen_action = random.choice(actions) await reply([chosen_action], event, modal_words=False, finish=chosen_action != "晚安") - await asyncio.sleep(random.randint(30, 120)) - await reply(["早上好!"], event, modal_words=False, finish=True) + if chosen_action == "晚安": + await asyncio.sleep(random.randint(30, 120)) + await reply(["早上好!"], event, modal_words=False, finish=True)
🧹 Nitpick comments (1)
src/module/tool/how_to_cook.py (1)
45-45: 移除多余的 f-string 前缀字符串中没有占位符,不需要 f-string。
- await reply([f"发送消息时发生错误,请联系管理员排障"], event, finish=True) + await reply(["发送消息时发生错误,请联系管理员排障"], event, finish=True)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/core/bot/message.py(1 hunks)src/module/interact.py(1 hunks)src/module/tool/how_to_cook.py(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/core/bot/message.py (2)
src/core/constants.py (1)
Constants(76-194)src/core/util/exception.py (1)
handle_exception(31-40)
src/module/tool/how_to_cook.py (6)
src/core/constants.py (1)
Constants(76-194)src/core/bot/message.py (1)
reply(11-37)src/core/util/tools.py (2)
png2jpg(213-221)fuzzy_matching(223-264)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/data/data_how_to_cook.py (1)
load_dishes(9-21)src/render/html/render_how_to_cook.py (1)
render_how_to_cook(22-42)
🪛 Ruff (0.12.2)
src/module/interact.py
32-32: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
38-38: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
99-99: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
src/module/tool/how_to_cook.py
21-21: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
45-45: f-string without any placeholders
Remove extraneous f prefix
(F541)
🔇 Additional comments (3)
src/core/bot/message.py (1)
11-37: LGTM! 消息处理逻辑设计良好
reply函数的实现很好地适配了不同的事件类型,支持混合内容(文本和图片),逻辑清晰且功能完整。src/module/tool/how_to_cook.py (1)
21-21: 忽略关于在参数默认值中使用 CommandArg() 的警告NoneBot v2 的参数注入机制中,
Message = CommandArg()属于官方推荐的标记注入写法,框架会在调用时按优先级注入对应对象,无需改为在函数体内再调用。
- 位置:
src/module/tool/how_to_cook.py中reply_how_to_cook的message: Message = CommandArg()参数声明Likely an incorrect or invalid review comment.
src/module/interact.py (1)
1-153: 整体架构设计良好代码从旧的 botpy 架构成功迁移到 NoneBot v2,命令处理逻辑清晰,功能模块化程度高。异步处理和错误处理的设计思路正确,只需要修复上述提到的具体问题。
|
|
||
| from nonebot import on_command | ||
| from nonebot.adapters import Message, Event | ||
| from nonebot.exception import ActionFailed, MatcherException |
Check notice
Code scanning / CodeQL
Unused import Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 11 months ago
To fix the problem, the unused import of ActionFailed should be removed from the import statement on line 5. Only MatcherException should remain imported from nonebot.exception, as it is the only symbol used in the code. This change should be made in the file src/module/tool/how_to_cook.py, specifically on line 5.
| @@ -2,7 +2,7 @@ | ||
|
|
||
| from nonebot import on_command | ||
| from nonebot.adapters import Message, Event | ||
| from nonebot.exception import ActionFailed, MatcherException | ||
| from nonebot.exception import MatcherException | ||
| from nonebot.params import CommandArg | ||
| from nonebot.rule import to_me | ||
| from nonebot_plugin_saa import MessageFactory |
|
Caution An unexpected error occurred while opening a pull request: Repository rule violations found Changes must be made through a pull request. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
src/module/tool/how_to_cook.py (2)
42-44: 建议优化文件缓存策略当前 PNG 文件在转换为 JPG 后仍保留在磁盘上(
remove_origin=False),可能导致缓存目录累积大量无用文件。建议删除源 PNG 文件或使用内存流处理图片,减少磁盘 I/O:
- await reply( - [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png", False)], event, - finish=True) + await reply( + [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png", True)], event, + finish=True)
26-27: 菜谱库为空时应立即返回当前代码在菜谱库为空时发送消息后继续执行,会导致后续代码访问空列表时出错。
在发送提示消息后应立即返回:
if len(dishes) == 0: await reply(["抱歉,菜谱库为空,请踢一踢管理员"], event, finish=True) + returnsrc/module/interact.py (3)
73-77: 添加平台查询的异常处理
get_contest_list()可能返回None或抛出异常,直接解包会导致程序崩溃。添加异常处理和空值检查:
for platform in queries: - running, upcoming, finished = platform.get_contest_list() - running_contests.extend(running) - upcoming_contests.extend(upcoming) - finished_contests.extend(finished) + try: + result = platform.get_contest_list() + if result is None: + Constants.log.warning(f"[contest] {platform.platform_name} 返回空结果") + continue + running, upcoming, finished = result + running_contests.extend(running) + upcoming_contests.extend(upcoming) + finished_contests.extend(finished) + except Exception as e: + Constants.log.exception(f"[contest] 获取 {platform.platform_name} 列表失败: {e}") + continue
110-112: 生成空二维码的逻辑问题当前在提示缺少内容后继续执行,会生成空内容的二维码。
在发送提示消息后应立即返回:
if len(content) == 0: - await reply(["请提供需要生成二维码的内容"], event) + await reply(["请提供需要生成二维码的内容"], event, finish=True) + return
139-142: 睡觉命令的逻辑与注释不符当前实现总是在延迟后发送"早上好!",即使选择的不是"晚安"。根据注释应该是"可能"发送。
修改逻辑,仅在回复"晚安"时才发送后续消息:
chosen_action = random.choice(actions) await reply([chosen_action], event, modal_words=False, finish=chosen_action != "晚安") - await asyncio.sleep(random.randint(30, 120)) - await reply(["早上好!"], event, modal_words=False, finish=True) + if chosen_action == "晚安": + await asyncio.sleep(random.randint(30, 120)) + await reply(["早上好!"], event, modal_words=False, finish=True)
🧹 Nitpick comments (1)
src/module/tool/how_to_cook.py (1)
48-49: 修复 f-string 格式化问题第 49 行的 f-string 中没有任何占位符,应该使用普通字符串。
- await reply([f"发送消息时发生错误,请联系管理员排障"], event, finish=True) + await reply(["发送消息时发生错误,请联系管理员排障"], event, finish=True)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/core/bot/message.py(1 hunks)src/module/interact.py(1 hunks)src/module/maintain.py(1 hunks)src/module/tool/how_to_cook.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/module/maintain.py
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/module/tool/how_to_cook.py (6)
src/core/bot/message.py (1)
reply(11-37)src/core/constants.py (1)
Constants(76-194)src/core/util/tools.py (2)
png2jpg(213-221)fuzzy_matching(223-264)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/data/data_how_to_cook.py (1)
load_dishes(9-21)src/render/html/render_how_to_cook.py (1)
render_how_to_cook(22-42)
src/core/bot/message.py (2)
src/core/constants.py (1)
Constants(76-194)src/core/util/exception.py (1)
handle_exception(31-40)
src/module/interact.py (13)
src/core/bot/decorator.py (1)
get_all_modules_info(69-78)src/core/bot/message.py (1)
reply(11-37)src/core/constants.py (1)
Constants(76-194)src/core/util/tools.py (4)
png2jpg(213-221)get_simple_qrcode(300-304)check_intersect(338-339)get_today_timestamp_range(292-293)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/platform/manual/manual.py (1)
ManualPlatform(11-61)src/platform/online/atcoder.py (1)
AtCoder(15-207)src/platform/online/codeforces.py (1)
Codeforces(23-683)src/platform/online/nowcoder.py (1)
NowCoder(13-316)src/render/pixie/render_about.py (1)
AboutRenderer(121-150)src/render/pixie/render_contest_list.py (1)
ContestListRenderer(244-282)src/render/pixie/render_help.py (1)
HelpRenderer(155-185)src/core/bot/interact.py (1)
reply_recent_contests(127-184)
🪛 Ruff (0.12.2)
src/module/tool/how_to_cook.py
23-23: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
49-49: f-string without any placeholders
Remove extraneous f prefix
(F541)
src/module/interact.py
36-36: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
43-43: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
108-108: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
| def report_exception(event: Event, module_name: str, e: Exception): | ||
| Constants.log.warning(f"[obot-module] 操作失败,模块 {module_name} 出现异常") | ||
| Constants.log.exception(f"[obot-module] {e}") | ||
| reply([handle_exception(e)], event, modal_words=False, finish=True) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
修复异步调用问题:缺少 await
reply 是异步函数,但在 report_exception 中没有使用 await。这会导致协程对象未执行,异常信息无法发送给用户。
由于 report_exception 是同步函数,需要将其改为异步函数并添加 await:
-def report_exception(event: Event, module_name: str, e: Exception):
+async def report_exception(event: Event, module_name: str, e: Exception):
Constants.log.warning(f"[obot-module] 操作失败,模块 {module_name} 出现异常")
Constants.log.exception(f"[obot-module] {e}")
- reply([handle_exception(e)], event, modal_words=False, finish=True)
+ await reply([handle_exception(e)], event, modal_words=False, finish=True)验证所有调用 report_exception 的地方是否已使用 await:
🏁 Script executed:
#!/bin/bash
# 查找所有调用 report_exception 的地方
rg -A 2 "report_exception\(" --type pyLength of output: 1494
修复 report_exception 为异步并在调用处使用 await
report_exception 目前是同步函数,内部调用 reply(异步函数)时未加 await,导致协程对象未执行,异常信息无法发送给用户。请:
-
将
report_exception改为异步函数并添加await:--- a/src/core/bot/message.py +++ b/src/core/bot/message.py @@ -37,7 +37,7 @@ def report_exception(event: Event, module_name: str, e: Exception): Constants.log.warning(f"[obot-module] 操作失败,模块 {module_name} 出现异常") Constants.log.exception(f"[obot-module] {e}") - reply([handle_exception(e)], event, modal_words=False, finish=True) + await reply([handle_exception(e)], event, modal_words=False, finish=True)
-
在所有调用处添加
await,并确保调用上下文为异步函数(async def)。以下文件中存在未使用await的调用,需要逐一更新:- src/core/bot/transit.py
- src/module/cp/peeper.py
- src/module/cp/atc.py
- src/module/cp/cf.py
- src/module/cp/nk.py
- src/module/tool/rand.py
例如:
--- a/src/core/bot/transit.py +++ b/src/core/bot/transit.py @@ -120,7 +120,7 @@ async def handle_transit(...): - message.report_exception('Core.Transit', e) + await message.report_exception('Core.Transit', e)
-
批量定位剩余调用:
rg -n 'message\.report_exception' -C1 src
🤖 Prompt for AI Agents
In src/core/bot/message.py around lines 40 to 43, report_exception is currently
sync but calls the async reply() without awaiting; change report_exception to
async def and await reply(...). Then update every callsite to await
message.report_exception(...) and ensure those callers are async functions
(convert to async def if necessary) — specifically update calls in
src/core/bot/transit.py, src/module/cp/peeper.py, src/module/cp/atc.py,
src/module/cp/cf.py, src/module/cp/nk.py and src/module/tool/rand.py; run a
project-wide search (e.g. rg -n 'message\.report_exception' -C1 src) to find
remaining uses and fix them similarly.
|
|
||
| @today_contests_1.handle() | ||
| @today_contests_2.handle() | ||
| async def reply_today_contests(event: Event, message: Message = CommandArg()): |
There was a problem hiding this comment.
修复函数默认参数的可变对象问题
在多个处理函数的默认参数中调用 CommandArg() 会在函数定义时执行,可能导致意外行为。
将这些函数的默认参数改为 None,在函数内部处理:
-async def reply_today_contests(event: Event, message: Message = CommandArg()):
+async def reply_today_contests(event: Event, message: Message = None):
+ if message is None:
+ message = CommandArg()
platform = message.extract_plain_text().strip().lower()
-async def reply_recent_contests(event: Event, message: Message = CommandArg()):
+async def reply_recent_contests(event: Event, message: Message = None):
+ if message is None:
+ message = CommandArg()
platform = message.extract_plain_text().strip().lower()
-async def reply_qrcode(event: Event, message: Message = CommandArg()):
+async def reply_qrcode(event: Event, message: Message = None):
+ if message is None:
+ message = CommandArg()
content = message.extract_plain_text().strip()Also applies to: 43-43, 108-108
🧰 Tools
🪛 Ruff (0.12.2)
36-36: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🤖 Prompt for AI Agents
In src/module/interact.py around lines 36, 43 and 108, the functions use
CommandArg() as a default parameter which runs at import time and can cause
shared/mutable default issues; change each function signature to use message:
Message | None = None (or simply message=None), then at the top of the function
body do if message is None: message = CommandArg(); keep types consistent and
preserve existing logic that uses message afterwards; update any similar
handlers in this file to follow the same pattern.
| dishes = load_dishes() | ||
|
|
||
| @how_to_cook.handle() | ||
| async def reply_how_to_cook(event: Event, message: Message = CommandArg()): |
There was a problem hiding this comment.
修复函数默认参数的可变对象问题
在函数默认参数中调用 CommandArg() 会在函数定义时执行,可能导致意外行为。
将默认参数改为 None,在函数内部处理:
-async def reply_how_to_cook(event: Event, message: Message = CommandArg()):
+async def reply_how_to_cook(event: Event, message: Message = None):
+ if message is None:
+ message = CommandArg()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def reply_how_to_cook(event: Event, message: Message = CommandArg()): | |
| async def reply_how_to_cook(event: Event, message: Message = None): | |
| if message is None: | |
| message = CommandArg() | |
| # … (rest of function body) |
🧰 Tools
🪛 Ruff (0.12.2)
23-23: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🤖 Prompt for AI Agents
In src/module/tool/how_to_cook.py around line 23, the function reply_how_to_cook
uses CommandArg() as a default parameter which is evaluated at definition time;
change the signature to use message: Message | None = None and inside the
function do if message is None: message = CommandArg() so CommandArg() is
invoked at call time; update any type hints/uses accordingly to preserve
behavior and avoid mutable/default-evaluation issues.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/module/tool/how_to_cook.py (2)
41-43: 避免缓存目录残留无用 PNG:移除 False,让 png2jpg 删除源 PNG;可进一步改为内存转换传入 False 会保留 PNG 源文件,增加磁盘占用。默认行为会删除源 PNG。进一步可将工具改为内存中生成 JPG,避免额外落盘。
最小改动(删除 PNG 源文件):
- await reply( - [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png", False)], event, - finish=True) + await reply( + [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png")], event, + finish=True)可选(工具函数内存化,避免 JPG 落盘;需在 src/core/util/tools.py 中调整):
# 在 src/core/util/tools.py 中替换 png2jpg 实现为内存转换 import io from PIL import Image import os def png2jpg(path: str, remove_origin: bool = True) -> bytes: with Image.open(path) as img: with io.BytesIO() as bio: img.convert("RGB").save(bio, format="JPEG") data = bio.getvalue() if remove_origin: os.remove(path) return data
25-27: 菜谱库为空时应立即返回,避免后续逻辑抛异常当前仅发送提示但未中止流程,后续解析与渲染会继续执行,导致随机选菜或索引访问异常。
- if len(dishes) == 0: - await reply(["抱歉,菜谱库为空,请踢一踢管理员"], event, finish=True) + if len(dishes) == 0: + await reply(["抱歉,菜谱库为空,请踢一踢管理员"], event, finish=True) + return
🧹 Nitpick comments (4)
src/module/tool/how_to_cook.py (4)
5-5: 移除未使用的导入 ActionFailedActionFailed 未被使用,且被静态分析标记。建议移除以通过 CI 并保持整洁。
-from nonebot.exception import ActionFailed, MatcherException +from nonebot.exception import MatcherException
22-22: CommandArg 作为默认参数是 NoneBot 的标准依赖注入用法;建议用 noqa 局部忽略 Ruff B008这里的写法在 NoneBot 中是惯用且正确的,Ruff 的 B008 在该场景属于误报。为避免 CI 噪音,建议在本行添加 noqa 说明原因。
-async def reply_how_to_cook(event: Event, message: Message = CommandArg()): +async def reply_how_to_cook(event: Event, message: Message = CommandArg()): # noqa: B008 - NoneBot 依赖注入
23-23: 临时“请稍等”提示建议关闭口癖词,降低噪音首条进度提示更适合简洁表达,建议 modal_words=False。
- await reply(["正在翻阅菜谱,请稍等"], event, finish=False) + await reply(["正在翻阅菜谱,请稍等"], event, modal_words=False, finish=False)
28-28: 细节优化:提取纯文本后先 strip 再 split,处理纯空白输入功能不变,边界更稳健。
- args = message.extract_plain_text().split() + args = message.extract_plain_text().strip().split()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/module/maintain.py(1 hunks)src/module/tool/how_to_cook.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/module/maintain.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/module/tool/how_to_cook.py (5)
src/core/bot/message.py (2)
reply(11-37)report_exception(40-43)src/core/util/tools.py (2)
png2jpg(213-221)fuzzy_matching(223-264)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/data/data_how_to_cook.py (1)
load_dishes(9-21)src/render/html/render_how_to_cook.py (1)
render_how_to_cook(22-42)
🪛 GitHub Check: CodeQL
src/module/tool/how_to_cook.py
[notice] 5-5: Unused import
Import of 'ActionFailed' is not used.
🪛 Ruff (0.12.2)
src/module/tool/how_to_cook.py
5-5: nonebot.exception.ActionFailed imported but unused
Remove unused import: nonebot.exception.ActionFailed
(F401)
22-22: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
| except Exception as e: | ||
| if isinstance(e, MatcherException): | ||
| raise e | ||
| else: | ||
| report_exception(event, "How-To-Cook", e) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
report_exception 未 await 可能导致异常消息不发送;建议改为异步并在此 await
根据提供的 src/core/bot/message.py 片段,report_exception 内部调用了异步 reply 却未 await,当前又以同步方式调用,实际可能丢消息。
建议:
- 将 report_exception 改为 async,并在内部 await reply;
- 此处使用 await 调用。
本文件最小改动:
- else:
- report_exception(event, "How-To-Cook", e)
+ else:
+ await report_exception(event, "How-To-Cook", e)跨文件改动(src/core/bot/message.py)示例:
# 将
def report_exception(event: Event, module_name: str, e: Exception):
Constants.log.warning(f"[obot-module] 操作失败,模块 {module_name} 出现异常")
Constants.log.exception(f"[obot-module] {e}")
reply([handle_exception(e)], event, modal_words=False, finish=True)
# 改为
async def report_exception(event: Event, module_name: str, e: Exception):
Constants.log.warning(f"[obot-module] 操作失败,模块 {module_name} 出现异常")
Constants.log.exception(f"[obot-module] {e}")
await reply([handle_exception(e)], event, modal_words=False, finish=True)可用脚本确认现状与影响点:
🏁 Script executed:
#!/bin/bash
# 检查 report_exception 是否为异步,以及内部是否 await 了 reply
rg -n -A3 -B2 $'def report_exception\\(' src | sed -n '1,120p'
rg -n $'await\\s+reply\\(' src/core/bot/message.py || true
# 统计全仓库调用点,方便批量替换为 await
rg -n $'\\breport_exception\\(' --glob '!**/site-packages/**'Length of output: 1435
将 report_exception 改为异步并在调用处 await
report_exception 在 src/core/bot/message.py 以同步函数调用了异步 reply,可能导致异常消息丢失。需要将其声明为 async 并在内部 await reply,且在所有调用点使用 await。
需要修改的位置:
- src/core/bot/message.py:40 行,将
def report_exception改为async def report_exception,并在调用 reply 时加上await - 以下所有调用处改为
await report_exception(...):- src/module/tool/how_to_cook.py:50
- src/module/tool/rand.py:129
- src/module/cp/peeper.py:97
- src/module/cp/nk.py:95
- src/module/cp/atc.py:133
- src/core/bot/transit.py:74、151、155
- src/module/cp/cf.py:529
示例 diff(src/core/bot/message.py):
--- a/src/core/bot/message.py
+++ b/src/core/bot/message.py
- def report_exception(event: Event, module_name: str, e: Exception):
+ async def report_exception(event: Event, module_name: str, e: Exception):
Constants.log.warning(f"[obot-module] 操作失败,模块 {module_name} 出现异常")
Constants.log.exception(f"[obot-module] {e}")
- reply([handle_exception(e)], event, modal_words=False, finish=True)
+ await reply([handle_exception(e)], event, modal_words=False, finish=True)示例 diff(调用处):
--- a/src/module/tool/how_to_cook.py
+++ b/src/module/tool/how_to_cook.py
- report_exception(event, "How-To-Cook", e)
+ await report_exception(event, "How-To-Cook", e)请依此更新所有调用点,确保异常消息能够正确发送。
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/module/tool/how_to_cook.py around lines 46 to 50, the call to
report_exception is currently synchronous; report_exception must be changed to
async in src/core/bot/message.py (line ~40) and its internal call to reply must
be awaited, and every call site must be updated to await report_exception(...).
Change def report_exception -> async def report_exception in
src/core/bot/message.py and add await where it calls reply, then update these
call sites to use await: src/module/tool/how_to_cook.py:50,
src/module/tool/rand.py:129, src/module/cp/peeper.py:97, src/module/cp/nk.py:95,
src/module/cp/atc.py:133, src/core/bot/transit.py:74,151,155, and
src/module/cp/cf.py:529 so the exception reporting is awaited.
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (11)
src/module/cron.py (2)
23-26: 异步函数应该正确调用同步函数
peeper_daily_update是异步函数,但注释中的daily_update_job()是同步函数。如果以后取消注释,直接调用可能会阻塞事件循环。建议使用
asyncio.to_thread或run_in_executor来避免阻塞:+import asyncio + @scheduler.scheduled_job("cron", id="peeper_daily_update", hour=0, minute=0, second=1) async def peeper_daily_update(): - # daily_update_job() + # await asyncio.to_thread(daily_update_job) pass
31-37: 函数参数缺少空格且缺少错误处理
- 函数参数之间缺少空格
- 发送消息失败时没有异常处理
- 没有可用 Bot 时没有日志提示
-async def heart_beat(content,id): +async def heart_beat(content, id): for bot_name in nonebot.get_adapter(OnebotAdapter).bots: bot = nonebot.get_bots().get(bot_name) if bot is not None: - Constants.log.info(f"[obot-heartbeat] Onebot 主人 {id} 的 Bot {bot.self_id} 正在发送心跳信息。") - await MessageFactory(content).send_to(target=TargetQQPrivate(user_id=id),bot=bot) - return + try: + Constants.log.info(f"[obot-heartbeat] Onebot 主人 {id} 的 Bot {bot.self_id} 正在发送心跳信息。") + await MessageFactory(content).send_to(target=TargetQQPrivate(user_id=id), bot=bot) + return + except Exception as e: + Constants.log.error(f"[obot-heartbeat] 发送心跳消息失败: {e}") + + Constants.log.warning("[obot-heartbeat] 没有可用的 Bot 发送心跳消息")src/module/tool/how_to_cook.py (4)
42-44: 考虑优化文件清理策略
png2jpg函数中remove_origin=False会导致 PNG 文件残留。建议清理源文件或使用内存处理。- await reply( - [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png", False)], event, - finish=True) + await reply( + [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png")], event, + finish=True)
23-23: 避免在函数默认参数中调用 CommandArg()在函数默认参数中调用
CommandArg()会在函数定义时执行,可能导致意外行为。-async def reply_how_to_cook(event: Event, message: Message = CommandArg()): +async def reply_how_to_cook(event: Event, message: Message = None): + if message is None: + message = CommandArg()
26-27: 菜谱库为空时应立即返回当前仅发送提示但继续执行后续逻辑,会导致空列表的随机选择异常。
if len(dishes) == 0: await reply(["抱歉,菜谱库为空,请踢一踢管理员"], event, finish=True) + return
47-51: report_exception 需要改为异步函数
report_exception内部调用了异步reply但自身不是异步函数,会导致消息无法发送。#!/bin/bash # 验证 report_exception 的函数签名和内部实现 ast-grep --pattern 'def report_exception($$$)'建议在调用处使用 await:
- report_exception(event, "How-To-Cook", e) + await report_exception(event, "How-To-Cook", e)注意:这需要同时修改
src/core/bot/message.py中的report_exception函数为异步函数。src/module/interact.py (5)
38-41: 使用 Annotated 避免在参数默认值中调用 CommandArg(修复 Ruff B008)当前写法会在函数定义期调用 CommandArg。建议改为 Annotated 注入,类型更准确且规避 B008。
-async def reply_today_contests(event: Event, message: Message = CommandArg()): +async def reply_today_contests(event: Event, message: Annotated[Message, CommandArg()]): platform = message.extract_plain_text().strip().lower() await reply_contests(event, platform, True)在文件顶部补充导入(新增代码,非选区修改):
from typing_extensions import Annotated
45-47: 同上:参数注入应使用 Annotated,避免默认值调用与上一处一致,使用 Annotated 注入 CommandArg。
-async def reply_recent_contests(event: Event, message: Message = CommandArg()): +async def reply_recent_contests(event: Event, message: Annotated[Message, CommandArg()]): platform = message.extract_plain_text().strip().lower() await reply_contests(event, platform, False)
74-80: 平台列表聚合缺少异常与空返回处理,易导致崩溃或扩展失败platform.get_contest_list 失败或返回 None/结构异常会导致 TypeError/ValueError。应做异常捕获与结果校验,并记录日志后跳过。
- for platform in queries: - running, upcoming, finished = platform.get_contest_list() - running_contests.extend(running) - upcoming_contests.extend(upcoming) - finished_contests.extend(finished) + for platform in queries: + try: + result = platform.get_contest_list() + except Exception as e: + Constants.log.exception(f"[contest] 获取 {platform.platform_name} 列表失败: {e}") + continue + if not result or not isinstance(result, (list, tuple)) or len(result) != 3: + Constants.log.warning(f"[contest] {platform.platform_name} 返回空或不合法结果,已跳过: {result}") + continue + running, upcoming, finished = result + running_contests.extend(running or []) + upcoming_contests.extend(upcoming or []) + finished_contests.extend(finished or [])可选优化(不必现在做):用 asyncio.to_thread 并发抓取,且保留上述每平台异常隔离。
110-114: 空内容时应立即结束,避免生成空二维码提示后应 finish 并 return,防止继续生成空内容二维码。
-async def reply_qrcode(event: Event, message: Message = CommandArg()): +async def reply_qrcode(event: Event, message: Annotated[Message, CommandArg()]): content = message.extract_plain_text().strip() if len(content) == 0: - await reply(["请提供需要生成二维码的内容"], event) + await reply(["请提供需要生成二维码的内容"], event, finish=True) + return
141-144: “睡觉”命令的“早上好”应按需发送,避免误扰仅当首条回复为“晚安”时才延时发送“早上好”,与注释一致。
chosen_action = random.choice(actions) await reply([chosen_action], event, modal_words=False, finish=chosen_action != "晚安") - await asyncio.sleep(random.randint(30, 120)) - await reply(["早上好!"], event, modal_words=False, finish=True) + if chosen_action == "晚安": + await asyncio.sleep(random.randint(30, 120)) + await reply(["早上好!"], event, modal_words=False, finish=True)
🧹 Nitpick comments (13)
src/module/cron.py (1)
21-21: 建议在 jobstore 配置中增加空格函数参数之间应该有空格以提高代码可读性。
-scheduler.add_jobstore("sqlalchemy",alias="default",url=f"sqlite:///{os.path.join(data_dir,'jobs.sqlite')}") +scheduler.add_jobstore("sqlalchemy", alias="default", url=f"sqlite:///{os.path.join(data_dir, 'jobs.sqlite')}")src/module/cp/contest_manual.py (4)
16-16: 建议修复装饰器参数格式装饰器参数之间缺少空格,影响代码可读性。
-add_manual_contest = on_command("导入比赛", aliases={"import_contest"}, priority=0,rule=to_me(),block=True,permission=SUPERUSER) +add_manual_contest = on_command("导入比赛", aliases={"import_contest"}, priority=0, rule=to_me(), block=True, permission=SUPERUSER)
24-24: 修复格式问题:缺少空格await 调用时参数之间缺少空格。
- await reply(["参数数量有误\n\n",f"{help_content}"],event,False,True) + await reply(["参数数量有误\n\n", f"{help_content}"], event, False, True)
30-30: 修复格式问题:缺少空格await 调用时参数之间缺少空格。
- await reply(["start_time 格式有误\n\n",f"{help_content}"],event,False,True) + await reply(["start_time 格式有误\n\n", f"{help_content}"], event, False, True)
36-36: 移除不必要的变量赋值
duration变量赋值后可以直接在ManualContest构造函数中使用int(duration_raw)。- duration = int(duration_raw) - contest = ManualContest(platform, abbr, name, start_time, duration, supplement) + contest = ManualContest(platform, abbr, name, start_time, int(duration_raw), supplement)src/module/tool/how_to_cook.py (1)
5-5: 移除未使用的导入
ActionFailed导入但未使用。-from nonebot.exception import ActionFailed, MatcherException +from nonebot.exception import MatcherExceptionsrc/module/cp/atc.py (1)
108-108: 移除不必要的 f-string 前缀字符串中没有占位符,不需要 f-string 前缀。
- await reply([f"[AtCoder] 近期比赛\n\n", f"{info}"], event, modal_words=False) + await reply(["[AtCoder] 近期比赛\n\n", info], event, modal_words=False)src/module/cp/nk.py (2)
56-57: 修复字符串拼接格式问题多行字符串拼接的缩进不一致,影响代码可读性。
- content = [f"[NowCoder] {handle}\n\n" - , f"{info}"] + content = [f"[NowCoder] {handle}\n\n", + f"{info}"]
73-73: 移除不必要的 f-string 前缀字符串中没有占位符,不需要 f-string 前缀。
- await reply([f"[NowCoder] 近期比赛\n\n", f"{info}"], event, modal_words=False) + await reply(["[NowCoder] 近期比赛\n\n", info], event, modal_words=False)src/module/interact.py (2)
85-98: 避免重复计算今天时间范围并简化过滤逻辑当前重复调用 get_today_timestamp_range,可先保存到变量,提升可读性与微小性能。
- if is_today: - running_contests = [contest for contest in running_contests if check_intersect( - range1=get_today_timestamp_range(), - range2=(contest.start_time, contest.start_time + contest.duration) - )] - upcoming_contests = [contest for contest in upcoming_contests if check_intersect( - range1=get_today_timestamp_range(), - range2=(contest.start_time, contest.start_time + contest.duration) - )] - finished_contests = [contest for contest in finished_contests if check_intersect( - range1=get_today_timestamp_range(), - range2=(contest.start_time, contest.start_time + contest.duration) - )] + if is_today: + today_range = get_today_timestamp_range() + running_contests = [c for c in running_contests if check_intersect( + range1=today_range, range2=(c.start_time, c.start_time + c.duration) + )] + upcoming_contests = [c for c in upcoming_contests if check_intersect( + range1=today_range, range2=(c.start_time, c.start_time + c.duration) + )] + finished_contests = [c for c in finished_contests if check_intersect( + range1=today_range, range2=(c.start_time, c.start_time + c.duration) + )]
155-157: 变量命名小建议:避免误导性命名HelpRenderer 的结果命名为 contest_list_img 容易引发误解,建议改为 help_img。
- contest_list_img = HelpRenderer().render() - contest_list_img.write_file(f"{cached_prefix}.png") + help_img = HelpRenderer().render() + help_img.write_file(f"{cached_prefix}.png")robot.py (2)
5-6: 适配器可用性校验(可选)若未安装对应适配器包(如 nonebot-adapter-qq),register_adapter 会失败。建议在部署/CI 中校验依赖或在此处 try/except 记录友好日志。
1-1: 移除 UTF-8 BOM文件首行包含 BOM(零宽字符),在部分工具链/差分中会造成噪音。建议保存为无 BOM 的 UTF-8。
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
robot.py(1 hunks)src/core/bot/decorator.py(0 hunks)src/module/__init__.py(0 hunks)src/module/cp/atc.py(2 hunks)src/module/cp/contest_manual.py(1 hunks)src/module/cp/nk.py(1 hunks)src/module/cron.py(1 hunks)src/module/interact.py(1 hunks)src/module/maintain.py(1 hunks)src/module/tool/how_to_cook.py(1 hunks)
💤 Files with no reviewable changes (2)
- src/module/init.py
- src/core/bot/decorator.py
🚧 Files skipped from review as they are similar to previous changes (1)
- src/module/maintain.py
🧰 Additional context used
🧬 Code Graph Analysis (7)
src/module/cron.py (4)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/constants.py (1)
Constants(76-194)src/module/cp/contest_manual.py (1)
register_module(50-51)src/module/cp/peeper.py (1)
daily_update_job(103-111)
src/module/cp/contest_manual.py (5)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (1)
reply(11-37)src/core/util/tools.py (2)
is_valid_date(389-394)check_is_int(171-176)src/data/data_contest_manual.py (2)
ManualContest(12-25)save_contest(43-52)src/platform/model.py (1)
format(23-35)
src/module/cp/atc.py (7)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (1)
reply(11-37)src/core/constants.py (2)
Constants(76-194)HelpStrList(40-43)src/core/util/tools.py (3)
get_simple_qrcode(300-304)png2jpg(213-221)download_img(187-210)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/platform/online/atcoder.py (1)
AtCoder(15-207)src/platform/model.py (1)
get_recent_contests(111-142)
src/module/cp/nk.py (7)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (1)
reply(11-37)src/core/constants.py (2)
Constants(76-194)HelpStrList(40-43)src/core/util/tools.py (2)
png2jpg(213-221)download_img(187-210)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/platform/online/nowcoder.py (1)
NowCoder(13-316)src/platform/model.py (1)
get_recent_contests(111-142)
robot.py (1)
src/core/constants.py (1)
Constants(76-194)
src/module/interact.py (8)
src/core/bot/decorator.py (3)
decorator(17-19)get_all_modules_info(37-46)module(8-24)src/core/bot/message.py (1)
reply(11-37)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/platform/manual/manual.py (1)
ManualPlatform(11-61)src/platform/online/atcoder.py (1)
AtCoder(15-207)src/render/pixie/render_about.py (1)
AboutRenderer(121-150)src/render/pixie/render_contest_list.py (1)
ContestListRenderer(244-282)src/platform/model.py (1)
get_contest_list(89-108)
src/module/tool/how_to_cook.py (6)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (2)
reply(11-37)report_exception(40-43)src/core/util/tools.py (2)
png2jpg(213-221)fuzzy_matching(223-264)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/data/data_how_to_cook.py (1)
load_dishes(9-21)src/render/html/render_how_to_cook.py (1)
render_how_to_cook(22-42)
🪛 Ruff (0.12.2)
src/module/cp/contest_manual.py
18-18: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
src/module/cp/atc.py
32-32: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
50-50: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
71-71: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
108-108: f-string without any placeholders
Remove extraneous f prefix
(F541)
src/module/cp/nk.py
29-29: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
48-48: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
73-73: f-string without any placeholders
Remove extraneous f prefix
(F541)
robot.py
29-29: Do not call getattr with a constant attribute value. It is not any safer than normal property access.
Replace getattr with attribute access
(B009)
src/module/interact.py
38-38: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
45-45: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
110-110: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
src/module/tool/how_to_cook.py
5-5: nonebot.exception.ActionFailed imported but unused
Remove unused import: nonebot.exception.ActionFailed
(F401)
23-23: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🔇 Additional comments (1)
src/module/interact.py (1)
50-67: 确认 recent 查询语义是否满足预期(可能仅返回“今日”)根据平台层实现摘要,CompetitivePlatform.get_contest_list 可能已按“今日”做过过滤;此处 recent 分支未再过滤但也无法跨日查询,可能导致“近期比赛”实为“今日比赛”。请核实平台层语义,避免用户困惑。
如需支持跨日建议:
- 为 get_contest_list 增加参数(如 time_range / is_today),由平台层控制过滤;
- 或在非 is_today 时调用平台私有 _get_contest_list 并在此聚合层自行过滤/截断(注意封装边界)。
我可据仓库实际实现生成最小变更方案。
|
|
||
| client.run(appid=Constants.botpy_conf["appid"], secret=Constants.botpy_conf["secret"]) | ||
| # 初始化 NoneBot | ||
| nonebot.init(superusers=Constants.role_conf['admin_id'],command_sep={' '}) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
superusers 参数类型不确定,可能导致管理员配置失效
NoneBot 期望 superusers 为 set[str]。当前从配置直接取值,若为 str 或 list/单值,可能类型不符。建议规范化为集合。
- nonebot.init(superusers=Constants.role_conf['admin_id'],command_sep={' '})
+ admin_id = Constants.role_conf.get("admin_id")
+ superusers = (
+ {str(x) for x in admin_id}
+ if isinstance(admin_id, (list, tuple, set))
+ else ({str(admin_id)} if admin_id else set())
+ )
+ nonebot.init(superusers=superusers, command_sep={' '})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| nonebot.init(superusers=Constants.role_conf['admin_id'],command_sep={' '}) | |
| admin_id = Constants.role_conf.get("admin_id") | |
| superusers = ( | |
| {str(x) for x in admin_id} | |
| if isinstance(admin_id, (list, tuple, set)) | |
| else ({str(admin_id)} if admin_id else set()) | |
| ) | |
| nonebot.init(superusers=superusers, command_sep={' '}) |
🤖 Prompt for AI Agents
In robot.py around line 10, the call
nonebot.init(superusers=Constants.role_conf['admin_id'],command_sep={' '}) may
pass a non-set type for superusers; coerce Constants.role_conf['admin_id'] into
a set[str] before passing it to nonebot.init: if the value is None use an empty
set, if it's a str wrap it in a single-item set, if it's an iterable
(list/tuple) convert each item to str and build a set, otherwise cast the value
to str and place in a set; then pass that normalized set as the superusers
argument.
| for plugin in get_loaded_plugins(): | ||
| if plugin.module_name.startswith("src"): | ||
| getattr(importlib.import_module(plugin.module_name),"register_module")() | ||
| nonebot.run() No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
调用 register_module 未做存在性检查,可能抛出 AttributeError
并非所有插件都提供 register_module;应做安全获取并校验可调用性。顺便消除 Ruff B009 提示。
- for plugin in get_loaded_plugins():
- if plugin.module_name.startswith("src"):
- getattr(importlib.import_module(plugin.module_name),"register_module")()
+ for plugin in get_loaded_plugins():
+ if plugin.module_name.startswith("src"):
+ module = importlib.import_module(plugin.module_name)
+ func = getattr(module, "register_module", None)
+ if callable(func):
+ func()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for plugin in get_loaded_plugins(): | |
| if plugin.module_name.startswith("src"): | |
| getattr(importlib.import_module(plugin.module_name),"register_module")() | |
| nonebot.run() | |
| for plugin in get_loaded_plugins(): | |
| if plugin.module_name.startswith("src"): | |
| module = importlib.import_module(plugin.module_name) | |
| func = getattr(module, "register_module", None) | |
| if callable(func): | |
| func() | |
| nonebot.run() |
🧰 Tools
🪛 Ruff (0.12.2)
29-29: Do not call getattr with a constant attribute value. It is not any safer than normal property access.
Replace getattr with attribute access
(B009)
🤖 Prompt for AI Agents
In robot.py around lines 27–30, the code calls register_module on imported
plugins without checking existence which can raise AttributeError and triggers
Ruff B009; change to assign the import to a variable (module =
importlib.import_module(plugin.module_name)), retrieve the attribute safely
(register = getattr(module, "register_module", None)), check callable(register)
and only then call register(); this both prevents AttributeError and removes the
unused-getattr B009 warning.
| def reply_manual_add_contest(message: RobotMessage): | ||
| add_manual_contest = on_command("导入比赛", aliases={"import_contest"}, priority=0,rule=to_me(),block=True,permission=SUPERUSER) | ||
| @add_manual_contest.handle() | ||
| async def reply_manual_add_contest(event:Event,message:Message = CommandArg()): |
There was a problem hiding this comment.
避免在函数默认参数中调用 CommandArg()
在函数默认参数中调用 CommandArg() 会在函数定义时执行,可能导致意外行为。
-async def reply_manual_add_contest(event:Event,message:Message = CommandArg()):
+async def reply_manual_add_contest(event: Event, message: Message = None):
+ if message is None:
+ message = CommandArg()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def reply_manual_add_contest(event:Event,message:Message = CommandArg()): | |
| async def reply_manual_add_contest(event: Event, message: Message = None): | |
| if message is None: | |
| message = CommandArg() |
🧰 Tools
🪛 Ruff (0.12.2)
18-18: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🤖 Prompt for AI Agents
In src/module/cp/contest_manual.py around line 18, the function signature uses
CommandArg() as a default parameter which runs at import time; change the
signature to accept message: Message = None (or omit default), then inside the
function check if message is None and assign message = CommandArg() at
call-time; update any type hints or imports if needed and ensure behavior is
identical by using the lazily-evaluated CommandArg() only when the function is
called.
|
|
||
|
|
||
| @id_query.handle() | ||
| async def send_user_id_card(event: Event, message: Message = CommandArg()): |
There was a problem hiding this comment.
避免在函数默认参数中调用 CommandArg()
在函数默认参数中调用 CommandArg() 会在函数定义时执行,可能导致意外行为。
-async def send_user_id_card(event: Event, message: Message = CommandArg()):
+async def send_user_id_card(event: Event, message: Message = None):
+ if message is None:
+ message = CommandArg()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def send_user_id_card(event: Event, message: Message = CommandArg()): | |
| async def send_user_id_card(event: Event, message: Message = None): | |
| if message is None: | |
| message = CommandArg() |
🧰 Tools
🪛 Ruff (0.12.2)
29-29: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🤖 Prompt for AI Agents
In src/module/cp/nk.py around line 29, the function defines a default parameter
by calling CommandArg() at definition time which can cause unexpected behavior;
change the signature to avoid calling CommandArg() in the default (e.g., use
message: Message | None = None) and inside the function check if message is None
and then call CommandArg() to obtain the runtime value, ensuring the call
happens when the function runs rather than at import/definition time.
| def send_user_info(message: RobotMessage, handle: str): | ||
| message.reply(f"正在查询 {handle} 的 NowCoder 平台信息,请稍等") | ||
| @info_query.handle() | ||
| async def send_user_info(event: Event, message: Message = CommandArg()): |
There was a problem hiding this comment.
避免在函数默认参数中调用 CommandArg()
在函数默认参数中调用 CommandArg() 会在函数定义时执行,可能导致意外行为。
-async def send_user_info(event: Event, message: Message = CommandArg()):
+async def send_user_info(event: Event, message: Message = None):
+ if message is None:
+ message = CommandArg()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def send_user_info(event: Event, message: Message = CommandArg()): | |
| async def send_user_info(event: Event, message: Message = None): | |
| if message is None: | |
| message = CommandArg() | |
| # ... rest of the function body ... |
🧰 Tools
🪛 Ruff (0.12.2)
48-48: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🤖 Prompt for AI Agents
In src/module/cp/nk.py around line 48, the function defines a default parameter
by calling CommandArg() at definition time which can cause unexpected behavior;
change the signature to accept message (e.g. message: Optional[Message] = None)
and inside the async function check if message is None and then call
CommandArg() to obtain the value at call time, ensuring the default is evaluated
lazily and avoiding side effects during import.
| @scheduler.scheduled_job("cron",hour=6,id="job_ping_6",kwargs={'content':"ping",'id':Constants.role_conf['heartbeat_id']}) | ||
| @scheduler.scheduled_job("cron",hour=12,id="job_ping_12",kwargs={'content':"pong",'id':Constants.role_conf['heartbeat_id']}) | ||
| @scheduler.scheduled_job("cron",hour=18,id="job_ping_18",kwargs={'content':"pingping",'id':Constants.role_conf['heartbeat_id']}) | ||
| @scheduler.scheduled_job("cron",hour=0,id="job_ping_0",kwargs={'content':"pongpong",'id':Constants.role_conf['heartbeat_id']}) |
There was a problem hiding this comment.
配置键可能不存在导致运行时错误
在定义装饰器时直接访问 Constants.role_conf['heartbeat_id'],如果配置文件中没有该键会导致启动时错误。
建议延迟获取配置值:
-@scheduler.scheduled_job("cron",hour=6,id="job_ping_6",kwargs={'content':"ping",'id':Constants.role_conf['heartbeat_id']})
-@scheduler.scheduled_job("cron",hour=12,id="job_ping_12",kwargs={'content':"pong",'id':Constants.role_conf['heartbeat_id']})
-@scheduler.scheduled_job("cron",hour=18,id="job_ping_18",kwargs={'content':"pingping",'id':Constants.role_conf['heartbeat_id']})
-@scheduler.scheduled_job("cron",hour=0,id="job_ping_0",kwargs={'content':"pongpong",'id':Constants.role_conf['heartbeat_id']})
+@scheduler.scheduled_job("cron", hour=6, id="job_ping_6")
+@scheduler.scheduled_job("cron", hour=12, id="job_ping_12")
+@scheduler.scheduled_job("cron", hour=18, id="job_ping_18")
+@scheduler.scheduled_job("cron", hour=0, id="job_ping_0")
+async def heart_beat_wrapper(content: str = None):
+ heartbeat_id = Constants.role_conf.get('heartbeat_id')
+ if not heartbeat_id:
+ Constants.log.warning("[obot-heartbeat] 心跳接收者 ID 未配置")
+ return
+
+ # 根据调度任务 ID 确定内容
+ job_contents = {
+ "job_ping_6": "ping",
+ "job_ping_12": "pong",
+ "job_ping_18": "pingping",
+ "job_ping_0": "pongpong"
+ }
+ job_id = scheduler.get_job(scheduler.current_job_id).id if hasattr(scheduler, 'current_job_id') else None
+ content = job_contents.get(job_id, "ping")
+
+ await heart_beat(content, heartbeat_id)Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/module/cron.py around lines 27 to 30, the decorators access
Constants.role_conf['heartbeat_id'] at import/decoration time which can raise a
KeyError if the config key is missing; remove the direct dict access from the
decorator arguments (e.g., don't hardcode 'id' using Constants.role_conf[...] in
the @scheduler.scheduled_job call) and instead have the scheduled job obtain the
heartbeat_id at runtime (use Constants.role_conf.get('heartbeat_id') inside the
job handler, handle None by logging or skipping the action), so configuration is
looked up lazily and missing keys are safely handled.
| cached_prefix = get_cached_prefix('Contest-List-Renderer') | ||
| contest_list_img = ContestListRenderer(running_contests, upcoming_contests, finished_contests).render() | ||
| contest_list_img.write_file(f"{cached_prefix}.png") | ||
| await reply([f"以下是当前查询到的{tip_time_range}比赛列表", png2jpg(f"{cached_prefix}.png")], event, | ||
| modal_words=False, finish=True) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
当无任何比赛时直接提示并结束,避免生成空图
若三类列表均为空,应直接回复“未查询到相关比赛”,无需渲染与写入文件。
- cached_prefix = get_cached_prefix('Contest-List-Renderer')
- contest_list_img = ContestListRenderer(running_contests, upcoming_contests, finished_contests).render()
- contest_list_img.write_file(f"{cached_prefix}.png")
- await reply([f"以下是当前查询到的{tip_time_range}比赛列表", png2jpg(f"{cached_prefix}.png")], event,
- modal_words=False, finish=True)
+ if not (running_contests or upcoming_contests or finished_contests):
+ await reply([f"未查询到{tip_time_range}比赛"], event, modal_words=False, finish=True)
+ return
+ cached_prefix = get_cached_prefix('Contest-List-Renderer')
+ contest_list_img = ContestListRenderer(running_contests, upcoming_contests, finished_contests).render()
+ contest_list_img.write_file(f"{cached_prefix}.png")
+ await reply([f"以下是当前查询到的{tip_time_range}比赛列表", png2jpg(f"{cached_prefix}.png")], event,
+ modal_words=False, finish=True)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cached_prefix = get_cached_prefix('Contest-List-Renderer') | |
| contest_list_img = ContestListRenderer(running_contests, upcoming_contests, finished_contests).render() | |
| contest_list_img.write_file(f"{cached_prefix}.png") | |
| await reply([f"以下是当前查询到的{tip_time_range}比赛列表", png2jpg(f"{cached_prefix}.png")], event, | |
| modal_words=False, finish=True) | |
| # 如果三类比赛列表均为空,直接回复并结束,避免生成空图 | |
| if not (running_contests or upcoming_contests or finished_contests): | |
| await reply([f"未查询到{tip_time_range}比赛"], event, modal_words=False, finish=True) | |
| return | |
| cached_prefix = get_cached_prefix('Contest-List-Renderer') | |
| contest_list_img = ContestListRenderer(running_contests, upcoming_contests, finished_contests).render() | |
| contest_list_img.write_file(f"{cached_prefix}.png") | |
| await reply([f"以下是当前查询到的{tip_time_range}比赛列表", png2jpg(f"{cached_prefix}.png")], event, | |
| modal_words=False, finish=True) |
🤖 Prompt for AI Agents
In src/module/interact.py around lines 99 to 103, the code always renders and
writes a contest list image even when no contests exist; add a guard that checks
if running_contests, upcoming_contests, and finished_contests are all empty and
if so immediately call reply with the message "未查询到相关比赛" (modal_words=False,
finish=True) and return/exit the handler to avoid rendering/writing an empty
image; otherwise proceed with the existing rendering, write_file and reply flow.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (7)
src/module/cp/peeper.py (1)
175-175: 修复函数默认参数问题在函数默认参数中调用
CommandArg()会在函数定义时执行,这可能导致意外行为。-async def send_yesterday_board(event:Event,message:Message = CommandArg()): +async def send_yesterday_board(event:Event, message: Message = None): + if message is None: + message = CommandArg()src/module/tool/how_to_cook.py (4)
42-44: 考虑删除源 PNG 文件以避免磁盘空间浪费
png2jpg函数调用时传入False参数不会删除源文件,可能导致缓存目录中积累大量无用文件。- await reply( - [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png", False)], event, - finish=True) + await reply( + [f"帮你找到了{query_tag}一个菜谱【{picked}】{query_more_tip}", png2jpg(f"{cached_prefix}.png")], event, + finish=True)
5-5: 删除未使用的导入
ActionFailed被导入但未使用。-from nonebot.exception import ActionFailed, MatcherException +from nonebot.exception import MatcherException
23-23: 修复函数默认参数的可变对象问题在函数默认参数中调用
CommandArg()会在函数定义时执行,可能导致意外行为。-async def reply_how_to_cook(event: Event, message: Message = CommandArg()): +async def reply_how_to_cook(event: Event, message: Message = None): + if message is None: + message = CommandArg()
26-27: 处理菜谱库为空后应立即返回当菜谱库为空时,代码发送了错误消息但没有返回,会继续执行后续逻辑导致异常。
if len(dishes) == 0: await reply(["抱歉,菜谱库为空,请踢一踢管理员"], event, finish=True) + returnrobot.py (2)
10-10: 规范化 superusers 参数类型NoneBot 期望
superusers参数为set[str]类型。当前直接从配置中取值可能导致类型不匹配。-nonebot.init(superusers=Constants.role_conf['admin_id'],command_sep={' '}) +admin_ids = Constants.role_conf.get('admin_id', []) +if isinstance(admin_ids, str): + superusers = {admin_ids} +elif isinstance(admin_ids, (list, tuple)): + superusers = {str(id) for id in admin_ids} +else: + superusers = set() +nonebot.init(superusers=superusers, command_sep={' '})
28-30: 安全调用 register_module 函数并非所有插件都提供
register_module函数,直接调用可能抛出AttributeError。同时可以消除 Ruff B009 警告。for plugin in get_loaded_plugins(): if plugin.module_name.startswith("src"): - getattr(importlib.import_module(plugin.module_name),"register_module")() + module = importlib.import_module(plugin.module_name) + register_func = getattr(module, "register_module", None) + if callable(register_func): + register_func()
🧹 Nitpick comments (3)
src/core/bot/message.py (1)
11-38: 考虑为 QQ 和非 QQ 事件提取公共逻辑当前
reply函数中 QQ 事件和非 QQ 事件的处理逻辑存在重复,如内容遍历和模态词添加。建议提取公共逻辑以提高可维护性。async def reply(contents: list[str | bytes], event: Event, modal_words: bool = True, finish: bool = True): - msg_builder = None - if isinstance(event, QQMessageEvent): - for content in contents: - if isinstance(content, bytes): - msg_builder = Image(content) if msg_builder is None else msg_builder + Image(content) - else: - msg_builder = Text(content) if msg_builder is None else msg_builder + Text(content) - if modal_words: - msg_builder = msg_builder + Text(random.choice(Constants.modal_words)) - else: - images: list[Image] = [] - texts = Text("") - for content in contents: - if isinstance(content, bytes): - images.append(Image(content)) - else: - texts = texts + Text(content) - if modal_words: - texts = texts + Text(random.choice(Constants.modal_words)) - msg_builder = MessageFactory(texts) - if len(images): - msg_builder = AggregatedMessageFactory([msg_builder, *images]) + def build_message_parts(contents: list[str | bytes]) -> tuple[list[Image], Text]: + images = [] + text_parts = [] + for content in contents: + if isinstance(content, bytes): + images.append(Image(content)) + else: + text_parts.append(content) + return images, Text("".join(text_parts)) + + images, texts = build_message_parts(contents) + + if modal_words: + texts = texts + Text(random.choice(Constants.modal_words)) + + if isinstance(event, QQMessageEvent): + msg_builder = texts + for img in images: + msg_builder = msg_builder + img + else: + msg_builder = MessageFactory(texts) + if images: + msg_builder = AggregatedMessageFactory([msg_builder, *images]) + if finish: await msg_builder.finish() else: await msg_builder.send()src/module/cp/peeper.py (1)
82-92: 优化错误重试逻辑当前代码在
_call_lib_method中硬编码了重试次数为 2,建议将其提取为常量或配置项,并考虑添加指数退避策略。在文件顶部添加常量:
MAX_RETRY_ATTEMPTS = 2修改重试逻辑:
- for _t in range(2): # 尝试2次 + for _t in range(MAX_RETRY_ATTEMPTS): id_prop = "" if no_id else f'--id {execute_conf["id"]} ' result = run_shell(f'cd {_lib_path} & python main.py {id_prop}{prop} ' f'--config {_cache_conf_payload(execute_conf)}')robot.py (1)
17-25: 考虑使用批量加载插件当前逐个加载插件的方式可以工作,但使用
nonebot.load_plugins批量加载会更简洁。如果需要控制加载顺序,可以考虑分组加载。-# nonebot.load_plugins("src/module") -nonebot.load_plugin("src.module.maintain") -nonebot.load_plugin("src.module.cron") -nonebot.load_plugin("src.module.tool.how_to_cook") -nonebot.load_plugin("src.module.interact") -nonebot.load_plugin("src.module.cp.nk") -nonebot.load_plugin("src.module.cp.atc") -nonebot.load_plugin("src.module.cp.contest_manual") -nonebot.load_plugin("src.module.cp.peeper") +# 核心模块优先加载 +core_plugins = [ + "src.module.maintain", + "src.module.cron", +] +for plugin in core_plugins: + nonebot.load_plugin(plugin) + +# 批量加载其他模块 +nonebot.load_plugins("src/module/tool") +nonebot.load_plugins("src/module/cp") +nonebot.load_plugin("src.module.interact")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (5)
requirements.txt(1 hunks)robot.py(1 hunks)src/core/bot/message.py(1 hunks)src/module/cp/peeper.py(3 hunks)src/module/tool/how_to_cook.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- requirements.txt
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/core/bot/message.py (2)
src/core/constants.py (1)
Constants(76-194)src/core/util/exception.py (1)
handle_exception(31-40)
robot.py (1)
src/core/constants.py (1)
Constants(76-194)
src/module/cp/peeper.py (5)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (2)
reply(11-37)report_exception(40-43)src/core/util/exception.py (1)
ModuleRuntimeError(12-16)src/data/data_cache.py (1)
get_cached_prefix(24-31)src/core/util/tools.py (1)
png2jpg(213-221)
🪛 Ruff (0.12.2)
robot.py
30-30: Do not call getattr with a constant attribute value. It is not any safer than normal property access.
Replace getattr with attribute access
(B009)
src/module/cp/peeper.py
175-175: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
src/module/tool/how_to_cook.py
5-5: nonebot.exception.ActionFailed imported but unused
Remove unused import: nonebot.exception.ActionFailed
(F401)
23-23: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/core/util/tools.py (2)
230-239: png2jpg 存在不必要的磁盘写入与资源释放问题,建议改为内存转换并确保关闭句柄该实现会写入临时 JPG 文件再读回 bytes,且 Image 未使用上下文管理器。改为内存中转换可减少 I/O、避免句柄泄漏。这与先前评论一致。
建议如下改动:
def png2jpg(path: str, remove_origin: bool = True) -> bytes: - img = Image.open(path) - new_path = os.path.splitext(path)[0] + '.jpg' - img.convert('RGB').save(new_path) - if remove_origin: - os.remove(path) - with open(new_path, 'rb') as f: - image_bytes = f.read() - return image_bytes + from io import BytesIO + with Image.open(path) as img: + buffer = BytesIO() + img.convert('RGB').save(buffer, format='JPEG', quality=85) + image_bytes = buffer.getvalue() + if remove_origin and os.path.exists(path): + os.remove(path) + return image_bytes需在文件顶部补充依赖:
from io import BytesIO
248-252: 修正 thefuzz 元组顺序的误用与负索引问题:当前会选错 picked 且允许 query_idx 取负数
- thefuzz 当 choices 为 dict 时返回 (key, score, value),现有实现按 (value, score, key) 处理,picked 将被错误设为 value 而非 key。
query_idx == 0的校验不足,负数将触发 Python 负索引行为,违反“从 1 开始编号”。建议修复如下:
- if len(query) == 0: - raise(ValueError("查询列表为空")) - if query_idx == 0: - raise(ValueError("查询从 1 开始编号")) + if len(query) == 0: + raise ValueError("查询列表为空") + if query_idx < 1: + raise ValueError("查询从 1 开始编号") match_results = process.extract(target, query, limit=5) if isinstance(query, dict): - # 传递 dict 时会返回 tuple(value, ratio, key) - picked_tuple = [(result[2], result[1]) for result in match_results if result[1] >= 20] # 相似度至少 20% + # dict 情况下返回 (key, ratio, value) + picked_tuple = [(result[0], result[1]) for result in match_results if result[1] >= 20] # 相似度至少 20% else: picked_tuple = [(result[0], result[1]) for result in match_results if result[1] >= 20] if len(picked_tuple) == 0: - raise(ValueError("没有找到满足条件的匹配项")) + raise ValueError("没有找到满足条件的匹配项") if query_idx > len(picked_tuple): - raise(ValueError(f"查询编号多余候选数量,当前有 {len(picked_tuple)} 个候选项")) + raise ValueError(f"查询编号多余候选数量,当前有 {len(picked_tuple)} 个候选项")如需统一语义并提升可用性,建议在 docstring 明示“当 query 为 dict 时,返回的 picked 为字典的 key(调用方可自行通过 key 取值)”。
Also applies to: 254-258, 260-262
🧹 Nitpick comments (1)
src/core/util/tools.py (1)
51-54: 保留换行符,避免把所有输出拼成一行;并避免潜在的 O(n²) 拼接开销当前对每行调用
strip()且直接info += line,会丢失换行并在长输出场景产生 O(n²) 时间复杂度。建议至少保留换行:
- info += line + info += line + "\n"进一步优化(需要同时调整未标注变更的行位点):将累积改为列表再 join
# 建议(需同步把 41 行改为 chunks = [],58 行改为 return ''.join(chunks)) chunks.append(line + "\n")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/core/util/tools.py(3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/core/util/tools.py (1)
src/core/constants.py (1)
Constants(76-194)
🔇 Additional comments (2)
src/core/util/tools.py (2)
24-24: 确认:thefuzz 已在依赖清单中并锁定版本已检查仓库依赖:requirements.txt 中存在 thefuzz==0.22.1(第15行);pyproject.toml 中未找到该依赖。
- requirements.txt — thefuzz==0.22.1(line 15)
- src/core/util/tools.py — 使用了
from thefuzz import process结论:依赖已声明并已锁定版本,无需阻塞本次变更。若项目主要使用 pyproject.toml/Poetry 管理依赖,请将该依赖同步到 pyproject.toml。
240-281: 确认:fuzzy_matching 的 dict 语义变更不会引入回归已检索仓库中所有 fuzzy_matching 调用(使用 rg),结果如下:
- src/module/tool/how_to_cook.py:39 — 调用为 fuzzy_matching(list(dishes.keys()), dish, index),传入的是 list(不是 dict),随后以 dishes[picked] 使用 picked 作为 key。
未发现其他将 dict 直接传入 fuzzy_matching 的调用点;因此将 dict 情况下的 picked 统一为 key 的修改不会影响现有代码。
| # 创建子进程,将stderr重定向到stdout以便统一读取 | ||
| cmd = subprocess.Popen( | ||
| shell, | ||
| stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, | ||
| universal_newlines=True, | ||
| shell=True, | ||
| bufsize=1 | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
加强子进程创建的安全性与鲁棒性:去掉未用的 stdin=PIPE,显式设置编码,并注意 shell=True 的注入风险
- 当前未使用 stdin,保留 PIPE 可能引入不必要的资源占用。
- 建议显式设置 encoding/错误策略,避免遇到不可解码字符时报错。
- 若 shell 字符串可受外部输入影响,shell=True 存在命令注入风险;优先考虑传入参数列表并使用 shell=False。
建议最小改动如下:
- cmd = subprocess.Popen(
- shell,
- stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,
- universal_newlines=True,
- shell=True,
- bufsize=1
- )
+ cmd = subprocess.Popen(
+ shell,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ universal_newlines=True, encoding="utf-8", errors="replace",
+ shell=True,
+ bufsize=1
+ )如需我协助改为 shell=False 的安全实现(参数列表、跨平台实时输出),可继续推进。
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # 创建子进程,将stderr重定向到stdout以便统一读取 | |
| cmd = subprocess.Popen( | |
| shell, | |
| stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, | |
| universal_newlines=True, | |
| shell=True, | |
| bufsize=1 | |
| ) | |
| # 创建子进程,将stderr重定向到stdout以便统一读取 | |
| cmd = subprocess.Popen( | |
| shell, | |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | |
| universal_newlines=True, encoding="utf-8", errors="replace", | |
| shell=True, | |
| bufsize=1 | |
| ) |
🤖 Prompt for AI Agents
In src/core/util/tools.py around lines 32 to 39, remove the unused
stdin=subprocess.PIPE, set explicit encoding and error handling (e.g.,
encoding='utf-8', errors='replace') and avoid duplicate shell=True argument; if
the command can include external input, change to passing an argument list and
use shell=False to eliminate injection risk, otherwise keep shell=True only when
the input is guaranteed safe; keep stderr redirected to STDOUT and maintain
bufsize for real-time output.
There was a problem hiding this comment.
Actionable comments posted: 3
🔭 Outside diff range comments (2)
src/data/data_cache.py (2)
10-12: 存在路径穿越风险:应校验/规范化 category,确保落盘路径不越界 cache 根目录当 category 为用户/外部输入时,传入诸如 ../../ 或绝对路径可能导致越权访问与删除。建议对 category 做规范化并校验其归属在 _cache_path 下。
可以在两个函数内就地修复,示例修改如下(保留你现有逻辑,最小入侵):
def clean_cache_hours_ago(category: str): - category_path = os.path.join(_cache_path, category) + base = os.path.abspath(_cache_path) + safe_category = os.path.normpath(str(category)).lstrip(os.sep) # 去绝对/规范化 + category_path = os.path.abspath(os.path.join(base, safe_category)) + if os.path.commonpath([base, category_path]) != base: + raise ValueError(f"Invalid cache category: {category}") if not os.path.exists(category_path): return @@ -def get_cached_prefix(category: str): - clean_cache_hours_ago(category) - - category_path = os.path.join(_cache_path, category) - if not os.path.exists(category_path): - os.makedirs(os.path.join(_cache_path, category)) +def get_cached_prefix(category: str): + base = os.path.abspath(_cache_path) + safe_category = os.path.normpath(str(category)).lstrip(os.sep) + category_path = os.path.abspath(os.path.join(base, safe_category)) + if os.path.commonpath([base, category_path]) != base: + raise ValueError(f"Invalid cache category: {category}") + clean_cache_hours_ago(safe_category) + if not os.path.exists(category_path): + os.makedirs(category_path, exist_ok=True)如需避免重复代码,可抽出一个内部工具函数用于解析 category_path,并在两处调用。
Also applies to: 24-29
15-21: 清理缓存时缺少异常处理,易在并发或权限问题下崩溃目录在列举/删除期间可能被其他线程/进程改动,或遇到权限问题。建议增加异常处理,避免清理任务把整个流程拖垮。
参考修改:
- one_hour_ago = datetime.now() - timedelta(hours=1) - for filename in os.listdir(category_path): - prefix = filename.rsplit('.', 1)[0] - if check_is_float(prefix): - file_mtime = datetime.fromtimestamp(float(prefix)) - if file_mtime < one_hour_ago: # 清理一小时前的缓存 - os.remove(os.path.join(category_path, filename)) + one_hour_ago = datetime.now() - timedelta(hours=1) + try: + for filename in os.listdir(category_path): + prefix = filename.rsplit('.', 1)[0] + if check_is_float(prefix): + file_mtime = datetime.fromtimestamp(float(prefix)) + if file_mtime < one_hour_ago: # 清理一小时前的缓存 + try: + os.remove(os.path.join(category_path, filename)) + except FileNotFoundError: + # 已被其他进程/线程删掉,忽略 + pass + except OSError: + # TODO: 记录日志,避免静默吞错 + pass + except FileNotFoundError: + # 目录在期间被删掉,直接返回 + return
♻️ Duplicate comments (1)
src/module/cp/peeper.py (1)
222-222: 模块级异步调用存在潜在问题这与之前的审查意见相同,在模块导入时使用
asyncio.run()可能导致事件循环冲突。建议使用延迟初始化或静态版本号来避免在模块级别执行异步函数。可以参考之前的审查建议进行修复。
🧹 Nitpick comments (2)
src/data/data_cache.py (1)
27-31: 复用已计算的 category_path,并容错创建目录减少重复拼接、收敛路径处理,且避免并发条件下 TOCTOU 问题造成的 FileExistsError。
建议如下修改:
def get_cached_prefix(category: str): clean_cache_hours_ago(category) category_path = os.path.join(_cache_path, category) - if not os.path.exists(category_path): - os.makedirs(os.path.join(_cache_path, category)) + if not os.path.exists(category_path): + os.makedirs(category_path, exist_ok=True) - return os.path.abspath(os.path.join(_cache_path, category, f"{datetime.now().timestamp()}")) + return os.path.abspath(os.path.join(category_path, f"{datetime.now().timestamp()}"))src/module/cp/peeper.py (1)
126-217: 代码清理工作完成良好大量注释掉的旧代码表明正在进行渐进式迁移,这是一个良好的实践。建议在迁移完成并充分测试后完全移除这些注释代码。
在确认新的 NoneBot 实现稳定后,可以完全移除这些注释掉的代码以保持代码库整洁。
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/data/data_cache.py(1 hunks)src/module/cp/peeper.py(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/data/data_cache.py (1)
src/core/constants.py (1)
get_cache_path(24-28)
src/module/cp/peeper.py (6)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (2)
reply(11-37)report_exception(40-43)src/core/util/tools.py (2)
run_shell(29-58)png2jpg(230-238)src/core/util/exception.py (1)
ModuleRuntimeError(12-16)src/core/constants.py (1)
Constants(76-194)src/data/data_cache.py (1)
get_cached_prefix(24-31)
🪛 Ruff (0.12.2)
src/module/cp/peeper.py
179-179: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🔇 Additional comments (4)
src/data/data_cache.py (1)
31-31: 改为返回绝对路径:已核查 — 暂无问题已在仓库中检索并检查所有 get_cached_prefix 的调用点与相关路径处理,结论:返回绝对路径是安全的,未发现依赖相对路径的隐式假设。要点:
- 我检查了所有调用点(典型位置):src/module/interact.py、src/module/tool/uptime.py、src/module/game/tetris.py、src/module/tool/how_to_cook.py、src/module/cp/*(nk/atc/cf/peeper)等。调用处均为直接在 cached_prefix 后拼接扩展名或把它当作完整文件路径传入(save/write_file、png2jpg、download_img、open、作为外部命令的 --output 参数、传入渲染器作为 svg_ts_path 等)。
- src/core/util/tools.py 中的 download_img 和 png2jpg 会对传入路径做父目录创建或直接读取,均支持绝对路径。
- Constants.get_cache_path 已基于项目目录构造缓存路径(绝对路径),get_cached_prefix 也在创建 category 目录后返回 abspath,目录存在性已保证。
相关代码行(最终状态):
return os.path.abspath(os.path.join(_cache_path, category, f"{datetime.now().timestamp()}"))结论:改动合理,可接受,无需进一步修改。
src/module/cp/peeper.py (3)
1-10: 导入结构良好,模块迁移清晰从 botpy 成功迁移到 NoneBot v2 框架,新增的导入项都是必要的:
asyncio用于异步操作nonebot相关模块用于命令处理和事件系统nonebot_plugin_saa用于跨适配器消息发送导入结构清晰,符合迁移目标。
107-109: 异常处理逻辑正确使用新的
report_exception函数进行异常报告,这与 NoneBot v2 的消息处理方式一致。实现正确。
191-200: 版本信息获取函数改进正确函数成功改为异步,逻辑正确。但调用方式仍存在问题。
| async def _call_lib_method(event:Event | None, prop: str, | ||
| no_id: bool = False) -> str | None: | ||
| """ | ||
| 执行 Peeper-Board-Generator 内的指令,message 可指定消息本体或消息 uuid,后者不会进行异常反馈 | ||
| """ | ||
| uuid = message.uuid if isinstance(message, RobotMessage) else message | ||
| execute_conf = _get_specified_conf(uuid) | ||
|
|
||
| if event is not None: | ||
| group_id = event.get_session_id().split("_")[1] | ||
| execute_conf = _get_specified_conf(group_id) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
函数签名改进但需要处理类型安全
函数从同步改为异步是正确的迁移方向,但 event.get_session_id().split("_")[1] 可能存在索引越界风险。
建议添加安全检查:
async def _call_lib_method(event:Event | None, prop: str,
no_id: bool = False) -> str | None:
if event is not None:
- group_id = event.get_session_id().split("_")[1]
+ session_parts = event.get_session_id().split("_")
+ if len(session_parts) < 2:
+ raise ValueError(f"Invalid session ID format: {event.get_session_id()}")
+ group_id = session_parts[1]
execute_conf = _get_specified_conf(group_id)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def _call_lib_method(event:Event | None, prop: str, | |
| no_id: bool = False) -> str | None: | |
| """ | |
| 执行 Peeper-Board-Generator 内的指令,message 可指定消息本体或消息 uuid,后者不会进行异常反馈 | |
| """ | |
| uuid = message.uuid if isinstance(message, RobotMessage) else message | |
| execute_conf = _get_specified_conf(uuid) | |
| if event is not None: | |
| group_id = event.get_session_id().split("_")[1] | |
| execute_conf = _get_specified_conf(group_id) | |
| async def _call_lib_method(event:Event | None, prop: str, | |
| no_id: bool = False) -> str | None: | |
| """ | |
| 执行 Peeper-Board-Generator 内的指令,message 可指定消息本体或消息 uuid,后者不会进行异常反馈 | |
| """ | |
| if event is not None: | |
| session_parts = event.get_session_id().split("_") | |
| if len(session_parts) < 2: | |
| raise ValueError(f"Invalid session ID format: {event.get_session_id()}") | |
| group_id = session_parts[1] | |
| execute_conf = _get_specified_conf(group_id) |
🤖 Prompt for AI Agents
In src/module/cp/peeper.py around lines 82 to 89, the code accesses
event.get_session_id().split("_")[1] which can raise IndexError or TypeError if
the session id is missing or not a string; update the function to safely obtain
the group id by first verifying event is not None and that session_id is a
non-empty str, then split with a maxsplit (or call split and check the resulting
list length > 1) before indexing, and if the check fails handle it gracefully
(return None or log and raise a clear exception) so the async function remains
type-safe and won’t crash on malformed session ids.
| yesterday_all = on_command("昨日总榜", aliases={"yesterday", "full"}, priority=50,rule=to_me(), block=True) | ||
| @yesterday_all.handle() | ||
| async def send_yesterday_board(event:Event,message:Message = CommandArg()): | ||
| single_col = (message.extract_plain_text() == "single") | ||
| await MessageFactory("正在查询昨日总榜,请稍等").send() | ||
|
|
||
| single_arg = "" if single_col else " --separate_cols" | ||
| cached_prefix = get_cached_prefix('Peeper-Board-Generator') | ||
| run = _call_lib_method(message, f"--full {single_arg} --output {cached_prefix}.png") | ||
| run = await _call_lib_method(event, f"--full {single_arg} --output {cached_prefix}.png") | ||
| if run is None: | ||
| return | ||
| await reply(["昨日卷王天梯榜",png2jpg(f"{cached_prefix}.png")], event,False,True) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
命令处理器实现基本正确但有改进空间
NoneBot v2 命令处理器的实现正确,但有一个静态分析警告需要处理。
根据 Ruff 警告,修复函数调用默认参数:
-async def send_yesterday_board(event:Event,message:Message = CommandArg()):
+async def send_yesterday_board(event: Event, message: Message = CommandArg()):实际上这个警告是误报,CommandArg() 在 NoneBot 中是正确的用法,但为了代码一致性,可以考虑使用依赖注入方式:
@yesterday_all.handle()
-async def send_yesterday_board(event:Event,message:Message = CommandArg()):
+async def send_yesterday_board(event: Event):
+ message = CommandArg()(event)
single_col = (message.extract_plain_text() == "single")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| yesterday_all = on_command("昨日总榜", aliases={"yesterday", "full"}, priority=50,rule=to_me(), block=True) | |
| @yesterday_all.handle() | |
| async def send_yesterday_board(event:Event,message:Message = CommandArg()): | |
| single_col = (message.extract_plain_text() == "single") | |
| await MessageFactory("正在查询昨日总榜,请稍等").send() | |
| single_arg = "" if single_col else " --separate_cols" | |
| cached_prefix = get_cached_prefix('Peeper-Board-Generator') | |
| run = _call_lib_method(message, f"--full {single_arg} --output {cached_prefix}.png") | |
| run = await _call_lib_method(event, f"--full {single_arg} --output {cached_prefix}.png") | |
| if run is None: | |
| return | |
| await reply(["昨日卷王天梯榜",png2jpg(f"{cached_prefix}.png")], event,False,True) | |
| yesterday_all = on_command("昨日总榜", aliases={"yesterday", "full"}, priority=50,rule=to_me(), block=True) | |
| @yesterday_all.handle() | |
| async def send_yesterday_board(event: Event): | |
| message = CommandArg()(event) | |
| single_col = (message.extract_plain_text() == "single") | |
| await MessageFactory("正在查询昨日总榜,请稍等").send() | |
| single_arg = "" if single_col else " --separate_cols" | |
| cached_prefix = get_cached_prefix('Peeper-Board-Generator') | |
| run = await _call_lib_method(event, f"--full {single_arg} --output {cached_prefix}.png") | |
| if run is None: | |
| return | |
| await reply(["昨日卷王天梯榜",png2jpg(f"{cached_prefix}.png")], event,False,True) |
🧰 Tools
🪛 Ruff (0.12.2)
179-179: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🤖 Prompt for AI Agents
In src/module/cp/peeper.py around lines 177 to 188, the handler parameter
message currently uses a default call CommandArg() which triggers a
static-analysis warning; replace the default parameter with a dependency
injection form (message: Message = Depends(CommandArg)) and add the required
import for Depends alongside CommandArg so the handler uses explicit dependency
injection instead of calling CommandArg() as a default value.
There was a problem hiding this comment.
Actionable comments posted: 3
🔭 Outside diff range comments (1)
src/module/cp/peeper.py (1)
35-36: 修复 verdict 映射表中的笔误:fe映射到了se
"fe": ["format error", "se"]明显笔误,导致format error永远无法被正确匹配到FE,同时与SE(system error)冲突。建议修复如下:
- "fe": ["format error", "se"], + "fe": ["format error", "fe"],
♻️ Duplicate comments (5)
src/module/cp/peeper.py (5)
9-9: Ruff B008:避免在默认参数中调用CommandArg(),使用依赖注入为保持一致性并消除静态检查告警,建议引入
Depends:-from nonebot.params import CommandArg +from nonebot.params import CommandArg, Depends(需与下方 handler 一起修改)
179-183: 命令处理器参数注入:使用 Depends(CommandArg) 代替默认调用当前写法在 Ruff 下触发 B008;改用依赖注入更清晰:
@yesterday_all.handle() -async def send_yesterday_board(event:Event,message:Message = CommandArg()): - single_col = (message.extract_plain_text() == "single") +async def send_yesterday_board(event: Event, message: Message = Depends(CommandArg)): + single_col = message.extract_plain_text() == "single" await MessageFactory("正在查询昨日总榜,请稍等").send()如果你更偏好保持 NoneBot 官方示例风格,也可以在该行添加
# noqa: B008抑制误报。
88-93: 安全解析 session_id,避免 IndexError 并在缺失时回退到默认配置此前已提示:
event.get_session_id().split("_")[1]存在越界风险。请在此处做健壮解析,并与上条建议结合(优先使用specified_uuid)。建议如下:- if event is not None: - group_id = event.get_session_id().split("_")[1] - execute_conf = _get_specified_conf(group_id) - else: - execute_conf = _get_specified_conf("") + if specified_uuid: + execute_conf = _get_specified_conf(specified_uuid) + elif event is not None: + session_id = event.get_session_id() or "" + parts = session_id.split("_", 1) + if len(parts) < 2: + Constants.log.warning(f"[peeper] 无法从 session_id 解析群号: {session_id!r},回退默认配置") + execute_conf = _get_specified_conf("") + else: + group_id = parts[1] + execute_conf = _get_specified_conf(group_id) + else: + execute_conf = _get_specified_conf("")
116-125: 定时任务失败与逻辑错误:参数类型不匹配、未按配置过滤、未按群组生成
- 目前调用
await _call_lib_method(uuid, ...)将str传入event,直接报错(str无get_session_id)。- 即使改为
None,也只会使用默认配置,无法针对每个uuid生成对应榜单。- 应优先读取对应配置并按
daily_report过滤,再用显式specified_uuid执行。建议修改如下(配合对
_call_lib_method的签名改造):for uuid in all_uuid: - cached_prefix = get_cached_prefix('Peeper-Board-Generator') - await _call_lib_method(uuid, f"--full --output {cached_prefix}.png") - if _get_specified_conf(uuid)["daily_report"]: - await AggregatedMessageFactory([MessageFactory("昨日卷王榜单已更新"),Image(png2jpg(f"{cached_prefix}.png"))]).send_to(TargetQQGroup(group_id=uuid),bot=get_bot()) + conf = _get_specified_conf(uuid) + if not conf["daily_report"]: + continue + cached_prefix = get_cached_prefix("Peeper-Board-Generator") + await _call_lib_method( + None, + f"--full --output {cached_prefix}.png", + specified_uuid=uuid, + ) + await AggregatedMessageFactory([ + MessageFactory("昨日卷王榜单已更新"), + Image(png2jpg(f"{cached_prefix}.png")), + ]).send_to(TargetQQGroup(group_id=uuid), bot=get_bot())如担心
get_bot()在无可用 Bot 时抛错,可在外层捕获并降级为日志告警。
222-225: 避免在导入期运行事件循环:惰性计算模块版本在模块导入时调用
asyncio.run()可能与已存在的事件循环冲突。结合src/core/bot/decorator.module对函数版本的支持,改为惰性获取更稳妥:@module( name="Peeper-Board-Generator", - version=asyncio.run(get_version_info()) + version=lambda: asyncio.run(get_version_info()) )如需完全避免在导入路径上运行事件循环,也可以在 Bot 启动完成的钩子中刷新版本缓存。
🧹 Nitpick comments (1)
src/module/cp/peeper.py (1)
187-191: 小优化:reply 的参数建议使用关键字,提升可读性并避免顺序误用当前
reply([...], event, False, True)依赖位置参数,后期维护易出错。建议改为关键字:- await reply(["昨日卷王天梯榜",png2jpg(f"{cached_prefix}.png")], event,False,True) + await reply( + ["昨日卷王天梯榜", png2jpg(f"{cached_prefix}.png")], + event, + modal_words=False, + finish=True, + )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/module/cp/peeper.py(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/module/cp/peeper.py (6)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (2)
reply(11-37)report_exception(40-43)src/core/util/tools.py (1)
run_shell(29-58)src/core/util/exception.py (1)
ModuleRuntimeError(12-16)src/core/constants.py (1)
Constants(76-194)src/data/data_cache.py (1)
get_cached_prefix(24-31)
🪛 Ruff (0.12.2)
src/module/cp/peeper.py
181-181: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
| async def _call_lib_method(event:Event | None, prop: str, | ||
| no_id: bool = False) -> str | None: | ||
| """ | ||
| 执行 Peeper-Board-Generator 内的指令,message 可指定消息本体或消息 uuid,后者不会进行异常反馈 | ||
| """ |
There was a problem hiding this comment.
🛠️ Refactor suggestion
为 _call_lib_method 增加显式 uuid 参数,避免在无事件上下文时无法选择配置
当前 daily_update_job 需要按配置 uuid 生成各自的榜单,但 _call_lib_method 只能从 event 推导 uuid,导致在定时任务中无法正确选择目标配置(更严重的是现在被错误地传入了 str 给 event 参数,见后续评论)。
建议在函数签名中增加 specified_uuid: str | None = None,优先用它获取配置,只有在未提供时才从 event 推导;并更新文档说明:
-async def _call_lib_method(event:Event | None, prop: str,
- no_id: bool = False) -> str | None:
+async def _call_lib_method(
+ event: Event | None,
+ prop: str,
+ no_id: bool = False,
+ specified_uuid: str | None = None,
+) -> str | None:
"""
- 执行 Peeper-Board-Generator 内的指令,message 可指定消息本体或消息 uuid,后者不会进行异常反馈
+ 执行 Peeper-Board-Generator 指令。
+ - event: 事件上下文(用于从会话中解析群/频道 id)
+ - specified_uuid: 显式指定配置 uuid,优先于从 event 解析
+ - no_id: 运行命令时是否省略 --id(仅用于获取版本等全局信息)
"""📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def _call_lib_method(event:Event | None, prop: str, | |
| no_id: bool = False) -> str | None: | |
| """ | |
| 执行 Peeper-Board-Generator 内的指令,message 可指定消息本体或消息 uuid,后者不会进行异常反馈 | |
| """ | |
| async def _call_lib_method( | |
| event: Event | None, | |
| prop: str, | |
| no_id: bool = False, | |
| specified_uuid: str | None = None, | |
| ) -> str | None: | |
| """ | |
| 执行 Peeper-Board-Generator 指令。 | |
| - event: 事件上下文(用于从会话中解析群/频道 id) | |
| - specified_uuid: 显式指定配置 uuid,优先于从 event 解析 | |
| - no_id: 运行命令时是否省略 --id(仅用于获取版本等全局信息) | |
| """ |
| sep = '&'if sys.platform == "win32" else '&&' | ||
| result = run_shell( | ||
| f'cd {os.path.abspath(_lib_path)} ' | ||
| f' {sep} ' | ||
| f' python main.py {id_prop}{prop} ' | ||
| f'--config "{os.path.abspath(_cache_conf_payload(execute_conf))}"' | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
避免 shell 注入与目录切换问题:改用 sys.executable + 绝对路径 + 安全转义
当前构造命令为 cd ... && python main.py ...,且拼接了未经严格转义的 prop。这存在潜在的 shell 注入风险(尽管目前参数来源看似受控),同时依赖 shell 的工作目录切换也不够稳健。建议直接调用解释器+脚本绝对路径,并对动态路径进行安全转义:
- sep = '&'if sys.platform == "win32" else '&&'
- result = run_shell(
- f'cd {os.path.abspath(_lib_path)} '
- f' {sep} '
- f' python main.py {id_prop}{prop} '
- f'--config "{os.path.abspath(_cache_conf_payload(execute_conf))}"'
- )
+ main_py = os.path.abspath(os.path.join(_lib_path, "main.py"))
+ config_path = os.path.abspath(_cache_conf_payload(execute_conf))
+ # 通过 sys.executable 确保使用当前 Python 解释器
+ result = run_shell(
+ f'{shlex.quote(sys.executable)} '
+ f'{shlex.quote(main_py)} '
+ f'{id_prop}{prop} '
+ f'--config {shlex.quote(config_path)}'
+ )此外,需要在文件头部增加导入(位于选区之外):
import shlex🤖 Prompt for AI Agents
In src/module/cp/peeper.py around lines 96 to 102, avoid building a shell
command with "cd ... && python main.py ..." and concatenating unescaped props
which risks shell injection and brittle cwd changes; instead call the Python
interpreter directly with the script's absolute path using sys.executable, build
an argv list (not a single shell string), shlex.quote any dynamic string parts
if you must pass through a shell, or better pass args as a list to
subprocess.run to bypass the shell entirely, and add "import shlex" at the file
top as requested; ensure you replace run_shell(...) with subprocess.run([...],
cwd=...) or similar to set working directory safely and pass the
id_prop/prop/config path as separate, properly escaped arguments.
| with open(os.path.join(_lib_path, "last_traceback.log"), "r", encoding='utf-8') as f: | ||
| traceback = f.read() | ||
| if traceback == "ok": | ||
| return result | ||
|
|
||
| if isinstance(message, RobotMessage): | ||
| message.report_exception('Peeper-Board-Generator', | ||
| ModuleRuntimeError(traceback.split('\n')[-2])) | ||
| if event is not None: | ||
| await report_exception(event,'Peeper-Board-Generator', | ||
| ModuleRuntimeError(traceback.split('\n')[-2])) | ||
|
|
There was a problem hiding this comment.
健壮处理 last_traceback.log:文件可能不存在且当前取倒数第二行易越界
last_traceback.log不一定存在/可读,应捕获FileNotFoundErrortraceback.split('\n')[-2]在空文件或仅 1 行时会 IndexError- 命名为
traceback也不利于可读性,建议用traceback_text
修正建议:
- with open(os.path.join(_lib_path, "last_traceback.log"), "r", encoding='utf-8') as f:
- traceback = f.read()
- if traceback == "ok":
- return result
+ try:
+ with open(os.path.join(_lib_path, "last_traceback.log"), "r", encoding="utf-8") as f:
+ traceback_text = f.read()
+ except FileNotFoundError:
+ traceback_text = ""
+ if traceback_text.strip() == "ok":
+ return result
@@
- if event is not None:
- await report_exception(event,'Peeper-Board-Generator',
- ModuleRuntimeError(traceback.split('\n')[-2]))
+ if event is not None:
+ lines = [ln for ln in traceback_text.splitlines() if ln.strip()]
+ last_line = lines[-1] if lines else "Unknown error"
+ await report_exception(
+ event,
+ "Peeper-Board-Generator",
+ ModuleRuntimeError(last_line),
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| with open(os.path.join(_lib_path, "last_traceback.log"), "r", encoding='utf-8') as f: | |
| traceback = f.read() | |
| if traceback == "ok": | |
| return result | |
| if isinstance(message, RobotMessage): | |
| message.report_exception('Peeper-Board-Generator', | |
| ModuleRuntimeError(traceback.split('\n')[-2])) | |
| if event is not None: | |
| await report_exception(event,'Peeper-Board-Generator', | |
| ModuleRuntimeError(traceback.split('\n')[-2])) | |
| try: | |
| with open(os.path.join(_lib_path, "last_traceback.log"), "r", encoding="utf-8") as f: | |
| traceback_text = f.read() | |
| except FileNotFoundError: | |
| traceback_text = "" | |
| if traceback_text.strip() == "ok": | |
| return result | |
| if event is not None: | |
| lines = [ln for ln in traceback_text.splitlines() if ln.strip()] | |
| last_line = lines[-1] if lines else "Unknown error" | |
| await report_exception( | |
| event, | |
| "Peeper-Board-Generator", | |
| ModuleRuntimeError(last_line), | |
| ) |
🤖 Prompt for AI Agents
In src/module/cp/peeper.py around lines 104 to 112, the code opens
last_traceback.log without guarding for missing/unreadable files, uses a
variable named traceback and indexes split lines with [-2] which can IndexError
on empty or single-line files; change the variable name to traceback_text, wrap
the open/read in a try/except that catches FileNotFoundError (and optionally
OSError) and treats missing file as no-op, check the read content for "ok" or
empty/whitespace and return result early, and when building the
ModuleRuntimeError safely extract the last meaningful line by splitting and
checking length (fall back to the whole text or a default message if too short)
instead of unguarded [-2] indexing.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (8)
src/module/cron.py (1)
26-29: 不要在装饰器参数中读取配置;补充异常处理并避免使用保留名 id当前在装饰器处直接访问
Constants.role_conf['heartbeat_id'],配置缺失会在导入时抛出 KeyError;同时缺少发送失败的异常处理,且参数名id会遮蔽内置函数。建议延迟在函数内读取配置,并增加健壮性处理。应用如下变更,去除装饰器中的配置读取,并在函数内获取 heartbeat_id、完善错误处理与命名:
-@scheduler.scheduled_job("cron",hour=6,id="job_ping_6",kwargs={'content':"ping",'id':Constants.role_conf['heartbeat_id']}) -@scheduler.scheduled_job("cron",hour=12,id="job_ping_12",kwargs={'content':"pong",'id':Constants.role_conf['heartbeat_id']}) -@scheduler.scheduled_job("cron",hour=18,id="job_ping_18",kwargs={'content':"pingping",'id':Constants.role_conf['heartbeat_id']}) -@scheduler.scheduled_job("cron",hour=0,id="job_ping_0",kwargs={'content':"pongpong",'id':Constants.role_conf['heartbeat_id']}) -async def heart_beat(content,id): - for bot_name in nonebot.get_adapter(OnebotAdapter).bots: - bot = nonebot.get_bots().get(bot_name) - if bot is not None: - Constants.log.info(f"[obot-heartbeat] Onebot 主人 {id} 的 Bot {bot.self_id} 正在发送心跳信息。") - await MessageFactory(content).send_to(target=TargetQQPrivate(user_id=id),bot=bot) - return +@scheduler.scheduled_job("cron", hour=6, id="job_ping_6", kwargs={"content": "ping"}) +@scheduler.scheduled_job("cron", hour=12, id="job_ping_12", kwargs={"content": "pong"}) +@scheduler.scheduled_job("cron", hour=18, id="job_ping_18", kwargs={"content": "pingping"}) +@scheduler.scheduled_job("cron", hour=0, id="job_ping_0", kwargs={"content": "pongpong"}) +async def heart_beat(content: str): + user_id = Constants.role_conf.get("heartbeat_id") + if not user_id: + Constants.log.warning("[obot-heartbeat] 心跳接收者 ID 未配置") + return + for bot_id, bot in nonebot.get_adapter(OnebotAdapter).bots.items(): + try: + Constants.log.info(f"[obot-heartbeat] Onebot 主人 {user_id} 的 Bot {bot.self_id} 正在发送心跳信息。") + await MessageFactory(content).send_to(target=TargetQQPrivate(user_id=user_id), bot=bot) + return + except Exception as e: + Constants.log.error(f"[obot-heartbeat] 发送心跳消息失败 (bot={bot_id}): {e}") + Constants.log.warning("[obot-heartbeat] 没有可用的 Bot 发送心跳消息")Also applies to: 30-36
robot.py (2)
10-10: 规范 superusers 类型并避免 KeyErrorNoneBot 期望
superusers为set[str]。当前代码直接索引配置键且未归一化类型,可能导致启动时异常或权限配置失效。- nonebot.init(superusers=Constants.role_conf['admin_id'],command_sep={' '}) + admin_id = Constants.role_conf.get("admin_id") + superusers = ( + {str(x) for x in admin_id} + if isinstance(admin_id, (list, tuple, set)) + else ({str(admin_id)} if admin_id else set()) + ) + nonebot.init(superusers=superusers, command_sep={' '})
28-31: 安全调用 register_module 并消除 Ruff B009不是所有插件都暴露
register_module;当前代码也触发 Ruff B009(对常量属性名使用 getattr 无意义)。建议做可调用性检查。- for plugin in get_loaded_plugins(): - if plugin.module_name.startswith("src"): - getattr(importlib.import_module(plugin.module_name),"register_module")() + for plugin in get_loaded_plugins(): + if plugin.module_name.startswith("src"): + module = importlib.import_module(plugin.module_name) + func = getattr(module, "register_module", None) + if callable(func): + func()src/module/cp/peeper.py (5)
83-93: 基于 Event 解析 group_id 的逻辑存在越界风险,需健壮化
event.get_session_id().split("_")[1]在格式异常时会抛出 IndexError。建议增加校验并给出明确错误信息。- if isinstance(event,Event): - group_id = event.get_session_id().split("_")[1] - execute_conf = _get_specified_conf(group_id) + if isinstance(event, Event): + session_id = event.get_session_id() + parts = session_id.split("_") if isinstance(session_id, str) else [] + if len(parts) < 2: + raise ValueError(f"Invalid session ID format: {session_id}") + group_id = parts[1] + execute_conf = _get_specified_conf(group_id) else: execute_conf = _get_specified_conf(event)
96-102: 构造 shell 命令存在注入与目录切换脆弱性,建议改为绝对路径 + 严格转义当前通过
cd && python main.py ...串接命令,且未对动态参数严格转义。建议使用sys.executable+ 绝对路径,并对动态部分做shlex.quote。- sep = '&'if sys.platform == "win32" else '&&' - result = run_shell( - f'cd {os.path.abspath(_lib_path)} ' - f' {sep} ' - f' python main.py {id_prop}{prop} ' - f'--config "{os.path.abspath(_cache_conf_payload(execute_conf))}"' - ) + main_py = os.path.abspath(os.path.join(_lib_path, "main.py")) + config_path = os.path.abspath(_cache_conf_payload(execute_conf)) + result = run_shell( + f'{shlex.quote(sys.executable)} ' + f'{shlex.quote(main_py)} ' + f'{id_prop}{prop} ' + f'--config {shlex.quote(config_path)}' + )此外需补充导入(位于本文件顶部任意合适位置):
import shlex
93-107: 健壮处理 last_traceback.log 读取与错误行提取
- 文件可能不存在或为空
- 直接使用
split('\n')[-2]易越界- 变量名
traceback易与模块名混淆- traceback = "" + traceback_text = "" @@ - with open(os.path.join(_lib_path, "last_traceback.log"), "r", encoding='utf-8') as f: - traceback = f.read() - if traceback == "ok": - return result + try: + with open(os.path.join(_lib_path, "last_traceback.log"), "r", encoding="utf-8") as f: + traceback_text = f.read() + except FileNotFoundError: + traceback_text = "" + if traceback_text.strip() == "ok": + return result @@ - if isinstance(event,Event): - await report_exception(event,'Peeper-Board-Generator', - ModuleRuntimeError(traceback.split('\n')[-2])) + if isinstance(event, Event): + lines = [ln for ln in traceback_text.splitlines() if ln.strip()] + last_line = lines[-1] if lines else "Unknown error" + await report_exception( + event, + "Peeper-Board-Generator", + ModuleRuntimeError(last_line), + )Also applies to: 109-111
179-183: 修复 B008:避免在参数默认值中调用 CommandArg按 NoneBot 依赖注入方式编写以规避静态分析告警,增强可读性。
-yesterday_all = on_command("昨日总榜", aliases={"yesterday", "full"}, priority=50,rule=to_me(), block=True) +yesterday_all = on_command("昨日总榜", aliases={"yesterday", "full"}, priority=50, rule=to_me(), block=True) @yesterday_all.handle() -async def send_yesterday_board(event:Event,message:Message = CommandArg()): +async def send_yesterday_board(event: Event, message: Message = Depends(CommandArg)): single_col = (message.extract_plain_text() == "single") await MessageFactory("正在查询昨日总榜,请稍等").send() @@ - await MessageFactory(Text("昨日卷王天梯榜")+Image(png2jpg(f"{cached_prefix}.png"))).finish() + await MessageFactory(Text("昨日卷王天梯榜") + Image(png2jpg(f"{cached_prefix}.png"))).finish()并在导入处加入 Depends(同文件第 9 行):
-from nonebot.params import CommandArg +from nonebot.params import CommandArg, DependsAlso applies to: 187-191
223-225: 避免在模块导入阶段执行异步:改为延迟计算版本号
asyncio.run(get_version_info())在导入期执行可能与运行时事件循环冲突。建议使用可调用的版本提供者。@module( name="Peeper-Board-Generator", - version=asyncio.run(get_version_info()) + version=lambda: asyncio.run(get_version_info()) )
🧹 Nitpick comments (2)
src/module/cp/peeper.py (2)
1-1: 文件头包含 BOM 字符,建议移除首行存在 BOM(零宽无间断空格),可能影响某些工具或比较器。
11-11: 移除未使用导入,保持代码整洁(Ruff F401)
AggregatedMessageFactory与reply未使用,可移除。-from nonebot_plugin_saa import MessageFactory, AggregatedMessageFactory, Image, TargetQQGroup, Text +from nonebot_plugin_saa import MessageFactory, Image, TargetQQGroup, Text @@ -from src.core.bot.message import reply, report_exception +from src.core.bot.message import report_exceptionAlso applies to: 15-15
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (3)
robot.py(1 hunks)src/module/cp/peeper.py(2 hunks)src/module/cron.py(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/module/cron.py (3)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/constants.py (1)
Constants(76-194)src/module/cp/peeper.py (1)
daily_update_job(116-125)
src/module/cp/peeper.py (5)
src/core/bot/decorator.py (2)
decorator(17-19)module(8-24)src/core/bot/message.py (2)
reply(11-37)report_exception(40-43)src/core/util/tools.py (1)
run_shell(29-58)src/core/util/exception.py (1)
ModuleRuntimeError(12-16)src/data/data_cache.py (1)
get_cached_prefix(24-31)
robot.py (1)
src/core/constants.py (1)
Constants(76-194)
🪛 Ruff (0.12.2)
src/module/cp/peeper.py
11-11: nonebot_plugin_saa.AggregatedMessageFactory imported but unused
Remove unused import: nonebot_plugin_saa.AggregatedMessageFactory
(F401)
15-15: src.core.bot.message.reply imported but unused
Remove unused import: src.core.bot.message.reply
(F401)
181-181: Do not perform function call CommandArg in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
robot.py
30-30: Do not call getattr with a constant attribute value. It is not any safer than normal property access.
Replace getattr with attribute access
(B009)
🔇 Additional comments (3)
src/module/cron.py (2)
23-25: peeper_daily_update 使用 await 正确对接异步任务,LGTM已切换为
await daily_update_job(),避免阻塞事件循环,符合 NoneBot + APScheduler 的最佳实践。
20-22: 已确认:SQLAlchemy 依赖已声明仓库依赖已包含所需包:requirements.txt 中有 sqlalchemy==2.0.43 和 APScheduler==3.10.4,pyproject.toml 已启用 nonebot_plugin_apscheduler。因而 scheduler.add_jobstore("sqlalchemy", ...) 所需的依赖在依赖声明中存在,不需要改动代码。
相关位置:
- requirements.txt — 包含:APScheduler==3.10.4、sqlalchemy==2.0.43
- pyproject.toml — plugins 中包含 "nonebot_plugin_apscheduler"
- src/module/cron.py — 调用:scheduler.add_jobstore("sqlalchemy", alias="default", url=f"sqlite:///{os.path.join(data_dir,'jobs.sqlite')}")
建议:部署时确保基于 requirements.txt 安装依赖(pip install -r requirements.txt)。
src/module/cp/peeper.py (1)
116-116: daily_update_job 迁移为异步方法,LGTM该函数签名与调用方(cron.peeper_daily_update)一致,异步链路正确。
会是一个很大的工程,并且需要非常激烈的review(
开个 draft 先。
Summary by CodeRabbit