# 🧩 XYBotV2 插件开发指南

## 插件

插件是XYBotV2的扩展，用于实现各种功能。

所有插件都在`plugins`文件夹内。每一个文件夹都是一个插件。一个插件都要包含`main.py`文件，作为插件入口。

而`main.py`里会有继承`PluginBase`的类，用来识别插件，定义插件所需要的方法、装饰器、等等。

插件文件夹里的`__init__.py`文件不是必须的，也可以为空文件。插件当作一个Python包被导入时，`__init__.py` 中的代码会自动执行。

如果要新加插件，可直接把插件文件放入`plugins`文件夹内。

如果你不要什么功能，直接把`plugins`文件夹中对应的文件夹删掉。（没错就是这么简单）

## 示例插件和插件模版

示例插件位于`plugins/ExamplePlugin`。

插件模版在 [XYBotV2PluginTemplate](https://github.com/HenryXiaoYang/XYBotV2PluginTemplate) Github仓库

## 编写插件

每个插件的`main.py`都需要继承`PluginBase`基类,并定义基本信息。以下是一个基本的插件示例:

```python
# main.py
from utils.plugin_base import PluginBase


class ExamplePlugin(PluginBase):
    description = "示例插件"
    author = "HenryXiaoYang"
    version = "1.0.0"
```

这是创建插件的最基本结构:

1. 继承 `PluginBase` 类以获取基本功能
2. 设置插件的描述信息、作者和版本号
3. 之后可以在这个类中添加各种事件处理函数

```python
    # ... 接上面 ... #
# 同步初始化
def __init__(self):
   super().__init__()
   # 在这里初始化插件需要的变量
   self.data = {}


# 异步初始化
async def async_init(self):
   # 在这里执行需要的异步初始化操作
   pass
```

- `__init__`: 初始化函数,用于设置插件的初始状态
- `async_init`: 异步初始化函数，用于执行插件的异步初始化操作

```python
    # ... 接上面 ... #
@on_text_message  # 处理文本消息
async def on_text(self, bot: WechatAPIClient, message: dict):
   pass
```

- `@on_text_message`: 处理文本消息的事件，所装饰的函数必须是异步函数。

## 事件类型列表

| 事件类型 | 装饰器                  | 触发条件      |
|------|----------------------|-----------|
| 文本消息 | `@on_text_message`   | 收到任意文本消息  |
| 图片消息 | `@on_image_message`  | 收到图片消息    |
| 语音消息 | `@on_voice_message`  | 收到语音消息    |
| 文件消息 | `@on_file_message`   | 收到文件消息    |
| 引用消息 | `@on_quote_message`  | 收到引用回复消息  |
| @消息  | `@on_at_message`     | 收到包含@的群消息 |
| 视频消息 | `@on_video_message`  | 收到视频消息    |
| 好友请求 | `@on_friend_request` | 收到新的好友请求  |
| 拍一拍  | `@on_pat_message`    | 收到拍一拍消息   |

## 消息事件处理函数

### 优先级机制

优先级机制用于控制多个插件处理同一事件时的执行顺序。

#### 优先级范围

- 优先级数值范围为0-99
- 数值越大,优先级越高
- 默认优先级为50

#### 设置方式

在事件装饰器中通过priority参数设置:

```python
    # ... 接上面 ... #
@on_text_message(priority=80)  # 设置较高优先级
async def handle_important(self, bot, message):
   # 优先处理重要消息
   pass


@on_text_message(priority=20)  # 设置较低优先级
async def handle_normal(self, bot, message):
   # 后处理普通消息
   pass
```

#### 执行顺序

1. 按优先级从高到低依次执行
2. 同优先级的处理函数按注册顺序执行

### 阻塞机制

阻塞机制用于控制是否继续执行后续的事件处理函数。

#### 基本概念

- 通过插件函数返回值来控制是否继续执行后续处理
- 返回`True`表示继续执行后续处理
- 返回`False`表示处理完成并阻止后续执行

#### 设置方式

```python
    @on_text_message
async def handle_sensitive(self, bot, message):
   if "敏感词" in message["Content"]:
      await bot.send_text(message["FromWxid"], "检测到敏感内容")
      return False  # 返回False表示处理完成并阻止后续执行
   return True  # 返回False表示继续执行后续处理


@on_text_message
async def handle_normal(self, bot, message):
   # 普通消息处理
   pass
   # 没有返回，默认继续执行
```

#### 注意事项

1. 合理使用阻塞机制,避免不必要的阻塞
2. 高优先级的阻塞会影响所有低优先级的处理函数

### 风控保护机制

风控保护机制用于保护机器人账号安全,防止触发微信的安全检测。本机器人的风控保护非常轻量，*不保证*机器人完全不会被风控。

1. **新设备登录限制**
   - 新设备登录后4小时内不可处理消息，不可调用函数，不可发送消息。只维持自动心跳和接受消息。

2. **消息发送频率**
   - 消息发送内置了队列，每秒只发一条消息。

### 异步处理

XYBot整个项目使用asyncio进行异步处理（个别地方除外）,所有插件函数必须是异步函数。

使用阻塞函数会导致主进程阻塞，无法接受消息，无法发送消息，无法调用函数。

如果需要使用阻塞函数，请使用`asyncio.run_in_executor`将其转换为异步函数。

### 资源管理

XYBot使用`loguru`进行日志管理，所有日志都会输出到`logs/xybot.log`文件中。

请将插件会使用到的静态资源存放到`resources`文件夹中。

请将临时会产生的文件存放到`resources/cache`文件夹中。

## 消息对象结构

### 文本消息示例

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 1,  # 消息类型（1表示文本消息）
   "Content": "@机器人\u2005",  # 消息内容
   "Status": 3,  # 消息状态码（3表示正常消息）
   "ImgStatus": 1,  # 图片状态
   "ImgBuf": {  # 图片缓冲区（通常为空）
      "iLen": 0
   },
   "CreateTime": 1739878408,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式，包含@列表等信息）
   "PushContent": "机器人在群聊中@了你。",  # 系统推送提示内容
   "NewMsgId": 1145141919810,  # 消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列（可用于撤回消息）
   "FromWxid": "123456789@chatroom",  # 消息来源ID（可以是个人wxid或群聊ID）
   "IsGroup": True,  # 是否来自群聊
   "SenderWxid": "wxid_11111111111111",  # 实际发送人微信ID
   "Ats": ["wxid_00000000000000"]  # 被@的用户列表
}
```

### 被@消息示例

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 1,  # 消息类型（1表示文本消息）
   "Content": "@机器人\u2005",  # 消息内容（\u2005是特殊空格符）
   "Status": 3,  # 消息状态码（3表示正常消息）
   "ImgStatus": 1,  # 图片状态
   "ImgBuf": {  # 图片缓冲区（通常为空）
      "iLen": 0
   },
   "CreateTime": 1739878408,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式，包含@列表等信息）
   "PushContent": "机器人在群聊中@了你。",  # 系统推送提示内容
   "NewMsgId": 1145141919810,  # 新消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列号（可用于撤回消息）
   "FromWxid": "123456789@chatroom",  # 消息来源ID（可以是个人wxid或群聊ID）
   "IsGroup": True,  # 是否群聊消息
   "SenderWxid": "wxid_11111111111111",  # 实际发送人微信ID
   "Ats": ["wxid_00000000000000"]  # 被@的用户列表（包含机器人自身ID）
}
```

### 图片消息示例

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 3,  # 消息类型（3表示图片消息）
   "Content": "/9j/4AAQSkZJ...base64content",  # 图片的Base64编码内容
   "Status": 3,  # 消息状态码（3表示正常消息）
   "ImgStatus": 2,  # 图片状态（2表示图片已下载）
   "ImgBuf": {  # 图片缓冲区（通常为空）
      "iLen": 0
   },
   "CreateTime": 1739879142,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式，包含图片相关信息）
   "PushContent": "XYBot : [图片]",  # 系统推送提示内容
   "NewMsgId": 1145141919810,  # 新消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列号（可用于撤回消息）
   "FromWxid": "wxid_11111111111111",  # 消息发送者的微信ID
   "SenderWxid": "wxid_11111111111111",  # 实际发送人微信ID（私聊时与FromWxid相同）
   "IsGroup": False  # 是否群聊消息（这里是私聊）
}
```

### 语音消息示例

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 34,  # 消息类型（34表示语音消息）
   "Content": b"VoiceData...",  # 语音数据内容（bytes类型）
   "Status": 3,  # 消息状态码（3表示正常消息）
   "ImgStatus": 1,  # 语音状态
   "ImgBuf": {  # 语音数据缓冲区
      "iLen": 1024,
      "buffer": "AudioData..."  # 语音数据内容（base64），群聊中的语音消息这个字段为空
   },
   "CreateTime": 1739879399,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式）
   "PushContent": "XYBot : [语音]",  # 系统推送提示内容
   "NewMsgId": 1145141919810,  # 新消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列号（可用于撤回消息）
   "FromWxid": "wxid_11111111111111",  # 消息发送者的微信ID
   "SenderWxid": "wxid_11111111111111",  # 实际发送人微信ID（私聊时与FromWxid相同）
   "IsGroup": False  # 是否群聊消息（这里是私聊）
}
```

### 文件消息示例

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 49,  # 消息类型（49表示XML消息）
   "Content": "<?xml version=\"1.0\"?><msg><appmsg>...</appmsg></msg>",  # 文件信息的XML内容
   "Status": 3,  # 消息状态码（3表示正常消息）
   "ImgStatus": 1,  # 文件状态
   "ImgBuf": {  # 文件数据缓冲区（通常为空）
      "iLen": 0
   },
   "CreateTime": 1739879893,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式）
   "PushContent": "XYBot : [文件]example.txt",  # 系统推送提示内容
   "NewMsgId": 1145141919810,  # 新消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列号（可用于撤回消息）
   "FromWxid": "wxid_11111111111111",  # 消息发送者的微信ID
   "SenderWxid": "wxid_11111111111111",  # 实际发送人微信ID（私聊时与FromWxid相同）
   "IsGroup": False,  # 是否群聊消息（这里是私聊）
   "Filename": "example.txt",  # 文件名
   "FileExtend": "txt",  # 文件扩展名
   "File": "FileData..."  # 文件数据内容（base64编码）
}
```

### 引用消息示例

这个装饰器还不是很完善。

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 49,  # 消息类型（49表示引用消息）
   "Content": "回复的文本内容",  # 回复消息的文本内容
   "Status": 3,  # 消息状态码（3表示正常消息）
   "ImgStatus": 1,  # 消息状态
   "ImgBuf": {  # 数据缓冲区（通常为空）
      "iLen": 0
   },
   "CreateTime": 1739880113,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式）
   "PushContent": "回复的文本内容",  # 系统推送提示内容
   "NewMsgId": 1145141919810,  # 新消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列号（可用于撤回消息）
   "FromWxid": "wxid_11111111111111",  # 消息发送者的微信ID
   "SenderWxid": "wxid_11111111111111",  # 实际发送人微信ID
   "IsGroup": False,  # 是否群聊消息
   "Quote": {  # 被引用的原始消息信息
      "MsgType": 1,  # 原始消息类型（1表示文本消息）
      "NewMsgId": "1145141919811",  # 原始消息的ID
      "ToWxid": "wxid_00000000000000",  # 原始消息接收者ID
      "FromWxid": "wxid_11111111111111",  # 原始消息发送者ID
      "Nickname": "XYBot",  # 原始消息发送者昵称
      "MsgSource": "<msgsource>...</msgsource>",  # 原始消息源数据
      "Content": "引用的消息内容",  # 引用的消息内容
      "Createtime": "1739879158"  # 原始消息创建时间
   }
}
```

### 视频消息示例

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 43,  # 消息类型（43表示视频消息）
   "Content": "<?xml version=\"1.0\"?>\n<msg>\n\t<videomsg aeskey=\"...\" cdnvideourl=\"...\" length=\"207801\" playlength=\"5\" /></msg>",
   # 视频信息的XML内容
   "Status": 3,  # 消息状态码（3表示正常消息）
   "ImgStatus": 1,  # 视频状态
   "ImgBuf": {  # 视频数据缓冲区（通常为空）
      "iLen": 0
   },
   "CreateTime": 1739880402,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式）
   "NewMsgId": 1145141919810,  # 新消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列号（可用于撤回消息）
   "FromWxid": "wxid_11111111111111",  # 消息发送者的微信ID
   "SenderWxid": "wxid_11111111111111",  # 实际发送人微信ID
   "IsGroup": False,  # 是否群聊消息（这里是私聊）
   "Video": "VideoData..."  # 视频数据内容（base64编码）
}
```

### 好友请求消息示例

之后写

### 拍一拍消息示例

```python
{
   "MsgId": 123456789,  # 消息唯一标识（可用于撤回消息）
   "ToWxid": "wxid_00000000000000",  # 接收者微信ID（通常是机器人自身ID）
   "MsgType": 10002,  # 消息类型（10002表示拍一拍消息）
   "Content": "\n<sysmsg type=\"pat\">\n<pat>\n  <fromusername>wxid_11111111111111</fromusername>\n  <chatusername>11111111111@chatroom</chatusername>\n  <pattedusername>wxid_00000000000000</pattedusername>\n  <patsuffix><![CDATA[拍了拍我]]></patsuffix>\n  <template><![CDATA[\"${wxid_11111111111111}\" 拍了拍我]]></template>\n</pat>\n</sysmsg>",
   # 拍一拍消息的XML内容
   "Status": 4,  # 消息状态码
   "ImgStatus": 1,  # 消息状态
   "ImgBuf": {  # 数据缓冲区（通常为空）
      "iLen": 0
   },
   "CreateTime": 1739880846,  # 消息创建时间戳（秒级）
   "MsgSource": "<msgsource>...</msgsource>",  # 消息源数据（XML格式）
   "NewMsgId": 1145141919810,  # 新消息ID（可用于撤回消息）
   "MsgSeq": 114514,  # 消息序列号（可用于撤回消息）
   "FromWxid": "11111111111@chatroom",  # 消息来源群聊ID
   "IsGroup": True,  # 是否群聊消息（这里是群聊）
   "SenderWxid": "11111111111@chatroom",  # 群聊ID
   "Patter": "wxid_11111111111111",  # 发起拍一拍的用户ID
   "Patted": "wxid_00000000000000",  # 被拍的用户ID
   "PatSuffix": "拍了拍我"  # 拍一拍的后缀文本
}
```

## 定时任务装饰器

`@schedule`装饰器用于创建定时执行的任务,支持多种触发方式。

### 基本用法

```python
from utils.decorators import schedule


class TimerPlugin(PluginBase):
   @schedule('interval', seconds=5)
   async def periodic_task(self, bot):
      # 每5秒执行一次
      await bot.send_text("example", "定时消息")

   @schedule('cron', hour=8, minute=30)
   async def daily_task(self, bot):
      # 每天早上8:30执行
      await bot.send_text("example", "早安")

   @schedule('date', run_date='2024-12-31 23:59:59')
   async def one_time_task(self, bot):
      # 在指定时间执行一次
      await bot.send_text("example", "新年快乐")
```

### 触发器类型

1. **interval - 间隔触发**
   对于参数请查看[apscheduler.triggers.interval](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/interval.html)
   ```python
   @schedule('interval', seconds=30)
   async def task(self, bot):
      # 每1小时30分钟执行一次
      pass
   ```

2. **cron - 定时触发**
   参数请查看[apscheduler.triggers.cron](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html)
   ```python
    @schedule('cron', day_of_week='mon-fri', hour='9-17')
    async def work_time_task(self, bot):
        # 工作日9点到17点每小时执行
        pass
   ```

3. **date - 指定日期触发**
   参数请查看[apscheduler.triggers.date](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/date.html)
   ```python
    @schedule('date', run_date='2024-01-01 00:00:00')
    async def new_year_task(self, bot):
        # 在2024年新年时执行一次
        pass
   ```

### 高级用法

1. **组合使用多个定时任务**

```python
class ComplexTimer(PluginBase):
   @schedule('cron', hour=8)
   async def morning(self, bot):
      await bot.send_text("example", "早安")

   @schedule('cron', hour=23)
   async def night(self, bot):
      await bot.send_text("example", "晚安")

   @schedule('interval', minutes=30)
   async def check_status(self, bot):
      # 每30分钟检查一次状态
      pass
```

## WechatAPIClient 机器人的函数

在事件函数中，可以调用`bot`对象的函数。

可在 [API文档](WechatAPIClient/index.html) 获取详细接口说明。

