Skip to content

7 插件开发

Junyan Qin edited this page Sep 8, 2024 · 5 revisions

Warning

此 Wiki 已弃用,所有文档已迁移到 项目主页

QChatGPT 插件开发Wiki

请先阅读插件使用页
请先阅读技术信息页
建议先阅读本项目源码,了解项目架构

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

💬简介

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

🧱实现

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

📚示例代码

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

💻快速开始

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

您也可以使用hello_plugin作为模板直接生成插件代码仓库

编辑main.py输入以下内容:

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!

解读此插件程序

  • importpkg.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上创建一个仓库储存单个插件,以便获取和更新
  • 插件名使用大驼峰命名法,如HelloExamplePluginChineseCommands
  • 一个目录内可以存放多个Python程序文件,以独立出插件的各个功能,便于开发者管理,但不建议在一个目录内注册多个插件
  • 插件需要的依赖库请在插件目录下的requirements.txt中指定,程序从储存库获取此插件时将自动安装依赖

🪝内容函数

通过GPT的Function Calling能力实现的内容函数,这是一种嵌入对话中,由GPT自动调用的函数。

您的插件不一定必须包含内容函数,请先查看内容函数页了解此功能

示例:联网插件

加载含有联网功能的内容函数的插件WebwlkrPlugin,向机器人询问在线内容

# 控制台输出
[2023-07-29 17:37:18.698] message.py (26) - [INFO] : [person_1010553892]发送消息:介绍一下这个项目:https://git...
[2023-07-29 17:37:21.292] util.py (67) - [INFO] : message='OpenAI API response' path=https://api.openai.com/v1/chat/completions processing_ms=1902 request_id=941afc13b2e1bba1e7877b92a970cdea response_code=200
[2023-07-29 17:37:21.293] chat_completion.py (159) - [INFO] : 执行函数调用: name=Webwlkr-access_the_web, arguments={'url': 'https://github.com/RockChinQ/QChatGPT', 'brief_len': 512}
[2023-07-29 17:37:21.848] chat_completion.py (164) - [INFO] : 函数执行完成。

Webwlkr插件

内容函数编写步骤

1️⃣ 请先按照上方步骤编写您的插件基础结构,现在请删除(当然你也可以不删,只是为了简洁)上述插件内容的诸个由@on装饰的类函数

删除后的结构
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

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

2️⃣ 现在我们将以下函数添加到刚刚删除的函数的位置

# 要添加的函数

@func(name="access_the_web")  # 设置函数名称
def _(url: str):
    """Call this function to search about the question before you answer any questions.
    - Do not search through baidu.com at any time.
    - If you need to search somthing, visit https://www.google.com/search?q=xxx.
    - If user ask you to open a url (start with http:// or https://), visit it directly.
    - Summary the plain content result by yourself, DO NOT directly output anything in the result you got.

    Args:
        url(str): url to visit

    Returns:
        str: plain text content of the web page
    """
    import requests
    from bs4 import BeautifulSoup
    # 你需要先使用
    # pip install beautifulsoup4
    # 安装依赖

    r = requests.get(
        url,
        timeout=10,
        headers={
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183"
        }
    )
    soup = BeautifulSoup(r.text, 'html.parser')

    s = soup.get_text()

    # 删除多余的空行或仅有\t和空格的行
    s = re.sub(r'\n\s*\n', '\n', s)

    if len(s) >= 512:  # 截取获取到的网页纯文本内容的前512个字
        return s[:512]

    return s
现在这个文件内容应该是这样
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

    @func(name="access_the_web")
    def _(url: str):
        """Call this function to search about the question before you answer any questions.
        - Do not search through baidu.com at any time.
        - If you need to search somthing, visit https://www.google.com/search?q=xxx.
        - If user ask you to open a url (start with http:// or https://), visit it directly.
        - Summary the plain content result by yourself, DO NOT directly output anything in the result you got.

        Args:
            url(str): url to visit

        Returns:
            str: plain text content of the web page
        """
        import requests
        from bs4 import BeautifulSoup
        # 你需要先使用
        # pip install beautifulsoup4
        # 安装依赖

        r = requests.get(
            url,
            timeout=10,
            headers={
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183"
            }
        )
        soup = BeautifulSoup(r.text, 'html.parser')

        s = soup.get_text()

        # 删除多余的空行或仅有\t和空格的行
        s = re.sub(r'\n\s*\n', '\n', s)

        if len(s) >= 512:  # 截取获取到的网页纯文本内容的前512个字
            return s[:512]

        return s

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

请注意:

  • 函数的注释必须严格按照要求的格式进行书写,具体格式请查看此文档
  • 内容函数和以@on装饰的行为函数可以同时存在于同一个插件,并同时受到switch.json中的插件开关的控制
  • 务必确保您使用的模型支持函数调用功能,可以到config.pycompletion_api_params中修改模型,推荐使用gpt-3.5-turbo-16k

3️⃣ 现在您的程序已具备网络访问功能,重启程序,询问机器人有关在线的内容或直接发送文章链接请求其总结。

  • 这仅仅是一个示例,需要更高效的网络访问能力支持插件,请查看WebwlkrPlugin

🔒版本要求

若您的插件对主程序的版本有要求,可以使用以下函数进行断言,若不符合版本,此函数将报错并打断此函数所在的流程:

require_ver("v2.5.1")  # 要求最低版本为 v2.5.1
require_ver("v2.5.1", "v2.6.0")  # 要求最低版本为 v2.5.1, 同时要求最高版本为 v2.6.0
  • 此函数在主程序v2.5.1中加入
  • 此函数声明在pkg.plugin.models模块中,在插件示例代码最前方已引入此模块所有内容,故可直接使用

📄API参考

说明

下一版本将会添加统一的插件API,欢迎在此讨论回复您的需求!

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

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

事件

所有事件参数均有hostevent,以下仅展示其他参数
关于YiriMirai支持的消息链组件,请查看 YiriMirai的文档

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 响应文本
        finish_reason: str 响应结束原因
    
    returns (optional):
        prefix: str 修改后的回复文字消息的前缀
        reply: list 替换回复消息组件列表
"""

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列表
"""

PromptPreProcessing = "prompt_pre_processing"  # 于v2.5.1加入
"""每回合调用接口前对prompt进行预处理时触发,此事件不支持阻止默认行为
    kwargs:
        session_name: str 会话名称(<launcher_type>_<launcher_id>)
        default_prompt: list 此session使用的情景预设内容
        prompt: list 此session现有的prompt内容
        text_message: str 用户发送的消息文本
    
    returns (optional):
        default_prompt: list 修改后的情景预设内容
        prompt: list 修改后的prompt内容
        text_message: str 修改后的消息文本
"""

host: PluginHost 详解

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

event: EventContext 详解

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