diff --git a/src/.vuepress/config.ts b/src/.vuepress/config.ts index af66ca1ae..252d2a8a9 100644 --- a/src/.vuepress/config.ts +++ b/src/.vuepress/config.ts @@ -1,5 +1,6 @@ import { defineUserConfig } from "vuepress"; import theme from "./theme.js"; +import { searchProPlugin } from "vuepress-plugin-search-pro"; export default defineUserConfig({ base: "/", @@ -9,7 +10,23 @@ export default defineUserConfig({ description: "QChatGPT项目的Wiki页", theme, - + plugins: [ + searchProPlugin({ + // 索引全部内容 + indexContent: true, + // 为分类和标签添加索引 + customFields: [ + { + getter: (page) => page.frontmatter.category, + formatter: "分类:$content", + }, + { + getter: (page) => page.frontmatter.tag, + formatter: "标签:$content", + }, + ], + }), + ], // Enable it with pwa // shouldPrefetch: false, }); diff --git a/src/.vuepress/styles/index.scss b/src/.vuepress/styles/index.scss index f6af38785..bdcbbb51d 100644 --- a/src/.vuepress/styles/index.scss +++ b/src/.vuepress/styles/index.scss @@ -1 +1,11 @@ // place your custom styles here +// .vuepress/styles/index.scss + +// 覆盖代码块背景色 +#app { + --text-color-light: #000; + + html[data-theme="dark"] & { + --text-color-light: #ffffff; + } + } \ No newline at end of file diff --git a/src/posts/PluginsDevelop.md b/src/posts/PluginsDevelop.md index ac9230615..6dfb6bd66 100644 --- a/src/posts/PluginsDevelop.md +++ b/src/posts/PluginsDevelop.md @@ -1,5 +1,487 @@ --- title: 插件开发 icon: code +author: RockChinQ +date: 2023-09-30 +category: + - 插件开发 +index: false --- +> 请先阅读[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/5-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8) +> 请先阅读[技术信息页](https://github.com/RockChinQ/QChatGPT/wiki/4-%E6%8A%80%E6%9C%AF%E4%BF%A1%E6%81%AF) +> 建议先阅读本项目源码,了解项目架构 + +> 问题、需求请到仓库issue发起 +> **提问前请先靠自己尝试** +:::info 目录 +[[toc]] +::: +## 💬简介 + +尽管“为一个基于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' () +``` + +> 建议在`config.py`中设置`logging_level = logging.DEBUG`以便开启调试输出 + +## ❗规范(重要) + +- 请每个插件独立一个目录以便管理,建议在Github上创建一个仓库储存单个插件,以便获取和更新 +- 插件名使用`大驼峰命名法`,如`Hello`、`ExamplePlugin`、`ChineseCommands`等 +- 一个目录内可以存放多个Python程序文件,以独立出插件的各个功能,便于开发者管理,但不建议在一个目录内注册多个插件 +- 插件需要的依赖库请在插件目录下的`requirements.txt`中指定,程序从储存库获取此插件时将自动安装依赖 + +## 🪝内容函数 + +通过[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling)实现的`内容函数`,这是一种嵌入对话中,由GPT自动调用的函数。 + +> 您的插件不一定必须包含内容函数,请先查看内容函数页了解此功能 + +
+示例:联网插件 + +加载含有联网功能的内容函数的插件[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) + +
+ +### 内容函数编写步骤 + +1️⃣ 请先按照上方步骤编写您的插件基础结构,现在请删除(当然你也可以不删,只是为了简洁)上述插件内容的诸个由`@on`装饰的类函数 + +
+删除后的结构 + +```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 + + # 插件卸载时触发 + def __del__(self): + pass +``` + +
+ +2️⃣ 现在我们将以下函数添加到刚刚删除的函数的位置 + +```Python + +# 要添加的函数 + +@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 + +``` +
+现在这个文件内容应该是这样 + +```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 +``` + +
+ +#### 请注意: + +- 函数的注释必须严格按照要求的格式进行书写,具体格式请查看[此文档](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format) +- 内容函数和`以@on装饰的行为函数`可以同时存在于同一个插件,并同时受到`switch.json`中的插件开关的控制 +- 务必确保您使用的模型支持函数调用功能,可以到`config.py`的`completion_api_params`中修改模型,推荐使用`gpt-3.5-turbo-16k` + +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`类的实例)是事件执行期间的上下文,提供对此次事件执行的一些操作方法。 + +事件返回值均为**可选**的,可以通过调用`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 响应文本 + finish_reason: str 响应结束原因 + + returns (optional): + prefix: str 修改后的回复文字消息的前缀 + reply: list 替换回复消息组件列表 +""" + +SessionFirstMessageReceived = "session_first_message_received" +"""会话被第一次交互时触发 + kwargs: + session_name: str 会话名称(_) + session: pkg.openai.session.Session 会话对象 + default_prompt: str 预设值 +""" + +SessionExplicitReset = "session_reset" +"""会话被用户手动重置时触发,此事件不支持阻止默认行为 + kwargs: + session_name: str 会话名称(_) + session: pkg.openai.session.Session 会话对象 +""" + +SessionExpired = "session_expired" +"""会话过期时触发 + kwargs: + session_name: str 会话名称(_) + 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 会话名称(_) + 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`类 + diff --git a/src/posts/Q&A.md b/src/posts/Q&A.md index 0a6766d92..94549a43a 100644 --- a/src/posts/Q&A.md +++ b/src/posts/Q&A.md @@ -6,8 +6,39 @@ date: 2023-09-30 category: - 问题解答 --- +> 之所以有问题解答这一页面,是因为很多人并不仔细阅读[功能介绍](./functionIntroduction.md),以至于产生了一系列问题,为此又增设此页面来提供解决方案 + :::info 目录 [[toc]] ::: +## 项目配置文件如config.py文件双击打不开 +:::tip 解答 +config.py文件是一个python文件,需要相应的编辑器才能打开,很多情况下我们可以使用记事本打开 +但是,我们**强烈建议下载一个代码编辑器**,这样能够极大地方便你对配置文件的修改 +这里以VS Code为例,[下载地址](https://code.visualstudio.com/) +如果你还是不知道,可以网上搜一下,这里提供一个[B站视频](https://www.bilibili.com/video/BV1bK411P767/?spm_id_from=333.337.search-card.all.click&vd_source=b09c71fce8dcc714b307c12de388d2f4) +::: +## [bot]err:调用API失败,请重试或联系管理员,或等待修复 +:::tip 解答 +通常是由于你的API key没有正确填写,打开`QChatGPT\config.py`文件(使用记事本或代码编辑器),找到这一片代码,将你的API Key按照示例填写到对应地方 +```python +openai_config = { + "api_key": { + "default": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "http_proxy": None, + "reverse_proxy": None +} +``` +> 本问题可见于[功能介绍的api-key的配置](./functionIntroduction.md/#api-key的配置) +::: +## [bot]person_ xxxxxxx会话调用API失败:Error communicating with OpenAI您的网络无法访问OpenAI接口或网络代理不正常 +:::tip 解答 +通常是由于你的网络环境所致,你需要使用全局代理或设置反代(推荐) +全局代理因你使用的代理软件而异,你应该了解的 +设置反代,打开`QChatGPT\config.py`文件,按图配置: +` "reverse_proxy": "http://api.openai.lazyshare.top/v1"` +![反代设置.jpg](https://s2.loli.net/2023/08/16/GeoiZCbLtfg3uqH.jpg) +> 本问题可见于[功能介绍的反向代理的设置](./functionIntroduction.md/#反向代理的设置) +::: - \ No newline at end of file