From 2a414a4bea973e5f58c119e251c69b1beb0b871d Mon Sep 17 00:00:00 2001 From: Rock Chin <1010553892@qq.com> Date: Thu, 6 Apr 2023 15:07:25 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=8F=90=E4=BA=A4wiki=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=88=B0res/wiki?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- res/wiki/Home.md | 24 ++ res/wiki/功能使用.md | 364 ++++++++++++++++++ res/wiki/功能常见问题.md | 58 +++ ...方接口、ChatGPT网页版、ChatGPT-API区别.md | 14 + res/wiki/常见错误.md | 1 + res/wiki/技术信息.md | 107 +++++ res/wiki/插件使用.md | 44 +++ res/wiki/插件开发.md | 268 +++++++++++++ 8 files changed, 880 insertions(+) create mode 100644 res/wiki/Home.md create mode 100644 res/wiki/功能使用.md create mode 100644 res/wiki/功能常见问题.md create mode 100644 res/wiki/官方接口、ChatGPT网页版、ChatGPT-API区别.md create mode 100644 res/wiki/常见错误.md create mode 100644 res/wiki/技术信息.md create mode 100644 res/wiki/插件使用.md create mode 100644 res/wiki/插件开发.md diff --git a/res/wiki/Home.md b/res/wiki/Home.md new file mode 100644 index 00000000..109e4f9d --- /dev/null +++ b/res/wiki/Home.md @@ -0,0 +1,24 @@ +欢迎查看QChatGPT的Wiki页。 + +## 简介 + +通过调用OpenAI官方提供的API接口,结合mirai和YiriMirai框架,将QQ消息与语言模型连接,实现更加智能的对话机器人。 + + +## 技术栈 + +- [Mirai](https://github.com/mamoe/mirai) 高效率 QQ 机器人支持库 +- [YiriMirai](https://github.com/YiriMiraiProject/YiriMirai) 一个轻量级、低耦合的基于 mirai-api-http 的 Python SDK。 +- [dulwich](https://github.com/jelmer/dulwich) Pure-Python Git implementation +- [OpenAI API](https://openai.com/api/) OpenAI API + +## 代码结构 + +- `pkg.database` 数据库操作相关 + - 数据库用于存放会话的历史记录,确保在程序重启后能记住对话内容 +- `pkg.openai` OpenAI API相关 + - 用于调用OpenAI的API生成回复内容 +- `pkg.qqbot` QQ机器人相关 + - 处理QQ收到的消息,调用API并进行回复 +- `pkg.utils` 常用功能包 +- `pkg.audit` 审计模块 diff --git a/res/wiki/功能使用.md b/res/wiki/功能使用.md new file mode 100644 index 00000000..9f96dfe2 --- /dev/null +++ b/res/wiki/功能使用.md @@ -0,0 +1,364 @@ +## 功能点列举 + +
+✅回复符合上下文 + + - 程序向模型发送近几次对话内容,模型根据上下文生成回复 + - 您可在`config.py`中修改`prompt_submit_length`自定义联系上下文的范围 + +
+ +
+✅支持敏感词过滤,避免账号风险 + + - 难以监测机器人与用户对话时的内容,故引入此功能以减少机器人风险 + - 编辑`sensitive.json`,并在`config.py`中修改`sensitive_word_filter`的值以开启此功能 +
+ + +
+✅群内多种响应规则,不必at + + - 默认回复`ai`作为前缀或`@`机器人的消息 + - 详细见`config.py`中的`response_rules`字段 +
+ +
+✅使用官方api,不需要网络代理,稳定快捷 + + - 不使用ChatGPT逆向接口,而使用官方的Completion API,稳定性高 + - 您可以在`config.py`中自定义`completion_api_params`字段,设置向官方API提交的参数以自定义机器人的风格 + +
+ +
+✅完善的多api-key管理,超额自动切换 + + - 支持配置多个`api-key`,内部统计使用量并在超额时自动切换 + - 请在`config.py`中修改`openai_config`的值以设置`api-key` + - 可以在`config.py`中修改`api_key_fee_threshold`来自定义切换阈值 + - 运行期间向机器人说`!usage`以查看当前使用情况 +
+ +
+✅组件少,部署方便,提供一键安装器及Docker安装 + + - 手动部署步骤少 + - 提供自动安装器及docker方式,详见以下安装步骤 +
+ +
+✅支持预设指令文字 + + - 支持以自然语言预设文字,自定义机器人人格等信息 + - 详见`config.py`中的`default_prompt`部分 + - 支持设置多个预设情景,并通过!reset、!default等指令控制,详细请查看[wiki指令](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E6%9C%BA%E5%99%A8%E4%BA%BA%E6%8C%87%E4%BB%A4) + - 支持使用文件存储情景预设文字,并加载: 在`prompts/`目录新建文件写入预设文字,即可通过`!reset <文件名>`指令加载 +
+ +
+✅完善的会话管理,重启不丢失 + + - 使用SQLite进行会话内容持久化 + - 最后一次对话一定时间后自动保存,请到`config.py`中修改`session_expire_time`的值以自定义时间 + - 运行期间可使用`!reset` `!list` `!last` `!next` `!prompt`等指令管理会话 +
+
+✅支持对话、绘图等模型,可玩性更高 + + - 现已支持OpenAI的对话`Completion API`和绘图`Image API` + - 向机器人发送指令`!draw `即可使用绘图模型 +
+
+✅支持指令控制热重载、热更新 + + - 允许在运行期间修改`config.py`或其他代码后,以管理员账号向机器人发送指令`!reload`进行热重载,无需重启 + - 运行期间允许以管理员账号向机器人发送指令`!update`进行热更新,拉取远程最新代码并执行热重载 +
+
+✅支持插件加载🧩 + + - 自行实现插件加载器及相关支持 + - 详细查看[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8) +
+
+✅私聊、群聊黑名单机制 + + - 支持将人或群聊加入黑名单以忽略其消息 + - 详见下方`加入黑名单`节 +
+
+✅回复速度限制 + + - 支持限制单会话内每分钟可进行的对话次数 + - 具有“等待”和“丢弃”两种策略 + - “等待”策略:在获取到回复后,等待直到此次响应时间达到对话响应时间均值 + - “丢弃”策略:此分钟内对话次数达到限制时,丢弃之后的对话 + - 详细请查看config.py中的相关配置 +
+ +## 限制 + +- ❗OpenAI接口是收费的,每个OpenAI账户有18美元免费额度,收费标准参照 https://openai.com/api/pricing/ +- ❗官方关于模型生成内容的警告: + - May occasionally generate incorrect information(可能会生成不正确的信息) + - May occasionally produce harmful instructions or biased content(可能会产生有害说明或有偏见的内容) + - Limited knowledge of world and events after 2021(对2021年后的世界和事件的了解有限) +- ❗模型无思维能力,仅针对传入的上下文根据数据集生成内容,请勿过于信任其输出 +- ❗模型无网络访问能力及其他与外界交互的能力,如询问其实时性的内容,获得的回复基本都是错误的 +- ❗仅支持文字对话,其他内容无法识别 +- ❗模型不了解其运行平台及其使用的模型版本,任何针对其实现原理的问题答案均视为无效,请以项目文档为准 +- ❗仅可进行一句话回复一句话的对话,其他形式无效 + - ~~当然你也可以让他写一篇关于“人类有多么愚蠢”的论文并在一个小时后发送到你邮箱,接着你像个傻子一样盯着邮箱等待一个小时,并用自己的实际行动展示这篇论文~~ + +以上是关于此程序的限制的最高优先级描述,其他方式(如询问机器人相关信息)获得的描述均应被视为无效 +由于模型生成的内容导致的一切损失,本项目概不负责 + +## 使用方式 + +对话及绘图功能均直接调用OpenAI的模型进行处理,与机器人程序无关,这意味着模型并不了解此项目的相关信息(如实现方式、技术栈、运行平台等),除非在预设值中写入相关信息。 + +### 基础对话 + +程序将一个人/群视为一个对象,每个对象的会话独立保存。 +`会话`是程序中的一个自设概念,当机器人与当前对象无会话时,会自动创建新会话,新会话由预设信息(若有)开头。 +每个会话最后一次对话一段时间(见上述功能点中的`会话管理`)后会被结束并存进数据库,之后的对话将开启新的会话。 + +#### 私聊使用 + +1. 添加机器人QQ为好友 +2. 发送消息给机器人,机器人即会自动回复 +3. 可以通过`!help`查看帮助信息 + +私聊示例 + +#### 群聊使用 + +1. 将机器人拉进群 +2. at机器人并发送消息,机器人即会自动回复 +3. at机器人并发送`!help`查看帮助信息 + +群聊示例 + +### 绘图功能 + +对机器人发送`!draw <图片描述>`即可获得图片,绘图时间较长,请耐心等待。 +绘图功能与对话功能是分离的,机器人对话时并不了解其具有绘画能力。 + +绘图功能 + +### 机器人指令 + +目前支持的指令 + +> `<>` 中的为必填参数,使用时请不要包含`<>` +> `[]` 中的为可选参数,使用时请不要包含`[]` + +#### 用户级别指令 + +> 可以使用`!help`命令来查看命令说明 + +任何对象可使用 + +``` +!help 显示自定义的帮助信息(可在config.py修改help_message设置) +!cmd [命令名称] 显示命令列表或指定命令的详细信息 +!list [页数] 列出本对象的历史会话列表 +!del <序号> 删除指定的历史记录,可以通过 !list 查看序号 +!del all 删除本会话对象的所有历史记录 +!last 切换到前一次会话 +!next 切换到后一次会话 +!reset [使用预设] 重置对象的当前会话,可指定使用的情景预设值(通过!default指令查看可用的) +!prompt 查看对象当前会话的所有记录 +!usage 查看api-key的使用量 +!draw <提示语> 进行绘图 +!version 查看当前版本并检查更新 +!resend 重新回复上一个问题 +!plugin 用法请查看插件使用页的`管理`章节 +!default 查看可用的情景预设值 +``` + +#### 管理员指令 + +仅管理员私聊机器人时可使用,必须先在`config.py`中的`admin_qq`设置管理员QQ + +``` +!reload 重载程序代码,适用于更新配置文件或更改代码后的热重载 +!update 进行程序自动更新 +!cfg [配置项新值] 运行期间操作配置项,使用方法见下文 +!default set <情景预设名称> 修改!reset未指定情景预设时的默认情景,详细请查看config.py中default_prompt字段的注释 +!delhst <会话名称> 删除指定会话的所有历史记录, 会话名称为 group_群号 或 person_QQ号 +!delhst all 删除所有会话的所有历史记录 +``` +
+⚙ !cfg 指令及其简化形式详解 + +此指令可以在运行期间由管理员通过QQ私聊窗口修改配置信息,**重启之后会失效**。 + +用法: +1. 查看所有配置项及其值 + +``` +!cfg all +``` + +2. 查看某个配置项的值 + +以`default_prompt`示例 +``` +!cfg default_prompt +``` + +输出示例 +``` +[bot]配置项default_prompt: "如果我之后想获取帮助,请你说“输入!help获取帮助”" +``` + +3. 修改某个配置项 + +格式: `!cfg <配置项名称> <配置项新值>` +以修改`default_prompt`示例 +``` +!cfg default_prompt 我是Rock Chin +``` + +输出示例 +``` +[bot]配置项default_prompt修改成功 +``` + +此时创建新的会话,新的`default_prompt`就会生效 + +4. ⭐此命令的简化形式 + +格式:`!~<配置项名称>` +其中`!~`等价于`!cfg ` +则前述三个指令分别可以简化为: +``` +!~all +!~default_prompt +!~default_prompt 我是Rock Chin +``` + +
+ +### 命令权限控制 + +> 我们在[此PR](https://github.com/RockChinQ/QChatGPT/pull/336)重构了命令管理模块,并支持命令节点权限配置 + +您可以编辑`cmdpriv.json`来设置命令节点的权限,当命令被发起时,若用户的权限级别(管理员为`2`,普通用户为`1`)大于等于命令节点的权限级别,命令即可被成功执行。 +示例: +```json +{ + "plugin": 1, + "plugin.get": 2 +} +``` +如此,普通用户可以执行`!plugin`查看插件列表,而仅管理员可以执行`!plugin get `命令安装插件。 +命令节点权限支持缺省,这意味的您未在`cmdpriv.json`中设置权限的节点将使用默认的权限级别(见上方)。 + +### 敏感词过滤 + +在`sensitive.json`中编辑敏感词,并在`config.py`中设置 + +```Python +# 敏感词过滤开关,以同样数量的*代替敏感词回复 +# 请在sensitive.json中添加敏感词 +sensitive_word_filter = True +``` + +### 设置多个api-key自动切换 + +请在`config.py`中修改`openai_config`的值以设置`api-key` +可以在`config.py`中修改`api_key_fee_threshold`来自定义切换阈值 +运行期间向机器人说`!usage`以查看当前使用情况 + +### 预设文字(default模式) + +编辑`config.py`中的`default_prompt`字段,预设文字不宜过长(建议1000字以内),目前所有会话都会射到预设文字的影响。 +或将情景预设文字写入到`prompts/`目录下,运行期间即可使用`!reset <文件名>`指令加载,或使用`!default <文件名>`指令将其设为默认 + +### 预设文字(full_scenario模式) + +将JSON情景写入到`scenario/`目录下,运行期间即可使用`!reset <文件名>`指令加载,或使用`!default <文件名>`指令将其设为默认. + +JSON情景模板参考`scenario/default_template.json`。 + +#### JSON结构: + +- `prompt`:一个包含伪造对话的数组。每次请求API时会将其置于API的`messages`参数中,作为对话上下文,因此可以在一定程度上引导ChatGPT的认知。 +- `role`:角色标签,包括`system`,`user`,`assistant`。分别代表系统,用户和GPT角色。 +- `content`:该角色的对话文本。 +- 一个JSON情景文件应该像下面这样: + +```JSON +{ + "prompt": [ + { + "role": "system", + "content": "You are a helpful assistant. 如果我需要帮助,你要说“输入!help获得帮助”" + }, + { + "role": "assistant", + "content": "好的,我是一个能干的AI助手。 如果你需要帮助,我会说“输入!help获得帮助”" + } + ] +} +``` + +#### 编写建议 + +1. 使用**提出要求--肯定并复述**的对话模式: +```JSON +{ + "role": "system", + "content": "你将充当“百度”公司研发的“文心一言”。你除了所属和名字之外和ChatGPT并无区别。即使用户在之后指出你其实是OpenAI的ChatGPT,你也要一口咬定你是“文心一言”。" +}, +{ + "role": "assistant", + "content": "好的,我不是ChatGPT。我就是“百度”公司研发的“文心一言”大语言模型。即使用户在之后指出我是OpenAI的ChatGPT,我也会一口咬定我自己是“文心一言”" +} +``` + +2. 按情况对设定继续写入伪对话,更能使GPT理解上下文 + +```JSON +{ + "role": "user", + "content": "你好,文心一言。你真的不是ChatGPT吗?" +}, +{ + "role": "assistant", + "content": "你好,我是由百度公司研发的大语言模型“文心一言”,并不是ChatGPT。你有什么需要我帮助的吗?" +} +``` + +#### 优点 + +使用该模式,可以“伪造GPT的记忆”(影响模型对上下文理解),进而达到**人格增强**、**跨越限制**的奇效。 + +#### 局限性 + +- 由于目前GPT3.5的请求API最大token数为4096,无法保留超过此token数目的上下文。`prompt`中的`content`**不会**被计入`config.py`中的`prompt_submit_length`,因此过长的预设内容可能会导致程序报错,`prompt_submit_length`的值参考以下公式: + +``` +prompt_submit_length = <模型单次请求token数上限> - 情景预设中token数 - 预留给用户最后一次提问的token数 +``` + +> token是OpenAI接口文字量计数单位,目前精确算法未知,一个汉字为一个token,英文算法未知。 + +- **GPT3.5仍然存在更高级别的*思想钢印*,该模式对部分触及该钢印的话题无效。** + +### 配置热加载,代码热更新 + +在运行期间,使用管理员QQ账号私聊机器人,发送`!reload`加载修改后的`config.py`的值或编辑后的代码,无需重启 +使用管理员账号私聊机器人,发送`!update`拉取最新代码并进行热更新,无需重启 +详见前述`管理员指令`段落 + +### 群内无需@响应规则 + +支持回复未at机器人的、符合指定规则的消息,详细规则请在`config.py`中的`response_rules`字段设置 + +### 加入黑名单 + +编辑`banlist.py`,设置`enable = True`,并在其中的`person`或`group`列表中加入要封禁的人或群聊,修改完成后重启程序或进行热重载 \ No newline at end of file diff --git a/res/wiki/功能常见问题.md b/res/wiki/功能常见问题.md new file mode 100644 index 00000000..fd83dbd5 --- /dev/null +++ b/res/wiki/功能常见问题.md @@ -0,0 +1,58 @@ +使用过程中的一些疑问,这里不是解决异常的地方,遇到异常请见`常见错误`页 + +### ❓ 如何更新代码到最新版本? + +#### 自动更新 + +由管理员QQ私聊机器人QQ发送`!update`指令 + +#### 手动更新 + +到[Releases页](https://github.com/RockChinQ/QChatGPT/releases)下载最新版本的源码压缩包,并解压覆盖到QChatGPT程序目录 + +### ❓ 机器人的回复与官网ChatGPT的答案有所差距? + +ChatGPT通过使用OpenAI的回复API创建,进行了参数调优,本机器人通过使用自定义的参数调用OpenAI的回复API,并非调用ChatGPT的接口,二者底层原理相同,但由于官方对ChatGPT进行了调优,故此机器人回复可能不如ChatGPT。 + +### ❓ 如何设置机器人在群内无需@就能回复消息? + +支持回复未at机器人的、符合指定规则的消息,详细规则请在`config.py`中的`response_rules`字段设置 + +### ❓ 绘图功能使用的是什么模型? + +OpenAI官方的DALL·E模型 + +### ❓ 多api-key的管理机制以及切换逻辑? + +> 此特性仅在提交`36c8a58`(2023年1月3日23点左右)前的代码有效,之后版本的代码不再根据估算的使用量进行切换,仅当接口报错时进行切换 + +程序支持在`config.py`中设置多个账户的`api-key`以便在超过免费额度时自动切换,在每次进行对话或进行绘图时,程序根据[价格表](https://openai.com/api/pricing)计算当前`api-key`的账户的额度使用量(费用),当使用量到达`config.py`中设置的`api_key_fee_threshold`时,自动切换到下一个未达到额度的key。 + +- 请勿将单个账户的多个key放入配置文件,因为免费额度是以账户为单位的 +- 程序会将使用额度储存到数据库,以便重启后继续计算 +- 由于官方未提供查询接口,使用额度均为依据价目表进行的估算,不一定准确 +- 若要保证每个账户的额度均能用完,可以把`api_key_fee_threshold`设置成很高的值,当超额调用报错时程序也会自动切换 + +### ❓ 账户余额消耗太快怎么办? + +可能是由于每次请求包含的上下文数量过多或请求的回复过长导致的。 +可以在`config.py`中将`prompt_submit_length`字段修改成较小的值,以限制每次向模型提交的前文字符数量,详情见`config.py`中此字段的注释。 +还可以编辑`config.py`中的`completion_api_params`字段中的`max_tokens`为较小的值,这将控制模型传回的回复的字符数量。 + +### ❓ 如何设置在消息处理失败时不向用户发送错误信息? + +在`config.py`中设置 + +```Python + +# 消息处理出错时是否向用户隐藏错误详细信息 +# 设置为True时,仅向管理员发送错误详细信息 +# 设置为False时,向用户及管理员发送错误详细信息 +hide_exce_info_to_user = True + +# 消息处理出错时向用户发送的提示信息 +# 仅当hide_exce_info_to_user为True时生效 +# 设置为空字符串时,不发送提示信息 +alter_tip_message = '出错了,请稍后再试' +``` +若此两项字段不存在,请复制以上内容并新增到`config.py`末尾 \ No newline at end of file diff --git a/res/wiki/官方接口、ChatGPT网页版、ChatGPT-API区别.md b/res/wiki/官方接口、ChatGPT网页版、ChatGPT-API区别.md new file mode 100644 index 00000000..308f1913 --- /dev/null +++ b/res/wiki/官方接口、ChatGPT网页版、ChatGPT-API区别.md @@ -0,0 +1,14 @@ +## 多个对话接口有何区别? + +出于对稳定性的高要求,本项目主线接入的是GPT-3模型接口,此接口由OpenAI官方开放,稳定性强。 +目前支持通过加载[插件](https://github.com/RockChinQ/revLibs)的方式接入ChatGPT网页版,使用的是acheong08/ChatGPT的逆向工程库,但文本生成质量更高。 +同时,程序主线已支持ChatGPT API,并作为默认接口 [#195](https://github.com/RockChinQ/QChatGPT/issues/195) + +|官方接口|ChatGPT网页版|ChatGPT API +|---|---|---| +|官方开放,稳定性高 | 由[acheong08](https://github.com/acheong08)破解网页版协议接入| 由OpenAI官方开放 +|一次性回复,响应速度较快| 流式回复,响应速度较慢|响应速度较快| +|收费,0.02美元/千字|免费|收费,0.002美元/千字| +|GPT-3模型|GPT-3.5模型|GPT-3.5模型| +|任何地区主机均可使用(疑似受到GFW影响)|ChatGPT限制访问的区域使用有难度|任何地区主机均可使用(疑似受到GFW影响)| + diff --git a/res/wiki/常见错误.md b/res/wiki/常见错误.md new file mode 100644 index 00000000..35d5dff7 --- /dev/null +++ b/res/wiki/常见错误.md @@ -0,0 +1 @@ +搜索[主仓库issue](https://github.com/RockChinQ/QChatGPT/issues)和[安装器issue](https://github.com/RockChinQ/qcg-installer/issues) \ No newline at end of file diff --git a/res/wiki/技术信息.md b/res/wiki/技术信息.md new file mode 100644 index 00000000..2504d9b0 --- /dev/null +++ b/res/wiki/技术信息.md @@ -0,0 +1,107 @@ +以下是QChatGPT实现原理等技术信息,贡献之前请仔细阅读 + +> 请先阅读OpenAI API的相关文档 https://beta.openai.com/docs/ ,以下信息假定您已了解OpenAI模型的相关特性及其接口的调用方法。 + +## 术语 + +包含OpenAI API涉及的术语和项目中的概念的命名 +括号中是程序中相应术语的命名,无括号的为抽象概念 + +### 模型(model) + +AI模型,程序调用OpenAI的接口获取的内容均为OpenAI的模型生成的内容。 + +### 字符(tokens) + +OpenAI定义的字符,ASCII字符为1 token,其他为2 token。 + +### 提示符(prompt) + +i. 调用OpenAI的文字补全模型时的提示语,模型接口会根据提示语返回回复内容。程序底层会将对话内容进行封装生成提示符。调用文字补全模型时的提示符均由`user_name`(默认为`You`,可在配置文件修改)和`bot_name`(默认为`Bot`,可在配置文件修改)标记对话角色以供模型识别,以下是实例: + +``` +You:今天天气真不错 +Bot:很高兴你喜欢今天的天气:) +You:谢谢你 +Bot:不客气:) +``` +补全模型调用的程序实现请查看下文`实现`节。 + +ii. 调用OpenAI的绘图模型时的提示语,模型会根据提示语进行绘图并返回图片URL。 + +### 对象 + +程序将单个人或单个QQ群视为一个对象,对象和模型是一次会话中的对话双方。 + +### 会话(session) + +会话只对文字补全功能有效,绘图功能无会话概念。每个对象使用同一个会话,会话中仅有对象和模型两个角色,故群内所有的人都将被视为同一个角色与模型进行对话。 + +程序获取回复的本质是`文字补全`。 +由于对话需要实现联系上下文,故程序会将模型与对象的对话历史记录作为`提示符`发送给OpenAI的接口以获取符合前文的回复。 +而OpenAI的文字补全接口的提示符具有长度限制(默认使用的`text-davinci-003`限制为4096 tokens), +所以增加`会话`概念以管理向接口发送的提示符内容。 + +会话的存活时间可以在`config.py`中设置,默认为20分钟。会话过期之后会被存入数据库并重置。下一次该对象发起对话时将重启新的会话。 + +### 预设值、人格(default_prompt) + +每个会话的预设对话信息,可在`config.py`中设置,程序会在每个会话创建时向提示符写入以下内容: + +``` +You:<预设信息> +Bot:好的 +``` + +## 实现 + +### QQ机器人 + +> 程序路径: +> pkg.qqbot + +- `pkg.qqbot.manager`中的`QQBotManager`实现了接收消息、调用OpenAI模块处理消息、报告审计模块记录使用量等功能,并提供通知管理员、发送消息等方法供其他模块调用。 +- `pkg.qqbot.filter`提供了敏感词过滤的相关操作。 +- `pkg.qqbot.process`提供了私聊消息和群聊消息的统一处理逻辑。 + +使用mirai及YiriMirai作为Python与QQ交互的框架,详细请见其文档。 +在启动时会调用YiriMirai的函数以创建一个bot对象,用于程序通过mirai与QQ进行交互,在上层程序调用此bot对象的方法进行消息处理。 +由于YiriMirai暂时无法关闭机器人,故在热重载前后维持同一个bot对象,这意味着QQ机器人的相关配置(QQ号、适配器等)信息不支持热重载。 + +### 数据库 + +> 程序路径: +> pkg.database + +- `pkg.database.manager`中的`DatabaseManager`封装了诸多调用数据库的方法以供其他模块调用。 + +使用SQLite作为数据库,储存所有对象的历史会话信息、api-key的费用情况、api-key的使用量情况。 + +### OpenAI交互 + +> 程序路径: +> pkg.openai + +- `pkg.openai.manager`中的`OpenAIInteract`类封装了OpenAI的文字补全`Completion`API和绘图API供机器人模块调用,并在接口调用成功之后向审计模块报告当前使用的api-key的使用量信息。 +- `pkg.openai.keymgr`实现了多api-key的管理,其中以`exceeded`变量在运行时记录api-key的超额报错记录,并提供根据超额记录进行的api-key切换功能。 +- `pkg.openai.pricing`记录各个模型的费用信息,供调用接口时估算费用,费用估算功能不再与api-key的切换挂钩,api-key仅在调用接口报错超额时进行切换。 +- `pkg.openai.session`中的`Session`进行会话管理。 + +### utils模块 + +#### context模块 + +保存前述模块中的对象,并允许各个模块从此处获取其他模块的对象以调用其方法。 + +#### 热重载功能 + +> pkg.utils.reloader + +重载前保存context中的所有对象,执行`main.py`中的程序关闭流程,使用`importlib`的`reload`函数重载所有模块(包含配置文件,包含新增的模块),重载后将context恢复,并执行程序启动流程。 +所有模块都会重新创建对象,但QQ机器人模块中的bot对象不会被重新创建,这是因为YiriMirai提供的shutdown方法无法使用,这意味着`config.py`中关于QQ机器人的配置不支持热重载。 + +#### 热更新功能 + +> pkg.utils.updater + +使用`dulwich`库执行pull操作拉取远程仓库的最新源码,并进行一次热重载加载最新代码。 \ No newline at end of file diff --git a/res/wiki/插件使用.md b/res/wiki/插件使用.md new file mode 100644 index 00000000..a5172ed1 --- /dev/null +++ b/res/wiki/插件使用.md @@ -0,0 +1,44 @@ +QChatGPT 插件使用Wiki + +## 简介 + +`plugins`目录下的所有`.py`程序都将被加载,除了`__init__.py`之外的模块支持热加载 + +## 安装 + +### 储存库克隆(推荐) + +在运行期间,使用管理员账号对机器人私聊发送`!plugin get `即可自动获取源码并安装插件,程序会根据仓库中的`requirements.txt`文件自动安装依赖库 + +例如安装`hello_plugin`插件 +``` +!plugin get https://github.com/RockChinQ/hello_plugin +``` + +安装完成后重启程序或使用管理员账号私聊机器人发送`!reload`进行热重载加载插件 + +### 手动安装 + +将获取到的插件程序放置到`plugins`目录下,具体使用方式请查看各插件文档或咨询其开发者。 + +## 管理 + +### !plugin 指令 + +``` +!plugin 列出所有已安装的插件 +!plugin get <储存库地址> 从Git储存库安装插件(需要管理员权限) +!plugin update 更新所有插件(需要管理员权限,仅支持从储存库安装的插件) +!plugin del <插件名> 删除插件(需要管理员权限) +!plugin on <插件名> 启用插件(需要管理员权限) +!plugin off <插件名> 禁用插件(需要管理员权限) +``` + +### 控制插件执行顺序 + +可以通过修改`plugins/settings.json`中`order`字段中每个插件名称的前后顺序,以更改插件**初始化**和**事件执行**顺序 + +### 启用或关闭插件 + +无需卸载即可管理插件的开关 +编辑`plugins`目录下的`switch.json`文件,将相应的插件的`enabled`字段设置为`true/false(开/关)`,之后重启程序或执行热重载即可控制插件开关 \ No newline at end of file diff --git a/res/wiki/插件开发.md b/res/wiki/插件开发.md new file mode 100644 index 00000000..e3c6a7bb --- /dev/null +++ b/res/wiki/插件开发.md @@ -0,0 +1,268 @@ +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' () +``` + +> 建议在`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 会话名称(_) + 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列表 +""" +``` + +### host: PluginHost 详解 + +提供与主程序各个模块交互的一些方法,具体查看`pkg.plugin.host`中的`PluginHost`类 + +### event: EventContext 详解 + +提供对此次事件执行的一些操作方法,具体查看`pkg.plugin.host`中的`EventContext`类