## File: README.md

## 简介

NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架，它基于 Python 的类型注解和异步特性，能够为你的需求实现提供便捷灵活的支持。

## 特色

- 异步优先：基于 Python 的异步特性，即使是~~非常~~大量的消息，也能吞吐自如
- 易于开发：配合 NB-CLI 脚手架，代码编写上手简单，没有过多的冗余代码，可以让开发者专注于业务逻辑
- 生而可靠：100% 类型注解覆盖，配合编辑器的类型推导功能，能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://nonebot.dev/docs/editor-support))
- 社区丰富：社区用户众多，直接和间接用户超过十万人，每天都有大量的活跃用户 ([社区资源](#社区资源))
- 海纳百川：一个框架，支持多个聊天软件平台，可自定义通信协议

  |                                                       协议名称                                                        | 状态 |                                   注释                                    |
  | :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |
  |               OneBot（[仓库](https://github.com/nonebot/adapter-onebot)，[协议](https://onebot.dev/)）                |  ✅  | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |
  |      Telegram（[仓库](https://github.com/nonebot/adapter-telegram)，[协议](https://core.telegram.org/bots/api)）      |  ✅  |                                                                           |
  |     飞书（[仓库](https://github.com/nonebot/adapter-feishu)，[协议](https://open.feishu.cn/document/home/index)）     |  ✅  |                                                                           |
  |         GitHub（[仓库](https://github.com/nonebot/adapter-github)，[协议](https://docs.github.com/en/apps)）          |  ✅  |                          GitHub APP & OAuth APP                           |
  |                QQ（[仓库](https://github.com/nonebot/adapter-qq)，[协议](https://bot.q.qq.com/wiki/)）                |  ✅  |                            QQ 官方接口调整较多                            |
  |                             Console（[仓库](https://github.com/nonebot/adapter-console)）                             |  ✅  |                                控制台交互                                 |
  |     Red（[仓库](https://github.com/nonebot/adapter-red)，[协议](https://chrononeko.github.io/QQNTRedProtocol/)）      |  ✅  |                                 QQNT 协议                                  |
  |           Satori（[仓库](https://github.com/nonebot/adapter-satori)，[协议](https://satori.js.org/zh-CN)）            |  ✅  |               支持 Onebot、TG、飞书、微信公众号、Koishi 等                |
  |   Discord（[仓库](https://github.com/nonebot/adapter-discord)，[协议](https://discord.com/developers/docs/intro)）    |  ✅  |                             Discord Bot 协议                              |
  |               DoDo（[仓库](https://github.com/nonebot/adapter-dodo)，[协议](https://open.imdodo.com/)）               |  ✅  |                               DoDo Bot 协议                               |
  |        Kritor（[仓库](https://github.com/nonebot/adapter-kritor)，[协议](https://github.com/KarinJS/kritor)）         |  ✅  |                Kritor (OnebotX) 协议，QQNT 机器人接口标准                  |
  |    Mirai（[仓库](https://github.com/nonebot/adapter-mirai)，[协议](https://docs.mirai.mamoe.net/mirai-api-http/)）    |  ✅  |                                  QQ 协议                                  |
  |    Milky（[仓库](https://github.com/nonebot/adapter-milky)，[协议](https://milky.ntqqrev.org/)）                      |  ✅  |                           QQNT 机器人应用接口标准                          |
  |         钉钉（[仓库](https://github.com/nonebot/adapter-ding)，[协议](https://open.dingtalk.com/document/)）          |  🤗  |                        寻找 Maintainer（暂不可用）                        |
  |     开黑啦（[仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila)，[协议](https://developer.kookapp.cn/)）     |  ↗️  |                                由社区贡献                                 |
  |                          Ntchat（[仓库](https://github.com/JustUndertaker/adapter-ntchat)）                           |  ↗️  |                           微信协议，由社区贡献                            |
  |                      MineCraft（[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)）                      |  ↗️  |                                由社区贡献                                 |
  |                       Walle-Q（[仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)）                       |  ↗️  |                            QQ 协议，由社区贡献                            |
  |                       Villa（[仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)）                        |  ❌  |                     米游社大别野 Bot 协议，官方已下线                     |
  | Rocket.Chat（[仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat)，[协议](https://developer.rocket.chat/)） |  ↗️  |                     Rocket.Chat Bot 协议，由社区贡献                      |
  |     Tailchat（[仓库](https://github.com/eya46/nonebot-adapter-tailchat)，[协议](https://tailchat.msgbyte.com/)）      |  ↗️  |                  Tailchat 开放平台 Bot 协议，由社区贡献                   |
  |                             Mail（[仓库](https://github.com/mobyw/nonebot-adapter-mail)）                             |  ↗️  |                         邮件收发协议，由社区贡献                          |
  |     黑盒语音（[仓库](https://github.com/lclbm/adapter-heybox)，[协议](https://github.com/QingFengOpen/HeychatDoc)）     |  ↗️  |                       黑盒语音机器人协议，由社区贡献                             |
  | 微信公众平台（[仓库](https://github.com/YangRucheng/nonebot-adapter-wxmp)，[协议](https://developers.weixin.qq.com/doc/)）|  ↗️  |                       微信公众平台协议，由社区贡献                             |
  | WxClaw（[仓库](https://github.com/shoucandanghehe/nonebot-adapter-wxclaw)，[协议](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin)） |  ↗️  |                  微信智能体协议，由社区贡献                   |
  | Gewechat（[仓库](https://github.com/Shine-Light/nonebot-adapter-gewechat)，[协议](https://github.com/Devo919/Gewechat)）|  ❌  |                      Gewechat 微信协议，Gewechat不再维护及可用                            |
  |  EFChat（[仓库](https://github.com/molanp/nonebot_adapter_efchat)，[协议](https://irinu-live.melon.fish/efc-help/)）   |  ↗️  |                            恒五聊平台协议，由社区贡献                          |
  |  VoceChat （[仓库](https://github.com/5656565566/nonebot-adapter-vocechat)，[协议](https://doc.voce.chat/zh-cn/bot/bot-and-webhook)）   |  ↗️  |                            VoceChat 平台协议，由社区贡献                          |
  |  B站直播间（[仓库](https://github.com/MingxuanGame/nonebot-adapter-bilibili-live)，[Web API 协议](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/live)，[开放平台协议](https://open-live.bilibili.com/document)）   |  ↗️  |                            B站直播间（Web API/开放平台）协议，由社区贡献                          |
  |  Matrix ([仓库](https://github.com/elysia-best/adapter-matrix)，[协议](https://spec.matrix.org/v1.18/)) | ↗️ | Matrix 协议，由社区贡献 |

- 坚实后盾：支持多种 web 框架，可自定义替换、组合

  |                              驱动框架                               |  类型  |
  | :-----------------------------------------------------------------: | :----: |
  |              [FastAPI](https://fastapi.tiangolo.com/)               | 服务端 |
  | [Quart](https://quart.palletsprojects.com/en/latest/)（异步 Flask） | 服务端 |
  |           [aiohttp](https://docs.aiohttp.org/en/stable/)            | 客户端 |
  |               [httpx](https://www.python-httpx.org/)                | 客户端 |
  |     [websockets](https://websockets.readthedocs.io/en/stable/)      | 客户端 |

更多：[概览](https://nonebot.dev/docs/)

## 什么不是 NoneBot2

NoneBot2 不是某个平台或者协议的具体实现，它只负责和已有协议适配器通信，并处理接收到的事件。所以，“NoneBot 有 blabla 平台的 blabla 功能吗？”这种问题是与 NoneBot2 无关的。请在相应平台的功能文档中确认，或与相应平台的协议适配开发者联系。

NoneBot2 不是 NoneBot1 的替代品。事实上，它们都在被积极的维护着。但是，如果你想尝试一些新功能，或者想要支持更多的平台，可以考虑使用 NoneBot2。

> ~~NoneBot2 和 NoneBot1 的区别，就像是 VisualStudio Code 和 VisualStudio 一样~~

## 即刻开始

~~完整~~文档可以在 [这里](https://nonebot.dev/) 查看。

懒得看文档？下面是快速安装指南：

1. 安装 [pipx](https://pypa.github.io/pipx/)

   ```bash
   python -m pip install --user pipx
   python -m pipx ensurepath
   ```

2. 安装脚手架

   ```bash
   pipx install nb-cli
   ```

3. 使用脚手架创建项目

   ```bash
   nb create
   ```

4. 运行项目

   ```bash
   nb run
   ```

## 社区资源

### 常见问题

- [常见问题解答(FAQ)](https://faq.nonebot.dev/)
- [论坛(Discussion)](https://discussions.nonebot.dev/)

### 教程/实际项目/经验分享

- [awesome-nonebot](https://github.com/nonebot/awesome-nonebot)

### 插件

此外，NoneBot2 还有丰富的官方以及第三方现成的插件供大家使用：

- [NoneBot-Plugin-Docs](https://github.com/nonebot/nonebot2/tree/master/packages/nonebot-plugin-docs)：离线文档至本地项目使用 (别再说文档打不开了！)

  在项目目录下执行：

  ```bash
  nb plugin install nonebot_plugin_docs
  ```

  或者尝试以下镜像：

  - [文档镜像(中国境内)](https://nb2.baka.icu)

- 其他插件请查看 [商店](https://nonebot.dev/store/plugins)

## 许可证

`NoneBot` 采用 `MIT` 许可证进行开源

```text
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.
```

## 贡献

请参考 [贡献指南](./CONTRIBUTING.md)

## 鸣谢

### 赞助者

感谢以下产品对 NoneBot 项目提供的赞助：





感谢以下赞助者对 NoneBot 项目提供的资金支持：

<a href="https://assets.nonebot.dev/sponsors.svg">
  
</a>

### 开发者

感谢以下开发者对 NoneBot2 作出的贡献：

<a href="https://github.com/nonebot/nonebot2/graphs/contributors">
  
</a>

---

## File: packages/nonebot-plugin-docs/README.md

## 使用方式

加载插件并启动 Bot ，在浏览器内打开 `http://host:port/website/`。

具体网址会在控制台内输出。

---

## File: website/docs/developer/adapter-writing.md

---
sidebar_position: 1
---

# 编写适配器

在编写适配器之前，我们需要先了解[适配器的功能与组成](../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) 来帮助适配器进行网络通信，主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式，并编写相关方法用于初始化适配器，与平台建立连接和进行交互：

##### 客户端通信方式

```
/* Detailed source-code truncated for AI context efficiency. */
```

##### 服务端通信方式

```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)页面，切换到适配器页签，点击**发布适配器**按钮，填写适配器相关信息并提交。

另外建议编写适配器文档或者一些插件开发示例，以便其他开发者使用我们的适配器。

---

## 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[提示]
本段所述的项目结构仅作推荐，不做强制要求。
:::

插件程序本身结构可参考[插件结构](../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，否则应该列出插件支持的适配器。
)
```

:::warning[注意]
`__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)的一员。

---

## File: website/docs/community/contact.md

---
sidebar-position: 0
---

# 参与讨论

如果在安装或者开发 NoneBot 过程中遇到了任何问题，或者有新奇的点子，欢迎参与我们的社区讨论：

1. 点击下方链接前往 GitHub，前往 Issues 页面，在 `New Issue` Template 中选择 `Question`

   NoneBot：[](https://github.com/nonebot/nonebot2)

2. 通过 QQ 群（点击下方链接直达）

   [](https://jq.qq.com/?_wv=1027&k=5OFifDh)

3. 通过 QQ 频道

   [](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka)

4. 通过 Discord 服务器（点击下方链接直达）

   [](https://discord.gg/VKtE6Gdc4h)

---

## 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` 类型，其元素为[通用消息段](./segment.md)。

你可以用如下方式获取 `UniMessage`：

<Tabs groupId="get_unimsg">
<TabItem value="depend" label="使用依赖注入">

通过提供的 `UniversalMessage` 或基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832)的 `UniMsg` 依赖注入器来获取 `UniMessage`。

```python
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)
    ...
```

</TabItem>
<TabItem value="method" label="使用 UniMessage.generate">

注意，`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。

```python
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)
```

</TabItem>
</Tabs>

## 发送消息

你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。

`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列：

```python
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` 对象，用于修改/撤回/表态消息：

```python
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` 的定义如下：

```python
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`: 发送目标，支持事件和[发送对象](./utils.mdx#发送对象)，不传入时会尝试从响应器上下文中获取。
- `bot`: 发送消息使用的 Bot 对象，若不传入则会尝试从响应器上下文中获取。
- `fallback`: [回退策略](#回退策略)。
- `at_sender`: 是否提醒发送者，默认为 `False`。当类型为 `str` 时，表示指定用户的 id。
- `reply_to`: 是否回复消息，默认为 `False`。
  - `str` 表示消息 id。
  - `bool` 表示是否回复当前消息。此时 `target` 不能是[发送对象](./utils.mdx#发送对象)。
  - `Reply` 表示直接使用回复元素。
- `**kwargs`: 各 `Bot.send` 的特定参数。

而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：

```python
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` 也可以用于主动发送消息：

```python
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` 对象可以用于修改/撤回/表态消息：

```python
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` 可以传入单个字符串/消息段，或可迭代的字符串/消息段：

```python
from nonebot_plugin_alconna.uniseg import UniMessage, At


msg = UniMessage("Hello")
msg1 = UniMessage(At("user", "124"))
msg2 = UniMessage(["Hello", At("user", "124")])
```

`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：

```python
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`，可以用于格式化消息，大体用法参考 [消息模板](../../../tutorial/message#使用消息模板)。

这里额外说明 `UniMessage.template` 的拓展控制符

相比 `Message`，UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行

以 At(...) 为例：

```python title=使用通用消息段的拓展控制符
>>> 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` 中的属性的功能：

```python title=在AlconnaMatcher中使用通用消息段的拓展控制符
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` 对象：

```python
# 消息序列与消息段相加
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` 方法，或者使用自加：

```python
msg = UniMessage([Text("text")])
# 自加
msg += "text"
msg += Text("text")
msg += UniMessage([Text("text")])
# 附加
msg.append(Text("text"))
# 扩展
msg.extend([Text("text")])
```

## 操作

### 检查消息段

我们可以通过 `in` 运算符或消息序列的 `has` 方法来：

```python
# 是否存在消息段
At("user", "1234") in message
# 是否存在指定类型的消息段
At in message
```

我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段：

```python
# 是否都为 "test"
message.only("test")
# 是否仅包含指定类型的消息段
message.only(Text)
```

### 获取消息纯文本

类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本：

```python
# 提取消息纯文本字符串
assert UniMessage(
    [At("user", "1234"), "text"]
).extract_plain_text() == "text"
```

### 遍历

通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段：

```python
for segment in message:  # type: Segment
    ...
```

### 过滤、索引与切片

消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片：

```python
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` 方法进行类型过滤：

```python
message.include(Text, At)
message.exclude(Reply)
```

或者使用 `filter` 方法：

```python
message.filter(lambda x: isinstance(x, At) and x.flag == "user")  # 仅保留 At("user", xxx) 的消息段
```

同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段：

```python
# 指定类型首个消息段索引
message.index(Text) == 1
# 指定类型消息段数量
message.count(Text) == 2
```

此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段：

```python
# 获取指定类型指定个数的消息段
message.get(Text, 1) == UniMessage([Text("test1")])
```

### 嵌套提取

消息序列的 `select` 方法可以递归地从消息中选择指定类型的消息段：

```python
message = UniMessage(
    [
        Text("text1"),
        Image(url="url1")(
            Text("text2"),
        )
    ]
)
assert message.select(Text) == UniMessage(
    [
        Text("text1"),
        Text("text2")
    ]
)
```

### 转换

消息序列的 `map` 方法可以简单地将消息段转换为指定类型的数据：

```python
# 转换消息段为另一类型的消息段，此时返回结果仍是 UniMessage
message.map(lambda x: Text(x.target))  # 转换为 Text 消息段
# 转换消息段为另一类型的数据，此时返回结果为 list[T]
message.map(lambda x: x.target)  # 转换为 list[str]
```

在此之上，消息序列还提供了 `transform` 和 `transform_async` 方法，允许你传入转换规则，将消息段转换为另一类型的消息段，并返回一个新的消息序列：

```python
rule = {
    "text": True,
    "at": lambda attrs, children: Text(attrs["target"])
}
message.transform(rule)
```

转换规则的类型一般为 `dict[str, Transformer]`，以消息元素类型的名称为键，定义方式如下：

```typescript
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`,

```python
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` 方法：

```python
msg = UniMessage.text("Hello").image(url="url")
data = msg.dump()  # [{"type": "text", "text": "Hello"}, {"type": "image", "url": "url"}]

assert UniMessage.load(data) == msg
```

### dump

`dump` 方法的定义如下：

```python
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`](../../data-storing.md) 并使用其提供的路径。否则 (即 `localstore` 未安装)，将会尝试使用当前工作目录。

### load

`load` 方法的定义如下：

```python
@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` 即可:

```python
from nonebot_plugin_alconna.uniseg import UniMessage
```

### 构建

你可以通过 `UniMessage` 上的快捷方法来链式构造消息:

```python
message = (
    UniMessage.text("hello world")
    .at("1234567890")
    .image(url="https://example.com/image.png")
)
```

也可以通过导入通用消息段来构建消息:

```python
from nonebot_plugin_alconna import Text, At, Image, UniMessage

message = UniMessage(
    [
        Text("hello world"),
        At("user", "1234567890"),
        Image(url="https://example.com/image.png"),
    ]
)
```

更深入一点，比如你想要发送一条包含多个按钮的消息，你可以这样做:

```python
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` 方法来发送消息:

```python
@matcher.handle()
async def _():
    message = UniMessage.text("hello world").image(url="https://example.com/image.png")
    await message.send()
    # 类似于 `matcher.finish`
    await message.finish()
```

你可以通过参数来让消息 @ 发送者:

```python
@matcher.handle()
async def _():
    message = UniMessage.text("hello world").image(url="https://example.com/image.png")
    await message.send(at_sender=True)
```

或者回复消息:

```python
@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` 方法来撤回，编辑和表态消息事件。

```python
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()
```

你也可以对你自己发送的消息进行撤回，编辑和表态:

```python
@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秒后撤回
```

### 处理消息

通过依赖注入，你可以在事件处理器中获取通用消息:

```python
from nonebot_plugin_alconna import UniMsg

@matcher.handle()
async def _(msg: UniMsg):
    ...
```

然后你可以通过 `UniMessage` 的方法来处理消息.

例如，你想知道消息中是否包含图片，你可以这样做:

```python
ans1 = Image in message
ans2 = message.has(Image)
ans3 = message.only(Image)
```

或者，提取所有的图片：

```python
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})
```

而后，如果你想提取出所有的图片链接，你可以这样做:

```python
urls = imgs.map(lambda x: x.url)
```

如果你想知道消息是否符合某个前缀，你可以这样做:

```python
@matcher.handle()
async def _(msg: UniMsg):
    if msg.startswith("hello"):
        await matcher.finish("hello world")
    else:
        await matcher.finish("not hello world")
```

或者你想接着去除掉前缀:

```python
@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 格式:

```python
from nonebot_plugin_alconna import UniMsg

@matcher.handle()
async def _(msg: UniMsg):
    data: list[dict] = msg.dump()
    # 你可以将 data 存储到数据库或者 JSON 文件中
```

而后你可以通过 `UniMessage.load` 方法来将 JSON 格式的消息反序列化为 `UniMessage` 对象:

```python
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 命令的参数定义，也可用于消息的构建和解析。

```python
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`](https://satori.js.org/zh-CN/) 协议的规定下，一类元素可以用其子元素来代表一类兼容性消息
（例如，qq 的商场表情在某些平台上可以用图片代替）。

为此，本插件提供了 `select` 方法来表达 "命令中获取子元素" 的方法：

```python
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)]])
```

也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)

:::

## 自定义消息段

`uniseg` 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化：

```python
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，通常用于回复/撤回/编辑/表态当前消息。

<Tabs groupId="get_msgid">
<TabItem value="depend" label="使用依赖注入">

通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。

```python
from nonebot_plugin_alconna.uniseg import MsgId


matcher = on_xxx(...)

@matcher.handle()
asycn def _(msg_id: MsgId):
    ...
```

</TabItem>
<TabItem value="method" label="使用获取函数">

```python
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)

```

</TabItem>
</Tabs>

:::warning

该方法获取的消息事件 ID 不推荐直接用于各适配器的 API 调用中，可能会操作失败。

:::

## 发送对象

消息发送对象是用来描述当前消息事件的可发送对象或者主动发送消息时的目标对象，它包含了以下属性：

```python
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]
    """额外信息，用于适配器扩展"""
```

<Tabs groupId="get_target">
<TabItem value="depend" label="使用依赖注入">

通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。

```python
from nonebot_plugin_alconna.uniseg import MsgTarget


matcher = on_xxx(...)

@matcher.handle()
asycn def _(target: MsgTarget):
    ...
```

</TabItem>
<TabItem value="method" label="使用获取函数">

```python
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)

```

</TabItem>
</Tabs>

主动构造一个发送对象时，则需要如下参数：

- `id`: 目标ID；若为群聊则为 `group_id` 或者 `channel_id`，若为私聊则为 `user_id`
- `parent_id`: 父级ID；若为频道则为 `guild_id`，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）
- `channel`: 是否为频道，仅当目标平台符合频道概念时
- `private`: 是否为私聊
- `source`: 可能的事件ID
- `self_id`: 机器人id，若为 None 则 Bot 对象会随机选择
- `selector`: 选择器，用于在多个 Bot 对象中选择特定 Bot
- `scope`: 平台范围，表示当前发送对象的平台类别
- `adapter`: 适配器名称，若为 None 则需要明确指定 Bot 对象
- `platform`: 平台名称，仅当目标适配器存在多个平台时使用
- `extra`: 额外信息，用于适配器扩展

通过 `Target` 对象，我们可以在 `UniMessage.send` 中指定发送对象：

```python
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` 对象：

```python
async def _():
    target = Target("12345", scope=SupportScope.qq_client)
    bot = await target.select()
```

:::

若配置了 [`alconna_apply_fetch_targets`](../config.md#alconna_apply_fetch_targets) 选项，则在启动时会主动拉取一次发送对象列表。即对于
某一主动构造的 `Target` 对象，插件将其与拉取下来的众多发送对象进行匹配，并选择第一个符合条件的发送对象，以选择对应的 Bot 对象。

## 撤回消息

通过 `message_recall` 方法来撤回消息事件。

```python
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` 方法的参数如下：

```python
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` 方法来编辑消息事件。

```python
from nonebot_plugin_alconna.uniseg import UniMessage, message_edit

matcher = on_xxx(...)

@matcher.handle()
async def _():
    await message_edit(UniMessage.text("1234"))
```

`message_edit` 方法的参数如下：

```python
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` 方法来表态消息事件。

```python
from nonebot_plugin_alconna.uniseg import message_reaction

matcher = on_xxx(...)

@matcher.handle()
async def _():
    await message_reaction("👍")
```

`message_reaction` 方法的参数如下：

```python
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` 规则只会在消息中 @ 机器人时触发。

```python
from nonebot_plugin_alconna.uniseg import at_me

matcher = on_xxx(..., rule=at_me())
```