QChatGPT 插件开发Wiki

> 请先阅读[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)  
> 请先阅读[技术信息页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8A%80%E6%9C%AF%E4%BF%A1%E6%81%AF)  
> 建议先阅读本项目源码，了解项目架构

> 问题、需求请到仓库issue发起  
> **提问前请先靠自己尝试** 

## 💬简介

尽管“为一个基于OpenAI API的QQ机器人开发插件支持”这事看起来有点小题大做，但萌生此想法后的几天内好几个人提出了这个需求，最终促使此项目正式支持插件。

## 🧱实现

基于`importlib`库加载模块的方法动态加载额外Python程序文件以便实现插件加载，插件均存放在`plugins`文件夹，其中的所有`.py`文件都将被加载(除了所有`__init__.py`)

## 📚示例代码

请查看代码目录`tests/plugin_examples`中的插件目录

## 💻快速开始

按照文档部署此项目，并使其正常运行。  
在`plugins`目录下新建目录`hello`，在其中新建空文件`__init__.py`以标记此目录为软件包，继续新建文件`main.py`。  

> 您也可以使用[hello_plugin](https://github.com/RockChinQ/hello_plugin)作为模板直接生成插件代码仓库

编辑`main.py`输入以下内容：

```Python
from pkg.plugin.models import *
from pkg.plugin.host import EventContext, PluginHost

"""
在收到私聊或群聊消息"hello"时，回复"hello, <发送者id>!"或"hello, everyone!"
"""


# 注册插件
@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
class HelloPlugin(Plugin):

    # 插件加载时触发
    # plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法，详细请查看其源码
    def __init__(self, plugin_host: PluginHost):
        pass

    # 当收到个人消息时触发
    @on(PersonNormalMessageReceived)
    def person_normal_message_received(self, event: EventContext, **kwargs):
        msg = kwargs['text_message']
        if msg == "hello":  # 如果消息为hello

            # 输出调试信息
            logging.debug("hello, {}".format(kwargs['sender_id']))

            # 回复消息 "hello, <发送者id>!"
            event.add_return("reply", ["hello, {}!".format(kwargs['sender_id'])])

            # 阻止该事件默认行为（向接口获取回复）
            event.prevent_default()

    # 当收到群消息时触发
    @on(GroupNormalMessageReceived)
    def group_normal_message_received(self, event: EventContext, **kwargs):
        msg = kwargs['text_message']
        if msg == "hello":  # 如果消息为hello

            # 输出调试信息
            logging.debug("hello, {}".format(kwargs['sender_id']))

            # 回复消息 "hello, everyone!"
            event.add_return("reply", ["hello, everyone!"])

            # 阻止该事件默认行为（向接口获取回复）
            event.prevent_default()

    # 插件卸载时触发
    def __del__(self):
        pass

```

此插件将实现：私聊收到`hello`消息时回复`hello, <发送者QQ号>!`，群聊收到`hello`消息时回复`hello, everyone!`

### 解读此插件程序

- `import`从`pkg.plugin`引入`models`模块的所有字段（此程序使用了其中的`register函数`、`on函数`、`Plugin类`、`PersonNormalMessageReceived事件`、`GroupNormalMessageReceived事件`）
- `@register()`将类`HelloPlugin`标记为一个插件类，声明插件名称为`Hello`以及插件简介、版本、作者
- 声明类`HelloPlugin`继承于`Plugin`，此类可以随意命名，插件名称只与`register`调用时的参数有关
- 声明此类的`__init__`方法，此方法是可选的，其中的代码将在主程序启动时加载插件的时候被执行
- `@on`将方法`person_normal_message_received`标记为一个事件处理器，处理`PersonNormalMessageReceived`（收到私聊消息并在获取OpenAI回复前触发）事件，此方法可以随意命名，绑定的事件只与`on`中的参数有关，更多支持的事件可到`pkg.plugin.models.py`文件中查看或查看下方`API`节
- 输出调试信息，程序中可通过`logging`将日志输出到控制台和`qchatgpt.log`文件
- 方法内部从参数中取出`text_message`参数，判断是否为`hello`，如果是就将返回值`reply`设置为`["hello, {}!".format(kwargs['sender_id'])]`，接下来调用`event`对象的`prevent_default`方法，阻止原程序默认行为
    - 每个事件`提供的参数`和`支持的返回值`请查看`pkg.plugin.models`中的每个事件的注释或查看下方`API`节
    - `event`对象提供的方法请查看`pkg.plugin.host`中的`EventContext`类或查看下方`API`节
- 用相似的程序注册`GroupNormalMessageReceived`事件处理群消息

编写完毕保存后，重新启动主程序，查看到输出中包含以下内容，即为加载成功：

```
[2023-01-16 18:29:47.193] host.py (43) - [INFO] : 加载模块: hello.main
[2023-01-16 18:29:47.194] models.py (209) - [INFO] : 插件注册完成: n='Hello', d='hello world', v=0.1, a='RockChinQ' (<class 'plugins.hello.main.HelloPlugin'>)
```

> 建议在`config.py`中设置`logging_level = logging.DEBUG`以便开启调试输出

## ❗规范(重要)

- 请每个插件独立一个目录以便管理，建议在Github上创建一个仓库储存单个插件，以便获取和更新
- 插件名使用`大驼峰命名法`，如`Hello`、`ExamplePlugin`、`ChineseCommands`等
- 一个目录内可以存放多个Python程序文件，以独立出插件的各个功能，便于开发者管理，但不建议在一个目录内注册多个插件
- 插件需要的依赖库请在插件目录下的`requirements.txt`中指定，程序从储存库获取此插件时将自动安装依赖

## 📄API参考

### 说明

事件处理函数将会获得一系列参数，可以在`kwargs`中取出。  
其中`host`参数（`pkg.plugin.host.PluginHost`类的实例）是插件宿主，提供与主程序各个模块交互的一些方法。  
`event`参数（`pkg.plugin.host.EventContext`类的实例）是事件执行期间的上下文，提供对此次事件执行的一些操作方法。  

事件返回值均为**可选**的，可以通过调用`event.add_return(key: str, ret)`来提交返回值

### 事件

所有事件参数均有`host`和`event`，以下仅展示其他参数  
关于`YiriMirai`支持的消息链组件，请查看 [YiriMirai的文档](https://yiri-mirai.wybxc.cc/docs/basic/message-chain)

```Python
PersonMessageReceived = "person_message_received"
"""收到私聊消息时，在判断是否应该响应前触发
    kwargs:
        launcher_type: str 发起对象类型(group/person)
        launcher_id: int 发起对象ID(群号/QQ号)
        sender_id: int 发送者ID(QQ号)
        message_chain: mirai.models.message.MessageChain 消息链
"""

GroupMessageReceived = "group_message_received"
"""收到群聊消息时，在判断是否应该响应前触发（所有群消息）
    kwargs:
        launcher_type: str 发起对象类型(group/person)
        launcher_id: int 发起对象ID(群号/QQ号)
        sender_id: int 发送者ID(QQ号)
        message_chain: mirai.models.message.MessageChain 消息链
"""

PersonNormalMessageReceived = "person_normal_message_received"
"""判断为应该处理的私聊普通消息时触发
    kwargs:
        launcher_type: str 发起对象类型(group/person)
        launcher_id: int 发起对象ID(群号/QQ号)
        sender_id: int 发送者ID(QQ号)
        text_message: str 消息文本
        
    returns (optional):
        alter: str 修改后的消息文本
        reply: list 回复消息组件列表，元素为YiriMirai支持的消息组件
"""

PersonCommandSent = "person_command_sent"
"""判断为应该处理的私聊指令时触发
    kwargs:
        launcher_type: str 发起对象类型(group/person)
        launcher_id: int 发起对象ID(群号/QQ号)
        sender_id: int 发送者ID(QQ号)
        command: str 指令
        params: list[str] 参数列表
        text_message: str 完整指令文本
        is_admin: bool 是否为管理员
    
    returns (optional):
        alter: str 修改后的完整指令文本
        reply: list 回复消息组件列表，元素为YiriMirai支持的消息组件
"""

GroupNormalMessageReceived = "group_normal_message_received"
"""判断为应该处理的群聊普通消息时触发
    kwargs:
        launcher_type: str 发起对象类型(group/person)
        launcher_id: int 发起对象ID(群号/QQ号)
        sender_id: int 发送者ID(QQ号)
        text_message: str 消息文本
        
    returns (optional):
        alter: str 修改后的消息文本
        reply: list 回复消息组件列表，元素为YiriMirai支持的消息组件
"""

GroupCommandSent = "group_command_sent"
"""判断为应该处理的群聊指令时触发
    kwargs:
        launcher_type: str 发起对象类型(group/person)
        launcher_id: int 发起对象ID(群号/QQ号)
        sender_id: int 发送者ID(QQ号)
        command: str 指令
        params: list[str] 参数列表
        text_message: str 完整指令文本
        is_admin: bool 是否为管理员
    
    returns (optional):
        alter: str 修改后的完整指令文本
        reply: list 回复消息组件列表，元素为YiriMirai支持的消息组件
"""

NormalMessageResponded = "normal_message_responded"
"""获取到对普通消息的文字响应时触发
    kwargs:
        launcher_type: str 发起对象类型(group/person)
        launcher_id: int 发起对象ID(群号/QQ号)
        sender_id: int 发送者ID(QQ号)
        session: pkg.openai.session.Session 会话对象
        prefix: str 回复文字消息的前缀
        response_text: str 响应文本
    
    returns (optional):
        prefix: str 修改后的回复文字消息的前缀
        reply: list 替换回复消息组件列表，元素为YiriMirai支持的消息组件
"""

SessionFirstMessageReceived = "session_first_message_received"
"""会话被第一次交互时触发
    kwargs:
        session_name: str 会话名称(<launcher_type>_<launcher_id>)
        session: pkg.openai.session.Session 会话对象
        default_prompt: str 预设值
"""

SessionExplicitReset = "session_reset"
"""会话被用户手动重置时触发，此事件不支持阻止默认行为
    kwargs:
        session_name: str 会话名称(<launcher_type>_<launcher_id>)
        session: pkg.openai.session.Session 会话对象
"""

SessionExpired = "session_expired"
"""会话过期时触发
    kwargs:
        session_name: str 会话名称(<launcher_type>_<launcher_id>)
        session: pkg.openai.session.Session 会话对象
        session_expire_time: int 已设置的会话过期时间(秒)   
"""

KeyExceeded = "key_exceeded"
"""api-key超额时触发
    kwargs:
        key_name: str 超额的api-key名称
        usage: dict 超额的api-key使用情况
        exceeded_keys: list[str] 超额的api-key列表
"""

KeySwitched = "key_switched"
"""api-key超额切换成功时触发，此事件不支持阻止默认行为
    kwargs:
        key_name: str 切换成功的api-key名称
        key_list: list[str] api-key列表
"""
```

### host: PluginHost 详解

提供与主程序各个模块交互的一些方法,具体查看`pkg.plugin.host`中的`PluginHost`类  

### event: EventContext 详解

提供对此次事件执行的一些操作方法,具体查看`pkg.plugin.host`中的`EventContext`类
