Compare commits

...

60 Commits

Author SHA1 Message Date
RockChinQ
14b9f814c7 chore: release v2.6.7 2023-12-09 22:25:44 +08:00
Junyan Qin
b11e5d99b0 Merge pull request #628 from RockChinQ/fix/image-generating
Fix: openai>=1.0时绘图命令不兼容
2023-12-09 22:22:42 +08:00
GitHub Actions
9590718da4 Update override-all.json 2023-12-09 14:17:55 +00:00
RockChinQ
8c2b53cffb fix: openai>=1.0时绘图命令不兼容 2023-12-09 22:17:26 +08:00
Junyan Qin
5a85c073a8 Update README.md 2023-12-08 17:03:16 +08:00
Junyan Qin
2d2fbd0a8b fix: 首次启动时无法创建配置文件 2023-12-08 07:27:23 +00:00
Junyan Qin
1b25a05122 Update README.md 2023-12-06 19:29:31 +08:00
RockChinQ
709cc1140b chore: 发布公告 2023-12-06 19:27:04 +08:00
Junyan Qin
1730962636 Merge pull request #625 from zuo-shi-yun/master
添加看门狗插件
2023-12-03 10:03:35 +08:00
zuo-shi-yun
a1de4f6f7a 添加看门狗插件 2023-12-02 23:58:18 +08:00
Junyan Qin
a5ccda5ed6 doc: 更新 NOTE 和 WARNING 的格式 2023-12-01 02:28:47 +00:00
Junyan Qin
f035e654ba Merge pull request #623 from zuo-shi-yun/master
添加discountAssistant插件
2023-12-01 10:04:49 +08:00
zuo-shi-yun
151d3e9f66 添加discountAssistant插件 2023-11-30 23:53:43 +08:00
Junyan Qin
c79207e197 Merge pull request #618 from RockChinQ/refactor/config-manager
Refactor: 使用 配置管理器 统一管理配置文件
2023-11-27 00:02:52 +08:00
RockChinQ
f9d461d9a1 feat: 移除过时的配置模块处理逻辑 2023-11-27 00:00:22 +08:00
RockChinQ
3e17bbb90f refactor: 适配配置管理器读取方式 2023-11-26 23:58:06 +08:00
RockChinQ
549a7eff7f refactor(qqbot): 适配配置管理器 2023-11-26 23:04:14 +08:00
RockChinQ
db2e366014 feat: 实现配置文件管理器并适配main.py中的引用 2023-11-26 22:46:27 +08:00
RockChinQ
26e4215054 feat: 新的override逻辑 2023-11-26 22:25:54 +08:00
RockChinQ
5f07ff8145 refactor: 启动流程现在异步 2023-11-26 22:19:36 +08:00
GitHub Actions
e396ba4649 Update override-all.json 2023-11-26 13:54:00 +00:00
RockChinQ
d1dff6dedd feat(main.py): 将配置加载流程放到start函数 2023-11-26 21:53:35 +08:00
RockChinQ
419354cb07 feat: 添加用于覆盖率测试的退出代码 2023-11-26 17:42:25 +08:00
RockChinQ
7708eaa82c perf: 为 context.py 中的方法添加类型提示 2023-11-26 17:33:13 +08:00
RockChinQ
9fccf84987 chore: release v2.6.6 2023-11-22 19:20:47 +08:00
Junyan Qin
0f59788184 Merge pull request #610 from RockChinQ/feat/no-reload-after-updating
Feat: 更新后不再自动热重载
2023-11-22 19:19:22 +08:00
RockChinQ
0ad52bcd3f perf: 优化输出文字 2023-11-22 19:17:23 +08:00
RockChinQ
d7d710ec07 feat: 更新后不再自动热重载 2023-11-22 19:08:33 +08:00
GitHub Actions
75a9a3e9af Update override-all.json 2023-11-22 11:06:11 +00:00
RockChinQ
70503bedb7 feat: 现在默认关闭强制延迟 2023-11-22 19:05:51 +08:00
Junyan Qin
7890eac3f8 Merge pull request #608 from RockChinQ/fix/reverse-proxy-invalid
Fix: 反向代理设置无效
2023-11-21 15:45:49 +08:00
RockChinQ
e15f3595b3 fix: 反向代理设置无效 2023-11-21 15:44:07 +08:00
RockChinQ
eebd6a6ba3 chore: release v2.6.5 2023-11-14 23:16:02 +08:00
Junyan Qin
0407f3e4ac Merge pull request #599 from RockChinQ/refactor/modern-openai-api-style
Refactor: 修改 情景预设 置入风格
2023-11-14 21:36:25 +08:00
RockChinQ
5abca84437 debug: 添加请求参数输出 2023-11-14 21:35:02 +08:00
GitHub Actions
d2776cc1e6 Update override-all.json 2023-11-14 13:06:22 +00:00
RockChinQ
9fe0ee2b77 refactor: 使用system role置入default prompt 2023-11-14 21:06:00 +08:00
Junyan Qin
b68daac323 Merge pull request #598 from RockChinQ/perf/import-style
Refactor: 修改引入风格
2023-11-13 22:00:27 +08:00
RockChinQ
665de5dc43 refactor: 修改引入风格 2023-11-13 21:59:23 +08:00
RockChinQ
e3b280758c chore: 发布更新公告 2023-11-13 18:03:26 +08:00
RockChinQ
374ae25d9c fix: 启动时自动解决依赖后不正确的异常处理 2023-11-12 23:16:09 +08:00
RockChinQ
c86529ac99 feat: 启动时不再自动更新websockets依赖 2023-11-12 22:59:49 +08:00
RockChinQ
6309f1fb78 chore(deps): 更换为自有分支yiri-mirai-rc 2023-11-12 20:31:07 +08:00
RockChinQ
c246fb6d8e chore: release v2.6.4 2023-11-12 14:42:48 +08:00
RockChinQ
ec6c041bcf ci(Dockerfile): 修复依赖安装问题 2023-11-12 14:42:07 +08:00
RockChinQ
2da5a9f3c7 ci(Dockerfile): 显式更新httpcore httpx和openai库 2023-11-12 14:18:42 +08:00
Junyan Qin
4e0df52d7c Merge pull request #592 from RockChinQ/fix/plugin-downloading
Feat: 通过 GitHub API 进行插件安装和更新
2023-11-12 14:07:52 +08:00
RockChinQ
71b8bf13e4 fix: 插件加载bug 2023-11-12 13:52:04 +08:00
RockChinQ
a8b1e6ce91 ci: test 2023-11-12 12:05:04 +08:00
RockChinQ
1419d7611d ci(cmdpriv): 本地测试通过 2023-11-12 12:03:52 +08:00
RockChinQ
89c83ebf20 fix: 错误的判空变量 2023-11-12 11:30:10 +08:00
RockChinQ
76d7db88ea feat: 基于元数据记录的插件更新实现 2023-11-11 23:17:28 +08:00
RockChinQ
67a208bc90 feat: 添加插件元数据操作模块 2023-11-11 17:38:52 +08:00
RockChinQ
acbd55ded2 feat: 插件安装改为直接下载源码 2023-11-10 23:01:56 +08:00
Junyan Qin
11a240a6d1 Merge pull request #591 from RockChinQ/feat/new-model-names
Feat: 更新模型索引
2023-11-10 21:23:22 +08:00
RockChinQ
97c85abbe7 feat: 更新模型索引 2023-11-10 21:16:33 +08:00
RockChinQ
06a0cd2a3d chore: 发布兼容性问题公告 2023-11-10 12:20:29 +08:00
GitHub Actions
572b215df8 Update override-all.json 2023-11-10 04:04:45 +00:00
RockChinQ
2c542bf412 chore: 不再默认在启动时升级依赖库 2023-11-10 12:04:25 +08:00
RockChinQ
1576ba7a01 chore: release v2.6.3 2023-11-10 12:01:20 +08:00
75 changed files with 1120 additions and 788 deletions

View File

@@ -10,6 +10,6 @@ updates:
schedule: schedule:
interval: "weekly" interval: "weekly"
allow: allow:
- dependency-name: "yiri-mirai" - dependency-name: "yiri-mirai-rc"
- dependency-name: "dulwich" - dependency-name: "dulwich"
- dependency-name: "openai" - dependency-name: "openai"

View File

@@ -21,12 +21,12 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.x python-version: 3.10.13
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade yiri-mirai-rc openai>=1.0.0 colorlog func_timeout dulwich Pillow CallingGPT tiktoken
python -m pip install --upgrade yiri-mirai openai colorlog func_timeout dulwich Pillow CallingGPT tiktoken python -m pip install -U openai>=1.0.0
- name: Copy Scripts - name: Copy Scripts
run: | run: |

View File

@@ -29,7 +29,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
# 在此处添加您的项目所需的其他依赖
- name: Copy Scripts - name: Copy Scripts
run: | run: |

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
config.py /config.py
.idea/ .idea/
__pycache__/ __pycache__/
database.db database.db

View File

@@ -1,12 +1,13 @@
FROM python:3.10.13-alpine3.18 FROM python:3.10.13-bullseye
WORKDIR /QChatGPT WORKDIR /QChatGPT
COPY . /QChatGPT/ COPY . /QChatGPT/
RUN ls RUN ls
RUN pip install -r requirements.txt RUN python -m pip install -r requirements.txt && \
RUN pip install -U websockets==10.0 python -m pip install -U websockets==10.0 && \
python -m pip install -U httpcore httpx openai
# 生成配置文件 # 生成配置文件
RUN python main.py RUN python main.py

View File

@@ -7,9 +7,6 @@
# QChatGPT # QChatGPT
<!-- 高稳定性/持续迭代/架构清晰/支持插件/高可自定义的 ChatGPT QQ机器人框架 -->
<!-- “当然下面是一个使用Java编写的快速排序算法的示例代码” -->
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/QChatGPT)](https://github.com/RockChinQ/QChatGPT/releases/latest) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/QChatGPT)](https://github.com/RockChinQ/QChatGPT/releases/latest)
<a href="https://hub.docker.com/repository/docker/rockchin/qchatgpt"> <a href="https://hub.docker.com/repository/docker/rockchin/qchatgpt">
<img src="https://img.shields.io/docker/pulls/rockchin/qchatgpt?color=blue" alt="docker pull"> <img src="https://img.shields.io/docker/pulls/rockchin/qchatgpt?color=blue" alt="docker pull">
@@ -35,6 +32,7 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Linux%E9%83%A8%E7%BD%B2%E8%A7%86%E9%A2%91-208647"> <img alt="Static Badge" src="https://img.shields.io/badge/Linux%E9%83%A8%E7%BD%B2%E8%A7%86%E9%A2%91-208647">
</a> </a>
<blockquote> 🥳 QChatGPT 一周年啦,感谢大家的支持!欢迎前往<a href="https://github.com/RockChinQ/QChatGPT/discussions/627">讨论</a>。</blockquote>
<details> <details>
<summary>回复效果演示(带有联网插件)</summary> <summary>回复效果演示(带有联网插件)</summary>
@@ -42,7 +40,7 @@
</details> </details>
</div> </div>
> **NOTE** > [!NOTE]
> 2023/9/13 现已支持通过[One API](https://github.com/songquanpeng/one-api)接入 Azure、Anthropic Claude、Google PaLM 2、智谱 ChatGLM、百度文心一言、讯飞星火认知、阿里通义千问以及 360 智脑等模型,欢迎测试并反馈。 > 2023/9/13 现已支持通过[One API](https://github.com/songquanpeng/one-api)接入 Azure、Anthropic Claude、Google PaLM 2、智谱 ChatGLM、百度文心一言、讯飞星火认知、阿里通义千问以及 360 智脑等模型,欢迎测试并反馈。
> 2023/8/29 [逆向库插件](https://github.com/RockChinQ/revLibs)已支持 gpt4free > 2023/8/29 [逆向库插件](https://github.com/RockChinQ/revLibs)已支持 gpt4free
> 2023/8/14 [逆向库插件](https://github.com/RockChinQ/revLibs)已支持Claude和Bard > 2023/8/14 [逆向库插件](https://github.com/RockChinQ/revLibs)已支持Claude和Bard
@@ -197,7 +195,7 @@
</summary> </summary>
> **NOTE** > **NOTE**
> - 部署过程中遇到任何问题,请先在[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里进行搜索
> - QChatGPT需要Python版本>=3.9 > - QChatGPT需要Python版本>=3.9
> - 官方群和社区群群号请见文档顶部 > - 官方群和社区群群号请见文档顶部
@@ -278,7 +276,7 @@ cd QChatGPT
2. 安装依赖 2. 安装依赖
```bash ```bash
pip3 install requests yiri-mirai openai colorlog func_timeout dulwich Pillow nakuru-project-idk CallingGPT tiktoken pip3 install requests -r requirements.txt
``` ```
3. 运行一次主程序,生成配置文件 3. 运行一次主程序,生成配置文件
@@ -346,6 +344,8 @@ python3 main.py
- [oliverkirk-sudo/QChatWeather](https://github.com/oliverkirk-sudo/QChatWeather) - 生成好看的天气图片,基于和风天气 - [oliverkirk-sudo/QChatWeather](https://github.com/oliverkirk-sudo/QChatWeather) - 生成好看的天气图片,基于和风天气
- [oliverkirk-sudo/QChatMarkdown](https://github.com/oliverkirk-sudo/QChatMarkdown) - 将机器人输出的markdown转换为图片基于[playwright](https://playwright.dev/python/docs/intro) - [oliverkirk-sudo/QChatMarkdown](https://github.com/oliverkirk-sudo/QChatMarkdown) - 将机器人输出的markdown转换为图片基于[playwright](https://playwright.dev/python/docs/intro)
- [ruuuux/WikipediaSearch](https://github.com/ruuuux/WikipediaSearch) - Wikipedia 搜索插件 - [ruuuux/WikipediaSearch](https://github.com/ruuuux/WikipediaSearch) - Wikipedia 搜索插件
- [zuo-shi-yun/discountAssistant](https://github.com/zuo-shi-yun/discountAssistant) - 自动筛选并发送羊毛群内的优惠券
- [zuo-shi-yun/Gatekeeper ](https://github.com/zuo-shi-yun/Gatekeeper) - QChatGPT的看门狗包含黑白名单、临时用户机制
</details> </details>

View File

@@ -49,7 +49,7 @@ English | [简体中文](README.md)
Install this [plugin](https://github.com/RockChinQ/Switcher) to switch between different models. Install this [plugin](https://github.com/RockChinQ/Switcher) to switch between different models.
## ✅Function Points ## ✅Features
<details> <details>
<summary>Details</summary> <summary>Details</summary>
@@ -141,7 +141,7 @@ cd QChatGPT
2. Install dependencies 2. Install dependencies
```bash ```bash
pip3 install requests yiri-mirai openai colorlog func_timeout dulwich Pillow nakuru-project-idk pip3 install requests yiri-mirai-rc openai colorlog func_timeout dulwich Pillow nakuru-project-idk
``` ```
3. Generate `config.py` 3. Generate `config.py`

View File

@@ -114,7 +114,7 @@ admin_qq = 0
# #
# 还可以加载文件中的预设文字使用方法请查看https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E9%A2%84%E8%AE%BE%E6%96%87%E5%AD%97 # 还可以加载文件中的预设文字使用方法请查看https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E9%A2%84%E8%AE%BE%E6%96%87%E5%AD%97
default_prompt = { default_prompt = {
"default": "如果之后想获取帮助,请你说“输入!help获取帮助”", "default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”",
} }
# 情景预设格式 # 情景预设格式
@@ -208,38 +208,65 @@ auto_reset = True
# OpenAI补全API的参数 # OpenAI补全API的参数
# 请在下方填写模型,程序自动选择接口 # 请在下方填写模型,程序自动选择接口
# 模型文档https://platform.openai.com/docs/models
# 现已支持的模型有: # 现已支持的模型有:
# #
# 'gpt-4' # ChatCompletions 接口:
# 'gpt-4-0613' # # GPT 4 系列
# 'gpt-4-32k' # "gpt-4-1106-preview",
# 'gpt-4-32k-0613' # "gpt-4-vision-preview",
# 'gpt-3.5-turbo' # "gpt-4",
# 'gpt-3.5-turbo-16k' # "gpt-4-32k",
# 'gpt-3.5-turbo-0613' # "gpt-4-0613",
# 'gpt-3.5-turbo-16k-0613' # "gpt-4-32k-0613",
# 'text-davinci-003' # "gpt-4-0314", # legacy
# 'text-davinci-002' # "gpt-4-32k-0314", # legacy
# 'code-davinci-002' # # GPT 3.5 系列
# 'code-cushman-001' # "gpt-3.5-turbo-1106",
# 'text-curie-001' # "gpt-3.5-turbo",
# 'text-babbage-001' # "gpt-3.5-turbo-16k",
# 'text-ada-001' # "gpt-3.5-turbo-0613", # legacy
# "gpt-3.5-turbo-16k-0613", # legacy
# "gpt-3.5-turbo-0301", # legacy
#
# Completions接口
# "text-davinci-003", # legacy
# "text-davinci-002", # legacy
# "code-davinci-002", # legacy
# "code-cushman-001", # legacy
# "text-curie-001", # legacy
# "text-babbage-001", # legacy
# "text-ada-001", # legacy
# "gpt-3.5-turbo-instruct",
# #
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create # 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create
# 请将内容修改到config.py中请勿修改config-template.py # 请将内容修改到config.py中请勿修改config-template.py
# #
# 支持通过 One API 接入多种模型请在上方的openai_config中设置One API的代理地址 # 支持通过 One API 接入多种模型请在上方的openai_config中设置One API的代理地址
# 并在此填写您要使用的模型名称详细请参考https://github.com/songquanpeng/one-api # 并在此填写您要使用的模型名称详细请参考https://github.com/songquanpeng/one-api
#
# 支持的 One API 模型:
# "SparkDesk",
# "chatglm_pro",
# "chatglm_std",
# "chatglm_lite",
# "qwen-v1",
# "qwen-plus-v1",
# "ERNIE-Bot",
# "ERNIE-Bot-turbo",
completion_api_params = { completion_api_params = {
"model": "gpt-3.5-turbo", "model": "gpt-3.5-turbo",
"temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1] "temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1]
} }
# OpenAI的Image API的参数 # OpenAI的Image API的参数
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/images/create # 具体请查看OpenAI的文档: https://platform.openai.com/docs/api-reference/images/create
image_api_params = { image_api_params = {
"size": "256x256", # 图片尺寸支持256x256, 512x512, 1024x1024 "model": "dall-e-2", # 默认使用 dall-e-2 模型,也可以改为 dall-e-3
# 图片尺寸
# dall-e-2 模型支持 256x256, 512x512, 1024x1024
# dall-e-3 模型支持 1024x1024, 1792x1024, 1024x1792
"size": "256x256",
} }
# 跟踪函数调用 # 跟踪函数调用
@@ -268,7 +295,7 @@ show_prefix = False
# 当此次消息处理时间低于此秒数时,将会强制延迟至此秒数 # 当此次消息处理时间低于此秒数时,将会强制延迟至此秒数
# 例如:[1.5, 3]则每次处理时会随机取一个1.5-3秒的随机数若处理时间低于此随机数则强制延迟至此随机秒数 # 例如:[1.5, 3]则每次处理时会随机取一个1.5-3秒的随机数若处理时间低于此随机数则强制延迟至此随机秒数
# 若您不需要此功能请将force_delay_range设置为[0, 0] # 若您不需要此功能请将force_delay_range设置为[0, 0]
force_delay_range = [1.5, 3] force_delay_range = [0, 0]
# 应用长消息处理策略的阈值 # 应用长消息处理策略的阈值
# 当回复消息长度超过此值时,将使用长消息处理策略 # 当回复消息长度超过此值时,将使用长消息处理策略
@@ -299,19 +326,6 @@ retry_times = 3
# 设置为False时向用户及管理员发送错误详细信息 # 设置为False时向用户及管理员发送错误详细信息
hide_exce_info_to_user = False hide_exce_info_to_user = False
# 线程池相关配置
# 该参数决定机器人可以同时处理几个人的消息,超出线程池数量的请求会被阻塞,不会被丢弃
# 如果你不清楚该参数的意义,请不要更改
# 程序运行本身线程池,无代码层面修改请勿更改
sys_pool_num = 8
# 执行管理员请求和指令的线程池并行线程数量,一般和管理员数量相等
admin_pool_num = 4
# 执行用户请求和指令的线程池并行线程数量
# 如需要更高的并发,可以增大该值
user_pool_num = 8
# 每个会话的过期时间,单位为秒 # 每个会话的过期时间,单位为秒
# 默认值20分钟 # 默认值20分钟
session_expire_time = 1200 session_expire_time = 1200
@@ -351,7 +365,7 @@ rate_limitation = {
rate_limit_strategy = "drop" rate_limit_strategy = "drop"
# 是否在启动时进行依赖库更新 # 是否在启动时进行依赖库更新
upgrade_dependencies = True upgrade_dependencies = False
# 是否上报统计信息 # 是否上报统计信息
# 用于统计机器人的使用情况,不会收集任何用户信息 # 用于统计机器人的使用情况,不会收集任何用户信息

220
main.py
View File

@@ -8,10 +8,56 @@ import time
import logging import logging
import sys import sys
import traceback import traceback
import asyncio
sys.path.append(".") sys.path.append(".")
def check_file():
# 检查是否有banlist.py,如果没有就把banlist-template.py复制一份
if not os.path.exists('banlist.py'):
shutil.copy('res/templates/banlist-template.py', 'banlist.py')
# 检查是否有sensitive.json
if not os.path.exists("sensitive.json"):
shutil.copy("res/templates/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")
# 检查cmdpriv.json
if not os.path.exists("cmdpriv.json"):
shutil.copy("res/templates/cmdpriv-template.json", "cmdpriv.json")
# 检查tips_custom
if not os.path.exists("tips.py"):
shutil.copy("tips-custom-template.py", "tips.py")
# 检查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 not os.path.exists('config.py'):
shutil.copy('config-template.py', 'config.py')
print('请先在config.py中填写配置')
sys.exit(0)
# 初始化相关文件
check_file()
from pkg.utils.log import init_runtime_log_file, reset_logging from pkg.utils.log import init_runtime_log_file, reset_logging
from pkg.config import manager as config_mgr
from pkg.config.impls import pymodule as pymodule_cfg
try: try:
import colorlog import colorlog
@@ -20,7 +66,6 @@ except ImportError:
import pkg.utils.pkgmgr as pkgmgr import pkg.utils.pkgmgr as pkgmgr
try: try:
pkgmgr.install_requirements("requirements.txt") pkgmgr.install_requirements("requirements.txt")
pkgmgr.install_upgrade("websockets")
import colorlog import colorlog
except ImportError: except ImportError:
print("依赖不满足,请查看 https://github.com/RockChinQ/qcg-installer/issues/15") print("依赖不满足,请查看 https://github.com/RockChinQ/qcg-installer/issues/15")
@@ -55,15 +100,15 @@ def ensure_dependencies():
known_exception_caught = False known_exception_caught = False
def override_config(): def override_config_manager():
import config config = pkg.utils.context.get_config_manager().data
# 检查override.json覆盖
if os.path.exists("override.json") and use_override: if os.path.exists("override.json") and use_override:
override_json = json.load(open("override.json", "r", encoding="utf-8")) override_json = json.load(open("override.json", "r", encoding="utf-8"))
overrided = [] overrided = []
for key in override_json: for key in override_json:
if hasattr(config, key): if key in config:
setattr(config, key, override_json[key]) config[key] = override_json[key]
# logging.info("覆写配置[{}]为[{}]".format(key, override_json[key])) # logging.info("覆写配置[{}]为[{}]".format(key, override_json[key]))
overrided.append(key) overrided.append(key)
else: else:
@@ -72,36 +117,6 @@ def override_config():
logging.info("已根据override.json覆写配置项: {}".format(", ".join(overrided))) logging.info("已根据override.json覆写配置项: {}".format(", ".join(overrided)))
# 临时函数用于加载config和上下文未来统一放在config类
def load_config():
logging.info("检查config模块完整性.")
# 完整性校验
non_exist_keys = []
is_integrity = True
config_template = importlib.import_module('config-template')
config = importlib.import_module('config')
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))
non_exist_keys.append(key)
is_integrity = False
if not is_integrity:
logging.warning("以下配置字段不存在: {}".format(", ".join(non_exist_keys)))
# 检查override.json覆盖
override_config()
if not is_integrity:
logging.warning("以上不存在的配置已被设为默认值您可以依据config-template.py检查config.py将在3秒后继续启动... ")
time.sleep(3)
# 存进上下文
pkg.utils.context.set_config(config)
def complete_tips(): def complete_tips():
"""根据tips-custom-template模块补全tips模块的属性""" """根据tips-custom-template模块补全tips模块的属性"""
non_exist_keys = [] non_exist_keys = []
@@ -124,17 +139,29 @@ def complete_tips():
time.sleep(3) time.sleep(3)
def start(first_time_init=False): async def start_process(first_time_init=False):
"""启动流程reload之后会被执行""" """启动流程reload之后会被执行"""
global known_exception_caught global known_exception_caught
import pkg.utils.context import pkg.utils.context
config = pkg.utils.context.get_config() # 加载配置
cfg_inst: pymodule_cfg.PythonModuleConfigFile = pymodule_cfg.PythonModuleConfigFile(
'config.py',
'config-template.py'
)
await config_mgr.ConfigManager(cfg_inst).load_config()
override_config_manager()
# 检查tips模块
complete_tips()
cfg = pkg.utils.context.get_config_manager().data
# 更新openai库到最新版本 # 更新openai库到最新版本
if not hasattr(config, 'upgrade_dependencies') or config.upgrade_dependencies: if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']:
print("正在更新依赖库,请等待...") print("正在更新依赖库,请等待...")
if not hasattr(config, 'upgrade_dependencies'): if 'upgrade_dependencies' not in cfg:
print("这个操作不是必须的,如果不想更新,请在config.py中添加upgrade_dependencies=False") print("这个操作不是必须的,如果不想更新,请在config.py中添加upgrade_dependencies=False")
else: else:
print("这个操作不是必须的,如果不想更新,请在config.py中将upgrade_dependencies设置为False") print("这个操作不是必须的,如果不想更新,请在config.py中将upgrade_dependencies设置为False")
@@ -143,6 +170,10 @@ def start(first_time_init=False):
except Exception as e: except Exception as e:
print("更新openai库失败:{}, 请忽略或自行更新".format(e)) print("更新openai库失败:{}, 请忽略或自行更新".format(e))
# 初始化文字转图片
from pkg.utils import text2img
text2img.initialize()
known_exception_caught = False known_exception_caught = False
try: try:
try: try:
@@ -151,11 +182,11 @@ def start(first_time_init=False):
pkg.utils.context.context['logger_handler'] = sh pkg.utils.context.context['logger_handler'] = sh
# 检查是否设置了管理员 # 检查是否设置了管理员
if not (hasattr(config, 'admin_qq') and config.admin_qq != 0): if cfg['admin_qq'] == 0:
# logging.warning("未设置管理员QQ,管理员权限指令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段") # logging.warning("未设置管理员QQ,管理员权限指令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
while True: while True:
try: try:
config.admin_qq = int(input("未设置管理员QQ,管理员权限指令及运行告警将无法使用,请输入管理员QQ号: ")) cfg['admin_qq'] = int(input("未设置管理员QQ,管理员权限指令及运行告警将无法使用,请输入管理员QQ号: "))
# 写入到文件 # 写入到文件
# 读取文件 # 读取文件
@@ -163,7 +194,7 @@ def start(first_time_init=False):
with open("config.py", "r", encoding="utf-8") as f: with open("config.py", "r", encoding="utf-8") as f:
config_file_str = f.read() config_file_str = f.read()
# 替换 # 替换
config_file_str = config_file_str.replace("admin_qq = 0", "admin_qq = " + str(config.admin_qq)) config_file_str = config_file_str.replace("admin_qq = 0", "admin_qq = " + str(cfg['admin_qq']))
# 写入 # 写入
with open("config.py", "w", encoding="utf-8") as f: with open("config.py", "w", encoding="utf-8") as f:
f.write(config_file_str) f.write(config_file_str)
@@ -192,22 +223,23 @@ def start(first_time_init=False):
# 配置OpenAI proxy # 配置OpenAI proxy
import openai import openai
openai.proxies = None # 先重置因为重载后可能需要清除proxy openai.proxies = None # 先重置因为重载后可能需要清除proxy
if "http_proxy" in config.openai_config and config.openai_config["http_proxy"] is not None: if "http_proxy" in cfg['openai_config'] and cfg['openai_config']["http_proxy"] is not None:
openai.proxies = { openai.proxies = {
"http": config.openai_config["http_proxy"], "http": cfg['openai_config']["http_proxy"],
"https": config.openai_config["http_proxy"] "https": cfg['openai_config']["http_proxy"]
} }
# 配置openai api_base # 配置openai api_base
if "reverse_proxy" in config.openai_config and config.openai_config["reverse_proxy"] is not None: if "reverse_proxy" in cfg['openai_config'] and cfg['openai_config']["reverse_proxy"] is not None:
openai.base_url = config.openai_config["reverse_proxy"] logging.debug("设置反向代理: "+cfg['openai_config']['reverse_proxy'])
openai.base_url = cfg['openai_config']["reverse_proxy"]
# 主启动流程 # 主启动流程
database = pkg.database.manager.DatabaseManager() database = pkg.database.manager.DatabaseManager()
database.initialize_database() database.initialize_database()
openai_interact = pkg.openai.manager.OpenAIInteract(config.openai_config['api_key']) openai_interact = pkg.openai.manager.OpenAIInteract(cfg['openai_config']['api_key'])
# 加载所有未超时的session # 加载所有未超时的session
pkg.openai.session.load_sessions() pkg.openai.session.load_sessions()
@@ -296,13 +328,12 @@ def start(first_time_init=False):
if first_time_init: if first_time_init:
if not known_exception_caught: if not known_exception_caught:
import config if cfg['msg_source_adapter'] == "yirimirai":
if config.msg_source_adapter == "yirimirai": logging.info("QQ: {}, MAH: {}".format(cfg['mirai_http_api_config']['qq'], cfg['mirai_http_api_config']['host']+":"+str(cfg['mirai_http_api_config']['port'])))
logging.info("QQ: {}, MAH: {}".format(config.mirai_http_api_config['qq'], config.mirai_http_api_config['host']+":"+str(config.mirai_http_api_config['port'])))
logging.critical('程序启动完成,如长时间未显示 "成功登录到账号xxxxx" ,并且不回复消息,解决办法(请勿到群里问): ' logging.critical('程序启动完成,如长时间未显示 "成功登录到账号xxxxx" ,并且不回复消息,解决办法(请勿到群里问): '
'https://github.com/RockChinQ/QChatGPT/issues/37') 'https://github.com/RockChinQ/QChatGPT/issues/37')
elif config.msg_source_adapter == 'nakuru': elif cfg['msg_source_adapter'] == 'nakuru':
logging.info("host: {}, port: {}, http_port: {}".format(config.nakuru_config['host'], config.nakuru_config['port'], config.nakuru_config['http_port'])) logging.info("host: {}, port: {}, http_port: {}".format(cfg['nakuru_config']['host'], cfg['nakuru_config']['port'], cfg['nakuru_config']['http_port']))
logging.critical('程序启动完成,如长时间未显示 "Protocol: connected" ,并且不回复消息,请检查config.py中的nakuru_config是否正确') logging.critical('程序启动完成,如长时间未显示 "Protocol: connected" ,并且不回复消息,请检查config.py中的nakuru_config是否正确')
else: else:
sys.exit(1) sys.exit(1)
@@ -310,7 +341,7 @@ def start(first_time_init=False):
logging.info('热重载完成') logging.info('热重载完成')
# 发送赞赏码 # 发送赞赏码
if config.encourage_sponsor_at_start \ if cfg['encourage_sponsor_at_start'] \
and pkg.utils.context.get_openai_manager().audit_mgr.get_total_text_length() >= 2048: and pkg.utils.context.get_openai_manager().audit_mgr.get_total_text_length() >= 2048:
logging.info("发送赞赏码") logging.info("发送赞赏码")
@@ -368,70 +399,22 @@ def stop():
raise e raise e
def check_file():
# 检查是否有banlist.py,如果没有就把banlist-template.py复制一份
if not os.path.exists('banlist.py'):
shutil.copy('res/templates/banlist-template.py', 'banlist.py')
# 检查是否有sensitive.json
if not os.path.exists("sensitive.json"):
shutil.copy("res/templates/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")
# 检查cmdpriv.json
if not os.path.exists("cmdpriv.json"):
shutil.copy("res/templates/cmdpriv-template.json", "cmdpriv.json")
# 检查tips_custom
if not os.path.exists("tips.py"):
shutil.copy("tips-custom-template.py", "tips.py")
# 检查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 not os.path.exists('config.py'):
shutil.copy('config-template.py', 'config.py')
print('请先在config.py中填写配置')
sys.exit(0)
def main(): def main():
global use_override global use_override
# 检查是否携带了 --override 或 -r 参数 # 检查是否携带了 --override 或 -r 参数
if '--override' in sys.argv or '-r' in sys.argv: if '--override' in sys.argv or '-r' in sys.argv:
use_override = True use_override = True
# 初始化相关文件
check_file()
# 初始化logging # 初始化logging
init_runtime_log_file() init_runtime_log_file()
pkg.utils.context.context['logger_handler'] = reset_logging() pkg.utils.context.context['logger_handler'] = reset_logging()
# 加载配置
load_config()
config = pkg.utils.context.get_config()
# 检查tips模块
complete_tips()
# 配置线程池 # 配置线程池
from pkg.utils import ThreadCtl from pkg.utils import ThreadCtl
thread_ctl = ThreadCtl( thread_ctl = ThreadCtl(
sys_pool_num=config.sys_pool_num, sys_pool_num=8,
admin_pool_num=config.admin_pool_num, admin_pool_num=4,
user_pool_num=config.user_pool_num user_pool_num=8
) )
# 存进上下文 # 存进上下文
pkg.utils.context.set_thread_ctl(thread_ctl) pkg.utils.context.set_thread_ctl(thread_ctl)
@@ -450,9 +433,11 @@ def main():
# 关闭urllib的http警告 # 关闭urllib的http警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
def run_wrapper():
asyncio.run(start_process(True))
pkg.utils.context.get_thread_ctl().submit_sys_task( pkg.utils.context.get_thread_ctl().submit_sys_task(
start, run_wrapper
True
) )
# 主线程循环 # 主线程循环
@@ -462,12 +447,19 @@ def main():
except: except:
stop() stop()
pkg.utils.context.get_thread_ctl().shutdown() pkg.utils.context.get_thread_ctl().shutdown()
import platform
if platform.system() == 'Windows': launch_args = sys.argv.copy()
cmd = "taskkill /F /PID {}".format(os.getpid())
elif platform.system() in ['Linux', 'Darwin']: if "--cov-report" not in launch_args:
cmd = "kill -9 {}".format(os.getpid()) import platform
os.system(cmd) if platform.system() == 'Windows':
cmd = "taskkill /F /PID {}".format(os.getpid())
elif platform.system() in ['Linux', 'Darwin']:
cmd = "kill -9 {}".format(os.getpid())
os.system(cmd)
else:
print("正常退出以生成覆盖率报告")
sys.exit(0)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -24,7 +24,7 @@
"switch_strategy": "active", "switch_strategy": "active",
"admin_qq": 0, "admin_qq": 0,
"default_prompt": { "default_prompt": {
"default": "如果之后想获取帮助,请你说“输入!help获取帮助”" "default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”"
}, },
"preset_mode": "normal", "preset_mode": "normal",
"response_rules": { "response_rules": {
@@ -60,6 +60,7 @@
"temperature": 0.9 "temperature": 0.9
}, },
"image_api_params": { "image_api_params": {
"model": "dall-e-2",
"size": "256x256" "size": "256x256"
}, },
"trace_function_calls": false, "trace_function_calls": false,
@@ -69,8 +70,8 @@
"process_message_timeout": 120, "process_message_timeout": 120,
"show_prefix": false, "show_prefix": false,
"force_delay_range": [ "force_delay_range": [
1.5, 0,
3 0
], ],
"blob_message_threshold": 256, "blob_message_threshold": 256,
"blob_message_strategy": "forward", "blob_message_strategy": "forward",
@@ -78,15 +79,12 @@
"font_path": "", "font_path": "",
"retry_times": 3, "retry_times": 3,
"hide_exce_info_to_user": false, "hide_exce_info_to_user": false,
"sys_pool_num": 8,
"admin_pool_num": 4,
"user_pool_num": 8,
"session_expire_time": 1200, "session_expire_time": 1200,
"rate_limitation": { "rate_limitation": {
"default": 60 "default": 60
}, },
"rate_limit_strategy": "drop", "rate_limit_strategy": "drop",
"upgrade_dependencies": true, "upgrade_dependencies": false,
"report_usage": true, "report_usage": true,
"logging_level": 20 "logging_level": 20
} }

View File

@@ -9,8 +9,8 @@ import threading
import requests import requests
import pkg.utils.context from ..utils import context
import pkg.utils.updater from ..utils import updater
class DataGatherer: class DataGatherer:
@@ -33,7 +33,7 @@ class DataGatherer:
def __init__(self): def __init__(self):
self.load_from_db() self.load_from_db()
try: try:
self.version_str = pkg.utils.updater.get_current_tag() # 从updater模块获取版本号 self.version_str = updater.get_current_tag() # 从updater模块获取版本号
except: except:
pass pass
@@ -47,10 +47,10 @@ class DataGatherer:
def thread_func(): def thread_func():
try: try:
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if not config.report_usage: if not config['report_usage']:
return return
res = requests.get("http://reports.rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}&msg_source={}".format(subservice_name, self.version_str, count, config.msg_source_adapter)) res = requests.get("http://reports.rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}&msg_source={}".format(subservice_name, self.version_str, count, config['msg_source_adapter']))
if res.status_code != 200 or res.text != "ok": if res.status_code != 200 or res.text != "ok":
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text)) logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
except: except:
@@ -64,7 +64,7 @@ class DataGatherer:
def report_text_model_usage(self, model, total_tokens): 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进行储存 key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5() # 以key的md5进行储存
if key_md5 not in self.usage: if key_md5 not in self.usage:
self.usage[key_md5] = {} self.usage[key_md5] = {}
@@ -84,7 +84,7 @@ class DataGatherer:
def report_image_model_usage(self, size): def report_image_model_usage(self, size):
"""调用方报告图片模型请求图片使用量""" """调用方报告图片模型请求图片使用量"""
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5() key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5()
if key_md5 not in self.usage: if key_md5 not in self.usage:
self.usage[key_md5] = {} self.usage[key_md5] = {}
@@ -131,9 +131,9 @@ class DataGatherer:
return total return total
def dump_to_db(self): def dump_to_db(self):
pkg.utils.context.get_database_manager().dump_usage_json(self.usage) context.get_database_manager().dump_usage_json(self.usage)
def load_from_db(self): def load_from_db(self):
json_str = pkg.utils.context.get_database_manager().load_usage_json() json_str = context.get_database_manager().load_usage_json()
if json_str is not None: if json_str is not None:
self.usage = json.loads(json_str) self.usage = json.loads(json_str)

0
pkg/config/__init__.py Normal file
View File

View File

@@ -0,0 +1,62 @@
import os
import shutil
import importlib
import logging
from .. import model as file_model
class PythonModuleConfigFile(file_model.ConfigFile):
"""Python模块配置文件"""
config_file_name: str = None
"""配置文件名"""
template_file_name: str = None
"""模板文件名"""
def __init__(self, config_file_name: str, template_file_name: str) -> None:
self.config_file_name = config_file_name
self.template_file_name = template_file_name
def exists(self) -> bool:
return os.path.exists(self.config_file_name)
async def create(self):
shutil.copyfile(self.template_file_name, self.config_file_name)
async def load(self) -> dict:
module_name = os.path.splitext(os.path.basename(self.config_file_name))[0]
module = importlib.import_module(module_name)
cfg = {}
allowed_types = (int, float, str, bool, list, dict)
for key in dir(module):
if key.startswith('__'):
continue
if not isinstance(getattr(module, key), allowed_types):
continue
cfg[key] = getattr(module, key)
# 从模板模块文件中进行补全
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
module = importlib.import_module(module_name)
for key in dir(module):
if key.startswith('__'):
continue
if not isinstance(getattr(module, key), allowed_types):
continue
if key not in cfg:
cfg[key] = getattr(module, key)
return cfg
async def save(self, data: dict):
logging.warning('Python模块配置文件不支持保存')

23
pkg/config/manager.py Normal file
View File

@@ -0,0 +1,23 @@
from . import model as file_model
from ..utils import context
class ConfigManager:
"""配置文件管理器"""
file: file_model.ConfigFile = None
"""配置文件实例"""
data: dict = None
"""配置数据"""
def __init__(self, cfg_file: file_model.ConfigFile) -> None:
self.file = cfg_file
self.data = {}
context.set_config_manager(self)
async def load_config(self):
self.data = await self.file.load()
async def dump_config(self):
await self.file.save(self.data)

27
pkg/config/model.py Normal file
View File

@@ -0,0 +1,27 @@
import abc
class ConfigFile(metaclass=abc.ABCMeta):
"""配置文件抽象类"""
config_file_name: str = None
"""配置文件名"""
template_file_name: str = None
"""模板文件名"""
@abc.abstractmethod
def exists(self) -> bool:
pass
@abc.abstractmethod
async def create(self):
pass
@abc.abstractmethod
async def load(self) -> dict:
pass
@abc.abstractmethod
async def save(self, data: dict):
pass

View File

@@ -5,11 +5,10 @@ import hashlib
import json import json
import logging import logging
import time import time
from sqlite3 import Cursor
import sqlite3 import sqlite3
import pkg.utils.context from ..utils import context
class DatabaseManager: class DatabaseManager:
@@ -22,7 +21,7 @@ class DatabaseManager:
self.reconnect() self.reconnect()
pkg.utils.context.set_database_manager(self) context.set_database_manager(self)
# 连接到数据库文件 # 连接到数据库文件
def reconnect(self): def reconnect(self):
@@ -33,7 +32,7 @@ class DatabaseManager:
def close(self): def close(self):
self.conn.close() self.conn.close()
def __execute__(self, *args, **kwargs) -> Cursor: def __execute__(self, *args, **kwargs) -> sqlite3.Cursor:
# logging.debug('SQL: {}'.format(sql)) # logging.debug('SQL: {}'.format(sql))
logging.debug('SQL: {}'.format(args)) logging.debug('SQL: {}'.format(args))
c = self.cursor.execute(*args, **kwargs) c = self.cursor.execute(*args, **kwargs)
@@ -145,11 +144,11 @@ class DatabaseManager:
# 从数据库加载还没过期的session数据 # 从数据库加载还没过期的session数据
def load_valid_sessions(self) -> dict: def load_valid_sessions(self) -> dict:
# 从数据库中加载所有还没过期的session # 从数据库中加载所有还没过期的session
config = pkg.utils.context.get_config() config = context.get_config_manager().data
self.__execute__(""" self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts` select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `last_interact_timestamp` > {} from `sessions` where `last_interact_timestamp` > {}
""".format(int(time.time()) - config.session_expire_time)) """.format(int(time.time()) - config['session_expire_time']))
results = self.cursor.fetchall() results = self.cursor.fetchall()
sessions = {} sessions = {}
for result in results: for result in results:

View File

@@ -1,11 +1,11 @@
import openai
from openai.types.chat import chat_completion_message
import json import json
import logging import logging
from .model import RequestBase import openai
from openai.types.chat import chat_completion_message
from ..funcmgr import get_func_schema_list, execute_function, get_func, get_func_schema, ContentFunctionNotFoundError from .model import RequestBase
from .. import funcmgr
class ChatCompletionRequest(RequestBase): class ChatCompletionRequest(RequestBase):
@@ -81,7 +81,7 @@ class ChatCompletionRequest(RequestBase):
"messages": self.messages, "messages": self.messages,
} }
funcs = get_func_schema_list() funcs = funcmgr.get_func_schema_list()
if len(funcs) > 0: if len(funcs) > 0:
args['functions'] = funcs args['functions'] = funcs
@@ -171,7 +171,7 @@ class ChatCompletionRequest(RequestBase):
# 若不是json格式的异常处理 # 若不是json格式的异常处理
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
# 获取函数的参数列表 # 获取函数的参数列表
func_schema = get_func_schema(func_name) func_schema = funcmgr.get_func_schema(func_name)
arguments = { arguments = {
func_schema['parameters']['required'][0]: cp_pending_func_call.arguments func_schema['parameters']['required'][0]: cp_pending_func_call.arguments
@@ -182,7 +182,7 @@ class ChatCompletionRequest(RequestBase):
# 执行函数调用 # 执行函数调用
ret = "" ret = ""
try: try:
ret = execute_function(func_name, arguments) ret = funcmgr.execute_function(func_name, arguments)
logging.info("函数执行完成。") logging.info("函数执行完成。")
except Exception as e: except Exception as e:
@@ -216,6 +216,5 @@ class ChatCompletionRequest(RequestBase):
} }
} }
except ContentFunctionNotFoundError: except funcmgr.ContentFunctionNotFoundError:
raise Exception("没有找到函数: {}".format(func_name)) raise Exception("没有找到函数: {}".format(func_name))

View File

@@ -1,10 +1,10 @@
import openai import openai
from openai.types import completion, completion_choice from openai.types import completion, completion_choice
from .model import RequestBase from . import model
class CompletionRequest(RequestBase): class CompletionRequest(model.RequestBase):
"""调用Completion接口的请求类。 """调用Completion接口的请求类。
调用方可以一直next completion直到finish_reason为stop。 调用方可以一直next completion直到finish_reason为stop。

View File

@@ -1,10 +1,10 @@
# 定义不同接口请求的模型 # 定义不同接口请求的模型
import threading
import asyncio
import logging import logging
import openai import openai
from ...utils import context
class RequestBase: class RequestBase:
@@ -16,19 +16,19 @@ class RequestBase:
raise NotImplementedError raise NotImplementedError
def _next_key(self): def _next_key(self):
import pkg.utils.context as context
switched, name = context.get_openai_manager().key_mgr.auto_switch() switched, name = context.get_openai_manager().key_mgr.auto_switch()
logging.debug("切换api-key: switched={}, name={}".format(switched, name)) logging.debug("切换api-key: switched={}, name={}".format(switched, name))
self.client.api_key = context.get_openai_manager().key_mgr.get_using_key() self.client.api_key = context.get_openai_manager().key_mgr.get_using_key()
def _req(self, **kwargs): def _req(self, **kwargs):
"""处理代理问题""" """处理代理问题"""
import config logging.debug("请求接口参数: %s", str(kwargs))
config = context.get_config_manager().data
ret = self.req_func(**kwargs) ret = self.req_func(**kwargs)
logging.debug("接口请求返回:%s", str(ret)) logging.debug("接口请求返回:%s", str(ret))
if config.switch_strategy == 'active': if config['switch_strategy'] == 'active':
self._next_key() self._next_key()
return ret return ret

View File

@@ -1,9 +1,10 @@
# 多情景预设值管理 # 多情景预设值管理
import json import json
import logging import logging
import config
import os import os
from ..utils import context
# __current__ = "default" # __current__ = "default"
# """当前默认使用的情景预设的名称 # """当前默认使用的情景预设的名称
@@ -16,10 +17,6 @@ import os
# __scenario_from_files__ = {} # __scenario_from_files__ = {}
__universal_first_reply__ = "ok, I'll follow your commands."
"""通用首次回复"""
class ScenarioMode: class ScenarioMode:
"""情景预设模式抽象类""" """情景预设模式抽象类"""
@@ -66,29 +63,24 @@ class NormalScenarioMode(ScenarioMode):
"""普通情景预设模式""" """普通情景预设模式"""
def __init__(self): def __init__(self):
global __universal_first_reply__ config = context.get_config_manager().data
# 加载config中的default_prompt值 # 加载config中的default_prompt值
if type(config.default_prompt) == str: if type(config['default_prompt']) == str:
self.using_prompt_name = "default" self.using_prompt_name = "default"
self.prompts = {"default": [ self.prompts = {"default": [
{ {
"role": "user", "role": "system",
"content": config.default_prompt "content": config['default_prompt']
},{
"role": "assistant",
"content": __universal_first_reply__
} }
]} ]}
elif type(config.default_prompt) == dict: elif type(config['default_prompt']) == dict:
for key in config.default_prompt: for key in config['default_prompt']:
self.prompts[key] = [ self.prompts[key] = [
{ {
"role": "user", "role": "system",
"content": config.default_prompt[key] "content": config['default_prompt'][key]
},{
"role": "assistant",
"content": __universal_first_reply__
} }
] ]
@@ -98,11 +90,8 @@ class NormalScenarioMode(ScenarioMode):
with open(os.path.join("prompts", file), encoding="utf-8") as f: with open(os.path.join("prompts", file), encoding="utf-8") as f:
self.prompts[file] = [ self.prompts[file] = [
{ {
"role": "user", "role": "system",
"content": f.read() "content": f.read()
},{
"role": "assistant",
"content": __universal_first_reply__
} }
] ]
@@ -137,9 +126,9 @@ def register_all():
def mode_inst() -> ScenarioMode: def mode_inst() -> ScenarioMode:
"""获取指定名称的情景预设模式对象""" """获取指定名称的情景预设模式对象"""
import config config = context.get_config_manager().data
if config.preset_mode == "default": if config['preset_mode'] == "default":
config.preset_mode = "normal" config['preset_mode'] = "normal"
return scenario_mode_mapping[config.preset_mode] return scenario_mode_mapping[config['preset_mode']]

View File

@@ -1,8 +1,7 @@
# 封装了function calling的一些支持函数 # 封装了function calling的一些支持函数
import logging import logging
from ..plugin import host
from pkg.plugin import host
class ContentFunctionNotFoundError(Exception): class ContentFunctionNotFoundError(Exception):

View File

@@ -2,8 +2,8 @@
import hashlib import hashlib
import logging import logging
import pkg.plugin.host as plugin_host from ..plugin import host as plugin_host
import pkg.plugin.models as plugin_models from ..plugin import models as plugin_models
class KeysManager: class KeysManager:

View File

@@ -1,13 +1,13 @@
import logging import logging
import openai import openai
from openai.types import images_response
import pkg.openai.keymgr from ..openai import keymgr
import pkg.utils.context from ..utils import context
import pkg.audit.gatherer from ..audit import gatherer
from pkg.openai.modelmgr import select_request_cls from ..openai import modelmgr
from ..openai.api import model as api_model
from pkg.openai.api.model import RequestBase
class OpenAIInteract: class OpenAIInteract:
@@ -16,9 +16,9 @@ class OpenAIInteract:
将文字接口和图片接口封装供调用方使用 将文字接口和图片接口封装供调用方使用
""" """
key_mgr: pkg.openai.keymgr.KeysManager = None key_mgr: keymgr.KeysManager = None
audit_mgr: pkg.audit.gatherer.DataGatherer = None audit_mgr: gatherer.DataGatherer = None
default_image_api_params = { default_image_api_params = {
"size": "256x256", "size": "256x256",
@@ -28,31 +28,32 @@ class OpenAIInteract:
def __init__(self, api_key: str): def __init__(self, api_key: str):
self.key_mgr = pkg.openai.keymgr.KeysManager(api_key) self.key_mgr = keymgr.KeysManager(api_key)
self.audit_mgr = pkg.audit.gatherer.DataGatherer() self.audit_mgr = gatherer.DataGatherer()
# logging.info("文字总使用量:%d", self.audit_mgr.get_total_text_length()) # logging.info("文字总使用量:%d", self.audit_mgr.get_total_text_length())
self.client = openai.Client( self.client = openai.Client(
api_key=self.key_mgr.get_using_key() api_key=self.key_mgr.get_using_key(),
base_url=openai.base_url
) )
pkg.utils.context.set_openai_manager(self) context.set_openai_manager(self)
def request_completion(self, messages: list): def request_completion(self, messages: list):
"""请求补全接口回复= """请求补全接口回复=
""" """
# 选择接口请求类 # 选择接口请求类
config = pkg.utils.context.get_config() config = context.get_config_manager().data
request: RequestBase request: api_model.RequestBase
model: str = config.completion_api_params['model'] model: str = config['completion_api_params']['model']
cp_parmas = config.completion_api_params.copy() cp_parmas = config['completion_api_params'].copy()
del cp_parmas['model'] del cp_parmas['model']
request = select_request_cls(self.client, model, messages, cp_parmas) request = modelmgr.select_request_cls(self.client, model, messages, cp_parmas)
# 请求接口 # 请求接口
for resp in request: for resp in request:
@@ -65,7 +66,7 @@ class OpenAIInteract:
yield resp yield resp
def request_image(self, prompt) -> dict: def request_image(self, prompt) -> images_response.ImagesResponse:
"""请求图片接口回复 """请求图片接口回复
Parameters: Parameters:
@@ -74,10 +75,10 @@ class OpenAIInteract:
Returns: Returns:
dict: 响应 dict: 响应
""" """
config = pkg.utils.context.get_config() config = context.get_config_manager().data
params = config.image_api_params params = config['image_api_params']
response = openai.Image.create( response = self.client.images.generate(
prompt=prompt, prompt=prompt,
n=1, n=1,
**params **params

View File

@@ -8,39 +8,47 @@ Completion - text-davinci-003 等模型
import tiktoken import tiktoken
import openai import openai
from pkg.openai.api.model import RequestBase from ..openai.api import model as api_model
from pkg.openai.api.completion import CompletionRequest from ..openai.api import completion as api_completion
from pkg.openai.api.chat_completion import ChatCompletionRequest from ..openai.api import chat_completion as api_chat_completion
COMPLETION_MODELS = { COMPLETION_MODELS = {
'text-davinci-003', "text-davinci-003", # legacy
'text-davinci-002', "text-davinci-002", # legacy
'code-davinci-002', "code-davinci-002", # legacy
'code-cushman-001', "code-cushman-001", # legacy
'text-curie-001', "text-curie-001", # legacy
'text-babbage-001', "text-babbage-001", # legacy
'text-ada-001', "text-ada-001", # legacy
"gpt-3.5-turbo-instruct",
} }
CHAT_COMPLETION_MODELS = { CHAT_COMPLETION_MODELS = {
'gpt-3.5-turbo', # GPT 4 系列
'gpt-3.5-turbo-16k', "gpt-4-1106-preview",
'gpt-3.5-turbo-0613', "gpt-4-vision-preview",
'gpt-3.5-turbo-16k-0613', "gpt-4",
# 'gpt-3.5-turbo-0301', "gpt-4-32k",
'gpt-4', "gpt-4-0613",
'gpt-4-0613', "gpt-4-32k-0613",
'gpt-4-32k', "gpt-4-0314", # legacy
'gpt-4-32k-0613', "gpt-4-32k-0314", # legacy
# GPT 3.5 系列
"gpt-3.5-turbo-1106",
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-0613", # legacy
"gpt-3.5-turbo-16k-0613", # legacy
"gpt-3.5-turbo-0301", # legacy
# One-API 接入 # One-API 接入
'SparkDesk', "SparkDesk",
'chatglm_pro', "chatglm_pro",
'chatglm_std', "chatglm_std",
'chatglm_lite', "chatglm_lite",
'qwen-v1', "qwen-v1",
'qwen-plus-v1', "qwen-plus-v1",
'ERNIE-Bot', "ERNIE-Bot",
'ERNIE-Bot-turbo', "ERNIE-Bot-turbo",
} }
EDIT_MODELS = { EDIT_MODELS = {
@@ -52,11 +60,11 @@ IMAGE_MODELS = {
} }
def select_request_cls(client: openai.Client, model_name: str, messages: list, args: dict) -> RequestBase: def select_request_cls(client: openai.Client, model_name: str, messages: list, args: dict) -> api_model.RequestBase:
if model_name in CHAT_COMPLETION_MODELS: if model_name in CHAT_COMPLETION_MODELS:
return ChatCompletionRequest(client, model_name, messages, **args) return api_chat_completion.ChatCompletionRequest(client, model_name, messages, **args)
elif model_name in COMPLETION_MODELS: elif model_name in COMPLETION_MODELS:
return CompletionRequest(client, model_name, messages, **args) return api_completion.CompletionRequest(client, model_name, messages, **args)
raise ValueError("不支持模型[{}],请检查配置文件".format(model_name)) raise ValueError("不支持模型[{}],请检查配置文件".format(model_name))

View File

@@ -8,15 +8,13 @@ import threading
import time import time
import json import json
import pkg.openai.manager from ..openai import manager as openai_manager
import pkg.openai.modelmgr from ..openai import modelmgr as openai_modelmgr
import pkg.database.manager from ..database import manager as database_manager
import pkg.utils.context from ..utils import context as context
import pkg.plugin.host as plugin_host from ..plugin import host as plugin_host
import pkg.plugin.models as plugin_models from ..plugin import models as plugin_models
from pkg.openai.modelmgr import count_tokens
# 运行时保存的所有session # 运行时保存的所有session
sessions = {} sessions = {}
@@ -38,11 +36,11 @@ def reset_session_prompt(session_name, prompt):
f.write(prompt) f.write(prompt)
f.close() f.close()
# 生成新数据 # 生成新数据
config = pkg.utils.context.get_config() config = context.get_config_manager().data
prompt = [ prompt = [
{ {
'role': 'system', 'role': 'system',
'content': config.default_prompt['default'] if type(config.default_prompt) == dict else config.default_prompt 'content': config['default_prompt']['default'] if type(config['default_prompt']) == dict else config['default_prompt']
} }
] ]
# 警告 # 警告
@@ -61,7 +59,7 @@ def load_sessions():
global sessions global sessions
db_inst = pkg.utils.context.get_database_manager() db_inst = context.get_database_manager()
session_data = db_inst.load_valid_sessions() session_data = db_inst.load_valid_sessions()
@@ -172,17 +170,17 @@ class Session:
if self.create_timestamp != create_timestamp or self not in sessions.values(): if self.create_timestamp != create_timestamp or self not in sessions.values():
return return
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if int(time.time()) - self.last_interact_timestamp > config.session_expire_time: if int(time.time()) - self.last_interact_timestamp > config['session_expire_time']:
logging.info('session {} 已过期'.format(self.name)) logging.info('session {} 已过期'.format(self.name))
# 触发插件事件 # 触发插件事件
args = { args = {
'session_name': self.name, 'session_name': self.name,
'session': self, 'session': self,
'session_expire_time': config.session_expire_time 'session_expire_time': config['session_expire_time']
} }
event = pkg.plugin.host.emit(plugin_models.SessionExpired, **args) event = plugin_host.emit(plugin_models.SessionExpired, **args)
if event.is_prevented_default(): if event.is_prevented_default():
return return
@@ -214,12 +212,12 @@ class Session:
'default_prompt': self.default_prompt, 'default_prompt': self.default_prompt,
} }
event = pkg.plugin.host.emit(plugin_models.SessionFirstMessageReceived, **args) event = plugin_host.emit(plugin_models.SessionFirstMessageReceived, **args)
if event.is_prevented_default(): if event.is_prevented_default():
return None, None, None return None, None, None
config = pkg.utils.context.get_config() config = context.get_config_manager().data
max_length = config.prompt_submit_length max_length = config['prompt_submit_length']
local_default_prompt = self.default_prompt.copy() local_default_prompt = self.default_prompt.copy()
local_prompt = self.prompt.copy() local_prompt = self.prompt.copy()
@@ -232,7 +230,7 @@ class Session:
'text_message': text, 'text_message': text,
} }
event = pkg.plugin.host.emit(plugin_models.PromptPreProcessing, **args) event = plugin_host.emit(plugin_models.PromptPreProcessing, **args)
if event.get_return_value('default_prompt') is not None: if event.get_return_value('default_prompt') is not None:
local_default_prompt = event.get_return_value('default_prompt') local_default_prompt = event.get_return_value('default_prompt')
@@ -243,6 +241,7 @@ class Session:
if event.get_return_value('text_message') is not None: if event.get_return_value('text_message') is not None:
text = event.get_return_value('text_message') text = event.get_return_value('text_message')
# 裁剪messages到合适长度
prompts, _ = self.cut_out(text, max_length, local_default_prompt, local_prompt) prompts, _ = self.cut_out(text, max_length, local_default_prompt, local_prompt)
res_text = "" res_text = ""
@@ -255,15 +254,15 @@ class Session:
funcs = [] funcs = []
trace_func_calls = config.trace_function_calls trace_func_calls = config['trace_function_calls']
botmgr = pkg.utils.context.get_qqbot_manager() botmgr = context.get_qqbot_manager()
session_name_spt: list[str] = self.name.split("_") session_name_spt: list[str] = self.name.split("_")
pending_res_text = "" pending_res_text = ""
# TODO 对不起,我知道这样非常非常屎山,但我之后会重构的 # TODO 对不起,我知道这样非常非常屎山,但我之后会重构的
for resp in pkg.utils.context.get_openai_manager().request_completion(prompts): for resp in context.get_openai_manager().request_completion(prompts):
if pending_res_text != "": if pending_res_text != "":
botmgr.adapter.send_message( botmgr.adapter.send_message(
@@ -325,7 +324,6 @@ class Session:
) )
pass pass
# 向API请求补全 # 向API请求补全
# message, total_token = pkg.utils.context.get_openai_manager().request_completion( # message, total_token = pkg.utils.context.get_openai_manager().request_completion(
# prompts, # prompts,
@@ -383,13 +381,13 @@ class Session:
# 包装目前的对话回合内容 # 包装目前的对话回合内容
changable_prompts = [] changable_prompts = []
use_model = pkg.utils.context.get_config().completion_api_params['model'] use_model = context.get_config_manager().data['completion_api_params']['model']
ptr = len(prompt) - 1 ptr = len(prompt) - 1
# 直接从后向前扫描拼接,不管是否是整回合 # 直接从后向前扫描拼接,不管是否是整回合
while ptr >= 0: while ptr >= 0:
if count_tokens(prompt[ptr:ptr+1]+changable_prompts, use_model) > max_tokens: if openai_modelmgr.count_tokens(prompt[ptr:ptr+1]+changable_prompts, use_model) > max_tokens:
break break
changable_prompts.insert(0, prompt[ptr]) changable_prompts.insert(0, prompt[ptr])
@@ -410,14 +408,14 @@ class Session:
logging.debug("cut_out: {}".format(json.dumps(result_prompt, ensure_ascii=False, indent=4))) logging.debug("cut_out: {}".format(json.dumps(result_prompt, ensure_ascii=False, indent=4)))
return result_prompt, count_tokens(changable_prompts, use_model) return result_prompt, openai_modelmgr.count_tokens(changable_prompts, use_model)
# 持久化session # 持久化session
def persistence(self): def persistence(self):
if self.prompt == self.get_default_prompt(): if self.prompt == self.get_default_prompt():
return return
db_inst = pkg.utils.context.get_database_manager() db_inst = context.get_database_manager()
name_spt = self.name.split('_') name_spt = self.name.split('_')
@@ -439,12 +437,12 @@ class Session:
} }
# 此事件不支持阻止默认行为 # 此事件不支持阻止默认行为
_ = pkg.plugin.host.emit(plugin_models.SessionExplicitReset, **args) _ = plugin_host.emit(plugin_models.SessionExplicitReset, **args)
pkg.utils.context.get_database_manager().explicit_close_session(self.name, self.create_timestamp) context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
if expired: if expired:
pkg.utils.context.get_database_manager().set_session_expired(self.name, self.create_timestamp) context.get_database_manager().set_session_expired(self.name, self.create_timestamp)
if not persist: # 不要求保持default prompt if not persist: # 不要求保持default prompt
self.default_prompt = self.get_default_prompt(use_prompt) self.default_prompt = self.get_default_prompt(use_prompt)
@@ -461,11 +459,11 @@ class Session:
# 将本session的数据库状态设置为on_going # 将本session的数据库状态设置为on_going
def set_ongoing(self): def set_ongoing(self):
pkg.utils.context.get_database_manager().set_session_ongoing(self.name, self.create_timestamp) context.get_database_manager().set_session_ongoing(self.name, self.create_timestamp)
# 切换到上一个session # 切换到上一个session
def last_session(self): def last_session(self):
last_one = pkg.utils.context.get_database_manager().last_session(self.name, self.last_interact_timestamp) last_one = context.get_database_manager().last_session(self.name, self.last_interact_timestamp)
if last_one is None: if last_one is None:
return None return None
else: else:
@@ -486,7 +484,7 @@ class Session:
# 切换到下一个session # 切换到下一个session
def next_session(self): def next_session(self):
next_one = pkg.utils.context.get_database_manager().next_session(self.name, self.last_interact_timestamp) next_one = context.get_database_manager().next_session(self.name, self.last_interact_timestamp)
if next_one is None: if next_one is None:
return None return None
else: else:
@@ -506,13 +504,13 @@ class Session:
return self return self
def list_history(self, capacity: int = 10, page: int = 0): def list_history(self, capacity: int = 10, page: int = 0):
return pkg.utils.context.get_database_manager().list_history(self.name, capacity, page) return context.get_database_manager().list_history(self.name, capacity, page)
def delete_history(self, index: int) -> bool: def delete_history(self, index: int) -> bool:
return pkg.utils.context.get_database_manager().delete_history(self.name, index) return context.get_database_manager().delete_history(self.name, index)
def delete_all_history(self) -> bool: def delete_all_history(self) -> bool:
return pkg.utils.context.get_database_manager().delete_all_history(self.name) return context.get_database_manager().delete_all_history(self.name)
def draw_image(self, prompt: str): def draw_image(self, prompt: str):
return pkg.utils.context.get_openai_manager().request_image(prompt) return context.get_openai_manager().request_image(prompt)

View File

@@ -7,14 +7,19 @@ import pkgutil
import sys import sys
import shutil import shutil
import traceback import traceback
import time
import re
import pkg.utils.updater as updater from ..utils import updater as updater
import pkg.utils.context as context from ..utils import network as network
import pkg.plugin.switch as switch from ..utils import context as context
import pkg.plugin.settings as settings from ..plugin import switch as switch
import pkg.qqbot.adapter as msadapter from ..plugin import settings as settings
from ..qqbot import adapter as msadapter
from ..plugin import metadata as metadata
from mirai import Mirai from mirai import Mirai
import requests
from CallingGPT.session.session import Session from CallingGPT.session.session import Session
@@ -65,6 +70,8 @@ def generate_plugin_order():
def iter_plugins(): def iter_plugins():
"""按照顺序迭代插件""" """按照顺序迭代插件"""
for plugin_name in __plugins_order__: for plugin_name in __plugins_order__:
if plugin_name not in __plugins__:
continue
yield __plugins__[plugin_name] yield __plugins__[plugin_name]
@@ -113,10 +120,15 @@ def load_plugins():
# 加载插件顺序 # 加载插件顺序
settings.load_settings() settings.load_settings()
logging.debug("registered plugins: {}".format(__plugins__))
# 输出已注册的内容函数列表 # 输出已注册的内容函数列表
logging.debug("registered content functions: {}".format(__callable_functions__)) logging.debug("registered content functions: {}".format(__callable_functions__))
logging.debug("function instance map: {}".format(__function_inst_map__)) logging.debug("function instance map: {}".format(__function_inst_map__))
# 迁移插件源地址记录
metadata.do_plugin_git_repo_migrate()
def initialize_plugins(): def initialize_plugins():
"""初始化插件""" """初始化插件"""
@@ -135,6 +147,7 @@ def initialize_plugins():
successfully_initialized_plugins.append(plugin['name']) successfully_initialized_plugins.append(plugin['name'])
except: except:
logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info())) logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
logging.debug(traceback.format_exc())
logging.info("以下插件已初始化: {}".format(", ".join(successfully_initialized_plugins))) logging.info("以下插件已初始化: {}".format(", ".join(successfully_initialized_plugins)))
@@ -155,34 +168,100 @@ def unload_plugins():
# logging.error("插件{}卸载时发生错误: {}".format(plugin['name'], sys.exc_info())) # logging.error("插件{}卸载时发生错误: {}".format(plugin['name'], sys.exc_info()))
def install_plugin(repo_url: str): def get_github_plugin_repo_label(repo_url: str) -> list[str]:
"""安装插件从git储存库获取并解决依赖""" """获取username, repo"""
try:
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.ensure_dulwich()
except:
pass
try: # 提取 username/repo , 正则表达式
import dulwich repo = re.findall(r'(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)', repo_url)
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
from dulwich import porcelain if len(repo) > 0: # github
return repo[0].split("/")
else:
return None
logging.info("克隆插件储存库: {}".format(repo_url))
repo = porcelain.clone(repo_url, "plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/", checkout=True)
def download_plugin_source_code(repo_url: str, target_path: str) -> str:
"""下载插件源码"""
# 检查源类型
# 提取 username/repo , 正则表达式
repo = get_github_plugin_repo_label(repo_url)
target_path += repo[1]
if repo is not None: # github
logging.info("从 GitHub 下载插件源码...")
zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD"
zip_resp = requests.get(
url=zipball_url,
proxies=network.wrapper_proxies(),
stream=True
)
if zip_resp.status_code != 200:
raise Exception("下载源码失败: {}".format(zip_resp.text))
if os.path.exists("temp/"+target_path):
shutil.rmtree("temp/"+target_path)
if os.path.exists(target_path):
shutil.rmtree(target_path)
os.makedirs("temp/"+target_path)
with open("temp/"+target_path+"/source.zip", "wb") as f:
for chunk in zip_resp.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
logging.info("下载完成, 解压...")
import zipfile
with zipfile.ZipFile("temp/"+target_path+"/source.zip", 'r') as zip_ref:
zip_ref.extractall("temp/"+target_path)
os.remove("temp/"+target_path+"/source.zip")
# 目标是 username-repo-hash , 用正则表达式提取完整的文件夹名,复制到 plugins/repo
import glob
# 获取解压后的文件夹名
unzip_dir = glob.glob("temp/"+target_path+"/*")[0]
# 复制到 plugins/repo
shutil.copytree(unzip_dir, target_path+"/")
# 删除解压后的文件夹
shutil.rmtree(unzip_dir)
logging.info("解压完成")
else:
raise Exception("暂不支持的源类型,请使用 GitHub 仓库发行插件。")
return repo[1]
def check_requirements(path: str):
# 检查此目录是否包含requirements.txt # 检查此目录是否包含requirements.txt
if os.path.exists("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt"): if os.path.exists(path+"/requirements.txt"):
logging.info("检测到requirements.txt正在安装依赖") logging.info("检测到requirements.txt正在安装依赖")
import pkg.utils.pkgmgr import pkg.utils.pkgmgr
pkg.utils.pkgmgr.install_requirements("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt") pkg.utils.pkgmgr.install_requirements(path+"/requirements.txt")
import pkg.utils.log as log import pkg.utils.log as log
log.reset_logging() log.reset_logging()
def install_plugin(repo_url: str):
"""安装插件从git储存库获取并解决依赖"""
repo_label = download_plugin_source_code(repo_url, "plugins/")
check_requirements("plugins/"+repo_label)
metadata.set_plugin_metadata(repo_label, repo_url, int(time.time()), "HEAD")
def uninstall_plugin(plugin_name: str) -> str: def uninstall_plugin(plugin_name: str) -> str:
"""卸载插件""" """卸载插件"""
if plugin_name not in __plugins__: if plugin_name not in __plugins__:
@@ -202,39 +281,43 @@ def uninstall_plugin(plugin_name: str) -> str:
def update_plugin(plugin_name: str): def update_plugin(plugin_name: str):
"""更新插件""" """更新插件"""
# 检查是否有远程地址记录 # 检查是否有远程地址记录
target_plugin_dir = "plugins/" + __plugins__[plugin_name]['path'].replace("\\", "/").split("plugins/")[1].split("/")[0] plugin_path_name = get_plugin_path_name_by_plugin_name(plugin_name)
remote_url = updater.get_remote_url(target_plugin_dir) meta = metadata.get_plugin_metadata(plugin_path_name)
if meta == {}:
raise Exception("没有此插件元数据信息,无法更新")
remote_url = meta['source']
if remote_url == "https://github.com/RockChinQ/QChatGPT" or remote_url == "https://gitee.com/RockChin/QChatGPT" \ if remote_url == "https://github.com/RockChinQ/QChatGPT" or remote_url == "https://gitee.com/RockChin/QChatGPT" \
or remote_url == "" or remote_url is None or remote_url == "http://github.com/RockChinQ/QChatGPT" or remote_url == "http://gitee.com/RockChin/QChatGPT": or remote_url == "" or remote_url is None or remote_url == "http://github.com/RockChinQ/QChatGPT" or remote_url == "http://gitee.com/RockChin/QChatGPT":
raise Exception("插件没有远程地址记录,无法更新") raise Exception("插件没有远程地址记录,无法更新")
# 把远程clone到temp/plugins/update/插件 # 重新安装插件
logging.info("克隆插件储存库: {}".format(remote_url)) logging.info("正在重新安装插件以进行更新...")
from dulwich import porcelain install_plugin(remote_url)
clone_target_dir = "temp/plugins/update/"+target_plugin_dir.split("/")[-1]+"/"
if os.path.exists(clone_target_dir):
shutil.rmtree(clone_target_dir)
if not os.path.exists(clone_target_dir): def get_plugin_name_by_path_name(plugin_path_name: str) -> str:
os.makedirs(clone_target_dir) for k, v in __plugins__.items():
repo = porcelain.clone(remote_url, clone_target_dir, checkout=True) if v['path'] == "plugins/"+plugin_path_name+"/main.py":
return k
return None
# 检查此目录是否包含requirements.txt
if os.path.exists(clone_target_dir+"requirements.txt"):
logging.info("检测到requirements.txt正在安装依赖")
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.install_requirements(clone_target_dir+"requirements.txt")
import pkg.utils.log as log def get_plugin_path_name_by_plugin_name(plugin_name: str) -> str:
log.reset_logging() if plugin_name not in __plugins__:
return None
plugin_main_module_path = __plugins__[plugin_name]['path']
# 将temp/plugins/update/插件名 覆盖到 plugins/插件名 plugin_main_module_path = plugin_main_module_path.replace("\\", "/")
shutil.rmtree(target_plugin_dir)
spt = plugin_main_module_path.split("/")
return spt[1]
shutil.copytree(clone_target_dir, target_plugin_dir)
class EventContext: class EventContext:
"""事件上下文""" """事件上下文"""

87
pkg/plugin/metadata.py Normal file
View File

@@ -0,0 +1,87 @@
import os
import shutil
import json
import time
import dulwich.errors as dulwich_err
from ..utils import updater
def read_metadata_file() -> dict:
# 读取 plugins/metadata.json 文件
if not os.path.exists('plugins/metadata.json'):
return {}
with open('plugins/metadata.json', 'r') as f:
return json.load(f)
def write_metadata_file(metadata: dict):
if not os.path.exists('plugins'):
os.mkdir('plugins')
with open('plugins/metadata.json', 'w') as f:
json.dump(metadata, f, indent=4, ensure_ascii=False)
def do_plugin_git_repo_migrate():
# 仅在 plugins/metadata.json 不存在时执行
if os.path.exists('plugins/metadata.json'):
return
metadata = read_metadata_file()
# 遍历 plugins 下所有目录获取目录的git远程地址
for plugin_name in os.listdir('plugins'):
plugin_path = os.path.join('plugins', plugin_name)
if not os.path.isdir(plugin_path):
continue
remote_url = None
try:
remote_url = updater.get_remote_url(plugin_path)
except dulwich_err.NotGitRepository:
continue
if remote_url == "https://github.com/RockChinQ/QChatGPT" or remote_url == "https://gitee.com/RockChin/QChatGPT" \
or remote_url == "" or remote_url is None or remote_url == "http://github.com/RockChinQ/QChatGPT" or remote_url == "http://gitee.com/RockChin/QChatGPT":
continue
from . import host
if plugin_name not in metadata:
metadata[plugin_name] = {
'source': remote_url,
'install_timestamp': int(time.time()),
'ref': 'HEAD',
}
write_metadata_file(metadata)
def set_plugin_metadata(
plugin_name: str,
source: str,
install_timestamp: int,
ref: str,
):
metadata = read_metadata_file()
metadata[plugin_name] = {
'source': source,
'install_timestamp': install_timestamp,
'ref': ref,
}
write_metadata_file(metadata)
def remove_plugin_metadata(plugin_name: str):
metadata = read_metadata_file()
if plugin_name in metadata:
del metadata[plugin_name]
write_metadata_file(metadata)
def get_plugin_metadata(plugin_name: str) -> dict:
metadata = read_metadata_file()
if plugin_name in metadata:
return metadata[plugin_name]
return {}

View File

@@ -1,7 +1,7 @@
import logging import logging
import pkg.plugin.host as host from ..plugin import host
import pkg.utils.context from ..utils import context
PersonMessageReceived = "person_message_received" PersonMessageReceived = "person_message_received"
"""收到私聊消息时,在判断是否应该响应前触发 """收到私聊消息时,在判断是否应该响应前触发
@@ -285,7 +285,7 @@ def register(name: str, description: str, version: str, author: str):
cls.description = description cls.description = description
cls.version = version cls.version = version
cls.author = author cls.author = author
cls.host = pkg.utils.context.get_plugin_host() cls.host = context.get_plugin_host()
cls.enabled = True cls.enabled = True
cls.path = host.__current_module_path__ cls.path = host.__current_module_path__

View File

@@ -1,9 +1,9 @@
import json import json
import os import os
import pkg.plugin.host as host
import logging import logging
from ..plugin import host
def wrapper_dict_from_runtime_context() -> dict: def wrapper_dict_from_runtime_context() -> dict:
"""从变量中包装settings.json的数据字典""" """从变量中包装settings.json的数据字典"""

View File

@@ -3,7 +3,7 @@ import json
import logging import logging
import os import os
import pkg.plugin.host as host from ..plugin import host
def wrapper_dict_from_plugin_list() -> dict: def wrapper_dict_from_plugin_list() -> dict:

View File

@@ -5,6 +5,7 @@ import mirai
class MessageSourceAdapter: class MessageSourceAdapter:
bot_account_id: int
def __init__(self, config: dict): def __init__(self, config: dict):
pass pass

View File

@@ -1,18 +1,18 @@
import pkg.utils.context from ..utils import context
def is_banned(launcher_type: str, launcher_id: int, sender_id: int) -> bool: def is_banned(launcher_type: str, launcher_id: int, sender_id: int) -> bool:
if not pkg.utils.context.get_qqbot_manager().enable_banlist: if not context.get_qqbot_manager().enable_banlist:
return False return False
result = False result = False
if launcher_type == 'group': if launcher_type == 'group':
# 检查是否显式声明发起人QQ要被person忽略 # 检查是否显式声明发起人QQ要被person忽略
if sender_id in pkg.utils.context.get_qqbot_manager().ban_person: if sender_id in context.get_qqbot_manager().ban_person:
result = True result = True
else: else:
for group_rule in pkg.utils.context.get_qqbot_manager().ban_group: for group_rule in context.get_qqbot_manager().ban_group:
if type(group_rule) == int: if type(group_rule) == int:
if group_rule == launcher_id: # 此群群号被禁用 if group_rule == launcher_id: # 此群群号被禁用
result = True result = True
@@ -32,7 +32,7 @@ def is_banned(launcher_type: str, launcher_id: int, sender_id: int) -> bool:
else: else:
# ban_person, 与群规则相同 # ban_person, 与群规则相同
for person_rule in pkg.utils.context.get_qqbot_manager().ban_person: for person_rule in context.get_qqbot_manager().ban_person:
if type(person_rule) == int: if type(person_rule) == int:
if person_rule == launcher_id: if person_rule == launcher_id:
result = True result = True

View File

@@ -2,21 +2,21 @@
import os import os
import time import time
import base64 import base64
import typing
import config
from mirai.models.message import MessageComponent, MessageChain, Image from mirai.models.message import MessageComponent, MessageChain, Image
from mirai.models.message import ForwardMessageNode from mirai.models.message import ForwardMessageNode
from mirai.models.base import MiraiBaseModel from mirai.models.base import MiraiBaseModel
from typing import List
import pkg.utils.context as context from ..utils import text2img
import pkg.utils.text2img as text2img from ..utils import context
class ForwardMessageDiaplay(MiraiBaseModel): class ForwardMessageDiaplay(MiraiBaseModel):
title: str = "群聊的聊天记录" title: str = "群聊的聊天记录"
brief: str = "[聊天记录]" brief: str = "[聊天记录]"
source: str = "聊天记录" source: str = "聊天记录"
preview: List[str] = [] preview: typing.List[str] = []
summary: str = "查看x条转发消息" summary: str = "查看x条转发消息"
@@ -26,7 +26,7 @@ class Forward(MessageComponent):
"""消息组件类型。""" """消息组件类型。"""
display: ForwardMessageDiaplay display: ForwardMessageDiaplay
"""显示信息""" """显示信息"""
node_list: List[ForwardMessageNode] node_list: typing.List[ForwardMessageNode]
"""转发消息节点列表。""" """转发消息节点列表。"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if len(args) == 1: if len(args) == 1:
@@ -64,13 +64,16 @@ def text_to_image(text: str) -> MessageComponent:
def check_text(text: str) -> list: def check_text(text: str) -> list:
"""检查文本是否为长消息,并转换成该使用的消息链组件""" """检查文本是否为长消息,并转换成该使用的消息链组件"""
if len(text) > config.blob_message_threshold:
config = context.get_config_manager().data
if len(text) > config['blob_message_threshold']:
# logging.info("长消息: {}".format(text)) # logging.info("长消息: {}".format(text))
if config.blob_message_strategy == 'image': if config['blob_message_strategy'] == 'image':
# 转换成图片 # 转换成图片
return [text_to_image(text)] return [text_to_image(text)]
elif config.blob_message_strategy == 'forward': elif config['blob_message_strategy'] == 'forward':
# 包装转发消息 # 包装转发消息
display = ForwardMessageDiaplay( display = ForwardMessageDiaplay(
@@ -82,7 +85,7 @@ def check_text(text: str) -> list:
) )
node = ForwardMessageNode( node = ForwardMessageNode(
sender_id=config.mirai_http_api_config['qq'], sender_id=config['mirai_http_api_config']['qq'],
sender_name='bot', sender_name='bot',
message_chain=MessageChain([text]) message_chain=MessageChain([text])
) )

View File

@@ -1,10 +1,7 @@
import importlib
import inspect
import logging import logging
import copy import copy
import pkgutil import pkgutil
import traceback import traceback
import types
import json import json

View File

@@ -1,11 +1,12 @@
from ..aamgr import AbstractCommandNode, Context
import logging import logging
from mirai import Image import mirai
import config
from .. import aamgr
from ....utils import context
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="draw", name="draw",
description="使用DALL·E生成图片", description="使用DALL·E生成图片",
@@ -13,9 +14,9 @@ import config
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class DrawCommand(AbstractCommandNode): class DrawCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
reply = [] reply = []
@@ -28,9 +29,9 @@ class DrawCommand(AbstractCommandNode):
res = session.draw_image(" ".join(ctx.params)) res = session.draw_image(" ".join(ctx.params))
logging.debug("draw_image result:{}".format(res)) logging.debug("draw_image result:{}".format(res))
reply = [Image(url=res['data'][0]['url'])] reply = [mirai.Image(url=res.data[0].url)]
if not (hasattr(config, 'include_image_description') config = context.get_config_manager().data
and not config.include_image_description): if config['include_image_description']:
reply.append(" ".join(ctx.params)) reply.append(" ".join(ctx.params))
return True, reply return True, reply

View File

@@ -1,10 +1,9 @@
from ..aamgr import AbstractCommandNode, Context
import logging import logging
import json import json
from .. import aamgr
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="func", name="func",
description="管理内容函数", description="管理内容函数",
@@ -12,9 +11,9 @@ import json
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class FuncCommand(AbstractCommandNode): class FuncCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
from pkg.plugin.models import host from pkg.plugin.models import host
reply = [] reply = []

View File

@@ -1,12 +1,9 @@
from ..aamgr import AbstractCommandNode, Context from ....plugin import host as plugin_host
from ....utils import updater
import os from .. import aamgr
import pkg.plugin.host as plugin_host
import pkg.utils.updater as updater
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="plugin", name="plugin",
description="插件管理", description="插件管理",
@@ -14,9 +11,9 @@ import pkg.utils.updater as updater
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class PluginCommand(AbstractCommandNode): class PluginCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = [] reply = []
plugin_list = plugin_host.__plugins__ plugin_list = plugin_host.__plugins__
if len(ctx.params) == 0: if len(ctx.params) == 0:
@@ -48,7 +45,7 @@ class PluginCommand(AbstractCommandNode):
return False, [] return False, []
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=PluginCommand, parent=PluginCommand,
name="get", name="get",
description="安装插件", description="安装插件",
@@ -56,9 +53,9 @@ class PluginCommand(AbstractCommandNode):
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class PluginGetCommand(AbstractCommandNode): class PluginGetCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import threading import threading
import logging import logging
import pkg.utils.context import pkg.utils.context
@@ -81,17 +78,17 @@ class PluginGetCommand(AbstractCommandNode):
return True, reply return True, reply
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=PluginCommand, parent=PluginCommand,
name="update", name="update",
description="更新所有插件", description="更新指定插件或全部插件",
usage="!plugin update", usage="!plugin update",
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class PluginUpdateCommand(AbstractCommandNode): class PluginUpdateCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import threading import threading
import logging import logging
plugin_list = plugin_host.__plugins__ plugin_list = plugin_host.__plugins__
@@ -110,7 +107,9 @@ class PluginUpdateCommand(AbstractCommandNode):
plugin_host.update_plugin(key) plugin_host.update_plugin(key)
updated.append(key) updated.append(key)
else: else:
if ctx.crt_params[0] in plugin_list: plugin_path_name = plugin_host.get_plugin_path_name_by_plugin_name(ctx.crt_params[0])
if plugin_path_name is not None:
plugin_host.update_plugin(ctx.crt_params[0]) plugin_host.update_plugin(ctx.crt_params[0])
updated.append(ctx.crt_params[0]) updated.append(ctx.crt_params[0])
else: else:
@@ -119,7 +118,7 @@ class PluginUpdateCommand(AbstractCommandNode):
pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}, 请发送 !reload 重载插件".format(", ".join(updated))) pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}, 请发送 !reload 重载插件".format(", ".join(updated)))
except Exception as e: except Exception as e:
logging.error("插件更新失败:{}".format(e)) logging.error("插件更新失败:{}".format(e))
pkg.utils.context.get_qqbot_manager().notify_admin("插件更新失败:{} 请尝试手动更新插件".format(e)) pkg.utils.context.get_qqbot_manager().notify_admin("插件更新失败:{}使用 !plugin 命令确认插件名称或尝试手动更新插件".format(e))
reply = ["[bot]正在更新插件,请勿重复发起..."] reply = ["[bot]正在更新插件,请勿重复发起..."]
threading.Thread(target=closure).start() threading.Thread(target=closure).start()
@@ -128,7 +127,7 @@ class PluginUpdateCommand(AbstractCommandNode):
return True, reply return True, reply
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=PluginCommand, parent=PluginCommand,
name="del", name="del",
description="删除插件", description="删除插件",
@@ -136,9 +135,9 @@ class PluginUpdateCommand(AbstractCommandNode):
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class PluginDelCommand(AbstractCommandNode): class PluginDelCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
plugin_list = plugin_host.__plugins__ plugin_list = plugin_host.__plugins__
reply = [] reply = []
@@ -155,7 +154,7 @@ class PluginDelCommand(AbstractCommandNode):
return True, reply return True, reply
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=PluginCommand, parent=PluginCommand,
name="on", name="on",
description="启用指定插件", description="启用指定插件",
@@ -163,7 +162,7 @@ class PluginDelCommand(AbstractCommandNode):
aliases=[], aliases=[],
privilege=2 privilege=2
) )
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=PluginCommand, parent=PluginCommand,
name="off", name="off",
description="禁用指定插件", description="禁用指定插件",
@@ -171,9 +170,9 @@ class PluginDelCommand(AbstractCommandNode):
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class PluginOnOffCommand(AbstractCommandNode): class PluginOnOffCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.plugin.switch as plugin_switch import pkg.plugin.switch as plugin_switch
plugin_list = plugin_host.__plugins__ plugin_list = plugin_host.__plugins__

View File

@@ -1,7 +1,8 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
from ....utils import context
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="default", name="default",
description="操作情景预设", description="操作情景预设",
@@ -9,19 +10,20 @@ from ..aamgr import AbstractCommandNode, Context
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class DefaultCommand(AbstractCommandNode): class DefaultCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
session_name = ctx.session_name session_name = ctx.session_name
params = ctx.params params = ctx.params
reply = [] reply = []
import config
config = context.get_config_manager().data
if len(params) == 0: if len(params) == 0:
# 输出目前所有情景预设 # 输出目前所有情景预设
import pkg.openai.dprompt as dprompt import pkg.openai.dprompt as dprompt
reply_str = "[bot]当前所有情景预设({}模式):\n\n".format(config.preset_mode) reply_str = "[bot]当前所有情景预设({}模式):\n\n".format(config['preset_mode'])
prompts = dprompt.mode_inst().list() prompts = dprompt.mode_inst().list()
@@ -45,7 +47,7 @@ class DefaultCommand(AbstractCommandNode):
return True, reply return True, reply
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=DefaultCommand, parent=DefaultCommand,
name="set", name="set",
description="设置默认情景预设", description="设置默认情景预设",
@@ -53,9 +55,9 @@ class DefaultCommand(AbstractCommandNode):
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class DefaultSetCommand(AbstractCommandNode): class DefaultSetCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = [] reply = []
if len(ctx.crt_params) == 0: if len(ctx.crt_params) == 0:

View File

@@ -1,8 +1,7 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
import datetime
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="del", name="del",
description="删除当前会话的历史记录", description="删除当前会话的历史记录",
@@ -10,9 +9,9 @@ import datetime
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class DelCommand(AbstractCommandNode): class DelCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
session_name = ctx.session_name session_name = ctx.session_name
params = ctx.params params = ctx.params
@@ -33,7 +32,7 @@ class DelCommand(AbstractCommandNode):
return True, reply return True, reply
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=DelCommand, parent=DelCommand,
name="all", name="all",
description="删除当前会话的全部历史记录", description="删除当前会话的全部历史记录",
@@ -41,9 +40,9 @@ class DelCommand(AbstractCommandNode):
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class DelAllCommand(AbstractCommandNode): class DelAllCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
session_name = ctx.session_name session_name = ctx.session_name
reply = [] reply = []

View File

@@ -1,7 +1,7 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="delhst", name="delhst",
description="删除指定会话的所有历史记录", description="删除指定会话的所有历史记录",
@@ -9,9 +9,9 @@ from ..aamgr import AbstractCommandNode, Context
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class DelHistoryCommand(AbstractCommandNode): class DelHistoryCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
import pkg.utils.context import pkg.utils.context
params = ctx.params params = ctx.params
@@ -31,7 +31,7 @@ class DelHistoryCommand(AbstractCommandNode):
return True, reply return True, reply
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=DelHistoryCommand, parent=DelHistoryCommand,
name="all", name="all",
description="删除所有会话的全部历史记录", description="删除所有会话的全部历史记录",
@@ -39,9 +39,9 @@ class DelHistoryCommand(AbstractCommandNode):
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class DelAllHistoryCommand(AbstractCommandNode): class DelAllHistoryCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.utils.context import pkg.utils.context
reply = [] reply = []
pkg.utils.context.get_database_manager().delete_all_session_history() pkg.utils.context.get_database_manager().delete_all_session_history()

View File

@@ -1,8 +1,9 @@
from ..aamgr import AbstractCommandNode, Context
import datetime import datetime
from .. import aamgr
@AbstractCommandNode.register(
@aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="last", name="last",
description="切换前一次对话", description="切换前一次对话",
@@ -10,9 +11,9 @@ import datetime
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class LastCommand(AbstractCommandNode): class LastCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
session_name = ctx.session_name session_name = ctx.session_name

View File

@@ -1,9 +1,10 @@
from ..aamgr import AbstractCommandNode, Context
import datetime import datetime
import json import json
from .. import aamgr
@AbstractCommandNode.register(
@aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name='list', name='list',
description='列出当前会话的所有历史记录', description='列出当前会话的所有历史记录',
@@ -11,9 +12,9 @@ import json
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class ListCommand(AbstractCommandNode): class ListCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
session_name = ctx.session_name session_name = ctx.session_name
params = ctx.params params = ctx.params

View File

@@ -1,8 +1,9 @@
from ..aamgr import AbstractCommandNode, Context
import datetime import datetime
from .. import aamgr
@AbstractCommandNode.register(
@aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="next", name="next",
description="切换后一次对话", description="切换后一次对话",
@@ -10,9 +11,9 @@ import datetime
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class NextCommand(AbstractCommandNode): class NextCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
session_name = ctx.session_name session_name = ctx.session_name
reply = [] reply = []

View File

@@ -1,8 +1,7 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
import datetime
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="prompt", name="prompt",
description="获取当前会话的前文", description="获取当前会话的前文",
@@ -10,9 +9,9 @@ import datetime
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class PromptCommand(AbstractCommandNode): class PromptCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session import pkg.openai.session
session_name = ctx.session_name session_name = ctx.session_name
params = ctx.params params = ctx.params

View File

@@ -1,8 +1,7 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
import datetime
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="resend", name="resend",
description="重新获取上一次问题的回复", description="重新获取上一次问题的回复",
@@ -10,20 +9,22 @@ import datetime
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class ResendCommand(AbstractCommandNode): class ResendCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session from ....openai import session as openai_session
from ....utils import context
from ....qqbot import message
import config import config
session_name = ctx.session_name session_name = ctx.session_name
reply = [] reply = []
session = pkg.openai.session.get_session(session_name) session = openai_session.get_session(session_name)
to_send = session.undo() to_send = session.undo()
mgr = pkg.utils.context.get_qqbot_manager() mgr = context.get_qqbot_manager()
reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config, reply = message.process_normal_message(to_send, mgr, config,
ctx.launcher_type, ctx.launcher_id, ctx.launcher_type, ctx.launcher_id,
ctx.sender_id) ctx.sender_id)

View File

@@ -1,11 +1,11 @@
from ..aamgr import AbstractCommandNode, Context
import tips as tips_custom import tips as tips_custom
import pkg.openai.session from .. import aamgr
import pkg.utils.context from ....openai import session
from ....utils import context
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name='reset', name='reset',
description='重置当前会话', description='重置当前会话',
@@ -13,21 +13,21 @@ import pkg.utils.context
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class ResetCommand(AbstractCommandNode): class ResetCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
params = ctx.params params = ctx.params
session_name = ctx.session_name session_name = ctx.session_name
reply = "" reply = ""
if len(params) == 0: if len(params) == 0:
pkg.openai.session.get_session(session_name).reset(explicit=True) session.get_session(session_name).reset(explicit=True)
reply = [tips_custom.command_reset_message] reply = [tips_custom.command_reset_message]
else: else:
try: try:
import pkg.openai.dprompt as dprompt import pkg.openai.dprompt as dprompt
pkg.openai.session.get_session(session_name).reset(explicit=True, use_prompt=params[0]) session.get_session(session_name).reset(explicit=True, use_prompt=params[0])
reply = [tips_custom.command_reset_name_message+"{}".format(dprompt.mode_inst().get_full_name(params[0]))] reply = [tips_custom.command_reset_name_message+"{}".format(dprompt.mode_inst().get_full_name(params[0]))]
except Exception as e: except Exception as e:
reply = ["[bot]会话重置失败:{}".format(e)] reply = ["[bot]会话重置失败:{}".format(e)]

View File

@@ -1,6 +1,7 @@
from ..aamgr import AbstractCommandNode, Context
import json import json
from .. import aamgr
def config_operation(cmd, params): def config_operation(cmd, params):
reply = [] reply = []
@@ -85,7 +86,7 @@ def config_operation(cmd, params):
return reply return reply
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="cfg", name="cfg",
description="配置项管理", description="配置项管理",
@@ -93,8 +94,8 @@ def config_operation(cmd, params):
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class CfgCommand(AbstractCommandNode): class CfgCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
return True, config_operation(ctx.command, ctx.params) return True, config_operation(ctx.command, ctx.params)

View File

@@ -1,7 +1,7 @@
from ..aamgr import AbstractCommandNode, Context, __command_list__ from .. import aamgr
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="cmd", name="cmd",
description="显示指令列表", description="显示指令列表",
@@ -9,10 +9,10 @@ from ..aamgr import AbstractCommandNode, Context, __command_list__
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class CmdCommand(AbstractCommandNode): class CmdCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
command_list = __command_list__ command_list = aamgr.__command_list__
reply = [] reply = []

View File

@@ -1,7 +1,7 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="help", name="help",
description="显示自定义的帮助信息", description="显示自定义的帮助信息",
@@ -9,9 +9,9 @@ from ..aamgr import AbstractCommandNode, Context
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class HelpCommand(AbstractCommandNode): class HelpCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import tips import tips
reply = ["[bot] "+tips.help_message + "\n请输入 !cmd 查看指令列表"] reply = ["[bot] "+tips.help_message + "\n请输入 !cmd 查看指令列表"]

View File

@@ -1,7 +1,9 @@
from ..aamgr import AbstractCommandNode, Context
import threading import threading
@AbstractCommandNode.register( from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="reload", name="reload",
description="执行热重载", description="执行热重载",
@@ -9,9 +11,9 @@ import threading
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class ReloadCommand(AbstractCommandNode): class ReloadCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = [] reply = []
import pkg.utils.reloader import pkg.utils.reloader

View File

@@ -1,9 +1,10 @@
from ..aamgr import AbstractCommandNode, Context
import threading import threading
import traceback import traceback
from .. import aamgr
@AbstractCommandNode.register(
@aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="update", name="update",
description="更新程序", description="更新程序",
@@ -11,9 +12,9 @@ import traceback
aliases=[], aliases=[],
privilege=2 privilege=2
) )
class UpdateCommand(AbstractCommandNode): class UpdateCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = [] reply = []
import pkg.utils.updater import pkg.utils.updater
import pkg.utils.reloader import pkg.utils.reloader
@@ -22,8 +23,7 @@ class UpdateCommand(AbstractCommandNode):
def update_task(): def update_task():
try: try:
if pkg.utils.updater.update_all(): if pkg.utils.updater.update_all():
pkg.utils.reloader.reload_all(notify=False) pkg.utils.context.get_qqbot_manager().notify_admin("更新完成, 请手动重启程序。")
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
else: else:
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本") pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
except Exception as e0: except Exception as e0:

View File

@@ -1,8 +1,7 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
import logging
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="usage", name="usage",
description="获取使用情况", description="获取使用情况",
@@ -10,9 +9,9 @@ import logging
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class UsageCommand(AbstractCommandNode): class UsageCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import config import config
import pkg.utils.credit as credit import pkg.utils.credit as credit
import pkg.utils.context import pkg.utils.context

View File

@@ -1,7 +1,7 @@
from ..aamgr import AbstractCommandNode, Context from .. import aamgr
@AbstractCommandNode.register( @aamgr.AbstractCommandNode.register(
parent=None, parent=None,
name="version", name="version",
description="查看版本信息", description="查看版本信息",
@@ -9,9 +9,9 @@ from ..aamgr import AbstractCommandNode, Context
aliases=[], aliases=[],
privilege=1 privilege=1
) )
class VersionCommand(AbstractCommandNode): class VersionCommand(aamgr.AbstractCommandNode):
@classmethod @classmethod
def process(cls, ctx: Context) -> tuple[bool, list]: def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = [] reply = []
import pkg.utils.updater import pkg.utils.updater

View File

@@ -1,26 +1,10 @@
# 指令处理模块 # 指令处理模块
import logging import logging
import json
import datetime
import os
import threading
import traceback
import pkg.openai.session from ..qqbot.cmds import aamgr as cmdmgr
import pkg.openai.manager
import pkg.utils.reloader
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
import pkg.qqbot.cmds.aamgr as cmdmgr
from mirai import Image
def process_command(session_name: str, text_message: str, mgr, config: dict,
def process_command(session_name: str, text_message: str, mgr, config,
launcher_type: str, launcher_id: int, sender_id: int, is_admin: bool) -> list: launcher_type: str, launcher_id: int, sender_id: int, is_admin: bool) -> list:
reply = [] reply = []
try: try:

View File

@@ -4,6 +4,8 @@ import requests
import json import json
import logging import logging
from ..utils import context
class ReplyFilter: class ReplyFilter:
sensitive_words = [] sensitive_words = []
@@ -20,12 +22,13 @@ class ReplyFilter:
self.sensitive_words = sensitive_words self.sensitive_words = sensitive_words
self.mask = mask self.mask = mask
self.mask_word = mask_word self.mask_word = mask_word
import config
self.baidu_check = config.baidu_check config = context.get_config_manager().data
self.baidu_api_key = config.baidu_api_key
self.baidu_secret_key = config.baidu_secret_key self.baidu_check = config['baidu_check']
self.inappropriate_message_tips = config.inappropriate_message_tips self.baidu_api_key = config['baidu_api_key']
self.baidu_secret_key = config['baidu_secret_key']
self.inappropriate_message_tips = config['inappropriate_message_tips']
def is_illegal(self, message: str) -> bool: def is_illegal(self, message: str) -> bool:
processed = self.process(message) processed = self.process(message)

View File

@@ -1,16 +1,18 @@
import re import re
from ..utils import context
def ignore(msg: str) -> bool: def ignore(msg: str) -> bool:
"""检查消息是否应该被忽略""" """检查消息是否应该被忽略"""
import config config = context.get_config_manager().data
if 'prefix' in config.ignore_rules: if 'prefix' in config['ignore_rules']:
for rule in config.ignore_rules['prefix']: for rule in config['ignore_rules']['prefix']:
if msg.startswith(rule): if msg.startswith(rule):
return True return True
if 'regexp' in config.ignore_rules: if 'regexp' in config['ignore_rules']:
for rule in config.ignore_rules['regexp']: for rule in config['ignore_rules']['regexp']:
if re.search(rule, msg): if re.search(rule, msg):
return True return True

View File

@@ -1,41 +1,34 @@
import asyncio
import json import json
import os import os
import threading
from mirai import At, GroupMessage, MessageEvent, Mirai, StrangerMessage, WebSocketAdapter, HTTPAdapter, \
FriendMessage, Image, MessageChain, Plain
from func_timeout import func_set_timeout
import pkg.openai.session
import pkg.openai.manager
from func_timeout import FunctionTimedOut
import logging import logging
import pkg.qqbot.filter from mirai import At, GroupMessage, MessageEvent, StrangerMessage, \
import pkg.qqbot.process as processor FriendMessage, Image, MessageChain, Plain
import pkg.utils.context import func_timeout
import pkg.plugin.host as plugin_host from ..openai import session as openai_session
import pkg.plugin.models as plugin_models
from ..qqbot import filter as qqbot_filter
from ..qqbot import process as processor
from ..utils import context
from ..plugin import host as plugin_host
from ..plugin import models as plugin_models
import tips as tips_custom import tips as tips_custom
from ..qqbot import adapter as msadapter
import pkg.qqbot.adapter as msadapter
# 检查消息是否符合泛响应匹配机制 # 检查消息是否符合泛响应匹配机制
def check_response_rule(group_id:int, text: str): def check_response_rule(group_id:int, text: str):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
rules = config.response_rules rules = config['response_rules']
# 检查是否有特定规则 # 检查是否有特定规则
if 'prefix' not in config.response_rules: if 'prefix' not in config['response_rules']:
if str(group_id) in config.response_rules: if str(group_id) in config['response_rules']:
rules = config.response_rules[str(group_id)] rules = config['response_rules'][str(group_id)]
else: else:
rules = config.response_rules['default'] rules = config['response_rules']['default']
# 检查前缀匹配 # 检查前缀匹配
if 'prefix' in rules: if 'prefix' in rules:
@@ -55,16 +48,16 @@ def check_response_rule(group_id:int, text: str):
def response_at(group_id: int): def response_at(group_id: int):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
use_response_rule = config.response_rules use_response_rule = config['response_rules']
# 检查是否有特定规则 # 检查是否有特定规则
if 'prefix' not in config.response_rules: if 'prefix' not in config['response_rules']:
if str(group_id) in config.response_rules: if str(group_id) in config['response_rules']:
use_response_rule = config.response_rules[str(group_id)] use_response_rule = config['response_rules'][str(group_id)]
else: else:
use_response_rule = config.response_rules['default'] use_response_rule = config['response_rules']['default']
if 'at' not in use_response_rule: if 'at' not in use_response_rule:
return True return True
@@ -73,16 +66,16 @@ def response_at(group_id: int):
def random_responding(group_id): def random_responding(group_id):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
use_response_rule = config.response_rules use_response_rule = config['response_rules']
# 检查是否有特定规则 # 检查是否有特定规则
if 'prefix' not in config.response_rules: if 'prefix' not in config['response_rules']:
if str(group_id) in config.response_rules: if str(group_id) in config['response_rules']:
use_response_rule = config.response_rules[str(group_id)] use_response_rule = config['response_rules'][str(group_id)]
else: else:
use_response_rule = config.response_rules['default'] use_response_rule = config['response_rules']['default']
if 'random_rate' in use_response_rule: if 'random_rate' in use_response_rule:
import random import random
@@ -109,31 +102,31 @@ class QQBotManager:
ban_group = [] ban_group = []
def __init__(self, first_time_init=True): def __init__(self, first_time_init=True):
import config config = context.get_config_manager().data
self.timeout = config.process_message_timeout self.timeout = config['process_message_timeout']
self.retry = config.retry_times self.retry = config['retry_times']
# 由于YiriMirai的bot对象是单例的且shutdown方法暂时无法使用 # 由于YiriMirai的bot对象是单例的且shutdown方法暂时无法使用
# 故只在第一次初始化时创建bot对象重载之后使用原bot对象 # 故只在第一次初始化时创建bot对象重载之后使用原bot对象
# 因此bot的配置不支持热重载 # 因此bot的配置不支持热重载
if first_time_init: if first_time_init:
logging.debug("Use adapter:" + config.msg_source_adapter) logging.debug("Use adapter:" + config['msg_source_adapter'])
if config.msg_source_adapter == 'yirimirai': if config['msg_source_adapter'] == 'yirimirai':
from pkg.qqbot.sources.yirimirai import YiriMiraiAdapter from pkg.qqbot.sources.yirimirai import YiriMiraiAdapter
mirai_http_api_config = config.mirai_http_api_config mirai_http_api_config = config['mirai_http_api_config']
self.bot_account_id = config.mirai_http_api_config['qq'] self.bot_account_id = config['mirai_http_api_config']['qq']
self.adapter = YiriMiraiAdapter(mirai_http_api_config) self.adapter = YiriMiraiAdapter(mirai_http_api_config)
elif config.msg_source_adapter == 'nakuru': elif config['msg_source_adapter'] == 'nakuru':
from pkg.qqbot.sources.nakuru import NakuruProjectAdapter from pkg.qqbot.sources.nakuru import NakuruProjectAdapter
self.adapter = NakuruProjectAdapter(config.nakuru_config) self.adapter = NakuruProjectAdapter(config['nakuru_config'])
self.bot_account_id = self.adapter.bot_account_id self.bot_account_id = self.adapter.bot_account_id
else: else:
self.adapter = pkg.utils.context.get_qqbot_manager().adapter self.adapter = context.get_qqbot_manager().adapter
self.bot_account_id = pkg.utils.context.get_qqbot_manager().bot_account_id self.bot_account_id = context.get_qqbot_manager().bot_account_id
pkg.utils.context.set_qqbot_manager(self) context.set_qqbot_manager(self)
# 注册诸事件 # 注册诸事件
# Caution: 注册新的事件处理器之后请务必在unsubscribe_all中编写相应的取消订阅代码 # Caution: 注册新的事件处理器之后请务必在unsubscribe_all中编写相应的取消订阅代码
@@ -154,7 +147,7 @@ class QQBotManager:
self.on_person_message(event) self.on_person_message(event)
pkg.utils.context.get_thread_ctl().submit_user_task( context.get_thread_ctl().submit_user_task(
friend_message_handler, friend_message_handler,
) )
self.adapter.register_listener( self.adapter.register_listener(
@@ -179,11 +172,11 @@ class QQBotManager:
self.on_person_message(event) self.on_person_message(event)
pkg.utils.context.get_thread_ctl().submit_user_task( context.get_thread_ctl().submit_user_task(
stranger_message_handler, stranger_message_handler,
) )
# nakuru不区分好友和陌生人故仅为yirimirai注册陌生人事件 # nakuru不区分好友和陌生人故仅为yirimirai注册陌生人事件
if config.msg_source_adapter == 'yirimirai': if config['msg_source_adapter'] == 'yirimirai':
self.adapter.register_listener( self.adapter.register_listener(
StrangerMessage, StrangerMessage,
on_stranger_message on_stranger_message
@@ -206,7 +199,7 @@ class QQBotManager:
self.on_group_message(event) self.on_group_message(event)
pkg.utils.context.get_thread_ctl().submit_user_task( context.get_thread_ctl().submit_user_task(
group_message_handler, group_message_handler,
event event
) )
@@ -220,12 +213,11 @@ class QQBotManager:
用于在热重载流程中卸载所有事件处理器 用于在热重载流程中卸载所有事件处理器
""" """
import config
self.adapter.unregister_listener( self.adapter.unregister_listener(
FriendMessage, FriendMessage,
on_friend_message on_friend_message
) )
if config.msg_source_adapter == 'yirimirai': if config['msg_source_adapter'] == 'yirimirai':
self.adapter.unregister_listener( self.adapter.unregister_listener(
StrangerMessage, StrangerMessage,
on_stranger_message on_stranger_message
@@ -250,31 +242,31 @@ class QQBotManager:
if hasattr(banlist, "enable_group"): if hasattr(banlist, "enable_group"):
self.enable_group = banlist.enable_group self.enable_group = banlist.enable_group
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if os.path.exists("sensitive.json") \ if os.path.exists("sensitive.json") \
and config.sensitive_word_filter is not None \ and config['sensitive_word_filter'] is not None \
and config.sensitive_word_filter: and config['sensitive_word_filter']:
with open("sensitive.json", "r", encoding="utf-8") as f: with open("sensitive.json", "r", encoding="utf-8") as f:
sensitive_json = json.load(f) sensitive_json = json.load(f)
self.reply_filter = pkg.qqbot.filter.ReplyFilter( self.reply_filter = qqbot_filter.ReplyFilter(
sensitive_words=sensitive_json['words'], sensitive_words=sensitive_json['words'],
mask=sensitive_json['mask'] if 'mask' in sensitive_json else '*', mask=sensitive_json['mask'] if 'mask' in sensitive_json else '*',
mask_word=sensitive_json['mask_word'] if 'mask_word' in sensitive_json else '' mask_word=sensitive_json['mask_word'] if 'mask_word' in sensitive_json else ''
) )
else: else:
self.reply_filter = pkg.qqbot.filter.ReplyFilter([]) self.reply_filter = qqbot_filter.ReplyFilter([])
def send(self, event, msg, check_quote=True, check_at_sender=True): def send(self, event, msg, check_quote=True, check_at_sender=True):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if check_at_sender and config.at_sender: if check_at_sender and config['at_sender']:
msg.insert( msg.insert(
0, 0,
Plain(" \n") Plain(" \n")
) )
# 当回复的正文中包含换行时quote可能会自带at此时就不再单独添加at只添加换行 # 当回复的正文中包含换行时quote可能会自带at此时就不再单独添加at只添加换行
if "\n" not in str(msg[1]) or config.msg_source_adapter == 'nakuru': if "\n" not in str(msg[1]) or config['msg_source_adapter'] == 'nakuru':
msg.insert( msg.insert(
0, 0,
At( At(
@@ -285,14 +277,15 @@ class QQBotManager:
self.adapter.reply_message( self.adapter.reply_message(
event, event,
msg, msg,
quote_origin=True if config.quote_origin and check_quote else False quote_origin=True if config['quote_origin'] and check_quote else False
) )
# 私聊消息处理 # 私聊消息处理
def on_person_message(self, event: MessageEvent): def on_person_message(self, event: MessageEvent):
import config
reply = '' reply = ''
config = context.get_config_manager().data
if not self.enable_private: if not self.enable_private:
logging.debug("已在banlist.py中禁用所有私聊") logging.debug("已在banlist.py中禁用所有私聊")
elif event.sender.id == self.bot_account_id: elif event.sender.id == self.bot_account_id:
@@ -306,7 +299,7 @@ class QQBotManager:
for i in range(self.retry): for i in range(self.retry):
try: try:
@func_set_timeout(config.process_message_timeout) @func_timeout.func_set_timeout(config['process_message_timeout'])
def time_ctrl_wrapper(): def time_ctrl_wrapper():
reply = processor.process_message('person', event.sender.id, str(event.message_chain), reply = processor.process_message('person', event.sender.id, str(event.message_chain),
event.message_chain, event.message_chain,
@@ -315,16 +308,16 @@ class QQBotManager:
reply = time_ctrl_wrapper() reply = time_ctrl_wrapper()
break break
except FunctionTimedOut: except func_timeout.FunctionTimedOut:
logging.warning("person_{}: 超时,重试中({})".format(event.sender.id, i)) logging.warning("person_{}: 超时,重试中({})".format(event.sender.id, i))
pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock() openai_session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
if "person_{}".format(event.sender.id) in pkg.qqbot.process.processing: if "person_{}".format(event.sender.id) in processor.processing:
pkg.qqbot.process.processing.remove('person_{}'.format(event.sender.id)) processor.processing.remove('person_{}'.format(event.sender.id))
failed += 1 failed += 1
continue continue
if failed == self.retry: if failed == self.retry:
pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock() openai_session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
self.notify_admin("{} 请求超时".format("person_{}".format(event.sender.id))) self.notify_admin("{} 请求超时".format("person_{}".format(event.sender.id)))
reply = [tips_custom.reply_message] reply = [tips_custom.reply_message]
@@ -333,8 +326,10 @@ class QQBotManager:
# 群消息处理 # 群消息处理
def on_group_message(self, event: GroupMessage): def on_group_message(self, event: GroupMessage):
import config
reply = '' reply = ''
config = context.get_config_manager().data
def process(text=None) -> str: def process(text=None) -> str:
replys = "" replys = ""
if At(self.bot_account_id) in event.message_chain: if At(self.bot_account_id) in event.message_chain:
@@ -344,7 +339,7 @@ class QQBotManager:
failed = 0 failed = 0
for i in range(self.retry): for i in range(self.retry):
try: try:
@func_set_timeout(config.process_message_timeout) @func_timeout.func_set_timeout(config['process_message_timeout'])
def time_ctrl_wrapper(): def time_ctrl_wrapper():
replys = processor.process_message('group', event.group.id, replys = processor.process_message('group', event.group.id,
str(event.message_chain).strip() if text is None else text, str(event.message_chain).strip() if text is None else text,
@@ -354,16 +349,16 @@ class QQBotManager:
replys = time_ctrl_wrapper() replys = time_ctrl_wrapper()
break break
except FunctionTimedOut: except func_timeout.FunctionTimedOut:
logging.warning("group_{}: 超时,重试中({})".format(event.group.id, i)) logging.warning("group_{}: 超时,重试中({})".format(event.group.id, i))
pkg.openai.session.get_session('group_{}'.format(event.group.id)).release_response_lock() openai_session.get_session('group_{}'.format(event.group.id)).release_response_lock()
if "group_{}".format(event.group.id) in pkg.qqbot.process.processing: if "group_{}".format(event.group.id) in processor.processing:
pkg.qqbot.process.processing.remove('group_{}'.format(event.group.id)) processor.processing.remove('group_{}'.format(event.group.id))
failed += 1 failed += 1
continue continue
if failed == self.retry: if failed == self.retry:
pkg.openai.session.get_session('group_{}'.format(event.group.id)).release_response_lock() openai_session.get_session('group_{}'.format(event.group.id)).release_response_lock()
self.notify_admin("{} 请求超时".format("group_{}".format(event.group.id))) self.notify_admin("{} 请求超时".format("group_{}".format(event.group.id)))
replys = [tips_custom.replys_message] replys = [tips_custom.replys_message]
@@ -392,17 +387,17 @@ class QQBotManager:
# 通知系统管理员 # 通知系统管理员
def notify_admin(self, message: str): def notify_admin(self, message: str):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if config.admin_qq != 0 and config.admin_qq != []: if config['admin_qq'] != 0 and config['admin_qq'] != []:
logging.info("通知管理员:{}".format(message)) logging.info("通知管理员:{}".format(message))
if type(config.admin_qq) == int: if type(config['admin_qq']) == int:
self.adapter.send_message( self.adapter.send_message(
"person", "person",
config.admin_qq, config['admin_qq'],
MessageChain([Plain("[bot]{}".format(message))]) MessageChain([Plain("[bot]{}".format(message))])
) )
else: else:
for adm in config.admin_qq: for adm in config['admin_qq']:
self.adapter.send_message( self.adapter.send_message(
"person", "person",
adm, adm,
@@ -410,17 +405,17 @@ class QQBotManager:
) )
def notify_admin_message_chain(self, message): def notify_admin_message_chain(self, message):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if config.admin_qq != 0 and config.admin_qq != []: if config['admin_qq'] != 0 and config['admin_qq'] != []:
logging.info("通知管理员:{}".format(message)) logging.info("通知管理员:{}".format(message))
if type(config.admin_qq) == int: if type(config['admin_qq']) == int:
self.adapter.send_message( self.adapter.send_message(
"person", "person",
config.admin_qq, config['admin_qq'],
message message
) )
else: else:
for adm in config.admin_qq: for adm in config['admin_qq']:
self.adapter.send_message( self.adapter.send_message(
"person", "person",
adm, adm,

View File

@@ -1,32 +1,33 @@
# 普通消息处理模块 # 普通消息处理模块
import logging import logging
import openai
import pkg.utils.context
import pkg.openai.session
import pkg.plugin.host as plugin_host import openai
import pkg.plugin.models as plugin_models
import pkg.qqbot.blob as blob from ..utils import context
from ..openai import session as openai_session
from ..plugin import host as plugin_host
from ..plugin import models as plugin_models
import tips as tips_custom import tips as tips_custom
def handle_exception(notify_admin: str = "", set_reply: str = "") -> list: def handle_exception(notify_admin: str = "", set_reply: str = "") -> list:
"""处理异常当notify_admin不为空时会通知管理员返回通知用户的消息""" """处理异常当notify_admin不为空时会通知管理员返回通知用户的消息"""
import config config = context.get_config_manager().data
pkg.utils.context.get_qqbot_manager().notify_admin(notify_admin) context.get_qqbot_manager().notify_admin(notify_admin)
if config.hide_exce_info_to_user: if config['hide_exce_info_to_user']:
return [tips_custom.alter_tip_message] if tips_custom.alter_tip_message else [] return [tips_custom.alter_tip_message] if tips_custom.alter_tip_message else []
else: else:
return [set_reply] return [set_reply]
def process_normal_message(text_message: str, mgr, config, launcher_type: str, def process_normal_message(text_message: str, mgr, config: dict, launcher_type: str,
launcher_id: int, sender_id: int) -> list: launcher_id: int, sender_id: int) -> list:
session_name = f"{launcher_type}_{launcher_id}" session_name = f"{launcher_type}_{launcher_id}"
logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + ( logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + (
"..." if len(text_message) > 20 else ""))) "..." if len(text_message) > 20 else "")))
session = pkg.openai.session.get_session(session_name) session = openai_session.get_session(session_name)
unexpected_exception_times = 0 unexpected_exception_times = 0
@@ -38,7 +39,7 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
reply = handle_exception(notify_admin=f"{session_name},多次尝试失败。", set_reply=f"[bot]多次尝试失败,请重试或联系管理员") reply = handle_exception(notify_admin=f"{session_name},多次尝试失败。", set_reply=f"[bot]多次尝试失败,请重试或联系管理员")
break break
try: try:
prefix = "[GPT]" if config.show_prefix else "" prefix = "[GPT]" if config['show_prefix'] else ""
text, finish_reason, funcs = session.query(text_message) text, finish_reason, funcs = session.query(text_message)
@@ -54,7 +55,7 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
"funcs_called": funcs, "funcs_called": funcs,
} }
event = pkg.plugin.host.emit(plugin_models.NormalMessageResponded, **args) event = plugin_host.emit(plugin_models.NormalMessageResponded, **args)
if event.get_return_value("prefix") is not None: if event.get_return_value("prefix") is not None:
prefix = event.get_return_value("prefix") prefix = event.get_return_value("prefix")
@@ -78,29 +79,29 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
if 'message' in e.error and e.error['message'].__contains__('You exceeded your current quota'): if 'message' in e.error and e.error['message'].__contains__('You exceeded your current quota'):
# 尝试切换api-key # 尝试切换api-key
current_key_name = pkg.utils.context.get_openai_manager().key_mgr.get_key_name( current_key_name = context.get_openai_manager().key_mgr.get_key_name(
pkg.utils.context.get_openai_manager().key_mgr.using_key context.get_openai_manager().key_mgr.using_key
) )
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded() context.get_openai_manager().key_mgr.set_current_exceeded()
# 触发插件事件 # 触发插件事件
args = { args = {
'key_name': current_key_name, 'key_name': current_key_name,
'usage': pkg.utils.context.get_openai_manager().audit_mgr 'usage': context.get_openai_manager().audit_mgr
.get_usage(pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()), .get_usage(context.get_openai_manager().key_mgr.get_using_key_md5()),
'exceeded_keys': pkg.utils.context.get_openai_manager().key_mgr.exceeded, 'exceeded_keys': context.get_openai_manager().key_mgr.exceeded,
} }
event = plugin_host.emit(plugin_models.KeyExceeded, **args) event = plugin_host.emit(plugin_models.KeyExceeded, **args)
if not event.is_prevented_default(): if not event.is_prevented_default():
switched, name = pkg.utils.context.get_openai_manager().key_mgr.auto_switch() switched, name = context.get_openai_manager().key_mgr.auto_switch()
if not switched: if not switched:
reply = handle_exception( reply = handle_exception(
"api-key调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key如果你认为这是误判请尝试重启程序。".format( "api-key调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key如果你认为这是误判请尝试重启程序。".format(
current_key_name), "[bot]err:API调用额度超额请联系管理员或等待修复") current_key_name), "[bot]err:API调用额度超额请联系管理员或等待修复")
else: else:
openai.api_key = pkg.utils.context.get_openai_manager().key_mgr.get_using_key() openai.api_key = context.get_openai_manager().key_mgr.get_using_key()
mgr.notify_admin("api-key调用额度超限({}),接口报错,已切换到{}".format(current_key_name, name)) mgr.notify_admin("api-key调用额度超限({}),接口报错,已切换到{}".format(current_key_name, name))
reply = ["[bot]err:API调用额度超额已自动切换请重新发送消息"] reply = ["[bot]err:API调用额度超额已自动切换请重新发送消息"]
continue continue
@@ -117,7 +118,7 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
reply = handle_exception("{}会话调用API失败:{}".format(session_name, e), reply = handle_exception("{}会话调用API失败:{}".format(session_name, e),
"[bot]err:RateLimitError,请重试或联系作者,或等待修复") "[bot]err:RateLimitError,请重试或联系作者,或等待修复")
except openai.BadRequestError as e: except openai.BadRequestError as e:
if config.auto_reset and "This model's maximum context length is" in str(e): if config['auto_reset'] and "This model's maximum context length is" in str(e):
session.reset(persist=True) session.reset(persist=True)
reply = [tips_custom.session_auto_reset_message] reply = [tips_custom.session_auto_reset_message]
else: else:

View File

@@ -1,32 +1,27 @@
# 此模块提供了消息处理的具体逻辑的接口 # 此模块提供了消息处理的具体逻辑的接口
import asyncio import asyncio
import time import time
import traceback
import mirai import mirai
import logging import logging
from mirai import MessageChain, Plain
# 这里不使用动态引入config # 这里不使用动态引入config
# 因为在这里动态引入会卡死程序 # 因为在这里动态引入会卡死程序
# 而此模块静态引用config与动态引入的表现一致 # 而此模块静态引用config与动态引入的表现一致
# 已弃用,由于超时时间现已动态使用 # 已弃用,由于超时时间现已动态使用
# import config as config_init_import # import config as config_init_import
import pkg.openai.session from ..qqbot import ratelimit
import pkg.openai.manager from ..qqbot import command, message
import pkg.utils.reloader from ..openai import session as openai_session
import pkg.utils.updater from ..utils import context
import pkg.utils.context
import pkg.qqbot.message
import pkg.qqbot.command
import pkg.qqbot.ratelimit as ratelimit
import pkg.plugin.host as plugin_host from ..plugin import host as plugin_host
import pkg.plugin.models as plugin_models from ..plugin import models as plugin_models
import pkg.qqbot.ignore as ignore from ..qqbot import ignore
import pkg.qqbot.banlist as banlist from ..qqbot import banlist
import pkg.qqbot.blob as blob from ..qqbot import blob
import tips as tips_custom import tips as tips_custom
processing = [] processing = []
@@ -34,18 +29,18 @@ processing = []
def is_admin(qq: int) -> bool: def is_admin(qq: int) -> bool:
"""兼容list和int类型的管理员判断""" """兼容list和int类型的管理员判断"""
import config config = context.get_config_manager().data
if type(config.admin_qq) == list: if type(config['admin_qq']) == list:
return qq in config.admin_qq return qq in config['admin_qq']
else: else:
return qq == config.admin_qq return qq == config['admin_qq']
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: MessageChain, def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: mirai.MessageChain,
sender_id: int) -> MessageChain: sender_id: int) -> mirai.MessageChain:
global processing global processing
mgr = pkg.utils.context.get_qqbot_manager() mgr = context.get_qqbot_manager()
reply = [] reply = []
session_name = "{}_{}".format(launcher_type, launcher_id) session_name = "{}_{}".format(launcher_type, launcher_id)
@@ -59,10 +54,10 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
logging.info("根据忽略规则忽略消息: {}".format(text_message)) logging.info("根据忽略规则忽略消息: {}".format(text_message))
return [] return []
import config config = context.get_config_manager().data
if not config.wait_last_done and session_name in processing: if not config['wait_last_done'] and session_name in processing:
return MessageChain([Plain(tips_custom.message_drop_tip)]) return mirai.MessageChain([mirai.Plain(tips_custom.message_drop_tip)])
# 检查是否被禁言 # 检查是否被禁言
if launcher_type == 'group': if launcher_type == 'group':
@@ -71,12 +66,11 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
logging.info("机器人被禁言,跳过消息处理(group_{})".format(launcher_id)) logging.info("机器人被禁言,跳过消息处理(group_{})".format(launcher_id))
return reply return reply
import config if config['income_msg_check']:
if config.income_msg_check:
if mgr.reply_filter.is_illegal(text_message): if mgr.reply_filter.is_illegal(text_message):
return MessageChain(Plain("[bot] 消息中存在不合适的内容, 请更换措辞")) return mirai.MessageChain(mirai.Plain("[bot] 消息中存在不合适的内容, 请更换措辞"))
pkg.openai.session.get_session(session_name).acquire_response_lock() openai_session.get_session(session_name).acquire_response_lock()
text_message = text_message.strip() text_message = text_message.strip()
@@ -87,8 +81,6 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
# 处理消息 # 处理消息
try: try:
config = pkg.utils.context.get_config()
processing.append(session_name) processing.append(session_name)
try: try:
if text_message.startswith('!') or text_message.startswith(""): # 指令 if text_message.startswith('!') or text_message.startswith(""): # 指令
@@ -114,17 +106,17 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
reply = event.get_return_value("reply") reply = event.get_return_value("reply")
if not event.is_prevented_default(): if not event.is_prevented_default():
reply = pkg.qqbot.command.process_command(session_name, text_message, reply = command.process_command(session_name, text_message,
mgr, config, launcher_type, launcher_id, sender_id, is_admin(sender_id)) mgr, config, launcher_type, launcher_id, sender_id, is_admin(sender_id))
else: # 消息 else: # 消息
# 限速丢弃检查 # 限速丢弃检查
# print(ratelimit.__crt_minute_usage__[session_name]) # print(ratelimit.__crt_minute_usage__[session_name])
if config.rate_limit_strategy == "drop": if config['rate_limit_strategy'] == "drop":
if ratelimit.is_reach_limit(session_name): if ratelimit.is_reach_limit(session_name):
logging.info("根据限速策略丢弃[{}]消息: {}".format(session_name, text_message)) logging.info("根据限速策略丢弃[{}]消息: {}".format(session_name, text_message))
return MessageChain(["[bot]"+tips_custom.rate_limit_drop_tip]) if tips_custom.rate_limit_drop_tip != "" else [] return mirai.MessageChain(["[bot]"+tips_custom.rate_limit_drop_tip]) if tips_custom.rate_limit_drop_tip != "" else []
before = time.time() before = time.time()
# 触发插件事件 # 触发插件事件
@@ -146,11 +138,11 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
reply = event.get_return_value("reply") reply = event.get_return_value("reply")
if not event.is_prevented_default(): if not event.is_prevented_default():
reply = pkg.qqbot.message.process_normal_message(text_message, reply = message.process_normal_message(text_message,
mgr, config, launcher_type, launcher_id, sender_id) mgr, config, launcher_type, launcher_id, sender_id)
# 限速等待时间 # 限速等待时间
if config.rate_limit_strategy == "wait": if config['rate_limit_strategy'] == "wait":
time.sleep(ratelimit.get_rest_wait_time(session_name, time.time() - before)) time.sleep(ratelimit.get_rest_wait_time(session_name, time.time() - before))
ratelimit.add_usage(session_name) ratelimit.add_usage(session_name)
@@ -170,16 +162,16 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
finally: finally:
processing.remove(session_name) processing.remove(session_name)
finally: finally:
pkg.openai.session.get_session(session_name).release_response_lock() openai_session.get_session(session_name).release_response_lock()
# 检查延迟时间 # 检查延迟时间
if config.force_delay_range[1] == 0: if config['force_delay_range'][1] == 0:
delay_time = 0 delay_time = 0
else: else:
import random import random
# 从延迟范围中随机取一个值(浮点) # 从延迟范围中随机取一个值(浮点)
rdm = random.uniform(config.force_delay_range[0], config.force_delay_range[1]) rdm = random.uniform(config['force_delay_range'][0], config['force_delay_range'][1])
spent = time.time() - start_time spent = time.time() - start_time
@@ -191,4 +183,4 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
logging.info("[风控] 强制延迟{:.2f}秒(如需关闭请到config.py修改force_delay_range字段)".format(delay_time)) logging.info("[风控] 强制延迟{:.2f}秒(如需关闭请到config.py修改force_delay_range字段)".format(delay_time))
time.sleep(delay_time) time.sleep(delay_time)
return MessageChain(reply) return mirai.MessageChain(reply)

View File

@@ -3,6 +3,9 @@ import time
import logging import logging
import threading import threading
from ..utils import context
__crt_minute_usage__ = {} __crt_minute_usage__ = {}
"""当前分钟每个会话的对话次数""" """当前分钟每个会话的对话次数"""
@@ -12,16 +15,16 @@ __timer_thr__: threading.Thread = None
def get_limitation(session_name: str) -> int: def get_limitation(session_name: str) -> int:
"""获取会话的限制次数""" """获取会话的限制次数"""
import config config = context.get_config_manager().data
if type(config.rate_limitation) == dict: if type(config['rate_limitation']) == dict:
# 如果被指定了 # 如果被指定了
if session_name in config.rate_limitation: if session_name in config['rate_limitation']:
return config.rate_limitation[session_name] return config['rate_limitation'][session_name]
else: else:
return config.rate_limitation["default"] return config['rate_limitation']["default"]
elif type(config.rate_limitation) == int: elif type(config['rate_limitation']) == int:
return config.rate_limitation return config['rate_limitation']
def add_usage(session_name: str): def add_usage(session_name: str):

View File

@@ -1,19 +1,19 @@
import mirai
from ..adapter import MessageSourceAdapter, MessageConverter, EventConverter
import nakuru
import nakuru.entities.components as nkc
import asyncio import asyncio
import typing import typing
import traceback import traceback
import logging import logging
import json
from pkg.qqbot.blob import Forward, ForwardMessageNode, ForwardMessageDiaplay import mirai
import nakuru
import nakuru.entities.components as nkc
from .. import adapter as adapter_model
from ...qqbot import blob
from ...utils import context
class NakuruProjectMessageConverter(MessageConverter): class NakuruProjectMessageConverter(adapter_model.MessageConverter):
"""消息转换器""" """消息转换器"""
@staticmethod @staticmethod
def yiri2target(message_chain: mirai.MessageChain) -> list: def yiri2target(message_chain: mirai.MessageChain) -> list:
@@ -49,7 +49,7 @@ class NakuruProjectMessageConverter(MessageConverter):
nakuru_msg_list.append(nkc.Record.fromURL(component.url)) nakuru_msg_list.append(nkc.Record.fromURL(component.url))
elif component.path is not None: elif component.path is not None:
nakuru_msg_list.append(nkc.Record.fromFileSystem(component.path)) nakuru_msg_list.append(nkc.Record.fromFileSystem(component.path))
elif type(component) is Forward: elif type(component) is blob.Forward:
# 转发消息 # 转发消息
yiri_forward_node_list = component.node_list yiri_forward_node_list = component.node_list
nakuru_forward_node_list = [] nakuru_forward_node_list = []
@@ -102,7 +102,7 @@ class NakuruProjectMessageConverter(MessageConverter):
return chain return chain
class NakuruProjectEventConverter(EventConverter): class NakuruProjectEventConverter(adapter_model.EventConverter):
"""事件转换器""" """事件转换器"""
@staticmethod @staticmethod
def yiri2target(event: typing.Type[mirai.Event]): def yiri2target(event: typing.Type[mirai.Event]):
@@ -157,7 +157,7 @@ class NakuruProjectEventConverter(EventConverter):
raise Exception("未支持转换的事件类型: " + str(event)) raise Exception("未支持转换的事件类型: " + str(event))
class NakuruProjectAdapter(MessageSourceAdapter): class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
"""nakuru-project适配器""" """nakuru-project适配器"""
bot: nakuru.CQHTTP bot: nakuru.CQHTTP
bot_account_id: int bot_account_id: int
@@ -173,19 +173,25 @@ class NakuruProjectAdapter(MessageSourceAdapter):
self.listener_list = [] self.listener_list = []
# nakuru库有bug这个接口没法带access_token会失败 # nakuru库有bug这个接口没法带access_token会失败
# 所以目前自行发请求 # 所以目前自行发请求
import config
config = context.get_config_manager().data
import requests import requests
resp = requests.get( resp = requests.get(
url="http://{}:{}/get_login_info".format(config.nakuru_config['host'], config.nakuru_config['http_port']), url="http://{}:{}/get_login_info".format(config['nakuru_config']['host'], config['nakuru_config']['http_port']),
headers={ headers={
'Authorization': "Bearer " + config.nakuru_config['token'] if 'token' in config.nakuru_config else "" 'Authorization': "Bearer " + config['nakuru_config']['token'] if 'token' in config['nakuru_config']else ""
}, },
timeout=5 timeout=5
) )
if resp.status_code == 403: if resp.status_code == 403:
logging.error("go-cqhttp拒绝访问请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配") logging.error("go-cqhttp拒绝访问请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配")
raise Exception("go-cqhttp拒绝访问请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配") raise Exception("go-cqhttp拒绝访问请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配")
self.bot_account_id = int(resp.json()['data']['user_id']) try:
self.bot_account_id = int(resp.json()['data']['user_id'])
except Exception as e:
logging.error("获取go-cqhttp账号信息失败: {}, 请检查是否已启动go-cqhttp并配置正确".format(e))
raise Exception("获取go-cqhttp账号信息失败: {}, 请检查是否已启动go-cqhttp并配置正确".format(e))
def send_message( def send_message(
self, self,
@@ -267,7 +273,7 @@ class NakuruProjectAdapter(MessageSourceAdapter):
logging.debug("注册监听器: " + str(event_type) + " -> " + str(callback)) logging.debug("注册监听器: " + str(event_type) + " -> " + str(callback))
# 包装函数 # 包装函数
async def listener_wrapper(app: nakuru.CQHTTP, source: self.event_converter.yiri2target(event_type)): async def listener_wrapper(app: nakuru.CQHTTP, source: NakuruProjectAdapter.event_converter.yiri2target(event_type)):
callback(self.event_converter.target2yiri(source)) callback(self.event_converter.target2yiri(source))
# 将包装函数和原函数的对应关系存入列表 # 将包装函数和原函数的对应关系存入列表

View File

@@ -1,13 +1,14 @@
from ..adapter import MessageSourceAdapter import asyncio
import typing
import mirai import mirai
import mirai.models.bus import mirai.models.bus
from mirai.bot import MiraiRunner from mirai.bot import MiraiRunner
import asyncio from .. import adapter as adapter_model
import typing
class YiriMiraiAdapter(MessageSourceAdapter): class YiriMiraiAdapter(adapter_model.MessageSourceAdapter):
"""YiriMirai适配器""" """YiriMirai适配器"""
bot: mirai.Mirai bot: mirai.Mirai

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,13 @@
from __future__ import annotations
import threading import threading
from pkg.utils import ThreadCtl from . import threadctl
from ..database import manager as db_mgr
from ..openai import manager as openai_mgr
from ..qqbot import manager as qqbot_mgr
from ..config import manager as config_mgr
from ..plugin import host as plugin_host
context = { context = {
@@ -7,6 +15,7 @@ context = {
'database.manager.DatabaseManager': None, 'database.manager.DatabaseManager': None,
'openai.manager.OpenAIInteract': None, 'openai.manager.OpenAIInteract': None,
'qqbot.manager.QQBotManager': None, 'qqbot.manager.QQBotManager': None,
'config.manager.ConfigManager': None,
}, },
'pool_ctl': None, 'pool_ctl': None,
'logger_handler': None, 'logger_handler': None,
@@ -29,66 +38,79 @@ def get_config():
return t return t
def set_database_manager(inst): def set_database_manager(inst: db_mgr.DatabaseManager):
context_lock.acquire() context_lock.acquire()
context['inst']['database.manager.DatabaseManager'] = inst context['inst']['database.manager.DatabaseManager'] = inst
context_lock.release() context_lock.release()
def get_database_manager(): def get_database_manager() -> db_mgr.DatabaseManager:
context_lock.acquire() context_lock.acquire()
t = context['inst']['database.manager.DatabaseManager'] t = context['inst']['database.manager.DatabaseManager']
context_lock.release() context_lock.release()
return t return t
def set_openai_manager(inst): def set_openai_manager(inst: openai_mgr.OpenAIInteract):
context_lock.acquire() context_lock.acquire()
context['inst']['openai.manager.OpenAIInteract'] = inst context['inst']['openai.manager.OpenAIInteract'] = inst
context_lock.release() context_lock.release()
def get_openai_manager(): def get_openai_manager() -> openai_mgr.OpenAIInteract:
context_lock.acquire() context_lock.acquire()
t = context['inst']['openai.manager.OpenAIInteract'] t = context['inst']['openai.manager.OpenAIInteract']
context_lock.release() context_lock.release()
return t return t
def set_qqbot_manager(inst): def set_qqbot_manager(inst: qqbot_mgr.QQBotManager):
context_lock.acquire() context_lock.acquire()
context['inst']['qqbot.manager.QQBotManager'] = inst context['inst']['qqbot.manager.QQBotManager'] = inst
context_lock.release() context_lock.release()
def get_qqbot_manager(): def get_qqbot_manager() -> qqbot_mgr.QQBotManager:
context_lock.acquire() context_lock.acquire()
t = context['inst']['qqbot.manager.QQBotManager'] t = context['inst']['qqbot.manager.QQBotManager']
context_lock.release() context_lock.release()
return t return t
def set_plugin_host(inst): def set_config_manager(inst: config_mgr.ConfigManager):
context_lock.acquire()
context['inst']['config.manager.ConfigManager'] = inst
context_lock.release()
def get_config_manager() -> config_mgr.ConfigManager:
context_lock.acquire()
t = context['inst']['config.manager.ConfigManager']
context_lock.release()
return t
def set_plugin_host(inst: plugin_host.PluginHost):
context_lock.acquire() context_lock.acquire()
context['plugin_host'] = inst context['plugin_host'] = inst
context_lock.release() context_lock.release()
def get_plugin_host(): def get_plugin_host() -> plugin_host.PluginHost:
context_lock.acquire() context_lock.acquire()
t = context['plugin_host'] t = context['plugin_host']
context_lock.release() context_lock.release()
return t return t
def set_thread_ctl(inst): def set_thread_ctl(inst: threadctl.ThreadCtl):
context_lock.acquire() context_lock.acquire()
context['pool_ctl'] = inst context['pool_ctl'] = inst
context_lock.release() context_lock.release()
def get_thread_ctl() -> ThreadCtl: def get_thread_ctl() -> threadctl.ThreadCtl:
context_lock.acquire() context_lock.acquire()
t: ThreadCtl = context['pool_ctl'] t: threadctl.ThreadCtl = context['pool_ctl']
context_lock.release() context_lock.release()
return t return t

View File

@@ -3,6 +3,8 @@ import time
import logging import logging
import shutil import shutil
from . import context
log_file_name = "qchatgpt.log" log_file_name = "qchatgpt.log"
@@ -36,7 +38,6 @@ def init_runtime_log_file():
def reset_logging(): def reset_logging():
global log_file_name global log_file_name
import config
import pkg.utils.context import pkg.utils.context
import colorlog import colorlog
@@ -46,7 +47,11 @@ def reset_logging():
for handler in logging.getLogger().handlers: for handler in logging.getLogger().handlers:
logging.getLogger().removeHandler(handler) logging.getLogger().removeHandler(handler)
logging.basicConfig(level=config.logging_level, # 设置日志输出格式 config_mgr = context.get_config_manager()
logging_level = logging.INFO if config_mgr is None else config_mgr.data['logging_level']
logging.basicConfig(level=logging_level, # 设置日志输出格式
filename=log_file_name, # log日志输出的文件位置和文件名 filename=log_file_name, # log日志输出的文件位置和文件名
format="[%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s", format="[%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s",
# 日志输出的格式 # 日志输出的格式
@@ -54,7 +59,7 @@ def reset_logging():
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式 datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
) )
sh = logging.StreamHandler() sh = logging.StreamHandler()
sh.setLevel(config.logging_level) sh.setLevel(logging_level)
sh.setFormatter(colorlog.ColoredFormatter( sh.setFormatter(colorlog.ColoredFormatter(
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : " fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
"%(message)s", "%(message)s",

View File

@@ -1,9 +1,11 @@
from . import context
def wrapper_proxies() -> dict: def wrapper_proxies() -> dict:
"""获取代理""" """获取代理"""
import config config = context.get_config_manager().data
return { return {
"http": config.openai_config['proxy'], "http": config['openai_config']['proxy'],
"https": 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 } if 'proxy' in config['openai_config'] and (config['openai_config']['proxy'] is not None) else None

View File

@@ -1,6 +1,6 @@
from pip._internal import main as pipmain from pip._internal import main as pipmain
import pkg.utils.log as log from . import log
def install(package): def install(package):
@@ -19,7 +19,7 @@ def run_pip(params: list):
def install_requirements(file): def install_requirements(file):
pipmain(['install', '-r', file, "--upgrade", "-i", "https://pypi.tuna.tsinghua.edu.cn/simple", pipmain(['install', '-r', file, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"]) "--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
log.reset_logging() log.reset_logging()

View File

@@ -1,10 +1,10 @@
import logging import logging
import threading
import importlib import importlib
import pkgutil import pkgutil
import pkg.utils.context as context import asyncio
import pkg.plugin.host
from . import context
from ..plugin import host as plugin_host
def walk(module, prefix='', path_prefix=''): def walk(module, prefix='', path_prefix=''):
@@ -15,7 +15,7 @@ def walk(module, prefix='', path_prefix=''):
walk(__import__(module.__name__ + '.' + item.name, fromlist=['']), prefix + item.name + '.', path_prefix + item.name + '/') walk(__import__(module.__name__ + '.' + item.name, fromlist=['']), prefix + item.name + '.', path_prefix + item.name + '/')
else: else:
logging.info('reload module: {}, path: {}'.format(prefix + item.name, path_prefix + item.name + '.py')) 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' plugin_host.__current_module_path__ = "plugins/" + path_prefix + item.name + '.py'
importlib.reload(__import__(module.__name__ + '.' + item.name, fromlist=[''])) importlib.reload(__import__(module.__name__ + '.' + item.name, fromlist=['']))
@@ -53,15 +53,17 @@ def reload_all(notify=True):
# 执行启动流程 # 执行启动流程
logging.info("执行程序启动流程") logging.info("执行程序启动流程")
main.load_config()
main.complete_tips()
context.get_thread_ctl().reload( context.get_thread_ctl().reload(
admin_pool_num=context.get_config().admin_pool_num, admin_pool_num=4,
user_pool_num=context.get_config().user_pool_num user_pool_num=8
) )
def run_wrapper():
asyncio.run(main.start_process(False))
context.get_thread_ctl().submit_sys_task( context.get_thread_ctl().submit_sys_task(
main.start, run_wrapper
False
) )
logging.info('程序启动完成') logging.info('程序启动完成')

View File

@@ -1,37 +1,42 @@
import logging import logging
from PIL import Image, ImageDraw, ImageFont
import re import re
import os import os
import config
import traceback import traceback
from PIL import Image, ImageDraw, ImageFont
from ..utils import context
text_render_font: ImageFont = None text_render_font: ImageFont = None
if config.blob_message_strategy == "image": # 仅在启用了image时才加载字体 def initialize():
use_font = config.font_path config = context.get_config_manager().data
try:
# 检查是否存在 if config['blob_message_strategy'] == "image": # 仅在启用了image时才加载字体
if not os.path.exists(use_font): use_font = config['font_path']
# 若是windows系统使用微软雅黑 try:
if os.name == "nt":
use_font = "C:/Windows/Fonts/msyh.ttc" # 检查是否存在
if not os.path.exists(use_font): if not os.path.exists(use_font):
logging.warn("未找到字体文件且无法使用Windows自带字体更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。") # 若是windows系统使用微软雅黑
config.blob_message_strategy = "forward" 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: else:
logging.info("使用Windows自带字体" + use_font) logging.warn("未找到字体文件,且无法使用Windows自带字体更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。")
text_render_font = ImageFont.truetype(use_font, 32, encoding="utf-8") config['blob_message_strategy'] = "forward"
else: else:
logging.warn("未找到字体文件且无法使用Windows自带字体更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。") text_render_font = ImageFont.truetype(use_font, 32, encoding="utf-8")
config.blob_message_strategy = "forward" except:
else: traceback.print_exc()
text_render_font = ImageFont.truetype(use_font, 32, encoding="utf-8") logging.error("加载字体文件失败({})更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。".format(use_font))
except: config['blob_message_strategy'] = "forward"
traceback.print_exc()
logging.error("加载字体文件失败({})更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。".format(use_font))
config.blob_message_strategy = "forward"
def indexNumber(path=''): def indexNumber(path=''):

View File

@@ -3,10 +3,9 @@ import logging
import os.path import os.path
import requests import requests
import json
import pkg.utils.constants from . import constants
import pkg.utils.network as network from . import network
def check_dulwich_closure(): def check_dulwich_closure():
@@ -70,7 +69,7 @@ def get_release_list() -> list:
def get_current_tag() -> str: def get_current_tag() -> str:
"""获取当前tag""" """获取当前tag"""
current_tag = pkg.utils.constants.semantic_version current_tag = constants.semantic_version
if os.path.exists("current_tag"): if os.path.exists("current_tag"):
with open("current_tag", "r") as f: with open("current_tag", "r") as f:
current_tag = f.read() current_tag = f.read()
@@ -204,9 +203,9 @@ def update_all(cli: bool = False) -> bool:
# 通知管理员 # 通知管理员
if not cli: if not cli:
import pkg.utils.context import pkg.utils.context
pkg.utils.context.get_qqbot_manager().notify_admin("已更新到最新版本: {}\n更新日志:\n{}\n完整的更新日志请前往 https://github.com/RockChinQ/QChatGPT/releases 查看".format(current_tag, "\n".join(rls_notes[:-1]))) pkg.utils.context.get_qqbot_manager().notify_admin("已更新到最新版本: {}\n更新日志:\n{}\n完整的更新日志请前往 https://github.com/RockChinQ/QChatGPT/releases 查看\n请手动重启程序以使用新版本。".format(current_tag, "\n".join(rls_notes[:-1])))
else: else:
print("已更新到最新版本: {}\n更新日志:\n{}\n完整的更新日志请前往 https://github.com/RockChinQ/QChatGPT/releases 查看".format(current_tag, "\n".join(rls_notes[:-1]))) print("已更新到最新版本: {}\n更新日志:\n{}\n完整的更新日志请前往 https://github.com/RockChinQ/QChatGPT/releases 查看。请手动重启程序以使用新版本。".format(current_tag, "\n".join(rls_notes[:-1])))
return True return True

View File

@@ -2,7 +2,7 @@ requests
openai openai
dulwich~=0.21.6 dulwich~=0.21.6
colorlog~=6.6.0 colorlog~=6.6.0
yiri-mirai yiri-mirai-rc
websockets websockets
urllib3 urllib3
func_timeout~=4.3.5 func_timeout~=4.3.5

View File

@@ -4,5 +4,23 @@
"time": "2023-08-01 10:49:26", "time": "2023-08-01 10:49:26",
"timestamp": 1690858166, "timestamp": 1690858166,
"content": "现已支持GPT函数调用功能欢迎了解https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8-%E5%86%85%E5%AE%B9%E5%87%BD%E6%95%B0" "content": "现已支持GPT函数调用功能欢迎了解https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8-%E5%86%85%E5%AE%B9%E5%87%BD%E6%95%B0"
},
{
"id": 3,
"time": "2023-11-10 12:20:09",
"timestamp": 1699590009,
"content": "OpenAI 库1.0版本已发行,若出现 OpenAI 调用问题,请更新 QChatGPT 版本。详见项目主页https://github.com/RockChinQ/QChatGPT"
},
{
"id": 4,
"time": "2023-11-13 18:02:39",
"timestamp": 1699869759,
"content": "近期 OpenAI 接口改动频繁正在积极适配并添加新功能请尽快更新到最新版本更新方式https://github.com/RockChinQ/QChatGPT/discussions/595"
},
{
"id": 5,
"time": "2023-12-07 9:20:00",
"timestamp": 1701912000,
"content": "QChatGPT 一周年啦感谢大家的选择和支持RockChinQ 在此衷心感谢素未谋面但又至关重要的你们每一个人,愿 AI 与我们同在欢迎前往https://github.com/RockChinQ/QChatGPT/discussions/627 参与讨论。"
} }
] ]

View File

@@ -1,5 +1,5 @@
> **Warning** > [!WARNING]
> 此文档已过时,请查看[QChatGPT 容器化部署指南](docker_deployment.md) > 此文档已过时,请查看[QChatGPT 容器化部署指南](docker_deployment.md)
## 操作步骤 ## 操作步骤

View File

@@ -1,6 +1,6 @@
# QChatGPT 容器化部署指南 # QChatGPT 容器化部署指南
> **Warning** > [!WARNING]
> 请您确保您**确实**需要 Docker 部署,您**必须**具有以下能力: > 请您确保您**确实**需要 Docker 部署,您**必须**具有以下能力:
> - 了解 `Docker` 和 `Docker Compose` 的使用 > - 了解 `Docker` 和 `Docker Compose` 的使用
> - 了解容器间网络通信配置方式 > - 了解容器间网络通信配置方式
@@ -15,7 +15,7 @@
QChatGPT 主程序需要连接`QQ登录框架`以与QQ通信您可以选择 [Mirai](https://github.com/mamoe/mirai)还需要配置mirai-api-http请查看此仓库README中手动部署部分 或 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp),我们仅发布 QChatGPT主程序 的镜像您需要自行配置QQ登录框架可以参考[README.md](https://github.com/RockChinQ/QChatGPT#-%E9%85%8D%E7%BD%AEqq%E7%99%BB%E5%BD%95%E6%A1%86%E6%9E%B6)中的教程,或自行寻找其镜像)并在 QChatGPT 的配置文件中设置连接地址。 QChatGPT 主程序需要连接`QQ登录框架`以与QQ通信您可以选择 [Mirai](https://github.com/mamoe/mirai)还需要配置mirai-api-http请查看此仓库README中手动部署部分 或 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp),我们仅发布 QChatGPT主程序 的镜像您需要自行配置QQ登录框架可以参考[README.md](https://github.com/RockChinQ/QChatGPT#-%E9%85%8D%E7%BD%AEqq%E7%99%BB%E5%BD%95%E6%A1%86%E6%9E%B6)中的教程,或自行寻找其镜像)并在 QChatGPT 的配置文件中设置连接地址。
> **Note** > [!NOTE]
> 请先确保 Docker 和 Docker Compose 已安装 > 请先确保 Docker 和 Docker Compose 已安装
## 准备文件 ## 准备文件

View File

@@ -0,0 +1,7 @@
import re
repo_url = "git@github.com:RockChinQ/WebwlkrPlugin.git"
repo = re.findall(r'(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)', repo_url)
print(repo)