Compare commits

..

110 Commits

Author SHA1 Message Date
Rock Chin
a724bfe155 Release v2.2.2 2023-03-15 20:39:10 +08:00
Rock Chin
179a372bfe feat: 更改到process.py处理长消息 2023-03-15 20:33:44 +08:00
Rock Chin
651d765ab0 doc: 添加New Bing说明 2023-03-15 17:33:31 +08:00
Rock Chin
7ddc853f63 chore: 忽略保存的公告 2023-03-15 15:50:14 +08:00
Rock Chin
1bd1bfc725 chore: 删除测试公告 2023-03-15 15:47:24 +08:00
Rock Chin
f6ec0fda7a Merge pull request #280 from RockChinQ/announcement
[Feat] 添加公告输出功能
2023-03-15 15:46:58 +08:00
Rock Chin
7be368ae8c feat: 添加公告功能 2023-03-15 15:43:36 +08:00
Rock Chin
f67db2617b debug: 测试公告内容1 2023-03-15 15:37:07 +08:00
Rock Chin
ed5bf8100f chore: 添加公告内容 2023-03-15 15:22:19 +08:00
Rock Chin
0ef8a1c9ae chore: 为new bing忽略cookies.json 2023-03-15 11:24:45 +08:00
Rock Chin
32460cbf78 doc: 添加GPT-4公告 2023-03-15 11:04:10 +08:00
Rock Chin
6f6c9c222c doc: 添加网页版GPT-4说明 2023-03-15 10:57:29 +08:00
Rock Chin
438d0ed1ea Merge pull request #277 from zyckk4/dev
chore: 去除多余import
2023-03-14 13:11:47 +08:00
zyckk4
3ef1c71cad chore: 去除多余import 2023-03-14 13:03:50 +08:00
Rock Chin
aaadf6b8ba doc: 部署方式依赖项指令 2023-03-14 10:57:02 +08:00
Rock Chin
6af614f319 doc: 整理致谢列表 2023-03-14 10:54:46 +08:00
Rock Chin
c75dbd67df doc: 整理致谢列表 2023-03-14 10:53:32 +08:00
Rock Chin
dc3d186e2a Merge pull request #274 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.2
chore(deps): update openai requirement from ~=0.27.0 to ~=0.27.2
2023-03-13 17:48:10 +08:00
dependabot[bot]
44550feddd chore(deps): update openai requirement from ~=0.27.0 to ~=0.27.2
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.0...v0.27.2)

---
updated-dependencies:
- dependency-name: openai
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 09:05:46 +00:00
Rock Chin
a0810d5f63 Merge pull request #271 from RockChinQ/config-covering
feat: 支持json格式的配置文件 (#265)
2023-03-13 11:05:11 +08:00
Rock Chin
cfc97fb22d feat: 支持json格式的配置文件 (#265) 2023-03-13 10:58:15 +08:00
Rock Chin
d67dbe8062 doc: 添加JSON格式情景预设的说明 2023-03-13 10:31:21 +08:00
Rock Chin
e89035e11c Release v2.2.1 2023-03-12 22:43:39 +08:00
Rock Chin
2ea711e629 fix: 更新包中包含新文件时更新失败 2023-03-12 22:43:02 +08:00
Rock Chin
a716f071be Release v2.2.0 2023-03-12 20:48:15 +08:00
Rock Chin
3450a91824 Merge pull request #262 from chordfish-k/json_scenario
[Feat] 情景预设(人格)完善
2023-03-12 20:40:20 +08:00
Rock Chin
d2c2b457e5 fix: !list指令显示的是机器人第一次回复 (#264) 2023-03-12 20:31:28 +08:00
Rock Chin
9cd7e49804 feat: 分离储存会话情景预设和对话内容 2023-03-11 23:44:22 +08:00
Rock Chin
e9155e836f feat: 允许通过前缀指定使用的JSON情景 2023-03-10 23:49:41 +08:00
Rock Chin
ed248539c7 doc: 标记二群已满 2023-03-10 23:33:54 +08:00
Rock Chin
54cc75506f feat: 使用模板储存默认的json格式的情景预设 2023-03-10 23:26:36 +08:00
Rock Chin
4269c7927e chore: typo 2023-03-10 23:14:32 +08:00
Rock Chin
064ac7f603 feat: 添加窗口处于暂停模式的提示 2023-03-10 22:50:07 +08:00
Rock Chin
48ccf15273 Merge pull request #263 from RockChinQ/history-deletion
[Feat] 支持删除指定当前会话的指定或全部历史记录 (#239)
2023-03-10 22:40:19 +08:00
Rock Chin
b920ced6d4 feat: !delhst 指令支持管理员删除会话历史记录 2023-03-10 21:20:19 +08:00
Rock Chin
69610a674c perf: 更改help中指令信息帮助 2023-03-10 21:11:55 +08:00
Rock Chin
1828e34190 feat: 支持删除指定当前会话的指定或全部历史记录 (#239) 2023-03-10 21:04:20 +08:00
chordfish
d53f4e3917 adjust:修改,去除neko.json,以及一些占位的变量等 2023-03-10 19:37:29 +08:00
chordfish
01706d5b4e Delete mesugaki.json 2023-03-10 16:17:47 +08:00
chordfish
8916b8a450 Update manager.py 2023-03-10 16:17:07 +08:00
chordfish
ed33af5638 Update README.md 2023-03-10 14:10:54 +08:00
chordfish
c94a9e1ae6 bug:修复上次更新后不响应的问题 2023-03-10 13:55:56 +08:00
chordfish
e2e93afd06 bug:修复上次更新后不响应的问题 2023-03-10 13:03:25 +08:00
chordfish
a810158d5b bug:修复上次更新后不响应的问题 2023-03-10 12:43:07 +08:00
chordfish
5a5ebb95fc bug:修复上次更新后不响应的问题 2023-03-10 12:35:58 +08:00
chordfish
61dd9e29c0 Merge branch 'master' into json_scenario 2023-03-10 10:20:18 +08:00
chordfish
ac65d81ba1 adjust:整理代码,仅添加json方式的prompt读取 2023-03-10 10:13:40 +08:00
chordfish
7288d3cb15 删除一部分注释和调试信息 2023-03-09 21:20:59 +08:00
chordfish
7477c7c67f 删除一部分注释和调试内容 2023-03-09 21:16:15 +08:00
chordfish
453952859e Merge branch 'full_scenario' 2023-03-09 21:08:47 +08:00
chordfish
85d46089e3 已按要求修改 2023-03-09 19:53:31 +08:00
chordfish
3b55f706de 修正防人格否定的一个Bug 2023-03-09 19:22:37 +08:00
chordfish
f448276423 Merge commit '830ee704da0903a8922dc757381cdf6fd68870a3' 2023-03-09 18:34:03 +08:00
chordfish
830ee704da Bug修复 2023-03-09 18:32:39 +08:00
chordfish
393369e446 Merge branch 'master' of https://github.com/chordfish-k/QChatGPT 2023-03-09 18:29:37 +08:00
chordfish
2cc6a09905 Bug修复 2023-03-09 18:29:31 +08:00
chordfish
d7d9d88e16 适配线程版本 2023-03-09 17:56:57 +08:00
chordfish
357d6aaf75 更新配置文件 2023-03-09 15:52:18 +08:00
chordfish
8059c422e3 Update README.md 2023-03-09 14:48:21 +08:00
chordfish
b336e1334d Update README.md 2023-03-09 14:47:39 +08:00
chordfish
12a0942ddb 初步追加通过json导入messages数组的方式进行情景预设 2023-03-09 14:44:33 +08:00
Rock Chin
7e5a77f77e doc: 添加致谢https://github.com/qq255204159 2023-03-08 16:16:41 +08:00
Rock Chin
2933d4843f Release v2.1.4 2023-03-07 08:50:43 +08:00
Rock Chin
c5de978098 Merge pull request #236 from RockChinQ/fix-234
[Fix] !reload 重新加载以后首次对话报错
2023-03-07 08:47:53 +08:00
Rock Chin
8b9cfab072 doc(main.py): 优化注释 2023-03-07 08:46:20 +08:00
Rock Chin
ea5f3c222f fix: 修改主线程main流程以初步修复 2023-03-06 20:53:40 +08:00
Rock Chin
36bcbca15b Merge pull request #233 from RockChinQ/respond-rule
[Feat] 支持设置不响应群内at消息及随机响应
2023-03-06 17:55:19 +08:00
Rock Chin
2b2060e71b feat: 支持设置不响应群内at消息;支持设置随机响应概率 2023-03-06 17:50:34 +08:00
Rock Chin
451688f2df Merge pull request #232 from RockChinQ/sensitive-mask
[Feat] 支持更换敏感词的掩盖字符
2023-03-06 15:27:15 +08:00
Rock Chin
d993852de7 feat: 支持将敏感词替换成整个字符串 2023-03-06 15:26:06 +08:00
Rock Chin
9d73770a4e feat: 支持更换敏感词的掩盖字符 2023-03-06 15:07:10 +08:00
Rock Chin
2541acf9d2 fix: 赞赏码base64值错误 2023-03-06 14:16:25 +08:00
Rock Chin
a1bfbad24e Release v2.1.3 2023-03-06 12:41:35 +08:00
Rock Chin
8af4918048 Merge pull request #230 from LINSTCL/config_integrity_check
添加配置文件完整性校验
2023-03-06 12:35:59 +08:00
Rock Chin
49f4ab0ec8 perf: 完整性检查忽略__开头的属性 2023-03-06 12:34:08 +08:00
LINSTCL
85c623fb0f 修改提示逻辑 2023-03-06 11:27:16 +08:00
Rock Chin
9e28298250 perf: 完善未启动情况下的自动更新 2023-03-06 11:18:31 +08:00
Rock Chin
7a04ef0985 feat: 未启动状态下的自动更新 (#223) 2023-03-06 11:04:25 +08:00
LINSTCL
83005e9ba9 添加配置文件完整性校验 2023-03-06 09:40:33 +08:00
Rock Chin
f0c78f0529 Merge pull request #222 from LINSTCL/threadpool-optimization
使用线程池控制线程数量,防止高并发崩溃
2023-03-06 08:51:47 +08:00
Rock Chin
3f638adcf9 perf(qqbot/manager.py): 优化控制台日志显示 2023-03-06 08:50:28 +08:00
Rock Chin
d9405d8d5d fix: main.py的字段版本兼容性问题 2023-03-06 08:48:50 +08:00
Rock Chin
606713a418 Merge pull request #228 from yichuxue/patch-1
启动时,更新openai和pillow库超时问题
2023-03-06 08:44:29 +08:00
Rock Chin
52102f0d0a feat(deps): trusted-host参数 2023-03-06 08:43:51 +08:00
Rock Chin
61c29829ed Release v2.1.2 2023-03-06 08:35:04 +08:00
依初雪
df30931aad 启动openai和pillow库超时问题
主要改动如下:
1、在ensure_dependencies函数更更新包时,出现超时的情况,指定更新源 https://pypi.douban.com/simple/
2023-03-06 00:32:46 +08:00
Rock Chin
5afcc03e8b fix: 错误的!version指令处理逻辑 2023-03-05 20:07:08 +08:00
Rock Chin
fbeb4673f4 Merge pull request #226 from RockChinQ/text2img-perf
[Feat] 不再自带字体文件
2023-03-05 19:59:16 +08:00
Rock Chin
4aba319560 fix: 错误的加载过程 2023-03-05 19:57:39 +08:00
Rock Chin
74f79e002c perf: 优化字体加载过程 2023-03-05 19:54:51 +08:00
Rock Chin
2668ef2b3f feat: 不再自带字体文件 2023-03-05 19:36:09 +08:00
Rock Chin
74c018e271 Merge pull request #225 from RockChinQ/fix-switch-exce
[Fix] 修复插件开关问题
2023-03-05 17:36:03 +08:00
Rock Chin
64776fd601 doc: OpenAI注册教程链接 2023-03-05 16:47:42 +08:00
LINSTCL
59877bf71d 添加日志输出 2023-03-05 16:47:07 +08:00
LINSTCL
d2800ac58b 使用线程池控制线程数量,防止高并发崩溃 2023-03-05 16:41:12 +08:00
Rock Chin
ffef944119 fix: 热重载后插件开关状态被重置 (#177) 2023-03-05 16:04:45 +08:00
Rock Chin
651b291ef6 doc: 添加部分注释 2023-03-05 15:39:13 +08:00
Rock Chin
e4b581f197 doc: 致谢添加贡献者 2023-03-05 14:37:14 +08:00
Rock Chin
4f3939e2d9 Merge pull request #219 from LINSTCL/modelmgr_optimization
优化模型接口底层的异常处理
2023-03-05 14:18:24 +08:00
LINSTCL
1048ca612d 补充错误情况 2023-03-05 14:06:07 +08:00
LINSTCL
b1a2d21ee9 优化异常处理 2023-03-05 13:52:43 +08:00
Rock Chin
dd4e8bdc8b perf: 优化版本识别逻辑 2023-03-05 12:26:51 +08:00
Rock Chin
e28c9bae0c feat: 修改上报功能识别版本的逻辑 2023-03-05 12:21:28 +08:00
Rock Chin
5c10f520fb Merge pull request #215 from RockChinQ/semantic-versions
[Feature] 使用语义化版本进行更新
2023-03-05 12:17:43 +08:00
Rock Chin
f8abe90674 perf: 完善更新检查功能 2023-03-05 12:09:44 +08:00
Rock Chin
964ad42cb4 perf: 完善更新提示 2023-03-05 12:02:59 +08:00
Rock Chin
424b970469 feat: 将依赖检查更改到main流程中 2023-03-05 11:58:18 +08:00
Rock Chin
792366e221 feat: 支持基于语义化版本的自动更新 2023-03-05 11:56:40 +08:00
Rock Chin
79e970c4c3 chore(deps): 删除dulwich依赖 2023-03-05 11:09:24 +08:00
Rock Chin
d12acd5f31 chore: git忽略temp目录 2023-03-05 11:05:42 +08:00
33 changed files with 822 additions and 281 deletions

9
.gitignore vendored
View File

@@ -9,4 +9,11 @@ plugins/
/revcfg.py
prompts/
logs/
sensitive.json
sensitive.json
temp/
current_tag
scenario/
!scenario/default-template.json
override.json
cookies.json
res/announcement_saved

View File

@@ -1,12 +1,14 @@
# QChatGPT🤖
> 2023/3/3 官方接口疑似被墙,可考虑使用网络代理 [#198](https://github.com/RockChinQ/QChatGPT/issues/198)
> 2023/3/15 逆向库已支持New Bing使用方法查看[插件文档](https://github.com/RockChinQ/revLibs)
> 2023/3/15 逆向库已支持GPT-4模型使用方法查看[插件](https://github.com/RockChinQ/revLibs)
> 2023/3/3 官方接口疑似被墙,可考虑使用网络代理[#198](https://github.com/RockChinQ/QChatGPT/issues/198)
> 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
- 交流、答疑群: ~~204785790~~(已满)、~~691226829~~(已满)、656285629
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
- QQ频道机器人见[QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT)
@@ -18,7 +20,9 @@
- OpenAI GPT-3.5模型(ChatGPT API), 本项目原生支持, 默认使用
- OpenAI GPT-3模型, 本项目原生支持, 部署完成后前往config.py切换
- ChatGPT网页版逆向API, 由[插件](https://github.com/RockChinQ/revLibs)接入
- ChatGPT网页版GPT-3.5模型, 由[插件](https://github.com/RockChinQ/revLibs)接入
- ChatGPT网页版GPT-4模型, 由[插件](https://github.com/RockChinQ/revLibs)接入
- New Bing逆向库, 由[插件](https://github.com/RockChinQ/revLibs)接入
### 故事续写
@@ -115,10 +119,9 @@
### - 注册OpenAI账号
**可以直接进群找群主购买**
或参考以下文章自行注册
参考以下文章自行注册
> ~~[只需 1 元搞定 ChatGPT 注册](https://zhuanlan.zhihu.com/p/589470082)~~(已失效)
> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
> [手把手教你如何注册ChatGPT超级详细](https://guxiaobei.com/51461)
注册成功后请前往[个人中心查看](https://beta.openai.com/account/api-keys)api_key
@@ -163,8 +166,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. 运行一次主程序,生成配置文件
@@ -225,10 +227,9 @@ python3 main.py
- [@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官方模型适配贡献者
- [@万神的星空](https://github.com/qq255204159) 整合包发行
以及其他所有为本项目提供支持的朋友们。
以及所有[贡献者](https://github.com/RockChinQ/QChatGPT/graphs/contributors)和其他为本项目提供支持的朋友们。
## 👍赞赏

View File

@@ -79,15 +79,24 @@ default_prompt = {
"default": "如果我之后想获取帮助,请你说“输入!help获取帮助”",
}
# 情景预设格式
# 参考值旧版本方式default | 完整情景full_scenario
# 旧版本的格式为上述default_prompt中的内容或prompts目录下的文件名
# 完整情景预设的格式为JSON在JSON文件中列出对话的每个回合编写方法见scenario/default-template.json
preset_mode = "default"
# 群内响应规则
# 符合此消息的群内消息即使不包含at机器人也会响应
# 支持消息前缀匹配及正则表达式匹配
# 支持设置是否响应at消息、随机响应概率
# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式(regexp)匹配的消息不会删除匹配的部分
# 前缀匹配优先级高于正则表达式匹配
# 正则表达式简明教程https://www.runoob.com/regexp/regexp-tutorial.html
response_rules = {
"at": True, # 是否响应at机器人的消息
"prefix": ["/ai", "!ai", "ai", "ai"],
"regexp": [] # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办"
"regexp": [], # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办"
"random_rate": 0.0, # 随机响应概率0.0-1.00.0为不随机响应1.0为响应所有消息, 仅在前几项判断不通过时生效
}
# 消息忽略规则
@@ -183,6 +192,12 @@ blob_message_threshold = 256
# - "forward": 将长消息转换为转发消息组件发送
blob_message_strategy = "forward"
# 文字转图片时使用的字体文件路径
# 当策略为"image"时生效
# 若在Windows系统下程序会自动使用Windows自带的微软雅黑字体
# 若未填写或不存在且不是Windows将禁用文字转图片功能改为使用转发消息组件
font_path = ""
# 消息处理超时重试次数
retry_times = 3
@@ -196,6 +211,11 @@ hide_exce_info_to_user = False
# 设置为空字符串时,不发送提示信息
alter_tip_message = '出错了,请稍后再试'
# 机器人线程池大小
# 该参数决定机器人可以同时处理几个人的消息,超出线程池数量的请求会被阻塞,不会被丢弃
# 如果你不清楚该参数的意义,请不要更改
pool_num = 10
# 每个会话的过期时间,单位为秒
# 默认值20分钟
session_expire_time = 60 * 20
@@ -235,11 +255,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)

146
main.py
View File

@@ -1,4 +1,5 @@
import importlib
import json
import os
import shutil
import threading
@@ -32,7 +33,7 @@ log_colors_config = {
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'bold_red',
'CRITICAL': 'cyan',
}
@@ -45,7 +46,9 @@ def init_db():
def ensure_dependencies():
import pkg.utils.pkgmgr as pkgmgr
pkgmgr.run_pip(["install", "openai", "Pillow", "--upgrade"])
pkgmgr.run_pip(["install", "openai", "Pillow", "--upgrade",
"-i", "https://pypi.douban.com/simple/",
"--trusted-host", "pypi.douban.com"])
known_exception_caught = False
@@ -105,13 +108,22 @@ def reset_logging():
def main(first_time_init=False):
"""启动流程reload之后会被执行"""
global known_exception_caught
# 检查并创建plugins、prompts目录
check_path = ["plugins", "prompts"]
for path in check_path:
if not os.path.exists(path):
os.mkdir(path)
import config
# 更新openai库到最新版本
if not hasattr(config, 'upgrade_dependencies') or config.upgrade_dependencies:
print("正在更新依赖库,请等待...")
if not hasattr(config, 'upgrade_dependencies'):
print("这个操作不是必须的,如果不想更新,请在config.py中添加upgrade_dependencies=False")
else:
print("这个操作不是必须的,如果不想更新,请在config.py中将upgrade_dependencies设置为False")
try:
ensure_dependencies()
except Exception as e:
print("更新openai库失败:{}, 请忽略或自行更新".format(e))
known_exception_caught = False
try:
@@ -120,13 +132,39 @@ def main(first_time_init=False):
config = importlib.import_module('config')
import pkg.utils.context
pkg.utils.context.set_config(config)
init_runtime_log_file()
sh = reset_logging()
# 配置完整性校验
is_integrity = True
config_template = importlib.import_module('config-template')
for key in dir(config_template):
if not key.startswith("__") and not hasattr(config, key):
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")
logging.warning("以上配置已被设为默认值将在5秒后继续启动... ")
# 检查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:
time.sleep(5)
import pkg.utils.context
pkg.utils.context.set_config(config)
# 检查是否设置了管理员
if not (hasattr(config, 'admin_qq') and config.admin_qq != 0):
# logging.warning("未设置管理员QQ,管理员权限指令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
@@ -158,6 +196,7 @@ def main(first_time_init=False):
import pkg.openai.dprompt
pkg.openai.dprompt.read_prompt_from_file()
pkg.openai.dprompt.read_scenario_from_file()
pkg.utils.context.context['logger_handler'] = sh
# 主启动流程
@@ -173,7 +212,7 @@ def main(first_time_init=False):
# 初始化qq机器人
qqbot = pkg.qqbot.manager.QQBotManager(mirai_http_api_config=config.mirai_http_api_config,
timeout=config.process_message_timeout, retry=config.retry_times,
first_time_init=first_time_init)
first_time_init=first_time_init, pool_num=config.pool_num)
# 加载插件
import pkg.plugin.host
@@ -181,7 +220,7 @@ def main(first_time_init=False):
pkg.plugin.host.initialize_plugins()
if first_time_init: # 不是热重载之后的启动,则启动新的bot线程
if first_time_init: # 不是热重载之后的启动,则启动新的bot线程
import mirai.exceptions
@@ -231,6 +270,11 @@ def main(first_time_init=False):
qq_bot_thread = threading.Thread(target=run_bot_wrapper, args=(), daemon=True)
qq_bot_thread.start()
finally:
# 判断若是Windows输出选择模式可能会暂停程序的警告
if os.name == 'nt':
time.sleep(2)
logging.info("您正在使用Windows系统若命令行窗口处于“选择”模式程序可能会被暂停此时请右键点击窗口空白区域使其取消选择模式。")
time.sleep(12)
if first_time_init:
if not known_exception_caught:
@@ -263,24 +307,22 @@ def main(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 进行自动更新")
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))
while True:
try:
time.sleep(10)
if qqbot != pkg.utils.context.get_qqbot_manager(): # 已经reload了
logging.info("以前的main流程由于reload退出")
break
except KeyboardInterrupt:
stop()
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))
print("程序退出")
sys.exit(0)
return qqbot
def stop():
@@ -314,46 +356,44 @@ if __name__ == '__main__':
if not os.path.exists('banlist.py'):
shutil.copy('banlist-template.py', 'banlist.py')
# 检查是否有sensitive.json,
# 检查是否有sensitive.json
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/")
# 检查并创建plugins、prompts目录
check_path = ["plugins", "prompts"]
for path in check_path:
if not os.path.exists(path):
os.mkdir(path)
if len(sys.argv) > 1 and sys.argv[1] == 'init_db':
init_db()
sys.exit(0)
elif len(sys.argv) > 1 and sys.argv[1] == 'update':
try:
try:
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.ensure_dulwich()
except:
pass
from dulwich import porcelain
repo = porcelain.open_repo('.')
porcelain.pull(repo)
except ModuleNotFoundError:
print("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
print("正在进行程序更新...")
import pkg.utils.updater as updater
updater.update_all(cli=True)
sys.exit(0)
# import pkg.utils.configmgr
#
# pkg.utils.configmgr.set_config_and_reload("quote_origin", False)
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
import config
# 更新openai库到最新版本
if not hasattr(config, 'upgrade_dependencies') or config.upgrade_dependencies:
print("正在更新依赖库,请等待...")
if not hasattr(config, 'upgrade_dependencies'):
print("这个操作不是必须的,如果不想更新,请在config.py中添加upgrade_dependencies=False")
else:
print("这个操作不是必须的,如果不想更新,请在config.py中将upgrade_dependencies设置为False")
try:
ensure_dependencies()
except Exception as e:
print("更新openai库失败:{}, 请忽略或自行更新".format(e))
qqbot = main(True)
main(True)
import pkg.utils.context
while True:
try:
time.sleep(10)
except KeyboardInterrupt:
stop()
print("程序退出")
sys.exit(0)

View File

@@ -0,0 +1,3 @@
"""
审计相关操作
"""

View File

@@ -1,3 +1,7 @@
"""
使用量统计以及数据上报功能实现
"""
import hashlib
import json
import logging
@@ -10,8 +14,11 @@ import pkg.utils.updater
class DataGatherer:
"""数据收集器"""
usage = {}
"""以key值md5为key,{
"""各api-key的使用量
以key值md5为key,{
"text": {
"text-davinci-003": 文字量:int,
},
@@ -25,11 +32,16 @@ class DataGatherer:
def __init__(self):
self.load_from_db()
try:
self.version_str = pkg.utils.updater.get_commit_id_and_time_and_msg()[:40 if len(pkg.utils.updater.get_commit_id_and_time_and_msg()) > 40 else len(pkg.utils.updater.get_commit_id_and_time_and_msg())]
self.version_str = pkg.utils.updater.get_current_tag() # 从updater模块获取版本号
except:
pass
def report_to_server(self, subservice_name: str, count: int):
"""向中央服务器报告使用量
只会报告此次请求的使用量,不会报告总量。
不包含除版本号、使用类型、使用量以外的任何信息,仅供开发者分析使用情况。
"""
try:
config = pkg.utils.context.get_config()
if hasattr(config, "report_usage") and not config.report_usage:
@@ -44,7 +56,9 @@ class DataGatherer:
return self.usage[key_md5] if key_md5 in self.usage else {}
def report_text_model_usage(self, model, total_tokens):
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
"""调用方报告文字模型请求文字使用量"""
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5() # 以key的md5进行储存
if key_md5 not in self.usage:
self.usage[key_md5] = {}
@@ -62,6 +76,8 @@ class DataGatherer:
self.report_to_server("text", length)
def report_image_model_usage(self, size):
"""调用方报告图片模型请求图片使用量"""
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
if key_md5 not in self.usage:
@@ -79,6 +95,7 @@ class DataGatherer:
self.report_to_server("image", 1)
def get_text_length_of_key(self, key):
"""获取指定api-key (明文) 的文字总使用量(本地记录)"""
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
if key_md5 not in self.usage:
return 0
@@ -88,6 +105,8 @@ class DataGatherer:
return sum(self.usage[key_md5]["text"].values())
def get_image_count_of_key(self, key):
"""获取指定api-key (明文) 的图片总使用量(本地记录)"""
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
if key_md5 not in self.usage:
return 0
@@ -97,6 +116,7 @@ class DataGatherer:
return sum(self.usage[key_md5]["image"].values())
def get_total_text_length(self):
"""获取所有api-key的文字总使用量(本地记录)"""
total = 0
for key in self.usage:
if "text" not in self.usage[key]:

View File

@@ -0,0 +1,3 @@
"""
数据库操作封装
"""

View File

@@ -1,3 +1,6 @@
"""
数据库管理模块
"""
import hashlib
import json
import logging
@@ -9,9 +12,9 @@ import sqlite3
import pkg.utils.context
# 数据库管理
# 为其他模块提供数据库操作接口
class DatabaseManager:
"""封装数据库底层操作,并提供方法给上层使用"""
conn = None
cursor = None
@@ -23,21 +26,25 @@ class DatabaseManager:
# 连接到数据库文件
def reconnect(self):
"""连接到数据库"""
self.conn = sqlite3.connect('database.db', check_same_thread=False)
self.cursor = self.conn.cursor()
def close(self):
self.conn.close()
def execute(self, *args, **kwargs) -> Cursor:
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
# 初始化数据库的函数
def initialize_database(self):
self.execute("""
"""创建数据表"""
self.__execute__("""
create table if not exists `sessions` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` varchar(255) not null,
@@ -46,11 +53,24 @@ class DatabaseManager:
`create_timestamp` bigint not null,
`last_interact_timestamp` bigint not null,
`status` varchar(255) not null default 'on_going',
`default_prompt` text not null default '',
`prompt` text not null
)
""")
self.execute("""
# 检查sessions表是否存在`default_prompt`字段
self.__execute__("PRAGMA table_info('sessions')")
columns = self.cursor.fetchall()
has_default_prompt = False
for field in columns:
if field[1] == 'default_prompt':
has_default_prompt = True
break
if not has_default_prompt:
self.__execute__("alter table `sessions` add column `default_prompt` text not null default ''")
self.__execute__("""
create table if not exists `account_fee`(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`key_md5` varchar(255) not null,
@@ -59,7 +79,7 @@ class DatabaseManager:
)
""")
self.execute("""
self.__execute__("""
create table if not exists `account_usage`(
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`json` text not null
@@ -69,47 +89,49 @@ 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 = ''):
"""持久化指定session"""
# 检查是否已经有了此name和create_timestamp的session
# 如果有就更新prompt和last_interact_timestamp
# 如果没有,就插入一条新的记录
self.execute("""
self.__execute__("""
select count(*) from `sessions` where `type` = '{}' and `number` = {} and `create_timestamp` = {}
""".format(subject_type, subject_number, create_timestamp))
count = self.cursor.fetchone()[0]
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`)
values (?, ?, ?, ?, ?, ?, ?)
"""
self.execute(sql,
("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp,
last_interact_timestamp, prompt))
self.__execute__(sql,
("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp,
last_interact_timestamp, prompt, default_prompt))
else:
sql = """
update `sessions` set `last_interact_timestamp` = ?, `prompt` = ?
where `type` = ? and `number` = ? and `create_timestamp` = ?
"""
self.execute(sql, (last_interact_timestamp, prompt, subject_type,
subject_number, create_timestamp))
self.__execute__(sql, (last_interact_timestamp, prompt, subject_type,
subject_number, create_timestamp))
# 显式关闭一个session
def explicit_close_session(self, session_name: str, create_timestamp: int):
self.execute("""
self.__execute__("""
update `sessions` set `status` = 'explicitly_closed' where `name` = '{}' and `create_timestamp` = {}
""".format(session_name, create_timestamp))
def set_session_ongoing(self, session_name: str, create_timestamp: int):
self.execute("""
self.__execute__("""
update `sessions` set `status` = 'on_going' where `name` = '{}' and `create_timestamp` = {}
""".format(session_name, create_timestamp))
# 设置session为过期
def set_session_expired(self, session_name: str, create_timestamp: int):
self.execute("""
self.__execute__("""
update `sessions` set `status` = 'expired' where `name` = '{}' and `create_timestamp` = {}
""".format(session_name, create_timestamp))
@@ -117,8 +139,8 @@ class DatabaseManager:
def load_valid_sessions(self) -> dict:
# 从数据库中加载所有还没过期的session
config = pkg.utils.context.get_config()
self.execute("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`
from `sessions` where `last_interact_timestamp` > {}
""".format(int(time.time()) - config.session_expire_time))
results = self.cursor.fetchall()
@@ -131,6 +153,7 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
default_prompt = result[7]
# 当且仅当最后一个该对象的会话是on_going状态时才会被加载
if status == 'on_going':
@@ -139,7 +162,8 @@ class DatabaseManager:
'subject_number': subject_number,
'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp,
'prompt': prompt
'prompt': prompt,
'default_prompt': default_prompt
}
else:
if session_name in sessions:
@@ -150,8 +174,8 @@ class DatabaseManager:
# 获取此session_name前一个session的数据
def last_session(self, session_name: str, cursor_timestamp: int):
self.execute("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`
from `sessions` where `name` = '{}' and `last_interact_timestamp` < {} order by `last_interact_timestamp` desc
limit 1
""".format(session_name, cursor_timestamp))
@@ -167,20 +191,22 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
default_prompt = result[7]
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
}
# 获取此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`
self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`
from `sessions` where `name` = '{}' and `last_interact_timestamp` > {} order by `last_interact_timestamp` asc
limit 1
""".format(session_name, cursor_timestamp))
@@ -196,19 +222,21 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
default_prompt = result[7]
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
}
# 列出与某个对象的所有对话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`
self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`
from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit {} offset {}
""".format(session_name, capacity, capacity * page))
results = self.cursor.fetchall()
@@ -221,17 +249,40 @@ class DatabaseManager:
last_interact_timestamp = result[4]
prompt = result[5]
status = result[6]
default_prompt = result[7]
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
})
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...')
@@ -246,22 +297,22 @@ class DatabaseManager:
usage_count = usage[key_md5]
# 将使用量存进数据库
# 先检查是否已存在
self.execute("""
self.__execute__("""
select count(*) from `api_key_usage` where `key_md5` = '{}'""".format(key_md5))
result = self.cursor.fetchone()
if result[0] == 0:
# 不存在则插入
self.execute("""
self.__execute__("""
insert into `api_key_usage` (`key_md5`, `usage`,`timestamp`) values ('{}', {}, {})
""".format(key_md5, usage_count, int(time.time())))
else:
# 存在则更新timestamp设置为当前
self.execute("""
self.__execute__("""
update `api_key_usage` set `usage` = {}, `timestamp` = {} where `key_md5` = '{}'
""".format(usage_count, int(time.time()), key_md5))
def load_api_key_usage(self):
self.execute("""
self.__execute__("""
select `key_md5`, `usage` from `api_key_usage`
""")
results = self.cursor.fetchall()
@@ -273,23 +324,24 @@ class DatabaseManager:
return usage
def dump_usage_json(self, usage: dict):
json_str = json.dumps(usage)
self.execute("""
self.__execute__("""
select count(*) from `account_usage`""")
result = self.cursor.fetchone()
if result[0] == 0:
# 不存在则插入
self.execute("""
self.__execute__("""
insert into `account_usage` (`json`) values ('{}')
""".format(json_str))
else:
# 存在则更新
self.execute("""
self.__execute__("""
update `account_usage` set `json` = '{}' where `id` = 1
""".format(json_str))
def load_usage_json(self):
self.execute("""
self.__execute__("""
select `json` from `account_usage` order by id desc limit 1
""")
result = self.cursor.fetchone()

View File

@@ -0,0 +1,2 @@
"""OpenAI 接口处理及会话管理相关
"""

View File

@@ -1,11 +1,20 @@
# 多情景预设值管理
import json
import logging
__current__ = "default"
"""当前默认使用的情景预设的名称
由管理员使用`!default <名称>`指令切换
"""
__prompts_from_files__ = {}
"""从文件中读取的情景预设值"""
__scenario_from_files__ = {}
def read_prompt_from_file() -> str:
def read_prompt_from_file():
"""从文件读取预设值"""
# 读取prompts/目录下的所有文件,以文件名为键,文件内容为值
# 保存在__prompts_from_files__中
@@ -18,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
@@ -60,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)

View File

@@ -5,18 +5,26 @@ import logging
import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
class KeysManager:
api_key = {}
"""所有api-key"""
# api-key的使用量
# 其中键为api-key的md5值值为使用量
using_key = ""
"""当前使用的api-key
"""
alerted = []
"""已提示过超额的key
记录在此以避免重复提示
"""
# 在此list中的都是经超额报错标记过的api-key
# 记录的是key值仅在运行时有效
exceeded = []
"""已超额的key
供自动切换功能识别
"""
def get_using_key(self):
return self.using_key
@@ -25,8 +33,6 @@ class KeysManager:
return hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
def __init__(self, api_key):
# if hasattr(config, 'api_key_usage_threshold'):
# self.api_key_usage_threshold = config.api_key_usage_threshold
if type(api_key) is dict:
self.api_key = api_key
@@ -42,9 +48,13 @@ class KeysManager:
self.auto_switch()
# 根据tested自动切换到可用的api-key
# 返回是否切换成功, 切换后的api-key的别名
def auto_switch(self) -> (bool, str):
"""尝试切换api-key
Returns:
是否切换成功, 切换后的api-key的别名
"""
for key_name in self.api_key:
if self.api_key[key_name] not in self.exceeded:
self.using_key = self.api_key[key_name]
@@ -68,12 +78,9 @@ class KeysManager:
def add(self, key_name, key):
self.api_key[key_name] = key
# 设置当前使用的api-key使用量超限
# 这是在尝试调用api时发生超限异常时调用的
def set_current_exceeded(self):
# md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
# self.usage[md5] = self.api_key_usage_threshold
# self.fee[md5] = self.api_key_fee_threshold
"""设置当前使用的api-key使用量超限
"""
self.exceeded.append(self.using_key)
def get_key_name(self, api_key):
@@ -81,4 +88,4 @@ class KeysManager:
for key_name in self.api_key:
if self.api_key[key_name] == api_key:
return key_name
return ""
return ""

View File

@@ -7,9 +7,12 @@ import pkg.utils.context
import pkg.audit.gatherer
from pkg.openai.modelmgr import ModelRequest, create_openai_model_request
# 为其他模块提供与OpenAI交互的接口
class OpenAIInteract:
api_params = {}
"""OpenAI 接口封装
将文字接口和图片接口封装供调用方使用
"""
key_mgr: pkg.openai.keymgr.KeysManager = None
@@ -20,7 +23,6 @@ class OpenAIInteract:
}
def __init__(self, api_key: str):
# self.api_key = api_key
self.key_mgr = pkg.openai.keymgr.KeysManager(api_key)
self.audit_mgr = pkg.audit.gatherer.DataGatherer()
@@ -32,7 +34,16 @@ class OpenAIInteract:
pkg.utils.context.set_openai_manager(self)
# 请求OpenAI Completion
def request_completion(self, prompts):
def request_completion(self, prompts) -> str:
"""请求补全接口回复
Parameters:
prompts (str): 提示语
Returns:
str: 回复
"""
config = pkg.utils.context.get_config()
# 根据模型选择使用的接口
@@ -58,8 +69,15 @@ class OpenAIInteract:
return ai.get_message()
def request_image(self, prompt):
def request_image(self, prompt) -> dict:
"""请求图片接口回复
Parameters:
prompt (str): 提示语
Returns:
dict: 响应
"""
config = pkg.utils.context.get_config()
params = config.image_api_params if hasattr(config, "image_api_params") else self.default_image_api_params

View File

@@ -1,5 +1,12 @@
# 提供与模型交互的抽象接口
"""OpenAI 接口底层封装
目前使用的对话接口有:
ChatCompletion - gpt-3.5-turbo 等模型
Completion - text-davinci-003 等模型
此模块封装此两个接口的请求实现,为上层提供统一的调用方式
"""
import openai, logging, threading, asyncio
import openai.error as aiE
COMPLETION_MODELS = {
'text-davinci-003',
@@ -25,26 +32,46 @@ IMAGE_MODELS = {
}
class ModelRequest():
"""GPT父类"""
can_chat = False
runtime:threading.Thread = None
ret = ""
proxy:str = None
class ModelRequest:
"""模型接口请求父类"""
def __init__(self, model_name, user_name, request_fun, http_proxy:str = None):
can_chat = False
runtime: threading.Thread = None
ret = {}
proxy: str = None
request_ready = True
error_info: str = "若在没有任何错误的情况下看到这句话请带着配置文件上报Issues"
def __init__(self, model_name, user_name, request_fun, http_proxy:str = None, time_out = None):
self.model_name = model_name
self.user_name = user_name
self.request_fun = request_fun
self.time_out = time_out
if http_proxy != None:
self.proxy = http_proxy
openai.proxy = self.proxy
self.request_ready = False
async def __a_request__(self, **kwargs):
self.ret = await self.request_fun(**kwargs)
"""异步请求"""
try:
self.ret:dict = await self.request_fun(**kwargs)
self.request_ready = True
except aiE.APIConnectionError as e:
self.error_info = "{}\n请检查网络连接或代理是否正常".format(e)
raise ConnectionError(self.error_info)
except ValueError as e:
self.error_info = "{}\n该错误可能是由于http_proxy格式设置错误引起的"
except Exception as e:
self.error_info = "{}\n由于请求异常产生的未知错误,请查看日志".format(e)
raise Exception(self.error_info)
def request(self, **kwargs):
"""向接口发起请求"""
if self.proxy != None: #异步请求
self.request_ready = False
loop = asyncio.new_event_loop()
self.runtime = threading.Thread(
target=loop.run_until_complete,
@@ -64,13 +91,15 @@ class ModelRequest():
若重写该方法应检查异步线程状态或在需要检查处super该方法
'''
if self.runtime != None and isinstance(self.runtime, threading.Thread):
self.runtime.join()
return
self.runtime.join(self.time_out)
if self.request_ready:
return
raise Exception(self.error_info)
def get_total_tokens(self):
try:
return self.ret['usage']['total_tokens']
except Exception:
except:
return 0
def get_message(self):
@@ -79,8 +108,10 @@ class ModelRequest():
def get_response(self):
return self.ret
class ChatCompletionModel(ModelRequest):
"""ChatCompletion类模型"""
"""ChatCompletion接口的请求实现"""
Chat_role = ['system', 'user', 'assistant']
def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
if http_proxy == None:
@@ -108,7 +139,8 @@ class ChatCompletionModel(ModelRequest):
class CompletionModel(ModelRequest):
"""Completion类模型"""
"""Completion接口的请求实现"""
def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
if http_proxy == None:
request_fun = openai.Completion.create

View File

@@ -1,3 +1,8 @@
"""主线使用的会话管理模块
每个人、每个群单独一个sessionsession内部保留了对话的上下文
"""
import logging
import threading
import time
@@ -19,6 +24,7 @@ class SessionOfflineStatus:
ON_GOING = 'on_going'
EXPLICITLY_CLOSED = 'explicitly_closed'
# 重置session.prompt
def reset_session_prompt(session_name, prompt):
# 备份原始数据
@@ -34,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
}
]
# 警告
@@ -43,11 +49,14 @@ def reset_session_prompt(session_name, prompt):
用户[{}]的数据已被重置,有可能是因为数据版本过旧或存储错误
原始数据将备份在:
{}""".format(session_name, bak_path)
)
) # 为保证多行文本格式正确故无缩进
return prompt
# 从数据加载session
def load_sessions():
"""从数据库加载sessions"""
global sessions
db_inst = pkg.utils.context.get_database_manager()
@@ -66,6 +75,8 @@ def load_sessions():
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
@@ -93,10 +104,16 @@ class Session:
name = ''
prompt = []
"""使用list来保存会话中的回合"""
default_prompt = []
"""本session的默认prompt"""
create_timestamp = 0
"""会话创建时间"""
last_interact_timestamp = 0
"""上次交互(产生回复)时间"""
just_switched_to_exist_session = False
@@ -116,25 +133,14 @@ class Session:
logging.debug('{},lock release successfully,{}'.format(self.name, self.response_lock))
# 从配置文件获取会话预设信息
def get_default_prompt(self, use_default: str=None):
config = pkg.utils.context.get_config()
def get_default_prompt(self, use_default: str = None):
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
@@ -143,7 +149,9 @@ class Session:
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):
@@ -182,14 +190,16 @@ class Session:
# 请求回复
# 这个函数是阻塞的
def append(self, text: str) -> str:
"""向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)
@@ -215,14 +225,14 @@ class Session:
res_ans = '\n\n'.join(res_ans_spt)
# 将此次对话的双方内容加入到prompt中
self.prompt.append({'role':'user', 'content':text})
self.prompt.append({'role':'assistant', 'content':res_ans})
self.prompt.append({'role': 'user', 'content': text})
self.prompt.append({'role': 'assistant', 'content': res_ans})
if self.just_switched_to_exist_session:
self.just_switched_to_exist_session = False
self.set_ongoing()
return res_ans if res_ans[0]!='\n' else res_ans[1:]
return res_ans if res_ans[0] != '\n' else res_ans[1:]
# 删除上一回合并返回上一回合的问题
def undo(self) -> str:
@@ -231,10 +241,10 @@ class Session:
# 删除最后两个消息
if len(self.prompt) < 2:
raise Exception('之前无对话,无法撤销')
question = self.prompt[-2]['content']
self.prompt = self.prompt[:-2]
# 返回上一回合的问题
return question
@@ -242,25 +252,29 @@ class Session:
def cut_out(self, msg: str, max_tokens: int) -> list:
"""将现有prompt进行切割处理使得新的prompt长度不超过max_tokens"""
# 如果用户消息长度超过max_tokens直接返回
temp_prompt = [
{
'role': 'user',
'content': msg
}
]
temp_prompt: list = []
temp_prompt += self.default_prompt
temp_prompt.append(
{
'role': 'user',
'content': msg
}
)
token_count = 0
for item in temp_prompt:
token_count += len(item['content'])
token_count = len(msg)
# 倒序遍历prompt
for i in range(len(self.prompt) - 1, -1, -1):
if token_count >= max_tokens:
break
# 将prompt加到temp_prompt头部
temp_prompt.insert(0, self.prompt[i])
# 将prompt加到temp_prompt倒数第二个位置
temp_prompt.insert(len(self.default_prompt), self.prompt[i])
token_count += len(self.prompt[i]['content'])
logging.debug('cut_out: {}'.format(str(temp_prompt)))
logging.debug('cut_out: {}'.format(json.dumps(temp_prompt, ensure_ascii=False, indent=4)))
return temp_prompt
@@ -277,11 +291,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))
# 重置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:
# 触发插件事件
@@ -297,7 +311,9 @@ 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.create_timestamp = int(time.time())
self.last_interact_timestamp = int(time.time())
self.just_switched_to_exist_session = False
@@ -326,6 +342,7 @@ class Session:
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
@@ -345,6 +362,7 @@ class Session:
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
@@ -352,5 +370,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)

View File

@@ -0,0 +1,4 @@
"""插件支持包
包含插件基类、插件宿主以及部分API接口
"""

View File

@@ -116,7 +116,9 @@ def initialize_plugins():
def unload_plugins():
""" 卸载插件 """
""" 卸载插件
"""
# 不再显式卸载插件,因为当程序结束时,插件的析构函数会被系统执行
# for plugin in __plugins__.values():
# if plugin['enabled'] and plugin['instance'] is not None:
# if not hasattr(plugin['instance'], '__del__'):

View File

@@ -145,6 +145,7 @@ __current_registering_plugin__ = ""
class Plugin:
"""插件基类"""
host: host.PluginHost
"""插件宿主,提供插件的一些基础功能"""

View File

@@ -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('未定义长消息处理策略')

View File

@@ -4,6 +4,7 @@ import json
import datetime
import os
import threading
import traceback
import pkg.openai.session
import pkg.openai.manager
@@ -234,7 +235,7 @@ def process_command(session_name: str, text_message: str, mgr, config,
if len(msg) >= 2:
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
msg[1]['content'])
msg[0]['content'])
else:
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
@@ -256,6 +257,20 @@ def process_command(session_name: str, text_message: str, mgr, config,
reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config,
launcher_type, launcher_id, sender_id)
elif cmd == 'del': # 删除指定会话历史记录
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查看序号"]
elif cmd == 'usage':
reply_str = "[bot]各api-key使用情况:\n\n"
@@ -322,6 +337,18 @@ def process_command(session_name: str, text_message: str, mgr, config,
reply = ["[bot]err: 未找到情景预设:{}".format(params[0])]
else:
reply = ["[bot]err: 仅管理员可设置默认情景预设"]
elif cmd == "delhst" and is_admin:
if len(params) == 0:
reply = ["[bot]err:请输入要删除的会话名: group_<群号> 或者 person_<QQ号>, 或使用 !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])]
elif cmd == 'reload' and is_admin:
def reload_task():
pkg.utils.reloader.reload_all()
@@ -336,6 +363,7 @@ def process_command(session_name: str, text_message: str, mgr, config,
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

View File

@@ -7,6 +7,8 @@ import logging
class ReplyFilter:
sensitive_words = []
mask = "*"
mask_word = ""
# 默认值( 兼容性考虑 )
baidu_check = False
@@ -14,8 +16,10 @@ class ReplyFilter:
baidu_secret_key = ""
inappropriate_message_tips = "[百度云]请珍惜机器人,当前返回内容不合规"
def __init__(self, sensitive_words: list):
def __init__(self, sensitive_words: list, mask: str = "*", mask_word: str = ""):
self.sensitive_words = sensitive_words
self.mask = mask
self.mask_word = mask_word
import config
if hasattr(config, 'baidu_check') and hasattr(config, 'baidu_api_key') and hasattr(config, 'baidu_secret_key'):
self.baidu_check = config.baidu_check
@@ -36,7 +40,10 @@ class ReplyFilter:
match = re.findall(word, message)
if len(match) > 0:
for i in range(len(match)):
message = message.replace(match[i], "*" * len(match[i]))
if self.mask_word == "":
message = message.replace(match[i], self.mask * len(match[i]))
else:
message = message.replace(match[i], self.mask_word)
# 百度云审核
if self.baidu_check:

View File

@@ -2,6 +2,7 @@ import asyncio
import json
import os
import threading
from concurrent.futures import ThreadPoolExecutor
import mirai.models.bus
from mirai import At, GroupMessage, MessageEvent, Mirai, StrangerMessage, WebSocketAdapter, HTTPAdapter, \
@@ -21,12 +22,6 @@ import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
# 并行运行
def go(func, args=()):
thread = threading.Thread(target=func, args=args, daemon=True)
thread.start()
# 检查消息是否符合泛响应匹配机制
def check_response_rule(text: str):
config = pkg.utils.context.get_config()
@@ -51,10 +46,29 @@ def check_response_rule(text: str):
return False, ""
def response_at():
config = pkg.utils.context.get_config()
if 'at' not in config.response_rules:
return True
return config.response_rules['at']
def random_responding():
config = pkg.utils.context.get_config()
if 'random_rate' in config.response_rules:
import random
return random.random() < config.response_rules['random_rate']
return False
# 控制QQ消息输入输出的类
class QQBotManager:
retry = 3
#线程池控制
pool = None
bot: Mirai = None
reply_filter = None
@@ -64,11 +78,14 @@ class QQBotManager:
ban_person = []
ban_group = []
def __init__(self, mirai_http_api_config: dict, timeout: int = 60, retry: int = 3, first_time_init=True):
def __init__(self, mirai_http_api_config: dict, timeout: int = 60, retry: int = 3, pool_num: int = 10, first_time_init=True):
self.timeout = timeout
self.retry = retry
self.pool_num = pool_num
self.pool = ThreadPoolExecutor(max_workers=self.pool_num)
logging.debug("Registered thread pool Size:{}".format(pool_num))
# 加载禁用列表
if os.path.exists("banlist.py"):
import banlist
@@ -82,7 +99,12 @@ class QQBotManager:
and config.sensitive_word_filter is not None \
and config.sensitive_word_filter:
with open("sensitive.json", "r", encoding="utf-8") as f:
self.reply_filter = pkg.qqbot.filter.ReplyFilter(json.load(f)['words'])
sensitive_json = json.load(f)
self.reply_filter = pkg.qqbot.filter.ReplyFilter(
sensitive_words=sensitive_json['words'],
mask=sensitive_json['mask'] if 'mask' in sensitive_json else '*',
mask_word=sensitive_json['mask_word'] if 'mask_word' in sensitive_json else ''
)
else:
self.reply_filter = pkg.qqbot.filter.ReplyFilter([])
@@ -116,7 +138,7 @@ class QQBotManager:
self.on_person_message(event)
go(friend_message_handler, (event,))
self.go(friend_message_handler, event)
@self.bot.on(StrangerMessage)
async def on_stranger_message(event: StrangerMessage):
@@ -136,7 +158,7 @@ class QQBotManager:
self.on_person_message(event)
go(stranger_message_handler, (event,))
self.go(stranger_message_handler, event)
@self.bot.on(GroupMessage)
async def on_group_message(event: GroupMessage):
@@ -156,7 +178,7 @@ class QQBotManager:
self.on_group_message(event)
go(group_message_handler, (event,))
self.go(group_message_handler, event)
def unsubscribe_all():
"""取消所有订阅
@@ -173,6 +195,9 @@ class QQBotManager:
self.unsubscribe_all = unsubscribe_all
def go(self, func, *args, **kwargs):
self.pool.submit(func, *args, **kwargs)
def first_time_init(self, mirai_http_api_config: dict):
"""热重载后不再运行此函数"""
@@ -288,14 +313,19 @@ class QQBotManager:
if Image in event.message_chain:
pass
elif At(self.bot.qq) not in event.message_chain:
check, result = check_response_rule(str(event.message_chain).strip())
if check:
reply = process(result.strip())
else:
# 直接调用
reply = process()
if At(self.bot.qq) in event.message_chain and response_at():
# 直接调用
reply = process()
else:
check, result = check_response_rule(str(event.message_chain).strip())
if check:
reply = process(result.strip())
# 检查是否随机响应
elif random_responding():
logging.info("随机响应group_{}消息".format(event.group.id))
reply = process()
if reply:
return self.send(event, reply)

View File

@@ -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'):

View File

@@ -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))

44
pkg/utils/announcement.py Normal file
View File

@@ -0,0 +1,44 @@
import base64
import os
import requests
def read_latest() -> str:
resp = requests.get(
url="https://api.github.com/repos/RockChinQ/QChatGPT/contents/res/announcement",
)
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()

File diff suppressed because one or more lines are too long

View File

@@ -7,13 +7,15 @@ import pkg.utils.context
import pkg.plugin.host
def walk(module, prefix=''):
def walk(module, prefix='', path_prefix=''):
"""遍历并重载所有模块"""
for item in pkgutil.iter_modules(module.__path__):
if item.ispkg:
walk(__import__(module.__name__ + '.' + item.name, fromlist=['']), prefix + item.name + '.')
walk(__import__(module.__name__ + '.' + item.name, fromlist=['']), prefix + item.name + '.', path_prefix + item.name + '/')
else:
logging.info('reload module: {}'.format(prefix + item.name))
logging.info('reload module: {}, path: {}'.format(prefix + item.name, path_prefix + item.name + '.py'))
pkg.plugin.host.__current_module_path__ = "plugins/" + path_prefix + item.name + '.py'
importlib.reload(__import__(module.__name__ + '.' + item.name, fromlist=['']))

View File

@@ -1,8 +1,37 @@
import logging
from PIL import Image, ImageDraw, ImageFont
import re
import os
import config
import traceback
text_render_font = ImageFont.truetype("res/simhei.ttf", 32, encoding="utf-8")
text_render_font: ImageFont = None
if hasattr(config, "blob_message_strategy") and config.blob_message_strategy == "image": # 仅在启用了image时才加载字体
use_font = config.font_path if hasattr(config, "font_path") else ""
try:
# 检查是否存在
if not os.path.exists(use_font):
# 若是windows系统使用微软雅黑
if os.name == "nt":
use_font = "C:/Windows/Fonts/msyh.ttc"
if not os.path.exists(use_font):
logging.warn("未找到字体文件且无法使用Windows自带字体更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。")
config.blob_message_strategy = "forward"
else:
logging.info("使用Windows自带字体" + use_font)
text_render_font = ImageFont.truetype(use_font, 32, encoding="utf-8")
else:
logging.warn("未找到字体文件且无法使用Windows自带字体更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。")
config.blob_message_strategy = "forward"
else:
text_render_font = ImageFont.truetype(use_font, 32, encoding="utf-8")
except:
traceback.print_exc()
logging.error("加载字体文件失败({})更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。".format(use_font))
config.blob_message_strategy = "forward"
def indexNumber(path=''):
@@ -123,7 +152,7 @@ def text_to_image(text_str: str, save_as="temp.png", width=800):
else:
continue
# 准备画布
img = Image.new('RGBA', (width, max(280, len(final_lines) * 35 + 45)), (255, 255, 255, 255))
img = Image.new('RGBA', (width, max(280, len(final_lines) * 35 + 65)), (255, 255, 255, 255))
draw = ImageDraw.Draw(img, mode='RGBA')

View File

@@ -1,6 +1,11 @@
import datetime
import logging
import os.path
import pkg.utils.context
import requests
import json
import pkg.utils.constants
def check_dulwich_closure():
@@ -28,34 +33,122 @@ def pull_latest(repo_path: str) -> bool:
return True
def update_all() -> bool:
"""使用dulwich更新源码"""
check_dulwich_closure()
import dulwich
try:
before_commit_id = get_current_commit_id()
from dulwich import porcelain
repo = porcelain.open_repo('.')
porcelain.pull(repo)
def get_release_list() -> list:
"""获取发行列表"""
rls_list_resp = requests.get(
url="https://api.github.com/repos/RockChinQ/QChatGPT/releases"
)
change_log = ""
rls_list = rls_list_resp.json()
for entry in repo.get_walker():
if str(entry.commit.id)[2:-1] == before_commit_id:
break
tz = datetime.timezone(datetime.timedelta(hours=entry.commit.commit_timezone // 3600))
dt = datetime.datetime.fromtimestamp(entry.commit.commit_time, tz)
change_log += dt.strftime('%Y-%m-%d %H:%M:%S') + " [" + str(entry.commit.message, encoding="utf-8").strip()+"]\n"
return rls_list
if change_log != "":
pkg.utils.context.get_qqbot_manager().notify_admin("代码拉取完成,更新内容如下:\n"+change_log)
return True
else:
return False
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
except dulwich.porcelain.DivergedBranches:
raise Exception("分支不一致,自动更新仅支持master分支,请手动更新(https://github.com/RockChinQ/QChatGPT/issues/76)")
def get_current_tag() -> str:
"""获取当前tag"""
current_tag = pkg.utils.constants.semantic_version
if os.path.exists("current_tag"):
with open("current_tag", "r") as f:
current_tag = f.read()
return current_tag
def update_all(cli: bool = False) -> bool:
"""检查更新并下载源码"""
current_tag = get_current_tag()
rls_list = get_release_list()
latest_rls = {}
rls_notes = []
for rls in rls_list:
rls_notes.append(rls['name']) # 使用发行名称作为note
if rls['tag_name'] == current_tag:
break
if latest_rls == {}:
latest_rls = rls
if not cli:
logging.info("更新日志: {}".format(rls_notes))
else:
print("更新日志: {}".format(rls_notes))
if latest_rls == {}: # 没有新版本
return False
# 下载最新版本的zip到temp目录
if not cli:
logging.info("开始下载最新版本: {}".format(latest_rls['zipball_url']))
else:
print("开始下载最新版本: {}".format(latest_rls['zipball_url']))
zip_url = latest_rls['zipball_url']
zip_resp = requests.get(url=zip_url)
zip_data = zip_resp.content
# 检查temp/updater目录
if not os.path.exists("temp"):
os.mkdir("temp")
if not os.path.exists("temp/updater"):
os.mkdir("temp/updater")
with open("temp/updater/{}.zip".format(latest_rls['tag_name']), "wb") as f:
f.write(zip_data)
if not cli:
logging.info("下载最新版本完成: {}".format("temp/updater/{}.zip".format(latest_rls['tag_name'])))
else:
print("下载最新版本完成: {}".format("temp/updater/{}.zip".format(latest_rls['tag_name'])))
# 解压zip到temp/updater/<tag_name>/
import zipfile
# 检查目标文件夹
if os.path.exists("temp/updater/{}".format(latest_rls['tag_name'])):
import shutil
shutil.rmtree("temp/updater/{}".format(latest_rls['tag_name']))
os.mkdir("temp/updater/{}".format(latest_rls['tag_name']))
with zipfile.ZipFile("temp/updater/{}.zip".format(latest_rls['tag_name']), 'r') as zip_ref:
zip_ref.extractall("temp/updater/{}".format(latest_rls['tag_name']))
# 覆盖源码
source_root = ""
# 找到temp/updater/<tag_name>/中的第一个子目录路径
for root, dirs, files in os.walk("temp/updater/{}".format(latest_rls['tag_name'])):
if root != "temp/updater/{}".format(latest_rls['tag_name']):
source_root = root
break
# 覆盖源码
import shutil
for root, dirs, files in os.walk(source_root):
# 覆盖所有子文件子目录
for file in files:
src = os.path.join(root, file)
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写入文件
current_tag = latest_rls['tag_name']
with open("current_tag", "w") as f:
f.write(current_tag)
# 通知管理员
if not cli:
import pkg.utils.context
pkg.utils.context.get_qqbot_manager().notify_admin("已更新到最新版本: {}\n更新日志:\n{}\n新功能通常可以在config-template.py中看到完整的更新日志请前往 https://github.com/RockChinQ/QChatGPT/releases 查看".format(current_tag, "\n".join(rls_notes)))
else:
print("已更新到最新版本: {}\n更新日志:\n{}\n新功能通常可以在config-template.py中看到完整的更新日志请前往 https://github.com/RockChinQ/QChatGPT/releases 查看".format(current_tag, "\n".join(rls_notes)))
return True
def is_repo(path: str) -> bool:
@@ -81,24 +174,12 @@ def get_remote_url(repo_path: str) -> str:
def get_current_version_info() -> str:
"""获取当前版本信息"""
check_dulwich_closure()
from dulwich import porcelain
repo = porcelain.open_repo('.')
version_str = ""
for entry in repo.get_walker():
version_str += "提交编号: "+str(entry.commit.id)[2:9] + "\n"
tz = datetime.timezone(datetime.timedelta(hours=entry.commit.commit_timezone // 3600))
dt = datetime.datetime.fromtimestamp(entry.commit.commit_time, tz)
version_str += "时间: "+dt.strftime('%m-%d %H:%M:%S') + "\n"
version_str += "说明: "+str(entry.commit.message, encoding="utf-8").strip() + "\n"
version_str += "提交作者: '" + str(entry.commit.author)[2:-1] + "'"
break
return version_str
rls_list = get_release_list()
current_tag = get_current_tag()
for rls in rls_list:
if rls['tag_name'] == current_tag:
return rls['name'] + "\n" + rls['body']
return "未知版本"
def get_commit_id_and_time_and_msg() -> str:
@@ -132,15 +213,42 @@ def get_current_commit_id() -> str:
def is_new_version_available() -> bool:
"""检查是否有新版本"""
check_dulwich_closure()
# 从github获取release列表
rls_list = get_release_list()
if rls_list is None:
return False
from dulwich import porcelain
# 获取当前版本
current_tag = get_current_tag()
repo = porcelain.open_repo('.')
fetch_res = porcelain.ls_remote(porcelain.get_remote_repo(repo, "origin")[1])
# 检查是否有新版本
for rls in rls_list:
if rls['tag_name'] == current_tag:
return False
else:
return True
current_commit_id = get_current_commit_id()
latest_commit_id = str(fetch_res[b'HEAD'])[2:-1]
def get_rls_notes() -> list:
"""获取更新日志"""
# 从github获取release列表
rls_list = get_release_list()
if rls_list is None:
return None
return current_commit_id != latest_commit_id
# 获取当前版本
current_tag = get_current_tag()
# 检查是否有新版本
rls_notes = []
for rls in rls_list:
if rls['tag_name'] == current_tag:
break
rls_notes.append(rls['name'])
return rls_notes
if __name__ == "__main__":
update_all()

View File

@@ -1,6 +1,5 @@
requests~=2.28.1
openai~=0.27.0
pip~=22.3.1
openai~=0.27.2
dulwich~=0.21.3
colorlog~=6.6.0
yiri-mirai~=0.2.6.1

1
res/announcement Normal file
View File

@@ -0,0 +1 @@

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"prompt": [
{
"role": "system",
"content": "You are a helpful assistant. 如果我需要帮助,你要说“输入!help获得帮助”"
},
{
"role": "assistant",
"content": "好的我是一个能干的AI助手。 如果你需要帮助,我会说“输入!help获得帮助”"
}
]
}

View File

@@ -1,4 +1,7 @@
{
"说明": "mask将替换敏感词中的每一个字若mask_word值不为空则将敏感词整个替换为mask_word的值",
"mask": "*",
"mask_word": "",
"words": [
"习近平",
"胡锦涛",