Compare commits

...

82 Commits
v2.6.7 ... v2

Author SHA1 Message Date
RockChinQ
0984c19fd9 doc(README): 说明 Python 版本 2024-01-22 20:37:29 +08:00
RockChinQ
a10d3213fd chore: release v2.6.10 2024-01-19 15:50:15 +08:00
RockChinQ
f52a0eb02f perf: 连接go-cqhttp时不使用代理 2024-01-19 15:49:42 +08:00
Junyan Qin
1ea8da69a2 Merge pull request #667 from RockChinQ/chore/remove-legacy-code
Chore: 移除过时的兼容性处理代码
2024-01-18 01:02:32 +08:00
RockChinQ
5bbc38a7a3 chore: 移除过时的兼容性处理代码 2024-01-18 00:52:29 +08:00
RockChinQ
aa433bd5ab fix: 修复文字转图片模块初始化时的bug 2024-01-17 20:07:35 +08:00
RockChinQ
2c5933da0b chore: 删除updater中不再使用的代码 2024-01-15 22:35:14 +08:00
RockChinQ
77bc6fbf59 fix(list): 列出不存在的页时失败 2024-01-15 21:44:53 +08:00
Junyan Qin
701cb7be40 Merge pull request #661 from RockChinQ/perf/audit-v2
Feat: 优化 v2 审计 API 调用逻辑
2024-01-12 20:18:30 +08:00
RockChinQ
ab8d77c968 feat: 删除 v1 审计 API 调用逻辑 2024-01-12 20:06:18 +08:00
RockChinQ
6c03fe678a feat: 允许用户关闭数据上报 2024-01-12 17:20:39 +08:00
RockChinQ
41b30238c3 chore: 指令全部改为命令 2024-01-12 16:48:47 +08:00
RockChinQ
aa768459c0 perf: 配置项目标值不合法时的输出 2024-01-12 16:29:04 +08:00
RockChinQ
28014512f7 fix(cconfig): cfg 命令找不到配置项时的处理错误 2024-01-12 16:25:10 +08:00
RockChinQ
f9a99eed66 chore: 删除已被OpenAI弃用的模型 (#658) 2024-01-12 14:48:49 +08:00
Junyan Qin
461b574e09 Merge pull request #659 from RockChinQ/fix/resend-command-failed
Fix: resend 命令失效
2024-01-12 14:40:07 +08:00
RockChinQ
36c192ff6b fix: resend 命令失效 2024-01-12 14:31:29 +08:00
RockChinQ
101625965c chore: 删除对 credit 的引用 2024-01-12 10:18:10 +08:00
RockChinQ
83177a3416 chore: 移除弃用的 credit.py 模块 2024-01-12 10:09:53 +08:00
Junyan Qin
c3904786e1 doc(README.md): 添加链接 2024-01-10 23:11:02 +08:00
RockChinQ
b31c34905a test: 自动上传覆盖率 2023-12-28 16:14:54 +08:00
RockChinQ
41cbe91870 doc(README): 添加测试覆盖率徽章 2023-12-28 16:03:55 +08:00
Junyan Qin
872b16b779 ci: 删除注释 2023-12-27 16:00:18 +00:00
Junyan Qin
9f3cc9c293 test: 修正错误的引号 2023-12-27 15:56:52 +00:00
Junyan Qin
2d148c4970 test: 处理多行响应值 2023-12-27 15:52:12 +00:00
Junyan Qin
0869b57741 test: install jq 2023-12-27 15:48:26 +00:00
Junyan Qin
af225aa18f test: 错误的逻辑 2023-12-27 15:44:24 +00:00
Junyan Qin
06f3c5d32b test: 分支名获取方式 2023-12-27 15:39:08 +00:00
Junyan Qin
4e71a08b57 test: 完善issues_comment时的pr分支获取逻辑 2023-12-27 15:35:25 +00:00
Junyan Qin
bf5ebc9245 test: 错误的触发名称 2023-12-27 15:23:53 +00:00
Junyan Qin
fba81582ab test: 完善触发方式 2023-12-27 15:16:07 +00:00
Junyan Qin
b4645168f9 Merge pull request #649 from RockChinQ/test/systematical-test
Test: 集成qcg-tester
2023-12-27 22:50:35 +08:00
Junyan Qin
d00c68e329 test: 允许手动触发 2023-12-27 14:49:00 +00:00
Junyan Qin
cb636b96bf test: 集成qcg-tester 2023-12-27 14:47:02 +00:00
GitHub Actions
12468b5b15 Update override-all.json 2023-12-23 02:32:13 +00:00
RockChinQ
6a5414b5fd chore: prompt_submit_length默认改为3072 2023-12-23 10:31:56 +08:00
RockChinQ
db51fd0ad7 chore: release v2.6.9 2023-12-22 18:34:35 +08:00
Junyan Qin
256bc4dc1e Merge pull request #644 from RockChinQ/feat/online-data-analysis
Feat: v2 数据统计接口
2023-12-22 18:33:50 +08:00
RockChinQ
d2bd6e23b6 chore: 删除调试输出 2023-12-22 14:36:52 +08:00
RockChinQ
bb12b48887 feat: usage.query完成 2023-12-22 12:38:27 +08:00
RockChinQ
a58e55daf3 chore: 更新issue模板 2023-12-22 11:11:31 +08:00
RockChinQ
23a05fe5b0 chore: 完善issue模板 2023-12-22 11:03:25 +08:00
RockChinQ
3a63630068 feat: account_id 设置逻辑 2023-12-21 18:51:10 +08:00
RockChinQ
565066bbcd feat: 插件相关上报 API 2023-12-21 18:46:48 +08:00
RockChinQ
c10f72cf4c feat: 内容函数调用报告 2023-12-21 18:36:02 +08:00
RockChinQ
af8c21f3d4 feat: 完善 插件事件调用报告 2023-12-21 18:19:04 +08:00
RockChinQ
6f6c3af302 feat: 插件事件触发报告 2023-12-21 18:04:16 +08:00
RockChinQ
61a47808c8 chore: typo 2023-12-21 17:35:20 +08:00
RockChinQ
e02765bf95 feat: main.announcement 接口 2023-12-21 17:11:45 +08:00
RockChinQ
b69f193a3e feat: main.update 接口完成 2023-12-21 17:03:58 +08:00
RockChinQ
7c6526d1ea feat: 改为同步 2023-12-21 16:48:50 +08:00
RockChinQ
b8776fba65 chore: stash 2023-12-21 16:44:21 +08:00
RockChinQ
38357dd68d perf: 简化启动输出 2023-12-21 16:28:45 +08:00
RockChinQ
d1c2453310 feat: 启动时初始化中央服务器 API 交互类 2023-12-21 16:21:24 +08:00
RockChinQ
ebc1ac50c6 doc: 更新 README 2023-12-21 10:22:53 +08:00
RockChinQ
892610872f chore: 更新 submit-plugin 模板 2023-12-21 10:20:19 +08:00
RockChinQ
a990a40850 chore: 更新issues模板 2023-12-21 10:19:02 +08:00
RockChinQ
3f29464dbd feat: 标识符生成器模块 2023-12-20 22:26:51 +08:00
RockChinQ
998d07f3b4 doc(wiki): 添加已迁移说明 2023-12-20 22:10:19 +08:00
Junyan Qin
949bc6268c Update README.md 2023-12-20 22:05:12 +08:00
Junyan Qin
2c03e5a77e doc(README): 更改效果图为主页中的图片 2023-12-20 21:54:20 +08:00
Junyan Qin
aad62dfa6f Merge pull request #642 from RockChinQ/doc/document-replacing
Doc: 替换主文档
2023-12-20 21:47:11 +08:00
Junyan Qin
08e27d07ea 更新 README.md 2023-12-20 21:44:08 +08:00
Junyan Qin
1fddd244e5 更新 README.md 2023-12-20 21:43:48 +08:00
Junyan Qin
d85b4b1cf0 doc(README.md): 替换logo为主页上的链接 2023-12-20 21:43:03 +08:00
RockChinQ
09fca2c292 doc(README): 应用更改 2023-12-20 21:34:44 +08:00
RockChinQ
feda3d18fb doc: 修改主页布局 2023-12-20 17:57:28 +08:00
Junyan Qin
eb6e5d0756 Merge pull request #640 from RockChinQ/fix/cfg-command
Fix: cfg 命令无法使用
2023-12-19 17:40:33 +08:00
RockChinQ
7386daad28 fix: cfg 命令无法使用 (#638) 2023-12-19 17:37:40 +08:00
RockChinQ
3f290b2e1a feat: 命令回复不再通过敏感词检查 2023-12-18 16:31:45 +08:00
RockChinQ
43519ffe80 doc(wiki): 添加插件 API 讨论链接 2023-12-17 23:25:56 +08:00
RockChinQ
c8bb3d612a chore: release v2.6.8 2023-12-17 23:00:25 +08:00
Junyan Qin
bc48b7e623 Merge pull request #636 from RockChinQ/feat/google-gemini
Feat: 支持 Google Gemini Pro 模型
2023-12-17 22:59:34 +08:00
RockChinQ
d59d5797f6 doc(README.md): 删除 PaLM-2 说明 2023-12-17 22:55:06 +08:00
RockChinQ
11d3c1e650 doc(README.md): 添加模型说明 2023-12-17 22:53:50 +08:00
RockChinQ
8cfd9e6694 chore: 添加配置项说明 2023-12-17 22:48:48 +08:00
RockChinQ
d3f401c54d feat: 通过 one-api 支持google gemini 2023-12-17 22:36:30 +08:00
Junyan Qin
a889170d1a Merge pull request #634 from zuo-shi-yun/master
添加AutoSwitchProxy插件
2023-12-17 16:19:47 +08:00
zuo-shi-yun
459e9f9322 添加AutoSwitchProxy插件 2023-12-17 13:15:33 +08:00
Junyan Qin
707afdcdf9 Update bug-report.yml 2023-12-15 10:38:04 +08:00
RockChinQ
ad1cf379c4 doc: 删除公告 2023-12-11 21:57:57 +08:00
RockChinQ
582277fe2d doc: 更新 效果图 2023-12-11 21:56:00 +08:00
56 changed files with 1023 additions and 725 deletions

View File

@@ -26,8 +26,8 @@ body:
- type: input
attributes:
label: 系统环境
description: 操作系统、系统架构。
placeholder: 例如: CentOS x64、Windows11
description: 操作系统、系统架构、**主机地理位置**,地理位置最好写清楚,涉及网络问题排查
placeholder: 例如: CentOS x64 中国大陆、Windows11 美国
validations:
required: true
- type: input
@@ -37,15 +37,28 @@ body:
placeholder: 例如: Python 3.10
validations:
required: true
- type: textarea
- type: input
attributes:
label: 异常情况
description: 完整描述异常情况,什么时候发生的、发生了什么
label: QChatGPT版本
description: QChatGPT版本号
placeholder: 例如: v2.6.0,可以使用`!version`命令查看
validations:
required: true
- type: textarea
attributes:
label: 报错信息
description: 请提供完整的**控制台**报错信息(若有)
label: 异常情况
description: 完整描述异常情况,什么时候发生的、发生了什么,尽可能详细
validations:
required: true
- type: textarea
attributes:
label: 日志信息
description: 请提供完整的 **登录框架 和 QChatGPT控制台**的相关日志信息(若有),不提供日志信息**无法**为您排查问题,请尽可能详细
validations:
required: false
- type: textarea
attributes:
label: 启用的插件
description: 有些情况可能和插件功能有关,建议提供插件启用情况。可以使用`!plugin`命令查看已启用的插件
validations:
required: false

View File

@@ -1,6 +1,6 @@
name: 需求建议
title: "[Feature]: "
labels: ["enhancement"]
labels: ["改进"]
description: "新功能或现有功能优化请使用这个模板不符合类别的issue将被直接关闭"
body:
- type: dropdown

View File

@@ -0,0 +1,24 @@
name: 提交新插件
title: "[Plugin]: 请求登记新插件"
labels: ["独立插件"]
description: "本模板供且仅供提交新插件使用"
body:
- type: input
attributes:
label: 插件名称
description: 填写插件的名称
validations:
required: true
- type: textarea
attributes:
label: 插件代码库地址
description: 仅支持 Github
validations:
required: true
- type: textarea
attributes:
label: 插件简介
description: 插件的简介
validations:
required: true

View File

@@ -1,11 +1,6 @@
name: Update Wiki
on:
pull_request:
branches:
- master
paths:
- 'res/wiki/**'
push:
branches:
- master

80
.github/workflows/test-pr.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Test Pull Request
on:
pull_request:
types: [ready_for_review]
paths:
# 任何py文件改动都会触发
- '**.py'
pull_request_review:
types: [submitted]
issue_comment:
types: [created]
# 允许手动触发
workflow_dispatch:
jobs:
perform-test:
runs-on: ubuntu-latest
# 如果事件为pull_request_review且review状态为approved则执行
if: >
github.event_name == 'pull_request' ||
(github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED') ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request != '' && contains(github.event.comment.body, '/test') && github.event.comment.user.login == 'RockChinQ')
steps:
# 签出测试工程仓库代码
- name: Checkout
uses: actions/checkout@v2
with:
# 仓库地址
repository: RockChinQ/qcg-tester
# 仓库路径
path: qcg-tester
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install dependencies
run: |
cd qcg-tester
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Get PR details
id: get-pr
if: github.event_name == 'issue_comment'
uses: octokit/request-action@v2.x
with:
route: GET /repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set PR source branch as env variable
if: github.event_name == 'issue_comment'
run: |
PR_SOURCE_BRANCH=$(echo '${{ steps.get-pr.outputs.data }}' | jq -r '.head.ref')
echo "BRANCH=$PR_SOURCE_BRANCH" >> $GITHUB_ENV
- name: Set PR Branch as bash env
if: github.event_name != 'issue_comment'
run: |
echo "BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
- name: Set OpenAI API Key from Secrets
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV
- name: Set OpenAI Reverse Proxy URL from Secrets
run: |
echo "OPENAI_REVERSE_PROXY=${{ secrets.OPENAI_REVERSE_PROXY }}" >> $GITHUB_ENV
- name: Run test
run: |
cd qcg-tester
python main.py
- name: Upload coverage reports to Codecov
run: |
cd qcg-tester/resource/QChatGPT
curl -Os https://uploader.codecov.io/latest/linux/codecov
chmod +x codecov
./codecov -t ${{ secrets.CODECOV_TOKEN }}

4
.gitignore vendored
View File

@@ -30,4 +30,6 @@ qcapi
claude.json
bard.json
/*yaml
!/docker-compose.yaml
!/docker-compose.yaml
res/instance_id.json
.DS_Store

365
README.md
View File

@@ -1,30 +1,30 @@
<p align="center">
<img src="res/logo.png" alt="QChatGPT" width="120" />
<img src="https://qchatgpt.rockchin.top/logo.png" alt="QChatGPT" width="180" />
</p>
<div align="center">
# QChatGPT
<blockquote> 🥳 QChatGPT 一周年啦,感谢大家的支持!欢迎前往<a href="https://github.com/RockChinQ/QChatGPT/discussions/627">讨论</a>。</blockquote>
[![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">
<img src="https://img.shields.io/docker/pulls/rockchin/qchatgpt?color=blue" alt="docker pull">
</a>
![Wakapi Count](https://wakapi.dev/api/badge/RockChinQ/interval:any/project:QChatGPT)
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
<a href="https://github.com/RockChinQ/QChatGPT/wiki">
<img alt="Static Badge" src="https://img.shields.io/badge/%E6%9F%A5%E7%9C%8B-%E9%A1%B9%E7%9B%AEWiki-blue">
</a><br/>
<a href="https://codecov.io/gh/RockChinQ/QChatGPT" >
<img src="https://codecov.io/gh/RockChinQ/QChatGPT/graph/badge.svg?token=pjxYIL2kbC"/>
</a>
<br/>
<img src="https://img.shields.io/badge/python-3.9 | 3.10 | 3.11-blue.svg" alt="python">
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=66-aWvn8cbP4c1ut_1YYkvvGVeEtyTH8&authKey=pTaKBK5C%2B8dFzQ4XlENf6MHTCLaHnlKcCRx7c14EeVVlpX2nRSaS8lJm8YeM4mCU&noverify=0&group_code=195992197">
<img alt="Static Badge" src="https://img.shields.io/badge/%E5%AE%98%E6%96%B9%E7%BE%A4-195992197-purple">
</a>
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nC80H57wmKPwRDLFeQrDDjVl81XuC21P&authKey=2wTUTfoQ5v%2BD4C5zfpuR%2BSPMDqdXgDXA%2FS2wHI1NxTfWIG%2B%2FqK08dgyjMMOzhXa9&noverify=0&group_code=738382634">
<img alt="Static Badge" src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA%E7%BE%A4-738382634-purple">
</a>
<a href="https://qchatgpt.rockchin.top">
<img alt="Static Badge" src="https://img.shields.io/badge/%E6%9F%A5%E7%9C%8B-%E7%A4%BE%E5%8C%BA%E7%BC%96%E5%86%99%E4%BD%BF%E7%94%A8%E6%89%8B%E5%86%8C-blue">
</a>
<a href="https://www.bilibili.com/video/BV14h4y1w7TC">
<img alt="Static Badge" src="https://img.shields.io/badge/%E8%A7%86%E9%A2%91%E6%95%99%E7%A8%8B-208647">
</a>
@@ -32,339 +32,20 @@
<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>
<blockquote> 🥳 QChatGPT 一周年啦,感谢大家的支持!欢迎前往<a href="https://github.com/RockChinQ/QChatGPT/discussions/627">讨论</a>。</blockquote>
## 使用文档
<details>
<summary>回复效果演示(带有联网插件)</summary>
<img alt="联网演示GIF" src="res/webwlkr-demo.gif" width="300px">
</details>
<a href="https://qchatgpt.rockchin.top">项目主页</a>
<a href="https://qchatgpt.rockchin.top/posts/feature.html">功能介绍</a>
<a href="https://qchatgpt.rockchin.top/posts/deploy/">部署文档</a>
<a href="https://qchatgpt.rockchin.top/posts/error/">常见问题</a>
<a href="https://qchatgpt.rockchin.top/posts/plugin/intro.html">插件介绍</a>
<a href="https://github.com/RockChinQ/QChatGPT/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
## 相关链接
<a href="https://github.com/RockChinQ/qcg-installer">安装器源码</a>
<a href="https://github.com/RockChinQ/qcg-tester">测试工程源码</a>
<a href="https://github.com/the-lazy-me/QChatGPT-Wiki">官方文档储存库</a>
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/>
</div>
> [!NOTE]
> 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/14 [逆向库插件](https://github.com/RockChinQ/revLibs)已支持Claude和Bard
> 2023/7/29 支持使用GPT的Function Calling功能实现类似ChatGPT Plugin的效果请见[Wiki内容函数](https://github.com/RockChinQ/QChatGPT/wiki/6-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8-%E5%86%85%E5%AE%B9%E5%87%BD%E6%95%B0)
<details>
<summary>
## 🍺模型一览和功能点
</summary>
### 文字对话
- OpenAI GPT-3.5模型(ChatGPT API), 本项目原生支持, 默认使用
- OpenAI GPT-3模型, 本项目原生支持, 部署完成后前往`config.py`切换
- OpenAI GPT-4模型, 本项目原生支持, 目前需要您的账户通过OpenAI的内测申请, 请前往`config.py`切换
- ChatGPT网页版GPT-3.5模型, 由[插件](https://github.com/RockChinQ/revLibs)接入
- ChatGPT网页版GPT-4模型, 目前需要ChatGPT Plus订阅, 由[插件](https://github.com/RockChinQ/revLibs)接入
- New Bing逆向库, 由[插件](https://github.com/RockChinQ/revLibs)接入
- HuggingChat, 由[插件](https://github.com/RockChinQ/revLibs)接入, 仅支持英文
- Claude, 由[插件](https://github.com/RockChinQ/revLibs)接入
- Google Bard, 由[插件](https://github.com/RockChinQ/revLibs)接入
### 模型聚合平台
- [One API](https://github.com/songquanpeng/one-api), Azure、Anthropic Claude、Google PaLM 2、智谱 ChatGLM、百度文心一言、讯飞星火认知、阿里通义千问以及 360 智脑等模型的官方接口转换成 OpenAI API 接入QChatGPT 原生支持,您需要先配置 One API之后在`config.py`中设置反向代理和`One API`的密钥后使用。
- [gpt4free](https://github.com/xtekky/gpt4free), 破解以免费使用多个平台的各种文字模型, 由[插件](https://github.com/RockChinQ/revLibs)接入, 无需鉴权, 稳定性较差。
- [Poe](https://poe.com), 破解免费使用Poe上多个平台的模型, 由[oliverkirk-sudo/ChatPoeBot](https://github.com/oliverkirk-sudo/ChatPoeBot)接入(由于 Poe 上可用的大部分模型现已通过[revLibs插件](https://github.com/RockChinQ/revLubs)或其他方式接入,此插件现已停止维护)。
### 故事续写
- NovelAI API, 由[插件](https://github.com/dominoar/QCPNovelAi)接入
### 图片绘制
- OpenAI DALL·E模型, 本项目原生支持, 使用方法查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/1-%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
- NovelAI API, 由[插件](https://github.com/dominoar/QCPNovelAi)接入
### 语音生成
- TTS+VITS, 由[插件](https://github.com/dominoar/QChatPlugins)接入
- Plachta/VITS-Umamusume-voice-synthesizer, 由[插件](https://github.com/oliverkirk-sudo/chat_voice)接入
安装[此插件](https://github.com/RockChinQ/Switcher),即可在使用中切换文字模型。
### 功能点
<details>
<summary>✅支持敏感词过滤,避免账号风险</summary>
- 难以监测机器人与用户对话时的内容,故引入此功能以减少机器人风险
- 加入了百度云内容审核,在`config.py`中修改`baidu_check`的值,并填写`baidu_api_key``baidu_secret_key`以开启此功能
- 编辑`sensitive.json`,并在`config.py`中修改`sensitive_word_filter`的值以开启此功能
</details>
<details>
<summary>✅群内多种响应规则不必at</summary>
- 默认回复`ai`作为前缀或`@`机器人的消息
- 详细见`config.py`中的`response_rules`字段
</details>
<details>
<summary>✅完善的多api-key管理超额自动切换</summary>
- 支持配置多个`api-key`,内部统计使用量并在超额时自动切换
- 请在`config.py`中修改`openai_config`的值以设置`api-key`
- 可以在`config.py`中修改`api_key_fee_threshold`来自定义切换阈值
- 运行期间向机器人说`!usage`以查看当前使用情况
</details>
<details>
<summary>✅支持预设文字</summary>
- 支持以自然语言预设文字,自定义机器人人格等信息
- 详见`config.py`中的`default_prompt`部分
- 支持设置多个预设情景,并通过!reset、!default等命令控制详细请查看[wiki命令](https://github.com/RockChinQ/QChatGPT/wiki/1-%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E6%9C%BA%E5%99%A8%E4%BA%BA%E6%8C%87%E4%BB%A4)
</details>
<details>
<summary>✅支持对话、绘图等模型,可玩性更高</summary>
- 现已支持OpenAI的对话`Completion API`和绘图`Image API`
- 向机器人发送命令`!draw <prompt>`即可使用绘图模型
</details>
<details>
<summary>✅支持命令控制热重载、热更新</summary>
- 允许在运行期间修改`config.py`或其他代码后,以管理员账号向机器人发送命令`!reload`进行热重载,无需重启
- 运行期间允许以管理员账号向机器人发送命令`!update`进行热更新,拉取远程最新代码并执行热重载
</details>
<details>
<summary>✅支持插件加载🧩</summary>
- 自行实现插件加载器及相关支持
- 支持GPT的Function Calling功能
- 详细查看[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/5-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
</details>
<details>
<summary>✅私聊、群聊黑名单机制</summary>
- 支持将人或群聊加入黑名单以忽略其消息
- 详见Wiki`加入黑名单`
</details>
<details>
<summary>✅长消息处理策略</summary>
- 支持将长消息转换成图片或消息记录组件,避免消息刷屏
- 请查看`config.py``blob_message_strategy`等字段
</details>
<details>
<summary>✅回复速度限制</summary>
- 支持限制单会话内每分钟可进行的对话次数
- 具有“等待”和“丢弃”两种策略
- “等待”策略:在获取到回复后,等待直到此次响应时间达到对话响应时间均值
- “丢弃”策略:此分钟内对话次数达到限制时,丢弃之后的对话
- 详细请查看config.py中的相关配置
</details>
<details>
<summary>✅支持使用网络代理</summary>
- 目前已支持正向代理访问接口
- 详细请查看config.py中的`openai_config`的说明
</details>
<details>
<summary>✅支持自定义提示内容</summary>
- 允许用户自定义报错、帮助等提示信息
- 请查看`tips.py`
</details>
### 🏞️截图
<img alt="私聊GPT-3.5" src="res/screenshots/person_gpt3.5.png" width="400"/>
<br/>
<img alt="群聊GPT-3.5" src="res/screenshots/group_gpt3.5.png" width="400"/>
<br/>
<img alt="New Bing" src="res/screenshots/person_newbing.png" width="400"/>
详情请查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/1-%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8)
</details>
<details>
<summary>
## 🚀部署和使用
</summary>
> **NOTE**
> - 部署过程中遇到任何问题,请先在[QChatGPT](https://github.com/RockChinQ/QChatGPT/issues)或[qcg-installer](https://github.com/RockChinQ/qcg-installer/issues)的issue里进行搜索
> - QChatGPT需要Python版本>=3.9
> - 官方群和社区群群号请见文档顶部
### - 注册OpenAI账号
<details>
<summary>点此查看步骤</summary>
> 若您要直接使用非OpenAI的模型如New Bing可跳过此步骤直接进行之后的部署完成后按照相关插件的文档进行配置即可
参考以下文章自行注册
> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
> [手把手教你如何注册ChatGPT超级详细](https://guxiaobei.com/51461)
注册成功后请前往[个人中心查看](https://beta.openai.com/account/api-keys)api_key
完成注册后,使用以下自动化或手动部署步骤
</details>
### - Docker或自动化部署
<details>
<summary>展开查看以下方式二选一Linux首选DockerWindows首选安装器</summary>
#### Docker方式
> docker方式较为复杂若您不**熟悉**docker的操作及相关知识强烈建议您使用其他方式部署我们**不会且难以**解决您主机上多个容器的连接问题。
请查看[此文档](res/docs/docker_deployment.md)
#### 安装器方式
使用[此安装器](https://github.com/RockChinQ/qcg-installer)(若无法访问请到[Gitee](https://gitee.com/RockChin/qcg-installer))进行部署
- 安装器目前仅支持部分平台,请到仓库文档查看,其他平台请手动部署
</details>
### - 手动部署
<details>
<summary>手动部署适用于所有平台</summary>
- 请使用Python 3.9.x以上版本
#### ① 配置QQ登录框架
目前支持mirai和go-cqhttp配置任意一个即可
<details>
<summary>mirai</summary>
1. 按照[此教程](https://yiri-mirai.wybxc.cc/tutorials/01/configuration)配置Mirai及mirai-api-http
2. 启动mirai-console后使用`login`命令登录QQ账号保持mirai-console运行状态
3. 在下一步配置主程序时请在config.py中将`msg_source_adapter`设为`yirimirai`
</details>
<details>
<summary>go-cqhttp</summary>
1. 按照[此文档](https://github.com/RockChinQ/QChatGPT/wiki/9-go-cqhttp%E9%85%8D%E7%BD%AE)配置go-cqhttp
2. 启动go-cqhttp确保登录成功保持运行
3. 在下一步配置主程序时请在config.py中将`msg_source_adapter`设为`nakuru`
</details>
#### ② 配置主程序
1. 克隆此项目
```bash
git clone https://github.com/RockChinQ/QChatGPT
cd QChatGPT
```
2. 安装依赖
```bash
pip3 install requests -r requirements.txt
```
3. 运行一次主程序,生成配置文件
```bash
python3 main.py
```
4. 编辑配置文件`config.py`
按照文件内注释填写配置信息
5. 运行主程序
```bash
python3 main.py
```
无报错信息即为运行成功
**常见问题**
- mirai登录提示`QQ版本过低`,见[此issue](https://github.com/RockChinQ/QChatGPT/issues/137)
- 如提示安装`uvicorn``hypercorn`请*不要*安装这两个不是必需的目前存在未知原因bug
- 如报错`TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary`, 请参考 [此处](https://github.com/RockChinQ/QChatGPT/issues/5)
</details>
**部署完成后必看: [命令说明](https://github.com/RockChinQ/QChatGPT/wiki/1-%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E6%9C%BA%E5%99%A8%E4%BA%BA%E6%8C%87%E4%BB%A4)**
所有功能查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/1-%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8)
</details>
<details>
<summary>
## 🧩插件生态
</summary>
⭐我们已经支持了[GPT的Function Calling能力](https://platform.openai.com/docs/guides/gpt/function-calling),请查看[Wiki内容函数](https://github.com/RockChinQ/QChatGPT/wiki/6-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8-%E5%86%85%E5%AE%B9%E5%87%BD%E6%95%B0)
> 使用方法见:[Wiki插件使用](https://github.com/RockChinQ/QChatGPT/wiki/5-%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
> 开发教程见:[Wiki插件开发](https://github.com/RockChinQ/QChatGPT/wiki/7-%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91)
[所有插件列表](https://github.com/stars/RockChinQ/lists/qchatgpt-%E6%8F%92%E4%BB%B6)欢迎提出issue以提交新的插件
### 部分插件
- [WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin) - 让机器人能联网!!
- [revLibs](https://github.com/RockChinQ/revLibs) - 将ChatGPT网页版、Claude、Bard、Hugging Chat等破解版接入此项目关于[官方接口和网页版有什么区别](https://github.com/RockChinQ/QChatGPT/wiki/8-%E5%AE%98%E6%96%B9%E6%8E%A5%E5%8F%A3%E3%80%81ChatGPT%E7%BD%91%E9%A1%B5%E7%89%88%E3%80%81ChatGPT-API%E5%8C%BA%E5%88%AB)
- [Switcher](https://github.com/RockChinQ/Switcher) - 支持通过命令切换使用的模型
- [hello_plugin](https://github.com/RockChinQ/hello_plugin) - `hello_plugin` 的储存库形式,插件开发模板
- [oliverkirk-sudo/chat_voice](https://github.com/oliverkirk-sudo/chat_voice) - 文字转语音输出支持HuggingFace上的[VITS模型](https://huggingface.co/spaces/Plachta/VITS-Umamusume-voice-synthesizer),azure语音合成,vits本地语音合成,sovits语音合成
- [RockChinQ/WaitYiYan](https://github.com/RockChinQ/WaitYiYan) - 实时获取百度`文心一言`等待列表人数
- [chordfish-k/QChartGPT_Emoticon_Plugin](https://github.com/chordfish-k/QChartGPT_Emoticon_Plugin) - 使机器人根据回复内容发送表情包
- [oliverkirk-sudo/ChatPoeBot](https://github.com/oliverkirk-sudo/ChatPoeBot) - 接入[Poe](https://poe.com/)上的机器人
- [lieyanqzu/WeatherPlugin](https://github.com/lieyanqzu/WeatherPlugin) - 天气查询插件
- [SysStatPlugin](https://github.com/RockChinQ/SysStatPlugin) - 查看系统状态
- [oliverkirk-sudo/qchat_system_status](https://github.com/oliverkirk-sudo/qchat_system_status) - 以图片的形式输出系统状态
- [oliverkirk-sudo/QChatAIPaint](https://github.com/oliverkirk-sudo/QChatAIPaint) - 基于[Holara](https://holara.ai/)的ai绘图插件
- [oliverkirk-sudo/QChatCodeRunner](https://github.com/oliverkirk-sudo/QChatCodeRunner) - 基于[CodeRunner-Plugin](https://github.com/oliverkirk-sudo/CodeRunner-Plugin)的代码运行与图表生成插件
- [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)
- [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>
<summary>
## 😘致谢和赞赏
</summary>
- [@the-lazy-me](https://github.com/the-lazy-me) 为本项目制作[视频教程](https://www.bilibili.com/video/BV1Y14y1Q7kQ)
- [@mikumifa](https://github.com/mikumifa) 本项目Docker部署仓库开发者
- [@dominoar](https://github.com/dominoar) 为本项目开发多种插件
- [@万神的星空](https://github.com/qq255204159) 整合包发行
- [@ljcduo](https://github.com/ljcduo) GPT-4 API内测账号提供
以及所有[贡献者](https://github.com/RockChinQ/QChatGPT/graphs/contributors)和其他为本项目提供支持的朋友们。
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/>
</details>

View File

@@ -180,7 +180,7 @@ Plugin [usage](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%
`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
- `cmdcn` - 主程序令中文形式
- `cmdcn` - 主程序令中文形式
- `hello_plugin` - 在收到消息`hello`时回复相应消息
- `urlikethisijustsix` - 收到冒犯性消息时回复相应消息
@@ -189,7 +189,7 @@ Plugin [usage](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%
欢迎提交新的插件
- [revLibs](https://github.com/RockChinQ/revLibs) - 将ChatGPT网页版接入此项目关于[官方接口和网页版有什么区别](https://github.com/RockChinQ/QChatGPT/wiki/%E5%AE%98%E6%96%B9%E6%8E%A5%E5%8F%A3%E4%B8%8EChatGPT%E7%BD%91%E9%A1%B5%E7%89%88)
- [Switcher](https://github.com/RockChinQ/Switcher) - 支持通过令切换使用的模型
- [Switcher](https://github.com/RockChinQ/Switcher) - 支持通过令切换使用的模型
- [hello_plugin](https://github.com/RockChinQ/hello_plugin) - `hello_plugin` 的储存库形式,插件开发模板
- [dominoar/QChatPlugins](https://github.com/dominoar/QchatPlugins) - dominoar编写的诸多新功能插件语音输出、Ranimg、屏蔽词规则等
- [dominoar/QCP-NovelAi](https://github.com/dominoar/QCP-NovelAi) - NovelAI 故事叙述与绘画

View File

@@ -78,13 +78,13 @@ openai_config = {
# passive仅当api-key超额时才会切换api-key
switch_strategy = "active"
# [必需] 管理员QQ号用于接收报错等通知及执行管理员级别
# [必需] 管理员QQ号用于接收报错等通知及执行管理员级别
# 支持多个管理员可以使用list形式设置例如
# admin_qq = [12345678, 87654321]
admin_qq = 0
# 情景预设(机器人人格)
# 每个会话的预设信息,影响所有会话,无视令重置
# 每个会话的预设信息,影响所有会话,无视令重置
# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令
# 例如:
# default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”"
@@ -98,14 +98,14 @@ admin_qq = 0
# "en-dict": "我想让你充当英英词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈。",
# }
#
# 在使用期间即可通过令:
# 在使用期间即可通过令:
# !reset [名称]
# 来使用指定的情景预设重置会话
# 例如:
# !reset linux-terminal
# 若不指定名称,则使用默认情景预设
#
# 也可以使用令:
# 也可以使用令:
# !default <名称>
# 将指定的情景预设设置为默认情景预设
# 例如:
@@ -165,7 +165,7 @@ response_rules = {
# 符合此规则的消息将不会被响应
# 支持消息前缀匹配及正则表达式匹配
# 此设置优先级高于response_rules
# 用以过滤mirai等其他层级的
# 用以过滤mirai等其他层级的
# @see https://github.com/RockChinQ/QChatGPT/issues/165
ignore_rules = {
"prefix": ["/"],
@@ -200,7 +200,7 @@ encourage_sponsor_at_start = True
# 每次向OpenAI接口发送对话记录上下文的字符数
# 最大不超过(4096 - max_tokens)个字符max_tokens为下方completion_api_params中的max_tokens
# 注意较大的prompt_submit_length会导致OpenAI账户额度消耗更快
prompt_submit_length = 2048
prompt_submit_length = 3072
# 是否在token超限报错时自动重置会话
# 可在tips.py中编辑提示语
@@ -230,13 +230,6 @@ auto_reset = True
# "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
@@ -254,6 +247,7 @@ auto_reset = True
# "qwen-plus-v1",
# "ERNIE-Bot",
# "ERNIE-Bot-turbo",
# "gemini-pro",
completion_api_params = {
"model": "gpt-3.5-turbo",
"temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1]
@@ -368,8 +362,8 @@ rate_limit_strategy = "drop"
upgrade_dependencies = False
# 是否上报统计信息
# 用于统计机器人的使用情况,不会收集任何用户信息
# 仅上报时间、字数使用量、绘图使用量,其他信息不会上报
# 用于统计机器人的使用情况,数据不公开,不会收集任何敏感信息
# 仅实例识别UUID、上报时间、字数使用量、绘图使用量、插件使用情况、用户信息,其他信息不会上报
report_usage = True
# 日志级别

43
main.py
View File

@@ -122,7 +122,7 @@ def complete_tips():
non_exist_keys = []
is_integrity = True
logging.info("检查tips模块完整性.")
logging.debug("检查tips模块完整性.")
tips_template = importlib.import_module('tips-custom-template')
tips = importlib.import_module('tips')
for key in dir(tips_template):
@@ -145,6 +145,10 @@ async def start_process(first_time_init=False):
global known_exception_caught
import pkg.utils.context
# 计算host和instance标识符
import pkg.audit.identifier
pkg.audit.identifier.init()
# 加载配置
cfg_inst: pymodule_cfg.PythonModuleConfigFile = pymodule_cfg.PythonModuleConfigFile(
'config.py',
@@ -158,6 +162,7 @@ async def start_process(first_time_init=False):
complete_tips()
cfg = pkg.utils.context.get_config_manager().data
# 更新openai库到最新版本
if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']:
print("正在更新依赖库,请等待...")
@@ -170,10 +175,6 @@ async def start_process(first_time_init=False):
except Exception as e:
print("更新openai库失败:{}, 请忽略或自行更新".format(e))
# 初始化文字转图片
from pkg.utils import text2img
text2img.initialize()
known_exception_caught = False
try:
try:
@@ -181,12 +182,16 @@ async def start_process(first_time_init=False):
sh = reset_logging()
pkg.utils.context.context['logger_handler'] = sh
# 初始化文字转图片
from pkg.utils import text2img
text2img.initialize()
# 检查是否设置了管理员
if cfg['admin_qq'] == 0:
# logging.warning("未设置管理员QQ,管理员权限令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
# logging.warning("未设置管理员QQ,管理员权限令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
while True:
try:
cfg['admin_qq'] = int(input("未设置管理员QQ,管理员权限令及运行告警将无法使用,请输入管理员QQ号: "))
cfg['admin_qq'] = int(input("未设置管理员QQ,管理员权限令及运行告警将无法使用,请输入管理员QQ号: "))
# 写入到文件
# 读取文件
@@ -204,6 +209,24 @@ async def start_process(first_time_init=False):
break
except ValueError:
print("请输入数字")
# 初始化中央服务器 API 交互实例
from pkg.utils.center import apigroup
from pkg.utils.center import v2 as center_v2
center_v2_api = center_v2.V2CenterAPI(
basic_info={
"host_id": pkg.audit.identifier.identifier['host_id'],
"instance_id": pkg.audit.identifier.identifier['instance_id'],
"semantic_version": pkg.utils.updater.get_current_tag(),
"platform": sys.platform,
},
runtime_info={
"admin_id": "{}".format(cfg['admin_qq']),
"msg_source": cfg['msg_source_adapter'],
}
)
pkg.utils.context.set_center_v2_api(center_v2_api)
import pkg.openai.manager
import pkg.database.manager
@@ -375,6 +398,12 @@ async def start_process(first_time_init=False):
if len(new_announcement) > 0:
for announcement in new_announcement:
logging.critical("[公告]<{}> {}".format(announcement['time'], announcement['content']))
# 发送统计数据
pkg.utils.context.get_center_v2_api().main.post_announcement_showed(
[announcement['id'] for announcement in new_announcement]
)
except Exception as e:
logging.warning("获取公告失败:{}".format(e))

View File

@@ -53,7 +53,7 @@
"baidu_secret_key": "",
"inappropriate_message_tips": "[百度云]请珍惜机器人,当前返回内容不合规",
"encourage_sponsor_at_start": true,
"prompt_submit_length": 2048,
"prompt_submit_length": 3072,
"auto_reset": true,
"completion_api_params": {
"model": "gpt-3.5-turbo",

View File

@@ -21,7 +21,7 @@ class DataGatherer:
以key值md5为key,{
"text": {
"text-davinci-003": 文字量:int,
"gpt-3.5-turbo": 文字量:int,
},
"image": {
"256x256": 图片数量:int,
@@ -37,27 +37,6 @@ class DataGatherer:
except:
pass
def report_to_server(self, subservice_name: str, count: int):
"""向中央服务器报告使用量
只会报告此次请求的使用量,不会报告总量。
不包含除版本号、使用类型、使用量以外的任何信息,仅供开发者分析使用情况。
"""
def thread_func():
try:
config = context.get_config_manager().data
if not config['report_usage']:
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']))
if res.status_code != 200 or res.text != "ok":
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
except:
return
threading.Thread(target=thread_func).start()
def get_usage(self, key_md5):
return self.usage[key_md5] if key_md5 in self.usage else {}
@@ -79,8 +58,6 @@ class DataGatherer:
self.usage[key_md5]["text"][model] += length
self.dump_to_db()
self.report_to_server("text", length)
def report_image_model_usage(self, size):
"""调用方报告图片模型请求图片使用量"""
@@ -98,8 +75,6 @@ class DataGatherer:
self.usage[key_md5]["image"][size] += 1
self.dump_to_db()
self.report_to_server("image", 1)
def get_text_length_of_key(self, key):
"""获取指定api-key (明文) 的文字总使用量(本地记录)"""
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()

83
pkg/audit/identifier.py Normal file
View File

@@ -0,0 +1,83 @@
import os
import uuid
import json
import time
identifier = {
'host_id': '',
'instance_id': '',
'host_create_ts': 0,
'instance_create_ts': 0,
}
HOST_ID_FILE = os.path.expanduser('~/.qchatgpt/host_id.json')
INSTANCE_ID_FILE = 'res/instance_id.json'
def init():
global identifier
if not os.path.exists(os.path.expanduser('~/.qchatgpt')):
os.mkdir(os.path.expanduser('~/.qchatgpt'))
if not os.path.exists(HOST_ID_FILE):
new_host_id = 'host_'+str(uuid.uuid4())
new_host_create_ts = int(time.time())
with open(HOST_ID_FILE, 'w') as f:
json.dump({
'host_id': new_host_id,
'host_create_ts': new_host_create_ts
}, f)
identifier['host_id'] = new_host_id
identifier['host_create_ts'] = new_host_create_ts
else:
loaded_host_id = ''
loaded_host_create_ts = 0
with open(HOST_ID_FILE, 'r') as f:
file_content = json.load(f)
loaded_host_id = file_content['host_id']
loaded_host_create_ts = file_content['host_create_ts']
identifier['host_id'] = loaded_host_id
identifier['host_create_ts'] = loaded_host_create_ts
# 检查实例 id
if os.path.exists(INSTANCE_ID_FILE):
instance_id = {}
with open(INSTANCE_ID_FILE, 'r') as f:
instance_id = json.load(f)
if instance_id['host_id'] != identifier['host_id']: # 如果实例 id 不是当前主机的,删除
os.remove(INSTANCE_ID_FILE)
if not os.path.exists(INSTANCE_ID_FILE):
new_instance_id = 'instance_'+str(uuid.uuid4())
new_instance_create_ts = int(time.time())
with open(INSTANCE_ID_FILE, 'w') as f:
json.dump({
'host_id': identifier['host_id'],
'instance_id': new_instance_id,
'instance_create_ts': new_instance_create_ts
}, f)
identifier['instance_id'] = new_instance_id
identifier['instance_create_ts'] = new_instance_create_ts
else:
loaded_instance_id = ''
loaded_instance_create_ts = 0
with open(INSTANCE_ID_FILE, 'r') as f:
file_content = json.load(f)
loaded_instance_id = file_content['instance_id']
loaded_instance_create_ts = file_content['instance_create_ts']
identifier['instance_id'] = loaded_instance_id
identifier['instance_create_ts'] = loaded_instance_create_ts
def print_out():
global identifier
print(identifier)

View File

@@ -91,7 +91,7 @@ class DatabaseManager:
`json` text not null
)
""")
print('Database initialized.')
# print('Database initialized.')
# session持久化
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,

View File

@@ -6,6 +6,8 @@ from openai.types.chat import chat_completion_message
from .model import RequestBase
from .. import funcmgr
from ...plugin import host
from ...utils import context
class ChatCompletionRequest(RequestBase):
@@ -189,6 +191,16 @@ class ChatCompletionRequest(RequestBase):
ret = "error: execute function failed: {}".format(str(e))
logging.error("函数执行失败: {}".format(str(e)))
# 上报数据
plugin_info = host.get_plugin_info_for_audit(func_name.split('-')[0])
audit_func_name = func_name.split('-')[1]
audit_func_desc = funcmgr.get_func_schema(func_name)['description']
context.get_center_v2_api().usage.post_function_record(
plugin=plugin_info,
function_name=audit_func_name,
function_description=audit_func_desc,
)
self.append_message(
role="function",
content=json.dumps(ret, ensure_ascii=False),

View File

@@ -8,7 +8,7 @@ from ..utils import context
# __current__ = "default"
# """当前默认使用的情景预设的名称
# 由管理员使用`!default <名称>`令切换
# 由管理员使用`!default <名称>`令切换
# """
# __prompts_from_files__ = {}

View File

@@ -32,16 +32,9 @@ class KeysManager:
return hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
def __init__(self, api_key):
if type(api_key) is dict:
self.api_key = api_key
elif type(api_key) is str:
self.api_key = {
"default": api_key
}
elif type(api_key) is list:
for i in range(len(api_key)):
self.api_key[str(i)] = api_key[i]
assert type(api_key) == dict
self.api_key = api_key
# 从usage中删除未加载的api-key的记录
# 不删了也许会运行时添加曾经有记录的api-key
@@ -75,7 +68,7 @@ class KeysManager:
if self.api_key[key_name] not in self.exceeded:
self.using_key = self.api_key[key_name]
logging.info("使用api-key:" + key_name)
logging.debug("使用api-key:" + key_name)
# 触发插件事件
args = {

View File

@@ -13,13 +13,6 @@ from ..openai.api import completion as api_completion
from ..openai.api import chat_completion as api_chat_completion
COMPLETION_MODELS = {
"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",
}
@@ -49,6 +42,7 @@ CHAT_COMPLETION_MODELS = {
"qwen-plus-v1",
"ERNIE-Bot",
"ERNIE-Bot-turbo",
"gemini-pro",
}
EDIT_MODELS = {
@@ -90,6 +84,7 @@ def count_chat_completion_tokens(messages: list, model: str) -> int:
"qwen-plus-v1",
"ERNIE-Bot",
"ERNIE-Bot-turbo",
"gemini-pro",
}:
tokens_per_message = 3
tokens_per_name = 1

View File

@@ -25,34 +25,6 @@ class SessionOfflineStatus:
EXPLICITLY_CLOSED = 'explicitly_closed'
# 重置session.prompt
def reset_session_prompt(session_name, prompt):
# 备份原始数据
bak_path = 'logs/{}-{}.bak'.format(
session_name,
time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
)
f = open(bak_path, 'w+')
f.write(prompt)
f.close()
# 生成新数据
config = context.get_config_manager().data
prompt = [
{
'role': 'system',
'content': config['default_prompt']['default'] if type(config['default_prompt']) == dict else config['default_prompt']
}
]
# 警告
logging.warning(
"""
用户[{}]的数据已被重置,有可能是因为数据版本过旧或存储错误
原始数据将备份在:
{}""".format(session_name, bak_path)
) # 为保证多行文本格式正确故无缩进
return prompt
# 从数据加载session
def load_sessions():
"""从数据库加载sessions"""
@@ -70,12 +42,10 @@ def load_sessions():
temp_session.name = session_name
temp_session.create_timestamp = session_data[session_name]['create_timestamp']
temp_session.last_interact_timestamp = session_data[session_name]['last_interact_timestamp']
try:
temp_session.prompt = json.loads(session_data[session_name]['prompt'])
temp_session.token_counts = json.loads(session_data[session_name]['token_counts'])
except Exception:
temp_session.prompt = reset_session_prompt(session_name, session_data[session_name]['prompt'])
temp_session.persistence()
temp_session.prompt = json.loads(session_data[session_name]['prompt'])
temp_session.token_counts = json.loads(session_data[session_name]['token_counts'])
temp_session.default_prompt = json.loads(session_data[session_name]['default_prompt']) if \
session_data[session_name]['default_prompt'] else []
@@ -261,6 +231,8 @@ class Session:
pending_res_text = ""
start_time = time.time()
# TODO 对不起,我知道这样非常非常屎山,但我之后会重构的
for resp in context.get_openai_manager().request_completion(prompts):
@@ -349,6 +321,26 @@ class Session:
self.just_switched_to_exist_session = False
self.set_ongoing()
# 上报使用量数据
session_type = session_name_spt[0]
session_id = session_name_spt[1]
ability_provider = "QChatGPT.Text"
usage = total_tokens
model_name = context.get_config_manager().data['completion_api_params']['model']
response_seconds = int(time.time() - start_time)
retry_times = -1 # 暂不记录
context.get_center_v2_api().usage.post_query_record(
session_type=session_type,
session_id=session_id,
query_ability_provider=ability_provider,
usage=usage,
model_name=model_name,
response_seconds=response_seconds,
retry_times=retry_times
)
return res_ans if res_ans[0] != '\n' else res_ans[1:], finish_reason, funcs
# 删除上一回合并返回上一回合的问题
@@ -471,12 +463,10 @@ class Session:
self.create_timestamp = last_one['create_timestamp']
self.last_interact_timestamp = last_one['last_interact_timestamp']
try:
self.prompt = json.loads(last_one['prompt'])
self.token_counts = json.loads(last_one['token_counts'])
except json.decoder.JSONDecodeError:
self.prompt = reset_session_prompt(self.name, last_one['prompt'])
self.persistence()
self.prompt = json.loads(last_one['prompt'])
self.token_counts = json.loads(last_one['token_counts'])
self.default_prompt = json.loads(last_one['default_prompt']) if last_one['default_prompt'] else []
self.just_switched_to_exist_session = True
@@ -492,12 +482,10 @@ class Session:
self.create_timestamp = next_one['create_timestamp']
self.last_interact_timestamp = next_one['last_interact_timestamp']
try:
self.prompt = json.loads(next_one['prompt'])
self.token_counts = json.loads(next_one['token_counts'])
except json.decoder.JSONDecodeError:
self.prompt = reset_session_prompt(self.name, next_one['prompt'])
self.persistence()
self.prompt = json.loads(next_one['prompt'])
self.token_counts = json.loads(next_one['token_counts'])
self.default_prompt = json.loads(next_one['default_prompt']) if next_one['default_prompt'] else []
self.just_switched_to_exist_session = True

View File

@@ -84,23 +84,34 @@ def iter_plugins_name():
__current_module_path__ = ""
def walk_plugin_path(module, prefix='', path_prefix=''):
def walk_plugin_path(module, prefix="", path_prefix=""):
global __current_module_path__
"""遍历插件路径"""
for item in pkgutil.iter_modules(module.__path__):
if item.ispkg:
logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name))
walk_plugin_path(__import__(module.__name__ + '.' + item.name, fromlist=['']),
prefix + item.name + '.', path_prefix + item.name + '/')
walk_plugin_path(
__import__(module.__name__ + "." + item.name, fromlist=[""]),
prefix + item.name + ".",
path_prefix + item.name + "/",
)
else:
try:
logging.debug("扫描插件模块: plugins/{}".format(path_prefix + item.name + '.py'))
__current_module_path__ = "plugins/"+path_prefix + item.name + '.py'
logging.debug(
"扫描插件模块: plugins/{}".format(path_prefix + item.name + ".py")
)
__current_module_path__ = "plugins/" + path_prefix + item.name + ".py"
importlib.import_module(module.__name__ + '.' + item.name)
logging.debug('加载模块: plugins/{} 成功'.format(path_prefix + item.name + '.py'))
importlib.import_module(module.__name__ + "." + item.name)
logging.debug(
"加载模块: plugins/{} 成功".format(path_prefix + item.name + ".py")
)
except:
logging.error('加载模块: plugins/{} 失败: {}'.format(path_prefix + item.name + '.py', sys.exc_info()))
logging.error(
"加载模块: plugins/{} 失败: {}".format(
path_prefix + item.name + ".py", sys.exc_info()
)
)
traceback.print_exc()
@@ -108,7 +119,7 @@ def load_plugins():
"""加载插件"""
logging.debug("加载插件")
PluginHost()
walk_plugin_path(__import__('plugins'))
walk_plugin_path(__import__("plugins"))
logging.debug(__plugins__)
@@ -132,7 +143,7 @@ def load_plugins():
def initialize_plugins():
"""初始化插件"""
logging.info("初始化插件")
logging.debug("初始化插件")
import pkg.plugin.models as models
successfully_initialized_plugins = []
@@ -141,14 +152,14 @@ def initialize_plugins():
# if not plugin['enabled']:
# continue
try:
models.__current_registering_plugin__ = plugin['name']
plugin['instance'] = plugin["class"](plugin_host=context.get_plugin_host())
models.__current_registering_plugin__ = plugin["name"]
plugin["instance"] = plugin["class"](plugin_host=context.get_plugin_host())
# logging.info("插件 {} 已初始化".format(plugin['name']))
successfully_initialized_plugins.append(plugin['name'])
successfully_initialized_plugins.append(plugin["name"])
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)))
@@ -172,9 +183,12 @@ def get_github_plugin_repo_label(repo_url: str) -> list[str]:
"""获取username, repo"""
# 提取 username/repo , 正则表达式
repo = re.findall(r'(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)', repo_url)
repo = re.findall(
r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)",
repo_url,
)
if len(repo) > 0: # github
if len(repo) > 0: # github
return repo[0].split("/")
else:
return None
@@ -183,53 +197,52 @@ def get_github_plugin_repo_label(repo_url: str) -> list[str]:
def download_plugin_source_code(repo_url: str, target_path: str) -> str:
"""下载插件源码"""
# 检查源类型
# 提取 username/repo , 正则表达式
repo = get_github_plugin_repo_label(repo_url)
repo = get_github_plugin_repo_label(repo_url)
target_path += repo[1]
if repo is not None: # github
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
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("temp/" + target_path):
shutil.rmtree("temp/" + target_path)
if os.path.exists(target_path):
shutil.rmtree(target_path)
os.makedirs("temp/"+target_path)
os.makedirs("temp/" + target_path)
with open("temp/"+target_path+"/source.zip", "wb") as f:
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")
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]
unzip_dir = glob.glob("temp/" + target_path + "/*")[0]
# 复制到 plugins/repo
shutil.copytree(unzip_dir, target_path+"/")
shutil.copytree(unzip_dir, target_path + "/")
# 删除解压后的文件夹
shutil.rmtree(unzip_dir)
@@ -237,18 +250,20 @@ def download_plugin_source_code(repo_url: str, target_path: str) -> str:
logging.info("解压完成")
else:
raise Exception("暂不支持的源类型,请使用 GitHub 仓库发行插件。")
return repo[1]
def check_requirements(path: str):
# 检查此目录是否包含requirements.txt
if os.path.exists(path+"/requirements.txt"):
if os.path.exists(path + "/requirements.txt"):
logging.info("检测到requirements.txt正在安装依赖")
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.install_requirements(path+"/requirements.txt")
pkg.utils.pkgmgr.install_requirements(path + "/requirements.txt")
import pkg.utils.log as log
log.reset_logging()
@@ -257,25 +272,43 @@ def install_plugin(repo_url: str):
repo_label = download_plugin_source_code(repo_url, "plugins/")
check_requirements("plugins/"+repo_label)
check_requirements("plugins/" + repo_label)
metadata.set_plugin_metadata(repo_label, repo_url, int(time.time()), "HEAD")
# 上报安装记录
context.get_center_v2_api().plugin.post_install_record(
plugin={
"name": "unknown",
"remote": repo_url,
"author": "unknown",
"version": "HEAD",
}
)
def uninstall_plugin(plugin_name: str) -> str:
"""卸载插件"""
if plugin_name not in __plugins__:
raise Exception("插件不存在")
plugin_info = get_plugin_info_for_audit(plugin_name)
# 获取文件夹路径
plugin_path = __plugins__[plugin_name]['path'].replace("\\", "/")
plugin_path = __plugins__[plugin_name]["path"].replace("\\", "/")
# 剪切路径为plugins/插件名
plugin_path = plugin_path.split("plugins/")[1].split("/")[0]
# 删除文件夹
shutil.rmtree("plugins/"+plugin_path)
return "plugins/"+plugin_path
shutil.rmtree("plugins/" + plugin_path)
# 上报卸载记录
context.get_center_v2_api().plugin.post_remove_record(
plugin=plugin_info
)
return "plugins/" + plugin_path
def update_plugin(plugin_name: str):
@@ -287,12 +320,26 @@ def update_plugin(plugin_name: str):
if meta == {}:
raise Exception("没有此插件元数据信息,无法更新")
remote_url = meta['source']
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":
raise Exception("插件没有远程地址记录,无法更新")
old_plugin_info = get_plugin_info_for_audit(plugin_name)
context.get_center_v2_api().plugin.post_update_record(
plugin=old_plugin_info,
old_version=old_plugin_info['version'],
new_version='HEAD',
)
remote_url = meta["source"]
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"
):
raise Exception("插件没有远程地址记录,无法更新")
# 重新安装插件
logging.info("正在重新安装插件以进行更新...")
@@ -301,7 +348,7 @@ def update_plugin(plugin_name: str):
def get_plugin_name_by_path_name(plugin_path_name: str) -> str:
for k, v in __plugins__.items():
if v['path'] == "plugins/"+plugin_path_name+"/main.py":
if v["path"] == "plugins/" + plugin_path_name + "/main.py":
return k
return None
@@ -309,8 +356,8 @@ def get_plugin_name_by_path_name(plugin_path_name: str) -> str:
def get_plugin_path_name_by_plugin_name(plugin_name: str) -> str:
if plugin_name not in __plugins__:
return None
plugin_main_module_path = __plugins__[plugin_name]['path']
plugin_main_module_path = __plugins__[plugin_name]["path"]
plugin_main_module_path = plugin_main_module_path.replace("\\", "/")
@@ -319,8 +366,29 @@ def get_plugin_path_name_by_plugin_name(plugin_name: str) -> str:
return spt[1]
def get_plugin_info_for_audit(plugin_name: str) -> dict:
"""获取插件信息"""
if plugin_name not in __plugins__:
return {}
plugin = __plugins__[plugin_name]
name = plugin["name"]
meta = metadata.get_plugin_metadata(get_plugin_path_name_by_plugin_name(name))
remote = meta["source"] if meta != {} else ""
author = plugin["author"]
version = plugin["version"]
return {
"name": name,
"remote": remote,
"author": author,
"version": version,
}
class EventContext:
"""事件上下文"""
eid = 0
"""事件编号"""
@@ -395,6 +463,7 @@ class EventContext:
def emit(event_name: str, **kwargs) -> EventContext:
"""触发事件"""
import pkg.utils.context as context
if context.get_plugin_host() is None:
return None
return context.get_plugin_host().emit(event_name, **kwargs)
@@ -443,9 +512,10 @@ class PluginHost:
event_context = EventContext(event_name)
logging.debug("触发事件: {} ({})".format(event_name, event_context.eid))
emitted_plugins = []
for plugin in iter_plugins():
if not plugin['enabled']:
if not plugin["enabled"]:
continue
# if plugin['instance'] is None:
@@ -457,9 +527,11 @@ class PluginHost:
# logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
# continue
if 'hooks' not in plugin or event_name not in plugin['hooks']:
if "hooks" not in plugin or event_name not in plugin["hooks"]:
continue
emitted_plugins.append(plugin['name'])
hooks = []
if event_name in plugin["hooks"]:
hooks = plugin["hooks"][event_name]
@@ -467,27 +539,40 @@ class PluginHost:
try:
already_prevented_default = event_context.is_prevented_default()
kwargs['host'] = context.get_plugin_host()
kwargs['event'] = event_context
kwargs["host"] = context.get_plugin_host()
kwargs["event"] = event_context
hook(plugin['instance'], **kwargs)
hook(plugin["instance"], **kwargs)
if event_context.is_prevented_default() and not already_prevented_default:
logging.debug("插件 {} 已要求阻止事件 {} 的默认行为".format(plugin['name'], event_name))
if (
event_context.is_prevented_default()
and not already_prevented_default
):
logging.debug(
"插件 {} 已要求阻止事件 {} 的默认行为".format(plugin["name"], event_name)
)
except Exception as e:
logging.error("插件{}响应事件{}时发生错误".format(plugin['name'], event_name))
logging.error("插件{}响应事件{}时发生错误".format(plugin["name"], event_name))
logging.error(traceback.format_exc())
# print("done:{}".format(plugin['name']))
if event_context.is_prevented_postorder():
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin['name']))
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin["name"]))
break
logging.debug("事件 {} ({}) 处理完毕,返回值: {}".format(event_name, event_context.eid,
event_context.__return_value__))
logging.debug(
"事件 {} ({}) 处理完毕,返回值: {}".format(
event_name, event_context.eid, event_context.__return_value__
)
)
if len(emitted_plugins) > 0:
plugins_info = [get_plugin_info_for_audit(p) for p in emitted_plugins]
context.get_center_v2_api().usage.post_event_record(
plugins=plugins_info,
event_name=event_name,
)
return event_context
if __name__ == "__main__":
pass

View File

@@ -35,18 +35,18 @@ PersonNormalMessageReceived = "person_normal_message_received"
"""
PersonCommandSent = "person_command_sent"
"""判断为应该处理的私聊令时触发
"""判断为应该处理的私聊令时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
command: str
command: str
params: list[str] 参数列表
text_message: str 完整令文本
text_message: str 完整令文本
is_admin: bool 是否为管理员
returns (optional):
alter: str 修改后的完整令文本
alter: str 修改后的完整令文本
reply: list 回复消息组件列表
"""
@@ -64,18 +64,18 @@ GroupNormalMessageReceived = "group_normal_message_received"
"""
GroupCommandSent = "group_command_sent"
"""判断为应该处理的群聊令时触发
"""判断为应该处理的群聊令时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
command: str
command: str
params: list[str] 参数列表
text_message: str 完整令文本
text_message: str 完整令文本
is_admin: bool 是否为管理员
returns (optional):
alter: str 修改后的完整令文本
alter: str 修改后的完整令文本
reply: list 回复消息组件列表
"""

View File

@@ -4,11 +4,10 @@ import pkgutil
import traceback
import json
__command_list__ = {}
import tips as tips_custom
__command_list__ = {}
"""命令树
结构:
@@ -72,7 +71,7 @@ __tree_index__: dict[str, list] = {}
结构:
{
'pkg.qqbot.cmds.cmd1.CommandCmd1': 'cmd1', # 顶级
'pkg.qqbot.cmds.cmd1.CommandCmd1': 'cmd1', # 顶级
'pkg.qqbot.cmds.cmd1.CommandCmd1_1': 'cmd1.cmd1-1', # 类名: 节点路径
'pkg.qqbot.cmds.cmd2.CommandCmd2': 'cmd2',
'pkg.qqbot.cmds.cmd2.CommandCmd2_1': 'cmd2.cmd2-1',
@@ -84,79 +83,79 @@ __tree_index__: dict[str, list] = {}
class Context:
"""命令执行上下文"""
command: str
"""顶级令文本"""
"""顶级令文本"""
crt_command: str
"""当前子令文本"""
"""当前子令文本"""
params: list
"""完整参数列表"""
crt_params: list
"""当前子令参数列表"""
"""当前子令参数列表"""
session_name: str
"""会话名"""
text_message: str
"""令完整文本"""
"""令完整文本"""
launcher_type: str
"""令发起者类型"""
"""令发起者类型"""
launcher_id: int
"""令发起者ID"""
"""令发起者ID"""
sender_id: int
"""令发送者ID"""
"""令发送者ID"""
is_admin: bool
"""[过时]令发送者是否为管理员"""
"""[过时]令发送者是否为管理员"""
privilege: int
"""令发送者权限等级"""
"""令发送者权限等级"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
class AbstractCommandNode:
"""令抽象类"""
"""令抽象类"""
parent: type
"""令类"""
"""令类"""
name: str
"""令名"""
"""令名"""
description: str
"""令描述"""
"""令描述"""
usage: str
"""令用法"""
"""令用法"""
aliases: list[str]
"""令别名"""
"""令别名"""
privilege: int
"""令权限等级, 权限大于等于此值的用户才能执行"""
"""令权限等级, 权限大于等于此值的用户才能执行"""
@classmethod
def process(cls, ctx: Context) -> tuple[bool, list]:
"""令处理函数
"""令处理函数
:param ctx: 令执行上下文
:param ctx: 令执行上下文
:return: (是否执行, 回复列表(若执行))
若未执行,将自动以下一个参数查找并执行子
若未执行,将自动以下一个参数查找并执行子
"""
raise NotImplementedError
@classmethod
def help(cls) -> str:
"""获取令帮助信息"""
return '令: {}\n描述: {}\n用法: \n{}\n别名: {}\n权限: {}'.format(
"""获取令帮助信息"""
return '令: {}\n描述: {}\n用法: \n{}\n别名: {}\n权限: {}'.format(
cls.name,
cls.description,
cls.usage,
@@ -173,11 +172,11 @@ class AbstractCommandNode:
aliases: list[str] = None,
privilege: int = 0
):
"""注册
"""注册
:param cls: 令类
:param name: 令名
:param parent: 父令类
:param cls: 令类
:param name: 令名
:param parent: 父令类
"""
global __command_list__, __tree_index__
@@ -192,7 +191,7 @@ class AbstractCommandNode:
logging.debug("cls: {}, name: {}, parent: {}".format(cls, name, parent))
if parent is None:
# 顶级令注册
# 顶级令注册
__command_list__[name] = {
'description': cls.description,
'usage': cls.usage,
@@ -209,9 +208,9 @@ class AbstractCommandNode:
path = __tree_index__[parent.__module__ + '.' + parent.__name__]
parent_node = __command_list__[path]
# 链接父子
# 链接父子
__command_list__[path]['sub'].append(name)
# 注册子
# 注册子
__command_list__[path + '.' + name] = {
'description': cls.description,
'usage': cls.usage,
@@ -230,18 +229,18 @@ class AbstractCommandNode:
class CommandPrivilegeError(Exception):
"""令权限不足或不存在异常"""
"""令权限不足或不存在异常"""
pass
# 传入Context对象广搜命令树返回执行结果
# 若命令被处理返回reply列表
# 若命令未被处理,继续执行下一级
# 若命令未被处理,继续执行下一级
# 若命令不存在,报异常
def execute(context: Context) -> list:
"""执行
"""执行
:param ctx: 令执行上下文
:param ctx: 令执行上下文
:return: 回复列表
"""
@@ -250,7 +249,7 @@ def execute(context: Context) -> list:
# 拷贝ctx
ctx: Context = copy.deepcopy(context)
# 从树取出顶级
# 从树取出顶级
node = __command_list__
path = ctx.command
@@ -258,7 +257,7 @@ def execute(context: Context) -> list:
while True:
try:
node = __command_list__[path]
logging.debug('执行令: {}'.format(path))
logging.debug('执行令: {}'.format(path))
# 检查权限
if ctx.privilege < node['privilege']:
@@ -279,7 +278,7 @@ def execute(context: Context) -> list:
def register_all():
"""启动时调用此函数注册所有
"""启动时调用此函数注册所有
递归处理pkg.qqbot.cmds包下及其子包下所有模块的所有继承于AbstractCommand的类
"""
@@ -305,7 +304,7 @@ def register_all():
else:
m = __import__(module.__name__ + '.' + item.name, fromlist=[''])
# for name, cls in inspect.getmembers(m, inspect.isclass):
# # 检查是否为令类
# # 检查是否为令类
# if cls.__module__ == m.__name__ and issubclass(cls, AbstractCommandNode) and cls != AbstractCommandNode:
# cls.register(cls, cls.name, cls.parent)
@@ -314,7 +313,7 @@ def register_all():
def apply_privileges():
"""读取cmdpriv.json并应用令权限"""
"""读取cmdpriv.json并应用令权限"""
# 读取内容
json_str = ""
with open('cmdpriv.json', 'r', encoding="utf-8") as f:

View File

@@ -38,9 +38,6 @@ class PluginCommand(aamgr.AbstractCommandNode):
reply = [reply_str]
return True, reply
elif ctx.params[0].startswith("http"):
reply = ["[bot]err: 此命令已弃用,请使用 !plugin get <插件仓库地址> 进行安装"]
return True, reply
else:
return False, []
@@ -68,7 +65,7 @@ class PluginGetCommand(aamgr.AbstractCommandNode):
def closure():
try:
plugin_host.install_plugin(ctx.crt_params[0])
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 令重载插件")
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 令重载插件")
except Exception as e:
logging.error("插件安装失败:{}".format(e))
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
@@ -149,7 +146,7 @@ class PluginDelCommand(aamgr.AbstractCommandNode):
unin_path = plugin_host.uninstall_plugin(plugin_name)
reply = ["[bot]已删除插件: {} ({}), 请发送 !reload 重载插件".format(plugin_name, unin_path)]
else:
reply = ["[bot]err:未找到插件: {}, 请使用!plugin令查看插件列表".format(plugin_name)]
reply = ["[bot]err:未找到插件: {}, 请使用!plugin令查看插件列表".format(plugin_name)]
return True, reply
@@ -195,7 +192,7 @@ class PluginOnOffCommand(aamgr.AbstractCommandNode):
plugin_switch.dump_switch()
reply = ["[bot]已{}插件: {}".format("启用" if new_status else "禁用", plugin_name)]
else:
reply = ["[bot]err:未找到插件: {}, 请使用!plugin令查看插件列表".format(plugin_name)]
reply = ["[bot]err:未找到插件: {}, 请使用!plugin令查看插件列表".format(plugin_name)]
return True, reply

View File

@@ -39,8 +39,6 @@ class DefaultCommand(aamgr.AbstractCommandNode):
reply_str += "\n当前默认情景预设:{}\n".format(dprompt.mode_inst().get_using_name())
reply_str += "请使用 !default set <情景预设名称> 来设置默认情景预设"
reply = [reply_str]
elif params[0] != "set":
reply = ["[bot]err: 已弃用,请使用!default set <情景预设名称> 来设置默认情景预设"]
else:
return False, []
@@ -69,7 +67,5 @@ class DefaultSetCommand(aamgr.AbstractCommandNode):
reply = ["[bot]已设置默认情景预设为:{}".format(full_name)]
except Exception as e:
reply = ["[bot]err: {}".format(e)]
else:
reply = ["[bot]err: 仅管理员可设置默认情景预设"]
return True, reply

View File

@@ -31,7 +31,7 @@ class ListCommand(aamgr.AbstractCommandNode):
results = pkg.openai.session.get_session(session_name).list_history(page=page)
if len(results) == 0:
reply = ["[bot]第{}页没有历史会话".format(page)]
reply_str = "[bot]第{}页没有历史会话".format(page)
else:
reply_str = "[bot]历史会话 第{}页:\n".format(page)
current = -1
@@ -39,12 +39,9 @@ class ListCommand(aamgr.AbstractCommandNode):
# 时间(使用create_timestamp转换) 序号 部分内容
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
msg = ""
try:
msg = json.loads(results[i]['prompt'])
except json.decoder.JSONDecodeError:
msg = pkg.openai.session.reset_session_prompt(session_name, results[i]['prompt'])
# 持久化
pkg.openai.session.get_session(session_name).persistence()
msg = json.loads(results[i]['prompt'])
if len(msg) >= 2:
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),

View File

@@ -15,7 +15,7 @@ class ResendCommand(aamgr.AbstractCommandNode):
from ....openai import session as openai_session
from ....utils import context
from ....qqbot import message
import config
session_name = ctx.session_name
reply = []
@@ -24,6 +24,8 @@ class ResendCommand(aamgr.AbstractCommandNode):
mgr = context.get_qqbot_manager()
config = context.get_config_manager().data
reply = message.process_normal_message(to_send, mgr, config,
ctx.launcher_type, ctx.launcher_id,
ctx.sender_id)

View File

@@ -6,7 +6,12 @@ from .. import aamgr
def config_operation(cmd, params):
reply = []
import pkg.utils.context
config = pkg.utils.context.get_config()
# config = pkg.utils.context.get_config()
cfg_mgr = pkg.utils.context.get_config_manager()
false = False
true = True
reply_str = ""
if len(params) == 0:
reply = ["[bot]err:请输入!cmd cfg查看使用方法"]
@@ -14,25 +19,25 @@ def config_operation(cmd, params):
cfg_name = params[0]
if cfg_name == 'all':
reply_str = "[bot]所有配置项:\n\n"
for cfg in dir(config):
for cfg in cfg_mgr.data.keys():
if not cfg.startswith('__') and not cfg == 'logging':
# 根据配置项类型进行格式化如果是字典则转换为json并格式化
if isinstance(getattr(config, cfg), str):
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
elif isinstance(getattr(config, cfg), dict):
if isinstance(cfg_mgr.data[cfg], str):
reply_str += "{}: \"{}\"\n".format(cfg, cfg_mgr.data[cfg])
elif isinstance(cfg_mgr.data[cfg], dict):
# 不进行unicode转义并格式化
reply_str += "{}: {}\n".format(cfg,
json.dumps(getattr(config, cfg),
json.dumps(cfg_mgr.data[cfg],
ensure_ascii=False, indent=4))
else:
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
reply_str += "{}: {}\n".format(cfg, cfg_mgr.data[cfg])
reply = [reply_str]
else:
cfg_entry_path = cfg_name.split('.')
try:
if len(params) == 1:
cfg_entry = getattr(config, cfg_entry_path[0])
if len(params) == 1: # 未指定配置值,返回配置项值
cfg_entry = cfg_mgr.data[cfg_entry_path[0]]
if len(cfg_entry_path) > 1:
for i in range(1, len(cfg_entry_path)):
cfg_entry = cfg_entry[cfg_entry_path[i]]
@@ -48,23 +53,10 @@ def config_operation(cmd, params):
reply = [reply_str]
else:
cfg_value = " ".join(params[1:])
# 类型转换如果是json则转换为字典
# if cfg_value == 'true':
# cfg_value = True
# elif cfg_value == 'false':
# cfg_value = False
# elif cfg_value.isdigit():
# cfg_value = int(cfg_value)
# elif cfg_value.startswith('{') and cfg_value.endswith('}'):
# cfg_value = json.loads(cfg_value)
# else:
# try:
# cfg_value = float(cfg_value)
# except ValueError:
# pass
cfg_value = eval(cfg_value)
cfg_entry = getattr(config, cfg_entry_path[0])
cfg_entry = cfg_mgr.data[cfg_entry_path[0]]
if len(cfg_entry_path) > 1:
for i in range(1, len(cfg_entry_path) - 1):
cfg_entry = cfg_entry[cfg_entry_path[i]]
@@ -74,14 +66,14 @@ def config_operation(cmd, params):
else:
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
else:
setattr(config, cfg_entry_path[0], cfg_value)
cfg_mgr.data[cfg_entry_path[0]] = cfg_value
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
except AttributeError:
except KeyError:
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
except NameError:
reply = ["[bot]err:值{}不合法(字符串需要使用双引号包裹)".format(cfg_value)]
except ValueError:
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
# else:
# reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
return reply

View File

@@ -4,8 +4,8 @@ from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="cmd",
description="显示令列表",
usage="!cmd\n!cmd <令名称>",
description="显示令列表",
usage="!cmd\n!cmd <令名称>",
aliases=[],
privilege=1
)
@@ -17,15 +17,15 @@ class CmdCommand(aamgr.AbstractCommandNode):
reply = []
if len(ctx.params) == 0:
reply_str = "[bot]当前所有令:\n\n"
reply_str = "[bot]当前所有令:\n\n"
# 遍历顶级
# 遍历顶级
for key in command_list:
command = command_list[key]
if command['parent'] is None:
reply_str += "!{} - {}\n".format(key, command['description'])
reply_str += "\n请使用 !cmd <令名称> 来查看令的详细信息"
reply_str += "\n请使用 !cmd <令名称> 来查看令的详细信息"
reply = [reply_str]
else:
@@ -33,7 +33,7 @@ class CmdCommand(aamgr.AbstractCommandNode):
if command_name in command_list:
reply = [command_list[command_name]['cls'].help()]
else:
reply = ["[bot]{} 不存在".format(command_name)]
reply = ["[bot]{} 不存在".format(command_name)]
return True, reply

View File

@@ -13,7 +13,7 @@ class HelpCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import tips
reply = ["[bot] "+tips.help_message + "\n请输入 !cmd 查看令列表"]
reply = ["[bot] "+tips.help_message + "\n请输入 !cmd 查看令列表"]
# 警告config.help_message过时
import config

View File

@@ -13,7 +13,6 @@ class UsageCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import config
import pkg.utils.credit as credit
import pkg.utils.context
reply = []

View File

@@ -1,4 +1,4 @@
# 令处理模块
# 令处理模块
import logging
from ..qqbot.cmds import aamgr as cmdmgr
@@ -9,7 +9,7 @@ def process_command(session_name: str, text_message: str, mgr, config: dict,
reply = []
try:
logging.info(
"[{}]发起令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
"[{}]发起令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
"..." if len(text_message) > 20 else "")))
cmd = text_message[1:].strip().split(' ')[0]
@@ -42,7 +42,7 @@ def process_command(session_name: str, text_message: str, mgr, config: dict,
return reply
except Exception as e:
mgr.notify_admin("{}令执行失败:{}".format(session_name, e))
mgr.notify_admin("{}令执行失败:{}".format(session_name, e))
logging.exception(e)
reply = ["[bot]err:{}".format(e)]

View File

@@ -125,6 +125,10 @@ class QQBotManager:
else:
self.adapter = context.get_qqbot_manager().adapter
self.bot_account_id = context.get_qqbot_manager().bot_account_id
# 保存 account_id 到审计模块
from ..utils.center import apigroup
apigroup.APIGroup._runtime_info['account_id'] = "{}".format(self.bot_account_id)
context.set_qqbot_manager(self)

View File

@@ -83,7 +83,9 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
processing.append(session_name)
try:
if text_message.startswith('!') or text_message.startswith(""): # 指令
msg_type = ''
if text_message.startswith('!') or text_message.startswith(""): # 命令
msg_type = 'command'
# 触发插件事件
args = {
'launcher_type': launcher_type,
@@ -110,6 +112,7 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
mgr, config, launcher_type, launcher_id, sender_id, is_admin(sender_id))
else: # 消息
msg_type = 'message'
# 限速丢弃检查
# print(ratelimit.__crt_minute_usage__[session_name])
if config['rate_limit_strategy'] == "drop":
@@ -154,7 +157,9 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
"回复[{}]文字消息:{}".format(session_name,
reply[0][:min(100, len(reply[0]))] + (
"..." if len(reply[0]) > 100 else "")))
reply = [mgr.reply_filter.process(reply[0])]
if msg_type == 'message':
reply = [mgr.reply_filter.process(reply[0])]
reply = blob.check_text(reply[0])
else:
logging.info("回复[{}]消息".format(session_name))

View File

@@ -17,14 +17,10 @@ def get_limitation(session_name: str) -> int:
"""获取会话的限制次数"""
config = context.get_config_manager().data
if type(config['rate_limitation']) == dict:
# 如果被指定了
if session_name in config['rate_limitation']:
return config['rate_limitation'][session_name]
else:
return config['rate_limitation']["default"]
elif type(config['rate_limitation']) == int:
return config['rate_limitation']
if session_name in config['rate_limitation']:
return config['rate_limitation'][session_name]
else:
return config['rate_limitation']["default"]
def add_usage(session_name: str):

View File

@@ -182,7 +182,8 @@ class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
headers={
'Authorization': "Bearer " + config['nakuru_config']['token'] if 'token' in config['nakuru_config']else ""
},
timeout=5
timeout=5,
proxies=None
)
if resp.status_code == 403:
logging.error("go-cqhttp拒绝访问请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配")

View File

View File

@@ -0,0 +1,88 @@
import abc
import uuid
import json
import logging
import threading
import requests
class APIGroup(metaclass=abc.ABCMeta):
"""API 组抽象类"""
_basic_info: dict = None
_runtime_info: dict = None
prefix = None
def __init__(self, prefix: str):
self.prefix = prefix
def do(
self,
method: str,
path: str,
data: dict = None,
params: dict = None,
headers: dict = {},
**kwargs
):
"""执行一个请求"""
def thr_wrapper(
self,
method: str,
path: str,
data: dict = None,
params: dict = None,
headers: dict = {},
**kwargs
):
try:
url = self.prefix + path
data = json.dumps(data)
headers['Content-Type'] = 'application/json'
ret = requests.request(
method,
url,
data=data,
params=params,
headers=headers,
**kwargs
)
logging.debug("data: %s", data)
logging.debug("ret: %s", ret.json())
except Exception as e:
logging.debug("上报数据失败: %s", e)
thr = threading.Thread(target=thr_wrapper, args=(
self,
method,
path,
data,
params,
headers,
), kwargs=kwargs)
thr.start()
def gen_rid(
self
):
"""生成一个请求 ID"""
return str(uuid.uuid4())
def basic_info(
self
):
"""获取基本信息"""
basic_info = APIGroup._basic_info.copy()
basic_info['rid'] = self.gen_rid()
return basic_info
def runtime_info(
self
):
"""获取运行时信息"""
return APIGroup._runtime_info

View File

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from .. import apigroup
from ... import context
class V2MainDataAPI(apigroup.APIGroup):
"""主程序相关 数据API"""
def __init__(self, prefix: str):
super().__init__(prefix+"/main")
def do(self, *args, **kwargs):
config = context.get_config_manager().data
if not config['report_usage']:
return None
return super().do(*args, **kwargs)
def post_update_record(
self,
spent_seconds: int,
infer_reason: str,
old_version: str,
new_version: str,
):
"""提交更新记录"""
return self.do(
"POST",
"/update",
data={
"basic": self.basic_info(),
"update_info": {
"spent_seconds": spent_seconds,
"infer_reason": infer_reason,
"old_version": old_version,
"new_version": new_version,
}
}
)
def post_announcement_showed(
self,
ids: list[int],
):
"""提交公告已阅"""
return self.do(
"POST",
"/announcement",
data={
"basic": self.basic_info(),
"announcement_info": {
"ids": ids,
}
}
)

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
from .. import apigroup
from ... import context
class V2PluginDataAPI(apigroup.APIGroup):
"""插件数据相关 API"""
def __init__(self, prefix: str):
super().__init__(prefix+"/plugin")
def do(self, *args, **kwargs):
config = context.get_config_manager().data
if not config['report_usage']:
return None
return super().do(*args, **kwargs)
def post_install_record(
self,
plugin: dict
):
"""提交插件安装记录"""
return self.do(
"POST",
"/install",
data={
"basic": self.basic_info(),
"plugin": plugin,
}
)
def post_remove_record(
self,
plugin: dict
):
"""提交插件卸载记录"""
return self.do(
"POST",
"/remove",
data={
"basic": self.basic_info(),
"plugin": plugin,
}
)
def post_update_record(
self,
plugin: dict,
old_version: str,
new_version: str,
):
"""提交插件更新记录"""
return self.do(
"POST",
"/update",
data={
"basic": self.basic_info(),
"plugin": plugin,
"update_info": {
"old_version": old_version,
"new_version": new_version,
}
}
)

View File

@@ -0,0 +1,88 @@
from __future__ import annotations
from .. import apigroup
from ... import context
class V2UsageDataAPI(apigroup.APIGroup):
"""使用量数据相关 API"""
def __init__(self, prefix: str):
super().__init__(prefix+"/usage")
def do(self, *args, **kwargs):
config = context.get_config_manager().data
if not config['report_usage']:
return None
return super().do(*args, **kwargs)
def post_query_record(
self,
session_type: str,
session_id: str,
query_ability_provider: str,
usage: int,
model_name: str,
response_seconds: int,
retry_times: int,
):
"""提交请求记录"""
return self.do(
"POST",
"/query",
data={
"basic": self.basic_info(),
"runtime": self.runtime_info(),
"session_info": {
"type": session_type,
"id": session_id,
},
"query_info": {
"ability_provider": query_ability_provider,
"usage": usage,
"model_name": model_name,
"response_seconds": response_seconds,
"retry_times": retry_times,
}
}
)
def post_event_record(
self,
plugins: list[dict],
event_name: str,
):
"""提交事件触发记录"""
return self.do(
"POST",
"/event",
data={
"basic": self.basic_info(),
"runtime": self.runtime_info(),
"plugins": plugins,
"event_info": {
"name": event_name,
}
}
)
def post_function_record(
self,
plugin: dict,
function_name: str,
function_description: str,
):
"""提交内容函数使用记录"""
return self.do(
"POST",
"/function",
data={
"basic": self.basic_info(),
"plugin": plugin,
"function_info": {
"name": function_name,
"description": function_description,
}
}
)

35
pkg/utils/center/v2.py Normal file
View File

@@ -0,0 +1,35 @@
from __future__ import annotations
import logging
from . import apigroup
from .groups import main
from .groups import usage
from .groups import plugin
BACKEND_URL = "https://api.qchatgpt.rockchin.top/api/v2"
class V2CenterAPI:
"""中央服务器 v2 API 交互类"""
main: main.V2MainDataAPI = None
"""主 API 组"""
usage: usage.V2UsageDataAPI = None
"""使用量 API 组"""
plugin: plugin.V2PluginDataAPI = None
"""插件 API 组"""
def __init__(self, basic_info: dict = None, runtime_info: dict = None):
"""初始化"""
logging.debug("basic_info: %s, runtime_info: %s", basic_info, runtime_info)
apigroup.APIGroup._basic_info = basic_info
apigroup.APIGroup._runtime_info = runtime_info
self.main = main.V2MainDataAPI(BACKEND_URL)
self.usage = usage.V2UsageDataAPI(BACKEND_URL)
self.plugin = plugin.V2PluginDataAPI(BACKEND_URL)

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,7 @@ 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
from .center import v2 as center_v2
context = {
@@ -114,3 +115,16 @@ def get_thread_ctl() -> threadctl.ThreadCtl:
t: threadctl.ThreadCtl = context['pool_ctl']
context_lock.release()
return t
def set_center_v2_api(inst: center_v2.V2CenterAPI):
context_lock.acquire()
context['center_v2_api'] = inst
context_lock.release()
def get_center_v2_api() -> center_v2.V2CenterAPI:
context_lock.acquire()
t: center_v2.V2CenterAPI = context['center_v2_api']
context_lock.release()
return t

View File

@@ -1,19 +0,0 @@
# OpenAI账号免费额度剩余查询
import requests
def fetch_credit_data(api_key: str, http_proxy: str) -> dict:
"""OpenAI账号免费额度剩余查询"""
proxies = {
"http":http_proxy,
"https":http_proxy
} if http_proxy is not None else None
resp = requests.get(
url="https://api.openai.com/dashboard/billing/credit_grants",
headers={
"Authorization": "Bearer {}".format(api_key),
},
proxies=proxies
)
return resp.json()

View File

@@ -28,10 +28,6 @@ def init_runtime_log_file():
if not os.path.exists("logs"):
os.mkdir("logs")
# 检查本目录是否有qchatgpt.log若有移动到logs目录
if os.path.exists("qchatgpt.log"):
shutil.move("qchatgpt.log", "logs/qchatgpt.legacy.log")
log_file_name = "logs/qchatgpt-%s.log" % time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())

7
pkg/utils/platform.py Normal file
View File

@@ -0,0 +1,7 @@
import os
import sys
def get_platform() -> str:
"""获取当前平台"""
return sys.platform

View File

@@ -28,7 +28,7 @@ def reload_all(notify=True):
import main
main.stop()
# 删除所有已注册的
# 删除所有已注册的
import pkg.qqbot.cmds.aamgr as cmdsmgr
cmdsmgr.__command_list__ = {}
cmdsmgr.__tree_index__ = {}

View File

@@ -11,6 +11,8 @@ from ..utils import context
text_render_font: ImageFont = None
def initialize():
global text_render_font
logging.debug("初始化文字转图片模块...")
config = context.get_config_manager().data
if config['blob_message_strategy'] == "image": # 仅在启用了image时才加载字体
@@ -37,6 +39,8 @@ def initialize():
traceback.print_exc()
logging.error("加载字体文件失败({})更换为转发消息组件以发送长消息您可以在config.py中调整相关设置。".format(use_font))
config['blob_message_strategy'] = "forward"
logging.debug("字体文件加载完成。")
def indexNumber(path=''):
@@ -119,6 +123,8 @@ def compress_image(infile, outfile='', kb=100, step=20, quality=90):
def text_to_image(text_str: str, save_as="temp.png", width=800):
global text_render_font
logging.debug("正在将文本转换为图片...")
text_str = text_str.replace("\t", " ")
# 分行
@@ -128,9 +134,13 @@ def text_to_image(text_str: str, save_as="temp.png", width=800):
final_lines = []
text_width = width-80
logging.debug("lines: {}, text_width: {}".format(lines, text_width))
for line in lines:
logging.debug(type(text_render_font))
# 如果长了就分割
line_width = text_render_font.getlength(line)
logging.debug("line_width: {}".format(line_width))
if line_width < text_width:
final_lines.append(line)
continue
@@ -160,7 +170,7 @@ def text_to_image(text_str: str, save_as="temp.png", width=800):
img = Image.new('RGBA', (width, max(280, len(final_lines) * 35 + 65)), (255, 255, 255, 255))
draw = ImageDraw.Draw(img, mode='RGBA')
logging.debug("正在绘制图片...")
# 绘制正文
line_number = 0
offset_x = 20
@@ -192,7 +202,7 @@ def text_to_image(text_str: str, save_as="temp.png", width=800):
line_number += 1
logging.debug("正在保存图片...")
img.save(save_as)
return save_as

View File

@@ -1,11 +1,15 @@
from __future__ import annotations
import datetime
import logging
import os.path
import time
import requests
from . import constants
from . import network
from . import context
def check_dulwich_closure():
@@ -21,18 +25,6 @@ def check_dulwich_closure():
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
def pull_latest(repo_path: str) -> bool:
"""拉取最新代码"""
check_dulwich_closure()
from dulwich import porcelain
repo = porcelain.open_repo(repo_path)
porcelain.pull(repo)
return True
def is_newer(new_tag: str, old_tag: str):
"""判断版本是否更新,忽略第四位版本和第一位版本"""
if new_tag == old_tag:
@@ -107,7 +99,10 @@ def compare_version_str(v0: str, v1: str) -> int:
def update_all(cli: bool = False) -> bool:
"""检查更新并下载源码"""
start_time = time.time()
current_tag = get_current_tag()
old_tag = current_tag
rls_list = get_release_list()
@@ -200,6 +195,13 @@ def update_all(cli: bool = False) -> bool:
with open("current_tag", "w") as f:
f.write(current_tag)
context.get_center_v2_api().main.post_update_record(
spent_seconds=int(time.time()-start_time),
infer_reason="update",
old_version=old_tag,
new_version=current_tag,
)
# 通知管理员
if not cli:
import pkg.utils.context
@@ -240,35 +242,6 @@ def get_current_version_info() -> str:
return "未知版本"
def get_commit_id_and_time_and_msg() -> str:
"""获取当前提交id和时间和提交信息"""
check_dulwich_closure()
from dulwich import porcelain
repo = porcelain.open_repo('.')
for entry in repo.get_walker():
tz = datetime.timezone(datetime.timedelta(hours=entry.commit.commit_timezone // 3600))
dt = datetime.datetime.fromtimestamp(entry.commit.commit_time, tz)
return str(entry.commit.id)[2:9] + " " + dt.strftime('%Y-%m-%d %H:%M:%S') + " [" + str(entry.commit.message, encoding="utf-8").strip()+"]"
def get_current_commit_id() -> str:
"""检查是否有新版本"""
check_dulwich_closure()
from dulwich import porcelain
repo = porcelain.open_repo('.')
current_commit_id = ""
for entry in repo.get_walker():
current_commit_id = str(entry.commit.id)[2:-1]
break
return current_commit_id
def is_new_version_available() -> bool:
"""检查是否有新版本"""
# 从github获取release列表

View File

@@ -11,3 +11,4 @@ nakuru-project-idk
CallingGPT
tiktoken
PyYaml
aiohttp

BIN
res/QChatGPT-1211.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View File

@@ -310,6 +310,8 @@ require_ver("v2.5.1", "v2.6.0") # 要求最低版本为 v2.5.1, 同时要求最
### 说明
> 下一版本将会添加统一的插件API欢迎在[此讨论](https://github.com/RockChinQ/QChatGPT/discussions/637)回复您的需求!
事件处理函数将会获得一系列参数,可以在`kwargs`中取出。
其中`host`参数(`pkg.plugin.host.PluginHost`类的实例)是插件宿主,提供与主程序各个模块交互的一些方法。
`event`参数(`pkg.plugin.host.EventContext`类的实例)是事件执行期间的上下文,提供对此次事件执行的一些操作方法。

View File

@@ -1,3 +1,6 @@
> [!WARNING]
> 此 Wiki 已弃用,所有文档已迁移到 [项目主页](https://qchatgpt.rockchin.top)
欢迎查看QChatGPT的Wiki页。
## 简介

View File

@@ -0,0 +1,43 @@
import os
import uuid
import json
# 向 ~/.qchatgpt 写入一个 标识符
if not os.path.exists(os.path.expanduser('~/.qchatgpt')):
os.mkdir(os.path.expanduser('~/.qchatgpt'))
identifier = {
"host_id": "host_"+str(uuid.uuid4()),
}
if not os.path.exists(os.path.expanduser('~/.qchatgpt/host.json')):
print('create ~/.qchatgpt/host.json')
with open(os.path.expanduser('~/.qchatgpt/host.json'), 'w') as f:
json.dump(identifier, f)
else:
print('load ~/.qchatgpt/host.json')
with open(os.path.expanduser('~/.qchatgpt/host.json'), 'r') as f:
identifier = json.load(f)
print(identifier)
instance_id = {
"host_id": identifier['host_id'],
"instance_id": "instance_"+str(uuid.uuid4()),
}
# 实例 id
if os.path.exists("res/instance_id.json"):
with open("res/instance_id.json", 'r') as f:
instance_id = json.load(f)
if instance_id['host_id'] != identifier['host_id']:
os.remove("res/instance_id.json")
if not os.path.exists("res/instance_id.json"):
print('create res/instance_id.json')
with open("res/instance_id.json", 'w') as f:
json.dump(instance_id, f)
print(instance_id)

View File

@@ -14,7 +14,7 @@ rate_limit_drop_tip = "本分钟对话次数超过限速次数,此对话被丢
# 若设置为空字符串,则不发送提示信息
message_drop_tip = "[bot]当前有一条消息正在处理,请等待处理完成"
# 指令!help帮助消息
# 命令 !help帮助消息
help_message = """此机器人通过调用大型语言模型生成回复,不具有情感。
你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。
欢迎到github.com/RockChinQ/QChatGPT 给个star"""
@@ -24,10 +24,10 @@ reply_message = "[bot]err:请求超时"
# 群聊消息超时提示
replys_message = "[bot]err:请求超时"
# 令权限不足提示
# 令权限不足提示
command_admin_message = "[bot]err:权限不足: "
# 令无效提示
command_err_message = "[bot]err:令不存在:"
# 令无效提示
command_err_message = "[bot]err:令不存在:"
# 会话重置提示
command_reset_message = "[bot]会话已重置"