doc: 更新插件开发教程

This commit is contained in:
RockChinQ
2024-03-20 23:27:35 +08:00
parent 1a6c02da41
commit aeb8fdae6d
2 changed files with 54 additions and 319 deletions
+52 -317
View File
@@ -13,12 +13,7 @@ category:
index: false
---
:::warning
本教程基于 QChatGPT 2.x 编写,3.0 具有不全面的向后兼容性,请尽可能使用本教程内推荐的方法进行开发。
:::
> 请先阅读[插件介绍页](../plugin/intro.md)
> 请先阅读[技术信息页](../develop/technology-info.md)
> 建议先阅读本项目源码,了解项目架构
> 问题、需求请到仓库issue发起
@@ -30,28 +25,24 @@ index: false
## 💬简介
尽管“为一个基于OpenAI API的QQ机器人开发插件支持”这事看起来有点小题大做,但萌生此想法后的几天内好几个人提出了这个需求,最终促使此项目正式支持插件。
本页讲解普通插件开发步骤,插件内部有两种方法:事件监听器、内容函数。
事件监听器能在某些事件触发时进行响应,更改行为。内容函数可供大语言模型调用,具体请查看插件介绍页。
## 🧱实现
基于`importlib`库加载模块的方法动态加载额外Python程序文件以便实现插件加载,插件均存放在`plugins`文件夹,其中的所有`.py`文件都将被加载(除了所有`__init__.py`)
## 📚示例代码
请查看代码目录`tests/plugin_examples`中的插件目录
QChatGPT 还支持组件扩展,可以不局限于监听预定事件,做到整个组件替换,具体请阅读完此页后查看**组件扩展**页。
## 💻快速开始
按照文档部署此项目,并使其正常运行。
在`plugins`目录下新建目录`hello`,在其中新建空文件`__init__.py`以标记此目录为软件包,继续新建文件`main.py`。
> 您也可以使用[hello_plugin](https://github.com/RockChinQ/hello_plugin)作为模板直接生成插件代码仓库
> 建议使用[hello_plugin](https://github.com/RockChinQ/hello_plugin)作为模板直接生成插件代码仓库后按照下方步骤进行修改。
编辑`main.py`输入以下内容:
```Python
from pkg.plugin.models import *
from pkg.plugin.host import EventContext, PluginHost
from pkg.plugin.context import register, handler, llm_func, BasePlugin, APIHost, EventContext
from pkg.plugin.events import * # 导入事件类
"""
在收到私聊或群聊消息"hello"时,回复"hello, <发送者id>!"或"hello, everyone!"
@@ -60,42 +51,45 @@ from pkg.plugin.host import EventContext, PluginHost
# 注册插件
@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
class HelloPlugin(Plugin):
class HelloPlugin(BasePlugin):
# 插件加载时触发
# plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
def __init__(self, plugin_host: PluginHost):
def __init__(self, host: APIHost):
pass
# 异步初始化
async def initialize(self):
pass
# 当收到个人消息时触发
@on(PersonNormalMessageReceived)
def person_normal_message_received(self, event: EventContext, **kwargs):
msg = kwargs['text_message']
@handler(PersonNormalMessageReceived)
async def person_normal_message_received(self, ctx: EventContext):
msg = ctx.event.text_message # 这里的 event 即为 PersonNormalMessageReceived 的对象
if msg == "hello": # 如果消息为hello
# 输出调试信息
logging.debug("hello, {}".format(kwargs['sender_id']))
self.ap.logger.debug("hello, {}".format(ctx.event.sender_id))
# 回复消息 "hello, <发送者id>!"
event.add_return("reply", ["hello, {}!".format(kwargs['sender_id'])])
ctx.add_return("reply", ["hello, {}!".format(ctx.event.sender_id)])
# 阻止该事件默认行为(向接口获取回复)
event.prevent_default()
ctx.prevent_default()
# 当收到群消息时触发
@on(GroupNormalMessageReceived)
def group_normal_message_received(self, event: EventContext, **kwargs):
msg = kwargs['text_message']
@handler(GroupNormalMessageReceived)
async def group_normal_message_received(self, ctx: EventContext):
msg = ctx.event.text_message # 这里的 event 即为 GroupNormalMessageReceived 的对象
if msg == "hello": # 如果消息为hello
# 输出调试信息
logging.debug("hello, {}".format(kwargs['sender_id']))
self.ap.logger.debug("hello, {}".format(ctx.event.sender_id))
# 回复消息 "hello, everyone!"
event.add_return("reply", ["hello, everyone!"])
ctx.add_return("reply", ["hello, everyone!"])
# 阻止该事件默认行为(向接口获取回复)
event.prevent_default()
ctx.prevent_default()
# 插件卸载时触发
def __del__(self):
@@ -107,25 +101,20 @@ class HelloPlugin(Plugin):
### 解读此插件程序
- `import`从`pkg.plugin`引入`models`模块的所有字段(此程序使用了其中的`register函数`、`on函数`、`Plugin类`、`PersonNormalMessageReceived事件`、`GroupNormalMessageReceived事件`)
- `@register()`将类`HelloPlugin`标记为一个插件类,声明插件名称为`Hello`以及插件简介、版本、作者
- 声明类`HelloPlugin`继承于`Plugin`,此类可以随意命名,插件名称只与`register`调用时的参数有关
- `import pkg.plugin.context`导入`register(用于注册插件类)`, `handler(用于注册事件监听器)`, `llm_func(用于注册内容函数)`,`BasePlugin(插件基类)`, `APIHost(API宿主)`, `EventContext(事件上下文类)`等内容
- `import pkg.plugin.events`导入所有支持的事件类
- `@register`将类`HelloPlugin`标记为一个插件类,声明插件名称为`Hello`以及插件简介、版本、作者
- 声明类`HelloPlugin`继承于`BasePlugin`,此类可以随意命名,插件名称只与`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`节
- 插件类中还支持添加一个异步方法`async def initialize(self)`用于异步初始化
- `@handler`将方法`person_normal_message_received`标记为一个事件监听器,处理`PersonNormalMessageReceived`(收到私聊消息并在获取OpenAI回复前触发)事件,此方法可以随意命名,绑定的事件只与`handler`中的参数有关,更多支持的事件可到`pkg.plugin.events`文件中查看或查看下方`API`节
- 通过`self.ap.logger`日志器输出调试信息,插件类父类中的`ap`对象是整个程序的上下文对象,可以通过这个对象访问程序内所有对象
- 事件监听器方法获得的第二个参数`ctx`即为本次事件的上下文,其中的`event`为本次事件的事件对象,可以从中取出相关参数,具体事件可以取得的参数请查看`pkg.plugin.events`文件中的每个事件类的注释
- 事件监听器方法内部从参数中取出`text_message`参数,判断是否为`hello`,如果是就将返回值`reply`设置为`["hello, {}!".format(ctx.event.sender_id)]`,接下来调用`ctx.prevent_default()`方法,阻止原程序默认行为
- 每个事件支持的返回值请查看`pkg.plugin.events`中的每个事件的注释
- 用相似的程序注册`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`以便开启调试输出
编写完毕保存后,重新启动主程序,向机器人发送`!plugin`命令,若有`Hello`插件则说明插件加载成功
## ❗规范(重要)
@@ -140,46 +129,21 @@ class HelloPlugin(Plugin):
> 您的插件不一定必须包含内容函数,请先查看内容函数页了解此功能
<details>
<summary>示例:联网插件</summary>
加载含有联网功能的内容函数的插件[WebwlkrPlugin](https://github.com/RockChinQ/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插件](https://github.com/RockChinQ/QChatGPT/blob/master/res/screenshots/webwlkr_plugin.png?raw=true)
</details>
### 内容函数编写步骤
1️⃣ 请先按照上方步骤编写您的插件基础结构,现在请删除(当然你也可以不删,只是为了简洁)上述插件内容的诸个由`@on`装饰的类函数
1️⃣ 请先按照上方步骤编写您的插件基础结构,现在请删除(当然你也可以不删,只是为了简洁)上述插件内容的诸个由`@handler`装饰的类函数
<details>
<summary>删除后的结构</summary>
<summary>删除后的类结构</summary>
```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):
def __init__(self, plugin_host: APIHost):
pass
# 插件卸载时触发
@@ -189,14 +153,13 @@ class HelloPlugin(Plugin):
</details>
2️⃣ 现在我们将以下函数添加到刚刚删除的函数的位置
2️⃣ 现在我们将以下函数添加到刚刚删除的函数的位置(作为类方法)
```Python
# 要添加的函数
@func(name="access_the_web") # 设置函数名称
def _(url: str):
@llm_fun(name="access_the_web") # 设置函数名称
async def access_web(self, query, 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.
@@ -235,264 +198,36 @@ def _(url: str):
return s
```
<details>
<summary>现在这个文件内容应该是这样</summary>
```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
@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
```
</details>
#### 请注意:
- 函数的注释必须严格按照要求的格式进行书写,具体格式请查看[此文档](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format)
- 内容函数和`以@on装饰的行为函数`可以同时存在于同一个插件,并同时受到`switch.json`中的插件开关的控制
- 务必确保您使用的模型支持函数调用功能,可以到`config.py`的`completion_api_params`中修改模型,推荐使用`gpt-3.5-turbo-16k`
- 内容函数和`以@llm_func装饰的行为函数`可以同时存在于同一个插件,并同时受到`plugins/plugins.json`中的插件开关的控制
- 务必确保您使用的模型支持函数调用功能
3️⃣ 现在您的程序已具备网络访问功能,重启程序,询问机器人有关在线的内容或直接发送文章链接请求其总结。
- 这仅仅是一个示例,需要更高效的网络访问能力支持插件,请查看[WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin)
## 🔒版本要求
若您的插件对主程序的版本有要求,可以使用以下函数进行断言,若不符合版本,此函数将报错并打断此函数所在的流程:
```python
require_ver("v2.5.1") # 要求最低版本为 v2.5.1
```
```python
require_ver("v2.5.1", "v2.6.0") # 要求最低版本为 v2.5.1, 同时要求最高版本为 v2.6.0
```
- 此函数在主程序`v2.5.1`中加入
- 此函数声明在`pkg.plugin.models`模块中,在插件示例代码最前方已引入此模块所有内容,故可直接使用
## 📄API参考
### 说明
事件处理函数将会获得一系列参数,可以在`kwargs`中取出。
其中`host`参数(`pkg.plugin.host.PluginHost`类的实例)是插件宿主,提供与主程序各个模块交互的一些方法。
`event`参数(`pkg.plugin.host.EventContext`类的实例)是事件执行期间的上下文,提供对此次事件执行的一些操作方法。
事件监听器方法获得的`ctx: EventContext`对象可以到`pkg.plugin.context`模块中查看其结构,`ctx`对象的`event`属性为本次事件的事件对象,可以从中取出相关参数,具体事件可以取得的参数请查看`pkg.plugin.events`文件中的每个事件类的注释。
事件返回值均为**可选**的,可以通过调用`event.add_return(key: str, ret)`来提交返回值
### 事件
所有事件参数均有`host`和`event`,以下仅展示其他参数
若一个事件是一次请求(用户发送消息)中的事件,其事件对象中会含有`query`对象,这个对象中包含了此次请求(即用户发送一条消息的处理过程,一个请求过程可能会触发多个事件)处理过程中的数据。
所有支持的事件,请查看`pkg.plugin.events`文件中的每个事件类的注释。
关于`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 消息链
"""
### BasePlugin 结构
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 消息链
"""
#### self.ap
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支持的消息组件
"""
在插件的方法中访问`self.ap`即为`pkg.core.app.Application`类的对象,包含了整个程序的上下文对象,可以通过这个对象访问程序内所有对象。
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`类
#### self.host
在插件的方法中访问`self.host`即为`pkg.plugin.context.APIHost`类的对象,提供了主程序的一些API,详细请查看其源码。
+2 -2
View File
@@ -18,9 +18,9 @@ index: false
## 插件分类
**插件**,是QChatGPT的扩展模块,分为**行为插件**和**内容函数**两种类型。行为插件由主程序运行中的事件驱动,而内容函数由GPT生成的内容驱动。这两种驱动方式都统一包装在插件类中。
**插件**,是QChatGPT的扩展模块,其中包含**事件监听器**和**内容函数**两种可执行内容。事件监听器由主程序运行中的事件驱动,而内容函数由GPT生成的内容驱动。这两种驱动方式都统一包装在插件类中。
### 行为插件
### 事件监听器
插件监听 QChatGPT 运行期间的各种事件,QChatGPT 在事件发生时,携带事件相关信息调用插件注册的监听器。插件可以通过注册监听器以在指定情况下被触发。