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