简介这是一份面向Python开发者与聊天机器人爱好者的QQ群机器人完整源码项目基于nonebot2框架构建适合希望学习事件驱动插件开发、异步编程与社交平台机器人落地的中初级开发者。项目以多用途群聊管理为核心涵盖自动回复、群管、互动娱乐与信息查询等典型场景可作为二次开发或框架学习的实践模板。压缩包共213个文件约3.04MB其中141个py源码文件构成机器人主体逻辑34个md文档提供说明与使用指引20个license文件、6张jpg与3张png示例图辅助理解功能效果另有json配置、sh脚本、js与vue前端资源及yml部署文件结构完整。目前已有633人学习下载。通过阅读源码与文档读者可掌握nonebot2插件组织方式、事件处理流程、配置管理与部署思路并借鉴其目录划分与模块复用方法快速搭建属于自己的群聊机器人。1. 多用途QQ群机器人从“能跑”到“敢长期挂机”的分水岭很多人第一次写 QQ 群机器人都是被一个很具体的需求推着走的群里要签到、要查天气、要自动回复关键词、要管理刷屏。搜到 nonebot2 之后照着文档跑通一个 echo 插件感觉一切都很美好。但真正把它丢进一个几百人的活跃群问题就来了进程半夜挂了没人知道、插件之间互相抢消息、数据库连接池被打满、群管理指令谁都能触发。这时候你才会意识到多用途 QQ 群机器人和“跑通一个 demo”完全是两码事。这篇笔记面向的是已经会一点 Python、想把机器人做成能长期稳定运行的多插件服务的从业者。我会按“协议选型 → 项目骨架 → 插件开发 → 持久化 → 权限与风控 → 排错”的顺序把 nonebot2 这套体系里真正影响落地的东西讲清楚。新手可以照着命令一步步复现熟手可以直接跳到参数和边界部分。核心结论先放这里多用途机器人的难点从来不在写插件而在协议适配层的选择、插件生命周期的管理以及你怎么处理“机器人挂了但没人发现”这件事。2. 协议适配选型为什么你的机器人总是掉线2.1 OneBot 协议与适配器的关系nonebot2 本身是一个事件驱动的机器人框架它不直接和 QQ 通信而是通过“适配器”对接某种协议实现。目前社区里最常见的做法是走 OneBot 协议OneBot V11 是事实标准由协议端负责和 QQ 客户端交互nonebot2 通过反向 WebSocket 或正向 WebSocket 接收事件、发送 API 调用。这里有个关键认知nonebot2 是“大脑”协议端是“手脚”。你的插件逻辑全在 nonebot2 里但消息能不能发出去、能不能收到群事件取决于协议端是否稳定。很多人把机器人掉线归咎于 nonebot2实际上九成问题出在协议端和网络链路上。常见做法是协议端和 nonebot2 部署在同一台机器上用反向 WebSocket 连接nonebot2 作为服务端监听协议端主动连过来。这样网络路径最短排查也最方便。2.2 反向 WS 与正向 WS 的取舍正向 WebSocket 是 nonebot2 主动去连协议端反向是协议端连 nonebot2。生产环境我一般推荐反向 WS原因有三点一是 nonebot2 作为服务端可以同时接多个协议端实例方便做多账号二是协议端重启后会自动重连不需要你手动干预三是防火墙只需要放行一个端口。配置上nonebot2 侧在.env里指定驱动和端口# .env 文件nonebot2 运行配置 DRIVER~fastapi # 使用 FastAPI 作为驱动自带 WebSocket 支持 HOST127.0.0.1 # 只监听本机协议端同机部署时最安全 PORT8080 # 反向 WS 监听端口 ONEBOT_ACCESS_TOKENyour_token_here # 访问令牌务必设置协议端侧配置连接地址为ws://127.0.0.1:8080/onebot/v11/ws并填入相同的 access token。这个 token 不是可选项公网环境下不设 token 等于把机器人 API 裸奔在互联网上任何人都能调用你的发送接口。参数说明HOST设为127.0.0.1意味着只有本机能连如果你把协议端部署在另一台机器需要改成0.0.0.0并配合防火墙白名单。PORT选一个不冲突的端口即可8080 只是惯例。ONEBOT_ACCESS_TOKEN建议用随机字符串不要用弱口令。2.3 多账号与多协议端并存多用途机器人往往需要同时服务多个群甚至多个 QQ 号。nonebot2 支持在同一进程内接入多个协议端连接每个连接对应一个“Bot”对象。你在插件里通过bot.self_id就能区分是哪个账号收到的事件。这里要注意不同协议端实例的 access token 可以不同nonebot2 会为每个连接创建独立的 Bot 实例。如果你要做跨账号消息转发需要显式指定目标bot否则默认回复到当前事件来源的账号。这个细节在写“多群广播”类插件时特别容易翻车我见过有人广播消息结果只发到了一个群就是因为没处理多 Bot 场景。3. 项目骨架把插件、配置和依赖拆干净3.1 目录结构与插件加载机制nonebot2 的项目结构直接决定了后期维护成本。我一般用官方脚手架nb-cli初始化然后按下面的结构组织# 项目根目录结构 mybot/ ├── bot.py # 入口文件加载 nonebot 并启动 ├── pyproject.toml # 依赖与 nonebot 插件声明 ├── .env # 全局配置驱动、端口、token ├── .env.prod # 生产环境覆盖配置 └── src/ └── plugins/ # 本地插件目录 ├── sign_in/ # 签到插件 │ ├── __init__.py │ └── config.py ├── group_admin/ # 群管理插件 └── weather/ # 天气查询插件bot.py的核心逻辑只有几行# bot.py 入口文件 import nonebot from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter # 注册适配器nonebot2 启动时会根据配置自动连接 nonebot.init() driver nonebot.get_driver() driver.register_adapter(OneBotV11Adapter) # 加载 src/plugins 下所有插件以及 pyproject.toml 里声明的第三方插件 nonebot.load_plugins(src/plugins) nonebot.load_from_toml(pyproject.toml) if __name__ __main__: nonebot.run()逻辑说明nonebot.init()读取.env配置并初始化驱动register_adapter把 OneBot V11 适配器挂上去load_plugins扫描本地目录每个含__init__.py的子目录会被当作插件加载。参数上load_plugins的路径是相对于运行目录的用绝对路径更稳妥避免从不同目录启动时找不到插件。3.2 配置分层.env 与插件级配置nonebot2 的配置体系支持多文件覆盖.env是基础.env.prod会在ENVIRONMENTprod时覆盖同名项。我习惯把敏感信息token、数据库密码放.env.prod把通用配置放.env。插件级配置用 Pydantic 的 BaseSettings 定义这样每个插件有自己的配置命名空间不会和全局配置打架# src/plugins/sign_in/config.py from pydantic import BaseSettings class SignInConfig(BaseSettings): sign_in_coins: int 10 # 每次签到获得积分 sign_in_limit: int 1 # 每日签到次数上限 sign_in_reset_hour: int 0 # 积分重置小时0 点 class Config: env_prefix SIGN_IN_ # 环境变量前缀避免冲突 extra ignore sign_in_config SignInConfig()这样在.env里写SIGN_IN_COINS20就能覆盖默认值。参数说明env_prefix是隔离的关键不同插件用不同前缀否则两个插件都定义LIMIT就会互相覆盖。extra ignore让插件忽略不认识的配置项避免因为全局配置里多了字段而报错。3.3 依赖管理与插件声明pyproject.toml里除了项目依赖还要声明 nonebot 插件这样load_from_toml才能识别# pyproject.toml 片段 [tool.nonebot] plugins [nonebot_plugin_apscheduler] # 第三方插件 plugin_dirs [src/plugins] # 本地插件目录 [tool.poetry.dependencies] python ^3.10 nonebot2 ^2.0.0 nonebot-adapter-onebot ^2.0.0 aiosqlite ^0.19.0 # 异步 SQLite轻量持久化这里有个血泪经验Python 版本别用太新的。3.12 刚出的时候部分 nonebot 插件依赖的 C 扩展还没适配装上去直接 import 报错。生产环境我一般锁 3.10 或 3.11稳定优先。依赖版本用^允许小版本升级但 nonebot2 和适配器建议锁死主版本避免大版本升级导致 API 不兼容。4. 插件开发从命令注册到消息处理4.1 命令注册与参数解析nonebot2 用装饰器注册命令最基础的是on_command# src/plugins/sign_in/__init__.py from nonebot import on_command from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import Message sign_in on_command(签到, priority10, blockTrue) sign_in.handle() async def handle_sign_in(bot: Bot, event: GroupMessageEvent, args: Message CommandArg()): user_id event.user_id group_id event.group_id # 这里调用持久化层记录签到下一章展开 await sign_in.finish(f用户 {user_id} 签到成功群 {group_id})逻辑说明on_command(签到)注册一个响应“签到”命令的处理器priority10决定多个插件同时匹配时的执行顺序数字越小越先执行blockTrue表示这个处理器执行后阻止更低优先级的处理器继续处理同一事件。参数上CommandArg()拿到命令后面的参数文本比如“签到 双倍”里的“双倍”。这里最容易踩的坑是block的语义。如果你写了一个通用关键词回复插件又写了一个命令插件两者都可能匹配到同一条消息。blockTrue能保证命令插件处理完就不再往下传但如果你希望多个插件都响应就要设blockFalse并小心优先级顺序。4.2 事件响应类型与规则组合除了命令nonebot2 还支持on_message、on_notice、on_request等。多用途机器人经常需要“关键词触发 权限校验 频率限制”的组合规则# 关键词触发 群聊限定 冷却时间 from nonebot import on_keyword from nonebot.rule import to_me from nonebot.permission import SUPERUSER weather on_keyword({天气, weather}, ruleto_me(), priority20, blockFalse) weather.handle() async def handle_weather(bot: Bot, event: GroupMessageEvent): city 北京 # 实际应从参数或用户配置读取 await weather.finish(f{city}今天晴25 度)逻辑说明on_keyword匹配消息中包含指定关键词的事件ruleto_me()要求消息必须 机器人避免群里随便说“天气”就触发priority20比签到低保证命令优先。参数上关键词集合用set传入匹配是“包含”语义而非精确相等。blockFalse让天气插件不阻断后续插件适合这种非独占的查询类功能。4.3 消息构造与富文本发送QQ 消息支持文本、图片、、表情等。nonebot2 用Message和MessageSegment构造from nonebot.adapters.onebot.v11 import Message, MessageSegment # 构造一条包含 和图片的消息 msg MessageSegment.at(user_id) 你的签到排名是第 3\n msg MessageSegment.image(file:///path/to/rank.png) await bot.send(eventevent, messagemsg)参数说明MessageSegment.at(user_id)生成 某人的段MessageSegment.image支持file://本地路径、http://网络地址和 base64。注意图片路径在协议端所在机器上必须可访问如果你 nonebot2 和协议端分机部署本地路径图片会发送失败这是很隐蔽的坑。5. 持久化与定时任务让数据活过重启5.1 异步 SQLite 做轻量持久化多用途机器人绕不开数据存储签到积分、用户配置、群开关。小规模场景用 SQLite 足够关键是选异步驱动别阻塞事件循环# src/plugins/sign_in/db.py import aiosqlite DB_PATH data/sign_in.db async def init_db(): async with aiosqlite.connect(DB_PATH) as db: await db.execute( CREATE TABLE IF NOT EXISTS sign_in ( user_id INTEGER, group_id INTEGER, date TEXT, coins INTEGER DEFAULT 0, PRIMARY KEY (user_id, group_id, date) ) ) await db.commit() async def add_sign_in(user_id: int, group_id: int, date: str, coins: int): async with aiosqlite.connect(DB_PATH) as db: await db.execute( INSERT OR REPLACE INTO sign_in (user_id, group_id, date, coins) VALUES (?, ?, ?, ?), (user_id, group_id, date, coins) ) await db.commit()逻辑说明init_db在机器人启动时调用一次建表add_sign_in用INSERT OR REPLACE保证同一天重复签到不会报错而是覆盖。参数上主键设为(user_id, group_id, date)三元组这样同一用户在不同群的签到互不影响。DB_PATH建议放在项目data/目录下方便备份。注意aiosqlite 每次操作都开新连接高频写入场景会有性能问题。如果签到量很大应该维护一个长连接或改用连接池。我一般会在启动时初始化一个全局连接但要注意 SQLite 的并发写限制必要时加锁。5.2 APScheduler 定时任务签到重置、定时提醒、每日播报都靠定时任务。nonebot2 社区常用nonebot_plugin_apschedulerfrom nonebot import require require(nonebot_plugin_apscheduler) from nonebot_plugin_apscheduler import scheduler # 每天 0 点重置签到状态 scheduler.scheduled_job(cron, hour0, minute0, idreset_sign_in) async def reset_sign_in(): # 实际逻辑清理或归档昨日数据 pass参数说明cron表达式和 Linux crontab 一致hour0, minute0表示每天零点。id必须唯一否则重复注册会报错。定时任务里如果要发消息需要拿到 Bot 实例可以用nonebot.get_bots()遍历所有在线 Bot但要注意判断 Bot 是否在线离线时发送会抛异常。5.3 数据备份与迁移SQLite 的好处是单文件备份就是复制文件。但机器人运行中直接复制可能拿到不一致的快照正确做法是用 SQLite 的备份 API 或先停服再复制。我一般写一个定时任务每天凌晨用VACUUM INTO导出到备份目录async def backup_db(): async with aiosqlite.connect(DB_PATH) as db: await db.execute(VACUUM INTO data/backup/sign_in_backup.db)这个命令会生成一个整理过的副本比直接cp安全。迁移时把备份文件拷到新机器改一下DB_PATH即可。6. 权限、风控与避坑排查6.1 权限分级与超级用户多用途机器人必须做权限分级否则群里任何人都能触发管理指令。nonebot2 内置SUPERUSER权限在.env里配置SUPERUSERS[123456789] # QQ 号列表JSON 格式插件里用permissionSUPERUSER限定只有超管能用。更细的群管理员权限需要自己查群成员信息from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent async def is_group_admin(bot: Bot, event: GroupMessageEvent) - bool: info await bot.get_group_member_info(group_idevent.group_id, user_idevent.user_id) return info[role] in (admin, owner)参数说明get_group_member_info返回的role字段有owner、admin、member三种。注意这个 API 调用有频率限制别在每条消息里都查应该缓存结果。6.2 避坑排查五条血泪记录现象一机器人启动后收不到任何群消息。原因通常是协议端没连上或者 access token 不匹配。解决先看 nonebot2 日志有没有 WebSocket 连接记录再看协议端日志有没有握手失败。token 不一致时协议端会反复重连但 nonebot2 侧看不到 Bot 上线。现象二插件加载报 “Plugin already loaded”。原因是同一个插件被load_plugins和load_from_toml重复加载。解决检查pyproject.toml的plugins列表和plugin_dirs是否有重叠本地插件不要同时写进plugins。现象三定时任务不执行。常见原因是require(nonebot_plugin_apscheduler)没写或者 scheduler 在 nonebot 初始化之前就被导入。解决确保require在插件模块顶层调用且bot.py里先nonebot.init()再加载插件。现象四SQLite 报 “database is locked”。多个协程同时写同一个库导致。解决写操作串行化或者改用 WAL 模式PRAGMA journal_modeWAL能显著缓解并发写冲突。现象五图片发送失败但文本正常。原因是图片路径在协议端机器上不存在或者用了file://但协议端没有读权限。解决改用网络图片 URL或者把图片放到协议端能访问的共享目录。6.3 日志与监控机器人长期挂机日志是唯一的黑匣子。nonebot2 默认用 loguru我一般配置日志轮转和错误上报from nonebot.log import logger logger.add(logs/bot_{time}.log, rotation1 day, retention7 days, levelINFO)参数说明rotation1 day每天切一个文件retention7 days只保留 7 天避免日志撑爆磁盘。levelINFO记录常规信息调试时临时改成DEBUG。更进一步可以接一个健康检查接口用外部监控定时探测机器人挂了能收到告警。7. 进阶用依赖注入把插件写成可测试的模块写到一定规模你会发现插件里的逻辑和 nonebot 的事件对象耦合太紧想写单元测试很痛苦。nonebot2 的依赖注入系统Depends就是解决这个问题的。它允许你把“获取数据库连接”“获取用户配置”这类操作抽成依赖在测试时替换成假实现。from nonebot.params import Depends async def get_user_coins(user_id: int, group_id: int) - int: # 真实实现查数据库 return 0 sign_in.handle() async def handle(bot: Bot, event: GroupMessageEvent, coins: int Depends(get_user_coins)): await sign_in.finish(f你当前积分 {coins})这样get_user_coins可以被单独测试也可以在测试时用nonebot.params.Depends的覆盖机制替换。参数上依赖函数的参数会被 nonebot 自动从事件上下文解析user_id和group_id这种名字能直接匹配到事件字段。验证方法上我习惯给每个插件写一个最小的 pytest 用例用nonebot.testing提供的工具构造假事件断言处理器的输出。这样改代码时能快速发现回归比每次手动在群里发消息测试高效得多。最后一个具体技巧把“机器人是否在线”做成一个内部 API用nonebot.get_bots()返回的字典判断。如果某个 Bot 掉线定时任务里发消息前先检查避免异常刷屏日志。这个习惯帮我省了很多半夜被报警叫醒的时间。希望帮到你。本文还有配套的精品资源点击获取