mirror of
https://github.com/nonebot/nonebot2.git
synced 2026-04-18 06:45:43 +00:00
🔖 Release 2.5.0
Some checks failed
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.10) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.11) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.12) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.13) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.10) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.11) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.12) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.13) (push) Failing after 3s
Pyright Lint / Pyright Lint (pydantic-v1) (push) Failing after 3s
Pyright Lint / Pyright Lint (pydantic-v2) (push) Failing after 3s
Ruff Lint / Ruff Lint (push) Failing after 3s
Site Deploy / publish (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.13) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.13) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.13) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.13) (push) Has been cancelled
Some checks failed
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.10) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.11) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.12) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.13) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.10) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.11) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.12) (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.13) (push) Failing after 3s
Pyright Lint / Pyright Lint (pydantic-v1) (push) Failing after 3s
Pyright Lint / Pyright Lint (pydantic-v2) (push) Failing after 3s
Ruff Lint / Ruff Lint (push) Failing after 3s
Site Deploy / publish (push) Failing after 3s
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.13) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.13) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.13) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.10) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.11) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.12) (push) Has been cancelled
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.13) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 编写适配器对接新的平台
|
||||
---
|
||||
|
||||
# 编写适配器
|
||||
|
||||
在编写适配器之前,我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成),适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成,在编写适配器时,我们需要继承 NoneBot 中的基类,并根据实际平台来编写每个部分功能。
|
||||
|
||||
## 组织结构
|
||||
|
||||
NoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名,并以**命名空间包**的形式编写,即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码,例如:
|
||||
|
||||
```tree
|
||||
📦 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 创建项目
|
||||
|
||||
我们可以使用脚手架快速创建项目:
|
||||
|
||||
```shell
|
||||
nb adapter create
|
||||
```
|
||||
|
||||
按照指引,输入适配器名称以及存储位置,即可创建一个带有基本结构的适配器项目。
|
||||
|
||||
## 组成部分
|
||||
|
||||
:::tip 提示
|
||||
|
||||
本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等,均为下文中适配器所编写的类,而非 NoneBot 中的基类。
|
||||
|
||||
:::
|
||||
|
||||
### Log
|
||||
|
||||
适配器在处理时通常需要打印日志,但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法,自定义一个 `log` 函数用于快捷打印适配器日志:
|
||||
|
||||
```python {3} title=log.py
|
||||
from nonebot.utils import logger_wrapper
|
||||
|
||||
log = logger_wrapper("your_adapter_name")
|
||||
```
|
||||
|
||||
这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀,它接收三个参数:日志等级、日志内容以及可选的异常,具体用法如下:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如:
|
||||
|
||||
```python title=config.py
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Config(BaseModel):
|
||||
xxx_id: str
|
||||
xxx_token: str
|
||||
```
|
||||
|
||||
配置项的读取将在下方 [Adapter](#adapter) 中介绍。
|
||||
|
||||
### Adapter
|
||||
|
||||
Adapter 负责转换事件、调用接口,以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前,我们需要继承基类,并实现适配器的基本信息:
|
||||
|
||||
```python {9,11,14,18} title=adapter.py
|
||||
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](../advanced/driver) 来帮助适配器进行网络通信,主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式,并编写相关方法用于初始化适配器,与平台建立连接和进行交互:
|
||||
|
||||
##### 客户端通信方式
|
||||
|
||||
```python {12,23,24} title=adapter.py
|
||||
import asyncio
|
||||
from typing_extensions import override
|
||||
|
||||
from nonebot import get_plugin_config
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers import Request, WebSocketClientMixin
|
||||
|
||||
class Adapter(BaseAdapter):
|
||||
@override
|
||||
def __init__(self, driver: Driver, **kwargs: Any):
|
||||
super().__init__(driver, **kwargs)
|
||||
self.adapter_config: Config = get_plugin_config(Config)
|
||||
self.task: Optional[asyncio.Task] = None # 存储 ws 任务
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
if not isinstance(self.driver, WebSocketClientMixin):
|
||||
# 判断用户配置的Driver类型是否符合适配器要求,不符合时应抛出异常
|
||||
raise RuntimeError(
|
||||
f"Current driver {self.config.driver} doesn't support websocket client connections!"
|
||||
f"{self.get_name()} Adapter need a WebSocket Client Driver to work."
|
||||
)
|
||||
# 在 NoneBot 启动和关闭时进行相关操作
|
||||
self.driver.on_startup(self.startup)
|
||||
self.driver.on_shutdown(self.shutdown)
|
||||
|
||||
async def startup(self) -> None:
|
||||
"""定义启动时的操作,例如和平台建立连接"""
|
||||
self.task = asyncio.create_task(self._forward_ws()) # 建立 ws 连接
|
||||
|
||||
async def _forward_ws(self):
|
||||
request = Request(
|
||||
method="GET",
|
||||
url="your_platform_websocket_url",
|
||||
headers={"token": "..."}, # 鉴权请求头
|
||||
)
|
||||
while True:
|
||||
try:
|
||||
async with self.websocket(request) as ws:
|
||||
try:
|
||||
# 处理 websocket
|
||||
...
|
||||
except WebSocketClosed as e:
|
||||
log(
|
||||
"ERROR",
|
||||
"<r><bg #f8bbd0>WebSocket Closed</bg #f8bbd0></r>",
|
||||
e,
|
||||
)
|
||||
except Exception as e:
|
||||
log(
|
||||
"ERROR",
|
||||
"<r><bg #f8bbd0>Error while process data from "
|
||||
"websocket platform_websocket_url. "
|
||||
"Trying to reconnect...</bg #f8bbd0></r>",
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
# 这里要断开 Bot 连接
|
||||
except Exception as e:
|
||||
# 尝试重连
|
||||
log(
|
||||
"ERROR",
|
||||
"<r><bg #f8bbd0>Error while setup websocket to "
|
||||
"platform_websocket_url. Trying to reconnect...</bg #f8bbd0></r>",
|
||||
e,
|
||||
)
|
||||
await asyncio.sleep(3) # 重连间隔
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""定义关闭时的操作,例如停止任务、断开连接"""
|
||||
|
||||
# 断开 ws 连接
|
||||
if self.task is not None and not self.task.done():
|
||||
self.task.cancel()
|
||||
```
|
||||
|
||||
##### 服务端通信方式
|
||||
|
||||
```python {30,38} title=adapter.py
|
||||
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](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST`
|
||||
- [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK`
|
||||
- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK`
|
||||
|
||||
#### 建立 Bot 连接
|
||||
|
||||
在与平台建立连接后,我们需要将 [Bot](#bot) 实例化,并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时,我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。
|
||||
|
||||
```python {7,8,11} title=adapter.py
|
||||
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](#event),并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理:
|
||||
|
||||
```python title=adapter.py
|
||||
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` 方法来发送请求。
|
||||
|
||||
```python {11} title=adapter.py
|
||||
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:
|
||||
|
||||
- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177)
|
||||
- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218)
|
||||
|
||||
HTTP:
|
||||
|
||||
- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215)
|
||||
- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266)
|
||||
- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605)
|
||||
- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253)
|
||||
- [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218)
|
||||
|
||||
### Bot
|
||||
|
||||
Bot 是机器人开发者能够直接获取并使用的核心对象,负责存储平台机器人相关信息,并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`,并实现相关方法:
|
||||
|
||||
```python {20,25,34} title=bot.py
|
||||
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`,并实现相关方法:
|
||||
|
||||
```python {5,8,13,18,23,28,33} title=event.py
|
||||
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` 方法,具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法,例如:
|
||||
|
||||
```python {7,16,20,25,34,42} title=event.py
|
||||
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` 两个类,并实现相关方法:
|
||||
|
||||
```python {9,12,17,22,27,30,36} title=message.py
|
||||
from typing import Type, Iterable
|
||||
from typing_extensions import override
|
||||
|
||||
from nonebot.utils import escape_tag
|
||||
|
||||
from nonebot.adapters import Message as BaseMessage
|
||||
from nonebot.adapters import MessageSegment as BaseMessageSegment
|
||||
|
||||
class MessageSegment(BaseMessageSegment["Message"]):
|
||||
@classmethod
|
||||
@override
|
||||
def get_message_class(cls) -> Type["Message"]:
|
||||
# 返回适配器的 Message 类型本身
|
||||
return Message
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
# 返回该消息段的纯文本表现形式,通常在日志中展示
|
||||
return "text of MessageSegment"
|
||||
|
||||
@override
|
||||
def is_text(self) -> bool:
|
||||
# 判断该消息段是否为纯文本
|
||||
return self.type == "text"
|
||||
|
||||
|
||||
class Message(BaseMessage[MessageSegment]):
|
||||
@classmethod
|
||||
@override
|
||||
def get_segment_class(cls) -> Type[MessageSegment]:
|
||||
# 返回适配器的 MessageSegment 类型本身
|
||||
return MessageSegment
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def _construct(msg: str) -> Iterable[MessageSegment]:
|
||||
# 实现从字符串中构造消息数组,如无字符串嵌入格式可直接返回文本类型 MessageSegment
|
||||
...
|
||||
```
|
||||
|
||||
然后根据平台具体的消息类型,来实现各种 `MessageSegment` 消息段,具体可以参考以下适配器:
|
||||
|
||||
- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259)
|
||||
- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520)
|
||||
- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414)
|
||||
|
||||
## 适配器测试
|
||||
|
||||
关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法:
|
||||
|
||||
1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码:
|
||||
|
||||
```python title=tests/conftest.py
|
||||
from pathlib import Path
|
||||
import nonebot.adapters
|
||||
nonebot.adapters.__path__.append( # type: ignore
|
||||
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
|
||||
)
|
||||
```
|
||||
|
||||
2. 需要计算适配器测试覆盖率,请在 `pyproject.toml` 中添加 pytest 配置:
|
||||
|
||||
```toml title=pyproject.toml
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing"
|
||||
```
|
||||
|
||||
## 后续工作
|
||||
|
||||
在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。
|
||||
|
||||
另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。
|
||||
@@ -0,0 +1,558 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 在商店发布自己的插件
|
||||
---
|
||||
|
||||
# 发布插件
|
||||
|
||||
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 提示
|
||||
本段所述的项目结构仅作推荐,不做强制要求。
|
||||
:::
|
||||
|
||||
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
||||
|
||||
插件项目的一种组织结构如下:
|
||||
|
||||
```tree
|
||||
📦 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](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
||||
|
||||
此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。
|
||||
|
||||
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
||||
|
||||
此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。
|
||||
|
||||
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
||||
|
||||
此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。
|
||||
|
||||
#### 1. 创建项目
|
||||
|
||||
1. 访问上述三个模板之一。
|
||||
2. 点击 **“Use this template”** → **“Create a new repository”**。
|
||||
3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。
|
||||
4. 点击 **“Create repository from template”**。
|
||||
|
||||
#### 2. 配置发布权限
|
||||
|
||||
1. 进入新仓库 → **Settings** → **Actions** → **General**。
|
||||
2. 在 **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'},
|
||||
]}>
|
||||
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||
|
||||
```bash
|
||||
# 安装 PDM(若未安装)
|
||||
curl -sSL https://pdm-project.org/install-pdm.py | python3 -
|
||||
|
||||
# 安装项目依赖(自动创建虚拟环境)
|
||||
pdm sync
|
||||
|
||||
# 添加新依赖(如 httpx)
|
||||
pdm add httpx
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="uv" label="uv + fllesser 模板">
|
||||
|
||||
```bash
|
||||
# 安装 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
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||
|
||||
```bash
|
||||
# 安装 Poetry(推荐方式)
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# 安装项目依赖
|
||||
poetry install
|
||||
|
||||
# 添加新依赖
|
||||
poetry add httpx
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### 5. 更新版本并发布
|
||||
|
||||
<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" },
|
||||
]}
|
||||
>
|
||||
<TabItem value="bump-my-version" label="使用 bump-my-version">
|
||||
|
||||
[bump-my-version](https://github.com/callowayproject/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'},
|
||||
]}>
|
||||
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||
|
||||
```bash
|
||||
# 安装 bump-my-version
|
||||
pdm add --dev bump-my-version
|
||||
|
||||
# 更新 patch 版本
|
||||
pdm run bump patch
|
||||
|
||||
# 推送 tag 触发发布
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="uv" label="uv + fllesser 模板">
|
||||
|
||||
```bash
|
||||
# 更新 patch 版本
|
||||
uv run poe bump patch
|
||||
|
||||
# 推送 tag 触发发布
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||
|
||||
```bash
|
||||
# 安装 bump-my-version
|
||||
poetry add --dev bump-my-version
|
||||
|
||||
# 更新 patch 版本
|
||||
poetry run bump patch
|
||||
|
||||
# 推送 tag 触发发布
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="bump-manager" label="使用项目管理器">
|
||||
|
||||
<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'},
|
||||
]}>
|
||||
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||
|
||||
需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。
|
||||
|
||||
```bash
|
||||
# 安装 pdm-bump
|
||||
pdm self add pdm-bump
|
||||
|
||||
# 更新 patch 版本
|
||||
pdm bump patch
|
||||
|
||||
# 推送 tag 触发发布
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="uv" label="uv + fllesser 模板">
|
||||
|
||||
```bash
|
||||
# 更新 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
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||
|
||||
```bash
|
||||
# 更新版本(自动提交并打标签)
|
||||
poetry version patch
|
||||
|
||||
# 推送 tag 触发发布
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="bump-manual" label="手动更新版本">
|
||||
|
||||
手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流
|
||||
|
||||
```bash
|
||||
git add pyproject.toml
|
||||
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
|
||||
git tag v0.1.1 # 替换为实际的版本号
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。
|
||||
|
||||
#### 6. 发布到 [PyPI](https://pypi.org)
|
||||
|
||||
<Tabs groupId="publish-method" defaultValue="template" values={[
|
||||
{label: '使用模板的自动发布工作流', value: 'template'},
|
||||
{label: '手动发布', value: 'manual'},
|
||||
]}>
|
||||
<TabItem value="template" label="使用模板的自动发布工作流">
|
||||
不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="manual" label="手动发布">
|
||||
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
||||
|
||||
:::tip 提示
|
||||
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),
|
||||
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
||||
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
||||
:::
|
||||
|
||||
<Tabs groupId="publishMethod">
|
||||
<TabItem value="poetry" label="Poetry" default>
|
||||
|
||||
```bash
|
||||
poetry publish --build # 构建并发布
|
||||
|
||||
# 等效于以下两个命令
|
||||
poetry build # 只构建
|
||||
poetry publish # 只发布先前的构建
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pdm" label="PDM" default>
|
||||
|
||||
```bash
|
||||
pdm publish # 构建并发布
|
||||
|
||||
# 等效于以下两个命令
|
||||
pdm build # 只构建
|
||||
pdm publish --no-build # 只发布先前的构建
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="setuptools" label="Setuptools (PEP 517)" default>
|
||||
|
||||
```bash
|
||||
pip install build twine # 安装通用构建与发布工具
|
||||
|
||||
python -m build --sdist --wheel . # 只构建
|
||||
twine upload dist/* # 只发布先前的构建
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::tip 提示
|
||||
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
||||
:::
|
||||
|
||||
## 基本要求
|
||||
|
||||
无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查:
|
||||
|
||||
### 能够正确加载
|
||||
|
||||
插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。
|
||||
|
||||
#### 依赖其他插件
|
||||
|
||||
如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。
|
||||
|
||||
使用示例如下:
|
||||
|
||||
```python title=nonebot_plugin_weather/__init__.py
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
```
|
||||
|
||||
#### 不能零配置加载的插件
|
||||
|
||||
如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。
|
||||
|
||||
但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。
|
||||
|
||||
### 插件元数据
|
||||
|
||||
插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。
|
||||
|
||||
下面是一个示例:
|
||||
|
||||
```python title=nonebot_plugin_weather/__init__.py
|
||||
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,否则应该列出插件支持的适配器。
|
||||
)
|
||||
```
|
||||
|
||||
:::caution 注意
|
||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
||||
|
||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
||||
:::
|
||||
|
||||
#### 继承其他插件支持的适配器
|
||||
|
||||
如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用
|
||||
[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。
|
||||
|
||||
示例用法如下:
|
||||
|
||||
```python title=nonebot_plugin_weather/__init__.py
|
||||
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` 为例):
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人
|
||||
```
|
||||
|
||||
**禁止**操作:
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人
|
||||
```
|
||||
|
||||
- 其他可能长时间运行阻塞事件循环的操作。
|
||||
|
||||
### 本地文件存储
|
||||
|
||||
如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。
|
||||
|
||||
参考示例:
|
||||
|
||||
```python title=nonebot_plugin_weather/__init__.py
|
||||
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 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。
|
||||
|
||||
在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。
|
||||
|
||||
完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。
|
||||
|
||||
### 等待插件审核
|
||||
|
||||
插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。
|
||||
|
||||
:::tip 提示
|
||||
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
||||
:::
|
||||
|
||||
之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。
|
||||
|
||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||
Reference in New Issue
Block a user