Skip to content

[Refactor] 重构至 Nonebot#34

Draft
qwedc001 wants to merge 19 commits into
mainfrom
refactor-nonebot
Draft

[Refactor] 重构至 Nonebot#34
qwedc001 wants to merge 19 commits into
mainfrom
refactor-nonebot

Conversation

@qwedc001

@qwedc001 qwedc001 commented Aug 12, 2025

Copy link
Copy Markdown
Collaborator

会是一个很大的工程,并且需要非常激烈的review(

开个 draft 先。

Summary by CodeRabbit

  • 新功能
    • 完整迁移至 NoneBot 平台,新增基于插件的命令(比赛查询、二维码、生辰/晚安问候、帮助/关于、来道菜、导入比赛、各平台查询、维护指令等)。
  • 改进
    • 统一异步回复与异常上报,支持文本/图片合成发送、模糊匹配、图像渲染与本地缓存;引入持久化调度与定时心跳任务。
  • 杂务
    • 添加 pyproject.toml 并更新依赖以支持 NoneBot 生态;移除旧的命令注册与自动模块注册副作用,若干模块接口调整与重构。

@coderabbitai

coderabbitai Bot commented Aug 12, 2025

Copy link
Copy Markdown

Walkthrough

将运行时从 botpy 迁移到 NoneBot:新增 NoneBot 及插件依赖,移除 botpy 客户端/命令/调度实现,改为 NoneBot 初始化、适配器注册与插件化模块(on_command),并引入统一的 reply/report_exception 消息层与 APScheduler 插件。

Changes

Cohort / File(s) Change Summary
依赖与项目元数据
requirements.txt, pyproject.toml
更新 requirements.txt(删除并重新添加 imgkit==1.2.3;新增 NoneBot 相关依赖如 nonebot2[all]==2.4.3nonebot-adapter-qq==1.6.5nonebot-adapter-onebot==2.4.6nonebot-plugin-apscheduler==0.5.0nonebot-plugin-localstore==0.7.4python-dateutil==2.9.0.post0sqlalchemy==2.0.43);新增 pyproject.toml(项目元信息、NoneBot 适配器/插件声明、pytest asyncio 配置,文件新增且末尾无换行)。
运行时入口与适配器
robot.py
移除基于 botpy 的 MyClient、原事件/命令处理与 BlockingScheduler 调度;新增 NoneBot 初始化、OneBot v11/QQ 适配器注册、插件加载逻辑并以 nonebot.run() 作为入口。
交互实现迁移 / 模块新增
src/core/bot/interact.py, src/module/interact.py, src/module/tool/how_to_cook.py
删除旧交互模块 src/core/bot/interact.py;新增 src/module/interact.pysrc/module/tool/how_to_cook.py,用 NoneBot on_command 实现比赛查询、二维码、睡前、help/about、来道菜 等命令,重写输入解析、渲染/缓存/回复流程。
消息发送层重写
src/core/bot/message.py
移除 RobotMessageMessageType,新增异步 reply(contents, event, modal_words=True, finish=True)report_exception(...),改用 NoneBot/OneBot 的 Event、MessageFactory、Image、AggregatedMessageFactory 等构建与发送消息。
日志入口切换
src/core/constants.py
Constants.log 从 botpy logging 切换为 nonebot.logger 实例,仅替换日志后端实例。
工具函数与模糊匹配
src/core/util/tools.py
run_shell 改为非阻塞实时读取并记录子进程输出;png2jpg 改为返回 JPG 二进制(bytes);新增 fuzzy_matching(query, target, query_idx),并添加 thefuzz.process 导入。
调度插件新增
src/module/cron.py
新增基于 nonebot_plugin_apscheduler 的 cron 模块:配置持久化 jobs.sqlite 作业存储,新增心跳/每日更新等任务接口(调用 daily_update_job)。
维护命令插件
src/module/maintain.py
新增维护命令(配置重载、会话 ID 查询、我的 ID),使用 NoneBot 命令实现并调用 Constants.reload_conf(),通过新的 reply(...) 回复。
命令装饰器与模块注册移除
src/core/bot/decorator.py, src/module/__init__.py
删除自定义 command 装饰器的注册逻辑与 register_all_modules() 自动注册函数,保留模块注册器其余部分,移除全局命令注册路径与自动导入副作用。
CP 平台模块迁移
src/module/cp/*
src/module/cp/atc.py, src/module/cp/nk.py, src/module/cp/contest_manual.py, src/module/cp/peeper.py
将 AtCoder/NowCoder/手动比赛导入/peeper 等模块迁移为 NoneBot on_command 异步处理,新增多路命令路由、更新函数签名并使用 reply/report_exception,调整图片下载/缓存与 png2jpg 转换;部分旧实现被注释或重构为 async。
数据缓存路径
src/data/data_cache.py
get_cached_prefix(...) 的返回值改为 os.path.abspath(...),使缓存路径为绝对路径。

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: 发送定时消息至目标(群/私聊)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Possibly related PRs

Poem

我是小兔子搬依赖,NoneBot 穿上新外衣,
插件排队心跳响,比赛二维码菜香来,
日志换了新声线,命令分流更安稳,
小兔敲键把代码搬,模块上线跳舞拍 🐇✨

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-nonebot

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b19ecd3 and c9fee3d.

📒 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 保持同一大版本即可。

依上述核对后,当前依赖版本组合合理,可放心安装。

Comment thread requirements.txt
Comment thread requirements.txt
Comment thread src/core/bot/message.py Fixed
Comment thread src/module/interact.py Fixed
Comment thread src/module/interact.py Fixed
Comment thread src/module/tool/how_to_cook.py Fixed
Comment thread src/module/tool/how_to_cook.py Fixed
Comment thread src/module/tool/how_to_cook.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c9fee3d and 62559cb.

📒 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() 作为缺省参数的提示

同上,属误报,可忽略。

Comment thread src/core/bot/message.py Outdated
Comment thread src/core/bot/message.py Outdated
Comment thread src/core/util/tools.py
Comment on lines +213 to +222
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment thread src/core/util/tools.py
Comment on lines +223 to 265
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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修正 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.

Suggested change
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.

Comment thread src/module/cron.py Outdated
Comment on lines +20 to +22
@scheduler.scheduled_job("cron", id="peeper_daily_update", hour=0, minute=0, second=1)
async def peeper_daily_update():
daily_update_job()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

异步函数调用同步函数可能导致阻塞

peeper_daily_update 是异步函数,但直接调用了同步函数 daily_update_job()。如果 daily_update_job() 执行时间较长,可能会阻塞事件循环。

建议使用 asyncio.to_threadrun_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.

Suggested change
@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.

Comment thread src/module/interact.py
Comment thread src/module/maintain.py Outdated
Comment thread src/module/maintain.py Outdated
Comment thread src/module/tool/how_to_cook.py
Comment thread src/module/tool/how_to_cook.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (8)
src/core/bot/message.py (2)

3-5: 删除未使用的导入

PrivateMessageEventGroupMessageEvent 被导入但未使用,应该删除以保持代码整洁。


40-43: 修复函数签名和异步调用问题

函数存在两个问题:

  1. 函数签名与之前评论中指出的问题相同,self 参数不应存在
  2. 异步函数 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

📥 Commits

Reviewing files that changed from the base of the PR and between 62559cb and ef99d60.

📒 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.pyreply_how_to_cookmessage: 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

Import of 'ActionFailed' is not used.

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.

Suggested changeset 1
src/module/tool/how_to_cook.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/module/tool/how_to_cook.py b/src/module/tool/how_to_cook.py
--- a/src/module/tool/how_to_cook.py
+++ b/src/module/tool/how_to_cook.py
@@ -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
EOF
@@ -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
Copilot is powered by AI and may make mistakes. Always verify output.
@qwedc001 qwedc001 added enhancement New feature or request dependencies Pull requests that update a dependency file labels Aug 13, 2025
@qwedc001 qwedc001 self-assigned this Aug 13, 2025
@coderabbitai

coderabbitai Bot commented Aug 13, 2025

Copy link
Copy Markdown

Caution

An unexpected error occurred while opening a pull request: Repository rule violations found

Changes must be made through a pull request.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
+        return
src/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

📥 Commits

Reviewing files that changed from the base of the PR and between ef99d60 and 8360d74.

📒 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)

Comment thread src/core/bot/message.py Outdated
Comment on lines +40 to +43
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 py

Length of output: 1494


修复 report_exception 为异步并在调用处使用 await

report_exception 目前是同步函数,内部调用 reply(异步函数)时未加 await,导致协程对象未执行,异常信息无法发送给用户。请:

  1. 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)
  2. 在所有调用处添加 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)
  3. 批量定位剩余调用:

    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.

Comment thread src/module/interact.py

@today_contests_1.handle()
@today_contests_2.handle()
async def reply_today_contests(event: Event, message: Message = CommandArg()):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修复函数默认参数的可变对象问题

在多个处理函数的默认参数中调用 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()):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修复函数默认参数的可变对象问题

在函数默认参数中调用 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.

Suggested change
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: 移除未使用的导入 ActionFailed

ActionFailed 未被使用,且被静态分析标记。建议移除以通过 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8360d74 and ff7e055.

📒 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)

Comment thread src/module/tool/how_to_cook.py Outdated
Comment on lines +46 to +50
except Exception as e:
if isinstance(e, MatcherException):
raise e
else:
report_exception(event, "How-To-Cook", e)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

♻️ Duplicate comments (11)
src/module/cron.py (2)

23-26: 异步函数应该正确调用同步函数

peeper_daily_update 是异步函数,但注释中的 daily_update_job() 是同步函数。如果以后取消注释,直接调用可能会阻塞事件循环。

建议使用 asyncio.to_threadrun_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: 函数参数缺少空格且缺少错误处理

  1. 函数参数之间缺少空格
  2. 发送消息失败时没有异常处理
  3. 没有可用 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 MatcherException
src/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

📥 Commits

Reviewing files that changed from the base of the PR and between ff7e055 and 3e3555f.

📒 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 并在此聚合层自行过滤/截断(注意封装边界)。

我可据仓库实际实现生成最小变更方案。

Comment thread robot.py

client.run(appid=Constants.botpy_conf["appid"], secret=Constants.botpy_conf["secret"])
# 初始化 NoneBot
nonebot.init(superusers=Constants.role_conf['admin_id'],command_sep={' '})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Suggested change
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.

Comment thread robot.py
Comment on lines +27 to +30
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

调用 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.

Suggested change
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.

Comment thread src/module/cp/atc.py
Comment thread src/module/cp/atc.py
Comment thread src/module/cp/atc.py
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()):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

避免在函数默认参数中调用 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.

Suggested change
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.

Comment thread src/module/cp/nk.py


@id_query.handle()
async def send_user_id_card(event: Event, message: Message = CommandArg()):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

避免在函数默认参数中调用 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.

Suggested change
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.

Comment thread src/module/cp/nk.py
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()):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

避免在函数默认参数中调用 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.

Suggested change
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.

Comment thread src/module/cron.py
Comment on lines +27 to +30
@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']})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

配置键可能不存在导致运行时错误

在定义装饰器时直接访问 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.

Comment thread src/module/interact.py
Comment on lines +99 to +103
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
+        return
robot.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.

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3555f and 4aeb9f7.

📒 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)

Comment thread src/module/cp/peeper.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 4aeb9f7 and 298a549.

📒 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 的修改不会影响现有代码。

Comment thread src/core/util/tools.py
Comment on lines +32 to +39
# 创建子进程,将stderr重定向到stdout以便统一读取
cmd = subprocess.Popen(
shell,
stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,
universal_newlines=True,
shell=True,
bufsize=1
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
# 创建子进程,将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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 298a549 and a915239.

📒 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: 版本信息获取函数改进正确

函数成功改为异步,逻辑正确。但调用方式仍存在问题。

Comment thread src/module/cp/peeper.py Outdated
Comment on lines +82 to +89
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment thread src/module/cp/peeper.py
Comment thread src/module/cp/peeper.py Outdated
Comment on lines +177 to +188
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,直接报错(strget_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.

📥 Commits

Reviewing files that changed from the base of the PR and between a915239 and 6079d50.

📒 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)

Comment thread src/module/cp/peeper.py Outdated
Comment on lines 83 to 87
async def _call_lib_method(event:Event | None, prop: str,
no_id: bool = False) -> str | None:
"""
执行 Peeper-Board-Generator 内的指令,message 可指定消息本体或消息 uuid,后者不会进行异常反馈
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

为 _call_lib_method 增加显式 uuid 参数,避免在无事件上下文时无法选择配置

当前 daily_update_job 需要按配置 uuid 生成各自的榜单,但 _call_lib_method 只能从 event 推导 uuid,导致在定时任务中无法正确选择目标配置(更严重的是现在被错误地传入了 strevent 参数,见后续评论)。

建议在函数签名中增加 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.

Suggested change
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仅用于获取版本等全局信息
"""

Comment thread src/module/cp/peeper.py
Comment on lines +96 to +102
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))}"'
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment thread src/module/cp/peeper.py
Comment on lines 104 to 112
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]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

健壮处理 last_traceback.log:文件可能不存在且当前取倒数第二行易越界

  • last_traceback.log 不一定存在/可读,应捕获 FileNotFoundError
  • traceback.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.

Suggested change
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 类型并避免 KeyError

NoneBot 期望 superusersset[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, Depends

Also 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)

AggregatedMessageFactoryreply 未使用,可移除。

-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_exception

Also 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 6079d50 and b4b157e.

📒 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)一致,异步链路正确。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants