File: README.md
简介
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
特色
异步优先:基于 Python 的异步特性,即使是
非常大量的消息,也能吞吐自如易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑
生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 (编辑器支持)
社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 (社区资源)
海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
协议名称 状态 注释 OneBot(仓库,协议) ✅ 支持 QQ、TG、微信公众号、KOOK 等平台 Telegram(仓库,协议) ✅ 飞书(仓库,协议) ✅ GitHub(仓库,协议) ✅ GitHub APP & OAuth APP QQ(仓库,协议) ✅ QQ 官方接口调整较多 Console(仓库) ✅ 控制台交互 Red(仓库,协议) ✅ QQNT 协议 Satori(仓库,协议) ✅ 支持 Onebot、TG、飞书、微信公众号、Koishi 等 Discord(仓库,协议) ✅ Discord Bot 协议 DoDo(仓库,协议) ✅ DoDo Bot 协议 Kritor(仓库,协议) ✅ Kritor (OnebotX) 协议,QQNT 机器人接口标准 Mirai(仓库,协议) ✅ QQ 协议 Milky(仓库,协议) ✅ QQNT 机器人应用接口标准 钉钉(仓库,协议) 🤗 寻找 Maintainer(暂不可用) 开黑啦(仓库,协议) ↗️ 由社区贡献 Ntchat(仓库) ↗️ 微信协议,由社区贡献 MineCraft(仓库) ↗️ 由社区贡献 Walle-Q(仓库) ↗️ QQ 协议,由社区贡献 Villa(仓库) ❌ 米游社大别野 Bot 协议,官方已下线 Rocket.Chat(仓库,协议) ↗️ Rocket.Chat Bot 协议,由社区贡献 Tailchat(仓库,协议) ↗️ Tailchat 开放平台 Bot 协议,由社区贡献 Mail(仓库) ↗️ 邮件收发协议,由社区贡献 黑盒语音(仓库,协议) ↗️ 黑盒语音机器人协议,由社区贡献 微信公众平台(仓库,协议) ↗️ 微信公众平台协议,由社区贡献 WxClaw(仓库,协议) ↗️ 微信智能体协议,由社区贡献 Gewechat(仓库,协议) ❌ Gewechat 微信协议,Gewechat不再维护及可用 EFChat(仓库,协议) ↗️ 恒五聊平台协议,由社区贡献 VoceChat (仓库,协议) ↗️ VoceChat 平台协议,由社区贡献 B站直播间(仓库,Web API 协议,开放平台协议) ↗️ B站直播间(Web API/开放平台)协议,由社区贡献 Matrix (仓库,协议) ↗️ Matrix 协议,由社区贡献 坚实后盾:支持多种 web 框架,可自定义替换、组合
驱动框架 类型 FastAPI 服务端 Quart(异步 Flask) 服务端 aiohttp 客户端 httpx 客户端 websockets 客户端
更多:概览
什么不是 NoneBot2
NoneBot2 不是某个平台或者协议的具体实现,它只负责和已有协议适配器通信,并处理接收到的事件。所以,“NoneBot 有 blabla 平台的 blabla 功能吗?”这种问题是与 NoneBot2 无关的。请在相应平台的功能文档中确认,或与相应平台的协议适配开发者联系。
NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维护着。但是,如果你想尝试一些新功能,或者想要支持更多的平台,可以考虑使用 NoneBot2。
NoneBot2 和 NoneBot1 的区别,就像是 VisualStudio Code 和 VisualStudio 一样
即刻开始
完整文档可以在 这里 查看。
懒得看文档?下面是快速安装指南:
安装 pipx
bashpython -m pip install --user pipx python -m pipx ensurepath安装脚手架
bashpipx install nb-cli使用脚手架创建项目
bashnb create运行项目
bashnb run
社区资源
常见问题
教程/实际项目/经验分享
插件
此外,NoneBot2 还有丰富的官方以及第三方现成的插件供大家使用:
NoneBot-Plugin-Docs:离线文档至本地项目使用 (别再说文档打不开了!)
在项目目录下执行:
bashnb plugin install nonebot_plugin_docs或者尝试以下镜像:
其他插件请查看 商店
许可证
NoneBot 采用 MIT 许可证进行开源
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.贡献
请参考 贡献指南
鸣谢
赞助者
感谢以下产品对 NoneBot 项目提供的赞助:
感谢以下赞助者对 NoneBot 项目提供的资金支持:
开发者
感谢以下开发者对 NoneBot2 作出的贡献:
File: packages/nonebot-plugin-docs/README.md
使用方式
加载插件并启动 Bot ,在浏览器内打开 http://host:port/website/。
具体网址会在控制台内输出。
File: website/docs/developer/adapter-writing.md
sidebar_position: 1
编写适配器
在编写适配器之前,我们需要先了解适配器的功能与组成,适配器通常由 Adapter、Bot、Event 和 Message 四个部分组成,在编写适配器时,我们需要继承 NoneBot 中的基类,并根据实际平台来编写每个部分功能。
组织结构
NoneBot 适配器项目通常以 nonebot-adapter-{adapter-name} 作为项目名,并以命名空间包的形式编写,即在 nonebot/adapters/{adapter-name} 目录中编写实际代码,例如:
📦 nonebot-adapter-{adapter-name}
├── 📂 nonebot
│ ├── 📂 adapters
│ │ ├── 📂 {adapter-name}
│ │ │ ├── 📜 __init__.py
│ │ │ ├── 📜 adapter.py
│ │ │ ├── 📜 bot.py
│ │ │ ├── 📜 config.py
│ │ │ ├── 📜 event.py
│ │ │ └── 📜 message.py
├── 📜 pyproject.toml
└── 📜 README.md:::tip[提示]
上述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。
:::
使用 NB-CLI 创建项目
我们可以使用脚手架快速创建项目:
nb adapter create按照指引,输入适配器名称以及存储位置,即可创建一个带有基本结构的适配器项目。
组成部分
:::tip[提示]
本章节的代码中提到的 Adapter、Bot、Event 和 Message 等,均为下文中适配器所编写的类,而非 NoneBot 中的基类。
:::
Log
适配器在处理时通常需要打印日志,但直接使用 NoneBot 的默认 logger 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 logger_wrapper 方法,自定义一个 log 函数用于快捷打印适配器日志:
from nonebot.utils import logger_wrapper
log = logger_wrapper("your_adapter_name")这个 log 函数会在默认 logger 中添加适配器名称前缀,它接收三个参数:日志等级、日志内容以及可选的异常,具体用法如下:
from .log import log
log("DEBUG", "A DEBUG log.")
log("INFO", "A INFO log.")
try:
...
except Exception as e:
log("ERROR", "something error.", e)Config
通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与插件配置类似,例如:
from pydantic import BaseModel
class Config(BaseModel):
xxx_id: str
xxx_token: str配置项的读取将在下方 Adapter 中介绍。
Adapter
Adapter 负责转换事件、调用接口,以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前,我们需要继承基类,并实现适配器的基本信息:
from typing import Any
from typing_extensions import override
from nonebot.drivers import Driver
from nonebot import get_plugin_config
from nonebot.adapters import Adapter as BaseAdapter
from .config import Config
class Adapter(BaseAdapter):
@override
def __init__(self, driver: Driver, **kwargs: Any):
super().__init__(driver, **kwargs)
# 读取适配器所需的配置项
self.adapter_config: Config = get_plugin_config(Config)
@classmethod
@override
def get_name(cls) -> str:
"""适配器名称"""
return "your_adapter_name"与平台交互
NoneBot 提供了多种 Driver 来帮助适配器进行网络通信,主要分为客户端和服务端两种类型。我们需要根据平台文档和特性选择合适的通信方式,并编写相关方法用于初始化适配器,与平台建立连接和进行交互:
客户端通信方式
/* Detailed source-code truncated for AI context efficiency. */服务端通信方式
from nonebot import get_plugin_config
from nonebot.drivers import (
Request,
ASGIMixin,
WebSocket,
HTTPServerSetup,
WebSocketServerSetup
)
class Adapter(BaseAdapter):
@override
def __init__(self, driver: Driver, **kwargs: Any):
super().__init__(driver, **kwargs)
self.adapter_config: Config = get_plugin_config(Config)
self.setup()
def setup(self) -> None:
if not isinstance(self.driver, ASGIMixin):
raise RuntimeError(
f"Current driver {self.config.driver} doesn't support asgi server!"
f"{self.get_name()} Adapter need a asgi server driver to work."
)
# 建立服务端路由
# HTTP Webhook 路由
http_setup = HTTPServerSetup(
URL("your_webhook_url"), # 路由地址
"POST", # 接收的方法
"WEBHOOK name", # 路由名称
self._handle_http, # 处理函数
)
self.setup_http_server(http_setup)
# 反向 Websocket 路由
ws_setup = WebSocketServerSetup(
URL("your_websocket_url"), # 路由地址
"WebSocket name", # 路由名称
self._handle_ws, # 处理函数
)
self.setup_websocket_server(ws_setup)
async def _handle_http(self, request: Request) -> Response:
"""HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response"""
...
return Response(
status_code=200, # 状态码
headers={"something": "something"}, # 响应头
content="xxx", # 响应内容
)
async def _handle_ws(self, websocket: WebSocket) -> Any:
"""WebSocket 路由处理函数,只有一个类型为 WebSocket 的参数"""
...更多通信交互方式可以参考以下适配器:
- OneBot -
WebSocket 客户端、WebSocket 服务端、HTTP WEBHOOK、HTTP POST - QQ -
WebSocket 服务端、HTTP WEBHOOK - Telegram -
HTTP WEBHOOK
建立 Bot 连接
在与平台建立连接后,我们需要将 Bot 实例化,并调用适配器提供的的 bot_connect 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时,我们需要调用 bot_disconnect 方法告知 NoneBot 断开了 Bot 连接。
from .bot import Bot
class Adapter(BaseAdapter):
def _handle_connect(self):
bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID
bot = Bot(self, self_id=bot_id) # 实例化 Bot
self.bot_connect(bot) # 建立 Bot 连接
def _handle_disconnect(self):
self.bot_disconnect(bot) # 断开 Bot 连接转换 Event 事件
在接收到来自平台的事件数据后,我们需要将其转为适配器的 Event,并调用 Bot 的 handle_event 方法来让 Bot 对事件进行处理:
import asyncio
from typing import Any, Dict
from nonebot.compat import type_validate_python
from .bot import Bot
from .event import Event
from .log import log
class Adapter(BaseAdapter):
@classmethod
def payload_to_event(cls, payload: Dict[str, Any]) -> Event:
"""根据平台事件的特性,转换平台 payload 为具体 Event
Event 模型继承自 pydantic.BaseModel,具体请参考 pydantic 文档
"""
# 做一层异常处理,以应对平台事件数据的变更
try:
return type_validate_python(your_event_class, payload)
except Exception as e:
# 无法正常解析为具体 Event 时,给出日志提示
log(
"WARNING",
f"Parse event error: {str(payload)}",
)
# 也可以尝试转为基础 Event 进行处理
return type_validate_python(Event, payload)
async def _forward(self, bot: Bot):
payload: Dict[str, Any] # 接收到的事件数据
event = self.payload_to_event(payload)
# 让 bot 对事件进行处理
asyncio.create_task(bot.handle_event(event))调用平台 API
我们需要实现 Adapter 的 _call_api 方法,使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 send 方法来发送数据,如果采用 HTTP 请求,则需要通过 NoneBot 提供的 Request 对象,调用 driver 的 request 方法来发送请求。
from typing import Any
from typing_extensions import override
from nonebot.drivers import Request, WebSocket
from .bot import Bot
class Adapter(BaseAdapter):
@override
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示
platform_data = your_handle_data_method(data) # 自行将数据转为平台所需要的格式
# 采用 HTTP 请求的方式,需要构造一个 Request 对象
request = Request(
method="GET", # 请求方法
url=api, # 接口地址
headers=..., # 请求头,通常需要包含鉴权信息
params=platform_data, # 自行处理数据的传输形式
# json=platform_data,
# data=platform_data,
)
# 发送请求,返回结果
return await self.driver.request(request)
# 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据
# 通过某种方式获取到 bot 对应的 websocket 对象
ws: WebSocket = your_get_websocket_method(bot.self_id)
await ws.send_text(platform_data) # 发送 str 类型的数据
await ws.send_bytes(platform_data) # 发送 bytes 类型的数据
await ws.send(platform_data) # 是以上两种方式的合体
# 接收并返回结果,同样的,也有 str 和 bytes 的区别
return await ws.receive_text()
return await ws.receive_bytes()
return await ws.receive()调用平台 API 实现方式具体可以参考以下适配器:
Websocket:
HTTP:
Bot
Bot 是机器人开发者能够直接获取并使用的核心对象,负责存储平台机器人相关信息,并提供回复事件、调用 API 的上层方法。我们需要继承基类 Bot,并实现相关方法:
from typing import TYPE_CHECKING, Any, Union
from typing_extensions import override
from nonebot.message import handle_event
from nonebot.adapters import Bot as BaseBot
from .event import Event
from .message import Message, MessageSegment
if TYPE_CHECKING:
from .adapter import Adapter
class Bot(BaseBot):
"""
your_adapter_name 协议 Bot 适配。
"""
@override
def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any):
super().__init__(adapter, self_id)
self.adapter: Adapter = adapter
# 一些有关 Bot 的信息也可以在此定义和存储
async def handle_event(self, event: Event):
# 根据需要,对事件进行某些预处理,例如:
# 检查事件是否和机器人有关操作,去除事件消息首尾的 @bot
# 检查事件是否有回复消息,调用平台 API 获取原始消息的消息内容
...
# 调用 handle_event 让 NoneBot 对事件进行处理
await handle_event(self, event)
@override
async def send(
self,
event: Event,
message: Union[str, Message, MessageSegment],
**kwargs: Any,
) -> Any:
# 根据平台实现 Bot 回复事件的方法
# 将消息处理为平台所需的格式后,调用发送消息接口进行发送,例如:
data = message_to_platform_data(message)
await self.send_message(
data=data,
...
)Event
Event 是 NoneBot 中的事件主体对象,所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 Event,并实现相关方法:
from typing_extensions import override
from nonebot.compat import model_dump
from nonebot.adapters import Event as BaseEvent
class Event(BaseEvent):
@override
def get_event_name(self) -> str:
# 返回事件的名称,用于日志打印
return "event name"
@override
def get_event_description(self) -> str:
# 返回事件的描述,用于日志打印,请注意转义 loguru tag
return escape_tag(repr(model_dump(self)))
@override
def get_message(self):
# 获取事件消息的方法,根据事件具体实现,如果事件非消息类型事件,则抛出异常
raise ValueError("Event has no message!")
@override
def get_user_id(self) -> str:
# 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常
raise ValueError("Event has no context!")
@override
def get_session_id(self) -> str:
# 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常
raise ValueError("Event has no context!")
@override
def is_tome(self) -> bool:
# 判断事件是否和机器人有关
return False然后根据平台消息的类型,编写各种不同的事件,并且注意要根据事件类型实现 get_type 方法,具体请参考事件类型。消息类型事件还应重写 get_message 和 get_user_id 等方法,例如:
from .message import Message
class HeartbeatEvent(Event):
"""心跳时间,通常为元事件"""
@override
def get_type(self) -> str:
return "meta_event"
class MessageEvent(Event):
"""消息事件"""
message_id: str
user_id: str
@override
def get_type(self) -> str:
return "message"
@override
def get_message(self) -> Message:
# 返回事件消息对应的 NoneBot Message 对象
return self.message
@override
def get_user_id(self) -> str:
return self.user_id
class JoinRoomEvent(Event):
"""加入房间事件,通常为通知事件"""
user_id: str
room_id: str
@override
def get_type(self) -> str:
return "notice"
class ApplyAddFriendEvent(Event):
"""申请添加好友事件,通常为请求事件"""
user_id: str
@override
def get_type(self) -> str:
return "request"Message
Message 负责正确序列化消息,以便机器人插件处理。我们需要继承 MessageSegment 和 Message 两个类,并实现相关方法:
然后根据平台具体的消息类型,来实现各种 MessageSegment 消息段,具体可以参考以下适配器:
适配器测试
关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法:
在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的
conftest.py内添加如下代码:python title=tests/conftest.pyfrom pathlib import Path import nonebot.adapters nonebot.adapters.__path__.append( # type: ignore str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) )需要计算适配器测试覆盖率,请在
pyproject.toml中添加 pytest 配置:toml title=pyproject.toml[tool.pytest.ini_options] addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing"
后续工作
在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往商店页面,切换到适配器页签,点击发布适配器按钮,填写适配器相关信息并提交。
另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。
File: website/docs/developer/plugin-publishing.mdx
sidebar_position: 0
发布插件
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
NoneBot 为开发者提供了分享插件的官方商店。本指南囊括从创建项目到发布到 PyPI,最终提交商店审核的全过程。
:::warning[警告]
如果你的插件只是满足自用需求,则完全可以选择不发布插件。发布插件不是使用插件的必要条件。
NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。
:::
:::tip[提示]
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
:::
准备工作
插件命名规范
NoneBot 插件使用下述命名规范:
- 对于项目名,建议统一以
nonebot-plugin-开头,之后为拟定的插件名字,词间用横杠-分隔;- 项目名用于代码仓库名称、PyPI 包的发布名称等;
- 本文使用
nonebot-plugin-{your-plugin-name}为例。
- 对于模块名,建议与项目名一致,但词间用下划线
_分隔,即统一以nonebot_plugin_开头,之后为拟定的插件名字;- 模块名用于程序导入使用,应为插件文件(夹)的名称;
- 本文使用
nonebot_plugin_{your_plugin_name}为例。
项目结构
:::tip[提示]
本段所述的项目结构仅作推荐,不做强制要求。
:::
插件程序本身结构可参考插件结构一节,唯一区别在于,插件包可以直接处于项目顶层。
插件项目的一种组织结构如下:
📦 nonebot-plugin-{your-plugin-name}
├── 📂 nonebot_plugin_{your_plugin_name}
│ ├── 📜 __init__.py
│ └── 📜 config.py
├── 📜 pyproject.toml
└── 📜 README.md功能开发可以在 __init__.py 中进行或在包内部创建其他模块并在 __init__.py 中导入。
从项目模板开始
为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。
:::tip[提示]
你只需选择一条与你习惯一致的路径,完整跟随即可成功发布。无需在不同工具间切换或猜测配置。
:::
NoneBot 生态目前有如下插件项目模板:
RF-Tar-Railt/nonebot-plugin-template
此路径使用 PDM 项目管理器,符合 PEP 621 标准,自动化程度高。
fllesser/nonebot-plugin-template
此路径使用 uv 项目管理器和 PoeThePoet 任务运行器,构建速度快,适合追求效率的开发者。
A-kirami/nonebot-plugin-template
此路径使用 Poetry 项目管理器,适合熟悉传统 Python 生态的开发者。
1. 创建项目
- 访问上述三个模板之一。
- 点击 “Use this template” → “Create a new repository”。
- 仓库名称填写:
nonebot-plugin-{your-plugin-name}(此部分以nonebot-plugin-weather为例)。 - 点击 “Create repository from template”。
2. 配置发布权限
- 进入新仓库 → Settings → Actions → General。
- 在 Workflow permissions 下,勾选 “Read and write permissions” → 点击 Save。
3. 全局替换项目信息
在仓库中点击 “Add file” → “Create new file”,创建一个空文件 LICENSE,选择开源协议并提交(此操作会触发工作流)。
然后在本地克隆仓库,使用编辑器对以下内容进行全局替换:
:::tip[提示]
此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。
:::
| 原内容 | 替换为 |
|---|---|
nonebot-plugin-template |
nonebot-plugin-weather |
nonebot_plugin_template |
nonebot_plugin_weather |
<your_plugin_humanized_name> |
天气查询 |
<your_plugin_description> |
查询指定城市的实时天气与未来预报 |
<your_github> |
你的GitHub用户名 |
<your_email> |
你的邮箱 |
4. 安装依赖与开发
<Tabs groupId="publish-path" defaultValue="pdm" values={[
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
{label: 'uv + fllesser 模板', value: 'uv'},
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
]}>
# 安装 PDM(若未安装)
curl -sSL https://pdm-project.org/install-pdm.py | python3 -
# 安装项目依赖(自动创建虚拟环境)
pdm sync
# 添加新依赖(如 httpx)
pdm add httpx# 安装 uv(Windows)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
# 安装 uv(macOS/Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 安装所有依赖(含 dev)
uv sync --all-groups -p 3.12
# 添加新依赖
uv add httpx# 安装 Poetry(推荐方式)
curl -sSL https://install.python-poetry.org | python3 -
# 安装项目依赖
poetry install
# 添加新依赖
poetry add httpx5. 更新版本并发布
<Tabs
groupId="publish-path-bump"
defaultValue="bump-my-version"
values={[
{ label: "使用 bump-my-version", value: "bump-my-version" },
{ label: "使用项目管理器", value: "bump-manager" },
{ label: "手动更新版本", value: "bump-manual" },
]}
bump-my-version 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。
<Tabs groupId="publish-path" defaultValue="pdm" values={[
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
{label: 'uv + fllesser 模板', value: 'uv'},
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
]}>
# 安装 bump-my-version
pdm add --dev bump-my-version
# 更新 patch 版本
pdm run bump patch
# 推送 tag 触发发布
git push origin --tags# 更新 patch 版本
uv run poe bump patch
# 推送 tag 触发发布
git push origin --tags# 安装 bump-my-version
poetry add --dev bump-my-version
# 更新 patch 版本
poetry run bump patch
# 推送 tag 触发发布
git push origin --tags<Tabs groupId="publish-path" defaultValue="pdm" values={[
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
{label: 'uv + fllesser 模板', value: 'uv'},
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
]}>
需要安装 PDM 插件 pdm-bump。
# 安装 pdm-bump
pdm self add pdm-bump
# 更新 patch 版本
pdm bump patch
# 推送 tag 触发发布
git push origin --tags# 更新 patch 版本
uv version --bump patch
# 创建相应提交与标签
git add pyproject.toml
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
git tag v0.1.1 # 替换为实际的版本号
# 推送 tag 触发发布
git push origin --tags# 更新版本(自动提交并打标签)
poetry version patch
# 推送 tag 触发发布
git push origin --tags手动更新 pyproject.toml 中的 version 字段,然后推送 tag 触发发布工作流
git add pyproject.toml
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
git tag v0.1.1 # 替换为实际的版本号
git push origin --tags推送 v* 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。
6. 发布到 [PyPI](https://pypi.org)
<Tabs groupId="publish-method" defaultValue="template" values={[
{label: '使用模板的自动发布工作流', value: 'template'},
{label: '手动发布', value: 'manual'},
]}>
不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。
:::tip[提示]
不同构建工具的使用可能存在差别。本文仅以 pdm,poetry, setuptools
构建系统本地构建与发布为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
:::
poetry publish --build # 构建并发布
# 等效于以下两个命令
poetry build # 只构建
poetry publish # 只发布先前的构建pdm publish # 构建并发布
# 等效于以下两个命令
pdm build # 只构建
pdm publish --no-build # 只发布先前的构建pip install build twine # 安装通用构建与发布工具
python -m build --sdist --wheel . # 只构建
twine upload dist/* # 只发布先前的构建:::tip[提示]
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
:::
基本要求
无论你选择哪条路径,以下内容必须完成,否则无法通过商店自动检查:
能够正确加载
插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 NoneFlow 自动化加载测试进行。
依赖其他插件
如果插件依赖其他插件提供的功能,则必须在代码中使用 require() 来引入该插件,然后才能 import 该插件提供的功能。具体细节参阅跨插件访问。
使用示例如下:
from nonebot import require
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler不能零配置加载的插件
如果插件需要必要配置项才能正常导入,则必须在商店提交表单中填写必要的配置项内容。
但一种更好的做法是,将插件设计为零配置即可加载(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。
插件元数据
插件包必须填写元数据才能通过 NoneFlow 自动化检查。
下面是一个示例:
from nonebot.plugin import PluginMetadata
from .config import Config
__plugin_meta__ = PluginMetadata(
# 基本信息(必填)
name="天气查询", # 插件名称
description="查询指定城市的实时天气与未来预报", # 插件介绍
usage="发送【天气 城市名】获取天气信息", # 插件用法
# 发布额外信息
type="application", # 插件分类
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
# 发布必填。
config=Config,
# 插件配置项类,如果有配置类则必须填写。
supported_adapters={"~onebot.v11"},
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
# 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。
):::warning[注意]__plugin_meta__ 变量必须处于插件最外层(如 __init__.py 中),否则无法正常识别。
一般做法是在 __init__.py 中定义 __plugin_meta__。
:::
继承其他插件支持的适配器
如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用
inherit_supported_adapters() 函数来继承其他插件支持的适配器。
示例用法如下:
from nonebot import require
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
from .config import Config
require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理
__plugin_meta__ = PluginMetadata(
name="天气查询",
description="查询指定城市的实时天气与未来预报",
usage="发送【天气 城市名】获取天气信息",
type="application",
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
config=Config,
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
# 继承 nonebot_plugin_alconna 插件的适配器支持列表
)准备项目主页
通常可以使用 GitHub 项目页面作为项目主页,在 README.md 文件中编写插件介绍等内容。
内容大致包括:
- 插件功能介绍;
- 安装方法
- 必须有 NB-CLI 方式安装
- 可选依赖可以给出其他安装方式
- 不得使用旧式的
bot.py配置
- 插件配置项(如
Config类字段,若无可跳过) - 插件设置的触发规则(若无可跳过)
- 插件的其它用法(按需编写)
- 效果图、权限说明(按需编写)
质量要求
以下内容强烈建议完成,否则社区成员将会要求修改:
依赖管理原则
- 必须包含
nonebot2。 - 必须将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加
nonebot-adapter-onebot依赖; - 禁止使用
==锁定单一版本,使用>=或~=。 - 禁止添加
nonebot(V1)作为依赖。 - 所有在代码中
import的第三方库,必须在pyproject.toml的dependencies中列出。
避免误用同步操作
NoneBot 是一个异步框架,插件中禁止使用任何可能阻塞事件循环的同步操作,例如:
同步 HTTP 请求(如
requests库);推荐操作(以
httpx为例):pythonimport httpx async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人禁止操作:
pythonimport requests requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人其他可能长时间运行阻塞事件循环的操作。
本地文件存储
如果插件需要在本地存储数据、配置或缓存文件,必须使用 nonebot-plugin-localstore 管理,具体细节参阅本地存储章节。
参考示例:
from pathlib import Path
from nonebot import require
require("nonebot_plugin_localstore")
import nonebot_plugin_localstore as store
# 获取插件缓存文件(夹)路径
weather_cache_dir: Path = store.get_plugin_cache_dir()
weather_cache_file: Path = store.get_plugin_cache_file("cache.json")
# 获取插件配置文件(夹)路径
weather_config_dir: Path = store.get_plugin_config_dir()
weather_config_file: Path = store.get_plugin_config_file("config.toml")
# 获取插件数据文件(夹)路径
weather_data_dir: Path = store.get_plugin_data_dir()
weather_data_file: Path = store.get_plugin_data_file("resource-index.json")商店审核
提交申请
完成在 PyPI 的插件发布流程后,前往商店页面,切换到插件页签,点击 发布插件 按钮。
在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。
完成填写后,点击 发布 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 Submit new issue 按钮进行最终提交即可。
等待插件审核
插件发布 Issue 创建后,将会经过 NoneFlow Bot 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。
:::tip[提示]
若插件检查未通过或信息有误,不必关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
:::
之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。
完成这些步骤后,您的插件将会被自动合并到商店,而您也将成为 NoneBot 贡献者的一员。
File: website/docs/community/contact.md
sidebar-position: 0
参与讨论
如果在安装或者开发 NoneBot 过程中遇到了任何问题,或者有新奇的点子,欢迎参与我们的社区讨论:
点击下方链接前往 GitHub,前往 Issues 页面,在
New IssueTemplate 中选择Question通过 QQ 群(点击下方链接直达)
通过 QQ 频道
通过 Discord 服务器(点击下方链接直达)
File: website/docs/best-practice/alconna/uniseg/message.mdx
sidebar_position: 3
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
通用消息序列
uniseg 提供了一个类似于 Message 的 UniMessage 类型,其元素为通用消息段。
你可以用如下方式获取 UniMessage:
通过提供的 UniversalMessage 或基于 Annotated 支持的 UniMsg 依赖注入器来获取 UniMessage。
from nonebot_plugin_alconna.uniseg import UniMsg, At, Text
matcher = on_xxx(...)
@matcher.handle()
async def _(msg: UniMsg):
text = msg[Text, 0]
print(text.text)
if msg.has(At):
ats = msg.get(At)
print(ats)
...注意,generate 方法在响应器以外的地方如果不传入 event 与 bot 则无法处理 reply。
from nonebot import Message, EventMessage
from nonebot_plugin_alconna.uniseg import UniMessage
matcher = on_xxx(...)
@matcher.handle()
async def _(message: Message = EventMessage()):
msg = await UniMessage.generate(message=message)
msg1 = UniMessage.generate_without_reply(message=message)发送消息
你还可以通过 UniMessage 的 export 与 send 方法来跨平台发送消息。
UniMessage.export 会通过传入的 bot: Bot 参数,或上下文中的 Bot 对象读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列:
from nonebot import Bot, on_command
from nonebot_plugin_alconna.uniseg import Image, UniMessage
test = on_command("test")
@test.handle()
async def handle_test():
await test.send(await UniMessage(Image(path="path/to/img")).export())除此之外 UniMessage.send, .finish 方法基于 UniMessage.export 并调用各适配器下的发送消息方法,返回一个 Receipt 对象,用于修改/撤回/表态消息:
from nonebot import Bot, on_command
from nonebot_plugin_alconna.uniseg import UniMessage
test = on_command("test")
@test.handle()
async def handle():
receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
await receipt.recall(delay=1)UniMessage.send 的定义如下:
async def send(
self,
target: Event | Target | None = None,
bot: Bot | None = None,
fallback: bool | FallbackStrategy = FallbackStrategy.rollback,
at_sender: str | bool = False,
reply_to: str | bool | Reply | None = False,
**kwargs: Any,
) -> Receipt:
...target: 发送目标,支持事件和发送对象,不传入时会尝试从响应器上下文中获取。bot: 发送消息使用的 Bot 对象,若不传入则会尝试从响应器上下文中获取。fallback: 回退策略。at_sender: 是否提醒发送者,默认为False。当类型为str时,表示指定用户的 id。reply_to: 是否回复消息,默认为False。str表示消息 id。bool表示是否回复当前消息。此时target不能是发送对象。Reply表示直接使用回复元素。
**kwargs: 各Bot.send的特定参数。
而在 AlconnaMatcher 下,got, send, reject 等可以发送消息的方法皆支持使用 UniMessage,不需要手动调用 export 方法:
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
from nonebot_plugin_alconna.uniseg import At, UniMessage
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
@test_cmd.handle()
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
if target.available:
matcher.set_path_arg("target", target.result)
@test_cmd.got_path("target", prompt="请输入目标")
async def tt(target: At):
await test_cmd.send(UniMessage([target, "\ndone."]))回退策略
send 方法的 fallback 参数用于指定回退策略(即当前适配器不支持的消息段如何处理):
FallbackStrategy.ignore: 忽略未转换的消息段FallbackStrategy.to_text: 将未转换的消息段转为文本元素FallbackStrategy.rollback: 从未转换消息段的子元素中提取可能的可发送消息段FallbackStrategy.forbid: 抛出异常FallbackStrategy.auto: 插件自动选择策略
另外 fallback 传入 bool 时,True 等价于 FallbackStrategy.auto,False 等价于 FallbackStrategy.forbid。
主动发送消息
UniMessage.send 也可以用于主动发送消息:
from nonebot_plugin_alconna.uniseg import UniMessage, Target, SupportScope
from nonebot import get_driver
driver = get_driver()
@driver.on_startup
async def on_startup():
target = Target("xxxx", scope=SupportScope.qq_client)
await UniMessage("Hello!").send(target=target):::warning
在响应器以外的地方,除非启用了 alconna_apply_fetch_targets 配置项,否则 bot 参数必须手动传入。
:::
Receipt 对象
send 方法返回的 Receipt 对象可以用于修改/撤回/表态消息:
async def handle():
receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
await receipt.recall(delay=1)
recept1 = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
await recept1.edit("world!")Receipt 对象拥有以下方法:
recallable: 表明是否可以撤回recall: 撤回消息editable: 表明是否可以修改edit: 修改消息reactionable: 表明是否可以表态reaction: 表态消息get_reply: 生成对已经发送的消息的回复元素send,finish: 发送消息reply: 回复已经发送的消息
构造
如同 Message, UniMessage 可以传入单个字符串/消息段,或可迭代的字符串/消息段:
from nonebot_plugin_alconna.uniseg import UniMessage, At
msg = UniMessage("Hello")
msg1 = UniMessage(At("user", "124"))
msg2 = UniMessage(["Hello", At("user", "124")])UniMessage 上同时存在便捷方法,令其可以链式地添加消息段:
from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
assert msg == UniMessage(
["Hello", At("user", "124"), Image(path="/path/to/img")]
)使用消息模板
UniMessage.template 同样类似于 Message.template,可以用于格式化消息,大体用法参考 消息模板。
这里额外说明 UniMessage.template 的拓展控制符
相比 Message,UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
以 At(...) 为例:
>>> from nonebot_plugin_alconna.uniseg import UniMessage
>>> UniMessage.template("{:At(user, target)}").format(target="123")
UniMessage(At("user", "123"))
>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
UniMessage(At("user", "123"))
>>> UniMessage.template("{:At(type=user, target=123)}").format()
UniMessage(At("user", "123"))而在 AlconnaMatcher 中,{:XXX} 更进一步地提供了获取 event 和 bot 中的属性的功能:
from arclet.alconna import Alconna, Args
from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
@test_cmd.handle()
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
if target.available:
matcher.set_path_arg("target", target.result)
@test_cmd.got_path(
"target",
prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
)
async def tt():
await test_cmd.send(
UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
)另外也有 $message_id 与 $target 两个特殊值。
:::tip
注意到上述代码中的 {target} 了吗?
在 AlconnaMatcher 中,UniMessage.template 的格式化方法会自动将 Arparma.all_matched_args、 state 中的变量传入到 format 方法中,因此你可以直接使用上述变量。
:::
拼接消息
str、UniMessage、Segment 对象之间可以直接相加,相加均会返回一个新的 UniMessage 对象:
# 消息序列与消息段相加
UniMessage("text") + Text("text")
# 消息序列与字符串相加
UniMessage([Text("text")]) + "text"
# 消息序列与消息序列相加
UniMessage("text") + UniMessage([Text("text")])
# 字符串与消息序列相加
"text" + UniMessage([Text("text")])
# 消息段与消息段相加
Text("text") + Text("text")
# 消息段与字符串相加
Text("text") + "text"
# 消息段与消息序列相加
Text("text") + UniMessage([Text("text")])
# 字符串与消息段相加
"text" + Text("text")如果需要在当前消息序列后直接拼接新的消息段,可以使用 Message.append、Message.extend 方法,或者使用自加:
msg = UniMessage([Text("text")])
# 自加
msg += "text"
msg += Text("text")
msg += UniMessage([Text("text")])
# 附加
msg.append(Text("text"))
# 扩展
msg.extend([Text("text")])操作
检查消息段
我们可以通过 in 运算符或消息序列的 has 方法来:
# 是否存在消息段
At("user", "1234") in message
# 是否存在指定类型的消息段
At in message我们还可以使用 only 方法来检查消息中是否仅包含指定的消息段:
# 是否都为 "test"
message.only("test")
# 是否仅包含指定类型的消息段
message.only(Text)获取消息纯文本
类似于 Message.extract_plain_text(),用于获取通用消息的纯文本:
# 提取消息纯文本字符串
assert UniMessage(
[At("user", "1234"), "text"]
).extract_plain_text() == "text"遍历
通用消息序列继承自 List[Segment] ,因此可以使用 for 循环遍历消息段:
for segment in message: # type: Segment
...过滤、索引与切片
消息序列对列表的索引与切片进行了增强,在原有列表 int 索引与 slice 切片的基础上,支持 type 过滤索引与切片:
message = UniMessage(
[
Reply(...),
"text1",
At("user", "1234"),
"text2"
]
)
# 索引
message[0] == Reply(...)
# 切片
message[0:2] == UniMessage([Reply(...), Text("text1")])
# 类型过滤
message[At] == Message([At("user", "1234")])
# 类型索引
message[At, 0] == At("user", "1234")
# 类型切片
message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])我们也可以通过消息序列的 include、exclude 方法进行类型过滤:
message.include(Text, At)
message.exclude(Reply)或者使用 filter 方法:
message.filter(lambda x: isinstance(x, At) and x.flag == "user") # 仅保留 At("user", xxx) 的消息段同样的,消息序列对列表的 index、count 方法也进行了增强,可以用于索引指定类型的消息段:
# 指定类型首个消息段索引
message.index(Text) == 1
# 指定类型消息段数量
message.count(Text) == 2此外,消息序列添加了一个 get 方法,可以用于获取指定类型指定个数的消息段:
# 获取指定类型指定个数的消息段
message.get(Text, 1) == UniMessage([Text("test1")])嵌套提取
消息序列的 select 方法可以递归地从消息中选择指定类型的消息段:
message = UniMessage(
[
Text("text1"),
Image(url="url1")(
Text("text2"),
)
]
)
assert message.select(Text) == UniMessage(
[
Text("text1"),
Text("text2")
]
)转换
消息序列的 map 方法可以简单地将消息段转换为指定类型的数据:
# 转换消息段为另一类型的消息段,此时返回结果仍是 UniMessage
message.map(lambda x: Text(x.target)) # 转换为 Text 消息段
# 转换消息段为另一类型的数据,此时返回结果为 list[T]
message.map(lambda x: x.target) # 转换为 list[str]在此之上,消息序列还提供了 transform 和 transform_async 方法,允许你传入转换规则,将消息段转换为另一类型的消息段,并返回一个新的消息序列:
rule = {
"text": True,
"at": lambda attrs, children: Text(attrs["target"])
}
message.transform(rule)转换规则的类型一般为 dict[str, Transformer],以消息元素类型的名称为键,定义方式如下:
type Fragment = Segment | Segment[];
type Render<T> = (attrs: dict, children: Segment[]) => T;
type Transformer = boolean | Fragment | Render;字符串操作
类似于 str,消息序列可以通过如下方法来操作消息内的文本部分:
split,replace,startwith,endswith,removeprefix,removesuffix,strip,lstrip,rstrip,
msg = UniMessage.text("foo bar").at("1234").text("baz qux")
# 分割,返回分割结果,类型为 list[UniMessage]
parts = msg.split(" ")
# 替换,返回替换结果,类型为 UniMessage。新文本可以用 str 或 Text 来替换
new_msg = msg.replace("ba", "baaa")
# 前缀/后缀检查
msg.startswith("foo") # True
msg.endswith("qux") # True
# 去除前缀/后缀
msg1 = msg.removeprefix("foo") # UniMessage([Text(" bar"), At("user", "1234"), Text("baz qux")])
msg2 = msg.removesuffix("qux") # UniMessage([Text("foo bar"), At("user", "1234"), Text("baz ")])
# 去除空格
msg1 = msg1.lstrip() # UniMessage([Text("bar"), At("user", "1234"), Text("baz qux")])
msg2 = msg2.rstrip() # UniMessage([Text("foo bar"), At("user", "1234"), Text("baz")])持久化
特别的,UniMessage 还支持消息持久化,具体来说为 dump 与 load 方法:
msg = UniMessage.text("Hello").image(url="url")
data = msg.dump() # [{"type": "text", "text": "Hello"}, {"type": "image", "url": "url"}]
assert UniMessage.load(data) == msgdump
dump 方法的定义如下:
def dump(self, media_save_dir: str | Path | bool | None = None, json: bool = False) -> str | list[dict[str, Any]]: ...其中,media_save_dir 用于指定持久化的媒体文件存储目录:
- 若
media_save_dir为 str 或 Path,则会将媒体文件保存到指定目录下。 - 若
media_save_dir为 False,则不会保存媒体文件。 - 若
media_save_dir为 True,则会将文件数据转为 base64 编码。 - 若不指定
media_save_dir,则会尝试导入nonebot_plugin_localstore并使用其提供的路径。否则 (即localstore未安装),将会尝试使用当前工作目录。
load
load 方法的定义如下:
@classmethod
def load(cls, data: str | list[dict[str, Any]]) -> UniMessage: ...其中 data 应符合 JSON 格式。
File: website/docs/best-practice/alconna/uniseg/README.md
通用消息组件
uniseg 模块属于 nonebot-plugin-alconna 的子插件。
通用消息组件内容较多,故分为了一个示例以及数个专题。
示例
导入
一般情况下,你只需要从 nonebot_plugin_alconna.uniseg 中导入 UniMessage 即可:
from nonebot_plugin_alconna.uniseg import UniMessage构建
你可以通过 UniMessage 上的快捷方法来链式构造消息:
message = (
UniMessage.text("hello world")
.at("1234567890")
.image(url="https://example.com/image.png")
)也可以通过导入通用消息段来构建消息:
from nonebot_plugin_alconna import Text, At, Image, UniMessage
message = UniMessage(
[
Text("hello world"),
At("user", "1234567890"),
Image(url="https://example.com/image.png"),
]
)更深入一点,比如你想要发送一条包含多个按钮的消息,你可以这样做:
from nonebot_plugin_alconna import Button, UniMessage
message = (
UniMessage.text("hello world")
.keyboard(
Button("link1", url="https://example.com/1"),
Button("link2", url="https://example.com/2"),
Button("link3", url="https://example.com/3"),
row=3,
)
)发送
你可以通过 .send 方法来发送消息:
@matcher.handle()
async def _():
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
await message.send()
# 类似于 `matcher.finish`
await message.finish()你可以通过参数来让消息 @ 发送者:
@matcher.handle()
async def _():
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
await message.send(at_sender=True)或者回复消息:
@matcher.handle()
async def _():
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
await message.send(reply_to=True)撤回,编辑,表态
你可以通过 message_recall, message_edit 和 message_reaction 方法来撤回,编辑和表态消息事件。
from nonebot_plugin_alconna import message_recall, message_edit, message_reaction
@matcher.handle()
async def _():
await message_edit(UniMessage.text("hello world"))
await message_reaction("👍")
await message_recall()你也可以对你自己发送的消息进行撤回,编辑和表态:
@matcher.handle()
async def _():
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
receipt = await message.send()
await receipt.edit(UniMessage.text("hello world!"))
await receipt.reaction("👍")
await receipt.recall(delay=5) # 5秒后撤回处理消息
通过依赖注入,你可以在事件处理器中获取通用消息:
from nonebot_plugin_alconna import UniMsg
@matcher.handle()
async def _(msg: UniMsg):
...然后你可以通过 UniMessage 的方法来处理消息.
例如,你想知道消息中是否包含图片,你可以这样做:
ans1 = Image in message
ans2 = message.has(Image)
ans3 = message.only(Image)或者,提取所有的图片:
imgs_1 = message[Image]
imgs_2 = message.get(Image)
imgs_3 = message.include(Image)
imgs_4 = message.select(Image)
imgs_5 = message.filter(lambda x: x.type == "image")
imgs_6 = message.tranform({"image": True})而后,如果你想提取出所有的图片链接,你可以这样做:
urls = imgs.map(lambda x: x.url)如果你想知道消息是否符合某个前缀,你可以这样做:
@matcher.handle()
async def _(msg: UniMsg):
if msg.startswith("hello"):
await matcher.finish("hello world")
else:
await matcher.finish("not hello world")或者你想接着去除掉前缀:
@matcher.handle()
async def _(msg: UniMsg):
if msg.startswith("hello"):
msg = msg.removeprefix("hello")
await matcher.finish(msg)
else:
await matcher.finish("not hello world")持久化
假设你在编写一个词库查询插件,你可以通过 UniMessage.dump 方法来将消息序列化为 JSON 格式:
from nonebot_plugin_alconna import UniMsg
@matcher.handle()
async def _(msg: UniMsg):
data: list[dict] = msg.dump()
# 你可以将 data 存储到数据库或者 JSON 文件中而后你可以通过 UniMessage.load 方法来将 JSON 格式的消息反序列化为 UniMessage 对象:
from nonebot_plugin_alconna import UniMessage
@matcher.handle()
async def _():
data = [
{"type": "text", "text": "hello world"},
{"type": "image", "url": "https://example.com/image.png"},
]
message = UniMessage.load(data)File: website/docs/best-practice/alconna/uniseg/segment.md
sidebar_position: 2
通用消息段
通用消息段是对各适配器中的消息段的抽象总结。其可用于 Alconna 命令的参数定义,也可用于消息的构建和解析。
from nonebot_plugin_alconna import Alconna, Args, Image, on_alconna
meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image]))
@meme.handle()
async def _(img: Image):
...模型定义
注意: 本节的内容经过简化。实际情况以源码为准。
/* Detailed source-code truncated for AI context efficiency. */:::tip
或许你注意到了 Segment 上有一个 children 属性。
这是因为在 Satori 协议的规定下,一类元素可以用其子元素来代表一类兼容性消息
(例如,qq 的商场表情在某些平台上可以用图片代替)。
为此,本插件提供了 select 方法来表达 "命令中获取子元素" 的方法:
from nonebot_plugin_alconna import Args, Image, Alconna, select
from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace
# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果
alc1 = Alconna("make_meme", Args["name", str]["img", select(Image).first]) # 也可以使用 select(Image).nth(0)
# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image
alc2 = Alconna("make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]])也可以参考通用消息的 嵌套提取
:::
自定义消息段
uniseg 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化:
from dataclasses import dataclass
from nonebot.adapters import Bot
from nonebot.adapters import MessageSegment as BaseMessageSegment
from nonebot.adapters.satori import Custom, Message, MessageSegment
from nonebot_plugin_alconna.uniseg.builder import MessageBuilder
from nonebot_plugin_alconna.uniseg.exporter import MessageExporter
from nonebot_plugin_alconna.uniseg import Segment, custom_handler, custom_register
@dataclass
class MarketFace(Segment):
tabId: str
faceId: str
key: str
@custom_register(MarketFace, "chronocat:marketface")
def mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):
if not isinstance(seg, Custom):
raise ValueError("MarketFace can only be built from Satori Message")
return MarketFace(**seg.data)(*builder.generate(seg.children))
@custom_handler(MarketFace)
async def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool):
if exporter.get_message_type() is Message:
return MessageSegment("chronocat:marketface", seg.data)(await exporter.export(seg.children, bot, fallback))具体而言,你可以使用 custom_register 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 custom_handler 来增加一个从 Segment 到 MessageSegment 的处理方法。
File: website/docs/best-practice/alconna/uniseg/utils.mdx
sidebar_position: 4
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
辅助功能
uniseg 模块同时提供了多种方法以通用消息操作。
:::note
这些方法中与 event, bot 相关的参数都会尝试从上下文中获取对象。
:::
消息事件 ID
消息事件 ID 是用来标识当前消息事件的唯一 ID,通常用于回复/撤回/编辑/表态当前消息。
通过提供的 MessageId 或 MsgId 依赖注入器来获取消息事件 id。
from nonebot_plugin_alconna.uniseg import MsgId
matcher = on_xxx(...)
@matcher.handle()
asycn def _(msg_id: MsgId):
...from nonebot import Event, Bot
from nonebot_plugin_alconna.uniseg import get_message_id
matcher = on_xxx(...)
@matcher.handle()
asycn def _(bot: Bot, event: Event):
msg_id: str = get_message_id(event, bot):::warning
该方法获取的消息事件 ID 不推荐直接用于各适配器的 API 调用中,可能会操作失败。
:::
发送对象
消息发送对象是用来描述当前消息事件的可发送对象或者主动发送消息时的目标对象,它包含了以下属性:
class Target:
id: str
"""目标id;若为群聊则为 group_id 或者 channel_id,若为私聊则为 user_id"""
parent_id: str
"""父级id;若为频道则为 guild_id,其他情况下可能为空字符串(例如 Feishu 下可作为部门 id)"""
channel: bool
"""是否为频道,仅当目标平台符合频道概念时"""
private: bool
"""是否为私聊"""
source: str
"""可能的事件id"""
self_id: str | None
"""机器人id,若为 None 则 Bot 对象会随机选择"""
selector: Callable[[Bot], Awaitable[bool]] | None
"""选择器,用于在多个 Bot 对象中选择特定 Bot"""
extra: dict[str, Any]
"""额外信息,用于适配器扩展"""通过提供的 MessageTarget 或 MsgTarget 依赖注入器来获取消息发送对象。
from nonebot_plugin_alconna.uniseg import MsgTarget
matcher = on_xxx(...)
@matcher.handle()
asycn def _(target: MsgTarget):
...from nonebot import Event, Bot
from nonebot_plugin_alconna.uniseg import Target, get_target
matcher = on_xxx(...)
@matcher.handle()
asycn def _(bot: Bot, event: Event):
target: Target = get_target(event, bot)主动构造一个发送对象时,则需要如下参数:
id: 目标ID;若为群聊则为group_id或者channel_id,若为私聊则为user_idparent_id: 父级ID;若为频道则为guild_id,其他情况下可能为空字符串(例如 Feishu 下可作为部门 id)channel: 是否为频道,仅当目标平台符合频道概念时private: 是否为私聊source: 可能的事件IDself_id: 机器人id,若为 None 则 Bot 对象会随机选择selector: 选择器,用于在多个 Bot 对象中选择特定 Botscope: 平台范围,表示当前发送对象的平台类别adapter: 适配器名称,若为 None 则需要明确指定 Bot 对象platform: 平台名称,仅当目标适配器存在多个平台时使用extra: 额外信息,用于适配器扩展
通过 Target 对象,我们可以在 UniMessage.send 中指定发送对象:
from nonebot_plugin_alconna.uniseg import UniMessage, MsgTarget, Target, SupportScope
matcher = on_xxx(...)
@matcher.handle()
async def _(target: MsgTarget):
# 将消息发送给当前事件的发送者
await UniMessage("Hello!").send(target=target)
# 主动发送消息给群号为 12345 的 QQ 群聊
target1 = Target("12345", scope=SupportScope.qq_client)
await UniMessage("Hello!").send(target=target1)选择器
一般来说,主动发送消息时,UniMessage.send 或 Target.self_id 应指定一个 Bot 对象。但是这样会加重开发者的负担。
因此,我们提供了选择器来帮助开发者选择一个 Bot 对象。当然,这并非说明一定需要传入 selector 参数。
事实上,构造 Target 对象时,self_id, scope, adapter 和 platform 都会参与到 selector 的构造中。
:::tip
你其实可以使用 Target 来帮你筛选 Bot 对象:
async def _():
target = Target("12345", scope=SupportScope.qq_client)
bot = await target.select():::
若配置了 alconna_apply_fetch_targets 选项,则在启动时会主动拉取一次发送对象列表。即对于
某一主动构造的 Target 对象,插件将其与拉取下来的众多发送对象进行匹配,并选择第一个符合条件的发送对象,以选择对应的 Bot 对象。
撤回消息
通过 message_recall 方法来撤回消息事件。
from nonebot_plugin_alconna.uniseg import message_recall
matcher = on_xxx(...)
@matcher.handle()
async def _(msg_id: MsgId):
await message_recall(msg_id)message_recall 方法的参数如下:
async def message_recall(
message_id: str | None = None,
event: Event | None = None,
bot: Bot | None = None,
adapter: str | None = None
): ...当 message_id 为 None 时,插件会尝试从 event 中获取消息事件 ID。
编辑消息
通过 message_edit 方法来编辑消息事件。
from nonebot_plugin_alconna.uniseg import UniMessage, message_edit
matcher = on_xxx(...)
@matcher.handle()
async def _():
await message_edit(UniMessage.text("1234"))message_edit 方法的参数如下:
async def message_edit(
msg: UniMessage,
message_id: str | None = None,
event: Event | None = None,
bot: Bot | None = None,
adapter: str | None = None,
): ...当 message_id 为 None 时,插件会尝试从 event 中获取消息事件 ID。
表态消息
:::warning
该方法属于实验性功能。其接口可能会在未来的版本中发生变化。
:::
通过 message_reaction 方法来表态消息事件。
from nonebot_plugin_alconna.uniseg import message_reaction
matcher = on_xxx(...)
@matcher.handle()
async def _():
await message_reaction("👍")message_reaction 方法的参数如下:
async def message_reaction(
reaction: str | Emoji,
message_id: str | None = None,
event: Event | None = None,
bot: Bot | None = None,
adapter: str | None = None,
delete: bool = False,
): ...当 message_id 为 None 时,插件会尝试从 event 中获取消息事件 ID。
delete 参数表示是否删除自己的表态消息,默认为 False。
响应规则
uniseg 模块提供了两个响应规则:
at_in: 是否在消息中 @ 了指定的用户at_me: 是否在消息中 @ 了机器人
相较于 NoneBot 内置的 to_me 规则,at_me 规则只会在消息中 @ 机器人时触发。
from nonebot_plugin_alconna.uniseg import at_me
matcher = on_xxx(..., rule=at_me())