diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..d505d8e
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,34 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/python
+{
+ "name": "QChatGPT 3.10",
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
+ "image": "mcr.microsoft.com/devcontainers/python:0-3.10",
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ // "features": {},
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [],
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ // "postCreateCommand": "pip3 install --user -r requirements.txt",
+
+ // Configure tool-specific properties.
+ // "customizations": {},
+ "customizations": {
+ "codespaces": {
+ "repositories": {
+ "RockChinQ/QChatGPT": {
+ "permissions": "write-all"
+ },
+ "RockChinQ/revLibs": {
+ "permissions": "write-all"
+ }
+ }
+ }
+ }
+
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
+}
diff --git a/.github/ISSUE_TEMPLATE/漏洞反馈.md b/.github/ISSUE_TEMPLATE/漏洞反馈.md
index 711fa24..e3696ee 100644
--- a/.github/ISSUE_TEMPLATE/漏洞反馈.md
+++ b/.github/ISSUE_TEMPLATE/漏洞反馈.md
@@ -1,6 +1,6 @@
---
name: 漏洞反馈
-about: 报错或漏洞请使用这个模板创建
+about: 报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭
title: "[BUG]"
labels: 'bug'
assignees: ''
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..8650393
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,25 @@
+## 概述
+
+实现/解决/优化的内容:
+
+### 事务
+
+- [ ] 已阅读仓库[贡献指引](../CONTRIBUTING.md)
+- [ ] 已与维护者在issues或其他平台沟通此PR大致内容
+
+## 以下内容可在起草PR后、合并PR前逐步完成
+
+### 功能
+
+- [ ] 已编写完善的配置文件字段说明(若有新增)
+- [ ] 已编写面向用户的新功能说明(若有必要)
+- [ ] 已测试新功能或更改
+
+### 兼容性
+
+- [ ] 已处理版本兼容性
+- [ ] 已处理插件兼容问题
+
+### 风险
+
+可能导致或已知的问题:
diff --git a/.gitignore b/.gitignore
index 362973b..c9b02dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,9 @@ prompts/
logs/
sensitive.json
temp/
-current_tag
\ No newline at end of file
+current_tag
+scenario/
+!scenario/default-template.json
+override.json
+cookies.json
+res/announcement_saved
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..156b43f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "QChatGPT.wiki"]
+ path = QChatGPT.wiki
+ url = https://github.com/RockChinQ/QChatGPT.wiki.git
diff --git a/QChatGPT.wiki b/QChatGPT.wiki
new file mode 160000
index 0000000..68c4ef5
--- /dev/null
+++ b/QChatGPT.wiki
@@ -0,0 +1 @@
+Subproject commit 68c4ef5d240877a871044e0b340db183453799bf
diff --git a/README.md b/README.md
index 034fe33..d7e1f3f 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,13 @@
# QChatGPT🤖
-> 2023/3/3 官方接口疑似被墙,可考虑使用网络代理 [#198](https://github.com/RockChinQ/QChatGPT/issues/198)
+
+> 2023/3/18 现已支持GPT-4 API(内测),请查看`config-template.py`中的`completion_api_params`
+> 2023/3/15 逆向库已支持New Bing,使用方法查看[插件文档](https://github.com/RockChinQ/revLibs)
+> 2023/3/15 逆向库已支持GPT-4模型,使用方法查看[插件](https://github.com/RockChinQ/revLibs)
> 2023/3/3 现已在主线支持官方ChatGPT接口,使用方法查看[#195](https://github.com/RockChinQ/QChatGPT/issues/195)
-> 2023/3/2 OpenAI已发布ChatGPT官方接口,我们正在全力接入,预计明日前完成,请查看[此PR](https://github.com/RockChinQ/QChatGPT/pull/194)
-> 2023/2/16 现已支持接入ChatGPT网页版,详情请完成部署并查看底部**插件**小节或[此仓库](https://github.com/RockChinQ/revLibs)
- 到[项目Wiki](https://github.com/RockChinQ/QChatGPT/wiki)可了解项目详细信息
-- 由bilibili TheLazy制作的[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)
-- 交流、答疑群: ~~204785790~~(已满)、691226829、656285629
+- ~~由bilibili TheLazy制作的[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)~~(寄了,求大佬做个新的)
+- 交流、答疑群: ~~204785790~~(已满)、~~691226829~~(已满)、656285629
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
- QQ频道机器人见[QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT)
@@ -14,11 +15,17 @@
## 🍺模型适配一览
+
+点击此处展开
+
### 文字对话
- OpenAI GPT-3.5模型(ChatGPT API), 本项目原生支持, 默认使用
-- OpenAI GPT-3模型, 本项目原生支持, 部署完成后前往config.py切换
-- ChatGPT网页版逆向API, 由[插件](https://github.com/RockChinQ/revLibs)接入
+- OpenAI GPT-3模型, 本项目原生支持, 部署完成后前往`config.py`切换
+- OpenAI GPT-4模型, 本项目原生支持, 目前需要您的账户通过OpenAI的内测申请, 请前往`config.py`切换
+- ChatGPT网页版GPT-3.5模型, 由[插件](https://github.com/RockChinQ/revLibs)接入
+- ChatGPT网页版GPT-4模型, 目前需要ChatGPT Plus订阅, 由[插件](https://github.com/RockChinQ/revLibs)接入
+- New Bing逆向库, 由[插件](https://github.com/RockChinQ/revLibs)接入
### 故事续写
@@ -32,6 +39,10 @@
### 语音生成
- TTS+VITS, 由[插件](https://github.com/dominoar/QChatPlugins)接入
+- Plachta/VITS-Umamusume-voice-synthesizer, 由[插件](https://github.com/oliverkirk-sudo/chat_voice)接入
+
+
+
## ✅功能
@@ -106,18 +117,26 @@
- “丢弃”策略:此分钟内对话次数达到限制时,丢弃之后的对话
- 详细请查看config.py中的相关配置
+
+✅支持使用网络代理
+
+ - 目前已支持正向代理访问接口
+ - 详细请查看config.py中的`openai_config`的说明
+
详情请查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
## 🔩部署
-**部署过程中遇到任何问题,请先在[QChatGPT](https://github.com/RockChinQ/QChatGPT/issues)或[qcg-installer](https://github.com/RockChinQ/qcg-installer/issues)的issue里进行搜索**
+**部署过程中遇到任何问题,请先在[QChatGPT](https://github.com/RockChinQ/QChatGPT/issues)或[qcg-installer](https://github.com/RockChinQ/qcg-installer/issues)的issue里进行搜索**
### - 注册OpenAI账号
+> 若您要直接使用非OpenAI的模型(如New Bing),可跳过此步骤,直接进行之后的部署,完成后按照相关插件的文档进行配置即可
+
参考以下文章自行注册
-> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
+> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
> [手把手教你如何注册ChatGPT,超级详细](https://guxiaobei.com/51461)
注册成功后请前往[个人中心查看](https://beta.openai.com/account/api-keys)api_key
@@ -162,8 +181,7 @@ cd QChatGPT
2. 安装依赖
```bash
-pip3 install yiri-mirai openai colorlog func_timeout
-pip3 install dulwich
+pip3 install yiri-mirai openai colorlog func_timeout dulwich Pillow
```
3. 运行一次主程序,生成配置文件
@@ -194,7 +212,8 @@ python3 main.py
## 🚀使用
-查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
+**部署完成后必看: [指令说明](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)**
+所有功能查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
## 🧩插件生态
@@ -202,6 +221,9 @@ python3 main.py
详见[Wiki插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
开发教程见[Wiki插件开发页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91)
+
+查看插件列表
+
### 示例插件
在`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
@@ -216,20 +238,23 @@ python3 main.py
- [revLibs](https://github.com/RockChinQ/revLibs) - 将ChatGPT网页版接入此项目,关于[官方接口和网页版有什么区别](https://github.com/RockChinQ/QChatGPT/wiki/%E5%AE%98%E6%96%B9%E6%8E%A5%E5%8F%A3%E4%B8%8EChatGPT%E7%BD%91%E9%A1%B5%E7%89%88)
- [hello_plugin](https://github.com/RockChinQ/hello_plugin) - `hello_plugin` 的储存库形式,插件开发模板
-- [dominoar/QChatPlugins](https://github.com/dominoar/QchatPlugins) - dominoar编写的诸多新功能插件(语言输出、Ranimg、屏蔽词规则等)
+- [dominoar/QChatPlugins](https://github.com/dominoar/QchatPlugins) - dominoar编写的诸多新功能插件(语音输出、Ranimg、屏蔽词规则等)
- [dominoar/QCP-NovelAi](https://github.com/dominoar/QCP-NovelAi) - NovelAI 故事叙述与绘画
+- [oliverkirk-sudo/chat_voice](https://github.com/oliverkirk-sudo/chat_voice) - 文字转语音输出,使用HuggingFace上的[VITS-Umamusume-voice-synthesizer模型](https://huggingface.co/spaces/Plachta/VITS-Umamusume-voice-synthesizer)
+- [RockChinQ/WaitYiYan](https://github.com/RockChinQ/WaitYiYan) - 实时获取百度`文心一言`等待列表人数
+- [QChartGPT_Emoticon_Plugin](https://github.com/chordfish-k/QChartGPT_Emoticon_Plugin) - 使机器人根据回复内容发送表情包
+
## 😘致谢
- [@the-lazy-me](https://github.com/the-lazy-me) 为本项目制作[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)
- [@mikumifa](https://github.com/mikumifa) 本项目Docker部署仓库开发者
- [@dominoar](https://github.com/dominoar) 为本项目开发多种插件
-- [@hissincn](https://github.com/hissincn) 本项目贡献者
-- [@LINSTCL](https://github.com/LINSTCL) GPT-3.5官方模型适配贡献者
-- [@Haibersut](https://github.com/Haibersut) 本项目贡献者
+- [@万神的星空](https://github.com/qq255204159) 整合包发行
+- [@ljcduo](https://github.com/ljcduo) GPT-4 API内测账号提供
-以及其他所有为本项目提供支持的朋友们。
+以及所有[贡献者](https://github.com/RockChinQ/QChatGPT/graphs/contributors)和其他为本项目提供支持的朋友们。
-## 👍赞赏
+
diff --git a/config-template.py b/config-template.py
index bc4fd34..d6e86c6 100644
--- a/config-template.py
+++ b/config-template.py
@@ -79,6 +79,35 @@ default_prompt = {
"default": "如果我之后想获取帮助,请你说“输入!help获取帮助”",
}
+# 情景预设格式
+# 参考值:旧版本方式:default | 完整情景:full_scenario
+# 旧版本的格式为上述default_prompt中的内容,或prompts目录下的文件名
+#
+# 完整情景预设的格式为JSON,在scenario目录下的JSON文件中列出对话的每个回合,编写方法见scenario/default-template.json
+# 编写方法例如:
+# {
+# "prompt": [
+# {
+# "role": "user",
+# "content": "之后当我需要帮助时,请说“输入!help获取帮助”"
+# },{
+# "role": "assistant",
+# "content": "好的,当你之后需要帮助时,我会说“输入!help获取帮助”"
+# },{
+# "role": "user",
+# "content": "帮助"
+# },{
+# "role": "assistant",
+# "content": "输入!help获取帮助"
+# }
+# ]
+# }
+#
+# 您可以按照上述格式编写自己的情景预设,在prompt中列出对话的每个回合,
+# role为user或assistant,分别表示用户和机器人的回复
+# 每个JSON文件是一个情景预设,文件名即为情景预设的名称
+preset_mode = "default"
+
# 群内响应规则
# 符合此消息的群内消息即使不包含at机器人也会响应
# 支持消息前缀匹配及正则表达式匹配
@@ -133,12 +162,16 @@ encourage_sponsor_at_start = True
# 每次向OpenAI接口发送对话记录上下文的字符数
# 最大不超过(4096 - max_tokens)个字符,max_tokens为下方completion_api_params中的max_tokens
# 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快
-prompt_submit_length = 1024
+prompt_submit_length = 2048
# OpenAI补全API的参数
# 请在下方填写模型,程序自动选择接口
# 现已支持的模型有:
#
+# 'gpt-4'
+# 'gpt-4-0314'
+# 'gpt-4-32k'
+# 'gpt-4-32k-0314'
# 'gpt-3.5-turbo'
# 'gpt-3.5-turbo-0301'
# 'text-davinci-003'
@@ -150,10 +183,10 @@ prompt_submit_length = 1024
# 'text-ada-001'
#
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create
+# 请将内容修改到config.py中,请勿修改config-template.py
completion_api_params = {
"model": "gpt-3.5-turbo",
"temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1]
- "max_tokens": 1024, # 每次获取OpenAI接口响应的文字量上限, 不高于4096
"top_p": 1, # 生成的文本的文本与要求的符合度, 取值范围[0, 1]
"frequency_penalty": 0.2,
"presence_penalty": 1.0,
@@ -257,11 +290,4 @@ help_message = """此机器人通过调用OpenAI的GPT-3大型语言模型生成
每次会话最后一次交互后{}分钟后会自动结束,结束后将开启新会话,如需继续前一次会话请发送 !last 重新开启
欢迎到github.com/RockChinQ/QChatGPT 给个star
-帮助信息:
-!help - 显示帮助
-!reset - 重置会话
-!last - 切换到前一次的对话
-!next - 切换到后一次的对话
-!prompt - 显示当前对话所有内容
-!list - 列出所有历史会话
-!usage - 列出各个api-key的使用量""".format(session_expire_time // 60)
+指令帮助信息请查看: 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""".format(session_expire_time // 60)
diff --git a/generate_override_all.py b/generate_override_all.py
new file mode 100644
index 0000000..69674c3
--- /dev/null
+++ b/generate_override_all.py
@@ -0,0 +1,23 @@
+# 使用config-template生成override.json的字段全集模板文件override-all.json
+# 关于override.json机制,请参考:https://github.com/RockChinQ/QChatGPT/pull/271
+import json
+import importlib
+
+
+template = importlib.import_module("config-template")
+output_json = {
+ "comment": "这是override.json支持的字段全集, 关于override.json机制, 请查看https://github.com/RockChinQ/QChatGPT/pull/271"
+}
+
+
+for k, v in template.__dict__.items():
+ if k.startswith("__"):
+ continue
+ # 如果是module
+ if type(v) == type(template):
+ continue
+ print(k, v, type(v))
+ output_json[k] = v
+
+with open("override-all.json", "w", encoding="utf-8") as f:
+ json.dump(output_json, f, indent=4, ensure_ascii=False)
diff --git a/main.py b/main.py
index 28b6936..16130fe 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,5 @@
import importlib
+import json
import os
import shutil
import threading
@@ -12,8 +13,8 @@ try:
except ImportError:
# 尝试安装
import pkg.utils.pkgmgr as pkgmgr
- pkgmgr.install_requirements("requirements.txt")
try:
+ pkgmgr.install_requirements("requirements.txt")
import colorlog
except ImportError:
print("依赖不满足,请查看 https://github.com/RockChinQ/qcg-installer/issues/15")
@@ -32,7 +33,7 @@ log_colors_config = {
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
- 'CRITICAL': 'bold_red',
+ 'CRITICAL': 'cyan',
}
@@ -114,8 +115,21 @@ def load_config():
setattr(config, key, getattr(config_template, key))
logging.warning("[{}]不存在".format(key))
is_integrity = False
+
if not is_integrity:
logging.warning("配置文件不完整,请依据config-template.py检查config.py")
+
+ # 检查override.json覆盖
+ if os.path.exists("override.json"):
+ override_json = json.load(open("override.json", "r", encoding="utf-8"))
+ for key in override_json:
+ if hasattr(config, key):
+ setattr(config, key, override_json[key])
+ logging.info("覆写配置[{}]为[{}]".format(key, override_json[key]))
+ else:
+ logging.error("无法覆写配置[{}]为[{}],该配置不存在,请检查override.json是否正确".format(key, override_json[key]))
+
+ if not is_integrity:
logging.warning("以上配置已被设为默认值,将在5秒后继续启动... ")
time.sleep(5)
@@ -146,7 +160,6 @@ def start(first_time_init=False):
try:
sh = reset_logging()
-
pkg.utils.context.context['logger_handler'] = sh
# 检查是否设置了管理员
@@ -180,6 +193,7 @@ def start(first_time_init=False):
import pkg.openai.dprompt
pkg.openai.dprompt.read_prompt_from_file()
+ pkg.openai.dprompt.read_scenario_from_file()
# 主启动流程
database = pkg.database.manager.DatabaseManager()
@@ -258,6 +272,13 @@ def start(first_time_init=False):
# run_bot_wrapper
# )
finally:
+ # 判断若是Windows,输出选择模式可能会暂停程序的警告
+ if os.name == 'nt':
+ time.sleep(2)
+ logging.info("您正在使用Windows系统,若命令行窗口处于“选择”模式,程序可能会被暂停,此时请右键点击窗口空白区域使其取消选择模式。")
+
+ time.sleep(12)
+
if first_time_init:
if not known_exception_caught:
logging.info('程序启动完成,如长时间未显示 ”成功登录到账号xxxxx“ ,并且不回复消息,请查看 '
@@ -289,13 +310,22 @@ def start(first_time_init=False):
import pkg.utils.updater
try:
if pkg.utils.updater.is_new_version_available():
- pkg.utils.context.get_qqbot_manager().notify_admin("新版本可用,请发送 !update 进行自动更新\n更新日志:\n{}".format("\n".join(pkg.utils.updater.get_rls_notes())))
+ logging.info("新版本可用,请发送 !update 进行自动更新\n更新日志:\n{}".format("\n".join(pkg.utils.updater.get_rls_notes())))
else:
logging.info("当前已是最新版本")
except Exception as e:
logging.warning("检查更新失败:{}".format(e))
+ try:
+ import pkg.utils.announcement as announcement
+ new_announcement = announcement.fetch_new()
+ if new_announcement != "":
+ logging.critical("[公告] {}".format(new_announcement))
+ except Exception as e:
+ logging.warning("获取公告失败:{}".format(e))
+
+ return qqbot
def stop():
import pkg.qqbot.manager
@@ -331,6 +361,10 @@ def check_file():
if not os.path.exists("sensitive.json"):
shutil.copy("sensitive-template.json", "sensitive.json")
+ # 检查是否有scenario/default.json
+ if not os.path.exists("scenario/default.json"):
+ shutil.copy("scenario/default-template.json", "scenario/default.json")
+
# 检查temp目录
if not os.path.exists("temp/"):
os.mkdir("temp/")
diff --git a/override-all.json b/override-all.json
new file mode 100644
index 0000000..cf39af0
--- /dev/null
+++ b/override-all.json
@@ -0,0 +1,75 @@
+{
+ "comment": "这是override.json支持的字段全集, 关于override.json机制, 请查看https://github.com/RockChinQ/QChatGPT/pull/271",
+ "mirai_http_api_config": {
+ "adapter": "WebSocketAdapter",
+ "host": "localhost",
+ "port": 8080,
+ "verifyKey": "yirimirai",
+ "qq": 1234567890
+ },
+ "openai_config": {
+ "api_key": {
+ "default": "openai_api_key"
+ },
+ "http_proxy": null
+ },
+ "admin_qq": 0,
+ "default_prompt": {
+ "default": "如果我之后想获取帮助,请你说“输入!help获取帮助”"
+ },
+ "preset_mode": "default",
+ "response_rules": {
+ "at": true,
+ "prefix": [
+ "/ai",
+ "!ai",
+ "!ai",
+ "ai"
+ ],
+ "regexp": [],
+ "random_rate": 0.0
+ },
+ "ignore_rules": {
+ "prefix": [
+ "/"
+ ],
+ "regexp": []
+ },
+ "income_msg_check": false,
+ "sensitive_word_filter": true,
+ "baidu_check": false,
+ "baidu_api_key": "",
+ "baidu_secret_key": "",
+ "inappropriate_message_tips": "[百度云]请珍惜机器人,当前返回内容不合规",
+ "encourage_sponsor_at_start": true,
+ "prompt_submit_length": 1024,
+ "completion_api_params": {
+ "model": "gpt-3.5-turbo",
+ "temperature": 0.9,
+ "top_p": 1,
+ "frequency_penalty": 0.2,
+ "presence_penalty": 1.0
+ },
+ "image_api_params": {
+ "size": "256x256"
+ },
+ "quote_origin": true,
+ "include_image_description": true,
+ "process_message_timeout": 30,
+ "show_prefix": false,
+ "blob_message_threshold": 256,
+ "blob_message_strategy": "forward",
+ "font_path": "",
+ "retry_times": 3,
+ "hide_exce_info_to_user": false,
+ "alter_tip_message": "出错了,请稍后再试",
+ "pool_num": 10,
+ "session_expire_time": 1200,
+ "rate_limitation": 60,
+ "rate_limit_strategy": "wait",
+ "rate_limit_drop_tip": "本分钟对话次数超过限速次数,此对话被丢弃",
+ "upgrade_dependencies": true,
+ "report_usage": true,
+ "logging_level": 20,
+ "help_message": "此机器人通过调用OpenAI的GPT-3大型语言模型生成回复,不具有情感。\n你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。\n了解此项目请找QQ 1010553892 联系作者\n请不要用其生成整篇文章或大段代码,因为每次只会向模型提交少部分文字,生成大部分文字会产生偏题、前后矛盾等问题\n每次会话最后一次交互后20分钟后会自动结束,结束后将开启新会话,如需继续前一次会话请发送 !last 重新开启\n欢迎到github.com/RockChinQ/QChatGPT 给个star\n\n指令帮助信息请查看: 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"
+}
\ No newline at end of file
diff --git a/pkg/audit/gatherer.py b/pkg/audit/gatherer.py
index 6237b8c..4768d27 100644
--- a/pkg/audit/gatherer.py
+++ b/pkg/audit/gatherer.py
@@ -46,7 +46,7 @@ class DataGatherer:
config = pkg.utils.context.get_config()
if hasattr(config, "report_usage") and not config.report_usage:
return
- res = requests.get("http://rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}".format(subservice_name, self.version_str, count))
+ res = requests.get("http://reports.rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}".format(subservice_name, self.version_str, count))
if res.status_code != 200 or res.text != "ok":
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
except:
diff --git a/pkg/database/manager.py b/pkg/database/manager.py
index 5fde3c2..33d6cfb 100644
--- a/pkg/database/manager.py
+++ b/pkg/database/manager.py
@@ -35,6 +35,7 @@ class DatabaseManager:
def __execute__(self, *args, **kwargs) -> Cursor:
# logging.debug('SQL: {}'.format(sql))
+ logging.debug('SQL: {}'.format(args))
c = self.cursor.execute(*args, **kwargs)
self.conn.commit()
return c
@@ -52,10 +53,30 @@ class DatabaseManager:
`create_timestamp` bigint not null,
`last_interact_timestamp` bigint not null,
`status` varchar(255) not null default 'on_going',
- `prompt` text not null
+ `default_prompt` text not null default '',
+ `prompt` text not null,
+ `token_counts` text not null default '[]'
)
""")
+ # 检查sessions表是否存在`default_prompt`字段, 检查是否存在`token_counts`字段
+ self.__execute__("PRAGMA table_info('sessions')")
+ columns = self.cursor.fetchall()
+ has_default_prompt = False
+ has_token_counts = False
+ for field in columns:
+ if field[1] == 'default_prompt':
+ has_default_prompt = True
+ if field[1] == 'token_counts':
+ has_token_counts = True
+ if has_default_prompt and has_token_counts:
+ break
+ if not has_default_prompt:
+ self.__execute__("alter table `sessions` add column `default_prompt` text not null default ''")
+ if not has_token_counts:
+ self.__execute__("alter table `sessions` add column `token_counts` text not null default '[]'")
+
+
self.__execute__("""
create table if not exists `account_fee`(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -75,7 +96,7 @@ class DatabaseManager:
# session持久化
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,
- last_interact_timestamp: int, prompt: str):
+ last_interact_timestamp: int, prompt: str, default_prompt: str = '', token_counts: str = ''):
"""持久化指定session"""
# 检查是否已经有了此name和create_timestamp的session
@@ -88,20 +109,20 @@ class DatabaseManager:
if count == 0:
sql = """
- insert into `sessions` (`name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`)
- values (?, ?, ?, ?, ?, ?)
+ insert into `sessions` (`name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `default_prompt`, `token_counts`)
+ values (?, ?, ?, ?, ?, ?, ?, ?)
"""
self.__execute__(sql,
("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp,
- last_interact_timestamp, prompt))
+ last_interact_timestamp, prompt, default_prompt, token_counts))
else:
sql = """
- update `sessions` set `last_interact_timestamp` = ?, `prompt` = ?
+ update `sessions` set `last_interact_timestamp` = ?, `prompt` = ?, `token_counts` = ?
where `type` = ? and `number` = ? and `create_timestamp` = ?
"""
- self.__execute__(sql, (last_interact_timestamp, prompt, subject_type,
+ self.__execute__(sql, (last_interact_timestamp, prompt, token_counts, subject_type,
subject_number, create_timestamp))
# 显式关闭一个session
@@ -126,7 +147,7 @@ class DatabaseManager:
# 从数据库中加载所有还没过期的session
config = pkg.utils.context.get_config()
self.__execute__("""
- select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
+ select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `last_interact_timestamp` > {}
""".format(int(time.time()) - config.session_expire_time))
results = self.cursor.fetchall()
@@ -139,6 +160,8 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
+ default_prompt = result[7]
+ token_counts = result[8]
# 当且仅当最后一个该对象的会话是on_going状态时,才会被加载
if status == 'on_going':
@@ -147,7 +170,9 @@ class DatabaseManager:
'subject_number': subject_number,
'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp,
- 'prompt': prompt
+ 'prompt': prompt,
+ 'default_prompt': default_prompt,
+ 'token_counts': token_counts
}
else:
if session_name in sessions:
@@ -159,7 +184,7 @@ class DatabaseManager:
def last_session(self, session_name: str, cursor_timestamp: int):
self.__execute__("""
- select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
+ select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `name` = '{}' and `last_interact_timestamp` < {} order by `last_interact_timestamp` desc
limit 1
""".format(session_name, cursor_timestamp))
@@ -175,20 +200,24 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
+ default_prompt = result[7]
+ token_counts = result[8]
return {
'subject_type': subject_type,
'subject_number': subject_number,
'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp,
- 'prompt': prompt
+ 'prompt': prompt,
+ 'default_prompt': default_prompt,
+ 'token_counts': token_counts
}
# 获取此session_name后一个session的数据
def next_session(self, session_name: str, cursor_timestamp: int):
self.__execute__("""
- select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
+ select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `name` = '{}' and `last_interact_timestamp` > {} order by `last_interact_timestamp` asc
limit 1
""".format(session_name, cursor_timestamp))
@@ -204,19 +233,23 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
+ default_prompt = result[7]
+ token_counts = result[8]
return {
'subject_type': subject_type,
'subject_number': subject_number,
'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp,
- 'prompt': prompt
+ 'prompt': prompt,
+ 'default_prompt': default_prompt,
+ 'token_counts': token_counts
}
# 列出与某个对象的所有对话session
def list_history(self, session_name: str, capacity: int, page: int):
self.__execute__("""
- select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
+ select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit {} offset {}
""".format(session_name, capacity, capacity * page))
results = self.cursor.fetchall()
@@ -229,17 +262,42 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
+ default_prompt = result[7]
+ token_counts = result[8]
sessions.append({
'subject_type': subject_type,
'subject_number': subject_number,
'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp,
- 'prompt': prompt
+ 'prompt': prompt,
+ 'default_prompt': default_prompt,
+ 'token_counts': token_counts
})
return sessions
+ def delete_history(self, session_name: str, index: int) -> bool:
+ # 删除倒序第index个session
+ # 查找其id再删除
+ self.__execute__("""
+ delete from `sessions` where `id` in (select `id` from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit 1 offset {})
+ """.format(session_name, index))
+
+ return self.cursor.rowcount == 1
+
+ def delete_all_history(self, session_name: str) -> bool:
+ self.__execute__("""
+ delete from `sessions` where `name` = '{}'
+ """.format(session_name))
+ return self.cursor.rowcount > 0
+
+ def delete_all_session_history(self) -> bool:
+ self.__execute__("""
+ delete from `sessions`
+ """)
+ return self.cursor.rowcount > 0
+
# 将apikey的使用量存进数据库
def dump_api_key_usage(self, api_keys: dict, usage: dict):
logging.debug('dumping api key usage...')
diff --git a/pkg/openai/dprompt.py b/pkg/openai/dprompt.py
index 3aba31c..84dc32f 100644
--- a/pkg/openai/dprompt.py
+++ b/pkg/openai/dprompt.py
@@ -1,4 +1,6 @@
# 多情景预设值管理
+import json
+import logging
__current__ = "default"
"""当前默认使用的情景预设的名称
@@ -9,8 +11,10 @@ __current__ = "default"
__prompts_from_files__ = {}
"""从文件中读取的情景预设值"""
+__scenario_from_files__ = {}
-def read_prompt_from_file() -> str:
+
+def read_prompt_from_file():
"""从文件读取预设值"""
# 读取prompts/目录下的所有文件,以文件名为键,文件内容为值
# 保存在__prompts_from_files__中
@@ -23,6 +27,19 @@ def read_prompt_from_file() -> str:
__prompts_from_files__[file] = f.read()
+def read_scenario_from_file():
+ """从JSON文件读取情景预设"""
+ global __scenario_from_files__
+ import os
+
+ __scenario_from_files__ = {}
+ for file in os.listdir("scenario"):
+ if file == "default-template.json":
+ continue
+ with open(os.path.join("scenario", file), encoding="utf-8") as f:
+ __scenario_from_files__[file] = json.load(f)
+
+
def get_prompt_dict() -> dict:
"""获取预设值字典"""
import config
@@ -65,15 +82,40 @@ def set_to_default():
__current__ = list(default_dict.keys())[0]
-def get_prompt(name: str = None) -> str:
+def get_prompt(name: str = None) -> list:
+ global __scenario_from_files__
+ import config
+ preset_mode = config.preset_mode
+
"""获取预设值"""
if name is None:
name = get_current()
- default_dict = get_prompt_dict()
+ # JSON预设方式
+ if preset_mode == 'full_scenario':
+ import os
- for key in default_dict:
- if key.lower().startswith(name.lower()):
- return default_dict[key]
+ for key in __scenario_from_files__:
+ if key.lower().startswith(name.lower()):
+ logging.debug('成功加载情景预设从JSON文件: {}'.format(key))
+ return __scenario_from_files__[key]['prompt']
+
+ # 默认预设方式
+ elif preset_mode == 'default':
- raise KeyError("未找到情景预设: " + name)
+ default_dict = get_prompt_dict()
+
+ for key in default_dict:
+ if key.lower().startswith(name.lower()):
+ return [
+ {
+ "role": "user",
+ "content": default_dict[key]
+ },
+ {
+ "role": "assistant",
+ "content": "好的。"
+ }
+ ]
+
+ raise KeyError("未找到默认情景预设: " + name)
diff --git a/pkg/openai/keymgr.py b/pkg/openai/keymgr.py
index 7127db8..4bb8203 100644
--- a/pkg/openai/keymgr.py
+++ b/pkg/openai/keymgr.py
@@ -88,4 +88,4 @@ class KeysManager:
for key_name in self.api_key:
if self.api_key[key_name] == api_key:
return key_name
- return ""
\ No newline at end of file
+ return ""
diff --git a/pkg/openai/manager.py b/pkg/openai/manager.py
index 4a3ceab..2d64e9a 100644
--- a/pkg/openai/manager.py
+++ b/pkg/openai/manager.py
@@ -34,7 +34,7 @@ class OpenAIInteract:
pkg.utils.context.set_openai_manager(self)
# 请求OpenAI Completion
- def request_completion(self, prompts) -> str:
+ def request_completion(self, prompts) -> tuple[str, int]:
"""请求补全接口回复
Parameters:
@@ -60,14 +60,18 @@ class OpenAIInteract:
logging.debug("OpenAI response: %s", response)
+ # 记录使用量
+ current_round_token = 0
if 'model' in config.completion_api_params:
self.audit_mgr.report_text_model_usage(config.completion_api_params['model'],
ai.get_total_tokens())
+ current_round_token = ai.get_total_tokens()
elif 'engine' in config.completion_api_params:
self.audit_mgr.report_text_model_usage(config.completion_api_params['engine'],
response['usage']['total_tokens'])
+ current_round_token = response['usage']['total_tokens']
- return ai.get_message()
+ return ai.get_message(), current_round_token
def request_image(self, prompt) -> dict:
"""请求图片接口回复
diff --git a/pkg/openai/modelmgr.py b/pkg/openai/modelmgr.py
index e67f98c..801c44d 100644
--- a/pkg/openai/modelmgr.py
+++ b/pkg/openai/modelmgr.py
@@ -21,6 +21,10 @@ COMPLETION_MODELS = {
CHAT_COMPLETION_MODELS = {
'gpt-3.5-turbo',
'gpt-3.5-turbo-0301',
+ 'gpt-4',
+ 'gpt-4-0314',
+ 'gpt-4-32k',
+ 'gpt-4-32k-0314'
}
EDIT_MODELS = {
diff --git a/pkg/openai/session.py b/pkg/openai/session.py
index 38a629d..6bef950 100644
--- a/pkg/openai/session.py
+++ b/pkg/openai/session.py
@@ -40,7 +40,7 @@ def reset_session_prompt(session_name, prompt):
prompt = [
{
'role': 'system',
- 'content': config.default_prompt['default']
+ 'content': config.default_prompt['default'] if type(config.default_prompt) == dict else config.default_prompt
}
]
# 警告
@@ -72,9 +72,12 @@ def load_sessions():
temp_session.last_interact_timestamp = session_data[session_name]['last_interact_timestamp']
try:
temp_session.prompt = json.loads(session_data[session_name]['prompt'])
+ temp_session.token_counts = json.loads(session_data[session_name]['token_counts'])
except Exception:
temp_session.prompt = reset_session_prompt(session_name, session_data[session_name]['prompt'])
temp_session.persistence()
+ temp_session.default_prompt = json.loads(session_data[session_name]['default_prompt']) if \
+ session_data[session_name]['default_prompt'] else []
sessions[session_name] = temp_session
@@ -104,6 +107,12 @@ class Session:
prompt = []
"""使用list来保存会话中的回合"""
+ token_counts = []
+ """每个回合的token数量"""
+
+ default_prompt = []
+ """本session的默认prompt"""
+
create_timestamp = 0
"""会话创建时间"""
@@ -129,33 +138,26 @@ class Session:
# 从配置文件获取会话预设信息
def get_default_prompt(self, use_default: str = None):
- config = pkg.utils.context.get_config()
-
import pkg.openai.dprompt as dprompt
if use_default is None:
- current_default_prompt = dprompt.get_prompt(dprompt.get_current())
- else:
- current_default_prompt = dprompt.get_prompt(use_default)
+ use_default = dprompt.get_current()
- return [
- {
- 'role': 'user',
- 'content': current_default_prompt
- }, {
- 'role': 'assistant',
- 'content': 'ok'
- }
- ]
+ current_default_prompt = dprompt.get_prompt(use_default)
+ return current_default_prompt
def __init__(self, name: str):
self.name = name
self.create_timestamp = int(time.time())
self.last_interact_timestamp = int(time.time())
+ self.prompt = []
+ self.token_counts = []
self.schedule()
self.response_lock = threading.Lock()
- self.prompt = self.get_default_prompt()
+
+ self.default_prompt = self.get_default_prompt()
+ logging.debug("prompt is: {}".format(self.default_prompt))
# 设定检查session最后一次对话是否超过过期时间的计时器
def schedule(self):
@@ -199,11 +201,11 @@ class Session:
self.last_interact_timestamp = int(time.time())
# 触发插件事件
- if self.prompt == self.get_default_prompt():
+ if not self.prompt:
args = {
'session_name': self.name,
'session': self,
- 'default_prompt': self.prompt,
+ 'default_prompt': self.default_prompt,
}
event = pkg.plugin.host.emit(plugin_models.SessionFirstMessageReceived, **args)
@@ -213,9 +215,16 @@ class Session:
config = pkg.utils.context.get_config()
max_length = config.prompt_submit_length if hasattr(config, "prompt_submit_length") else 1024
+ prompts, counts = self.cut_out(text, max_length)
+
+ # 计算请求前的prompt数量
+ total_token_before_query = 0
+ for token_count in counts:
+ total_token_before_query += token_count
+
# 向API请求补全
- message = pkg.utils.context.get_openai_manager().request_completion(
- self.cut_out(text, max_length),
+ message, total_token = pkg.utils.context.get_openai_manager().request_completion(
+ prompts,
)
# 成功获取,处理回复
@@ -232,6 +241,10 @@ class Session:
self.prompt.append({'role': 'user', 'content': text})
self.prompt.append({'role': 'assistant', 'content': res_ans})
+ # 向token_counts中添加本回合的token数量
+ self.token_counts.append(total_token-total_token_before_query)
+ logging.debug("本回合使用token: {}, session counts: {}".format(total_token-total_token_before_query, self.token_counts))
+
if self.just_switched_to_exist_session:
self.just_switched_to_exist_session = False
self.set_ongoing()
@@ -248,35 +261,61 @@ class Session:
question = self.prompt[-2]['content']
self.prompt = self.prompt[:-2]
+ self.token_counts = self.token_counts[:-1]
# 返回上一回合的问题
return question
# 构建对话体
- def cut_out(self, msg: str, max_tokens: int) -> list:
- """将现有prompt进行切割处理,使得新的prompt长度不超过max_tokens"""
- # 如果用户消息长度超过max_tokens,直接返回
+ def cut_out(self, msg: str, max_tokens: int) -> tuple[list, list]:
+ """将现有prompt进行切割处理,使得新的prompt长度不超过max_tokens
- temp_prompt = [
+ :return: (新的prompt, 新的token_counts)
+ """
+
+ # 最终由三个部分组成
+ # - default_prompt 情景预设固定值
+ # - changable_prompts 可变部分, 此会话中的历史对话回合
+ # - current_question 当前问题
+
+ # 包装目前的对话回合内容
+ changable_prompts = []
+ changable_counts = []
+ # 倒着来, 遍历prompt的步长为2, 遍历tokens_counts的步长为1
+ changable_index = len(self.prompt) - 1
+ token_count_index = len(self.token_counts) - 1
+
+ packed_tokens = 0
+
+ while changable_index >= 0 and token_count_index >= 0:
+ if packed_tokens + self.token_counts[token_count_index] > max_tokens:
+ break
+
+ changable_prompts.insert(0, self.prompt[changable_index])
+ changable_prompts.insert(0, self.prompt[changable_index - 1])
+ changable_counts.insert(0, self.token_counts[token_count_index])
+ packed_tokens += self.token_counts[token_count_index]
+
+ changable_index -= 2
+ token_count_index -= 1
+
+ # 将default_prompt和changable_prompts合并
+ result_prompt = self.default_prompt + changable_prompts
+
+ # 添加当前问题
+ result_prompt.append(
{
'role': 'user',
'content': msg
}
- ]
+ )
- token_count = len(msg)
- # 倒序遍历prompt
- for i in range(len(self.prompt) - 1, -1, -1):
- if token_count >= max_tokens:
- break
+ logging.debug('cut_out: {}\nchangable section tokens: {}\npacked counts: {}\nsession counts: {}'.format(json.dumps(result_prompt, ensure_ascii=False, indent=4),
+ packed_tokens,
+ changable_counts,
+ self.token_counts))
- # 将prompt加到temp_prompt头部
- temp_prompt.insert(0, self.prompt[i])
- token_count += len(self.prompt[i]['content'])
-
- logging.debug('cut_out: {}'.format(str(temp_prompt)))
-
- return temp_prompt
+ return result_prompt, changable_counts
# 持久化session
def persistence(self):
@@ -291,11 +330,11 @@ class Session:
subject_number = int(name_spt[1])
db_inst.persistence_session(subject_type, subject_number, self.create_timestamp, self.last_interact_timestamp,
- json.dumps(self.prompt))
+ json.dumps(self.prompt), json.dumps(self.default_prompt), json.dumps(self.token_counts))
# 重置session
def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True, use_prompt: str = None):
- if self.prompt[-1]['role'] != "system":
+ if self.prompt:
self.persistence()
if explicit:
# 触发插件事件
@@ -311,7 +350,10 @@ class Session:
if expired:
pkg.utils.context.get_database_manager().set_session_expired(self.name, self.create_timestamp)
- self.prompt = self.get_default_prompt(use_prompt)
+
+ self.default_prompt = self.get_default_prompt(use_prompt)
+ self.prompt = []
+ self.token_counts = []
self.create_timestamp = int(time.time())
self.last_interact_timestamp = int(time.time())
self.just_switched_to_exist_session = False
@@ -337,9 +379,11 @@ class Session:
self.last_interact_timestamp = last_one['last_interact_timestamp']
try:
self.prompt = json.loads(last_one['prompt'])
+ self.token_counts = json.loads(last_one['token_counts'])
except json.decoder.JSONDecodeError:
self.prompt = reset_session_prompt(self.name, last_one['prompt'])
self.persistence()
+ self.default_prompt = json.loads(last_one['default_prompt']) if last_one['default_prompt'] else []
self.just_switched_to_exist_session = True
return self
@@ -356,9 +400,11 @@ class Session:
self.last_interact_timestamp = next_one['last_interact_timestamp']
try:
self.prompt = json.loads(next_one['prompt'])
+ self.token_counts = json.loads(next_one['token_counts'])
except json.decoder.JSONDecodeError:
self.prompt = reset_session_prompt(self.name, next_one['prompt'])
self.persistence()
+ self.default_prompt = json.loads(next_one['default_prompt']) if next_one['default_prompt'] else []
self.just_switched_to_exist_session = True
return self
@@ -366,5 +412,11 @@ class Session:
def list_history(self, capacity: int = 10, page: int = 0):
return pkg.utils.context.get_database_manager().list_history(self.name, capacity, page)
+ def delete_history(self, index: int) -> bool:
+ return pkg.utils.context.get_database_manager().delete_history(self.name, index)
+
+ def delete_all_history(self) -> bool:
+ return pkg.utils.context.get_database_manager().delete_all_history(self.name)
+
def draw_image(self, prompt: str):
return pkg.utils.context.get_openai_manager().request_image(prompt)
diff --git a/pkg/plugin/host.py b/pkg/plugin/host.py
index a8163f1..ae3aee6 100644
--- a/pkg/plugin/host.py
+++ b/pkg/plugin/host.py
@@ -5,6 +5,7 @@ import importlib
import os
import pkgutil
import sys
+import shutil
import traceback
import pkg.utils.context as context
@@ -160,6 +161,22 @@ def install_plugin(repo_url: str):
main.reset_logging()
+def uninstall_plugin(plugin_name: str) -> str:
+ """ 卸载插件 """
+ if plugin_name not in __plugins__:
+ raise Exception("插件不存在")
+
+ # 获取文件夹路径
+ plugin_path = __plugins__[plugin_name]['path'].replace("\\", "/")
+
+ # 剪切路径为plugins/插件名
+ plugin_path = plugin_path.split("plugins/")[1].split("/")[0]
+
+ # 删除文件夹
+ shutil.rmtree("plugins/"+plugin_path)
+ return "plugins/"+plugin_path
+
+
class EventContext:
""" 事件上下文 """
eid = 0
diff --git a/pkg/qqbot/blob.py b/pkg/qqbot/blob.py
index c6edff2..abcccf6 100644
--- a/pkg/qqbot/blob.py
+++ b/pkg/qqbot/blob.py
@@ -1,5 +1,4 @@
# 长消息处理相关
-import logging
import os
import time
import base64
@@ -67,7 +66,7 @@ def check_text(text: str) -> list:
"""检查文本是否为长消息,并转换成该使用的消息链组件"""
if not hasattr(config, 'blob_message_threshold'):
return [text]
-
+
if len(text) > config.blob_message_threshold:
if not hasattr(config, 'blob_message_strategy'):
raise AttributeError('未定义长消息处理策略')
@@ -77,8 +76,6 @@ def check_text(text: str) -> list:
# 转换成图片
return [text_to_image(text)]
elif config.blob_message_strategy == 'forward':
- # 敏感词屏蔽
- text = context.get_qqbot_manager().reply_filter.process(text)
# 包装转发消息
display = ForwardMessageDiaplay(
diff --git a/pkg/qqbot/cmds/__init__.py b/pkg/qqbot/cmds/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pkg/qqbot/cmds/func.py b/pkg/qqbot/cmds/func.py
new file mode 100644
index 0000000..9ee73cf
--- /dev/null
+++ b/pkg/qqbot/cmds/func.py
@@ -0,0 +1,36 @@
+from pkg.qqbot.cmds.model import command
+
+import logging
+
+from mirai import Image
+
+import config
+import pkg.openai.session
+
+@command(
+ "draw",
+ "使用DALL·E模型作画",
+ "!draw <图片提示语>",
+ [],
+ False
+)
+def cmd_draw(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """使用DALL·E模型作画"""
+ reply = []
+
+ if len(params) == 0:
+ reply = ["[bot]err:请输入图片描述文字"]
+ else:
+ session = pkg.openai.session.get_session(session_name)
+
+ res = session.draw_image(" ".join(params))
+
+ logging.debug("draw_image result:{}".format(res))
+ reply = [Image(url=res['data'][0]['url'])]
+ if not (hasattr(config, 'include_image_description')
+ and not config.include_image_description):
+ reply.append(" ".join(params))
+
+ return reply
\ No newline at end of file
diff --git a/pkg/qqbot/cmds/model.py b/pkg/qqbot/cmds/model.py
new file mode 100644
index 0000000..b8bb274
--- /dev/null
+++ b/pkg/qqbot/cmds/model.py
@@ -0,0 +1,45 @@
+# 指令模型
+import logging
+
+commands = []
+"""已注册的指令类
+{
+ "name": "指令名",
+ "description": "指令描述",
+ "usage": "指令用法",
+ "aliases": ["别名1", "别名2"],
+ "admin_only": "是否仅管理员可用",
+ "func": "指令执行函数"
+}
+"""
+
+
+def command(name: str, description: str, usage: str, aliases: list = None, admin_only: bool = False):
+ """指令装饰器"""
+
+ def wrapper(fun):
+ commands.append({
+ "name": name,
+ "description": description,
+ "usage": usage,
+ "aliases": aliases,
+ "admin_only": admin_only,
+ "func": fun
+ })
+ return fun
+
+ return wrapper
+
+
+def search(cmd: str) -> dict:
+ """查找指令"""
+ for command in commands:
+ if (command["name"] == cmd) or (cmd in command["aliases"]):
+ return command
+ return None
+
+
+import pkg.qqbot.cmds.func
+import pkg.qqbot.cmds.system
+import pkg.qqbot.cmds.session
+import pkg.qqbot.cmds.plugin
diff --git a/pkg/qqbot/cmds/plugin.py b/pkg/qqbot/cmds/plugin.py
new file mode 100644
index 0000000..0e40040
--- /dev/null
+++ b/pkg/qqbot/cmds/plugin.py
@@ -0,0 +1,129 @@
+from pkg.qqbot.cmds.model import command
+import pkg.utils.context
+import pkg.plugin.switch as plugin_switch
+
+import os
+import threading
+import logging
+
+
+def plugin_operation(cmd, params, is_admin):
+ reply = []
+
+ import pkg.plugin.host as plugin_host
+ import pkg.utils.updater as updater
+
+ plugin_list = plugin_host.__plugins__
+
+ if len(params) == 0:
+ reply_str = "[bot]所有插件({}):\n".format(len(plugin_host.__plugins__))
+ idx = 0
+ for key in plugin_host.iter_plugins_name():
+ plugin = plugin_list[key]
+ reply_str += "\n#{} {} {}\n{}\nv{}\n作者: {}\n"\
+ .format((idx+1), plugin['name'],
+ "[已禁用]" if not plugin['enabled'] else "",
+ plugin['description'],
+ plugin['version'], plugin['author'])
+
+ if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
+ remote_url = updater.get_remote_url("/".join(plugin['path'].split('/')[:-1]))
+ if remote_url != "https://github.com/RockChinQ/QChatGPT" and remote_url != "https://gitee.com/RockChin/QChatGPT":
+ reply_str += "源码: "+remote_url+"\n"
+
+ idx += 1
+
+ reply = [reply_str]
+ elif params[0] == 'update':
+ # 更新所有插件
+ if is_admin:
+ def closure():
+ import pkg.utils.context
+ updated = []
+ for key in plugin_list:
+ plugin = plugin_list[key]
+ if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
+ success = updater.pull_latest("/".join(plugin['path'].split('/')[:-1]))
+ if success:
+ updated.append(plugin['name'])
+
+ # 检查是否有requirements.txt
+ pkg.utils.context.get_qqbot_manager().notify_admin("正在安装依赖...")
+ for key in plugin_list:
+ plugin = plugin_list[key]
+ if os.path.exists("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt"):
+ logging.info("{}检测到requirements.txt,安装依赖".format(plugin['name']))
+ import pkg.utils.pkgmgr
+ pkg.utils.pkgmgr.install_requirements("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt")
+
+ import main
+ main.reset_logging()
+
+ pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}".format(", ".join(updated)))
+
+ threading.Thread(target=closure).start()
+ reply = ["[bot]正在更新所有插件,请勿重复发起..."]
+ else:
+ reply = ["[bot]err:权限不足"]
+ elif params[0] == 'del' or params[0] == 'delete':
+ if is_admin:
+ if len(params) < 2:
+ reply = ["[bot]err:未指定插件名"]
+ else:
+ plugin_name = params[1]
+ if plugin_name in plugin_list:
+ unin_path = plugin_host.uninstall_plugin(plugin_name)
+ reply = ["[bot]已删除插件: {} ({}), 请发送 !reload 重载插件".format(plugin_name, unin_path)]
+ else:
+ reply = ["[bot]err:未找到插件: {}, 请使用!plugin指令查看插件列表".format(plugin_name)]
+ else:
+ reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
+ elif params[0] == 'on' or params[0] == 'off' :
+ new_status = params[0] == 'on'
+ if is_admin:
+ if len(params) < 2:
+ reply = ["[bot]err:未指定插件名"]
+ else:
+ plugin_name = params[1]
+ if plugin_name in plugin_list:
+ plugin_list[plugin_name]['enabled'] = new_status
+ plugin_switch.dump_switch()
+ reply = ["[bot]已{}插件: {}".format("启用" if new_status else "禁用", plugin_name)]
+ else:
+ reply = ["[bot]err:未找到插件: {}, 请使用!plugin指令查看插件列表".format(plugin_name)]
+ else:
+ reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
+ elif params[0].startswith("http"):
+ if is_admin:
+
+ def closure():
+ try:
+ plugin_host.install_plugin(params[0])
+ pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 指令重载插件")
+ except Exception as e:
+ logging.error("插件安装失败:{}".format(e))
+ pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
+
+ threading.Thread(target=closure, args=()).start()
+ reply = ["[bot]正在安装插件..."]
+ else:
+ reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
+ else:
+ reply = ["[bot]err:未知参数: {}".format(params)]
+
+ return reply
+
+
+@command(
+ "plugin",
+ "插件相关操作",
+ "!plugin\n!plugin <插件仓库地址>\!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>",
+ [],
+ False
+)
+def cmd_plugin(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """插件相关操作"""
+ reply = plugin_operation(cmd, params, is_admin)
+ return reply
\ No newline at end of file
diff --git a/pkg/qqbot/cmds/session.py b/pkg/qqbot/cmds/session.py
new file mode 100644
index 0000000..8693dfd
--- /dev/null
+++ b/pkg/qqbot/cmds/session.py
@@ -0,0 +1,282 @@
+# 会话管理相关指令
+import datetime
+import json
+
+from pkg.qqbot.cmds.model import command
+import pkg.openai.session
+import pkg.utils.context
+import config
+
+@command(
+ "reset",
+ "重置当前会话",
+ "!reset\n!reset [使用情景预设名称]",
+ [],
+ False
+)
+def cmd_reset(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """重置会话"""
+ reply = []
+
+ if len(params) == 0:
+ pkg.openai.session.get_session(session_name).reset(explicit=True)
+ reply = ["[bot]会话已重置"]
+ else:
+ pkg.openai.session.get_session(session_name).reset(explicit=True, use_prompt=params[0])
+ reply = ["[bot]会话已重置,使用场景预设:{}".format(params[0])]
+
+ return reply
+
+
+@command(
+ "last",
+ "切换到前一次会话",
+ "!last",
+ [],
+ False
+)
+def cmd_last(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """切换到前一次会话"""
+ reply = []
+ result = pkg.openai.session.get_session(session_name).last_session()
+ if result is None:
+ reply = ["[bot]没有前一次的对话"]
+ else:
+ datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
+ '%Y-%m-%d %H:%M:%S')
+ reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(datetime_str)]
+
+ return reply
+
+@command(
+ "next",
+ "切换到后一次会话",
+ "!next",
+ [],
+ False
+)
+def cmd_next(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: int, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """切换到后一次会话"""
+ reply = []
+
+ result = pkg.openai.session.get_session(session_name).next_session()
+ if result is None:
+ reply = ["[bot]没有后一次的对话"]
+ else:
+ datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
+ '%Y-%m-%d %H:%M:%S')
+ reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(datetime_str)]
+
+ return reply
+
+
+@command(
+ "prompt",
+ "获取当前会话的前文",
+ "!prompt",
+ [],
+ False
+)
+def cmd_prompt(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """获取当前会话的前文"""
+ reply = []
+
+ msgs = ""
+ session:list = pkg.openai.session.get_session(session_name).prompt
+ for msg in session:
+ if len(params) != 0 and params[0] in ['-all', '-a']:
+ msgs = msgs + "{}: {}\n\n".format(msg['role'], msg['content'])
+ elif len(msg['content']) > 30:
+ msgs = msgs + "[{}]: {}...\n\n".format(msg['role'], msg['content'][:30])
+ else:
+ msgs = msgs + "[{}]: {}\n\n".format(msg['role'], msg['content'])
+ reply = ["[bot]当前对话所有内容:\n{}".format(msgs)]
+
+ return reply
+
+
+@command(
+ "list",
+ "列出当前会话的所有历史记录",
+ "!list\n!list [页数]",
+ [],
+ False
+)
+def cmd_list(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """列出当前会话的所有历史记录"""
+ reply = []
+
+ pkg.openai.session.get_session(session_name).persistence()
+ page = 0
+
+ if len(params) > 0:
+ try:
+ page = int(params[0])
+ except ValueError:
+ pass
+
+ results = pkg.openai.session.get_session(session_name).list_history(page=page)
+ if len(results) == 0:
+ reply = ["[bot]第{}页没有历史会话".format(page)]
+ else:
+ reply_str = "[bot]历史会话 第{}页:\n".format(page)
+ current = -1
+ for i in range(len(results)):
+ # 时间(使用create_timestamp转换) 序号 部分内容
+ datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
+ msg = ""
+ try:
+ msg = json.loads(results[i]['prompt'])
+ except json.decoder.JSONDecodeError:
+ msg = pkg.openai.session.reset_session_prompt(session_name, results[i]['prompt'])
+ # 持久化
+ pkg.openai.session.get_session(session_name).persistence()
+ if len(msg) >= 2:
+ reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
+ datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
+ msg[0]['content'])
+ else:
+ reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
+ datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
+ "无内容")
+ if results[i]['create_timestamp'] == pkg.openai.session.get_session(
+ session_name).create_timestamp:
+ current = i + page * 10
+
+ reply_str += "\n以上信息倒序排列"
+ if current != -1:
+ reply_str += ",当前会话是 #{}\n".format(current)
+ else:
+ reply_str += ",当前处于全新会话或不在此页"
+
+ reply = [reply_str]
+
+ return reply
+
+
+@command(
+ "resend",
+ "重新获取上一次问题的回复",
+ "!resend",
+ [],
+ False
+)
+def cmd_resend(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """重新获取上一次问题的回复"""
+ reply = []
+
+ session = pkg.openai.session.get_session(session_name)
+ to_send = session.undo()
+
+ mgr = pkg.utils.context.get_qqbot_manager()
+
+ reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config,
+ launcher_type, launcher_id, sender_id)
+
+ return reply
+
+
+@command(
+ "del",
+ "删除当前会话的历史记录",
+ "!del <序号>\n!del all",
+ [],
+ False
+)
+def cmd_del(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """删除当前会话的历史记录"""
+ reply = []
+
+ if len(params) == 0:
+ reply = ["[bot]参数不足, 格式: !del <序号>\n可以通过!list查看序号"]
+ else:
+ if params[0] == 'all':
+ pkg.openai.session.get_session(session_name).delete_all_history()
+ reply = ["[bot]已删除所有历史会话"]
+ elif params[0].isdigit():
+ if pkg.openai.session.get_session(session_name).delete_history(int(params[0])):
+ reply = ["[bot]已删除历史会话 #{}".format(params[0])]
+ else:
+ reply = ["[bot]没有历史会话 #{}".format(params[0])]
+ else:
+ reply = ["[bot]参数错误, 格式: !del <序号>\n可以通过!list查看序号"]
+ return reply
+
+
+@command(
+ "default",
+ "操作情景预设",
+ "!default\n!default [指定情景预设为默认]",
+ [],
+ False
+)
+def cmd_default(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """操作情景预设"""
+ reply = []
+
+ if len(params) == 0:
+ # 输出目前所有情景预设
+ import pkg.openai.dprompt as dprompt
+ reply_str = "[bot]当前所有情景预设:\n\n"
+ for key,value in dprompt.get_prompt_dict().items():
+ reply_str += " - {}: {}\n".format(key,value)
+
+ reply_str += "\n当前默认情景预设:{}\n".format(dprompt.get_current())
+ reply_str += "请使用!default <情景预设>来设置默认情景预设"
+ reply = [reply_str]
+ elif len(params) >0 and is_admin:
+ # 设置默认情景
+ import pkg.openai.dprompt as dprompt
+ try:
+ dprompt.set_current(params[0])
+ reply = ["[bot]已设置默认情景预设为:{}".format(dprompt.get_current())]
+ except KeyError:
+ reply = ["[bot]err: 未找到情景预设:{}".format(params[0])]
+ else:
+ reply = ["[bot]err: 仅管理员可设置默认情景预设"]
+
+ return reply
+
+
+@command(
+ "delhst",
+ "删除指定会话的所有历史记录",
+ "!delhst <会话名称>\n!delhst all",
+ [],
+ True
+)
+def cmd_delhst(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """删除指定会话的所有历史记录"""
+ reply = []
+
+ if len(params) == 0:
+ reply = ["[bot]err:请输入要删除的会话名: group_<群号> 或者 person_, 或使用 !delhst all 删除所有会话的历史记录"]
+ else:
+ if params[0] == "all":
+ pkg.utils.context.get_database_manager().delete_all_session_history()
+ reply = ["[bot]已删除所有会话的历史记录"]
+ else:
+ if pkg.utils.context.get_database_manager().delete_all_history(params[0]):
+ reply = ["[bot]已删除会话 {} 的所有历史记录".format(params[0])]
+ else:
+ reply = ["[bot]未找到会话 {} 的历史记录".format(params[0])]
+
+ return reply
diff --git a/pkg/qqbot/cmds/system.py b/pkg/qqbot/cmds/system.py
new file mode 100644
index 0000000..1d6c671
--- /dev/null
+++ b/pkg/qqbot/cmds/system.py
@@ -0,0 +1,216 @@
+from pkg.qqbot.cmds.model import command
+import pkg.utils.context
+import pkg.utils.updater
+import pkg.utils.credit as credit
+import config
+
+import logging
+import os
+import threading
+import traceback
+import json
+
+@command(
+ "help",
+ "获取帮助信息",
+ "!help",
+ [],
+ False
+)
+def cmd_help(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """获取帮助信息"""
+ return ["[bot]" + config.help_message]
+
+
+@command(
+ "usage",
+ "获取使用情况",
+ "!usage",
+ [],
+ False
+)
+def cmd_usage(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """获取使用情况"""
+ reply = []
+
+ reply_str = "[bot]各api-key使用情况:\n\n"
+
+ api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
+ for key_name in api_keys:
+ text_length = pkg.utils.context.get_openai_manager().audit_mgr \
+ .get_text_length_of_key(api_keys[key_name])
+ image_count = pkg.utils.context.get_openai_manager().audit_mgr \
+ .get_image_count_of_key(api_keys[key_name])
+ reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
+ int(image_count))
+ # 获取此key的额度
+ try:
+ http_proxy = config.openai_config["http_proxy"] if "http_proxy" in config.openai_config else None
+ credit_data = credit.fetch_credit_data(api_keys[key_name], http_proxy)
+ reply_str += " - 使用额度:{:.2f}/{:.2f}\n".format(credit_data['total_used'],credit_data['total_granted'])
+ except Exception as e:
+ logging.warning("获取额度失败:{}".format(e))
+
+ reply = [reply_str]
+ return reply
+
+
+@command(
+ "version",
+ "查看版本信息",
+ "!version",
+ [],
+ False
+)
+def cmd_version(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """查看版本信息"""
+ reply = []
+
+ reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
+ try:
+ if pkg.utils.updater.is_new_version_available():
+ reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
+ except:
+ pass
+
+ reply = [reply_str]
+
+ return reply
+
+
+@command(
+ "reload",
+ "执行热重载",
+ "!reload",
+ [],
+ True
+)
+def cmd_reload(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """执行热重载"""
+ import pkg.utils.reloader
+ def reload_task():
+ pkg.utils.reloader.reload_all()
+
+ threading.Thread(target=reload_task, daemon=True).start()
+
+
+@command(
+ "update",
+ "更新程序",
+ "!update",
+ [],
+ True
+)
+def cmd_update(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """更新程序"""
+ reply = []
+ import pkg.utils.updater
+ import pkg.utils.reloader
+ import pkg.utils.context
+
+ def update_task():
+ try:
+ if pkg.utils.updater.update_all():
+ pkg.utils.reloader.reload_all(notify=False)
+ pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
+ else:
+ pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
+ except Exception as e0:
+ traceback.print_exc()
+ pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
+ return
+
+ threading.Thread(target=update_task, daemon=True).start()
+
+ reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
+
+
+def config_operation(cmd, params):
+ reply = []
+ config = pkg.utils.context.get_config()
+ reply_str = ""
+ if len(params) == 0:
+ reply = ["[bot]err:请输入配置项"]
+ else:
+ cfg_name = params[0]
+ if cfg_name == 'all':
+ reply_str = "[bot]所有配置项:\n\n"
+ for cfg in dir(config):
+ if not cfg.startswith('__') and not cfg == 'logging':
+ # 根据配置项类型进行格式化,如果是字典则转换为json并格式化
+ if isinstance(getattr(config, cfg), str):
+ reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
+ elif isinstance(getattr(config, cfg), dict):
+ # 不进行unicode转义,并格式化
+ reply_str += "{}: {}\n".format(cfg,
+ json.dumps(getattr(config, cfg),
+ ensure_ascii=False, indent=4))
+ else:
+ reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
+ reply = [reply_str]
+ elif cfg_name in dir(config):
+ if len(params) == 1:
+ # 按照配置项类型进行格式化
+ if isinstance(getattr(config, cfg_name), str):
+ reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
+ elif isinstance(getattr(config, cfg_name), dict):
+ reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
+ json.dumps(getattr(config, cfg_name),
+ ensure_ascii=False, indent=4))
+ else:
+ reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
+ reply = [reply_str]
+ else:
+ cfg_value = " ".join(params[1:])
+ # 类型转换,如果是json则转换为字典
+ if cfg_value == 'true':
+ cfg_value = True
+ elif cfg_value == 'false':
+ cfg_value = False
+ elif cfg_value.isdigit():
+ cfg_value = int(cfg_value)
+ elif cfg_value.startswith('{') and cfg_value.endswith('}'):
+ cfg_value = json.loads(cfg_value)
+ else:
+ try:
+ cfg_value = float(cfg_value)
+ except ValueError:
+ pass
+
+ # 检查类型是否匹配
+ if isinstance(getattr(config, cfg_name), type(cfg_value)):
+ setattr(config, cfg_name, cfg_value)
+ pkg.utils.context.set_config(config)
+ reply = ["[bot]配置项{}修改成功".format(cfg_name)]
+ else:
+ reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
+
+ else:
+ reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
+
+ return reply
+
+
+@command(
+ "cfg",
+ "配置文件相关操作",
+ "!cfg all\n!cfg <配置项名称>\n!cfg <配置项名称> <配置项新值>",
+ [],
+ True
+)
+def cmd_cfg(cmd: str, params: list, session_name: str,
+ text_message: str, launcher_type: str, launcher_id: int,
+ sender_id: int, is_admin: bool) -> list:
+ """配置文件相关操作"""
+ reply = config_operation(cmd, params)
+ return reply
diff --git a/pkg/qqbot/command.py b/pkg/qqbot/command.py
index b174d45..dddc43e 100644
--- a/pkg/qqbot/command.py
+++ b/pkg/qqbot/command.py
@@ -4,6 +4,7 @@ import json
import datetime
import os
import threading
+import traceback
import pkg.openai.session
import pkg.openai.manager
@@ -12,151 +13,11 @@ import pkg.utils.updater
import pkg.utils.context
import pkg.qqbot.message
import pkg.utils.credit as credit
+import pkg.qqbot.cmds.model as cmdmodel
from mirai import Image
-def config_operation(cmd, params):
- reply = []
- config = pkg.utils.context.get_config()
- reply_str = ""
- if len(params) == 0:
- reply = ["[bot]err:请输入配置项"]
- else:
- cfg_name = params[0]
- if cfg_name == 'all':
- reply_str = "[bot]所有配置项:\n\n"
- for cfg in dir(config):
- if not cfg.startswith('__') and not cfg == 'logging':
- # 根据配置项类型进行格式化,如果是字典则转换为json并格式化
- if isinstance(getattr(config, cfg), str):
- reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
- elif isinstance(getattr(config, cfg), dict):
- # 不进行unicode转义,并格式化
- reply_str += "{}: {}\n".format(cfg,
- json.dumps(getattr(config, cfg),
- ensure_ascii=False, indent=4))
- else:
- reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
- reply = [reply_str]
- elif cfg_name in dir(config):
- if len(params) == 1:
- # 按照配置项类型进行格式化
- if isinstance(getattr(config, cfg_name), str):
- reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
- elif isinstance(getattr(config, cfg_name), dict):
- reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
- json.dumps(getattr(config, cfg_name),
- ensure_ascii=False, indent=4))
- else:
- reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
- reply = [reply_str]
- else:
- cfg_value = " ".join(params[1:])
- # 类型转换,如果是json则转换为字典
- if cfg_value == 'true':
- cfg_value = True
- elif cfg_value == 'false':
- cfg_value = False
- elif cfg_value.isdigit():
- cfg_value = int(cfg_value)
- elif cfg_value.startswith('{') and cfg_value.endswith('}'):
- cfg_value = json.loads(cfg_value)
- else:
- try:
- cfg_value = float(cfg_value)
- except ValueError:
- pass
-
- # 检查类型是否匹配
- if isinstance(getattr(config, cfg_name), type(cfg_value)):
- setattr(config, cfg_name, cfg_value)
- pkg.utils.context.set_config(config)
- reply = ["[bot]配置项{}修改成功".format(cfg_name)]
- else:
- reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
-
- else:
- reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
-
- return reply
-
-
-def plugin_operation(cmd, params, is_admin):
- reply = []
-
- import pkg.plugin.host as plugin_host
- import pkg.utils.updater as updater
-
- plugin_list = plugin_host.__plugins__
-
- if len(params) == 0:
- reply_str = "[bot]所有插件({}):\n".format(len(plugin_host.__plugins__))
- idx = 0
- for key in plugin_host.iter_plugins_name():
- plugin = plugin_list[key]
- reply_str += "\n#{} {} {}\n{}\nv{}\n作者: {}\n"\
- .format((idx+1), plugin['name'],
- "[已禁用]" if not plugin['enabled'] else "",
- plugin['description'],
- plugin['version'], plugin['author'])
-
- if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
- remote_url = updater.get_remote_url("/".join(plugin['path'].split('/')[:-1]))
- if remote_url != "https://github.com/RockChinQ/QChatGPT" and remote_url != "https://gitee.com/RockChin/QChatGPT":
- reply_str += "源码: "+remote_url+"\n"
-
- idx += 1
-
- reply = [reply_str]
- elif params[0] == 'update':
- # 更新所有插件
- if is_admin:
- def closure():
- import pkg.utils.context
- updated = []
- for key in plugin_list:
- plugin = plugin_list[key]
- if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
- success = updater.pull_latest("/".join(plugin['path'].split('/')[:-1]))
- if success:
- updated.append(plugin['name'])
-
- # 检查是否有requirements.txt
- pkg.utils.context.get_qqbot_manager().notify_admin("正在安装依赖...")
- for key in plugin_list:
- plugin = plugin_list[key]
- if os.path.exists("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt"):
- logging.info("{}检测到requirements.txt,安装依赖".format(plugin['name']))
- import pkg.utils.pkgmgr
- pkg.utils.pkgmgr.install_requirements("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt")
-
- import main
- main.reset_logging()
-
- pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}".format(", ".join(updated)))
-
- threading.Thread(target=closure).start()
- reply = ["[bot]正在更新所有插件,请勿重复发起..."]
- else:
- reply = ["[bot]err:权限不足"]
- elif params[0].startswith("http"):
- if is_admin:
-
- def closure():
- try:
- plugin_host.install_plugin(params[0])
- pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 指令重载插件")
- except Exception as e:
- logging.error("插件安装失败:{}".format(e))
- pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
-
- threading.Thread(target=closure, args=()).start()
- reply = ["[bot]正在安装插件..."]
- else:
- reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
- return reply
-
def process_command(session_name: str, text_message: str, mgr, config,
launcher_type: str, launcher_id: int, sender_id: int, is_admin: bool) -> list:
@@ -169,188 +30,30 @@ def process_command(session_name: str, text_message: str, mgr, config,
cmd = text_message[1:].strip().split(' ')[0]
params = text_message[1:].strip().split(' ')[1:]
- if cmd == 'help':
- reply = ["[bot]" + config.help_message]
- elif cmd == 'reset':
- if len(params) == 0:
- pkg.openai.session.get_session(session_name).reset(explicit=True)
- reply = ["[bot]会话已重置"]
- else:
- pkg.openai.session.get_session(session_name).reset(explicit=True, use_prompt=params[0])
- reply = ["[bot]会话已重置,使用场景预设:{}".format(params[0])]
- elif cmd == 'last':
- result = pkg.openai.session.get_session(session_name).last_session()
- if result is None:
- reply = ["[bot]没有前一次的对话"]
- else:
- datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
- '%Y-%m-%d %H:%M:%S')
- reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(datetime_str)]
- elif cmd == 'next':
- result = pkg.openai.session.get_session(session_name).next_session()
- if result is None:
- reply = ["[bot]没有后一次的对话"]
- else:
- datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
- '%Y-%m-%d %H:%M:%S')
- reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(datetime_str)]
- elif cmd == 'prompt':
- msgs = ""
- session:list = pkg.openai.session.get_session(session_name).prompt
- for msg in session:
- if len(params) != 0 and params[0] in ['-all', '-a']:
- msgs = msgs + "{}: {}\n\n".format(msg['role'], msg['content'])
- elif len(msg['content']) > 30:
- msgs = msgs + "[{}]: {}...\n\n".format(msg['role'], msg['content'][:30])
- else:
- msgs = msgs + "[{}]: {}\n\n".format(msg['role'], msg['content'])
- reply = ["[bot]当前对话所有内容:\n{}".format(msgs)]
- elif cmd == 'list':
- pkg.openai.session.get_session(session_name).persistence()
- page = 0
- if len(params) > 0:
- try:
- page = int(params[0])
- except ValueError:
- pass
+ # 把!~开头的转换成!cfg
+ if cmd.startswith('~'):
+ params = [cmd[1:]] + params
+ cmd = 'cfg'
- results = pkg.openai.session.get_session(session_name).list_history(page=page)
- if len(results) == 0:
- reply = ["[bot]第{}页没有历史会话".format(page)]
- else:
- reply_str = "[bot]历史会话 第{}页:\n".format(page)
- current = -1
- for i in range(len(results)):
- # 时间(使用create_timestamp转换) 序号 部分内容
- datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
- msg = ""
- try:
- msg = json.loads(results[i]['prompt'])
- except json.decoder.JSONDecodeError:
- msg = pkg.openai.session.reset_session_prompt(session_name, results[i]['prompt'])
- # 持久化
- pkg.openai.session.get_session(session_name).persistence()
- if len(msg) >= 2:
- reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
- datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
- msg[1]['content'])
- else:
- reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
- datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
- "无内容")
- if results[i]['create_timestamp'] == pkg.openai.session.get_session(
- session_name).create_timestamp:
- current = i + page * 10
-
- reply_str += "\n以上信息倒序排列"
- if current != -1:
- reply_str += ",当前会话是 #{}\n".format(current)
- else:
- reply_str += ",当前处于全新会话或不在此页"
-
- reply = [reply_str]
- elif cmd == 'resend':
- session = pkg.openai.session.get_session(session_name)
- to_send = session.undo()
-
- reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config,
- launcher_type, launcher_id, sender_id)
- elif cmd == 'usage':
- reply_str = "[bot]各api-key使用情况:\n\n"
-
- api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
- for key_name in api_keys:
- text_length = pkg.utils.context.get_openai_manager().audit_mgr \
- .get_text_length_of_key(api_keys[key_name])
- image_count = pkg.utils.context.get_openai_manager().audit_mgr \
- .get_image_count_of_key(api_keys[key_name])
- reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
- int(image_count))
- # 获取此key的额度
- try:
- credit_data = credit.fetch_credit_data(api_keys[key_name])
- reply_str += " - 使用额度:{:.2f}/{:.2f}\n".format(credit_data['total_used'],credit_data['total_granted'])
- except Exception as e:
- logging.warning("获取额度失败:{}".format(e))
-
- reply = [reply_str]
- elif cmd == 'draw':
- if len(params) == 0:
- reply = ["[bot]err:请输入图片描述文字"]
- else:
- session = pkg.openai.session.get_session(session_name)
-
- res = session.draw_image(" ".join(params))
-
- logging.debug("draw_image result:{}".format(res))
- reply = [Image(url=res['data'][0]['url'])]
- if not (hasattr(config, 'include_image_description')
- and not config.include_image_description):
- reply.append(" ".join(params))
- elif cmd == 'version':
- reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
- try:
- if pkg.utils.updater.is_new_version_available():
- reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
- except:
- pass
-
- reply = [reply_str]
-
- elif cmd == 'plugin':
- reply = plugin_operation(cmd, params, is_admin)
-
- elif cmd == 'default':
- if len(params) == 0:
- # 输出目前所有情景预设
- import pkg.openai.dprompt as dprompt
- reply_str = "[bot]当前所有情景预设:\n\n"
- for key,value in dprompt.get_prompt_dict().items():
- reply_str += " - {}: {}\n".format(key,value)
-
- reply_str += "\n当前默认情景预设:{}\n".format(dprompt.get_current())
- reply_str += "请使用!default <情景预设>来设置默认情景预设"
- reply = [reply_str]
- elif len(params) >0 and is_admin:
- # 设置默认情景
- import pkg.openai.dprompt as dprompt
- try:
- dprompt.set_current(params[0])
- reply = ["[bot]已设置默认情景预设为:{}".format(dprompt.get_current())]
- except KeyError:
- reply = ["[bot]err: 未找到情景预设:{}".format(params[0])]
- else:
- reply = ["[bot]err: 仅管理员可设置默认情景预设"]
- elif cmd == 'reload' and is_admin:
- def reload_task():
- pkg.utils.reloader.reload_all()
-
- threading.Thread(target=reload_task, daemon=True).start()
- elif cmd == 'update' and is_admin:
- def update_task():
- try:
- if pkg.utils.updater.update_all():
- pkg.utils.reloader.reload_all(notify=False)
- pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
- else:
- pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
- except Exception as e0:
- pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
- return
-
- threading.Thread(target=update_task, daemon=True).start()
-
- reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
- elif cmd == 'cfg' and is_admin:
- reply = config_operation(cmd, params)
+ # 选择指令处理函数
+ cmd_obj = cmdmodel.search(cmd)
+ if cmd_obj is not None and (cmd_obj['admin_only'] is False or is_admin):
+ cmd_func = cmd_obj['func']
+ reply = cmd_func(
+ cmd=cmd,
+ params=params,
+ session_name=session_name,
+ text_message=text_message,
+ launcher_type=launcher_type,
+ launcher_id=launcher_id,
+ sender_id=sender_id,
+ is_admin=is_admin,
+ )
else:
- if cmd.startswith("~") and is_admin:
- config_item = cmd[1:]
- params = [config_item] + params
- reply = config_operation("cfg", params)
- else:
- reply = ["[bot]err:未知的指令或权限不足: " + cmd]
+ reply = ["[bot]err:未知的指令或权限不足: " + cmd]
+
+ return reply
except Exception as e:
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
logging.exception(e)
diff --git a/pkg/qqbot/message.py b/pkg/qqbot/message.py
index e6106df..9d124fc 100644
--- a/pkg/qqbot/message.py
+++ b/pkg/qqbot/message.py
@@ -1,6 +1,5 @@
# 普通消息处理模块
import logging
-import time
import openai
import pkg.utils.context
import pkg.openai.session
@@ -64,7 +63,7 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
reply = event.get_return_value("reply")
if not event.is_prevented_default():
- reply = blob.check_text(prefix + text)
+ reply = [prefix + text]
except openai.error.APIConnectionError as e:
err_msg = str(e)
if err_msg.__contains__('Error communicating with OpenAI'):
@@ -117,8 +116,7 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
reply = handle_exception("{}会话调用API失败:{}".format(session_name, e),
"[bot]err:RateLimitError,请重试或联系作者,或等待修复")
except openai.error.InvalidRequestError as e:
- reply = handle_exception("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或"
- "completion_api_params中的max_tokens参数数值过大导致的,请尝试将其降低".format(
+ reply = handle_exception("{}API调用参数错误:{}\n".format(
session_name, e), "[bot]err:API调用参数错误,请联系管理员,或等待修复")
except openai.error.ServiceUnavailableError as e:
reply = handle_exception("{}API调用服务不可用:{}".format(session_name, e), "[bot]err:API调用服务不可用,请重试或联系管理员,或等待修复")
diff --git a/pkg/qqbot/process.py b/pkg/qqbot/process.py
index 3ca275a..4dda752 100644
--- a/pkg/qqbot/process.py
+++ b/pkg/qqbot/process.py
@@ -26,6 +26,7 @@ import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
import pkg.qqbot.ignore as ignore
import pkg.qqbot.banlist as banlist
+import pkg.qqbot.blob as blob
processing = []
@@ -157,6 +158,7 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
reply[0][:min(100, len(reply[0]))] + (
"..." if len(reply[0]) > 100 else "")))
reply = [mgr.reply_filter.process(reply[0])]
+ reply = blob.check_text(reply[0])
else:
logging.info("回复[{}]消息".format(session_name))
diff --git a/pkg/utils/announcement.py b/pkg/utils/announcement.py
new file mode 100644
index 0000000..bec74ba
--- /dev/null
+++ b/pkg/utils/announcement.py
@@ -0,0 +1,47 @@
+import base64
+import os
+
+import requests
+
+import pkg.utils.network as network
+
+
+def read_latest() -> str:
+ resp = requests.get(
+ url="https://api.github.com/repos/RockChinQ/QChatGPT/contents/res/announcement",
+ proxies=network.wrapper_proxies()
+ )
+ obj_json = resp.json()
+ b64_content = obj_json["content"]
+ # 解码
+ content = base64.b64decode(b64_content).decode("utf-8")
+ return content
+
+
+def read_saved() -> str:
+ # 已保存的在res/announcement_saved
+ # 检查是否存在
+ if not os.path.exists("res/announcement_saved"):
+ with open("res/announcement_saved", "w") as f:
+ f.write("")
+
+ with open("res/announcement_saved", "r") as f:
+ content = f.read()
+
+ return content
+
+
+def write_saved(content: str):
+ # 已保存的在res/announcement_saved
+ with open("res/announcement_saved", "w") as f:
+ f.write(content)
+
+
+def fetch_new() -> str:
+ latest = read_latest()
+ saved = read_saved()
+ if latest.replace(saved, "").strip() == "":
+ return ""
+ else:
+ write_saved(latest)
+ return latest.replace(saved, "").strip()
diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py
index 08712b4..251bbe3 100644
--- a/pkg/utils/constants.py
+++ b/pkg/utils/constants.py
@@ -2,4 +2,4 @@ alipay_qr_b64 = """/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAA
wechat_qr_b64 = """iVBORw0KGgoAAAANSUhEUgAAASwAAAFSCAYAAABIVeLEAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAAEZ0FNQQAAsY8L/GEFAAAACXBIWXMAAA7EAAAOxAGVKw4bAAB5iUlEQVR4Xu2dB2Ac1dHH5/qdumTJvVdsMJjimI7BQAi99wChl5jQe/sIhN4CgdAChB56gNB7sTE2uIAx2Ma4SrZ61/Vv/rO70up0p2LLts6eH6xv9+3u273T7f9m5s17zxFnSFEUJQ1wmq+Koig9HhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUsRVHSBhUspduorq6mc889l958802zpGNef/11uuCCC6i8vNwsUZTU6MzPSrdx9NFH00svvSTr0WiUnM6Ofw8dDoe8HnPMMfTCCy/IuqKkQi0sJSXLli2jSy65hIqLi82S9olEIuaacW5HrFq1ylwj6uzvJs656qqraPny5WaJsjmhgqWkZL/99qO77rqLJk+ebJa0T//+/c016pSLV1paaq4RFRUVmWvtM378eLrlllvk3pTNDxUsJSXbbbedvE6aNEleO2K33XYz14jq6urMtdRUVVWZa0Tbb7+9uZaalStXUkVFhax7vV55VTYvNIaltAusoM5aP2DPPfek3r1704svvmiWtM+BBx5IsViM3nrrrQ5jXk8//TSddNJJsn7llVfS3/72N1lXNh9UsDYzlixZQsOGDTO30guIFUQLzJo1q9kC7IilS5fSkCFDzC0lnVGXcDPizjvvpOHDh9PQoUPNkvTC3orYWbEaMGCAvN/77rvPLFHSGRWszQhYVwAWx/oEsabuBq2C4XBY1u2xso6wWiKt966kNypYmxGwsO6//36aN2+eWdL9HHLIITRw4EDJq+pOamtrzTWiE044wVzrGLzXf/zjH3TrrbeaJUo6ozEspVvJzs6WFkKPx0OhUMgs7R4QaK+pqREBUjZPVLCUbgV5Uj/88IOsI5HU5XLJejK+++47mj59Op1zzjnNGe+K0h7qEirdij15dM2aNeZacpB7dd5559Ff//pXs0RR2kcFK80pKysT6wStYVZQemOC/oQgLy+P+vXrJ+upyMjIkNfBgwfL68YG9wyLsLNdkZQNjwpWmvPFF1/IK1rDFi5cKOud5eWXX6apU6e26gO4rpx22mliWVVWVpolqamvr5fE1FNOOcUsWXcQN7vooouaO2F3ll9//VVGm0AS65dffmmWKj0NjWFtAlx88cViYeFB7SwIXufm5sr6qaeeSo8//rispzvHHXdcc75WV7/ayJyHiN58881midLTUMHajAkEAtTU1CTrsC5ycnJkPRW/+93v5IGeOXOmnLshQDrDTjvtJO7jjBkzzNLkoHUSrZQW+tXe9FCXcBMDD/UHH3xgbrXPvffea64RXXHFFeZacr755hv69ttvaf78+fT555+bpck5//zzxeL78ccfzZK154EHHpB6cO0PP/zQLE0OBg+0eOihh8y19nn//fc7FEKlBwELS9k0WLZsGUwKWVi0zNL2sY7HEolEzNLkHHvssfGDDjooHg6HzZK2xGKx5vrOPvtss3TtGT9+fHN9JSUlZmlyJk2aJMc5nU6zpH2uu+665rpXrlxplio9GRWsTYiqqqrmB3Du3Llmaftcc801zefgAe4O2MKKjxo1qsN7wP12hHVvffr0MUtSA9E555xz4rNmzTJLUrNgwYLmurGw62nuUXoyKlibGNXV1R1aIonYH9wNxa233irXO/roo82Strz77rvN9/WXv/zFLO0eIIBW3TfccINZqvR0NOieRqDJ/dFHH5UMcazn5+fTEUcc0ekB9lJx5plnSr0AMSMkc6YCOUoPP/ywnGNPEu0qW221VXOMK9VX8OSTT6Z///vfsv7OO+902yijSOXA+wRjx46VuJwdDF3z/PPPS0sq8rK22WYbYvfW3KtsVCBYSs/nyiuvbLYIEpdhw4bFf/31V/PIFp544on4vvvuG//pp5/MkuRUVlY215WdnW2WJodFSo7bfvvtzZK1o1evXlJPe64ei0nzfTU0NJilbZk8ebIcM3v2bLMkNdOnT2+uE0tZWZm5Jx5fuHBhfMiQIa3225fucpmVtUcFKw2A6FgPjdfrje+6665xtjYkTmR/oL7//nvzDAOrHOd3BALq1vEQulSwRSfHnH766WZJ1wkGg83XGjdunFnaFraw5JjDDjvMLGkLhMyq68YbbzRLU+PxeJqP//vf/26W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"""
-semantic_version = "v2.1.4"
+semantic_version = "v2.2.5"
diff --git a/pkg/utils/credit.py b/pkg/utils/credit.py
index f263ed9..e09c74e 100644
--- a/pkg/utils/credit.py
+++ b/pkg/utils/credit.py
@@ -1,13 +1,19 @@
# OpenAI账号免费额度剩余查询
import requests
-
-def fetch_credit_data(api_key: str) -> dict:
+def fetch_credit_data(api_key: str, http_proxy: str) -> dict:
"""OpenAI账号免费额度剩余查询"""
+ proxies = {
+ "http":http_proxy,
+ "https":http_proxy
+ } if http_proxy is not None else None
+
resp = requests.get(
url="https://api.openai.com/dashboard/billing/credit_grants",
headers={
"Authorization": "Bearer {}".format(api_key),
- }
+ },
+ proxies=proxies
)
+
return resp.json()
\ No newline at end of file
diff --git a/pkg/utils/network.py b/pkg/utils/network.py
new file mode 100644
index 0000000..7295065
--- /dev/null
+++ b/pkg/utils/network.py
@@ -0,0 +1,9 @@
+
+def wrapper_proxies() -> dict:
+ """获取代理"""
+ import config
+
+ return {
+ "http": config.openai_config['proxy'],
+ "https": config.openai_config['proxy']
+ } if 'proxy' in config.openai_config and (config.openai_config['proxy'] is not None) else None
diff --git a/pkg/utils/updater.py b/pkg/utils/updater.py
index d2d8951..57f81a9 100644
--- a/pkg/utils/updater.py
+++ b/pkg/utils/updater.py
@@ -6,6 +6,7 @@ import requests
import json
import pkg.utils.constants
+import pkg.utils.network as network
def check_dulwich_closure():
@@ -36,7 +37,8 @@ def pull_latest(repo_path: str) -> bool:
def get_release_list() -> list:
"""获取发行列表"""
rls_list_resp = requests.get(
- url="https://api.github.com/repos/RockChinQ/QChatGPT/releases"
+ url="https://api.github.com/repos/RockChinQ/QChatGPT/releases",
+ proxies=network.wrapper_proxies()
)
rls_list = rls_list_resp.json()
@@ -83,7 +85,10 @@ def update_all(cli: bool = False) -> bool:
else:
print("开始下载最新版本: {}".format(latest_rls['zipball_url']))
zip_url = latest_rls['zipball_url']
- zip_resp = requests.get(url=zip_url)
+ zip_resp = requests.get(
+ url=zip_url,
+ proxies=network.wrapper_proxies()
+ )
zip_data = zip_resp.content
# 检查temp/updater目录
@@ -126,6 +131,15 @@ def update_all(cli: bool = False) -> bool:
dst = src.replace(source_root, ".")
if os.path.exists(dst):
os.remove(dst)
+
+ # 检查目标文件夹是否存在
+ if not os.path.exists(os.path.dirname(dst)):
+ os.makedirs(os.path.dirname(dst))
+ # 检查目标文件是否存在
+ if not os.path.exists(dst):
+ # 创建目标文件
+ open(dst, "w").close()
+
shutil.copy(src, dst)
# 把current_tag写入文件
diff --git a/requirements.txt b/requirements.txt
index a61c1a0..74f52a0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
requests~=2.28.1
-openai~=0.27.0
+openai~=0.27.2
dulwich~=0.21.3
colorlog~=6.6.0
yiri-mirai~=0.2.6.1
diff --git a/res/announcement b/res/announcement
new file mode 100644
index 0000000..d3f5a12
--- /dev/null
+++ b/res/announcement
@@ -0,0 +1 @@
+
diff --git a/scenario/default-template.json b/scenario/default-template.json
new file mode 100644
index 0000000..d9b7267
--- /dev/null
+++ b/scenario/default-template.json
@@ -0,0 +1,12 @@
+{
+ "prompt": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant. 如果我需要帮助,你要说“输入!help获得帮助”"
+ },
+ {
+ "role": "assistant",
+ "content": "好的,我是一个能干的AI助手。 如果你需要帮助,我会说“输入!help获得帮助”"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/token_test/__init__.py b/tests/token_test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/token_test/token_count.py b/tests/token_test/token_count.py
new file mode 100644
index 0000000..e69de29