Compare commits

..

752 Commits

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
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
Junyan Qin 45e4096a12 Merge pull request #587 from RockChinQ/hotfix/openai-1.0-adaptation
Feat: 适配openai>=1.0.0
2023-11-10 11:49:20 +08:00
GitHub Actions 8a1d4fe287 Update override-all.json 2023-11-10 03:47:30 +00:00
RockChinQ 98f880ebc2 chore: 群内回复不再默认引用原消息 2023-11-10 11:47:10 +08:00
RockChinQ 2b852853f3 feat: 适配completion和chat_completions 2023-11-10 11:31:14 +08:00
RockChinQ c7a9988033 feat: 以新的方式设置正向代理 2023-11-10 10:54:03 +08:00
RockChinQ c475eebe1c chore: 不再限制openai版本 2023-11-10 10:14:11 +08:00
RockChinQ 0fe7355ae0 hotfix: 适配openai>=1.0.0 2023-11-10 10:13:50 +08:00
Junyan Qin 57de96e3a2 chore(requirements.txt): 锁定openai版本到0.28.1 2023-11-10 09:31:27 +08:00
Junyan Qin 70571cef50 Update README.md 2023-10-02 17:31:08 +08:00
Junyan Qin 0b6deb3340 Update README.md 2023-10-02 17:23:36 +08:00
Junyan Qin dcda85a825 Merge pull request #580 from RockChinQ/dependabot/pip/openai-approx-eq-0.28.1
chore(deps): update openai requirement from ~=0.28.0 to ~=0.28.1
2023-10-02 16:10:37 +08:00
dependabot[bot] 9d3bff018b chore(deps): update openai requirement from ~=0.28.0 to ~=0.28.1
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.28.0...v0.28.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 08:09:30 +00:00
RockChinQ 051376e0d2 Release v2.6.1 2023-09-28 12:18:46 +00:00
Junyan Qin a113785211 Merge pull request #578 from RockChinQ/fix/blocked-audit-upload
[Fix] 阻塞地发送审计报告数据
2023-09-28 20:17:26 +08:00
RockChinQ 3f4ed4dc3c fix: 阻塞地发送审计报告数据 2023-09-28 12:16:30 +00:00
Junyan Qin ac80764fae Merge pull request #577 from RockChinQ/doc/deadlinks-in-wiki
[Doc] 修复wiki中的死链
2023-09-28 20:04:01 +08:00
RockChinQ e43afd4891 doc: 修复wiki中的死链 2023-09-28 12:03:27 +00:00
RockChinQ f1aea1d495 doc: 统一改称指令为命令 2023-09-28 11:46:33 +00:00
GitHub Actions 0e2a5db104 Update override-all.json 2023-09-26 16:10:22 +00:00
Junyan Qin 3a4c9771fa feat(config): 默认超时时间改为两分钟 2023-09-27 00:09:58 +08:00
RockChinQ f4f8ef9523 ci: 工作流统一双空格缩进 2023-09-13 08:27:47 +00:00
RockChinQ b9ace69a72 Release v2.6.0 2023-09-13 08:13:24 +00:00
RockChinQ aef0b2a26e ci: 修复GITHUB_REF判断逻辑 2023-09-13 08:12:46 +00:00
RockChinQ f7712d71ec feat(pkgmgr): 使用清华源执行pip操作 2023-09-13 07:54:53 +00:00
RockChinQ e94b44e3b8 chore: 更新.gitignore 2023-09-13 07:22:12 +00:00
Junyan Qin 524e863c78 Merge pull request #567 from ruuuux/patch-1
添加 WikipediaSearch 插件
2023-09-13 11:57:54 +08:00
ruuuux bbc80ac901 添加 WikipediaSearch 插件 2023-09-13 11:55:11 +08:00
GitHub Actions f969ddd6ca Update override-all.json 2023-09-13 03:09:55 +00:00
RockChinQ 1cc9781333 chore(config): 添加 One API 的注释说明 2023-09-13 03:09:35 +00:00
Junyan Qin a609801bae Merge pull request #551 from flashszn/master
加入one-api项目支持的国内大模型
2023-09-13 11:00:21 +08:00
RockChinQ d8b606d372 doc(README.md): 添加 One API 支持公告 2023-09-13 02:45:04 +00:00
RockChinQ 572a440e65 doc(README.md): 添加 One API 的说明 2023-09-13 02:41:22 +00:00
RockChinQ 6e4eeae9b7 doc: 添加one-api模型注释说明 2023-09-13 02:34:11 +00:00
Shi Zhenning 1a73669df8 加入符合oneapi项目接口的国内模型
oneapi是一个api整合项目,通过这个项目的反代理,可以像使用gpt系列的/v1/completion接口一样调用国内的大模型,仅仅需要更改一下模型名字
2023-09-13 02:34:11 +00:00
RockChinQ 91ebaf1122 doc(README.md): 添加内容 2023-09-12 13:26:41 +00:00
RockChinQ 46703eb906 doc(README.md): docker部署说明 2023-09-12 13:09:42 +00:00
Junyan Qin b9dd9d5193 Merge pull request #566 from RockChinQ/docker-deployment
[CI] Docker 部署最佳实践
2023-09-12 21:06:53 +08:00
RockChinQ 884481a4ec doc(README.md): 镜像徽章 2023-09-12 13:04:18 +00:00
RockChinQ 9040b37a63 chore: 默认安装PyYaml依赖 2023-09-12 12:53:16 +00:00
RockChinQ 99d47b2fa2 doc: 修改Docker部署指引 2023-09-12 12:53:03 +00:00
RockChinQ 6575359a94 doc: 添加docker部署指南 2023-09-12 12:51:06 +00:00
RockChinQ a2fc726372 deploy: 添加docker-compose.yaml 2023-09-12 12:50:49 +00:00
RockChinQ 3bfce8ab51 ci: 优化docker镜像构建脚本 2023-09-12 10:21:40 +00:00
RockChinQ ff9a9830f2 chore: 更新requirements.txt 2023-09-12 10:21:19 +00:00
RockChinQ e2b59e8efe ci: 更新Dockerfile 2023-09-12 10:21:03 +00:00
Junyan Qin 04dad9757f Merge pull request #565 from RockChinQ/docker-image-test
[CI] Docker 部署脚本同步
2023-09-12 16:34:38 +08:00
Junyan Qin 75ea1080ad Merge pull request #351 from q123458384/patch-2
Create build_docker_image.yml
2023-09-12 15:57:44 +08:00
Junyan Qin e25b064319 doc(README.md): 为群号添加链接 2023-09-10 23:22:38 +08:00
Junyan Qin 5d0dbc40ce doc(README.md): 添加社区使用手册链接 2023-09-10 23:17:22 +08:00
Junyan Qin beae8de5eb Merge pull request #563 from RockChinQ/dependabot/pip/dulwich-approx-eq-0.21.6
chore(deps): update dulwich requirement from ~=0.21.5 to ~=0.21.6
2023-09-04 17:23:52 +08:00
dependabot[bot] c4ff30c722 chore(deps): update dulwich requirement from ~=0.21.5 to ~=0.21.6
Updates the requirements on [dulwich](https://github.com/dulwich/dulwich) to permit the latest version.
- [Release notes](https://github.com/dulwich/dulwich/releases)
- [Changelog](https://github.com/jelmer/dulwich/blob/master/NEWS)
- [Commits](https://github.com/dulwich/dulwich/compare/dulwich-0.21.5...dulwich-0.21.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 09:09:35 +00:00
Junyan Qin 6f4ecb101b Merge pull request #562 from RockChinQ/dependabot/pip/openai-approx-eq-0.28.0
chore(deps): update openai requirement from ~=0.27.9 to ~=0.28.0
2023-09-04 17:08:47 +08:00
dependabot[bot] 9f9b0ef846 chore(deps): update openai requirement from ~=0.27.9 to ~=0.28.0
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.9...v0.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 08:06:36 +00:00
RockChinQ de6957062c chore(config): 修改公用反代地址 2023-09-02 19:48:45 +08:00
Junyan Qin 0a9b43e6fa doc(README.md):社区群群号 2023-09-01 08:49:47 +08:00
Junyan Qin 5b0edd9937 Merge pull request #559 from oliverkirk-sudo/master
新增插件
2023-08-31 18:14:52 +08:00
oliverkirk-sudo 8a400d202a Update README.md
添加插件
2023-08-31 18:10:46 +08:00
RockChinQ 5a1e9f7fb2 doc(README.md): 修改徽章样式 2023-08-31 08:40:11 +00:00
RockChinQ e03af75cf8 doc: 更新部署节说明 2023-08-31 02:26:13 +00:00
RockChinQ 0da4919255 doc: 整理README.md格式 2023-08-31 02:23:30 +00:00
RockChinQ 914e566d1f doc(README.md): 更新wiki链接 2023-08-30 08:46:17 +00:00
RockChinQ 6ec2b653fe doc(wiki): 为wiki页标号 2023-08-30 08:41:59 +00:00
RockChinQ ba0a088b9c doc(wiki): 常见问题标号 2023-08-30 08:38:44 +00:00
RockChinQ 478e83bcd9 ci: 更新wiki同步工作流 2023-08-30 08:38:26 +00:00
RockChinQ 386124a3b9 doc(wiki): 页面标号 2023-08-30 08:36:30 +00:00
RockChinQ ff5e7c16d1 doc(wiki): 插件相关文档typo 2023-08-30 08:34:03 +00:00
RockChinQ 7ff7a66012 doc: 更新gpt4free的说明文档 2023-08-29 14:42:44 +08:00
Junyan Qin c99dfb8a86 Merge pull request #557 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.9
chore(deps): update openai requirement from ~=0.27.8 to ~=0.27.9
2023-08-28 16:29:55 +08:00
dependabot[bot] 10f9d4c6b3 chore(deps): update openai requirement from ~=0.27.8 to ~=0.27.9
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.8...v0.27.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-28 08:29:19 +00:00
Junyan Qin d347813411 Merge pull request #556 from oliverkirk-sudo/master
Update README.md
2023-08-24 17:06:29 +08:00
oliverkirk-sudo 7a93898b3f Update README.md 2023-08-24 17:00:18 +08:00
Junyan Qin c057ea900f Merge pull request #548 from oliverkirk-sudo/master
修改插件信息
2023-08-15 09:35:55 +08:00
oliverkirk-sudo 512266e74f 修改插件信息 2023-08-14 23:21:51 +08:00
RockChinQ e36aee11c7 doc(README.md): 更新README.md 2023-08-14 19:12:39 +08:00
RockChinQ 97421299f5 doc(README.md): Claude和Bard的说明 2023-08-14 19:11:48 +08:00
Junyan Qin bc41e5aa80 Update README.md 2023-08-14 16:57:55 +08:00
RockChinQ 2fa30e7def doc(README.md): 徽章格式 2023-08-14 16:56:50 +08:00
RockChinQ 1c6a7d9ba5 doc(README.md): 添加群号徽章 2023-08-14 16:09:11 +08:00
RockChinQ 47435c42a5 doc(README.md): 更新视频教程 2023-08-12 12:26:00 +08:00
Junyan Qin 39a1b421e6 doc(CONTRIBUTING.md): 添加字段使用规范 2023-08-09 20:12:46 +08:00
Junyan Qin b5edf2295b doc(CONTRIBUTING.md): 添加代码规范 2023-08-08 20:14:00 +08:00
RockChinQ fb650a3d7a chore: config.py添加反代地址 2023-08-07 11:43:28 +08:00
Junyan Qin 521541f311 Merge pull request #534 from RockChinQ/doc-add-gif
[Doc] 添加演示GIF图
2023-08-06 17:20:49 +08:00
Junyan Qin 7020abadbf Add files via upload 2023-08-06 17:19:54 +08:00
Junyan Qin d95fb3b5be Delete webwlkr-demo.gif 2023-08-06 17:18:58 +08:00
Junyan Qin 3e524dc790 Add files via upload 2023-08-06 17:18:00 +08:00
Junyan Qin a64940bff8 Update README.md 2023-08-06 17:13:10 +08:00
Junyan Qin c739290f0b Add files via upload 2023-08-06 17:07:22 +08:00
RockChinQ af292fe050 Release v2.5.2 2023-08-06 14:58:13 +08:00
Junyan Qin 634c7fb302 Merge pull request #533 from RockChinQ/perf-function-call-process
[Perf] 优化函数调用的底层逻辑
2023-08-06 14:52:01 +08:00
RockChinQ 33efb94013 feat: 应用cmdpriv时忽略不存在的命令 2023-08-06 14:50:22 +08:00
GitHub Actions 549e4dc02e Update override-all.json 2023-08-06 06:41:00 +00:00
RockChinQ 3d40909c02 feat: 不再默认启用trace_function_calls 2023-08-06 14:40:35 +08:00
RockChinQ 1aef81e38f perf: 修改网络问题时的报错 2023-08-06 12:17:04 +08:00
RockChinQ 1b0ae8da58 refactor: session append重命名为query 2023-08-05 22:00:32 +08:00
GitHub Actions Bot 7979a8e97f Update cmdpriv-template.json 2023-08-05 13:52:03 +00:00
RockChinQ 080e53d9a9 feat: 刪除continue命令 2023-08-05 21:51:34 +08:00
GitHub Actions 89bb364b16 Update override-all.json 2023-08-05 13:44:30 +00:00
RockChinQ 3586cd941f feat: 支持跟踪函数调用过程并默认启用 2023-08-05 21:44:11 +08:00
RockChinQ 054d0839ac fix: 未序列化的function_call属性 2023-08-04 19:08:48 +08:00
RockChinQ dd75f98d85 feat: 世界上最先进的调用流程 2023-08-04 18:41:04 +08:00
RockChinQ ec23bb5268 doc(README.md): 添加视频教程链接 2023-08-04 17:22:13 +08:00
Junyan Qin bc99db4fc1 Merge pull request #531 from RockChinQ/feat-load-balance
[Feat] api-key主动负载均衡
2023-08-04 17:14:03 +08:00
RockChinQ c8275fcfbf feat(openai): 支持apikey主动切换策略 2023-08-04 17:10:07 +08:00
GitHub Actions a345043c30 Update override-all.json 2023-08-04 07:21:52 +00:00
RockChinQ 382d37d479 chore: 添加key切换策略配置项 2023-08-04 15:21:31 +08:00
RockChinQ 32c144a75d doc(README.md): 增加简介 2023-08-03 23:40:01 +08:00
RockChinQ 7ca2aa5e39 doc(wiki): 说明逆向库插件也支持函数调用 2023-08-03 18:44:16 +08:00
RockChinQ 86cc4a23ac fix: func命令列表标号未自增 2023-08-03 17:53:43 +08:00
RockChinQ 08d1e138bd doc: 删除过时公告 2023-08-02 21:46:59 +08:00
Junyan Qin a9fe86542f Merge pull request #530 from RockChinQ/doc-readme
[Docs] 重新整理README.md格式
2023-08-02 21:09:39 +08:00
RockChinQ 4e29776fcd doc: 整理插件生态章节 2023-08-02 21:07:31 +08:00
RockChinQ ee3eae8f4d doc: 完善徽章 2023-08-02 21:06:34 +08:00
RockChinQ a84575858a doc: 整理徽章 2023-08-02 21:04:23 +08:00
RockChinQ ac472291c7 doc: 赞赏章节 2023-08-02 20:59:01 +08:00
RockChinQ f304873c6a doc(wiki): 内容函数页 2023-08-02 20:59:01 +08:00
RockChinQ 18caf8face doc: 致谢章节 2023-08-02 20:59:01 +08:00
RockChinQ d21115aaa8 doc: 优化起始章节 2023-08-02 20:59:01 +08:00
RockChinQ a05ecd2e7f doc: 更多section 2023-08-02 20:59:01 +08:00
RockChinQ 32a725126d doc: 模型适配一览 2023-08-02 20:59:01 +08:00
RockChinQ 0528690622 doc: 修改logo 2023-08-02 20:51:07 +08:00
Junyan Qin 819339142e Merge pull request #529 from RockChinQ/feat-funcs-called-args
[Feat] NormalMessageResponded添加func_called参数
2023-08-02 18:02:48 +08:00
RockChinQ 1d0573e7ff feat: NormalMessageResponded添加func_called参数 2023-08-02 18:01:02 +08:00
RockChinQ 00623bc431 typo(plugin): 插件执行报错提示 2023-08-02 11:35:20 +08:00
Junyan Qin c872264456 Merge pull request #525 from RockChinQ/feat-finish-reason-param
[Feat] 为NormalMessageResponded事件添加finish_reason参数
2023-08-01 14:40:14 +08:00
RockChinQ 1336d3cb9a fix: chat_completion不传回finish_reason的问题 2023-08-01 14:39:57 +08:00
RockChinQ d1459578cd doc(wiki): 插件开发页说明 2023-08-01 14:33:32 +08:00
RockChinQ 8a67fcf40f feat: 为NormalMessageResponded事件添加finish_reason参数 2023-08-01 14:31:38 +08:00
RockChinQ 7930370aa9 chore: 发布函数调用功能公告 2023-08-01 10:50:23 +08:00
RockChinQ 0b854bdcf1 feat(chat_completion): 不生成到stop以使max_tokens参数生效 2023-08-01 10:26:23 +08:00
Junyan Qin cba6aab48d Merge pull request #524 from RockChinQ/feat-at-sender
[Feat] 支持在群内回复时at发送者
2023-08-01 10:14:34 +08:00
GitHub Actions 12a9ca7a77 Update override-all.json 2023-08-01 02:13:35 +00:00
RockChinQ a6cbd226e1 feat: 支持设置群内回复时at发送者 2023-08-01 10:13:15 +08:00
RockChinQ 3577e62b41 perf: 简化启动时输出 2023-07-31 21:11:28 +08:00
RockChinQ f86e69fcd1 perf: 简化启动时的输出信息 2023-07-31 21:05:23 +08:00
RockChinQ 292e00b078 perf: 简化启动时的输出信息 2023-07-31 21:04:59 +08:00
RockChinQ 2a91497bcf chore: .gitignore排除qcapi/ 2023-07-31 20:23:54 +08:00
RockChinQ b0cca0a4c2 Release v2.5.1 2023-07-31 18:12:59 +08:00
Junyan Qin a2bda85a9c Merge pull request #523 from RockChinQ/feat-prompt-preprocess-event
[Feat] 新增PromptPreprocessing事件
2023-07-31 17:55:06 +08:00
RockChinQ 20677cff86 doc(wiki): 插件开发页增加版本断言说明 2023-07-31 17:53:33 +08:00
RockChinQ c8af5d8445 feat: 添加版本断言函数require_ver 2023-07-31 17:46:30 +08:00
RockChinQ 2dbe984539 doc(wiki): 添加事件wiki说明 2023-07-31 17:27:28 +08:00
RockChinQ 6b8fa664f1 feat: 新增PromptPreprocessing事件 2023-07-31 17:21:09 +08:00
RockChinQ 2b9612e933 chore: 提交部分测试文件 2023-07-31 16:24:39 +08:00
RockChinQ 749d0219fb chore: 删除弃用模块 2023-07-31 16:23:31 +08:00
Junyan Qin a11a152bd7 ci: 解决sync-wiki.yml异常退出问题 2023-07-31 15:41:37 +08:00
Junyan Qin fc803a3742 Merge pull request #522 from RockChinQ/feat-generating-stop-case
[Feat] 新增!continue命令
2023-07-31 15:34:30 +08:00
GitHub Actions Bot 13a1e15f24 Update cmdpriv-template.json 2023-07-31 07:24:14 +00:00
RockChinQ 3f41b94da5 feat: 完善命令文档 2023-07-31 15:23:42 +08:00
RockChinQ 0fb5bfda20 ci: 添加tiktoken依赖 2023-07-31 15:20:23 +08:00
RockChinQ dc1fd73ebb feat: 添加continue命令 2023-07-31 15:17:49 +08:00
Junyan Qin 161b694f71 Merge pull request #521 from RockChinQ/fix-usage-not-reported
[Fix] text的使用量未上报
2023-07-31 14:31:48 +08:00
RockChinQ 45d1c89e45 fix: text的使用量未上报 2023-07-31 14:28:48 +08:00
Junyan Qin e26664aa51 Merge pull request #520 from RockChinQ/feat-accurately-calculate-tokens
feat: 使用tiktoken计算tokens数
2023-07-31 12:16:10 +08:00
RockChinQ e29691efbd feat: 使用tiktoken计算tokens数 2023-07-31 11:59:22 +08:00
RockChinQ 6d45327882 debug: 接口底层添加返回数据debug信息 2023-07-31 10:37:45 +08:00
RockChinQ fbd41eef49 chore: 删除devcontainer.json 2023-07-31 10:37:14 +08:00
Junyan Qin 0a30c88322 doc(README.md): 插件列表 2023-07-31 00:07:39 +08:00
Junyan Qin 4f5af0e8c8 Merge pull request #518 from RockChinQ/fix-cannot-disable-funcs-dynamically
[Fix] plugin启用禁用命令对内容函数不生效
2023-07-30 23:56:01 +08:00
RockChinQ df3f0fd159 fix: plugin启用禁用命令对内容函数不生效 2023-07-30 23:54:56 +08:00
RockChinQ f2493c79dd doc(wiki): 添加联网内容函数提问示例 2023-07-29 19:34:47 +08:00
RockChinQ a86a035b6b doc: 更新README.md 2023-07-29 19:26:28 +08:00
RockChinQ 7995793bfd doc(wiki): 添加内容函数页 2023-07-29 19:24:56 +08:00
RockChinQ a56b340646 Release v2.5.0 2023-07-29 18:59:25 +08:00
Junyan Qin 7473cdfe16 Merge pull request #513 from RockChinQ/feat-function-calling-integration
[Feat] 支持GPT的函数调用功能
2023-07-29 18:57:29 +08:00
RockChinQ 24273ac158 doc: README添加内容函数相关内容 2023-07-29 18:55:18 +08:00
RockChinQ fe6275000e doc(wiki): 更新wiki插件页 2023-07-29 18:40:49 +08:00
RockChinQ 5fbf369f82 doc(wiki): 更新插件页 2023-07-29 18:37:03 +08:00
Junyan Qin 4400475ffa chore: 添加Webwlkr插件示例 2023-07-29 17:41:56 +08:00
GitHub Actions Bot 796eb7c95d Update cmdpriv-template.json 2023-07-29 09:30:22 +00:00
RockChinQ 89a01378e7 ci: 跑工作流 2023-07-29 17:29:52 +08:00
RockChinQ f4735e5e30 ci(cmd_priv): 添加CallingGPT依赖 2023-07-29 17:28:11 +08:00
RockChinQ f1bb3045aa feat: 添加func命令 2023-07-29 17:26:07 +08:00
RockChinQ 96e474a555 feat: 插件开关对其内容函数生效 2023-07-29 17:10:47 +08:00
RockChinQ 833d29b101 typo: enable->enabled 2023-07-29 16:55:01 +08:00
RockChinQ dce6734ba2 feat: 改为推荐使用func()装饰器注册内容函数 2023-07-29 16:51:19 +08:00
RockChinQ 0481167dc6 feat: 改为在start流程设置openai.proxy 2023-07-29 16:36:31 +08:00
RockChinQ a002f93f7b chore: 删除过时代码 2023-07-29 16:30:09 +08:00
RockChinQ 3c894fe70e feat: chat_completion的函数开关支持 2023-07-29 16:29:16 +08:00
RockChinQ 8c69b8a1d9 feat: 内容函数全局开关支持 2023-07-29 16:28:18 +08:00
Junyan Qin a9dae05303 doc(README.md): 修改社区群群号 2023-07-29 13:31:58 +08:00
RockChinQ ae6994e241 feat(contentPlugin): 完成基本的内容函数调用功能 2023-07-28 19:03:02 +08:00
Rock Chin caa72fa40c feat: 在插件层面初步支持内容函数 2023-07-27 14:27:36 +08:00
Junyan Qin 46cc9220c3 Merge pull request #506 from RockChinQ/perf-persist-dprompt-when-auto-reset
[Perf] 在session自动重置时保留非default的prompt
2023-07-07 17:53:29 +08:00
Rock Chin ddb56d7a8e fix: reset命令错误的逻辑 2023-07-07 17:49:43 +08:00
Rock Chin a0267416d7 fix: 修复reset逻辑导致的无法初始化情景预设问题 2023-07-07 16:37:05 +08:00
Rock Chin 56e1ef3602 fix: 修复reset可能引起的bug 2023-07-07 16:35:37 +08:00
Rock Chin b4fc1057d1 perf: 在session自动重置时保留非default的prompt (#494) 2023-07-06 23:09:39 +08:00
Rock Chin 06037df607 ci: 仅在master分支运行sync-wiki工作流 2023-06-20 22:42:18 +08:00
Rock Chin dce134d08d Release v2.4.7 2023-06-16 19:43:13 +08:00
Junyan Qin cca471d068 Merge pull request #500 from RockChinQ/perf-more-model-support
Perf more model support
2023-06-16 19:40:29 +08:00
Rock Chin ddb211b74a feat: 支持新的模型 2023-06-16 19:35:26 +08:00
Rock Chin cef70751ff chore: 修改配置文件说明 2023-06-16 19:35:06 +08:00
JunYan Qin 2d2219fc6e 更新 README.md 2023-06-13 11:59:30 +08:00
JunYan Qin 514a6b4192 更新 README.md 2023-06-13 11:59:07 +08:00
JunYan Qin 7a552b3434 Merge pull request #496 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.8
chore(deps): update openai requirement from ~=0.27.7 to ~=0.27.8
2023-06-12 23:20:02 +08:00
dependabot[bot] ecebd1b0e0 chore(deps): update openai requirement from ~=0.27.7 to ~=0.27.8
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.7...v0.27.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-12 09:01:51 +00:00
Rock Chin 8dc34d2a88 doc(README.md): 添加卖号网站 2023-06-12 14:13:36 +08:00
Rock Chin d52644ceec doc: 更新README.md 2023-06-12 12:24:59 +08:00
Rock Chin 3052510591 Release v2.4.6 2023-06-08 14:01:18 +08:00
Rock Chin 777a5617db Merge pull request #492 from RockChinQ/feat-ignore-major-vernum
[Feat] 更新时忽略主版本号不同的版本
2023-06-08 14:00:32 +08:00
Rock Chin e17c1087e9 feat(updater.py): 更新时忽略主版本号不同的版本 2023-06-08 13:57:24 +08:00
Rock Chin 633695175a Merge pull request #491 from RockChinQ/feat-tokens-auto-reset
[Feat] token超限报错时自动重置会话
2023-06-08 13:49:49 +08:00
Rock Chin 9e78bf3d21 perf: 更严格的重置条件判断 2023-06-08 13:49:20 +08:00
Rock Chin 43aa68a55d feat: 支持在token超限时自动重置会话 2023-06-08 13:45:54 +08:00
Rock Chin b8308f8c57 Merge branch 'feat-tokens-auto-reset' of https://github.com/RockChinQ/QChatGPT into feat-tokens-auto-reset 2023-06-08 13:43:36 +08:00
Rock Chin 466bfbddeb perf: 提示语格式 2023-06-08 13:43:33 +08:00
GitHub Actions b6da07b225 Update override-all.json 2023-06-08 05:20:55 +00:00
Rock Chin 2f2159239a chore: 添加开关和提示语配置项 2023-06-08 13:20:33 +08:00
Rock Chin 67d1ca8a65 Merge pull request #490 from RockChinQ/feat-global-group-private-enable
[Feat] 支持设置全局群聊/私聊消息禁用
2023-06-07 23:49:53 +08:00
Rock Chin 497a393e83 doc: 修改wiki 2023-06-07 23:49:09 +08:00
Rock Chin 782c0e22ea feat: 支持设置全局群聊、私聊禁用 2023-06-07 23:47:13 +08:00
Rock Chin 2932fc6dfd chore(banlist-template.py): 添加配置项 2023-06-07 23:23:21 +08:00
Rock Chin 0a9eab2113 chore(requirements.txt): 更新requests版本 2023-06-06 09:37:00 +08:00
Rock Chin 50a673a8ec doc: 添加插件列表list 2023-06-05 22:37:19 +08:00
Rock Chin 9e25d0f9e4 Release v2.4.5 2023-05-31 18:31:25 +08:00
Rock Chin 23cd7be711 Merge pull request #487 from RockChinQ/feat-banlist-syntax-check
feat: 初始化流程异常处理
2023-05-31 18:25:46 +08:00
Rock Chin 025b9e33f1 feat: 初始化流程异常处理 2023-05-31 18:24:01 +08:00
Rock Chin bab2f64913 doc(README_en.md): 添加wakapi计时 2023-05-29 11:12:07 +08:00
Rock Chin b00e09aa9c doc: 添加wakapi计时 2023-05-29 11:10:49 +08:00
Rock Chin 0b109fdc7a Merge pull request #479 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.7
chore(deps): update openai requirement from ~=0.27.6 to ~=0.27.7
2023-05-22 17:13:50 +08:00
dependabot[bot] 018fea2ddb chore(deps): update openai requirement from ~=0.27.6 to ~=0.27.7
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.6...v0.27.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-22 09:05:00 +00:00
Rock Chin f8a3cc4352 doc: 折起OpenAI注册步骤 2023-05-21 18:25:35 +08:00
Rock Chin 6ab853acc1 doc: 修改关于HuggingChat的说明 2023-05-21 17:50:35 +08:00
Rock Chin e825dea02f chore: 排除hugchat.json 2023-05-21 17:47:42 +08:00
Rock Chin cf8740d16e Merge branch 'master' of https://github.com/RockChinQ/QChatGPT 2023-05-21 17:33:47 +08:00
Rock Chin 9c4809e26f chore: 发布revLibs相关公告 2023-05-21 17:33:44 +08:00
Rock Chin 0a232fd9ef Merge pull request #477 from RockChinQ/feature-detailed-cfg-cmd
[Feat] 支持使用!cfg指令修改子配置项
2023-05-21 15:59:59 +08:00
Rock Chin 23016a0791 doc: 更新wiki说明 2023-05-21 15:58:21 +08:00
Rock Chin cdcc67ff23 feat(!cfg): 使用eval()函数进行类型转换 2023-05-21 15:53:56 +08:00
Rock Chin 92274bfc34 feat(!cfg): 支持使用点号索引子配置项 2023-05-21 15:49:56 +08:00
Rock Chin 2fed6f61ba Release v2.4.4 2023-05-21 15:15:28 +08:00
Rock Chin 59b2cd26d2 Merge pull request #476 from RockChinQ/hotfix-471-at-no-response-aft-reload
[Fix] 热重载之后不响应群内at
2023-05-21 15:12:43 +08:00
Rock Chin f7b87e99d2 fix(manager.py): 热重载之后不响应群内at 2023-05-21 15:11:34 +08:00
Rock Chin 70bc985145 perf(nakuru.py): access-token被拒时报警 2023-05-18 21:06:32 +08:00
Rock Chin 070dbe9108 chore: 排除venv/目录 2023-05-18 21:05:45 +08:00
Rock Chin a63fa6d955 chore: yiri-mirai使用0.2.7 2023-05-18 21:05:30 +08:00
Rock Chin c7703809b0 Merge pull request #475 from RockChinQ/actively-delay
[Feat] 支持设置消息回复强制延迟以降低风控概率
2023-05-18 20:15:52 +08:00
GitHub Actions 37eb74338f Update override-all.json 2023-05-18 12:14:19 +00:00
Rock Chin 77d5585b7c feat: 修改强制延迟默认范围 2023-05-18 20:13:53 +08:00
Rock Chin 6cab3ef029 Merge branch 'actively-delay' of https://github.com/RockChinQ/QChatGPT into actively-delay 2023-05-18 20:12:39 +08:00
Rock Chin 820a7b78fc feat: 处理过程支持强制延迟 2023-05-18 20:12:36 +08:00
GitHub Actions c51dffef3a Update override-all.json 2023-05-18 12:10:33 +00:00
Rock Chin 983bc3da3c chore: 添加强制延迟配置项 2023-05-18 20:10:08 +08:00
Rock Chin 09be956a58 Merge pull request #474 from RockChinQ/command-notfound-err
[Perf] 修改指令不存在时的提示信息
2023-05-18 19:45:25 +08:00
Rock Chin 5eded50c53 perf: 修改指令不存在时的提示信息 2023-05-18 19:44:20 +08:00
Rock Chin 6d8eebd314 doc: 添加微信赞赏码 2023-05-16 15:36:07 +08:00
Rock Chin 19a0572b5f Release v2.4.3.1 2023-05-15 17:38:03 +08:00
Rock Chin 6272e98474 Merge pull request #467 from RockChinQ/perf-plugin-update
[Perf] 优化插件更新相关操作
2023-05-14 18:45:36 +08:00
Rock Chin 45042fe7d4 doc: 更新插件更新命令wiki 2023-05-14 18:44:14 +08:00
Rock Chin d85e840126 perf: 优化插件更新操作,支持更新单个插件 2023-05-14 18:41:20 +08:00
Rock Chin 804889f1de perf: 加载模块的输出改为debug级别 2023-05-14 17:30:05 +08:00
Rock Chin 919c996434 doc: 添加HuggingChat 2023-05-14 17:14:46 +08:00
Rock Chin 00823b3d62 doc(README): 添加HuggingChat 2023-05-14 17:14:28 +08:00
Rock Chin af54efd24a doc(README.md): 添加系统状态插件 2023-05-14 14:58:48 +08:00
Rock Chin b1c9b121f6 Update go-cqhttp配置.md 2023-05-08 21:51:03 +08:00
Rock Chin 7b5649d153 Merge pull request #461 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.6
chore(deps): update openai requirement from ~=0.27.5 to ~=0.27.6
2023-05-08 18:48:20 +08:00
dependabot[bot] 52bf716d84 chore(deps): update openai requirement from ~=0.27.5 to ~=0.27.6
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.5...v0.27.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-08 10:34:21 +00:00
Rock Chin c149dd7b66 Merge pull request #462 from RockChinQ/dependabot/pip/dulwich-approx-eq-0.21.5
chore(deps): update dulwich requirement from ~=0.21.3 to ~=0.21.5
2023-05-08 18:24:59 +08:00
dependabot[bot] 65d5a1ed63 chore(deps): update dulwich requirement from ~=0.21.3 to ~=0.21.5
Updates the requirements on [dulwich](https://github.com/dulwich/dulwich) to permit the latest version.
- [Release notes](https://github.com/dulwich/dulwich/releases)
- [Changelog](https://github.com/jelmer/dulwich/blob/master/NEWS)
- [Commits](https://github.com/dulwich/dulwich/compare/dulwich-0.21.3...dulwich-0.21.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-08 09:01:55 +00:00
Rock Chin 5516754bbb doc(README.md): 添加docker部署提示 2023-05-02 14:30:25 +08:00
Rock Chin 08082f2ee3 Merge pull request #452 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.5
chore(deps): update openai requirement from ~=0.27.4 to ~=0.27.5
2023-05-01 17:26:18 +08:00
dependabot[bot] 8489266080 chore(deps): update openai requirement from ~=0.27.4 to ~=0.27.5
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.4...v0.27.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 09:06:44 +00:00
Rock Chin 51c7e0b235 doc(README.md): 四群群号 2023-04-28 00:37:29 +08:00
Rock Chin 628b6b0bb4 Merge branch 'master' of https://github.com/RockChinQ/QChatGPT 2023-04-27 15:01:47 +08:00
Rock Chin 7e024d860d doc: 增加LightQChat的公告 2023-04-27 15:01:44 +08:00
Rock Chin c2f6273f70 Merge pull request #442 from oliverkirk-sudo/master
修复异常输出时的类型问题
2023-04-26 17:31:19 +08:00
oliverkirk-sudo 96e401ec7b 修复异常输出时的类型问题 2023-04-26 17:27:33 +08:00
Rock Chin ae8ac65447 feat: 更换使用清华源 (#438) 2023-04-26 11:52:07 +08:00
Rock Chin 2d4f59f36e doc: 强调02 2023-04-26 11:18:07 +08:00
Rock Chin 0e85467e02 Release v2.4.2 2023-04-25 10:27:57 +08:00
Rock Chin eb41cf5481 fix(plugin.py): 兼容性问题 2023-04-25 10:27:07 +08:00
Rock Chin b970a42d07 fix(plugin.py): send_message封装实现的兼容性问题 2023-04-25 10:26:03 +08:00
Rock Chin 8c9d123e1c Merge pull request #433 from RockChinQ/detailed-response-rules
[Feat] 细化到单个群的响应规则
2023-04-25 09:39:56 +08:00
Rock Chin ab2a95e347 Merge branch 'detailed-response-rules' of https://github.com/RockChinQ/QChatGPT into detailed-response-rules 2023-04-25 09:31:56 +08:00
Rock Chin 2184c558a4 feat: 支持配置细化到单个群的响应规则 2023-04-25 09:31:44 +08:00
GitHub Actions 83cb8588fd Update override-all.json 2023-04-25 01:28:56 +00:00
Rock Chin 007e82c533 feat: 配置文件支持 2023-04-25 09:28:31 +08:00
Rock Chin 499f8580a7 doc: 修改wiki格式 2023-04-25 08:45:58 +08:00
Rock Chin a7dc3c5dab Release v2.4.1 2023-04-25 00:01:40 +08:00
Rock Chin d01d3a3c53 perf: 启动时提示使用的QQ号 2023-04-24 23:57:57 +08:00
Rock Chin 580e062dbf feat: 上报使用量时带上msg_source_adapter 2023-04-24 23:51:00 +08:00
Rock Chin c8cee8410c doc: 完善格式 2023-04-24 20:04:33 +08:00
Rock Chin 6bf331c2e3 doc: 完善wiki 2023-04-24 19:53:20 +08:00
Rock Chin 4c4930737c chore: issue模板新增登录框架字段 2023-04-24 19:28:00 +08:00
Rock Chin 9de01e9525 Release v2.4.0 2023-04-24 16:09:46 +08:00
Rock Chin c6a16f5974 Merge pull request #427 from RockChinQ/nakuru-support
[Feat] 支持通过nakuru-project框架连接go-cqhttp
2023-04-24 16:07:12 +08:00
Rock Chin 253ef44d17 chore: 公告 2023-04-24 16:05:47 +08:00
Rock Chin 15a1f00b73 doc(README.md): 添加go-cqhttp公告 2023-04-24 16:04:25 +08:00
Rock Chin b5fa2ea8b8 feat(main.py): 添加nakuru-project-idk的依赖更新项 2023-04-24 16:01:43 +08:00
Rock Chin 449e024771 doc: 添加针对老用户的说明 2023-04-24 15:59:07 +08:00
Rock Chin 1bee7a146b feat: 支持语音组件 2023-04-24 15:55:21 +08:00
Rock Chin 270a632789 doc: 修改标号 2023-04-24 15:48:28 +08:00
Rock Chin 418bb05b4c doc: 添加go-cqhttp配置说明 2023-04-24 15:46:58 +08:00
Rock Chin 052b834151 doc: 完善config-template.py的说明 2023-04-24 15:46:26 +08:00
Rock Chin 58ee204a75 doc: wiki添加go-cqhttp配置步骤 2023-04-24 15:41:28 +08:00
Rock Chin 0a02ee8c04 feat: 启动时添加nakuru的提示检查 2023-04-24 15:04:07 +08:00
Rock Chin 950ef4a181 doc: 更新README.md 2023-04-24 14:57:28 +08:00
Rock Chin 7b7cdd8adb perf: 在日志文件包含输出文件路径 2023-04-24 13:52:22 +08:00
Rock Chin 471768e760 feat: 支持发送转发消息 2023-04-24 12:46:33 +08:00
Rock Chin c7517d31a4 chore: 更换使用nakuru-project-idk包 2023-04-24 11:37:01 +08:00
Rock Chin 7d10d0398e fix: nakuru热重载失败 2023-04-24 11:21:51 +08:00
Rock Chin a2bc25c08b feat: 支持引用原消息回复 2023-04-24 10:57:43 +08:00
Rock Chin 3cb49fe2d8 feat: 支持检测群内禁言 2023-04-24 10:34:51 +08:00
Rock Chin 5b96ac122f feat: 适配nakuru基本功能 2023-04-23 23:40:08 +08:00
Rock Chin 612033f478 feat: nakuru适配器基础模型 2023-04-23 15:58:37 +08:00
GitHub Actions 48ee940d8e Update override-all.json 2023-04-23 01:32:36 +00:00
Rock Chin e74df0b37d chore: 添加nakuru相关配置; 使用nakuru-project-test临时包 2023-04-23 09:32:01 +08:00
GitHub Actions 640afdc49c Update override-all.json 2023-04-22 13:51:02 +00:00
Rock Chin 6b39df5b9b chore: 删除NoneBot2相关配置 2023-04-22 21:50:41 +08:00
Rock Chin e7e698765e fix(plugin.py): 缺少的换行符 2023-04-22 17:40:41 +08:00
Rock Chin 43fea13dab Merge pull request #418 from RockChinQ/im-impl-decoupling
[Refactor] 新增抽象层以解耦消息来源(MessageSource)组件
2023-04-21 18:10:42 +08:00
GitHub Actions bc899e5bd0 Update override-all.json 2023-04-21 09:52:31 +00:00
Rock Chin 160086feb9 refactor: 完成MessageSource适配器解耦 2023-04-21 17:51:58 +08:00
Rock Chin 016391c976 refactor: 不再向QQBotManager中传递config中可读的参数 2023-04-21 17:15:32 +08:00
Rock Chin 91746448a3 feat: 消息源适配器模型及YiriMirai的适配器 2023-04-21 16:36:59 +08:00
Rock Chin 5cb0543237 doc(README.md): 更新wiki链接 2023-04-20 20:50:00 +08:00
Rock Chin fac29a24a8 doc(README.md): social.png更改成圆角 2023-04-20 10:54:06 +08:00
Rock Chin 4d3a2a21d0 Update README_en.md 2023-04-20 00:22:05 +08:00
Rock Chin 6d4f88041c Update README.md 2023-04-20 00:21:37 +08:00
Rock Chin 18587d3690 doc(README.md): 修改social图格式 2023-04-20 00:15:11 +08:00
Rock Chin 423090dccd doc(README.md): 更改使用social图 2023-04-20 00:13:11 +08:00
Rock Chin 78e88baab3 doc(README.md): 优化LOGO图格式 2023-04-20 00:08:00 +08:00
Rock Chin 6a276767b3 doc(README.md): 添加LOGO 2023-04-20 00:06:52 +08:00
Rock Chin 2cb26c7c70 doc: 添加LOGO文件 2023-04-20 00:04:01 +08:00
Rock Chin ff66c88060 doc(README.md): 优化图片格式 2023-04-17 10:18:23 +08:00
Rock Chin 611e82b8f9 doc(README.md): 添加使用截图 2023-04-17 10:15:50 +08:00
Rock Chin 59bdee7137 feat: 添加IM框架模型 2023-04-15 23:38:52 +08:00
Rock Chin e8dbd426ae Release v2.3.9 2023-04-15 17:36:59 +08:00
Rock Chin 40d6e809a0 Merge pull request #417 from RockChinQ/354-feature-single-concurrency
[Feat] 支持设置单会话内同时仅处理一条消息
2023-04-15 17:35:36 +08:00
GitHub Actions 236c540d18 Update override-all.json 2023-04-15 09:34:16 +00:00
Rock Chin d6ca059f6c feat: 支持设置单会话内同时仅处理一条消息 2023-04-15 17:33:57 +08:00
Rock Chin 52c06a60ca fix: 公告功能bug 2023-04-15 16:54:50 +08:00
Rock Chin 6353644ec3 test: 测试公告 2023-04-15 16:49:11 +08:00
Rock Chin 20df9ded3d Merge pull request #416 from RockChinQ/413-feature-json-format-anouns
[Feat] 支持JSON格式的公告
2023-04-15 16:47:03 +08:00
Rock Chin 7569b18a4c feat: 支持JSON格式的公告 2023-04-15 16:45:26 +08:00
Rock Chin b9da4f4951 Merge pull request #415 from RockChinQ/413-feature-json-format-anouns
[Feat] 新增`announcement.json`文件
2023-04-15 16:33:03 +08:00
Rock Chin 89b9e29257 Update pull_request_template.md 2023-04-15 16:25:24 +08:00
Rock Chin d605de9de4 feat: 添加公告模板及公告发布脚本 2023-04-15 09:38:46 +08:00
Rock Chin d46c94d7c3 Release v2.3.8 2023-04-14 23:47:00 +08:00
Rock Chin 2db9c00530 Merge pull request #414 from RockChinQ/detailed-rate-limit
[Feat] 速度限制支持细化到单个人或群
2023-04-14 19:46:24 +08:00
GitHub Actions 66d8d159f9 Update override-all.json 2023-04-14 11:44:26 +00:00
Rock Chin 9fa1446284 feat: 支持细化到个人和群的限速 2023-04-14 19:44:03 +08:00
Rock Chin b3e4cb48c7 Merge pull request #412 from RockChinQ/349-bugfix-auto-deps-solving-failure
[Fix] 循环依赖导致的依赖自动解决失败
2023-04-14 18:44:40 +08:00
Rock Chin 0bca7b2247 fix: 循环引用导致的依赖自动解决失败 2023-04-14 18:42:09 +08:00
Rock Chin 7812e03c9d chore: 删除requirements.txt中对websockets的版本要求以防冲突 2023-04-14 18:27:44 +08:00
Rock Chin 7a852ae5af Merge pull request #410 from 2675hujilo/tips
删除tips-custom-template.py中无用字段
2023-04-14 17:43:30 +08:00
26751 706d9e61c1 删除tips-custom-template.py中无用字段 2023-04-14 02:00:45 +08:00
Rock Chin 8f0ed4ff4b Merge branch 'master' of https://github.com/RockChinQ/QChatGPT 2023-04-12 15:28:59 +08:00
Rock Chin 3415b6f121 doc: 添加lieyanqzu/WeatherPlugin 2023-04-12 15:28:56 +08:00
Rock Chin 256ba6fb86 Merge pull request #406 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.4
chore(deps): update openai requirement from ~=0.27.2 to ~=0.27.4
2023-04-10 18:31:39 +08:00
dependabot[bot] d30b2b9afe chore(deps): update openai requirement from ~=0.27.2 to ~=0.27.4
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.2...v0.27.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-10 09:03:02 +00:00
Rock Chin be943ca1fc doc: 链接文档 2023-04-08 19:39:42 +08:00
Rock Chin 1ddab2a97a doc: README.md in English 2023-04-08 19:32:31 +08:00
Rock Chin e15fd4695c Merge branch 'master' of https://github.com/RockChinQ/QChatGPT 2023-04-08 18:26:10 +08:00
Rock Chin ffa4b1b4a1 fix(modelmgr): 使用异步请求时的异常类型丢失 2023-04-08 18:26:08 +08:00
Rock Chin f8eee3a2a6 Merge pull request #399 from RockChinQ/optional-config-override
[Feat] override.json可选应用
2023-04-08 16:26:56 +08:00
Rock Chin eeee7a8343 feat: 仅在提供命令行参数时应用override.json的内容 2023-04-08 16:21:40 +08:00
Rock Chin 8447b73fcb doc(README.md): 删除ChatAPI2D插件 2023-04-08 16:15:11 +08:00
Rock Chin 2863945d5f feat(config-template): 更改为常量表示超时时间 2023-04-08 15:36:35 +08:00
Rock Chin cb1f8ca6f7 doc(README.md): 添加wenyinos/ChatAPI2D插件 2023-04-08 00:33:47 +08:00
Rock Chin 1d9964bcb1 Release v2.3.7 2023-04-08 00:21:21 +08:00
GitHub Actions Bot 15cb8016d3 Update cmdpriv-template.json 2023-04-07 16:20:13 +00:00
Rock Chin 895cc0a2c5 ci: test 2023-04-08 00:19:37 +08:00
Rock Chin 20bf349e4e ci: cmdpriv模板脚本 2023-04-08 00:18:00 +08:00
Rock Chin e297763da1 fix: !cfg指令失效 2023-04-08 00:13:19 +08:00
Rock Chin e471970654 ci: test 2023-04-07 20:32:51 +08:00
Rock Chin 12faaaced8 ci: 仅在wiki文件更新时提交 2023-04-07 20:31:33 +08:00
Rock Chin 083cbc55cc Release v2.3.6 2023-04-07 17:15:17 +08:00
Rock Chin 8aa7a3273d Merge pull request #390 from RockChinQ/customizable-tips
[Feat] 支持自定义提示消息
2023-04-07 17:13:28 +08:00
Rock Chin 255e2c4385 doc: 添加自定义提示消息的说明 2023-04-07 17:12:33 +08:00
Rock Chin 9856306870 feat: 修改文件生成顺序 2023-04-07 17:10:44 +08:00
GitHub Actions 527ab8b8a7 Update override-all.json 2023-04-07 09:08:21 +00:00
Rock Chin f8e19ba9b3 feat: 删除config-template.py中多余的属性 2023-04-07 17:07:43 +08:00
GitHub Actions 7649dbfbbc Update override-all.json 2023-04-07 09:00:40 +00:00
Rock Chin 81e734644d feat: 删除config-template.py中的help_message模板 2023-04-07 17:00:16 +08:00
Rock Chin ae55cf5b1e feat: 适配help指令 2023-04-07 16:59:51 +08:00
Rock Chin af539546ef Merge pull request #356 from 2675hujilo/tips
[Feat] 支持自定义提示消息
2023-04-07 16:43:58 +08:00
Rock Chin 0031ce57d0 Merge branch 'customizable-tips' into tips 2023-04-07 16:40:26 +08:00
Rock Chin 2f48a2ce57 Merge branch 'customizable-tips' of https://github.com/RockChinQ/QChatGPT into customizable-tips 2023-04-07 16:39:48 +08:00
Rock Chin 6068ab7100 feat: 修改help_message为主线的内容 2023-04-07 16:39:25 +08:00
GitHub Actions 29a7dccef4 Update override-all.json 2023-04-07 08:34:23 +00:00
Rock Chin e2073da86e Merge branch '2675hujilo-tips' into customizable-tips 2023-04-07 16:32:32 +08:00
2675hujilo ae079526f7 删除tips-customs-template.py中不必要注释 2023-04-07 16:29:09 +08:00
26751 947bae8e26 删除tips-customs-template.py中不必要注释
Signed-off-by: 26751 <2675174581@qq.com>
2023-04-07 16:22:22 +08:00
Rock Chin a68e29dff6 feat: tips模块完整性检查 2023-04-07 16:02:22 +08:00
Rock Chin a588d7f960 feat: 热重载加上tips模块 2023-04-07 13:28:07 +08:00
Rock Chin 66224e5a32 fix: 热重载后未检查配置文件存在性 2023-04-07 13:25:57 +08:00
Rock Chin 07abad6a14 feat: 将tips的值统一为str类型 2023-04-07 13:23:58 +08:00
Rock Chin 83d02aaaac chore: 修改配置文件名称 2023-04-07 13:20:57 +08:00
Rock Chin 5a27ac165e Merge branch 'master' of https://github.com/RockChinQ/QChatGPT 2023-04-06 21:37:56 +08:00
Rock Chin bd9a523233 Release v2.3.5 2023-04-06 21:37:51 +08:00
Rock Chin 43959b158f Merge pull request #385 from RockChinQ/impl-337-bugfix-version-ignorance
[Feat] 更新逻辑优化
2023-04-06 21:36:53 +08:00
Rock Chin d81b457bba feat: 更新完成后不展示更新前版本的更新日志 (#340) 2023-04-06 21:34:30 +08:00
Rock Chin b40d639785 feat: 忽略第四位版本号 2023-04-06 21:31:56 +08:00
Rock Chin 0a8d8f4f66 Merge pull request #381 from RockChinQ/impl-339-redundance-comp-check
[Chore] 删除冗余的兼容性检查判断
2023-04-06 21:03:33 +08:00
Rock Chin d16cb25cde chore: 删除冗余的兼容性检查判断 2023-04-06 20:34:56 +08:00
Rock Chin 7aef1758e0 ci: test 2023-04-06 18:41:21 +08:00
Rock Chin 9758756fdd ci: 错误的路径 2023-04-06 18:39:21 +08:00
Rock Chin 13ef35f96f fix: 热重载后!draw无法使用的问题 2023-04-06 18:37:07 +08:00
Rock Chin 6b8c1209b7 chore: 整理根目录文件 2023-04-06 17:23:30 +08:00
Rock Chin 7184f3053a doc: README.md添加社区群说明 2023-04-06 15:55:48 +08:00
Rock Chin b83eac10e6 doc: 完善wiki 2023-04-06 15:20:08 +08:00
Rock Chin cb42eaef69 test: Home.md 2023-04-06 15:18:35 +08:00
Rock Chin 0dfd636a7e ci: 工作流 2023-04-06 15:18:02 +08:00
Rock Chin 21ff0fd258 test: 测试wiki同步工作流 2023-04-06 15:13:40 +08:00
Rock Chin c2eaeb2c72 chore: wiki同步工作流 2023-04-06 15:12:12 +08:00
Rock Chin 2a414a4bea chore: 提交wiki文件到res/wiki 2023-04-06 15:07:25 +08:00
Rock Chin fc0c38c8af chore: 删除子模块 2023-04-06 10:13:34 +08:00
Rock Chin 595e6c8a0c chore: 删除子模块 2023-04-06 10:13:08 +08:00
Rock Chin ced16fd221 chore: 移动docker部署教程 2023-04-06 10:10:09 +08:00
Rock Chin 0817c3f148 chore: 将工作流脚本移动到res/scripts 2023-04-06 10:08:15 +08:00
Rock Chin fb40af81ac doc: 完善文档 2023-04-06 09:44:07 +08:00
Rock Chin 1c5ad05e89 typo: plugin命令的提示错字 2023-04-06 09:29:45 +08:00
Rock Chin 86bef566c4 Release v2.3.4 2023-04-05 17:13:05 +08:00
Rock Chin 0983ccb61e doc: 添加模型切换器插件 2023-04-05 16:59:06 +08:00
Rock Chin a1d9f469c0 doc: 添加模型切换器插件 2023-04-05 16:58:15 +08:00
Rock Chin 952124f783 feat: 禁用的插件仍进行初始化 2023-04-05 16:50:35 +08:00
GitHub Actions 6be12e8ace Update override-all.json 2023-04-05 07:48:46 +00:00
Rock Chin 0799f380e1 feat: 更改默认help_message 2023-04-05 15:48:21 +08:00
Rock Chin f65270ee7e feat: 启动时输出mah相关配置项 2023-04-05 15:46:49 +08:00
Rock Chin 414910719c Release v2.3.3 2023-04-05 09:57:21 +08:00
Rock Chin 10a1e8faa6 fix: 回复内容不完整问题 (#208) 2023-04-05 09:56:27 +08:00
Rock Chin 4eea21927e doc: 补充手动部署中缺失的requests库 (#375) 2023-04-04 16:49:59 +08:00
Rock Chin 48c7f659f9 Release v2.3.2 2023-04-04 03:22:19 +00:00
Rock Chin b33333f4aa Merge pull request #372 from RockChinQ/363-bug-helpmessage-creditapi
[Fix] help_message问题、额度检测接口问题
2023-04-04 11:20:34 +08:00
Rock Chin 9edb32b081 feat: usage命令不再显示额度 2023-04-04 03:15:07 +00:00
Rock Chin c9b25fe806 doc: cmds指令的说明 2023-04-03 14:55:01 +00:00
GitHub Actions Bot b6ee3939be Update cmdpriv-template.json 2023-04-03 14:41:25 +00:00
Rock Chin e5485cddd0 feat: 更改使用!cmd指令查看指令列表 2023-04-03 14:40:27 +00:00
Rock Chin ac81597236 feat: 插件更新异常处理 2023-04-03 14:09:30 +00:00
Rock Chin 58d991df0a Merge pull request #368 from zyckk4/docstring-improvements
[Chore] 统一docstring格式
2023-04-03 22:02:11 +08:00
Rock Chin 3f8e380da4 Merge pull request #369 from zyckk4/fix-type-hint
[Fix] 修复一处类型注解的错误
2023-04-03 13:39:56 +08:00
zyckk4 ae831a2654 [Fix] 修复一处类型注解的错误 2023-04-03 10:13:20 +08:00
zyckk4 ae72cf2283 chore: 统一docstring格式 2023-04-03 00:19:28 +08:00
Rock Chin 8164f4b506 Release v2.3.1 2023-04-02 16:32:52 +08:00
Rock Chin 9617be0ca4 fix: 未指定utf-8保存已输出的公告 2023-04-02 16:30:42 +08:00
Rock Chin f079d7b9fa fix: Windows上无法读取和应用命令权限配置的问题 2023-04-02 16:24:30 +08:00
Rock Chin 00afda452f Merge pull request #365 from zyckk4/style-improvements
去除行尾空格
2023-04-02 16:04:52 +08:00
zyckk4 70386abadd 去除行尾空格 2023-04-02 14:43:34 +08:00
26751 5865ac017c 增加tips_custom.py提示
Signed-off-by: 26751 <2675174581@qq.com>
2023-04-02 13:46:15 +08:00
26751 4061a92f8e 删除override-all.json中无效的字段
Signed-off-by: 26751 <2675174581@qq.com>
2023-04-02 13:36:51 +08:00
2675hujilo d37c31b31c Update tips_custom_template.py 2023-04-01 18:43:03 +08:00
2675hujilo 973ef0078f Delete tips_custom.py 2023-04-01 18:36:33 +08:00
26751 48dcd257da Signed-off-by: 26751 <2675174581@qq.com> 2023-04-01 18:33:37 +08:00
26751 da03911610 Signed-off-by: 26751 <2675174581@qq.com> 2023-04-01 16:39:02 +08:00
Rock Chin aba9d945b5 doc: 收起功能概述 2023-04-01 09:59:33 +08:00
26751 b6f7f3b73f Signed-off-by: 26751 <2675174581@qq.com> 2023-04-01 02:35:27 +08:00
26751 2050d20ea7 Signed-off-by: 26751 <2675174581@qq.com> 2023-04-01 02:23:40 +08:00
26751 ac1fb4a63a 修改自定义提示语 2023-04-01 01:02:59 +08:00
Rock Chin ced38490e1 chore: 兼容性问题公告 2023-03-31 21:37:35 +08:00
Rock Chin ad28b69198 doc: 添加ChatPoeBot插件链接 (#352) 2023-03-31 21:31:40 +08:00
crosscc 8c67d3c58f Create build_docker_image.yml
利用github action 自动构建docker镜像:
## 1、
`workflow_dispatch:` 是需要作者在action手动点击进行构建

```
  release:
    types: [published]
```
这个是发布release的时候自动构建镜像。根据作者需求启用或者删除注释掉

## 2、
`tag: latest,${{ steps.get_version.outputs.VERSION }}`是可以镜像打标为latest和release发布的版本号

## 3、
docker hub userid 在setting创建secrets, name=DOCKER_USERNAME   value=dockerid
docker hub password,在setting创建secrets, name=DOCKER_PASSWORD   value=dockerpassword

这样作者就不用在自己机器上构建docker镜像,利用action 自动完成全平台镜像 速度也快。
2023-03-31 16:22:20 +08:00
Rock Chin 7171817de8 Release v2.3.0 2023-03-31 07:42:06 +00:00
GitHub Actions Bot 73f9d674e1 Update cmdpriv-template.json 2023-03-31 07:40:07 +00:00
Rock Chin 5e046399f8 test: 删除测试文件 2023-03-31 07:39:35 +00:00
GitHub Actions Bot 4966cd9ac7 Update cmdpriv-template.json 2023-03-31 07:35:48 +00:00
Rock Chin da936ecfe3 test: ci 2023-03-31 07:35:11 +00:00
Rock Chin 89e10d43de ci: 解决所有依赖 2023-03-31 07:34:45 +00:00
Rock Chin 3bf289af69 test: 测试 2023-03-31 07:29:23 +00:00
Rock Chin c7c9a6c5ca ci: 运行前完善配置文件 2023-03-31 07:28:33 +00:00
Rock Chin aee8446a23 test: 测试工作流 2023-03-31 07:25:53 +00:00
Rock Chin 2bb4f1fbb8 ci: 工作流 2023-03-31 07:25:27 +00:00
Rock Chin 6e7b0ee4ff test: 测试工作流 2023-03-31 07:24:17 +00:00
Rock Chin 204f5b9a54 ci: 工作流语法错误 2023-03-31 07:23:35 +00:00
Rock Chin 8c41e3506f test: 测试工作流 2023-03-31 07:22:33 +00:00
Rock Chin c2c33e45b8 ci: 更新工作流文件 2023-03-31 07:21:03 +00:00
Rock Chin 1acaf4e58b Merge pull request #336 from RockChinQ/cmds-permission-ctrl
[Refactor&Feat] 命令节点权限控制
2023-03-31 15:18:44 +08:00
Rock Chin eca80d5a4c ci: 添加cmdpriv-template.json的自动化生成脚本 2023-03-31 07:18:08 +00:00
Rock Chin f538957be9 doc: 更新wiki 2023-03-31 07:06:42 +00:00
Rock Chin 82a839a60a doc: 完善命令权限功能说明 2023-03-31 07:06:18 +00:00
Rock Chin df494da9e4 feat: 支持命令限权 2023-03-31 06:49:13 +00:00
Rock Chin 1ea53f7f04 Merge pull request #342 from q123458384/patch-1
Update docker_deploy.md
2023-03-30 22:30:34 +08:00
Rock Chin ac6d695f6d doc: 完善主程序容器启动指令的挂载项 2023-03-30 21:26:10 +08:00
Rock Chin 73dccb21f5 feat: 添加指令权限配置文件 2023-03-30 11:29:04 +00:00
Rock Chin 4221102ad5 chore: 删除过时的命令架构文件 2023-03-30 11:12:27 +00:00
Rock Chin b100f12e7f refactor: 完成所有指令 2023-03-30 11:11:39 +00:00
Rock Chin 2069ba6836 refactor: system类命令 2023-03-30 03:38:33 +00:00
crosscc ea57976808 Update docker_deploy.md
2.1中 `network host` 就是开放容器内的所有端口,和 `-p 端口:端口` 不共用
2.1中  `-v ./qq/xxx` 在群晖中不能用,改成了`${PWD}/qq/xxx`
3 中 容器名和上面的重复了,映射整个目录会无法运行,改成只映射 config.py

以上是我docker部署中遇到的问题及修改
2023-03-29 16:44:16 +08:00
Rock Chin 4055d3542b refactor: 完成会话管理相关指令 2023-03-28 13:47:45 +00:00
Rock Chin 0b0271a1f4 refactor: 更改使用装饰器注册命令 2023-03-28 12:53:46 +00:00
Rock Chin e03585ad4d feat: 扁平化储存命令 2023-03-28 12:18:19 +00:00
Rock Chin 11a385791e doc: 添加贡献相关说明 2023-03-28 12:52:37 +08:00
Rock Chin e228225178 refactor: 指令注册架构 2023-03-28 03:12:19 +00:00
Rock Chin 1c96d971e1 Update bug-report.yml 2023-03-27 21:22:56 +08:00
Rock Chin b799de7995 refactor: 迁移旧的处理模块 2023-03-27 13:09:40 +00:00
Rock Chin b01d246555 doc: 删除安装器使用警告 2023-03-27 18:52:40 +08:00
Rock Chin 9363b073cf Merge pull request #334 from maimierjiafude/patch-1
[Fix] 修改模块无法找到的问题
2023-03-27 18:51:05 +08:00
maimierjiafude 12ca04ac6f 修改模块无法找到的问题 2023-03-27 18:45:29 +08:00
Rock Chin 51737c28bd Delete 需求建议.md 2023-03-27 11:31:05 +08:00
Rock Chin 50d5ec224a Create feature-request.yml 2023-03-27 11:30:40 +08:00
Rock Chin 95a7397d14 Update bug-report.yml 2023-03-27 11:23:10 +08:00
Rock Chin aedac6d22c Create bug-report.yml 2023-03-27 11:21:45 +08:00
Rock Chin d522975ecc Delete 漏洞反馈.yml 2023-03-27 11:17:14 +08:00
Rock Chin 68fda8d7f3 Update 漏洞反馈.yml 2023-03-27 11:16:48 +08:00
Rock Chin b0cfec9913 Update 漏洞反馈.yml 2023-03-27 11:11:07 +08:00
Rock Chin ba8eba1581 Update 漏洞反馈.yml 2023-03-27 11:10:41 +08:00
Rock Chin f9eaed41c1 Update 漏洞反馈.yml 2023-03-27 11:07:16 +08:00
Rock Chin 1202a62df7 Update 漏洞反馈.yml 2023-03-27 11:06:11 +08:00
Rock Chin 8c1f7796f6 Update 漏洞反馈.yml 2023-03-27 11:02:18 +08:00
Rock Chin 42aee35789 Update 漏洞反馈.yml 2023-03-27 11:01:47 +08:00
Rock Chin b628849caa Update 漏洞反馈.yml 2023-03-27 11:00:21 +08:00
Rock Chin 031f08b0d4 Rename 漏洞反馈.md to 漏洞反馈.yml 2023-03-27 10:57:40 +08:00
Rock Chin fab6f9b93f Update 漏洞反馈.md 2023-03-27 10:57:00 +08:00
GitHub Actions 564c5d937d Update override-all.json 2023-03-26 15:45:06 +00:00
Rock Chin 2d3bb01487 debug: 测试完毕 2023-03-26 23:44:49 +08:00
GitHub Actions 607ea2d293 Update override-all.json 2023-03-26 15:43:54 +00:00
Rock Chin d817b53780 debug: 测试工作流 2023-03-26 23:43:34 +08:00
Rock Chin e8a2cbe06a Rename update override-all.json to update-override-all.yml 2023-03-26 23:42:42 +08:00
Rock Chin d2b0577752 Update update override-all.json 2023-03-26 23:41:15 +08:00
Rock Chin b4edd5cbad Update update override-all.json 2023-03-26 23:38:38 +08:00
Rock Chin 348477747e debug: 测试override-all.json工作流 2023-03-26 23:35:44 +08:00
Rock Chin bb7ee174ea Create update override-all.json 2023-03-26 23:34:50 +08:00
Rock Chin ab5add14ef chore: 完善override-all.json 2023-03-26 15:27:17 +00:00
Rock Chin 44f4820cee Merge pull request #332 from RockChinQ/reverse-proxy
[Feat] 支持反向代理
2023-03-26 22:51:06 +08:00
Rock Chin 8f1609b944 doc: 完善反代地址说明 2023-03-26 14:50:03 +00:00
Rock Chin 66b5b75631 feat: 支持反向代理 2023-03-26 13:50:43 +00:00
Rock Chin 17e293afe8 Merge pull request #325 from RockChinQ/fix-289-full-default-compatibility
[Feat] 完善情景预设相关内容
2023-03-26 21:40:36 +08:00
Rock Chin 1cf35f59fd Merge branch 'master' into fix-289-full-default-compatibility 2023-03-26 21:40:21 +08:00
Rock Chin bb4b897934 feat(dprompt.py): 解耦完成 2023-03-26 13:28:26 +00:00
Rock Chin 0eaf1af2e3 doc: 添加Python环境冲突警告 2023-03-26 15:25:21 +08:00
Rock Chin f70c12540b Merge pull request #327 from mikumifa/master
Dockerfile部署
2023-03-25 23:12:52 +08:00
Rock Chin 479fe73c24 doc: 在README.md链接docker教程 2023-03-25 23:12:26 +08:00
Rock Chin f6cad85476 feat: 使用normal作为情景预设默认模式的名称 2023-03-24 20:02:50 +08:00
mikumifa 888197e6ce Dockerfile部署 2023-03-24 19:58:27 +08:00
Rock Chin e634305759 doc: 完善full_scenario的说明 2023-03-24 11:30:53 +00:00
Rock Chin fe054211f4 chore: 代码格式优化 2023-03-23 23:44:10 +08:00
Rock Chin f102a29ea0 Merge pull request #323 from RockChinQ/multi-threads-control
[Feat] 基于线程池的多线程控制方案
2023-03-23 22:56:51 +08:00
Rock Chin 2b8bd45bcd Merge branch 'master' into multi-threads-control 2023-03-23 21:43:41 +08:00
Rock Chin 7f730c4be0 Merge pull request #252 from LINSTCL/multi-threads-control
添加线程控制类,修改main结构,修改启动流程
2023-03-23 21:35:22 +08:00
Rock Chin b6e31cac23 fix: 重载时重复调用load_config() 2023-03-23 21:29:51 +08:00
Rock Chin 9fe4f218d5 chore: config-template格式 2023-03-23 21:09:40 +08:00
LINSTCL cc38cc2676 修复bug 2023-03-23 16:43:41 +08:00
LINSTCL f56c6876d1 暂时解决reload后的config无法加载问题 2023-03-23 16:42:15 +08:00
LINSTCL 196e424c88 添加说明 2023-03-23 16:37:01 +08:00
Rock Chin 9270dc2c52 Release v2.2.5 2023-03-20 14:02:38 +00:00
Rock Chin 14aec251b4 Merge pull request #315 from RockChinQ/impl-312
[Feat] 访问GitHub API时使用openai_config中设置的代理地址
2023-03-20 21:49:33 +08:00
Rock Chin d2a7a57245 feat: 为GitHub API的访问使用代理 (#312) 2023-03-20 13:40:23 +00:00
Rock Chin 1964fc76c8 doc: 完善wiki指引 2023-03-20 13:25:02 +00:00
Rock Chin b8d4b490ce doc: 添加部署说明 2023-03-20 13:12:25 +00:00
Rock Chin 76891e4855 doc: 添加指令说明指引 2023-03-20 13:09:05 +00:00
Rock Chin 3d868b3a39 Merge pull request #308 from RockChinQ/plugin-ctrl-cmd
[Feat] 解耦指令处理、完善插件管理指令
2023-03-20 21:04:06 +08:00
Rock Chin 7b56bcf7a9 feat: 添加插件启用禁用指令 2023-03-20 13:02:30 +00:00
Rock Chin f96ae56bce feat: 支持指令删除插件 (#286) 2023-03-20 12:50:25 +00:00
Rock Chin d52108f4e1 doc: 完善README.md 2023-03-20 12:49:18 +00:00
Rock Chin 5f07b7ad1f refactor: 完成所有指令重构 2023-03-20 12:06:02 +00:00
Rock Chin cda10cf1a6 Update 漏洞反馈.md 2023-03-20 19:17:53 +08:00
Rock Chin d226b8ebc5 doc: 完善文档 (#310) 2023-03-20 14:46:39 +08:00
Rock Chin d08794579c feat: 现有指令占位 2023-03-19 14:33:01 +00:00
Rock Chin 7450494741 Update pull_request_template.md 2023-03-19 20:33:23 +08:00
Rock Chin 36dca7ae2f feat: 添加指令抽象类 2023-03-19 12:27:21 +00:00
Rock Chin 5dae777e79 doc: 添加wiki为submodule 2023-03-19 09:43:45 +00:00
Rock Chin e518d172d7 Merge pull request #304 from RockChinQ/bd-check-exception
[Perf] 百度云审核的异常处理
2023-03-19 17:13:37 +08:00
Rock Chin af29277acd feat: 长消息检查函数不再检查敏感词 2023-03-19 09:06:32 +00:00
Rock Chin 79bfa0792d feat: 删除print调试信息 2023-03-19 08:45:54 +00:00
Rock Chin cf23c5d31c Release v2.2.4 2023-03-19 08:38:07 +00:00
Rock Chin 84418a296b doc: 完善pr模板 2023-03-19 08:37:23 +00:00
Rock Chin 5f83cc6bb7 Merge pull request #300 from RockChinQ/token-process
[Perf] Tokens相关处理逻辑优化
2023-03-19 16:35:25 +08:00
Rock Chin cde168c93c doc: full_scenario的编写教程 (#301) 2023-03-19 08:32:34 +00:00
Rock Chin fed24c0748 doc: 添加chordfish-k/QChartGPT_Emoticon_Plugin 2023-03-19 13:35:20 +08:00
Rock Chin b45d11b3c3 Update pull_request_template.md 2023-03-19 11:28:38 +08:00
Rock Chin 84d9af69bb Update pull_request_template.md 2023-03-19 11:28:17 +08:00
Rock Chin 684d356646 Update pull_request_template.md 2023-03-19 11:17:07 +08:00
Rock Chin 975300c9fc Create pull_request_template.md 2023-03-19 11:15:45 +08:00
Rock Chin ca349e33fc feat: 实现新的前文剪切模式 2023-03-18 15:57:28 +00:00
Rock Chin ccf62fe95c doc: 致谢GPT-4内测提供者 2023-03-18 22:28:06 +08:00
Rock Chin d056cb6769 feat: 数据库接口支持 2023-03-18 12:57:36 +00:00
Rock Chin b0016eebf9 feat: 添加override-all.json 2023-03-18 20:44:14 +08:00
Rock Chin 0490ad9207 test: token计数测试 2023-03-18 11:26:18 +00:00
Rock Chin 4a20ae236b doc: README.md格式错误 2023-03-18 09:15:26 +00:00
Rock Chin 9be1c7fc6f doc: 添加WaitYiYan插件链接 2023-03-18 08:17:51 +00:00
Rock Chin 5621d32b30 doc: GPT-4说明 2023-03-18 04:42:46 +00:00
Rock Chin b7642fe876 feat: 支持GPT-4 API 2023-03-18 04:38:48 +00:00
Rock Chin c842485d33 perf: 尝试安装依赖时的逻辑 2023-03-17 07:49:27 +00:00
Rock Chin 341444ef1c chore: 添加devcontainer配置 2023-03-17 07:39:16 +00:00
Rock Chin 66f5a219d2 feat: 不再提示InvalidRequestError的可能原因 2023-03-16 21:10:10 +08:00
Rock Chin cf678aa345 feat: 修改日志初始化顺序 2023-03-16 20:55:57 +08:00
Rock Chin d1549b3df0 chore: 代码格式优化 2023-03-16 20:22:18 +08:00
Rock Chin 002919fffe doc: 优化README.md格式 2023-03-16 19:38:35 +08:00
Rock Chin 087d097204 feat: 不再默认提供max_tokens 2023-03-16 13:37:48 +08:00
Rock Chin ca4eeda6f0 doc: 添加oliverkirk-sudo的文字转语音插件 2023-03-16 09:08:00 +08:00
Rock Chin 94543a4708 Merge pull request #282 from systemtang/bugfix
[Feat] 修复usage命令的代理问题
2023-03-16 08:53:25 +08:00
Rock Chin d4738dfb46 Release v2.2.3 2023-03-15 22:50:40 +08:00
Rock Chin 3bdf6810aa fix: 消息处理时的错误 2023-03-15 22:47:20 +08:00
systemt f489c2f3b4 修复usage命令的代理问题 2023-03-15 21:04:55 +08:00
Rock Chin a724bfe155 Release v2.2.2 2023-03-15 20:39:10 +08:00
Rock Chin 179a372bfe feat: 更改到process.py处理长消息 2023-03-15 20:33:44 +08:00
Rock Chin 651d765ab0 doc: 添加New Bing说明 2023-03-15 17:33:31 +08:00
Rock Chin 7ddc853f63 chore: 忽略保存的公告 2023-03-15 15:50:14 +08:00
Rock Chin 1bd1bfc725 chore: 删除测试公告 2023-03-15 15:47:24 +08:00
Rock Chin f6ec0fda7a Merge pull request #280 from RockChinQ/announcement
[Feat] 添加公告输出功能
2023-03-15 15:46:58 +08:00
Rock Chin 7be368ae8c feat: 添加公告功能 2023-03-15 15:43:36 +08:00
Rock Chin f67db2617b debug: 测试公告内容1 2023-03-15 15:37:07 +08:00
Rock Chin ed5bf8100f chore: 添加公告内容 2023-03-15 15:22:19 +08:00
Rock Chin 0ef8a1c9ae chore: 为new bing忽略cookies.json 2023-03-15 11:24:45 +08:00
Rock Chin 32460cbf78 doc: 添加GPT-4公告 2023-03-15 11:04:10 +08:00
Rock Chin 6f6c9c222c doc: 添加网页版GPT-4说明 2023-03-15 10:57:29 +08:00
Rock Chin 438d0ed1ea Merge pull request #277 from zyckk4/dev
chore: 去除多余import
2023-03-14 13:11:47 +08:00
zyckk4 3ef1c71cad chore: 去除多余import 2023-03-14 13:03:50 +08:00
Rock Chin aaadf6b8ba doc: 部署方式依赖项指令 2023-03-14 10:57:02 +08:00
Rock Chin 6af614f319 doc: 整理致谢列表 2023-03-14 10:54:46 +08:00
Rock Chin c75dbd67df doc: 整理致谢列表 2023-03-14 10:53:32 +08:00
Rock Chin dc3d186e2a Merge pull request #274 from RockChinQ/dependabot/pip/openai-approx-eq-0.27.2
chore(deps): update openai requirement from ~=0.27.0 to ~=0.27.2
2023-03-13 17:48:10 +08:00
dependabot[bot] 44550feddd chore(deps): update openai requirement from ~=0.27.0 to ~=0.27.2
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Commits](https://github.com/openai/openai-python/compare/v0.27.0...v0.27.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 09:05:46 +00:00
Rock Chin a0810d5f63 Merge pull request #271 from RockChinQ/config-covering
feat: 支持json格式的配置文件 (#265)
2023-03-13 11:05:11 +08:00
Rock Chin cfc97fb22d feat: 支持json格式的配置文件 (#265) 2023-03-13 10:58:15 +08:00
Rock Chin d67dbe8062 doc: 添加JSON格式情景预设的说明 2023-03-13 10:31:21 +08:00
LINSTCL 3aca987176 暴力修复程序无法退出的bug 2023-03-10 09:35:59 +08:00
LINSTCL e0caeb5dd2 Fix bugs 2023-03-08 16:08:09 +08:00
LINSTCL 77076f3bdd 添加线程控制类,修改main结构,修改启动流程 2023-03-08 15:21:37 +08:00
142 changed files with 8054 additions and 1935 deletions
+64
View File
@@ -0,0 +1,64 @@
name: 漏洞反馈
description: 报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭
title: "[Bug]: "
labels: ["bug?"]
body:
- type: dropdown
attributes:
label: 部署方式
description: "主程序使用的部署方式"
options:
- 手动部署
- 安装器部署
- 一键安装包部署
- Docker部署
validations:
required: true
- type: dropdown
attributes:
label: 登录框架
description: "连接QQ使用的框架"
options:
- Mirai
- go-cqhttp
validations:
required: false
- type: input
attributes:
label: 系统环境
description: 操作系统、系统架构、**主机地理位置**,地理位置最好写清楚,涉及网络问题排查。
placeholder: 例如: CentOS x64 中国大陆、Windows11 美国
validations:
required: true
- type: input
attributes:
label: Python环境
description: 运行程序的Python版本
placeholder: 例如: Python 3.10
validations:
required: true
- type: input
attributes:
label: QChatGPT版本
description: QChatGPT版本号
placeholder: 例如: v2.6.0,可以使用`!version`命令查看
validations:
required: true
- type: textarea
attributes:
label: 异常情况
description: 完整描述异常情况,什么时候发生的、发生了什么,尽可能详细
validations:
required: true
- type: textarea
attributes:
label: 日志信息
description: 请提供完整的 **登录框架 和 QChatGPT控制台**的相关日志信息(若有),不提供日志信息**无法**为您排查问题,请尽可能详细
validations:
required: false
- type: textarea
attributes:
label: 启用的插件
description: 有些情况可能和插件功能有关,建议提供插件启用情况。可以使用`!plugin`命令查看已启用的插件
validations:
required: false
@@ -0,0 +1,21 @@
name: 需求建议
title: "[Feature]: "
labels: ["改进"]
description: "新功能或现有功能优化请使用这个模板;不符合类别的issue将被直接关闭"
body:
- type: dropdown
attributes:
label: 这是一个?
description: 新功能建议还是现有功能优化
options:
- 新功能
- 现有功能优化
validations:
required: true
- type: textarea
attributes:
label: 详细描述
description: 详细描述,越详细越好
validations:
required: true
+24
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
-24
View File
@@ -1,24 +0,0 @@
---
name: 漏洞反馈
about: 报错或漏洞请使用这个模板创建
title: "[BUG]"
labels: 'bug'
assignees: ''
---
请认真按照实际情况填写以下信息!!!!
**运行环境**
- 部署方式:
手动部署/自动部署/Docker部署
- 系统环境:
例如: Centos x64
- Python环境(仅手动部署填写):
例如: Python 3.10.9
**描述漏洞**
什么时候发生的,mirai还是主程序,越详细越好
**完整报错信息**
完整的报错信息
-10
View File
@@ -1,10 +0,0 @@
---
name: 需求建议
about: 软件优化建议请使用这个模板创建
title: "[ENHANCE]"
labels: 'enhancement'
assignees: ''
---
不是需求建议请勿填写此模板!!!!
+1 -1
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"
+25
View File
@@ -0,0 +1,25 @@
## 概述
实现/解决/优化的内容:
### 事务
- [ ] 已阅读仓库[贡献指引](https://github.com/RockChinQ/QChatGPT/blob/master/CONTRIBUTING.md)
- [ ] 已与维护者在issues或其他平台沟通此PR大致内容
## 以下内容可在起草PR后、合并PR前逐步完成
### 功能
- [ ] 已编写完善的配置文件字段说明(若有新增)
- [ ] 已编写面向用户的新功能说明(若有必要)
- [ ] 已测试新功能或更改
### 兼容性
- [ ] 已处理版本兼容性
- [ ] 已处理插件兼容问题
### 风险
可能导致或已知的问题:
+38
View File
@@ -0,0 +1,38 @@
name: Build Docker Image
on:
#防止fork乱用action设置只能手动触发构建
workflow_dispatch:
## 发布release的时候会自动构建
release:
types: [published]
jobs:
publish-docker-image:
runs-on: ubuntu-latest
name: Build image
steps:
- name: Checkout
uses: actions/checkout@v2
- name: judge has env GITHUB_REF # 如果没有GITHUB_REF环境变量,则把github.ref变量赋值给GITHUB_REF
run: |
if [ -z "$GITHUB_REF" ]; then
export GITHUB_REF=${{ github.ref }}
fi
- name: Check GITHUB_REF env
run: echo $GITHUB_REF
- name: Get version
id: get_version
if: (startsWith(env.GITHUB_REF, 'refs/tags/')||startsWith(github.ref, 'refs/tags/')) && startsWith(github.repository, 'RockChinQ/QChatGPT')
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Build # image name: rockchin/qchatgpt:<VERSION>
run: docker build --network=host -t rockchin/qchatgpt:${{ steps.get_version.outputs.VERSION }} -t rockchin/qchatgpt:latest .
- name: Login to Registry
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
- name: Push image
if: (startsWith(env.GITHUB_REF, 'refs/tags/')||startsWith(github.ref, 'refs/tags/')) && startsWith(github.repository, 'RockChinQ/QChatGPT')
run: docker push rockchin/qchatgpt:${{ steps.get_version.outputs.VERSION }}
- name: Push latest image
if: (startsWith(env.GITHUB_REF, 'refs/tags/')||startsWith(github.ref, 'refs/tags/')) && startsWith(github.repository, 'RockChinQ/QChatGPT')
run: docker push rockchin/qchatgpt:latest
+43
View File
@@ -0,0 +1,43 @@
name: Update Wiki
on:
push:
branches:
- master
paths:
- 'res/wiki/**'
jobs:
update-wiki:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Git
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Clone Wiki Repository
uses: actions/checkout@v2
with:
repository: RockChinQ/QChatGPT.wiki
path: wiki
- name: Delete old wiki content
run: |
rm -rf wiki/*
- name: Copy res/wiki content to wiki
run: |
cp -r res/wiki/* wiki/
- name: Check for changes
run: |
cd wiki
if git diff --quiet; then
echo "No changes to commit."
exit 0
fi
- name: Commit and Push Changes
run: |
cd wiki
git add .
git commit -m "Update wiki"
git push
+80
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 }}
@@ -0,0 +1,58 @@
name: Update cmdpriv-template
on:
push:
paths:
- 'pkg/qqbot/cmds/**'
pull_request:
types: [closed]
paths:
- 'pkg/qqbot/cmds/**'
jobs:
update-cmdpriv-template:
if: github.event.pull_request.merged == true || github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.10.13
- name: Install dependencies
run: |
python -m pip install --upgrade yiri-mirai-rc openai>=1.0.0 colorlog func_timeout dulwich Pillow CallingGPT tiktoken
python -m pip install -U openai>=1.0.0
- name: Copy Scripts
run: |
cp res/scripts/generate_cmdpriv_template.py .
- name: Generate Files
run: |
python main.py
- name: Run generate_cmdpriv_template.py
run: python3 generate_cmdpriv_template.py
- name: Check for changes in cmdpriv-template.json
id: check_changes
run: |
if git diff --name-only | grep -q "res/templates/cmdpriv-template.json"; then
echo "::set-output name=changes_detected::true"
else
echo "::set-output name=changes_detected::false"
fi
- name: Commit changes to cmdpriv-template.json
if: steps.check_changes.outputs.changes_detected == 'true'
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "<github-actions@github.com>"
git add res/templates/cmdpriv-template.json
git commit -m "Update cmdpriv-template.json"
git push
+52
View File
@@ -0,0 +1,52 @@
name: Check and Update override_all
on:
push:
paths:
- 'config-template.py'
pull_request:
types:
- closed
branches:
- master
paths:
- 'config-template.py'
jobs:
update-override-all:
name: check and update
if: github.event.pull_request.merged == true || github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- name: Copy Scripts
run: |
cp res/scripts/generate_override_all.py .
- name: Run generate_override_all.py
run: python3 generate_override_all.py
- name: Check for changes in override-all.json
id: check_changes
run: |
git diff --exit-code override-all.json || echo "::set-output name=changes_detected::true"
- name: Commit and push changes
if: steps.check_changes.outputs.changes_detected == 'true'
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "GitHub Actions"
git add override-all.json
git commit -m "Update override-all.json"
git push
+20 -1
View File
@@ -1,4 +1,4 @@
config.py /config.py
.idea/ .idea/
__pycache__/ __pycache__/
database.db database.db
@@ -14,3 +14,22 @@ temp/
current_tag current_tag
scenario/ scenario/
!scenario/default-template.json !scenario/default-template.json
override.json
cookies.json
res/announcement_saved
res/announcement_saved.json
cmdpriv.json
tips.py
.venv
bin/
.vscode
test_*
venv/
hugchat.json
qcapi
claude.json
bard.json
/*yaml
!/docker-compose.yaml
res/instance_id.json
.DS_Store
+7
View File
@@ -17,3 +17,10 @@
- 解决本项目或衍生项目的issues中亟待解决的问题 - 解决本项目或衍生项目的issues中亟待解决的问题
- 阅读并完善本项目文档 - 阅读并完善本项目文档
- 在各个社交媒体撰写本项目教程等 - 在各个社交媒体撰写本项目教程等
### 代码规范
- 代码中的注解`务必`符合Google风格的规范
- 模块顶部的引入代码请遵循`系统模块``第三方库模块``自定义模块`的顺序进行引入
- `不要`直接引入模块的特定属性,而是引入这个模块,再通过`xxx.yyy`的形式使用属性
- 任何作用域的字段`必须`先声明后使用,并在声明处注明类型提示
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.10.13-bullseye
WORKDIR /QChatGPT
COPY . /QChatGPT/
RUN ls
RUN python -m pip install -r requirements.txt && \
python -m pip install -U websockets==10.0 && \
python -m pip install -U httpcore httpx openai
# 生成配置文件
RUN python main.py
CMD [ "python", "main.py" ]
+50 -235
View File
@@ -1,236 +1,51 @@
# QChatGPT🤖
> 2023/3/3 官方接口疑似被墙,可考虑使用网络代理 [#198](https://github.com/RockChinQ/QChatGPT/issues/198)
> 2023/3/3 现已在主线支持官方ChatGPT接口,使用方法查看[#195](https://github.com/RockChinQ/QChatGPT/issues/195)
> 2023/3/2 OpenAI已发布ChatGPT官方接口,我们正在全力接入,预计明日前完成,请查看[此PR](https://github.com/RockChinQ/QChatGPT/pull/194)
> 2023/2/16 现已支持接入ChatGPT网页版,详情请完成部署并查看底部**插件**小节或[此仓库](https://github.com/RockChinQ/revLibs)
- 到[项目Wiki](https://github.com/RockChinQ/QChatGPT/wiki)可了解项目详细信息 <p align="center">
- 由bilibili TheLazy制作的[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP) <img src="https://qchatgpt.rockchin.top/logo.png" alt="QChatGPT" width="180" />
- 交流、答疑群: ~~204785790~~(已满)、~~691226829~~(已满)、656285629 </p>
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
- QQ频道机器人见[QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT) <div align="center">
通过调用OpenAI的ChatGPT等语言模型来实现一个更加智能的QQ机器人 # 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">
- OpenAI GPT-3.5模型(ChatGPT API), 本项目原生支持, 默认使用 <img src="https://img.shields.io/docker/pulls/rockchin/qchatgpt?color=blue" alt="docker pull">
- OpenAI GPT-3模型, 本项目原生支持, 部署完成后前往config.py切换 </a>
- ChatGPT网页版逆向API, 由[插件](https://github.com/RockChinQ/revLibs)接入 ![Wakapi Count](https://wakapi.dev/api/badge/RockChinQ/interval:any/project:QChatGPT)
<a href="https://codecov.io/gh/RockChinQ/QChatGPT" >
### 故事续写 <img src="https://codecov.io/gh/RockChinQ/QChatGPT/graph/badge.svg?token=pjxYIL2kbC"/>
</a>
- NovelAI API, 由[插件](https://github.com/dominoar/QCPNovelAi)接入 <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">
- OpenAI DALL·E模型, 本项目原生支持, 使用方法查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE) </a>
- NovelAI API, 由[插件](https://github.com/dominoar/QCPNovelAi)接入 <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://www.bilibili.com/video/BV14h4y1w7TC">
- TTS+VITS, 由[插件](https://github.com/dominoar/QChatPlugins)接入 <img alt="Static Badge" src="https://img.shields.io/badge/%E8%A7%86%E9%A2%91%E6%95%99%E7%A8%8B-208647">
</a>
## ✅功能 <a href="https://www.bilibili.com/video/BV11h4y1y74H">
<img alt="Static Badge" src="https://img.shields.io/badge/Linux%E9%83%A8%E7%BD%B2%E8%A7%86%E9%A2%91-208647">
<details> </a>
<summary>✅支持敏感词过滤,避免账号风险</summary>
## 使用文档
- 难以监测机器人与用户对话时的内容,故引入此功能以减少机器人风险
- 加入了百度云内容审核,在`config.py`中修改`baidu_check`的值,并填写`baidu_api_key``baidu_secret_key`以开启此功能 <a href="https://qchatgpt.rockchin.top">项目主页</a>
- 编辑`sensitive.json`,并在`config.py`中修改`sensitive_word_filter`的值以开启此功能 <a href="https://qchatgpt.rockchin.top/posts/feature.html">功能介绍</a>
</details> <a href="https://qchatgpt.rockchin.top/posts/deploy/">部署文档</a>
<a href="https://qchatgpt.rockchin.top/posts/error/">常见问题</a>
<details> <a href="https://qchatgpt.rockchin.top/posts/plugin/intro.html">插件介绍</a>
<summary>✅群内多种响应规则,不必at</summary> <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>
- 默认回复`ai`作为前缀或`@`机器人的消息 ## 相关链接
- 详细见`config.py`中的`response_rules`字段
</details> <a href="https://github.com/RockChinQ/qcg-installer">安装器源码</a>
<a href="https://github.com/RockChinQ/qcg-tester">测试工程源码</a>
<details> <a href="https://github.com/the-lazy-me/QChatGPT-Wiki">官方文档储存库</a>
<summary>✅完善的多api-key管理,超额自动切换</summary>
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/>
- 支持配置多个`api-key`,内部统计使用量并在超额时自动切换 </div>
- 请在`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/%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>
- 自行实现插件加载器及相关支持
- 详细查看[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%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>
详情请查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
## 🔩部署
**部署过程中遇到任何问题,请先在[QChatGPT](https://github.com/RockChinQ/QChatGPT/issues)或[qcg-installer](https://github.com/RockChinQ/qcg-installer/issues)的issue里进行搜索**
### - 注册OpenAI账号
参考以下文章自行注册
> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
> [手把手教你如何注册ChatGPT,超级详细](https://guxiaobei.com/51461)
注册成功后请前往[个人中心查看](https://beta.openai.com/account/api-keys)api_key
完成注册后,使用以下自动化或手动部署步骤
### - 自动化部署
<details>
<summary>展开查看,以下方式二选一,Linux首选DockerWindows首选安装器</summary>
#### Docker方式
请查看此仓库[mikumifa/QChatGPT-Docker-Installer](https://github.com/mikumifa/QChatGPT-Docker-Installer)
#### 安装器方式
使用[此安装器](https://github.com/RockChinQ/qcg-installer)(若无法访问请到[Gitee](https://gitee.com/RockChin/qcg-installer))进行部署
- 安装器目前仅支持部分平台,请到仓库文档查看,其他平台请手动部署
</details>
### - 手动部署
<details>
<summary>手动部署适用于所有平台</summary>
- 请使用Python 3.9.x以上版本
#### 配置Mirai
按照[此教程](https://yiri-mirai.wybxc.cc/tutorials/01/configuration)配置Mirai及YiriMirai
启动mirai-console后,使用`login`命令登录QQ账号,保持mirai-console运行状态
#### 配置主程序
1. 克隆此项目
```bash
git clone https://github.com/RockChinQ/QChatGPT
cd QChatGPT
```
2. 安装依赖
```bash
pip3 install yiri-mirai openai colorlog func_timeout
pip3 install dulwich
```
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>
## 🚀使用
查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
## 🧩插件生态
现已支持自行开发插件对功能进行扩展或自定义程序行为
详见[Wiki插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
开发教程见[Wiki插件开发页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91)
### 示例插件
`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
- `cmdcn` - 主程序指令中文形式
- `hello_plugin` - 在收到消息`hello`时回复相应消息
- `urlikethisijustsix` - 收到冒犯性消息时回复相应消息
### 更多
欢迎提交新的插件
- [revLibs](https://github.com/RockChinQ/revLibs) - 将ChatGPT网页版接入此项目,关于[官方接口和网页版有什么区别](https://github.com/RockChinQ/QChatGPT/wiki/%E5%AE%98%E6%96%B9%E6%8E%A5%E5%8F%A3%E4%B8%8EChatGPT%E7%BD%91%E9%A1%B5%E7%89%88)
- [hello_plugin](https://github.com/RockChinQ/hello_plugin) - `hello_plugin` 的储存库形式,插件开发模板
- [dominoar/QChatPlugins](https://github.com/dominoar/QchatPlugins) - dominoar编写的诸多新功能插件(语言输出、Ranimg、屏蔽词规则等)
- [dominoar/QCP-NovelAi](https://github.com/dominoar/QCP-NovelAi) - NovelAI 故事叙述与绘画
## 😘致谢
- [@the-lazy-me](https://github.com/the-lazy-me) 为本项目制作[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)
- [@mikumifa](https://github.com/mikumifa) 本项目Docker部署仓库开发者
- [@dominoar](https://github.com/dominoar) 为本项目开发多种插件
- [@hissincn](https://github.com/hissincn) 本项目贡献者
- [@LINSTCL](https://github.com/LINSTCL) GPT-3.5官方模型适配贡献者
- [@Haibersut](https://github.com/Haibersut) 本项目贡献者
- [@万神的星空](https://github.com/qq255204159) 整合包发行
以及其他所有为本项目提供支持的朋友们。
## 👍赞赏
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/>
+215
View File
@@ -0,0 +1,215 @@
# QChatGPT🤖
<p align="center">
<img src="res/social.png" alt="QChatGPT" width="640" />
</p>
English | [简体中文](README.md)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/QChatGPT?style=flat-square)](https://github.com/RockChinQ/QChatGPT/releases/latest)
![Wakapi Count](https://wakapi.dev/api/badge/RockChinQ/interval:any/project:QChatGPT)
- Refer to [Wiki](https://github.com/RockChinQ/QChatGPT/wiki) to get further information.
- Official QQ group: 656285629
- Community QQ group: 362515018
- QQ channel robot: [QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT)
- Any contribution is welcome, please refer to [CONTRIBUTING.md](CONTRIBUTING.md)
## 🍺List of supported models
<details>
<summary>Details</summary>
### Chat
- OpenAI GPT-3.5 (ChatGPT API), default model
- OpenAI GPT-3, supported natively, switch to it in `config.py`
- OpenAI GPT-4, supported natively, qualification for internal testing required, switch to it in `config.py`
- ChatGPT website edition (GPT-3.5), see [revLibs plugin](https://github.com/RockChinQ/revLibs)
- ChatGPT website edition (GPT-4), ChatGPT plus subscription required, see [revLibs plugin](https://github.com/RockChinQ/revLibs)
- New Bing, see [revLibs plugin](https://github.com/RockChinQ/revLibs)
- HuggingChat, see [revLibs plugin](https://github.com/RockChinQ/revLibs), English only
### Story
- NovelAI API, see [QCPNovelAi plugin](https://github.com/dominoar/QCPNovelAi)
### Image
- OpenAI DALL·E, supported natively, see [Wiki(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
- NovelAI API, see [QCPNovelAi plugin](https://github.com/dominoar/QCPNovelAi)
### Voice
- TTS+VITS, see [QChatPlugins](https://github.com/dominoar/QChatPlugins)
- Plachta/VITS-Umamusume-voice-synthesizer, see [chat_voice plugin](https://github.com/oliverkirk-sudo/chat_voice)
</details>
Install this [plugin](https://github.com/RockChinQ/Switcher) to switch between different models.
## ✅Features
<details>
<summary>Details</summary>
- ✅Sensitive word filtering, avoid being banned
- ✅Multiple responding rules, including regular expression matching
- ✅Multiple api-key management, automatic switching when exceeding
- ✅Support for customizing the preset prompt text
- ✅Chat, story, image, voice, etc. models are supported
- ✅Support for hot reloading and hot updating
- ✅Support for plugin loading
- ✅Blacklist mechanism for private chat and group chat
- ✅Excellent long message processing strategy
- ✅Reply rate limitation
- ✅Support for network proxy
- ✅Support for customizing the output format
</details>
More details, see [Wiki(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
## 🔩Deployment
**If you encounter any problems during deployment, please search in the issue of [QChatGPT](https://github.com/RockChinQ/QChatGPT/issues) or [qcg-installer](https://github.com/RockChinQ/qcg-installer/issues) first.**
### - Register OpenAI account
> If you want to use a model other than OpenAI (such as New Bing), you can skip this step and directly refer to following steps, and then configure it according to the relevant plugin documentation.
To register OpenAI account, please refer to the following articles(in Chinese):
> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
> [手把手教你如何注册ChatGPT,超级详细](https://guxiaobei.com/51461)
Check your api-key in [personal center](https://beta.openai.com/account/api-keys) after registration, and then follow the following steps to deploy.
### - Deploy Automatically
<details>
<summary>Details</summary>
#### Docker
See [this document(cn)](res/docs/docker_deploy.md)
Contributed by [@mikumifa](https://github.com/mikumifa)
#### Installer
Use [this installer](https://github.com/RockChinQ/qcg-installer) to deploy.
- The installer currently only supports some platforms, please refer to the repository document for details, and manually deploy for other platforms
</details>
### - Deploy Manually
<details>
<summary>Manually deployment supports any platforms</summary>
- Python 3.9.x or higher
#### 配置QQ登录框架
Currently supports mirai and go-cqhttp, configure either one
<details>
<summary>mirai</summary>
Follow [this tutorial(cn)](https://yiri-mirai.wybxc.cc/tutorials/01/configuration) to configure Mirai and YiriMirai.
After starting mirai-console, use the `login` command to log in to the QQ account, and keep the mirai-console running.
</details>
<details>
<summary>go-cqhttp</summary>
1. Follow [this tutorial(cn)](https://github.com/RockChinQ/QChatGPT/wiki/go-cqhttp%E9%85%8D%E7%BD%AE) to configure go-cqhttp.
2. Start go-cqhttp, make sure it is logged in and running.
</details>
#### Configure QChatGPT
1. Clone the repository
```bash
git clone https://github.com/RockChinQ/QChatGPT
cd QChatGPT
```
2. Install dependencies
```bash
pip3 install requests yiri-mirai-rc openai colorlog func_timeout dulwich Pillow nakuru-project-idk
```
3. Generate `config.py`
```bash
python3 main.py
```
4. Edit `config.py`
5. Run
```bash
python3 main.py
```
Any problems, please refer to the issues page.
</details>
## 🚀Usage
**After deployment, please read: [Commands(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E6%9C%BA%E5%99%A8%E4%BA%BA%E6%8C%87%E4%BB%A4)**
**For more details, please refer to the [Wiki(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)**
## 🧩Plugin Ecosystem
Plugin [usage](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8) and [development](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91) are supported.
<details>
<summary>List of plugins (cn)</summary>
### Examples
`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
- `cmdcn` - 主程序命令中文形式
- `hello_plugin` - 在收到消息`hello`时回复相应消息
- `urlikethisijustsix` - 收到冒犯性消息时回复相应消息
### More Plugins
欢迎提交新的插件
- [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) - 支持通过命令切换使用的模型
- [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 故事叙述与绘画
- [oliverkirk-sudo/chat_voice](https://github.com/oliverkirk-sudo/chat_voice) - 文字转语音输出,使用HuggingFace上的[VITS-Umamusume-voice-synthesizer模型](https://huggingface.co/spaces/Plachta/VITS-Umamusume-voice-synthesizer)
- [RockChinQ/WaitYiYan](https://github.com/RockChinQ/WaitYiYan) - 实时获取百度`文心一言`等待列表人数
- [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) - 天气查询插件
</details>
## 😘Thanks
- [@the-lazy-me](https://github.com/the-lazy-me) video tutorial creator
- [@mikumifa](https://github.com/mikumifa) Docker deployment
- [@dominoar](https://github.com/dominoar) Plugin development
- [@万神的星空](https://github.com/qq255204159) Packages publisher
- [@ljcduo](https://github.com/ljcduo) GPT-4 API internal test account
And all [contributors](https://github.com/RockChinQ/QChatGPT/graphs/contributors) and other friends who support this project.
<!-- ## 👍赞赏
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/> -->
+174 -61
View File
@@ -1,7 +1,13 @@
# 配置文件: 注释里标[必需]的参数必须修改, 其他参数根据需要修改, 但请勿删除 # 配置文件: 注释里标[必需]的参数必须修改, 其他参数根据需要修改, 但请勿删除
import logging import logging
# [必需] Mirai的配置 # 消息处理协议适配器
# 目前支持以下适配器:
# - "yirimirai": mirai的通信框架,YiriMirai框架适配器, 请同时填写下方mirai_http_api_config
# - "nakuru": go-cqhttp通信框架,请同时填写下方nakuru_config
msg_source_adapter = "yirimirai"
# [必需(与nakuru二选一,取决于msg_source_adapter)] Mirai的配置
# 请到配置mirai的步骤中的教程查看每个字段的信息 # 请到配置mirai的步骤中的教程查看每个字段的信息
# adapter: 选择适配器,目前支持HTTPAdapter和WebSocketAdapter # adapter: 选择适配器,目前支持HTTPAdapter和WebSocketAdapter
# host: 运行mirai的主机地址 # host: 运行mirai的主机地址
@@ -18,6 +24,15 @@ mirai_http_api_config = {
"qq": 1234567890 "qq": 1234567890
} }
# [必需(与mirai二选一,取决于msg_source_adapter)]
# 使用nakuru-project框架连接go-cqhttp的配置
nakuru_config = {
"host": "localhost", # go-cqhttp的地址
"port": 6700, # go-cqhttp的正向websocket端口
"http_port": 5700, # go-cqhttp的正向http端口
"token": "" # 若在go-cqhttp的config.yml设置了access_token, 则填写此处
}
# [必需] OpenAI的配置 # [必需] OpenAI的配置
# api_key: OpenAI的API Key # api_key: OpenAI的API Key
# http_proxy: 请求OpenAI时使用的代理,None为不使用,https和socks5暂不能使用 # http_proxy: 请求OpenAI时使用的代理,None为不使用,https和socks5暂不能使用
@@ -33,20 +48,43 @@ mirai_http_api_config = {
# }, # },
# "http_proxy": "http://127.0.0.1:12345" # "http_proxy": "http://127.0.0.1:12345"
# } # }
#
# 现已支持反向代理,可以添加reverse_proxy字段以使用反向代理
# 使用反向代理可以在国内使用OpenAI的API,反向代理的配置请参考
# https://github.com/Ice-Hazymoon/openai-scf-proxy
#
# 反向代理填写示例:
# openai_config = {
# "api_key": {
# "default": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
# "key1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
# "key2": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
# },
# "reverse_proxy": "http://example.com:12345/v1"
# }
#
# 作者开设公用反向代理地址: https://api.openai.rockchin.top/v1
# 随时可能关闭,仅供测试使用,有条件建议使用正向代理或者自建反向代理
openai_config = { openai_config = {
"api_key": { "api_key": {
"default": "openai_api_key" "default": "openai_api_key"
}, },
"http_proxy": None "http_proxy": None,
"reverse_proxy": None
} }
# [必需] 管理员QQ号,用于接收报错等通知及执行管理员级别指令 # api-key切换策略
# active:每次请求时都会切换api-key
# passive:仅当api-key超额时才会切换api-key
switch_strategy = "active"
# [必需] 管理员QQ号,用于接收报错等通知及执行管理员级别命令
# 支持多个管理员,可以使用list形式设置,例如: # 支持多个管理员,可以使用list形式设置,例如:
# admin_qq = [12345678, 87654321] # admin_qq = [12345678, 87654321]
admin_qq = 0 admin_qq = 0
# 情景预设(机器人人格) # 情景预设(机器人人格)
# 每个会话的预设信息,影响所有会话,无视令重置 # 每个会话的预设信息,影响所有会话,无视令重置
# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令 # 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令
# 例如: # 例如:
# default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”" # default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”"
@@ -60,14 +98,14 @@ admin_qq = 0
# "en-dict": "我想让你充当英英词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈。", # "en-dict": "我想让你充当英英词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈。",
# } # }
# #
# 在使用期间即可通过令: # 在使用期间即可通过令:
# !reset [名称] # !reset [名称]
# 来使用指定的情景预设重置会话 # 来使用指定的情景预设重置会话
# 例如: # 例如:
# !reset linux-terminal # !reset linux-terminal
# 若不指定名称,则使用默认情景预设 # 若不指定名称,则使用默认情景预设
# #
# 也可以使用令: # 也可以使用令:
# !default <名称> # !default <名称>
# 将指定的情景预设设置为默认情景预设 # 将指定的情景预设设置为默认情景预设
# 例如: # 例如:
@@ -76,13 +114,15 @@ 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获取帮助”",
} }
# 实验性设置项: JSON完整情景导入 # 情景预设格式
# 预设prompt模式 # 参考值:默认方式:normal | 完整情景:full_scenario
# 参考值:旧版本方式:default | 完整情景:full_scenario # 默认方式 的格式为上述default_prompt中的内容,或prompts目录下的文件名
preset_mode = "default" # 完整情景方式 的格式为JSON,在scenario目录下的JSON文件中列出对话的每个回合,编写方法见scenario/default-template.json
# 编写方法请查看: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%97full_scenario%E6%A8%A1%E5%BC%8F
preset_mode = "normal"
# 群内响应规则 # 群内响应规则
# 符合此消息的群内消息即使不包含at机器人也会响应 # 符合此消息的群内消息即使不包含at机器人也会响应
@@ -91,19 +131,41 @@ preset_mode = "default"
# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式(regexp)匹配的消息不会删除匹配的部分 # 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式(regexp)匹配的消息不会删除匹配的部分
# 前缀匹配优先级高于正则表达式匹配 # 前缀匹配优先级高于正则表达式匹配
# 正则表达式简明教程:https://www.runoob.com/regexp/regexp-tutorial.html # 正则表达式简明教程:https://www.runoob.com/regexp/regexp-tutorial.html
#
# 支持针对不同群设置不同的响应规则,例如:
# response_rules = {
# "default": {
# "at": True,
# "prefix": ["/ai", "!ai", "ai", "ai"],
# "regexp": [],
# "random_rate": 0.0,
# },
# "12345678": {
# "at": False,
# "prefix": ["/ai", "!ai", "ai", "ai"],
# "regexp": [],
# "random_rate": 0.0,
# },
# }
#
# 以上设置将会在群号为12345678的群中关闭at响应
# 未单独设置的群将使用default规则
response_rules = { response_rules = {
"default": {
"at": True, # 是否响应at机器人的消息 "at": True, # 是否响应at机器人的消息
"prefix": ["/ai", "!ai", "ai", "ai"], "prefix": ["/ai", "!ai", "ai", "ai"],
"regexp": [], # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办" "regexp": [], # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办"
"random_rate": 0.0, # 随机响应概率,0.0-1.0,0.0为不随机响应,1.0为响应所有消息, 仅在前几项判断不通过时生效 "random_rate": 0.0, # 随机响应概率,0.0-1.0,0.0为不随机响应,1.0为响应所有消息, 仅在前几项判断不通过时生效
},
} }
# 消息忽略规则 # 消息忽略规则
# 适用于私聊及群聊 # 适用于私聊及群聊
# 符合此规则的消息将不会被响应 # 符合此规则的消息将不会被响应
# 支持消息前缀匹配及正则表达式匹配 # 支持消息前缀匹配及正则表达式匹配
# 此设置优先级高于response_rules # 此设置优先级高于response_rules
# 用以过滤mirai等其他层级的 # 用以过滤mirai等其他层级的
# @see https://github.com/RockChinQ/QChatGPT/issues/165 # @see https://github.com/RockChinQ/QChatGPT/issues/165
ignore_rules = { ignore_rules = {
"prefix": ["/"], "prefix": ["/"],
@@ -138,50 +200,97 @@ encourage_sponsor_at_start = True
# 每次向OpenAI接口发送对话记录上下文的字符数 # 每次向OpenAI接口发送对话记录上下文的字符数
# 最大不超过(4096 - max_tokens)个字符,max_tokens为下方completion_api_params中的max_tokens # 最大不超过(4096 - max_tokens)个字符,max_tokens为下方completion_api_params中的max_tokens
# 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快 # 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快
prompt_submit_length = 1024 prompt_submit_length = 3072
# 是否在token超限报错时自动重置会话
# 可在tips.py中编辑提示语
auto_reset = True
# OpenAI补全API的参数 # OpenAI补全API的参数
# 请在下方填写模型,程序自动选择接口 # 请在下方填写模型,程序自动选择接口
# 模型文档:https://platform.openai.com/docs/models
# 现已支持的模型有: # 现已支持的模型有:
# #
# 'gpt-3.5-turbo' # ChatCompletions 接口:
# 'gpt-3.5-turbo-0301' # # GPT 4 系列
# 'text-davinci-003' # "gpt-4-1106-preview",
# 'text-davinci-002' # "gpt-4-vision-preview",
# 'code-davinci-002' # "gpt-4",
# 'code-cushman-001' # "gpt-4-32k",
# 'text-curie-001' # "gpt-4-0613",
# 'text-babbage-001' # "gpt-4-32k-0613",
# 'text-ada-001' # "gpt-4-0314", # legacy
# "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
#
# Completions接口:
# "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
#
# 支持通过 One API 接入多种模型,请在上方的openai_config中设置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",
# "gemini-pro",
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]
"max_tokens": 1024, # 每次获取OpenAI接口响应的文字量上限, 不高于4096
"top_p": 1, # 生成的文本的文本与要求的符合度, 取值范围[0, 1]
"frequency_penalty": 0.2,
"presence_penalty": 1.0,
} }
# 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",
} }
# 跟踪函数调用
# 为True时,在每次GPT进行Function Calling时都会输出发送一条回复给用户
# 同时,一次提问内所有的Function Calling和普通回复消息都会单独发送给用户
trace_function_calls = False
# 群内回复消息时是否引用原消息 # 群内回复消息时是否引用原消息
quote_origin = True quote_origin = False
# 群内回复消息时是否at发送者
at_sender = False
# 回复绘图时是否包含图片描述 # 回复绘图时是否包含图片描述
include_image_description = True include_image_description = True
# 消息处理的超时时间,单位为秒 # 消息处理的超时时间,单位为秒
process_message_timeout = 30 process_message_timeout = 120
# 回复消息时是否显示[GPT]前缀 # 回复消息时是否显示[GPT]前缀
show_prefix = False show_prefix = False
# 回复前的强制延迟时间,降低机器人被腾讯风控概率
# *此机制对命令和消息、私聊及群聊均生效
# 每次处理时从以下的范围取一个随机秒数,
# 当此次消息处理时间低于此秒数时,将会强制延迟至此秒数
# 例如:[1.5, 3],则每次处理时会随机取一个1.5-3秒的随机数,若处理时间低于此随机数,则强制延迟至此随机秒数
# 若您不需要此功能,请将force_delay_range设置为[0, 0]
force_delay_range = [0, 0]
# 应用长消息处理策略的阈值 # 应用长消息处理策略的阈值
# 当回复消息长度超过此值时,将使用长消息处理策略 # 当回复消息长度超过此值时,将使用长消息处理策略
blob_message_threshold = 256 blob_message_threshold = 256
@@ -191,6 +300,12 @@ blob_message_threshold = 256
# - "forward": 将长消息转换为转发消息组件发送 # - "forward": 将长消息转换为转发消息组件发送
blob_message_strategy = "forward" blob_message_strategy = "forward"
# 允许等待
# 同一会话内,是否等待上一条消息处理完成后再处理下一条消息
# 若设置为False,若上一条未处理完时收到了新消息,将会丢弃新消息
# 丢弃消息时的提示信息可以在tips.py中修改
wait_last_done = True
# 文字转图片时使用的字体文件路径 # 文字转图片时使用的字体文件路径
# 当策略为"image"时生效 # 当策略为"image"时生效
# 若在Windows系统下,程序会自动使用Windows自带的微软雅黑字体 # 若在Windows系统下,程序会自动使用Windows自带的微软雅黑字体
@@ -205,53 +320,51 @@ retry_times = 3
# 设置为False时,向用户及管理员发送错误详细信息 # 设置为False时,向用户及管理员发送错误详细信息
hide_exce_info_to_user = False hide_exce_info_to_user = False
# 消息处理出错时向用户发送的提示信息
# 仅当hide_exce_info_to_user为True时生效
# 设置为空字符串时,不发送提示信息
alter_tip_message = '出错了,请稍后再试'
# 机器人线程池大小
# 该参数决定机器人可以同时处理几个人的消息,超出线程池数量的请求会被阻塞,不会被丢弃
# 如果你不清楚该参数的意义,请不要更改
pool_num = 10
# 每个会话的过期时间,单位为秒 # 每个会话的过期时间,单位为秒
# 默认值20分钟 # 默认值20分钟
session_expire_time = 60 * 20 session_expire_time = 1200
# 会话限速 # 会话限速
# 单会话内每分钟可进行的对话次数 # 单会话内每分钟可进行的对话次数
# 若不需要限速,可以设置为一个很大的值 # 若不需要限速,可以设置为一个很大的值
# 默认值60次,基本上不会触发限速 # 默认值60次,基本上不会触发限速
rate_limitation = 60 #
# 若要设置针对某特定群的限速,请使用如下格式:
# {
# "group_<群号>": 60,
# "default": 60,
# }
# 若要设置针对某特定用户私聊的限速,请使用如下格式:
# {
# "person_<用户QQ>": 60,
# "default": 60,
# }
# 同时设置多个群和私聊的限速,示例:
# {
# "group_12345678": 60,
# "group_87654321": 60,
# "person_234567890": 60,
# "person_345678901": 60,
# "default": 60,
# }
#
# 注意: 未指定的都使用default的限速值,default不可删除
rate_limitation = {
"default": 60,
}
# 会话限速策略 # 会话限速策略
# - "wait": 每次对话获取到回复时,等待一定时间再发送回复,保证其不会超过限速均值 # - "wait": 每次对话获取到回复时,等待一定时间再发送回复,保证其不会超过限速均值
# - "drop": 此分钟内,若对话次数超过限速次数,则丢弃之后的对话,每自然分钟重置 # - "drop": 此分钟内,若对话次数超过限速次数,则丢弃之后的对话,每自然分钟重置
rate_limit_strategy = "wait" rate_limit_strategy = "drop"
# drop策略时,超过限速均值时,丢弃的对话的提示信息
# 仅当rate_limitation_strategy为"drop"时生效
# 若设置为空字符串,则不发送提示信息
rate_limit_drop_tip = "本分钟对话次数超过限速次数,此对话被丢弃"
# 是否在启动时进行依赖库更新 # 是否在启动时进行依赖库更新
upgrade_dependencies = True upgrade_dependencies = False
# 是否上报统计信息 # 是否上报统计信息
# 用于统计机器人的使用情况,不会收集任何用户信息 # 用于统计机器人的使用情况,数据不公开,不会收集任何敏感信息
# 仅上报时间、字数使用量、绘图使用量,其他信息不会上报 # 仅实例识别UUID、上报时间、字数使用量、绘图使用量、插件使用情况、用户信息,其他信息不会上报
report_usage = True report_usage = True
# 日志级别 # 日志级别
logging_level = logging.INFO logging_level = logging.INFO
# 定制帮助消息
help_message = """此机器人通过调用OpenAI的GPT-3大型语言模型生成回复,不具有情感。
你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。
了解此项目请找QQ 1010553892 联系作者
请不要用其生成整篇文章或大段代码,因为每次只会向模型提交少部分文字,生成大部分文字会产生偏题、前后矛盾等问题
每次会话最后一次交互后{}分钟后会自动结束,结束后将开启新会话,如需继续前一次会话请发送 !last 重新开启
欢迎到github.com/RockChinQ/QChatGPT 给个star
指令帮助信息请查看: https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E6%9C%BA%E5%99%A8%E4%BA%BA%E6%8C%87%E4%BB%A4""".format(session_expire_time // 60)
+18
View File
@@ -0,0 +1,18 @@
version: "3"
services:
qchatgpt:
image: rockchin/qchatgpt:latest
volumes:
- ./config.py:/QChatGPT/config.py
- ./banlist.py:/QChatGPT/banlist.py
- ./cmdpriv.json:/QChatGPT/cmdpriv.json
- ./sensitive.json:/QChatGPT/sensitive.json
- ./tips.py:/QChatGPT/tips.py
# 目录映射
- ./plugins:/QChatGPT/plugins
- ./scenario:/QChatGPT/scenario
- ./temp:/QChatGPT/temp
- ./logs:/QChatGPT/logs
restart: always
# 根据具体环境配置网络
+258 -139
View File
@@ -1,4 +1,5 @@
import importlib import importlib
import json
import os import os
import shutil import shutil
import threading import threading
@@ -6,14 +7,65 @@ import time
import logging import logging
import sys import sys
import traceback
import asyncio
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.config import manager as config_mgr
from pkg.config.impls import pymodule as pymodule_cfg
try: try:
import colorlog import colorlog
except ImportError: except ImportError:
# 尝试安装 # 尝试安装
import pkg.utils.pkgmgr as pkgmgr import pkg.utils.pkgmgr as pkgmgr
pkgmgr.install_requirements("requirements.txt")
try: try:
pkgmgr.install_requirements("requirements.txt")
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")
@@ -23,17 +75,12 @@ import colorlog
import requests import requests
import websockets.exceptions import websockets.exceptions
from urllib3.exceptions import InsecureRequestWarning from urllib3.exceptions import InsecureRequestWarning
import pkg.utils.context
sys.path.append(".") # 是否使用override.json覆盖配置
# 仅在启动时提供 --override 或 -r 参数时生效
log_colors_config = { use_override = False
'DEBUG': 'green', # cyan white
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'bold_red',
}
def init_db(): def init_db():
@@ -45,77 +92,81 @@ def init_db():
def ensure_dependencies(): def ensure_dependencies():
import pkg.utils.pkgmgr as pkgmgr import pkg.utils.pkgmgr as pkgmgr
pkgmgr.run_pip(["install", "openai", "Pillow", "--upgrade", pkgmgr.run_pip(["install", "openai", "Pillow", "nakuru-project-idk", "CallingGPT", "tiktoken", "--upgrade",
"-i", "https://pypi.douban.com/simple/", "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
"--trusted-host", "pypi.douban.com"]) "--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
known_exception_caught = False known_exception_caught = False
log_file_name = "qchatgpt.log"
def override_config_manager():
config = pkg.utils.context.get_config_manager().data
if os.path.exists("override.json") and use_override:
override_json = json.load(open("override.json", "r", encoding="utf-8"))
overrided = []
for key in override_json:
if key in config:
config[key] = override_json[key]
# logging.info("覆写配置[{}]为[{}]".format(key, override_json[key]))
overrided.append(key)
else:
logging.error("无法覆写配置[{}]为[{}],该配置不存在,请检查override.json是否正确".format(key, override_json[key]))
if len(overrided) > 0:
logging.info("已根据override.json覆写配置项: {}".format(", ".join(overrided)))
def init_runtime_log_file(): def complete_tips():
"""为此次运行生成日志文件 """根据tips-custom-template模块补全tips模块的属性"""
格式: qchatgpt-yyyy-MM-dd-HH-mm-ss.log non_exist_keys = []
"""
global log_file_name
# 检查logs目录是否存在 is_integrity = True
if not os.path.exists("logs"): logging.debug("检查tips模块完整性.")
os.mkdir("logs") tips_template = importlib.import_module('tips-custom-template')
tips = importlib.import_module('tips')
for key in dir(tips_template):
if not key.startswith("__") and not hasattr(tips, key):
setattr(tips, key, getattr(tips_template, key))
# logging.warning("[{}]不存在".format(key))
non_exist_keys.append(key)
is_integrity = False
# 检查本目录是否有qchatgpt.log,若有,移动到logs目录 if not is_integrity:
if os.path.exists("qchatgpt.log"): logging.warning("以下提示语字段不存在: {}".format(", ".join(non_exist_keys)))
shutil.move("qchatgpt.log", "logs/qchatgpt.legacy.log") logging.warning("tips模块不完整,您可以依据tips-custom-template.py检查tips.py")
logging.warning("以上配置已被设为默认值,将在3秒后继续启动... ")
log_file_name = "logs/qchatgpt-%s.log" % time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) time.sleep(3)
def reset_logging(): async def start_process(first_time_init=False):
global log_file_name
assert os.path.exists('config.py')
config = importlib.import_module('config')
import pkg.utils.context
if pkg.utils.context.context['logger_handler'] is not None:
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
for handler in logging.getLogger().handlers:
logging.getLogger().removeHandler(handler)
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
filename=log_file_name, # log日志输出的文件位置和文件名
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
# 日志输出的格式
# -8表示占位符,让输出左对齐,输出长度都为8位
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
)
sh = logging.StreamHandler()
sh.setLevel(config.logging_level)
sh.setFormatter(colorlog.ColoredFormatter(
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
"%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors=log_colors_config
))
logging.getLogger().addHandler(sh)
pkg.utils.context.context['logger_handler'] = sh
return sh
def main(first_time_init=False):
"""启动流程,reload之后会被执行""" """启动流程,reload之后会被执行"""
global known_exception_caught 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',
'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
import config
# 更新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")
@@ -126,37 +177,21 @@ def main(first_time_init=False):
known_exception_caught = False known_exception_caught = False
try: try:
# 导入config.py try:
assert os.path.exists('config.py')
config = importlib.import_module('config')
init_runtime_log_file()
sh = reset_logging() sh = reset_logging()
pkg.utils.context.context['logger_handler'] = sh
# 配置完整性校验 # 初始化文字转图片
is_integrity = True from pkg.utils import text2img
config_template = importlib.import_module('config-template') text2img.initialize()
for key in dir(config_template):
if not key.startswith("__") and not hasattr(config, key):
setattr(config, key, getattr(config_template, key))
logging.warning("[{}]不存在".format(key))
is_integrity = False
if not is_integrity:
logging.warning("配置文件不完整,请依据config-template.py检查config.py")
logging.warning("以上配置已被设为默认值,将在5秒后继续启动... ")
time.sleep(5)
import pkg.utils.context
pkg.utils.context.set_config(config)
# 检查是否设置了管理员 # 检查是否设置了管理员
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号: "))
# 写入到文件 # 写入到文件
# 读取文件 # 读取文件
@@ -164,7 +199,7 @@ def main(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)
@@ -175,30 +210,65 @@ def main(first_time_init=False):
except ValueError: except ValueError:
print("请输入数字") 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.openai.manager
import pkg.database.manager import pkg.database.manager
import pkg.openai.session import pkg.openai.session
import pkg.qqbot.manager import pkg.qqbot.manager
import pkg.openai.dprompt import pkg.openai.dprompt
import pkg.qqbot.cmds.aamgr
pkg.openai.dprompt.read_prompt_from_file() try:
pkg.openai.dprompt.read_scenario_from_file() pkg.openai.dprompt.register_all()
pkg.qqbot.cmds.aamgr.register_all()
pkg.qqbot.cmds.aamgr.apply_privileges()
except Exception as e:
logging.error(e)
traceback.print_exc()
# 配置OpenAI proxy
import openai
openai.proxies = None # 先重置,因为重载后可能需要清除proxy
if "http_proxy" in cfg['openai_config'] and cfg['openai_config']["http_proxy"] is not None:
openai.proxies = {
"http": cfg['openai_config']["http_proxy"],
"https": cfg['openai_config']["http_proxy"]
}
# 配置openai api_base
if "reverse_proxy" in cfg['openai_config'] and cfg['openai_config']["reverse_proxy"] is not None:
logging.debug("设置反向代理: "+cfg['openai_config']['reverse_proxy'])
openai.base_url = cfg['openai_config']["reverse_proxy"]
pkg.utils.context.context['logger_handler'] = sh
# 主启动流程 # 主启动流程
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()
# 初始化qq机器人 # 初始化qq机器人
qqbot = pkg.qqbot.manager.QQBotManager(mirai_http_api_config=config.mirai_http_api_config, qqbot = pkg.qqbot.manager.QQBotManager(first_time_init=first_time_init)
timeout=config.process_message_timeout, retry=config.retry_times,
first_time_init=first_time_init, pool_num=config.pool_num)
# 加载插件 # 加载插件
import pkg.plugin.host import pkg.plugin.host
@@ -213,7 +283,8 @@ def main(first_time_init=False):
def run_bot_wrapper(): def run_bot_wrapper():
global known_exception_caught global known_exception_caught
try: try:
qqbot.bot.run() logging.debug("使用账号: {}".format(qqbot.bot_account_id))
qqbot.adapter.run_sync()
except TypeError as e: except TypeError as e:
if str(e).__contains__("argument 'debug'"): if str(e).__contains__("argument 'debug'"):
logging.error( logging.error(
@@ -248,13 +319,28 @@ def main(first_time_init=False):
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format( "mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
e)) e))
else: else:
import traceback
traceback.print_exc()
logging.error( logging.error(
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/QChatGPT/issues 查找或提issue".format(e)) "捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/QChatGPT/issues 查找或提issue".format(e))
known_exception_caught = True known_exception_caught = True
raise e raise e
finally:
qq_bot_thread = threading.Thread(target=run_bot_wrapper, args=(), daemon=True) time.sleep(12)
qq_bot_thread.start() threading.Thread(
target=run_bot_wrapper
).start()
except Exception as e:
traceback.print_exc()
if isinstance(e, KeyboardInterrupt):
logging.info("程序被用户中止")
sys.exit(0)
elif isinstance(e, SyntaxError):
logging.error("配置文件存在语法错误,请检查配置文件:\n1. 是否存在中文符号\n2. 是否已按照文件中的说明填写正确")
sys.exit(1)
else:
logging.error("初始化失败:{}".format(e))
sys.exit(1)
finally: finally:
# 判断若是Windows,输出选择模式可能会暂停程序的警告 # 判断若是Windows,输出选择模式可能会暂停程序的警告
if os.name == 'nt': if os.name == 'nt':
@@ -262,18 +348,23 @@ def main(first_time_init=False):
logging.info("您正在使用Windows系统,若命令行窗口处于“选择”模式,程序可能会被暂停,此时请右键点击窗口空白区域使其取消选择模式。") logging.info("您正在使用Windows系统,若命令行窗口处于“选择”模式,程序可能会被暂停,此时请右键点击窗口空白区域使其取消选择模式。")
time.sleep(12) time.sleep(12)
if first_time_init: if first_time_init:
if not known_exception_caught: if not known_exception_caught:
logging.info('程序启动完成,如长时间未显示 ”成功登录到账号xxxxx“ ,并且不回复消息,请查看 ' if cfg['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.critical('程序启动完成,如长时间未显示 "成功登录到账号xxxxx" ,并且不回复消息,解决办法(请勿到群里问): '
'https://github.com/RockChinQ/QChatGPT/issues/37') 'https://github.com/RockChinQ/QChatGPT/issues/37')
elif cfg['msg_source_adapter'] == 'nakuru':
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是否正确')
else: else:
sys.exit(1) sys.exit(1)
else: else:
logging.info('热重载完成') logging.info('热重载完成')
# 发送赞赏码 # 发送赞赏码
if hasattr(config, 'encourage_sponsor_at_start') \ if cfg['encourage_sponsor_at_start'] \
and config.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("发送赞赏码")
@@ -293,18 +384,32 @@ def main(first_time_init=False):
import pkg.utils.updater import pkg.utils.updater
try: try:
if pkg.utils.updater.is_new_version_available(): if pkg.utils.updater.is_new_version_available():
pkg.utils.context.get_qqbot_manager().notify_admin("新版本可用,请发送 !update 进行自动更新\n更新日志:\n{}".format("\n".join(pkg.utils.updater.get_rls_notes()))) logging.info("新版本可用,请发送 !update 进行自动更新\n更新日志:\n{}".format("\n".join(pkg.utils.updater.get_rls_notes())))
else: else:
logging.info("当前已是最新版本") # logging.info("当前已是最新版本")
pass
except Exception as e: except Exception as e:
logging.warning("检查更新失败:{}".format(e)) logging.warning("检查更新失败:{}".format(e))
try:
import pkg.utils.announcement as announcement
new_announcement = announcement.fetch_new()
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))
return qqbot return qqbot
def stop(): def stop():
import pkg.utils.context
import pkg.qqbot.manager import pkg.qqbot.manager
import pkg.openai.session import pkg.openai.session
try: try:
@@ -323,35 +428,27 @@ def stop():
raise e raise e
if __name__ == '__main__': def main():
# 检查是否有config.py,如果没有就把config-template.py复制一份,并退出程序 global use_override
if not os.path.exists('config.py'): # 检查是否携带了 --override 或 -r 参数
shutil.copy('config-template.py', 'config.py') if '--override' in sys.argv or '-r' in sys.argv:
print('请先在config.py中填写配置') use_override = True
sys.exit(0)
# 检查是否有banlist.py,如果没有就把banlist-template.py复制一份 # 初始化logging
if not os.path.exists('banlist.py'): init_runtime_log_file()
shutil.copy('banlist-template.py', 'banlist.py') pkg.utils.context.context['logger_handler'] = reset_logging()
# 检查是否有sensitive.json # 配置线程池
if not os.path.exists("sensitive.json"): from pkg.utils import ThreadCtl
shutil.copy("sensitive-template.json", "sensitive.json") thread_ctl = ThreadCtl(
sys_pool_num=8,
# 检查是否有scenario/default.json admin_pool_num=4,
if not os.path.exists("scenario/default.json"): user_pool_num=8
shutil.copy("scenario/default-template.json", "scenario/default.json") )
# 存进上下文
# 检查temp目录 pkg.utils.context.set_thread_ctl(thread_ctl)
if not os.path.exists("temp/"):
os.mkdir("temp/")
# 检查并创建plugins、prompts目录
check_path = ["plugins", "prompts"]
for path in check_path:
if not os.path.exists(path):
os.mkdir(path)
# 启动指令处理
if len(sys.argv) > 1 and sys.argv[1] == 'init_db': if len(sys.argv) > 1 and sys.argv[1] == 'init_db':
init_db() init_db()
sys.exit(0) sys.exit(0)
@@ -362,16 +459,38 @@ if __name__ == '__main__':
updater.update_all(cli=True) updater.update_all(cli=True)
sys.exit(0) sys.exit(0)
# 关闭urllib的http警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
qqbot = main(True) def run_wrapper():
asyncio.run(start_process(True))
import pkg.utils.context pkg.utils.context.get_thread_ctl().submit_sys_task(
run_wrapper
)
# 主线程循环
while True: while True:
try: try:
time.sleep(10) time.sleep(0xFF)
except KeyboardInterrupt: except:
stop() stop()
pkg.utils.context.get_thread_ctl().shutdown()
print("程序退出") launch_args = sys.argv.copy()
if "--cov-report" not in launch_args:
import platform
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) sys.exit(0)
if __name__ == '__main__':
main()
+90
View File
@@ -0,0 +1,90 @@
{
"comment": "这是override.json支持的字段全集, 关于override.json机制, 请查看https://github.com/RockChinQ/QChatGPT/pull/271",
"msg_source_adapter": "yirimirai",
"mirai_http_api_config": {
"adapter": "WebSocketAdapter",
"host": "localhost",
"port": 8080,
"verifyKey": "yirimirai",
"qq": 1234567890
},
"nakuru_config": {
"host": "localhost",
"port": 6700,
"http_port": 5700,
"token": ""
},
"openai_config": {
"api_key": {
"default": "openai_api_key"
},
"http_proxy": null,
"reverse_proxy": null
},
"switch_strategy": "active",
"admin_qq": 0,
"default_prompt": {
"default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。"
},
"preset_mode": "normal",
"response_rules": {
"default": {
"at": true,
"prefix": [
"/ai",
"!ai",
"ai",
"ai"
],
"regexp": [],
"random_rate": 0.0
}
},
"ignore_rules": {
"prefix": [
"/"
],
"regexp": []
},
"income_msg_check": false,
"sensitive_word_filter": true,
"baidu_check": false,
"baidu_api_key": "",
"baidu_secret_key": "",
"inappropriate_message_tips": "[百度云]请珍惜机器人,当前返回内容不合规",
"encourage_sponsor_at_start": true,
"prompt_submit_length": 3072,
"auto_reset": true,
"completion_api_params": {
"model": "gpt-3.5-turbo",
"temperature": 0.9
},
"image_api_params": {
"model": "dall-e-2",
"size": "256x256"
},
"trace_function_calls": false,
"quote_origin": false,
"at_sender": false,
"include_image_description": true,
"process_message_timeout": 120,
"show_prefix": false,
"force_delay_range": [
0,
0
],
"blob_message_threshold": 256,
"blob_message_strategy": "forward",
"wait_last_done": true,
"font_path": "",
"retry_times": 3,
"hide_exce_info_to_user": false,
"session_expire_time": 1200,
"rate_limitation": {
"default": 60
},
"rate_limit_strategy": "drop",
"upgrade_dependencies": false,
"report_usage": true,
"logging_level": 20
}
+9 -28
View File
@@ -5,11 +5,12 @@
import hashlib import hashlib
import json import json
import logging import logging
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:
@@ -20,7 +21,7 @@ class DataGatherer:
以key值md5为key,{ 以key值md5为key,{
"text": { "text": {
"text-davinci-003": 文字量:int, "gpt-3.5-turbo": 文字量:int,
}, },
"image": { "image": {
"256x256": 图片数量:int, "256x256": 图片数量:int,
@@ -32,33 +33,17 @@ 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
def report_to_server(self, subservice_name: str, count: int):
"""向中央服务器报告使用量
只会报告此次请求的使用量,不会报告总量。
不包含除版本号、使用类型、使用量以外的任何信息,仅供开发者分析使用情况。
"""
try:
config = pkg.utils.context.get_config()
if hasattr(config, "report_usage") and not config.report_usage:
return
res = requests.get("http://rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}".format(subservice_name, self.version_str, count))
if res.status_code != 200 or res.text != "ok":
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
except:
return
def get_usage(self, key_md5): def get_usage(self, key_md5):
return self.usage[key_md5] if key_md5 in self.usage else {} return self.usage[key_md5] if key_md5 in self.usage else {}
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] = {}
@@ -73,12 +58,10 @@ class DataGatherer:
self.usage[key_md5]["text"][model] += length self.usage[key_md5]["text"][model] += length
self.dump_to_db() self.dump_to_db()
self.report_to_server("text", length)
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] = {}
@@ -92,8 +75,6 @@ class DataGatherer:
self.usage[key_md5]["image"][size] += 1 self.usage[key_md5]["image"][size] += 1
self.dump_to_db() self.dump_to_db()
self.report_to_server("image", 1)
def get_text_length_of_key(self, key): def get_text_length_of_key(self, key):
"""获取指定api-key (明文) 的文字总使用量(本地记录)""" """获取指定api-key (明文) 的文字总使用量(本地记录)"""
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest() key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
@@ -125,9 +106,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)
+83
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
+62
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
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
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
+37 -23
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)
@@ -54,20 +53,27 @@ class DatabaseManager:
`last_interact_timestamp` bigint not null, `last_interact_timestamp` bigint not null,
`status` varchar(255) not null default 'on_going', `status` varchar(255) not null default 'on_going',
`default_prompt` text not null default '', `default_prompt` text not null default '',
`prompt` text not null `prompt` text not null,
`token_counts` text not null default '[]'
) )
""") """)
# 检查sessions表是否存在`default_prompt`字段 # 检查sessions表是否存在`default_prompt`字段, 检查是否存在`token_counts`字段
self.__execute__("PRAGMA table_info('sessions')") self.__execute__("PRAGMA table_info('sessions')")
columns = self.cursor.fetchall() columns = self.cursor.fetchall()
has_default_prompt = False has_default_prompt = False
has_token_counts = False
for field in columns: for field in columns:
if field[1] == 'default_prompt': if field[1] == 'default_prompt':
has_default_prompt = True has_default_prompt = True
if field[1] == 'token_counts':
has_token_counts = True
if has_default_prompt and has_token_counts:
break break
if not has_default_prompt: if not has_default_prompt:
self.__execute__("alter table `sessions` add column `default_prompt` text not null default ''") self.__execute__("alter table `sessions` add column `default_prompt` text not null default ''")
if not has_token_counts:
self.__execute__("alter table `sessions` add column `token_counts` text not null default '[]'")
self.__execute__(""" self.__execute__("""
@@ -85,11 +91,11 @@ class DatabaseManager:
`json` text not null `json` text not null
) )
""") """)
print('Database initialized.') # print('Database initialized.')
# session持久化 # session持久化
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int, def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,
last_interact_timestamp: int, prompt: str, default_prompt: str = ''): last_interact_timestamp: int, prompt: str, default_prompt: str = '', token_counts: str = ''):
"""持久化指定session""" """持久化指定session"""
# 检查是否已经有了此name和create_timestamp的session # 检查是否已经有了此name和create_timestamp的session
@@ -102,20 +108,20 @@ class DatabaseManager:
if count == 0: if count == 0:
sql = """ sql = """
insert into `sessions` (`name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `default_prompt`) insert into `sessions` (`name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `default_prompt`, `token_counts`)
values (?, ?, ?, ?, ?, ?, ?) values (?, ?, ?, ?, ?, ?, ?, ?)
""" """
self.__execute__(sql, self.__execute__(sql,
("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp, ("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp,
last_interact_timestamp, prompt, default_prompt)) last_interact_timestamp, prompt, default_prompt, token_counts))
else: else:
sql = """ sql = """
update `sessions` set `last_interact_timestamp` = ?, `prompt` = ? update `sessions` set `last_interact_timestamp` = ?, `prompt` = ?, `token_counts` = ?
where `type` = ? and `number` = ? and `create_timestamp` = ? where `type` = ? and `number` = ? and `create_timestamp` = ?
""" """
self.__execute__(sql, (last_interact_timestamp, prompt, subject_type, self.__execute__(sql, (last_interact_timestamp, prompt, token_counts, subject_type,
subject_number, create_timestamp)) subject_number, create_timestamp))
# 显式关闭一个session # 显式关闭一个session
@@ -138,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` 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:
@@ -154,6 +160,7 @@ class DatabaseManager:
prompt = result[5] prompt = result[5]
status = result[6] status = result[6]
default_prompt = result[7] default_prompt = result[7]
token_counts = result[8]
# 当且仅当最后一个该对象的会话是on_going状态时,才会被加载 # 当且仅当最后一个该对象的会话是on_going状态时,才会被加载
if status == 'on_going': if status == 'on_going':
@@ -163,7 +170,8 @@ class DatabaseManager:
'create_timestamp': create_timestamp, 'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp, 'last_interact_timestamp': last_interact_timestamp,
'prompt': prompt, 'prompt': prompt,
'default_prompt': default_prompt 'default_prompt': default_prompt,
'token_counts': token_counts
} }
else: else:
if session_name in sessions: if session_name in sessions:
@@ -175,7 +183,7 @@ class DatabaseManager:
def last_session(self, session_name: str, cursor_timestamp: int): def last_session(self, session_name: str, cursor_timestamp: int):
self.__execute__(""" self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt` select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `name` = '{}' and `last_interact_timestamp` < {} order by `last_interact_timestamp` desc from `sessions` where `name` = '{}' and `last_interact_timestamp` < {} order by `last_interact_timestamp` desc
limit 1 limit 1
""".format(session_name, cursor_timestamp)) """.format(session_name, cursor_timestamp))
@@ -192,6 +200,7 @@ class DatabaseManager:
prompt = result[5] prompt = result[5]
status = result[6] status = result[6]
default_prompt = result[7] default_prompt = result[7]
token_counts = result[8]
return { return {
'subject_type': subject_type, 'subject_type': subject_type,
@@ -199,14 +208,15 @@ class DatabaseManager:
'create_timestamp': create_timestamp, 'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp, 'last_interact_timestamp': last_interact_timestamp,
'prompt': prompt, 'prompt': prompt,
'default_prompt': default_prompt 'default_prompt': default_prompt,
'token_counts': token_counts
} }
# 获取此session_name后一个session的数据 # 获取此session_name后一个session的数据
def next_session(self, session_name: str, cursor_timestamp: int): def next_session(self, session_name: str, cursor_timestamp: int):
self.__execute__(""" self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt` select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `name` = '{}' and `last_interact_timestamp` > {} order by `last_interact_timestamp` asc from `sessions` where `name` = '{}' and `last_interact_timestamp` > {} order by `last_interact_timestamp` asc
limit 1 limit 1
""".format(session_name, cursor_timestamp)) """.format(session_name, cursor_timestamp))
@@ -223,6 +233,7 @@ class DatabaseManager:
prompt = result[5] prompt = result[5]
status = result[6] status = result[6]
default_prompt = result[7] default_prompt = result[7]
token_counts = result[8]
return { return {
'subject_type': subject_type, 'subject_type': subject_type,
@@ -230,13 +241,14 @@ class DatabaseManager:
'create_timestamp': create_timestamp, 'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp, 'last_interact_timestamp': last_interact_timestamp,
'prompt': prompt, 'prompt': prompt,
'default_prompt': default_prompt 'default_prompt': default_prompt,
'token_counts': token_counts
} }
# 列出与某个对象的所有对话session # 列出与某个对象的所有对话session
def list_history(self, session_name: str, capacity: int, page: int): def list_history(self, session_name: str, capacity: int, page: int):
self.__execute__(""" self.__execute__("""
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt` select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit {} offset {} from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit {} offset {}
""".format(session_name, capacity, capacity * page)) """.format(session_name, capacity, capacity * page))
results = self.cursor.fetchall() results = self.cursor.fetchall()
@@ -250,6 +262,7 @@ class DatabaseManager:
prompt = result[5] prompt = result[5]
status = result[6] status = result[6]
default_prompt = result[7] default_prompt = result[7]
token_counts = result[8]
sessions.append({ sessions.append({
'subject_type': subject_type, 'subject_type': subject_type,
@@ -257,7 +270,8 @@ class DatabaseManager:
'create_timestamp': create_timestamp, 'create_timestamp': create_timestamp,
'last_interact_timestamp': last_interact_timestamp, 'last_interact_timestamp': last_interact_timestamp,
'prompt': prompt, 'prompt': prompt,
'default_prompt': default_prompt 'default_prompt': default_prompt,
'token_counts': token_counts
}) })
return sessions return sessions
+1 -2
View File
@@ -1,2 +1 @@
"""OpenAI 接口处理及会话管理相关 """OpenAI 接口处理及会话管理相关"""
"""
View File
+232
View File
@@ -0,0 +1,232 @@
import json
import logging
import openai
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):
"""调用ChatCompletion接口的请求类。
此类保证每一次返回的角色为assistant的信息的finish_reason一定为stop。
若有函数调用响应,本类的返回瀑布是:函数调用请求->函数调用结果->...->assistant的信息->stop。
"""
model: str
messages: list[dict[str, str]]
kwargs: dict
stopped: bool = False
pending_func_call: chat_completion_message.FunctionCall = None
pending_msg: str
def flush_pending_msg(self):
self.append_message(
role="assistant",
content=self.pending_msg
)
self.pending_msg = ""
def append_message(self, role: str, content: str, name: str=None, function_call: dict=None):
msg = {
"role": role,
"content": content
}
if name is not None:
msg['name'] = name
if function_call is not None:
msg['function_call'] = function_call
self.messages.append(msg)
def __init__(
self,
client: openai.Client,
model: str,
messages: list[dict[str, str]],
**kwargs
):
self.client = client
self.model = model
self.messages = messages.copy()
self.kwargs = kwargs
self.req_func = self.client.chat.completions.create
self.pending_func_call = None
self.stopped = False
self.pending_msg = ""
def __iter__(self):
return self
def __next__(self) -> dict:
if self.stopped:
raise StopIteration()
if self.pending_func_call is None: # 没有待处理的函数调用请求
args = {
"model": self.model,
"messages": self.messages,
}
funcs = funcmgr.get_func_schema_list()
if len(funcs) > 0:
args['functions'] = funcs
# 拼接kwargs
args = {**args, **self.kwargs}
from openai.types.chat import chat_completion
resp: chat_completion.ChatCompletion = self._req(**args)
choice0 = resp.choices[0]
# 如果不是函数调用,且finish_reason为stop,则停止迭代
if choice0.finish_reason == 'stop': # and choice0["finish_reason"] == "stop"
self.stopped = True
if hasattr(choice0.message, 'function_call') and choice0.message.function_call is not None:
self.pending_func_call = choice0.message.function_call
self.append_message(
role="assistant",
content=choice0.message.content,
function_call=choice0.message.function_call
)
return {
"id": resp.id,
"choices": [
{
"index": choice0.index,
"message": {
"role": "assistant",
"type": "function_call",
"content": choice0.message.content,
"function_call": {
"name": choice0.message.function_call.name,
"arguments": choice0.message.function_call.arguments
}
},
"finish_reason": "function_call"
}
],
"usage": {
"prompt_tokens": resp.usage.prompt_tokens,
"completion_tokens": resp.usage.completion_tokens,
"total_tokens": resp.usage.total_tokens
}
}
else:
# self.pending_msg += choice0['message']['content']
# 普通回复一定处于最后方,故不用再追加进内部messages
return {
"id": resp.id,
"choices": [
{
"index": choice0.index,
"message": {
"role": "assistant",
"type": "text",
"content": choice0.message.content
},
"finish_reason": choice0.finish_reason
}
],
"usage": {
"prompt_tokens": resp.usage.prompt_tokens,
"completion_tokens": resp.usage.completion_tokens,
"total_tokens": resp.usage.total_tokens
}
}
else: # 处理函数调用请求
cp_pending_func_call = self.pending_func_call.copy()
self.pending_func_call = None
func_name = cp_pending_func_call.name
arguments = {}
try:
try:
arguments = json.loads(cp_pending_func_call.arguments)
# 若不是json格式的异常处理
except json.decoder.JSONDecodeError:
# 获取函数的参数列表
func_schema = funcmgr.get_func_schema(func_name)
arguments = {
func_schema['parameters']['required'][0]: cp_pending_func_call.arguments
}
logging.info("执行函数调用: name={}, arguments={}".format(func_name, arguments))
# 执行函数调用
ret = ""
try:
ret = funcmgr.execute_function(func_name, arguments)
logging.info("函数执行完成。")
except Exception as e:
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),
name=func_name
)
return {
"id": -1,
"choices": [
{
"index": -1,
"message": {
"role": "function",
"type": "function_return",
"function_name": func_name,
"content": json.dumps(ret, ensure_ascii=False)
},
"finish_reason": "function_return"
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
except funcmgr.ContentFunctionNotFoundError:
raise Exception("没有找到函数: {}".format(func_name))
+100
View File
@@ -0,0 +1,100 @@
import openai
from openai.types import completion, completion_choice
from . import model
class CompletionRequest(model.RequestBase):
"""调用Completion接口的请求类。
调用方可以一直next completion直到finish_reason为stop。
"""
model: str
prompt: str
kwargs: dict
stopped: bool = False
def __init__(
self,
client: openai.Client,
model: str,
messages: list[dict[str, str]],
**kwargs
):
self.client = client
self.model = model
self.prompt = ""
for message in messages:
self.prompt += message["role"] + ": " + message["content"] + "\n"
self.prompt += "assistant: "
self.kwargs = kwargs
self.req_func = self.client.completions.create
def __iter__(self):
return self
def __next__(self) -> dict:
"""调用Completion接口,返回生成的文本
{
"id": "id",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"type": "text",
"content": "message"
},
"finish_reason": "reason"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30
}
}
"""
if self.stopped:
raise StopIteration()
resp: completion.Completion = self._req(
model=self.model,
prompt=self.prompt,
**self.kwargs
)
if resp.choices[0].finish_reason == "stop":
self.stopped = True
choice0: completion_choice.CompletionChoice = resp.choices[0]
self.prompt += choice0.text
return {
"id": resp.id,
"choices": [
{
"index": choice0.index,
"message": {
"role": "assistant",
"type": "text",
"content": choice0.text
},
"finish_reason": choice0.finish_reason
}
],
"usage": {
"prompt_tokens": resp.usage.prompt_tokens,
"completion_tokens": resp.usage.completion_tokens,
"total_tokens": resp.usage.total_tokens
}
}
+40
View File
@@ -0,0 +1,40 @@
# 定义不同接口请求的模型
import logging
import openai
from ...utils import context
class RequestBase:
client: openai.Client
req_func: callable
def __init__(self, *args, **kwargs):
raise NotImplementedError
def _next_key(self):
switched, name = context.get_openai_manager().key_mgr.auto_switch()
logging.debug("切换api-key: switched={}, name={}".format(switched, name))
self.client.api_key = context.get_openai_manager().key_mgr.get_using_key()
def _req(self, **kwargs):
"""处理代理问题"""
logging.debug("请求接口参数: %s", str(kwargs))
config = context.get_config_manager().data
ret = self.req_func(**kwargs)
logging.debug("接口请求返回:%s", str(ret))
if config['switch_strategy'] == 'active':
self._next_key()
return ret
def __iter__(self):
raise self
def __next__(self):
raise NotImplementedError
+114 -101
View File
@@ -1,121 +1,134 @@
# 多情景预设值管理 # 多情景预设值管理
import json import json
import logging import logging
__current__ = "default"
"""当前默认使用的情景预设的名称
由管理员使用`!default <名称>`指令切换
"""
__prompts_from_files__ = {}
"""从文件中读取的情景预设值"""
__scenario_from_files__ = {}
def read_prompt_from_file():
"""从文件读取预设值"""
# 读取prompts/目录下的所有文件,以文件名为键,文件内容为值
# 保存在__prompts_from_files__中
global __prompts_from_files__
import os import os
__prompts_from_files__ = {} from ..utils import context
# __current__ = "default"
# """当前默认使用的情景预设的名称
# 由管理员使用`!default <名称>`命令切换
# """
# __prompts_from_files__ = {}
# """从文件中读取的情景预设值"""
# __scenario_from_files__ = {}
class ScenarioMode:
"""情景预设模式抽象类"""
using_prompt_name = "default"
"""新session创建时使用的prompt名称"""
prompts: dict[str, list] = {}
def __init__(self):
logging.debug("prompts: {}".format(self.prompts))
def list(self) -> dict[str, list]:
"""获取所有情景预设的名称及内容"""
return self.prompts
def get_prompt(self, name: str) -> tuple[list, str]:
"""获取指定情景预设的名称及内容"""
for key in self.prompts:
if key.startswith(name):
return self.prompts[key], key
raise Exception("没有找到情景预设: {}".format(name))
def set_using_name(self, name: str) -> str:
"""设置默认情景预设"""
for key in self.prompts:
if key.startswith(name):
self.using_prompt_name = key
return key
raise Exception("没有找到情景预设: {}".format(name))
def get_full_name(self, name: str) -> str:
"""获取完整的情景预设名称"""
for key in self.prompts:
if key.startswith(name):
return key
raise Exception("没有找到情景预设: {}".format(name))
def get_using_name(self) -> str:
"""获取默认情景预设"""
return self.using_prompt_name
class NormalScenarioMode(ScenarioMode):
"""普通情景预设模式"""
def __init__(self):
config = context.get_config_manager().data
# 加载config中的default_prompt值
if type(config['default_prompt']) == str:
self.using_prompt_name = "default"
self.prompts = {"default": [
{
"role": "system",
"content": config['default_prompt']
}
]}
elif type(config['default_prompt']) == dict:
for key in config['default_prompt']:
self.prompts[key] = [
{
"role": "system",
"content": config['default_prompt'][key]
}
]
# 从prompts/目录下的文件中载入
# 遍历文件
for file in os.listdir("prompts"): for file in os.listdir("prompts"):
with open(os.path.join("prompts", file), encoding="utf-8") as f: with open(os.path.join("prompts", file), encoding="utf-8") as f:
__prompts_from_files__[file] = f.read() self.prompts[file] = [
{
"role": "system",
"content": f.read()
}
]
def read_scenario_from_file(): class FullScenarioMode(ScenarioMode):
"""从JSON文件读取情景预设""" """完整情景预设模式"""
global __scenario_from_files__
import os
__scenario_from_files__ = {} def __init__(self):
"""从json读取所有"""
# 遍历scenario/目录下的所有文件,以文件名为键,文件内容中的prompt为值
for file in os.listdir("scenario"): for file in os.listdir("scenario"):
if file == "default-template.json": if file == "default-template.json":
continue continue
with open(os.path.join("scenario", file), encoding="utf-8") as f: with open(os.path.join("scenario", file), encoding="utf-8") as f:
__scenario_from_files__[file] = json.load(f) self.prompts[file] = json.load(f)["prompt"]
super().__init__()
def get_prompt_dict() -> dict: scenario_mode_mapping = {}
"""获取预设值字典""" """情景预设模式名称与对象的映射"""
import config
default_prompt = config.default_prompt
if type(default_prompt) == str:
default_prompt = {"default": default_prompt}
elif type(default_prompt) == dict:
pass
else:
raise TypeError("default_prompt must be str or dict")
# 将文件中的预设值合并到default_prompt中
for key in __prompts_from_files__:
default_prompt[key] = __prompts_from_files__[key]
return default_prompt
def set_current(name): def register_all():
global __current__ """注册所有情景预设模式,不使用装饰器,因为装饰器的方式不支持热重载"""
for key in get_prompt_dict(): global scenario_mode_mapping
if key.lower().startswith(name.lower()): scenario_mode_mapping = {
__current__ = key "normal": NormalScenarioMode(),
return "full_scenario": FullScenarioMode()
raise KeyError("未找到情景预设: " + name)
def get_current():
global __current__
return __current__
def set_to_default():
global __current__
default_dict = get_prompt_dict()
if "default" in default_dict:
__current__ = "default"
else:
__current__ = list(default_dict.keys())[0]
def get_prompt(name: str = None) -> list:
global __scenario_from_files__
import config
preset_mode = config.preset_mode
"""获取预设值"""
if name is None:
name = get_current()
# JSON预设方式
if preset_mode == 'full_scenario':
import os
for key in __scenario_from_files__:
if key.lower().startswith(name.lower()):
logging.debug('成功加载情景预设从JSON文件: {}'.format(key))
return __scenario_from_files__[key]['prompt']
# 默认预设方式
elif preset_mode == 'default':
default_dict = get_prompt_dict()
for key in default_dict:
if key.lower().startswith(name.lower()):
return [
{
"role": "user",
"content": default_dict[key]
},
{
"role": "assistant",
"content": "好的。"
} }
]
raise KeyError("未找到默认情景预设: " + name)
def mode_inst() -> ScenarioMode:
"""获取指定名称的情景预设模式对象"""
config = context.get_config_manager().data
if config['preset_mode'] == "default":
config['preset_mode'] = "normal"
return scenario_mode_mapping[config['preset_mode']]
+46
View File
@@ -0,0 +1,46 @@
# 封装了function calling的一些支持函数
import logging
from ..plugin import host
class ContentFunctionNotFoundError(Exception):
pass
def get_func_schema_list() -> list:
"""从plugin包中的函数结构中获取并处理成受GPT支持的格式"""
if not host.__enable_content_functions__:
return []
schemas = []
for func in host.__callable_functions__:
if func['enabled']:
fun_cp = func.copy()
del fun_cp['enabled']
schemas.append(fun_cp)
return schemas
def get_func(name: str) -> callable:
if name not in host.__function_inst_map__:
raise ContentFunctionNotFoundError("没有找到内容函数: {}".format(name))
return host.__function_inst_map__[name]
def get_func_schema(name: str) -> dict:
for func in host.__callable_functions__:
if func['name'] == name:
return func
raise ContentFunctionNotFoundError("没有找到内容函数: {}".format(name))
def execute_function(name: str, kwargs: dict) -> any:
"""执行函数调用"""
logging.debug("executing function: name='{}', kwargs={}".format(name, kwargs))
func = get_func(name)
return func(**kwargs)
+31 -19
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:
@@ -11,8 +11,7 @@ class KeysManager:
"""所有api-key""" """所有api-key"""
using_key = "" using_key = ""
"""当前使用的api-key """当前使用的api-key"""
"""
alerted = [] alerted = []
"""已提示过超额的key """已提示过超额的key
@@ -34,32 +33,42 @@ class KeysManager:
def __init__(self, api_key): def __init__(self, api_key):
if type(api_key) is dict: assert type(api_key) == dict
self.api_key = api_key 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]
# 从usage中删除未加载的api-key的记录 # 从usage中删除未加载的api-key的记录
# 不删了,也许会运行时添加曾经有记录的api-key # 不删了,也许会运行时添加曾经有记录的api-key
self.auto_switch() self.auto_switch()
def auto_switch(self) -> (bool, str): def auto_switch(self) -> tuple[bool, str]:
"""尝试切换api-key """尝试切换api-key
Returns: Returns:
是否切换成功, 切换后的api-key的别名 是否切换成功, 切换后的api-key的别名
""" """
index = 0
for key_name in self.api_key: for key_name in self.api_key:
if self.api_key[key_name] == self.using_key:
break
index += 1
# 从当前key开始向后轮询
start_index = index
index += 1
if index >= len(self.api_key):
index = 0
while index != start_index:
key_name = list(self.api_key.keys())[index]
if self.api_key[key_name] not in self.exceeded: if self.api_key[key_name] not in self.exceeded:
self.using_key = self.api_key[key_name] self.using_key = self.api_key[key_name]
logging.info("使用api-key:" + key_name) logging.debug("使用api-key:" + key_name)
# 触发插件事件 # 触发插件事件
args = { args = {
@@ -70,17 +79,20 @@ class KeysManager:
return True, key_name return True, key_name
self.using_key = list(self.api_key.values())[0] index += 1
logging.info("使用api-key:" + list(self.api_key.keys())[0]) if index >= len(self.api_key):
index = 0
return False, "" self.using_key = list(self.api_key.values())[start_index]
logging.debug("使用api-key:" + list(self.api_key.keys())[start_index])
return False, list(self.api_key.keys())[start_index]
def add(self, key_name, key): def add(self, key_name, key):
self.api_key[key_name] = key self.api_key[key_name] = key
def set_current_exceeded(self): def set_current_exceeded(self):
"""设置当前使用的api-key使用量超限 """设置当前使用的api-key使用量超限"""
"""
self.exceeded.append(self.using_key) self.exceeded.append(self.using_key)
def get_key_name(self, api_key): def get_key_name(self, api_key):
+42 -45
View File
@@ -1,11 +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 ModelRequest, create_openai_model_request from ..openai import modelmgr
from ..openai.api import model as api_model
class OpenAIInteract: class OpenAIInteract:
@@ -14,62 +16,57 @@ 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",
} }
client: openai.Client = None
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())
openai.api_key = self.key_mgr.get_using_key() self.client = openai.Client(
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)
# 请求OpenAI Completion def request_completion(self, messages: list):
def request_completion(self, prompts) -> str: """请求补全接口回复=
"""请求补全接口回复
Parameters:
prompts (str): 提示语
Returns:
str: 回复
""" """
# 选择接口请求类
config = context.get_config_manager().data
config = pkg.utils.context.get_config() request: api_model.RequestBase
# 根据模型选择使用的接口 model: str = config['completion_api_params']['model']
ai: ModelRequest = create_openai_model_request(
config.completion_api_params['model'], cp_parmas = config['completion_api_params'].copy()
'user', del cp_parmas['model']
config.openai_config["http_proxy"] if "http_proxy" in config.openai_config else None
request = modelmgr.select_request_cls(self.client, model, messages, cp_parmas)
# 请求接口
for resp in request:
if resp['usage']['total_tokens'] > 0:
self.audit_mgr.report_text_model_usage(
model,
resp['usage']['total_tokens']
) )
ai.request(
prompts,
**config.completion_api_params
)
response = ai.get_response()
logging.debug("OpenAI response: %s", response) yield resp
if 'model' in config.completion_api_params: def request_image(self, prompt) -> images_response.ImagesResponse:
self.audit_mgr.report_text_model_usage(config.completion_api_params['model'],
ai.get_total_tokens())
elif 'engine' in config.completion_api_params:
self.audit_mgr.report_text_model_usage(config.completion_api_params['engine'],
response['usage']['total_tokens'])
return ai.get_message()
def request_image(self, prompt) -> dict:
"""请求图片接口回复 """请求图片接口回复
Parameters: Parameters:
@@ -78,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 if hasattr(config, "image_api_params") else self.default_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
+113 -158
View File
@@ -5,22 +5,44 @@ ChatCompletion - gpt-3.5-turbo 等模型
Completion - text-davinci-003 等模型 Completion - text-davinci-003 等模型
此模块封装此两个接口的请求实现,为上层提供统一的调用方式 此模块封装此两个接口的请求实现,为上层提供统一的调用方式
""" """
import openai, logging, threading, asyncio import tiktoken
import openai.error as aiE import openai
from ..openai.api import model as api_model
from ..openai.api import completion as api_completion
from ..openai.api import chat_completion as api_chat_completion
COMPLETION_MODELS = { COMPLETION_MODELS = {
'text-davinci-003', "gpt-3.5-turbo-instruct",
'text-davinci-002',
'code-davinci-002',
'code-cushman-001',
'text-curie-001',
'text-babbage-001',
'text-ada-001',
} }
CHAT_COMPLETION_MODELS = { CHAT_COMPLETION_MODELS = {
'gpt-3.5-turbo', # GPT 4 系列
'gpt-3.5-turbo-0301', "gpt-4-1106-preview",
"gpt-4-vision-preview",
"gpt-4",
"gpt-4-32k",
"gpt-4-0613",
"gpt-4-32k-0613",
"gpt-4-0314", # legacy
"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 接入
"SparkDesk",
"chatglm_pro",
"chatglm_std",
"chatglm_lite",
"qwen-v1",
"qwen-plus-v1",
"ERNIE-Bot",
"ERNIE-Bot-turbo",
"gemini-pro",
} }
EDIT_MODELS = { EDIT_MODELS = {
@@ -32,153 +54,86 @@ IMAGE_MODELS = {
} }
class ModelRequest: def select_request_cls(client: openai.Client, model_name: str, messages: list, args: dict) -> api_model.RequestBase:
"""模型接口请求父类"""
can_chat = False
runtime: threading.Thread = None
ret = {}
proxy: str = None
request_ready = True
error_info: str = "若在没有任何错误的情况下看到这句话,请带着配置文件上报Issues"
def __init__(self, model_name, user_name, request_fun, http_proxy:str = None, time_out = None):
self.model_name = model_name
self.user_name = user_name
self.request_fun = request_fun
self.time_out = time_out
if http_proxy != None:
self.proxy = http_proxy
openai.proxy = self.proxy
self.request_ready = False
async def __a_request__(self, **kwargs):
"""异步请求"""
try:
self.ret:dict = await self.request_fun(**kwargs)
self.request_ready = True
except aiE.APIConnectionError as e:
self.error_info = "{}\n请检查网络连接或代理是否正常".format(e)
raise ConnectionError(self.error_info)
except ValueError as e:
self.error_info = "{}\n该错误可能是由于http_proxy格式设置错误引起的"
except Exception as e:
self.error_info = "{}\n由于请求异常产生的未知错误,请查看日志".format(e)
raise Exception(self.error_info)
def request(self, **kwargs):
"""向接口发起请求"""
if self.proxy != None: #异步请求
self.request_ready = False
loop = asyncio.new_event_loop()
self.runtime = threading.Thread(
target=loop.run_until_complete,
args=(self.__a_request__(**kwargs),)
)
self.runtime.start()
else: #同步请求
self.ret = self.request_fun(**kwargs)
def __msg_handle__(self, msg):
"""将prompt dict转换成接口需要的格式"""
return msg
def ret_handle(self):
'''
API消息返回处理函数
若重写该方法,应检查异步线程状态,或在需要检查处super该方法
'''
if self.runtime != None and isinstance(self.runtime, threading.Thread):
self.runtime.join(self.time_out)
if self.request_ready:
return
raise Exception(self.error_info)
def get_total_tokens(self):
try:
return self.ret['usage']['total_tokens']
except:
return 0
def get_message(self):
return self.message
def get_response(self):
return self.ret
class ChatCompletionModel(ModelRequest):
"""ChatCompletion接口的请求实现"""
Chat_role = ['system', 'user', 'assistant']
def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
if http_proxy == None:
request_fun = openai.ChatCompletion.create
else:
request_fun = openai.ChatCompletion.acreate
self.can_chat = True
super().__init__(model_name, user_name, request_fun, http_proxy, **kwargs)
def request(self, prompts, **kwargs):
prompts = self.__msg_handle__(prompts)
kwargs['messages'] = prompts
super().request(**kwargs)
self.ret_handle()
def __msg_handle__(self, msgs):
temp_msgs = []
# 把msgs拷贝进temp_msgs
for msg in msgs:
temp_msgs.append(msg.copy())
return temp_msgs
def get_message(self):
return self.ret["choices"][0]["message"]['content'] #需要时直接加载加快请求速度,降低内存消耗
class CompletionModel(ModelRequest):
"""Completion接口的请求实现"""
def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
if http_proxy == None:
request_fun = openai.Completion.create
else:
request_fun = openai.Completion.acreate
super().__init__(model_name, user_name, request_fun, http_proxy, **kwargs)
def request(self, prompts, **kwargs):
prompts = self.__msg_handle__(prompts)
kwargs['prompt'] = prompts
super().request(**kwargs)
self.ret_handle()
def __msg_handle__(self, msgs):
prompt = ''
for msg in msgs:
prompt = prompt + "{}: {}\n".format(msg['role'], msg['content'])
# for msg in msgs:
# if msg['role'] == 'assistant':
# prompt = prompt + "{}\n".format(msg['content'])
# else:
# prompt = prompt + "{}:{}\n".format(msg['role'] , msg['content'])
prompt = prompt + "assistant: "
return prompt
def get_message(self):
return self.ret["choices"][0]["text"]
def create_openai_model_request(model_name: str, user_name: str = 'user', http_proxy:str = None) -> ModelRequest:
"""使用给定的模型名称创建模型请求对象"""
if model_name in CHAT_COMPLETION_MODELS: if model_name in CHAT_COMPLETION_MODELS:
model = ChatCompletionModel(model_name, user_name, http_proxy) return api_chat_completion.ChatCompletionRequest(client, model_name, messages, **args)
elif model_name in COMPLETION_MODELS: elif model_name in COMPLETION_MODELS:
model = CompletionModel(model_name, user_name, http_proxy) return api_completion.CompletionRequest(client, model_name, messages, **args)
raise ValueError("不支持模型[{}],请检查配置文件".format(model_name))
def count_chat_completion_tokens(messages: list, model: str) -> int:
"""Return the number of tokens used by a list of messages."""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
print("Warning: model not found. Using cl100k_base encoding.")
encoding = tiktoken.get_encoding("cl100k_base")
if model in {
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"gpt-4-0314",
"gpt-4-32k-0314",
"gpt-4-0613",
"gpt-4-32k-0613",
"SparkDesk",
"chatglm_pro",
"chatglm_std",
"chatglm_lite",
"qwen-v1",
"qwen-plus-v1",
"ERNIE-Bot",
"ERNIE-Bot-turbo",
"gemini-pro",
}:
tokens_per_message = 3
tokens_per_name = 1
elif model == "gpt-3.5-turbo-0301":
tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
tokens_per_name = -1 # if there's a name, the role is omitted
elif "gpt-3.5-turbo" in model:
# print("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.")
return count_chat_completion_tokens(messages, model="gpt-3.5-turbo-0613")
elif "gpt-4" in model:
# print("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
return count_chat_completion_tokens(messages, model="gpt-4-0613")
else: else:
log = "找不到模型[{}],请检查配置文件".format(model_name) raise NotImplementedError(
logging.error(log) f"""count_chat_completion_tokens() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens."""
raise IndexError(log) )
logging.debug("使用接口[{}]创建模型请求[{}]".format(model.__class__.__name__, model_name)) num_tokens = 0
return model for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
return num_tokens
def count_completion_tokens(messages: list, model: str) -> int:
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
print("Warning: model not found. Using cl100k_base encoding.")
encoding = tiktoken.get_encoding("cl100k_base")
text = ""
for message in messages:
text += message['role'] + message['content'] + "\n"
text += "assistant: "
return len(encoding.encode(text))
def count_tokens(messages: list, model: str):
if model in CHAT_COMPLETION_MODELS:
return count_chat_completion_tokens(messages, model)
elif model in COMPLETION_MODELS:
return count_completion_tokens(messages, model)
raise ValueError("不支持模型[{}],请检查配置文件".format(model))
-28
View File
@@ -1,28 +0,0 @@
# 计费模块
# 已弃用 https://github.com/RockChinQ/QChatGPT/issues/81
import logging
pricing = {
"base": { # 文字模型单位是1000字符
"text-davinci-003": 0.02,
},
"image": {
"256x256": 0.016,
"512x512": 0.018,
"1024x1024": 0.02,
}
}
def language_base_price(model, text):
salt_rate = 0.93
length = ((len(text.encode('utf-8')) - len(text)) / 2 + len(text)) * salt_rate
logging.debug("text length: %d" % length)
return pricing["base"][model] * length / 1000
def image_price(size):
logging.debug("image size: %s" % size)
return pricing["image"][size]
+232 -108
View File
@@ -8,13 +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
# 运行时保存的所有session # 运行时保存的所有session
sessions = {} sessions = {}
@@ -25,56 +25,27 @@ class SessionOfflineStatus:
EXPLICITLY_CLOSED = 'explicitly_closed' 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 = pkg.utils.context.get_config()
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 # 从数据加载session
def load_sessions(): def load_sessions():
"""从数据库加载sessions""" """从数据库加载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()
for session_name in session_data: for session_name in session_data:
logging.info('加载session: {}'.format(session_name)) logging.debug('加载session: {}'.format(session_name))
temp_session = Session(session_name) temp_session = Session(session_name)
temp_session.name = session_name temp_session.name = session_name
temp_session.create_timestamp = session_data[session_name]['create_timestamp'] temp_session.create_timestamp = session_data[session_name]['create_timestamp']
temp_session.last_interact_timestamp = session_data[session_name]['last_interact_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.prompt = json.loads(session_data[session_name]['prompt'])
except Exception: temp_session.token_counts = json.loads(session_data[session_name]['token_counts'])
temp_session.prompt = reset_session_prompt(session_name, session_data[session_name]['prompt'])
temp_session.persistence()
temp_session.default_prompt = json.loads(session_data[session_name]['default_prompt']) if \ temp_session.default_prompt = json.loads(session_data[session_name]['default_prompt']) if \
session_data[session_name]['default_prompt'] else [] session_data[session_name]['default_prompt'] else []
@@ -82,7 +53,7 @@ def load_sessions():
# 获取指定名称的session,如果不存在则创建一个新的 # 获取指定名称的session,如果不存在则创建一个新的
def get_session(session_name: str): def get_session(session_name: str) -> 'Session':
global sessions global sessions
if session_name not in sessions: if session_name not in sessions:
sessions[session_name] = Session(session_name) sessions[session_name] = Session(session_name)
@@ -137,15 +108,17 @@ class Session:
import pkg.openai.dprompt as dprompt import pkg.openai.dprompt as dprompt
if use_default is None: if use_default is None:
use_default = dprompt.get_current() use_default = dprompt.mode_inst().get_using_name()
current_default_prompt = dprompt.get_prompt(use_default) current_default_prompt, _ = dprompt.mode_inst().get_prompt(use_default)
return current_default_prompt return current_default_prompt
def __init__(self, name: str): def __init__(self, name: str):
self.name = name self.name = name
self.create_timestamp = int(time.time()) self.create_timestamp = int(time.time())
self.last_interact_timestamp = int(time.time()) self.last_interact_timestamp = int(time.time())
self.prompt = []
self.token_counts = []
self.schedule() self.schedule()
self.response_lock = threading.Lock() self.response_lock = threading.Lock()
@@ -167,17 +140,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
@@ -189,8 +162,15 @@ class Session:
# 请求回复 # 请求回复
# 这个函数是阻塞的 # 这个函数是阻塞的
def append(self, text: str) -> str: def query(self, text: str=None) -> tuple[str, str, list[str]]:
"""向session中添加一条消息,返回接口回复""" """向session中添加一条消息,返回接口回复
Args:
text (str): 用户消息
Returns:
tuple[str, str]: (接口回复, finish_reason, 已调用的函数列表)
"""
self.last_interact_timestamp = int(time.time()) self.last_interact_timestamp = int(time.time())
@@ -202,37 +182,166 @@ 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 return None, None, None
config = pkg.utils.context.get_config() config = context.get_config_manager().data
max_length = config.prompt_submit_length if hasattr(config, "prompt_submit_length") else 1024 max_length = config['prompt_submit_length']
# 向API请求补全 local_default_prompt = self.default_prompt.copy()
message = pkg.utils.context.get_openai_manager().request_completion( local_prompt = self.prompt.copy()
self.cut_out(text, max_length),
# 触发PromptPreProcessing事件
args = {
'session_name': self.name,
'default_prompt': self.default_prompt,
'prompt': self.prompt,
'text_message': text,
}
event = plugin_host.emit(plugin_models.PromptPreProcessing, **args)
if event.get_return_value('default_prompt') is not None:
local_default_prompt = event.get_return_value('default_prompt')
if event.get_return_value('prompt') is not None:
local_prompt = event.get_return_value('prompt')
if event.get_return_value('text_message') is not None:
text = event.get_return_value('text_message')
# 裁剪messages到合适长度
prompts, _ = self.cut_out(text, max_length, local_default_prompt, local_prompt)
res_text = ""
pending_msgs = []
total_tokens = 0
finish_reason: str = ""
funcs = []
trace_func_calls = config['trace_function_calls']
botmgr = context.get_qqbot_manager()
session_name_spt: list[str] = self.name.split("_")
pending_res_text = ""
start_time = time.time()
# TODO 对不起,我知道这样非常非常屎山,但我之后会重构的
for resp in context.get_openai_manager().request_completion(prompts):
if pending_res_text != "":
botmgr.adapter.send_message(
session_name_spt[0],
session_name_spt[1],
pending_res_text
)
pending_res_text = ""
finish_reason = resp['choices'][0]['finish_reason']
if resp['choices'][0]['message']['role'] == "assistant" and resp['choices'][0]['message']['content'] != None: # 包含纯文本响应
if not trace_func_calls:
res_text += resp['choices'][0]['message']['content']
else:
res_text = resp['choices'][0]['message']['content']
pending_res_text = resp['choices'][0]['message']['content']
total_tokens += resp['usage']['total_tokens']
msg = {
"role": "assistant",
"content": resp['choices'][0]['message']['content']
}
if 'function_call' in resp['choices'][0]['message']:
msg['function_call'] = json.dumps(resp['choices'][0]['message']['function_call'])
pending_msgs.append(msg)
if resp['choices'][0]['message']['type'] == 'function_call':
# self.prompt.append(
# {
# "role": "assistant",
# "content": "function call: "+json.dumps(resp['choices'][0]['message']['function_call'])
# }
# )
if trace_func_calls:
botmgr.adapter.send_message(
session_name_spt[0],
session_name_spt[1],
"调用函数 "+resp['choices'][0]['message']['function_call']['name'] + "..."
) )
# 成功获取,处理回复 total_tokens += resp['usage']['total_tokens']
res_test = message elif resp['choices'][0]['message']['type'] == 'function_return':
res_ans = res_test # self.prompt.append(
# {
# "role": "function",
# "name": resp['choices'][0]['message']['function_name'],
# "content": json.dumps(resp['choices'][0]['message']['content'])
# }
# )
# 去除开头可能的提示 # total_tokens += resp['usage']['total_tokens']
res_ans_spt = res_test.split("\n\n") funcs.append(
if len(res_ans_spt) > 1: resp['choices'][0]['message']['function_name']
del (res_ans_spt[0]) )
res_ans = '\n\n'.join(res_ans_spt) pass
# 向API请求补全
# message, total_token = pkg.utils.context.get_openai_manager().request_completion(
# prompts,
# )
# 成功获取,处理回复
# res_test = message
res_ans = res_text.strip()
# 将此次对话的双方内容加入到prompt中 # 将此次对话的双方内容加入到prompt中
# self.prompt.append({'role': 'user', 'content': text})
# self.prompt.append({'role': 'assistant', 'content': res_ans})
if text:
self.prompt.append({'role': 'user', 'content': text}) self.prompt.append({'role': 'user', 'content': text})
self.prompt.append({'role': 'assistant', 'content': res_ans}) # 添加pending_msgs
self.prompt += pending_msgs
# 向token_counts中添加本回合的token数量
# self.token_counts.append(total_tokens-total_token_before_query)
# logging.debug("本回合使用token: {}, session counts: {}".format(total_tokens-total_token_before_query, self.token_counts))
if self.just_switched_to_exist_session: if self.just_switched_to_exist_session:
self.just_switched_to_exist_session = False self.just_switched_to_exist_session = False
self.set_ongoing() self.set_ongoing()
return res_ans if res_ans[0] != '\n' else res_ans[1:] # 上报使用量数据
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
# 删除上一回合并返回上一回合的问题 # 删除上一回合并返回上一回合的问题
def undo(self) -> str: def undo(self) -> str:
@@ -244,46 +353,61 @@ class Session:
question = self.prompt[-2]['content'] question = self.prompt[-2]['content']
self.prompt = self.prompt[:-2] self.prompt = self.prompt[:-2]
self.token_counts = self.token_counts[:-1]
# 返回上一回合的问题 # 返回上一回合的问题
return question return question
# 构建对话体 # 构建对话体
def cut_out(self, msg: str, max_tokens: int) -> list: def cut_out(self, msg: str, max_tokens: int, default_prompt: list, prompt: list) -> tuple[list, list]:
"""将现有prompt进行切割处理,使得新的prompt长度不超过max_tokens""" """将现有prompt进行切割处理,使得新的prompt长度不超过max_tokens
# 如果用户消息长度超过max_tokens,直接返回
temp_prompt: list = [] :return: (新的prompt, 新的token_counts)
temp_prompt += self.default_prompt """
temp_prompt.append(
# 最终由三个部分组成
# - default_prompt 情景预设固定值
# - changable_prompts 可变部分, 此会话中的历史对话回合
# - current_question 当前问题
# 包装目前的对话回合内容
changable_prompts = []
use_model = context.get_config_manager().data['completion_api_params']['model']
ptr = len(prompt) - 1
# 直接从后向前扫描拼接,不管是否是整回合
while ptr >= 0:
if openai_modelmgr.count_tokens(prompt[ptr:ptr+1]+changable_prompts, use_model) > max_tokens:
break
changable_prompts.insert(0, prompt[ptr])
ptr -= 1
# 将default_prompt和changable_prompts合并
result_prompt = default_prompt + changable_prompts
# 添加当前问题
if msg:
result_prompt.append(
{ {
'role': 'user', 'role': 'user',
'content': msg 'content': msg
} }
) )
token_count = 0 logging.debug("cut_out: {}".format(json.dumps(result_prompt, ensure_ascii=False, indent=4)))
for item in temp_prompt:
token_count += len(item['content'])
# 倒序遍历prompt return result_prompt, openai_modelmgr.count_tokens(changable_prompts, use_model)
for i in range(len(self.prompt) - 1, -1, -1):
if token_count >= max_tokens:
break
# 将prompt加到temp_prompt倒数第二个位置
temp_prompt.insert(len(self.default_prompt), self.prompt[i])
token_count += len(self.prompt[i]['content'])
logging.debug('cut_out: {}'.format(json.dumps(temp_prompt, ensure_ascii=False, indent=4)))
return temp_prompt
# 持久化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('_')
@@ -291,10 +415,10 @@ class Session:
subject_number = int(name_spt[1]) subject_number = int(name_spt[1])
db_inst.persistence_session(subject_type, subject_number, self.create_timestamp, self.last_interact_timestamp, db_inst.persistence_session(subject_type, subject_number, self.create_timestamp, self.last_interact_timestamp,
json.dumps(self.prompt), json.dumps(self.default_prompt)) json.dumps(self.prompt), json.dumps(self.default_prompt), json.dumps(self.token_counts))
# 重置session # 重置session
def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True, use_prompt: str = None): def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True, use_prompt: str = None, persist: bool = False):
if self.prompt: if self.prompt:
self.persistence() self.persistence()
if explicit: if explicit:
@@ -305,15 +429,17 @@ 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
self.default_prompt = self.get_default_prompt(use_prompt) self.default_prompt = self.get_default_prompt(use_prompt)
self.prompt = [] self.prompt = []
self.token_counts = []
self.create_timestamp = int(time.time()) self.create_timestamp = int(time.time())
self.last_interact_timestamp = int(time.time()) self.last_interact_timestamp = int(time.time())
self.just_switched_to_exist_session = False self.just_switched_to_exist_session = False
@@ -325,11 +451,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:
@@ -337,11 +463,10 @@ class Session:
self.create_timestamp = last_one['create_timestamp'] self.create_timestamp = last_one['create_timestamp']
self.last_interact_timestamp = last_one['last_interact_timestamp'] self.last_interact_timestamp = last_one['last_interact_timestamp']
try:
self.prompt = json.loads(last_one['prompt']) self.prompt = json.loads(last_one['prompt'])
except json.decoder.JSONDecodeError: self.token_counts = json.loads(last_one['token_counts'])
self.prompt = reset_session_prompt(self.name, last_one['prompt'])
self.persistence()
self.default_prompt = json.loads(last_one['default_prompt']) if last_one['default_prompt'] else [] self.default_prompt = json.loads(last_one['default_prompt']) if last_one['default_prompt'] else []
self.just_switched_to_exist_session = True self.just_switched_to_exist_session = True
@@ -349,7 +474,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:
@@ -357,24 +482,23 @@ class Session:
self.create_timestamp = next_one['create_timestamp'] self.create_timestamp = next_one['create_timestamp']
self.last_interact_timestamp = next_one['last_interact_timestamp'] self.last_interact_timestamp = next_one['last_interact_timestamp']
try:
self.prompt = json.loads(next_one['prompt']) self.prompt = json.loads(next_one['prompt'])
except json.decoder.JSONDecodeError: self.token_counts = json.loads(next_one['token_counts'])
self.prompt = reset_session_prompt(self.name, next_one['prompt'])
self.persistence()
self.default_prompt = json.loads(next_one['default_prompt']) if next_one['default_prompt'] else [] self.default_prompt = json.loads(next_one['default_prompt']) if next_one['default_prompt'] else []
self.just_switched_to_exist_session = True self.just_switched_to_exist_session = True
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)
+311 -58
View File
@@ -5,17 +5,26 @@ import importlib
import os import os
import pkgutil import pkgutil
import sys import sys
import shutil
import traceback import traceback
import time
import re
import pkg.utils.context as context from ..utils import updater as updater
import pkg.plugin.switch as switch from ..utils import network as network
import pkg.plugin.settings as settings from ..utils import context as context
from ..plugin import switch as switch
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
__plugins__ = {} __plugins__ = {}
""" """插件列表
插件列表
示例: 示例:
{ {
@@ -34,11 +43,21 @@ __plugins__ = {}
}, },
"instance": None "instance": None
} }
}""" }
"""
__plugins_order__ = [] __plugins_order__ = []
"""插件顺序""" """插件顺序"""
__enable_content_functions__ = True
"""是否启用内容函数"""
__callable_functions__ = []
"""供GPT调用的函数结构"""
__function_inst_map__: dict[str, callable] = {}
"""函数名:实例 映射"""
def generate_plugin_order(): def generate_plugin_order():
"""根据__plugin__生成插件初始顺序,无视是否启用""" """根据__plugin__生成插件初始顺序,无视是否启用"""
@@ -51,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]
@@ -63,31 +84,42 @@ def iter_plugins_name():
__current_module_path__ = "" __current_module_path__ = ""
def walk_plugin_path(module, prefix='', path_prefix=''): def walk_plugin_path(module, prefix="", path_prefix=""):
global __current_module_path__ global __current_module_path__
"""遍历插件路径""" """遍历插件路径"""
for item in pkgutil.iter_modules(module.__path__): for item in pkgutil.iter_modules(module.__path__):
if item.ispkg: if item.ispkg:
logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name)) logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name))
walk_plugin_path(__import__(module.__name__ + '.' + item.name, fromlist=['']), walk_plugin_path(
prefix + item.name + '.', path_prefix + item.name + '/') __import__(module.__name__ + "." + item.name, fromlist=[""]),
prefix + item.name + ".",
path_prefix + item.name + "/",
)
else: else:
try: try:
logging.debug("扫描插件模块: plugins/{}".format(path_prefix + item.name + '.py')) logging.debug(
__current_module_path__ = "plugins/"+path_prefix + item.name + '.py' "扫描插件模块: plugins/{}".format(path_prefix + item.name + ".py")
)
__current_module_path__ = "plugins/" + path_prefix + item.name + ".py"
importlib.import_module(module.__name__ + '.' + item.name) importlib.import_module(module.__name__ + "." + item.name)
logging.info('加载模块: plugins/{} 成功'.format(path_prefix + item.name + '.py')) logging.debug(
"加载模块: plugins/{} 成功".format(path_prefix + item.name + ".py")
)
except: 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() traceback.print_exc()
def load_plugins(): def load_plugins():
"""加载插件""" """加载插件"""
logging.info("加载插件") logging.debug("加载插件")
PluginHost() PluginHost()
walk_plugin_path(__import__('plugins')) walk_plugin_path(__import__("plugins"))
logging.debug(__plugins__) logging.debug(__plugins__)
@@ -99,25 +131,40 @@ 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("function instance map: {}".format(__function_inst_map__))
# 迁移插件源地址记录
metadata.do_plugin_git_repo_migrate()
def initialize_plugins(): def initialize_plugins():
"""初始化插件""" """初始化插件"""
logging.info("初始化插件") logging.debug("初始化插件")
import pkg.plugin.models as models import pkg.plugin.models as models
successfully_initialized_plugins = []
for plugin in iter_plugins(): for plugin in iter_plugins():
if not plugin['enabled']: # if not plugin['enabled']:
continue # continue
try: try:
models.__current_registering_plugin__ = plugin['name'] models.__current_registering_plugin__ = plugin["name"]
plugin['instance'] = plugin["class"](plugin_host=context.get_plugin_host()) plugin["instance"] = plugin["class"](plugin_host=context.get_plugin_host())
logging.info("插件 {} 已初始化".format(plugin['name'])) # logging.info("插件 {} 已初始化".format(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)))
def unload_plugins(): def unload_plugins():
""" 卸载插件 """卸载插件"""
"""
# 不再显式卸载插件,因为当程序结束时,插件的析构函数会被系统执行 # 不再显式卸载插件,因为当程序结束时,插件的析构函数会被系统执行
# for plugin in __plugins__.values(): # for plugin in __plugins__.values():
# if plugin['enabled'] and plugin['instance'] is not None: # if plugin['enabled'] and plugin['instance'] is not None:
@@ -132,36 +179,216 @@ 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(
except ModuleNotFoundError: r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)",
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77") repo_url,
)
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")
import main pkg.utils.pkgmgr.install_requirements(path + "/requirements.txt")
main.reset_logging()
import pkg.utils.log as log
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")
# 上报安装记录
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("\\", "/")
# 剪切路径为plugins/插件名
plugin_path = plugin_path.split("plugins/")[1].split("/")[0]
# 删除文件夹
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):
"""更新插件"""
# 检查是否有远程地址记录
plugin_path_name = get_plugin_path_name_by_plugin_name(plugin_name)
meta = metadata.get_plugin_metadata(plugin_path_name)
if meta == {}:
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("正在重新安装插件以进行更新...")
install_plugin(remote_url)
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":
return k
return None
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 = plugin_main_module_path.replace("\\", "/")
spt = plugin_main_module_path.split("/")
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: class EventContext:
"""事件上下文""" """事件上下文"""
eid = 0 eid = 0
"""事件编号""" """事件编号"""
@@ -196,7 +423,7 @@ class EventContext:
self.__return_value__[key] = [] self.__return_value__[key] = []
self.__return_value__[key].append(ret) self.__return_value__[key].append(ret)
def get_return(self, key: str): def get_return(self, key: str) -> list:
"""获取key的所有返回值""" """获取key的所有返回值"""
if key in self.__return_value__: if key in self.__return_value__:
return self.__return_value__[key] return self.__return_value__[key]
@@ -236,6 +463,7 @@ class EventContext:
def emit(event_name: str, **kwargs) -> EventContext: def emit(event_name: str, **kwargs) -> EventContext:
"""触发事件""" """触发事件"""
import pkg.utils.context as context import pkg.utils.context as context
if context.get_plugin_host() is None: if context.get_plugin_host() is None:
return None return None
return context.get_plugin_host().emit(event_name, **kwargs) return context.get_plugin_host().emit(event_name, **kwargs)
@@ -245,7 +473,9 @@ class PluginHost:
"""插件宿主""" """插件宿主"""
def __init__(self): def __init__(self):
"""初始化插件宿主"""
context.set_plugin_host(self) context.set_plugin_host(self)
self.calling_gpt_session = Session([])
def get_runtime_context(self) -> context: def get_runtime_context(self) -> context:
"""获取运行时上下文(pkg.utils.context模块的对象) """获取运行时上下文(pkg.utils.context模块的对象)
@@ -260,13 +490,17 @@ class PluginHost:
"""获取机器人对象""" """获取机器人对象"""
return context.get_qqbot_manager().bot return context.get_qqbot_manager().bot
def get_bot_adapter(self) -> msadapter.MessageSourceAdapter:
"""获取消息源适配器"""
return context.get_qqbot_manager().adapter
def send_person_message(self, person, message): def send_person_message(self, person, message):
"""发送私聊消息""" """发送私聊消息"""
asyncio.run(self.get_bot().send_friend_message(person, message)) self.get_bot_adapter().send_message("person", person, message)
def send_group_message(self, group, message): def send_group_message(self, group, message):
"""发送群消息""" """发送群消息"""
asyncio.run(self.get_bot().send_group_message(group, message)) self.get_bot_adapter().send_message("group", group, message)
def notify_admin(self, message): def notify_admin(self, message):
"""通知管理员""" """通知管理员"""
@@ -278,9 +512,10 @@ class PluginHost:
event_context = EventContext(event_name) event_context = EventContext(event_name)
logging.debug("触发事件: {} ({})".format(event_name, event_context.eid)) logging.debug("触发事件: {} ({})".format(event_name, event_context.eid))
for plugin in iter_plugins():
if not plugin['enabled']: emitted_plugins = []
for plugin in iter_plugins():
if not plugin["enabled"]:
continue continue
# if plugin['instance'] is None: # if plugin['instance'] is None:
@@ -292,9 +527,11 @@ class PluginHost:
# logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info())) # logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
# continue # 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 continue
emitted_plugins.append(plugin['name'])
hooks = [] hooks = []
if event_name in plugin["hooks"]: if event_name in plugin["hooks"]:
hooks = plugin["hooks"][event_name] hooks = plugin["hooks"][event_name]
@@ -302,24 +539,40 @@ class PluginHost:
try: try:
already_prevented_default = event_context.is_prevented_default() already_prevented_default = event_context.is_prevented_default()
kwargs['host'] = context.get_plugin_host() kwargs["host"] = context.get_plugin_host()
kwargs['event'] = event_context kwargs["event"] = event_context
hook(plugin['instance'], **kwargs) hook(plugin["instance"], **kwargs)
if event_context.is_prevented_default() and not already_prevented_default: if (
logging.debug("插件 {} 已要求阻止事件 {} 的默认行为".format(plugin['name'], event_name)) event_context.is_prevented_default()
and not already_prevented_default
):
logging.debug(
"插件 {} 已要求阻止事件 {} 的默认行为".format(plugin["name"], event_name)
)
except Exception as e: except Exception as e:
logging.error("插件{}触发事件{}时发生错误".format(plugin['name'], event_name)) logging.error("插件{}响应事件{}时发生错误".format(plugin["name"], event_name))
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
# print("done:{}".format(plugin['name'])) # print("done:{}".format(plugin['name']))
if event_context.is_prevented_postorder(): if event_context.is_prevented_postorder():
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin['name'])) logging.debug("插件 {} 阻止了后序插件的执行".format(plugin["name"]))
break break
logging.debug("事件 {} ({}) 处理完毕,返回值: {}".format(event_name, event_context.eid, logging.debug(
event_context.__return_value__)) "事件 {} ({}) 处理完毕,返回值: {}".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 return event_context
+87
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 {}
+92 -16
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"
"""收到私聊消息时,在判断是否应该响应前触发 """收到私聊消息时,在判断是否应该响应前触发
@@ -35,18 +35,18 @@ PersonNormalMessageReceived = "person_normal_message_received"
""" """
PersonCommandSent = "person_command_sent" PersonCommandSent = "person_command_sent"
"""判断为应该处理的私聊令时触发 """判断为应该处理的私聊令时触发
kwargs: kwargs:
launcher_type: str 发起对象类型(group/person) launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号) launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号) sender_id: int 发送者ID(QQ号)
command: str command: str
params: list[str] 参数列表 params: list[str] 参数列表
text_message: str 完整令文本 text_message: str 完整令文本
is_admin: bool 是否为管理员 is_admin: bool 是否为管理员
returns (optional): returns (optional):
alter: str 修改后的完整令文本 alter: str 修改后的完整令文本
reply: list 回复消息组件列表 reply: list 回复消息组件列表
""" """
@@ -64,18 +64,18 @@ GroupNormalMessageReceived = "group_normal_message_received"
""" """
GroupCommandSent = "group_command_sent" GroupCommandSent = "group_command_sent"
"""判断为应该处理的群聊令时触发 """判断为应该处理的群聊令时触发
kwargs: kwargs:
launcher_type: str 发起对象类型(group/person) launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号) launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号) sender_id: int 发送者ID(QQ号)
command: str command: str
params: list[str] 参数列表 params: list[str] 参数列表
text_message: str 完整令文本 text_message: str 完整令文本
is_admin: bool 是否为管理员 is_admin: bool 是否为管理员
returns (optional): returns (optional):
alter: str 修改后的完整令文本 alter: str 修改后的完整令文本
reply: list 回复消息组件列表 reply: list 回复消息组件列表
""" """
@@ -88,6 +88,8 @@ NormalMessageResponded = "normal_message_responded"
session: pkg.openai.session.Session 会话对象 session: pkg.openai.session.Session 会话对象
prefix: str 回复文字消息的前缀 prefix: str 回复文字消息的前缀
response_text: str 响应文本 response_text: str 响应文本
finish_reason: str 响应结束原因
funcs_called: list[str] 此次响应中调用的函数列表
returns (optional): returns (optional):
prefix: str 修改后的回复文字消息的前缀 prefix: str 修改后的回复文字消息的前缀
@@ -132,18 +134,64 @@ KeySwitched = "key_switched"
key_list: list[str] api-key列表 key_list: list[str] api-key列表
""" """
PromptPreProcessing = "prompt_pre_processing"
"""每回合调用接口前对prompt进行预处理时触发,此事件不支持阻止默认行为
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
default_prompt: list 此session使用的情景预设内容
prompt: list 此session现有的prompt内容
text_message: str 用户发送的消息文本
def on(event: str): returns (optional):
"""注册事件监听器 default_prompt: list 修改后的情景预设内容
:param prompt: list 修改后的prompt内容
event: str 事件名称 text_message: str 修改后的消息文本
""" """
return Plugin.on(event)
def on(*args, **kwargs):
"""注册事件监听器
"""
return Plugin.on(*args, **kwargs)
def func(*args, **kwargs):
"""注册内容函数,声明此函数为一个内容函数,在对话中将发送此函数给GPT以供其调用
此函数可以具有任意的参数,但必须按照[此文档](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format)
所述的格式编写函数的docstring。
此功能仅支持在使用gpt-3.5或gpt-4系列模型时使用。
"""
return Plugin.func(*args, **kwargs)
__current_registering_plugin__ = "" __current_registering_plugin__ = ""
def require_ver(ge: str, le: str="v999.9.9") -> bool:
"""插件版本要求装饰器
Args:
ge (str): 最低版本要求
le (str, optional): 最高版本要求
Returns:
bool: 是否满足要求, False时为无法获取版本号,True时为满足要求,报错为不满足要求
"""
qchatgpt_version = ""
from pkg.utils.updater import get_current_tag, compare_version_str
try:
qchatgpt_version = get_current_tag() # 从updater模块获取版本号
except:
return False
if compare_version_str(qchatgpt_version, ge) < 0 or \
(compare_version_str(qchatgpt_version, le) > 0):
raise Exception("QChatGPT 版本不满足要求,某些功能(可能是由插件提供的)无法正常使用。(要求版本:{}-{},但当前版本:{}".format(ge, le, qchatgpt_version))
return True
class Plugin: class Plugin:
"""插件基类""" """插件基类"""
@@ -176,6 +224,34 @@ class Plugin:
return wrapper return wrapper
@classmethod
def func(cls, name: str=None):
"""内容函数装饰器
"""
global __current_registering_plugin__
from CallingGPT.entities.namespace import get_func_schema
def wrapper(func):
function_schema = get_func_schema(func)
function_schema['name'] = __current_registering_plugin__ + '-' + (func.__name__ if name is None else name)
function_schema['enabled'] = True
host.__function_inst_map__[function_schema['name']] = function_schema['function']
del function_schema['function']
# logging.debug("registering content function: p='{}', f='{}', s={}".format(__current_registering_plugin__, func, function_schema))
host.__callable_functions__.append(
function_schema
)
return func
return wrapper
def register(name: str, description: str, version: str, author: str): def register(name: str, description: str, version: str, author: str):
"""注册插件, 此函数作为装饰器使用 """注册插件, 此函数作为装饰器使用
@@ -209,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__
+21 -2
View File
@@ -1,14 +1,17 @@
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的数据字典"""
settings = { settings = {
"order": [] "order": [],
"functions": {
"enabled": host.__enable_content_functions__
}
} }
for plugin_name in host.__plugins_order__: for plugin_name in host.__plugins_order__:
@@ -22,6 +25,11 @@ def apply_settings(settings: dict):
if "order" in settings: if "order" in settings:
host.__plugins_order__ = settings["order"] host.__plugins_order__ = settings["order"]
if "functions" in settings:
if "enabled" in settings["functions"]:
host.__enable_content_functions__ = settings["functions"]["enabled"]
# logging.debug("set content function enabled: {}".format(host.__enable_content_functions__))
def dump_settings(): def dump_settings():
"""保存settings.json数据""" """保存settings.json数据"""
@@ -78,6 +86,17 @@ def load_settings():
settings["order"].append(plugin_name) settings["order"].append(plugin_name)
settings_modified = True settings_modified = True
if "functions" not in settings:
settings["functions"] = {
"enabled": host.__enable_content_functions__
}
settings_modified = True
elif "enabled" not in settings["functions"]:
settings["functions"]["enabled"] = host.__enable_content_functions__
settings_modified = True
logging.info("已全局{}内容函数。".format("启用" if settings["functions"]["enabled"] else "禁用"))
apply_settings(settings) apply_settings(settings)
if settings_modified: if settings_modified:
+6 -1
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:
@@ -28,6 +28,11 @@ def apply_switch(switch: dict):
for plugin_name in switch: for plugin_name in switch:
host.__plugins__[plugin_name]["enabled"] = switch[plugin_name]["enabled"] host.__plugins__[plugin_name]["enabled"] = switch[plugin_name]["enabled"]
# 查找此插件的所有内容函数
for func in host.__callable_functions__:
if func['name'].startswith(plugin_name + '-'):
func['enabled'] = switch[plugin_name]["enabled"]
def dump_switch(): def dump_switch():
"""保存开关数据""" """保存开关数据"""
+137
View File
@@ -0,0 +1,137 @@
# MessageSource的适配器
import typing
import mirai
class MessageSourceAdapter:
bot_account_id: int
def __init__(self, config: dict):
pass
def send_message(
self,
target_type: str,
target_id: str,
message: mirai.MessageChain
):
"""发送消息
Args:
target_type (str): 目标类型,`person`或`group`
target_id (str): 目标ID
message (mirai.MessageChain): YiriMirai库的消息链
"""
raise NotImplementedError
def reply_message(
self,
message_source: mirai.MessageEvent,
message: mirai.MessageChain,
quote_origin: bool = False
):
"""回复消息
Args:
message_source (mirai.MessageEvent): YiriMirai消息源事件
message (mirai.MessageChain): YiriMirai库的消息链
quote_origin (bool, optional): 是否引用原消息. Defaults to False.
"""
raise NotImplementedError
def is_muted(self, group_id: int) -> bool:
"""获取账号是否在指定群被禁言"""
raise NotImplementedError
def register_listener(
self,
event_type: typing.Type[mirai.Event],
callback: typing.Callable[[mirai.Event], None]
):
"""注册事件监听器
Args:
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
"""
raise NotImplementedError
def unregister_listener(
self,
event_type: typing.Type[mirai.Event],
callback: typing.Callable[[mirai.Event], None]
):
"""注销事件监听器
Args:
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
"""
raise NotImplementedError
def run_sync(self):
"""以阻塞的方式运行适配器"""
raise NotImplementedError
def kill(self) -> bool:
"""关闭适配器
Returns:
bool: 是否成功关闭,热重载时若此函数返回False则不会重载MessageSource底层
"""
raise NotImplementedError
class MessageConverter:
"""消息链转换器基类"""
@staticmethod
def yiri2target(message_chain: mirai.MessageChain):
"""将YiriMirai消息链转换为目标消息链
Args:
message_chain (mirai.MessageChain): YiriMirai消息链
Returns:
typing.Any: 目标消息链
"""
raise NotImplementedError
@staticmethod
def target2yiri(message_chain: typing.Any) -> mirai.MessageChain:
"""将目标消息链转换为YiriMirai消息链
Args:
message_chain (typing.Any): 目标消息链
Returns:
mirai.MessageChain: YiriMirai消息链
"""
raise NotImplementedError
class EventConverter:
"""事件转换器基类"""
@staticmethod
def yiri2target(event: typing.Type[mirai.Event]):
"""将YiriMirai事件转换为目标事件
Args:
event (typing.Type[mirai.Event]): YiriMirai事件
Returns:
typing.Any: 目标事件
"""
raise NotImplementedError
@staticmethod
def target2yiri(event: typing.Any) -> mirai.Event:
"""将目标事件的调用参数转换为YiriMirai的事件参数对象
Args:
event (typing.Any): 目标事件
Returns:
typing.Type[mirai.Event]: YiriMirai事件
"""
raise NotImplementedError
+5 -5
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
+12 -17
View File
@@ -1,23 +1,22 @@
# 长消息处理相关 # 长消息处理相关
import logging
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条转发消息"
@@ -27,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:
@@ -65,20 +64,16 @@ def text_to_image(text: str) -> MessageComponent:
def check_text(text: str) -> list: def check_text(text: str) -> list:
"""检查文本是否为长消息,并转换成该使用的消息链组件""" """检查文本是否为长消息,并转换成该使用的消息链组件"""
if not hasattr(config, 'blob_message_threshold'):
return [text]
if len(text) > config.blob_message_threshold: config = context.get_config_manager().data
if not hasattr(config, 'blob_message_strategy'):
raise AttributeError('未定义长消息处理策略') 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':
# 敏感词屏蔽
text = context.get_qqbot_manager().reply_filter.process(text)
# 包装转发消息 # 包装转发消息
display = ForwardMessageDiaplay( display = ForwardMessageDiaplay(
@@ -90,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
+333
View File
@@ -0,0 +1,333 @@
import logging
import copy
import pkgutil
import traceback
import json
import tips as tips_custom
__command_list__ = {}
"""命令树
结构:
{
'cmd1': {
'description': 'cmd1 description',
'usage': 'cmd1 usage',
'aliases': ['cmd1 alias1', 'cmd1 alias2'],
'privilege': 0,
'parent': None,
'cls': <class 'pkg.qqbot.cmds.cmd1.CommandCmd1'>,
'sub': [
'cmd1-1'
]
},
'cmd1.cmd1-1: {
'description': 'cmd1-1 description',
'usage': 'cmd1-1 usage',
'aliases': ['cmd1-1 alias1', 'cmd1-1 alias2'],
'privilege': 0,
'parent': 'cmd1',
'cls': <class 'pkg.qqbot.cmds.cmd1.CommandCmd1_1'>,
'sub': []
},
'cmd2': {
'description': 'cmd2 description',
'usage': 'cmd2 usage',
'aliases': ['cmd2 alias1', 'cmd2 alias2'],
'privilege': 0,
'parent': None,
'cls': <class 'pkg.qqbot.cmds.cmd2.CommandCmd2'>,
'sub': [
'cmd2-1'
]
},
'cmd2.cmd2-1': {
'description': 'cmd2-1 description',
'usage': 'cmd2-1 usage',
'aliases': ['cmd2-1 alias1', 'cmd2-1 alias2'],
'privilege': 0,
'parent': 'cmd2',
'cls': <class 'pkg.qqbot.cmds.cmd2.CommandCmd2_1'>,
'sub': [
'cmd2-1-1'
]
},
'cmd2.cmd2-1.cmd2-1-1': {
'description': 'cmd2-1-1 description',
'usage': 'cmd2-1-1 usage',
'aliases': ['cmd2-1-1 alias1', 'cmd2-1-1 alias2'],
'privilege': 0,
'parent': 'cmd2.cmd2-1',
'cls': <class 'pkg.qqbot.cmds.cmd2.CommandCmd2_1_1'>,
'sub': []
},
}
"""
__tree_index__: dict[str, list] = {}
"""命令树索引
结构:
{
'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',
'pkg.qqbot.cmds.cmd2.CommandCmd2_1_1': 'cmd2.cmd2-1.cmd2-1-1',
}
"""
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"""
sender_id: int
"""命令发送者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: 命令执行上下文
:return: (是否执行, 回复列表(若执行))
若未执行,将自动以下一个参数查找并执行子命令
"""
raise NotImplementedError
@classmethod
def help(cls) -> str:
"""获取命令帮助信息"""
return '命令: {}\n描述: {}\n用法: \n{}\n别名: {}\n权限: {}'.format(
cls.name,
cls.description,
cls.usage,
', '.join(cls.aliases),
cls.privilege
)
@staticmethod
def register(
parent: type = None,
name: str = None,
description: str = None,
usage: str = None,
aliases: list[str] = None,
privilege: int = 0
):
"""注册命令
:param cls: 命令类
:param name: 命令名
:param parent: 父命令类
"""
global __command_list__, __tree_index__
def wrapper(cls):
cls.name = name
cls.parent = parent
cls.description = description
cls.usage = usage
cls.aliases = aliases
cls.privilege = privilege
logging.debug("cls: {}, name: {}, parent: {}".format(cls, name, parent))
if parent is None:
# 顶级命令注册
__command_list__[name] = {
'description': cls.description,
'usage': cls.usage,
'aliases': cls.aliases,
'privilege': cls.privilege,
'parent': None,
'cls': cls,
'sub': []
}
# 更新索引
__tree_index__[cls.__module__ + '.' + cls.__name__] = name
else:
# 获取父节点名称
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,
'aliases': cls.aliases,
'privilege': cls.privilege,
'parent': path,
'cls': cls,
'sub': []
}
# 更新索引
__tree_index__[cls.__module__ + '.' + cls.__name__] = path + '.' + name
return cls
return wrapper
class CommandPrivilegeError(Exception):
"""命令权限不足或不存在异常"""
pass
# 传入Context对象,广搜命令树,返回执行结果
# 若命令被处理,返回reply列表
# 若命令未被处理,继续执行下一级命令
# 若命令不存在,报异常
def execute(context: Context) -> list:
"""执行命令
:param ctx: 命令执行上下文
:return: 回复列表
"""
global __command_list__
# 拷贝ctx
ctx: Context = copy.deepcopy(context)
# 从树取出顶级命令
node = __command_list__
path = ctx.command
while True:
try:
node = __command_list__[path]
logging.debug('执行命令: {}'.format(path))
# 检查权限
if ctx.privilege < node['privilege']:
raise CommandPrivilegeError(tips_custom.command_admin_message+"{}".format(path))
# 执行
execed, reply = node['cls'].process(ctx)
if execed:
return reply
else:
# 删除crt_params第一个参数
ctx.crt_command = ctx.crt_params.pop(0)
# 下一个path
path = path + '.' + ctx.crt_command
except KeyError:
traceback.print_exc()
raise CommandPrivilegeError(tips_custom.command_err_message+"{}".format(path))
def register_all():
"""启动时调用此函数注册所有命令
递归处理pkg.qqbot.cmds包下及其子包下所有模块的所有继承于AbstractCommand的类
"""
# 模块:遍历其中的继承于AbstractCommand的类,进行注册
# 包:递归处理包下的模块
# 排除__开头的属性
global __command_list__, __tree_index__
import pkg.qqbot.cmds
def walk(module, prefix, path_prefix):
# 排除不处于pkg.qqbot.cmds中的包
if not module.__name__.startswith('pkg.qqbot.cmds'):
return
logging.debug('walk: {}, path: {}'.format(module.__name__, module.__path__))
for item in pkgutil.iter_modules(module.__path__):
if item.name.startswith('__'):
continue
if item.ispkg:
walk(__import__(module.__name__ + '.' + item.name, fromlist=['']), prefix + item.name + '.', path_prefix + item.name + '/')
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)
walk(pkg.qqbot.cmds, '', '')
logging.debug(__command_list__)
def apply_privileges():
"""读取cmdpriv.json并应用命令权限"""
# 读取内容
json_str = ""
with open('cmdpriv.json', 'r', encoding="utf-8") as f:
json_str = f.read()
data = json.loads(json_str)
for path, priv in data.items():
if path == 'comment':
continue
if path not in __command_list__:
continue
if __command_list__[path]['privilege'] != priv:
logging.debug('应用权限: {} -> {}(default: {})'.format(path, priv, __command_list__[path]['privilege']))
__command_list__[path]['privilege'] = priv
View File
+37
View File
@@ -0,0 +1,37 @@
import logging
import mirai
from .. import aamgr
from ....utils import context
@aamgr.AbstractCommandNode.register(
parent=None,
name="draw",
description="使用DALL·E生成图片",
usage="!draw <图片提示语>",
aliases=[],
privilege=1
)
class DrawCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
reply = []
if len(ctx.params) == 0:
reply = ["[bot]err: 未提供图片描述文字"]
else:
session = pkg.openai.session.get_session(ctx.session_name)
res = session.draw_image(" ".join(ctx.params))
logging.debug("draw_image result:{}".format(res))
reply = [mirai.Image(url=res.data[0].url)]
config = context.get_config_manager().data
if config['include_image_description']:
reply.append(" ".join(ctx.params))
return True, reply
+32
View File
@@ -0,0 +1,32 @@
import logging
import json
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="func",
description="管理内容函数",
usage="!func",
aliases=[],
privilege=1
)
class FuncCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
from pkg.plugin.models import host
reply = []
reply_str = "当前已加载的内容函数:\n\n"
logging.debug("host.__callable_functions__: {}".format(json.dumps(host.__callable_functions__, indent=4)))
index = 1
for func in host.__callable_functions__:
reply_str += "{}. {}{}:\n{}\n\n".format(index, ("(已禁用) " if not func['enabled'] else ""), func['name'], func['description'])
index += 1
reply = [reply_str]
return True, reply
View File
+198
View File
@@ -0,0 +1,198 @@
from ....plugin import host as plugin_host
from ....utils import updater
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="plugin",
description="插件管理",
usage="!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>",
aliases=[],
privilege=1
)
class PluginCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = []
plugin_list = plugin_host.__plugins__
if len(ctx.params) == 0:
# 列出所有插件
reply_str = "[bot]所有插件({}):\n".format(len(plugin_host.__plugins__))
idx = 0
for key in plugin_host.iter_plugins_name():
plugin = plugin_list[key]
reply_str += "\n#{} {} {}\n{}\nv{}\n作者: {}\n"\
.format((idx+1), plugin['name'],
"[已禁用]" if not plugin['enabled'] else "",
plugin['description'],
plugin['version'], plugin['author'])
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
remote_url = updater.get_remote_url("/".join(plugin['path'].split('/')[:-1]))
if remote_url != "https://github.com/RockChinQ/QChatGPT" and remote_url != "https://gitee.com/RockChin/QChatGPT":
reply_str += "源码: "+remote_url+"\n"
idx += 1
reply = [reply_str]
return True, reply
else:
return False, []
@aamgr.AbstractCommandNode.register(
parent=PluginCommand,
name="get",
description="安装插件",
usage="!plugin get <插件仓库地址>",
aliases=[],
privilege=2
)
class PluginGetCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import threading
import logging
import pkg.utils.context
if len(ctx.crt_params) == 0:
reply = ["[bot]err: 请提供插件仓库地址"]
return True, reply
reply = []
def closure():
try:
plugin_host.install_plugin(ctx.crt_params[0])
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 命令重载插件")
except Exception as e:
logging.error("插件安装失败:{}".format(e))
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
threading.Thread(target=closure, args=()).start()
reply = ["[bot]正在安装插件..."]
return True, reply
@aamgr.AbstractCommandNode.register(
parent=PluginCommand,
name="update",
description="更新指定插件或全部插件",
usage="!plugin update",
aliases=[],
privilege=2
)
class PluginUpdateCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import threading
import logging
plugin_list = plugin_host.__plugins__
reply = []
if len(ctx.crt_params) > 0:
def closure():
try:
import pkg.utils.context
updated = []
if ctx.crt_params[0] == 'all':
for key in plugin_list:
plugin_host.update_plugin(key)
updated.append(key)
else:
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])
updated.append(ctx.crt_params[0])
else:
raise Exception("未找到插件: {}".format(ctx.crt_params[0]))
pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}, 请发送 !reload 重载插件".format(", ".join(updated)))
except Exception as e:
logging.error("插件更新失败:{}".format(e))
pkg.utils.context.get_qqbot_manager().notify_admin("插件更新失败:{} 请使用 !plugin 命令确认插件名称或尝试手动更新插件".format(e))
reply = ["[bot]正在更新插件,请勿重复发起..."]
threading.Thread(target=closure).start()
else:
reply = ["[bot]请指定要更新的插件, 或使用 !plugin update all 更新所有插件"]
return True, reply
@aamgr.AbstractCommandNode.register(
parent=PluginCommand,
name="del",
description="删除插件",
usage="!plugin del <插件名>",
aliases=[],
privilege=2
)
class PluginDelCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
plugin_list = plugin_host.__plugins__
reply = []
if len(ctx.crt_params) < 1:
reply = ["[bot]err: 未指定插件名"]
else:
plugin_name = ctx.crt_params[0]
if plugin_name in plugin_list:
unin_path = plugin_host.uninstall_plugin(plugin_name)
reply = ["[bot]已删除插件: {} ({}), 请发送 !reload 重载插件".format(plugin_name, unin_path)]
else:
reply = ["[bot]err:未找到插件: {}, 请使用!plugin命令查看插件列表".format(plugin_name)]
return True, reply
@aamgr.AbstractCommandNode.register(
parent=PluginCommand,
name="on",
description="启用指定插件",
usage="!plugin on <插件名>",
aliases=[],
privilege=2
)
@aamgr.AbstractCommandNode.register(
parent=PluginCommand,
name="off",
description="禁用指定插件",
usage="!plugin off <插件名>",
aliases=[],
privilege=2
)
class PluginOnOffCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.plugin.switch as plugin_switch
plugin_list = plugin_host.__plugins__
reply = []
print(ctx.params)
new_status = ctx.params[0] == 'on'
if len(ctx.crt_params) < 1:
reply = ["[bot]err: 未指定插件名"]
else:
plugin_name = ctx.crt_params[0]
if plugin_name in plugin_list:
plugin_list[plugin_name]['enabled'] = new_status
for func in plugin_host.__callable_functions__:
if func['name'].startswith(plugin_name+"-"):
func['enabled'] = new_status
plugin_switch.dump_switch()
reply = ["[bot]已{}插件: {}".format("启用" if new_status else "禁用", plugin_name)]
else:
reply = ["[bot]err:未找到插件: {}, 请使用!plugin命令查看插件列表".format(plugin_name)]
return True, reply
View File
+71
View File
@@ -0,0 +1,71 @@
from .. import aamgr
from ....utils import context
@aamgr.AbstractCommandNode.register(
parent=None,
name="default",
description="操作情景预设",
usage="!default\n!default set [指定情景预设为默认]",
aliases=[],
privilege=1
)
class DefaultCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
session_name = ctx.session_name
params = ctx.params
reply = []
config = context.get_config_manager().data
if len(params) == 0:
# 输出目前所有情景预设
import pkg.openai.dprompt as dprompt
reply_str = "[bot]当前所有情景预设({}模式):\n\n".format(config['preset_mode'])
prompts = dprompt.mode_inst().list()
for key in prompts:
pro = prompts[key]
reply_str += "名称: {}".format(key)
for r in pro:
reply_str += "\n - [{}]: {}".format(r['role'], r['content'])
reply_str += "\n\n"
reply_str += "\n当前默认情景预设:{}\n".format(dprompt.mode_inst().get_using_name())
reply_str += "请使用 !default set <情景预设名称> 来设置默认情景预设"
reply = [reply_str]
else:
return False, []
return True, reply
@aamgr.AbstractCommandNode.register(
parent=DefaultCommand,
name="set",
description="设置默认情景预设",
usage="!default set <情景预设名称>",
aliases=[],
privilege=2
)
class DefaultSetCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = []
if len(ctx.crt_params) == 0:
reply = ["[bot]err: 请指定情景预设名称"]
elif len(ctx.crt_params) > 0:
import pkg.openai.dprompt as dprompt
try:
full_name = dprompt.mode_inst().set_using_name(ctx.crt_params[0])
reply = ["[bot]已设置默认情景预设为:{}".format(full_name)]
except Exception as e:
reply = ["[bot]err: {}".format(e)]
return True, reply
+51
View File
@@ -0,0 +1,51 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="del",
description="删除当前会话的历史记录",
usage="!del <序号>\n!del all",
aliases=[],
privilege=1
)
class DelCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
session_name = ctx.session_name
params = ctx.params
reply = []
if len(params) == 0:
reply = ["[bot]参数不足, 格式: !del <序号>\n可以通过!list查看序号"]
else:
if params[0] == 'all':
return False, []
elif params[0].isdigit():
if pkg.openai.session.get_session(session_name).delete_history(int(params[0])):
reply = ["[bot]已删除历史会话 #{}".format(params[0])]
else:
reply = ["[bot]没有历史会话 #{}".format(params[0])]
else:
reply = ["[bot]参数错误, 格式: !del <序号>\n可以通过!list查看序号"]
return True, reply
@aamgr.AbstractCommandNode.register(
parent=DelCommand,
name="all",
description="删除当前会话的全部历史记录",
usage="!del all",
aliases=[],
privilege=1
)
class DelAllCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
session_name = ctx.session_name
reply = []
pkg.openai.session.get_session(session_name).delete_all_history()
reply = ["[bot]已删除所有历史会话"]
return True, reply
+50
View File
@@ -0,0 +1,50 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="delhst",
description="删除指定会话的所有历史记录",
usage="!delhst <会话名称>\n!delhst all",
aliases=[],
privilege=2
)
class DelHistoryCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
import pkg.utils.context
params = ctx.params
reply = []
if len(params) == 0:
reply = [
"[bot]err:请输入要删除的会话名: group_<群号> 或者 person_<QQ号>, 或使用 !delhst all 删除所有会话的历史记录"]
else:
if params[0] == 'all':
return False, []
else:
if pkg.utils.context.get_database_manager().delete_all_history(params[0]):
reply = ["[bot]已删除会话 {} 的所有历史记录".format(params[0])]
else:
reply = ["[bot]未找到会话 {} 的历史记录".format(params[0])]
return True, reply
@aamgr.AbstractCommandNode.register(
parent=DelHistoryCommand,
name="all",
description="删除所有会话的全部历史记录",
usage="!delhst all",
aliases=[],
privilege=2
)
class DelAllHistoryCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.utils.context
reply = []
pkg.utils.context.get_database_manager().delete_all_session_history()
reply = ["[bot]已删除所有会话的历史记录"]
return True, reply
+29
View File
@@ -0,0 +1,29 @@
import datetime
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="last",
description="切换前一次对话",
usage="!last",
aliases=[],
privilege=1
)
class LastCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
session_name = ctx.session_name
reply = []
result = pkg.openai.session.get_session(session_name).last_session()
if result is None:
reply = ["[bot]没有前一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(datetime_str)]
return True, reply
+65
View File
@@ -0,0 +1,65 @@
import datetime
import json
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name='list',
description='列出当前会话的所有历史记录',
usage='!list\n!list [页数]',
aliases=[],
privilege=1
)
class ListCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
session_name = ctx.session_name
params = ctx.params
reply = []
pkg.openai.session.get_session(session_name).persistence()
page = 0
if len(params) > 0:
try:
page = int(params[0])
except ValueError:
pass
results = pkg.openai.session.get_session(session_name).list_history(page=page)
if len(results) == 0:
reply_str = "[bot]第{}页没有历史会话".format(page)
else:
reply_str = "[bot]历史会话 第{}页:\n".format(page)
current = -1
for i in range(len(results)):
# 时间(使用create_timestamp转换) 序号 部分内容
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
msg = ""
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"),
msg[0]['content'])
else:
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
"无内容")
if results[i]['create_timestamp'] == pkg.openai.session.get_session(
session_name).create_timestamp:
current = i + page * 10
reply_str += "\n以上信息倒序排列"
if current != -1:
reply_str += ",当前会话是 #{}\n".format(current)
else:
reply_str += ",当前处于全新会话或不在此页"
reply = [reply_str]
return True, reply
+29
View File
@@ -0,0 +1,29 @@
import datetime
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="next",
description="切换后一次对话",
usage="!next",
aliases=[],
privilege=1
)
class NextCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
session_name = ctx.session_name
reply = []
result = pkg.openai.session.get_session(session_name).next_session()
if result is None:
reply = ["[bot]没有后一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(datetime_str)]
return True, reply
+31
View File
@@ -0,0 +1,31 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="prompt",
description="获取当前会话的前文",
usage="!prompt",
aliases=[],
privilege=1
)
class PromptCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import pkg.openai.session
session_name = ctx.session_name
params = ctx.params
reply = []
msgs = ""
session: list = pkg.openai.session.get_session(session_name).prompt
for msg in session:
if len(params) != 0 and params[0] in ['-all', '-a']:
msgs = msgs + "{}: {}\n\n".format(msg['role'], msg['content'])
elif len(msg['content']) > 30:
msgs = msgs + "[{}]: {}...\n\n".format(msg['role'], msg['content'][:30])
else:
msgs = msgs + "[{}]: {}\n\n".format(msg['role'], msg['content'])
reply = ["[bot]当前对话所有内容:\n{}".format(msgs)]
return True, reply
+33
View File
@@ -0,0 +1,33 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="resend",
description="重新获取上一次问题的回复",
usage="!resend",
aliases=[],
privilege=1
)
class ResendCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
from ....openai import session as openai_session
from ....utils import context
from ....qqbot import message
session_name = ctx.session_name
reply = []
session = openai_session.get_session(session_name)
to_send = session.undo()
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)
return True, reply
+35
View File
@@ -0,0 +1,35 @@
import tips as tips_custom
from .. import aamgr
from ....openai import session
from ....utils import context
@aamgr.AbstractCommandNode.register(
parent=None,
name='reset',
description='重置当前会话',
usage='!reset',
aliases=[],
privilege=1
)
class ResetCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
params = ctx.params
session_name = ctx.session_name
reply = ""
if len(params) == 0:
session.get_session(session_name).reset(explicit=True)
reply = [tips_custom.command_reset_message]
else:
try:
import pkg.openai.dprompt as dprompt
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]))]
except Exception as e:
reply = ["[bot]会话重置失败:{}".format(e)]
return True, reply
View File
+93
View File
@@ -0,0 +1,93 @@
import json
from .. import aamgr
def config_operation(cmd, params):
reply = []
import pkg.utils.context
# 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查看使用方法"]
else:
cfg_name = params[0]
if cfg_name == 'all':
reply_str = "[bot]所有配置项:\n\n"
for cfg in cfg_mgr.data.keys():
if not cfg.startswith('__') and not cfg == 'logging':
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
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(cfg_mgr.data[cfg],
ensure_ascii=False, indent=4))
else:
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 = 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]]
if isinstance(cfg_entry, str):
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, cfg_entry)
elif isinstance(cfg_entry, dict):
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
json.dumps(cfg_entry,
ensure_ascii=False, indent=4))
else:
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, cfg_entry)
reply = [reply_str]
else:
cfg_value = " ".join(params[1:])
cfg_value = eval(cfg_value)
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]]
if isinstance(cfg_entry[cfg_entry_path[-1]], type(cfg_value)):
cfg_entry[cfg_entry_path[-1]] = cfg_value
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
else:
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
else:
cfg_mgr.data[cfg_entry_path[0]] = cfg_value
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
except KeyError:
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
except NameError:
reply = ["[bot]err:值{}不合法(字符串需要使用双引号包裹)".format(cfg_value)]
except ValueError:
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
return reply
@aamgr.AbstractCommandNode.register(
parent=None,
name="cfg",
description="配置项管理",
usage="!cfg <配置项> [配置值]\n!cfg all",
aliases=[],
privilege=2
)
class CfgCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
return True, config_operation(ctx.command, ctx.params)
+39
View File
@@ -0,0 +1,39 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="cmd",
description="显示命令列表",
usage="!cmd\n!cmd <命令名称>",
aliases=[],
privilege=1
)
class CmdCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
command_list = aamgr.__command_list__
reply = []
if len(ctx.params) == 0:
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 = [reply_str]
else:
command_name = ctx.params[0]
if command_name in command_list:
reply = [command_list[command_name]['cls'].help()]
else:
reply = ["[bot]命令 {} 不存在".format(command_name)]
return True, reply
+24
View File
@@ -0,0 +1,24 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="help",
description="显示自定义的帮助信息",
usage="!help",
aliases=[],
privilege=1
)
class HelpCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import tips
reply = ["[bot] "+tips.help_message + "\n请输入 !cmd 查看命令列表"]
# 警告config.help_message过时
import config
if hasattr(config, "help_message"):
reply[0] += "\n\n警告:config.py中的help_message已过时,不再生效,请使用tips.py中的help_message替代"
return True, reply
+25
View File
@@ -0,0 +1,25 @@
import threading
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="reload",
description="执行热重载",
usage="!reload",
aliases=[],
privilege=2
)
class ReloadCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = []
import pkg.utils.reloader
def reload_task():
pkg.utils.reloader.reload_all()
threading.Thread(target=reload_task, daemon=True).start()
return True, reply
+38
View File
@@ -0,0 +1,38 @@
import threading
import traceback
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="update",
description="更新程序",
usage="!update",
aliases=[],
privilege=2
)
class UpdateCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = []
import pkg.utils.updater
import pkg.utils.reloader
import pkg.utils.context
def update_task():
try:
if pkg.utils.updater.update_all():
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成, 请手动重启程序。")
else:
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
except Exception as e0:
traceback.print_exc()
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
return
threading.Thread(target=update_task, daemon=True).start()
reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
return True, reply
+33
View File
@@ -0,0 +1,33 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="usage",
description="获取使用情况",
usage="!usage",
aliases=[],
privilege=1
)
class UsageCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
import config
import pkg.utils.context
reply = []
reply_str = "[bot]各api-key使用情况:\n\n"
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
for key_name in api_keys:
text_length = pkg.utils.context.get_openai_manager().audit_mgr \
.get_text_length_of_key(api_keys[key_name])
image_count = pkg.utils.context.get_openai_manager().audit_mgr \
.get_image_count_of_key(api_keys[key_name])
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
int(image_count))
reply = [reply_str]
return True, reply
+27
View File
@@ -0,0 +1,27 @@
from .. import aamgr
@aamgr.AbstractCommandNode.register(
parent=None,
name="version",
description="查看版本信息",
usage="!version",
aliases=[],
privilege=1
)
class VersionCommand(aamgr.AbstractCommandNode):
@classmethod
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
reply = []
import pkg.utils.updater
reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
try:
if pkg.utils.updater.is_new_version_available():
reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
except:
pass
reply = [reply_str]
return True, reply
+28 -366
View File
@@ -1,386 +1,48 @@
# 令处理模块 # 令处理模块
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
from mirai import Image
def config_operation(cmd, params): def process_command(session_name: str, text_message: str, mgr, config: dict,
reply = []
config = pkg.utils.context.get_config()
reply_str = ""
if len(params) == 0:
reply = ["[bot]err:请输入配置项"]
else:
cfg_name = params[0]
if cfg_name == 'all':
reply_str = "[bot]所有配置项:\n\n"
for cfg in dir(config):
if not cfg.startswith('__') and not cfg == 'logging':
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
if isinstance(getattr(config, cfg), str):
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
elif isinstance(getattr(config, cfg), dict):
# 不进行unicode转义,并格式化
reply_str += "{}: {}\n".format(cfg,
json.dumps(getattr(config, cfg),
ensure_ascii=False, indent=4))
else:
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
reply = [reply_str]
elif cfg_name in dir(config):
if len(params) == 1:
# 按照配置项类型进行格式化
if isinstance(getattr(config, cfg_name), str):
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
elif isinstance(getattr(config, cfg_name), dict):
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
json.dumps(getattr(config, cfg_name),
ensure_ascii=False, indent=4))
else:
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
reply = [reply_str]
else:
cfg_value = " ".join(params[1:])
# 类型转换,如果是json则转换为字典
if cfg_value == 'true':
cfg_value = True
elif cfg_value == 'false':
cfg_value = False
elif cfg_value.isdigit():
cfg_value = int(cfg_value)
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
cfg_value = json.loads(cfg_value)
else:
try:
cfg_value = float(cfg_value)
except ValueError:
pass
# 检查类型是否匹配
if isinstance(getattr(config, cfg_name), type(cfg_value)):
setattr(config, cfg_name, cfg_value)
pkg.utils.context.set_config(config)
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
else:
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
else:
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
return reply
def plugin_operation(cmd, params, is_admin):
reply = []
import pkg.plugin.host as plugin_host
import pkg.utils.updater as updater
plugin_list = plugin_host.__plugins__
if len(params) == 0:
reply_str = "[bot]所有插件({}):\n".format(len(plugin_host.__plugins__))
idx = 0
for key in plugin_host.iter_plugins_name():
plugin = plugin_list[key]
reply_str += "\n#{} {} {}\n{}\nv{}\n作者: {}\n"\
.format((idx+1), plugin['name'],
"[已禁用]" if not plugin['enabled'] else "",
plugin['description'],
plugin['version'], plugin['author'])
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
remote_url = updater.get_remote_url("/".join(plugin['path'].split('/')[:-1]))
if remote_url != "https://github.com/RockChinQ/QChatGPT" and remote_url != "https://gitee.com/RockChin/QChatGPT":
reply_str += "源码: "+remote_url+"\n"
idx += 1
reply = [reply_str]
elif params[0] == 'update':
# 更新所有插件
if is_admin:
def closure():
import pkg.utils.context
updated = []
for key in plugin_list:
plugin = plugin_list[key]
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
success = updater.pull_latest("/".join(plugin['path'].split('/')[:-1]))
if success:
updated.append(plugin['name'])
# 检查是否有requirements.txt
pkg.utils.context.get_qqbot_manager().notify_admin("正在安装依赖...")
for key in plugin_list:
plugin = plugin_list[key]
if os.path.exists("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt"):
logging.info("{}检测到requirements.txt,安装依赖".format(plugin['name']))
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.install_requirements("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt")
import main
main.reset_logging()
pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}".format(", ".join(updated)))
threading.Thread(target=closure).start()
reply = ["[bot]正在更新所有插件,请勿重复发起..."]
else:
reply = ["[bot]err:权限不足"]
elif params[0].startswith("http"):
if is_admin:
def closure():
try:
plugin_host.install_plugin(params[0])
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 指令重载插件")
except Exception as e:
logging.error("插件安装失败:{}".format(e))
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
threading.Thread(target=closure, args=()).start()
reply = ["[bot]正在安装插件..."]
else:
reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
return reply
def process_command(session_name: str, text_message: str, mgr, config,
launcher_type: str, launcher_id: int, sender_id: int, is_admin: bool) -> list: launcher_type: str, launcher_id: int, sender_id: int, is_admin: bool) -> list:
reply = [] reply = []
try: try:
logging.info( 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 ""))) "..." if len(text_message) > 20 else "")))
cmd = text_message[1:].strip().split(' ')[0] cmd = text_message[1:].strip().split(' ')[0]
params = text_message[1:].strip().split(' ')[1:] params = text_message[1:].strip().split(' ')[1:]
if cmd == 'help':
reply = ["[bot]" + config.help_message]
elif cmd == 'reset':
if len(params) == 0:
pkg.openai.session.get_session(session_name).reset(explicit=True)
reply = ["[bot]会话已重置"]
else:
pkg.openai.session.get_session(session_name).reset(explicit=True, use_prompt=params[0])
reply = ["[bot]会话已重置,使用场景预设:{}".format(params[0])]
elif cmd == 'last':
result = pkg.openai.session.get_session(session_name).last_session()
if result is None:
reply = ["[bot]没有前一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(datetime_str)]
elif cmd == 'next':
result = pkg.openai.session.get_session(session_name).next_session()
if result is None:
reply = ["[bot]没有后一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(datetime_str)]
elif cmd == 'prompt':
msgs = ""
session:list = pkg.openai.session.get_session(session_name).prompt
for msg in session:
if len(params) != 0 and params[0] in ['-all', '-a']:
msgs = msgs + "{}: {}\n\n".format(msg['role'], msg['content'])
elif len(msg['content']) > 30:
msgs = msgs + "[{}]: {}...\n\n".format(msg['role'], msg['content'][:30])
else:
msgs = msgs + "[{}]: {}\n\n".format(msg['role'], msg['content'])
reply = ["[bot]当前对话所有内容:\n{}".format(msgs)]
elif cmd == 'list':
pkg.openai.session.get_session(session_name).persistence()
page = 0
if len(params) > 0: # 把!~开头的转换成!cfg
if cmd.startswith('~'):
params = [cmd[1:]] + params
cmd = 'cfg'
# 包装参数
context = cmdmgr.Context(
command=cmd,
crt_command=cmd,
params=params,
crt_params=params[:],
session_name=session_name,
text_message=text_message,
launcher_type=launcher_type,
launcher_id=launcher_id,
sender_id=sender_id,
is_admin=is_admin,
privilege=2 if is_admin else 1, # 普通用户1,管理员2
)
try: try:
page = int(params[0]) reply = cmdmgr.execute(context)
except ValueError: except cmdmgr.CommandPrivilegeError as e:
pass reply = ["{}".format(e)]
results = pkg.openai.session.get_session(session_name).list_history(page=page) return reply
if len(results) == 0:
reply = ["[bot]第{}页没有历史会话".format(page)]
else:
reply_str = "[bot]历史会话 第{}页:\n".format(page)
current = -1
for i in range(len(results)):
# 时间(使用create_timestamp转换) 序号 部分内容
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
msg = ""
try:
msg = json.loads(results[i]['prompt'])
except json.decoder.JSONDecodeError:
msg = pkg.openai.session.reset_session_prompt(session_name, results[i]['prompt'])
# 持久化
pkg.openai.session.get_session(session_name).persistence()
if len(msg) >= 2:
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
msg[0]['content'])
else:
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
"无内容")
if results[i]['create_timestamp'] == pkg.openai.session.get_session(
session_name).create_timestamp:
current = i + page * 10
reply_str += "\n以上信息倒序排列"
if current != -1:
reply_str += ",当前会话是 #{}\n".format(current)
else:
reply_str += ",当前处于全新会话或不在此页"
reply = [reply_str]
elif cmd == 'resend':
session = pkg.openai.session.get_session(session_name)
to_send = session.undo()
reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config,
launcher_type, launcher_id, sender_id)
elif cmd == 'del': # 删除指定会话历史记录
if len(params) == 0:
reply = ["[bot]参数不足, 格式: !del <序号>\n可以通过!list查看序号"]
else:
if params[0] == 'all':
pkg.openai.session.get_session(session_name).delete_all_history()
reply = ["[bot]已删除所有历史会话"]
elif params[0].isdigit():
if pkg.openai.session.get_session(session_name).delete_history(int(params[0])):
reply = ["[bot]已删除历史会话 #{}".format(params[0])]
else:
reply = ["[bot]没有历史会话 #{}".format(params[0])]
else:
reply = ["[bot]参数错误, 格式: !del <序号>\n可以通过!list查看序号"]
elif cmd == 'usage':
reply_str = "[bot]各api-key使用情况:\n\n"
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
for key_name in api_keys:
text_length = pkg.utils.context.get_openai_manager().audit_mgr \
.get_text_length_of_key(api_keys[key_name])
image_count = pkg.utils.context.get_openai_manager().audit_mgr \
.get_image_count_of_key(api_keys[key_name])
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
int(image_count))
# 获取此key的额度
try:
credit_data = credit.fetch_credit_data(api_keys[key_name])
reply_str += " - 使用额度:{:.2f}/{:.2f}\n".format(credit_data['total_used'],credit_data['total_granted'])
except Exception as e: except Exception as e:
logging.warning("获取额度失败:{}".format(e)) mgr.notify_admin("{}命令执行失败:{}".format(session_name, e))
reply = [reply_str]
elif cmd == 'draw':
if len(params) == 0:
reply = ["[bot]err:请输入图片描述文字"]
else:
session = pkg.openai.session.get_session(session_name)
res = session.draw_image(" ".join(params))
logging.debug("draw_image result:{}".format(res))
reply = [Image(url=res['data'][0]['url'])]
if not (hasattr(config, 'include_image_description')
and not config.include_image_description):
reply.append(" ".join(params))
elif cmd == 'version':
reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
try:
if pkg.utils.updater.is_new_version_available():
reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
except:
pass
reply = [reply_str]
elif cmd == 'plugin':
reply = plugin_operation(cmd, params, is_admin)
elif cmd == 'default':
if len(params) == 0:
# 输出目前所有情景预设
import pkg.openai.dprompt as dprompt
reply_str = "[bot]当前所有情景预设:\n\n"
for key,value in dprompt.get_prompt_dict().items():
reply_str += " - {}: {}\n".format(key,value)
reply_str += "\n当前默认情景预设:{}\n".format(dprompt.get_current())
reply_str += "请使用!default <情景预设>来设置默认情景预设"
reply = [reply_str]
elif len(params) >0 and is_admin:
# 设置默认情景
import pkg.openai.dprompt as dprompt
try:
dprompt.set_current(params[0])
reply = ["[bot]已设置默认情景预设为:{}".format(dprompt.get_current())]
except KeyError:
reply = ["[bot]err: 未找到情景预设:{}".format(params[0])]
else:
reply = ["[bot]err: 仅管理员可设置默认情景预设"]
elif cmd == "delhst" and is_admin:
if len(params) == 0:
reply = ["[bot]err:请输入要删除的会话名: group_<群号> 或者 person_<QQ号>, 或使用 !delhst all 删除所有会话的历史记录"]
else:
if params[0] == "all":
pkg.utils.context.get_database_manager().delete_all_session_history()
reply = ["[bot]已删除所有会话的历史记录"]
else:
if pkg.utils.context.get_database_manager().delete_all_history(params[0]):
reply = ["[bot]已删除会话 {} 的所有历史记录".format(params[0])]
else:
reply = ["[bot]未找到会话 {} 的历史记录".format(params[0])]
elif cmd == 'reload' and is_admin:
def reload_task():
pkg.utils.reloader.reload_all()
threading.Thread(target=reload_task, daemon=True).start()
elif cmd == 'update' and is_admin:
def update_task():
try:
if pkg.utils.updater.update_all():
pkg.utils.reloader.reload_all(notify=False)
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
else:
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
except Exception as e0:
traceback.print_exc()
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
return
threading.Thread(target=update_task, daemon=True).start()
reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
elif cmd == 'cfg' and is_admin:
reply = config_operation(cmd, params)
else:
if cmd.startswith("~") and is_admin:
config_item = cmd[1:]
params = [config_item] + params
reply = config_operation("cfg", params)
else:
reply = ["[bot]err:未知的指令或权限不足: " + cmd]
except Exception as e:
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
logging.exception(e) logging.exception(e)
reply = ["[bot]err:{}".format(e)] reply = ["[bot]err:{}".format(e)]
+9 -6
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
if hasattr(config, 'baidu_check') and hasattr(config, 'baidu_api_key') and hasattr(config, 'baidu_secret_key'): config = context.get_config_manager().data
self.baidu_check = config.baidu_check
self.baidu_api_key = config.baidu_api_key self.baidu_check = config['baidu_check']
self.baidu_secret_key = config.baidu_secret_key self.baidu_api_key = config['baidu_api_key']
self.inappropriate_message_tips = config.inappropriate_message_tips 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)
+7 -8
View File
@@ -1,19 +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 not hasattr(config, 'ignore_rules'): if 'prefix' in config['ignore_rules']:
return False for rule in config['ignore_rules']['prefix']:
if 'prefix' in config.ignore_rules:
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
+223 -153
View File
@@ -1,34 +1,35 @@
import asyncio
import json import json
import os import os
import threading
from concurrent.futures import ThreadPoolExecutor
import mirai.models.bus
from mirai import At, GroupMessage, MessageEvent, Mirai, StrangerMessage, WebSocketAdapter, HTTPAdapter, \
FriendMessage, Image
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
from ..qqbot import adapter as msadapter
# 检查消息是否符合泛响应匹配机制 # 检查消息是否符合泛响应匹配机制
def check_response_rule(text: str): def check_response_rule(group_id:int, text: str):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if not hasattr(config, 'response_rules'):
return False, '' rules = config['response_rules']
# 检查是否有特定规则
if 'prefix' not in config['response_rules']:
if str(group_id) in config['response_rules']:
rules = config['response_rules'][str(group_id)]
else:
rules = config['response_rules']['default']
rules = config.response_rules
# 检查前缀匹配 # 检查前缀匹配
if 'prefix' in rules: if 'prefix' in rules:
for rule in rules['prefix']: for rule in rules['prefix']:
@@ -46,19 +47,39 @@ def check_response_rule(text: str):
return False, "" return False, ""
def response_at(): def response_at(group_id: int):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if 'at' not in config.response_rules:
use_response_rule = config['response_rules']
# 检查是否有特定规则
if 'prefix' not in config['response_rules']:
if str(group_id) in config['response_rules']:
use_response_rule = config['response_rules'][str(group_id)]
else:
use_response_rule = config['response_rules']['default']
if 'at' not in use_response_rule:
return True return True
return config.response_rules['at'] return use_response_rule['at']
def random_responding(): def random_responding(group_id):
config = pkg.utils.context.get_config() config = context.get_config_manager().data
if 'random_rate' in config.response_rules:
use_response_rule = config['response_rules']
# 检查是否有特定规则
if 'prefix' not in config['response_rules']:
if str(group_id) in config['response_rules']:
use_response_rule = config['response_rules'][str(group_id)]
else:
use_response_rule = config['response_rules']['default']
if 'random_rate' in use_response_rule:
import random import random
return random.random() < config.response_rules['random_rate'] return random.random() < use_response_rule['random_rate']
return False return False
@@ -66,64 +87,56 @@ def random_responding():
class QQBotManager: class QQBotManager:
retry = 3 retry = 3
#线程池控制 adapter: msadapter.MessageSourceAdapter = None
pool = None
bot: Mirai = None bot_account_id: int = 0
reply_filter = None reply_filter = None
enable_banlist = False enable_banlist = False
enable_private = True
enable_group = True
ban_person = [] ban_person = []
ban_group = [] ban_group = []
def __init__(self, mirai_http_api_config: dict, timeout: int = 60, retry: int = 3, pool_num: int = 10, first_time_init=True): def __init__(self, first_time_init=True):
self.timeout = timeout config = context.get_config_manager().data
self.retry = retry
self.pool_num = pool_num self.timeout = config['process_message_timeout']
self.pool = ThreadPoolExecutor(max_workers=self.pool_num) self.retry = config['retry_times']
logging.debug("Registered thread pool Size:{}".format(pool_num))
# 加载禁用列表
if os.path.exists("banlist.py"):
import banlist
self.enable_banlist = banlist.enable
self.ban_person = banlist.person
self.ban_group = banlist.group
logging.info("加载禁用列表: person: {}, group: {}".format(self.ban_person, self.ban_group))
config = pkg.utils.context.get_config()
if os.path.exists("sensitive.json") \
and config.sensitive_word_filter is not None \
and config.sensitive_word_filter:
with open("sensitive.json", "r", encoding="utf-8") as f:
sensitive_json = json.load(f)
self.reply_filter = pkg.qqbot.filter.ReplyFilter(
sensitive_words=sensitive_json['words'],
mask=sensitive_json['mask'] if 'mask' in sensitive_json else '*',
mask_word=sensitive_json['mask_word'] if 'mask_word' in sensitive_json else ''
)
else:
self.reply_filter = pkg.qqbot.filter.ReplyFilter([])
# 由于YiriMirai的bot对象是单例的,且shutdown方法暂时无法使用 # 由于YiriMirai的bot对象是单例的,且shutdown方法暂时无法使用
# 故只在第一次初始化时创建bot对象,重载之后使用原bot对象 # 故只在第一次初始化时创建bot对象,重载之后使用原bot对象
# 因此,bot的配置不支持热重载 # 因此,bot的配置不支持热重载
if first_time_init: if first_time_init:
self.first_time_init(mirai_http_api_config) logging.debug("Use adapter:" + config['msg_source_adapter'])
if config['msg_source_adapter'] == 'yirimirai':
from pkg.qqbot.sources.yirimirai import YiriMiraiAdapter
mirai_http_api_config = config['mirai_http_api_config']
self.bot_account_id = config['mirai_http_api_config']['qq']
self.adapter = YiriMiraiAdapter(mirai_http_api_config)
elif config['msg_source_adapter'] == 'nakuru':
from pkg.qqbot.sources.nakuru import NakuruProjectAdapter
self.adapter = NakuruProjectAdapter(config['nakuru_config'])
self.bot_account_id = self.adapter.bot_account_id
else: else:
self.bot = pkg.utils.context.get_qqbot_manager().bot self.adapter = context.get_qqbot_manager().adapter
self.bot_account_id = context.get_qqbot_manager().bot_account_id
pkg.utils.context.set_qqbot_manager(self) # 保存 account_id 到审计模块
from ..utils.center import apigroup
apigroup.APIGroup._runtime_info['account_id'] = "{}".format(self.bot_account_id)
context.set_qqbot_manager(self)
# 注册诸事件
# Caution: 注册新的事件处理器之后,请务必在unsubscribe_all中编写相应的取消订阅代码 # Caution: 注册新的事件处理器之后,请务必在unsubscribe_all中编写相应的取消订阅代码
@self.bot.on(FriendMessage) def on_friend_message(event: FriendMessage):
async def on_friend_message(event: FriendMessage):
def friend_message_handler(event: FriendMessage):
def friend_message_handler():
# 触发事件 # 触发事件
args = { args = {
"launcher_type": "person", "launcher_type": "person",
@@ -138,12 +151,17 @@ class QQBotManager:
self.on_person_message(event) self.on_person_message(event)
self.go(friend_message_handler, event) context.get_thread_ctl().submit_user_task(
friend_message_handler,
)
self.adapter.register_listener(
FriendMessage,
on_friend_message
)
@self.bot.on(StrangerMessage) def on_stranger_message(event: StrangerMessage):
async def on_stranger_message(event: StrangerMessage):
def stranger_message_handler(event: StrangerMessage): def stranger_message_handler():
# 触发事件 # 触发事件
args = { args = {
"launcher_type": "person", "launcher_type": "person",
@@ -158,10 +176,17 @@ class QQBotManager:
self.on_person_message(event) self.on_person_message(event)
self.go(stranger_message_handler, event) context.get_thread_ctl().submit_user_task(
stranger_message_handler,
)
# nakuru不区分好友和陌生人,故仅为yirimirai注册陌生人事件
if config['msg_source_adapter'] == 'yirimirai':
self.adapter.register_listener(
StrangerMessage,
on_stranger_message
)
@self.bot.on(GroupMessage) def on_group_message(event: GroupMessage):
async def on_group_message(event: GroupMessage):
def group_message_handler(event: GroupMessage): def group_message_handler(event: GroupMessage):
# 触发事件 # 触发事件
@@ -178,65 +203,96 @@ class QQBotManager:
self.on_group_message(event) self.on_group_message(event)
self.go(group_message_handler, event) context.get_thread_ctl().submit_user_task(
group_message_handler,
event
)
self.adapter.register_listener(
GroupMessage,
on_group_message
)
def unsubscribe_all(): def unsubscribe_all():
"""取消所有订阅 """取消所有订阅
用于在热重载流程中卸载所有事件处理器 用于在热重载流程中卸载所有事件处理器
""" """
assert isinstance(self.bot, Mirai) self.adapter.unregister_listener(
bus = self.bot.bus FriendMessage,
assert isinstance(bus, mirai.models.bus.ModelEventBus) on_friend_message
)
bus.unsubscribe(FriendMessage, on_friend_message) if config['msg_source_adapter'] == 'yirimirai':
bus.unsubscribe(StrangerMessage, on_stranger_message) self.adapter.unregister_listener(
bus.unsubscribe(GroupMessage, on_group_message) StrangerMessage,
on_stranger_message
)
self.adapter.unregister_listener(
GroupMessage,
on_group_message
)
self.unsubscribe_all = unsubscribe_all self.unsubscribe_all = unsubscribe_all
def go(self, func, *args, **kwargs): # 加载禁用列表
self.pool.submit(func, *args, **kwargs) if os.path.exists("banlist.py"):
import banlist
self.enable_banlist = banlist.enable
self.ban_person = banlist.person
self.ban_group = banlist.group
logging.info("加载禁用列表: person: {}, group: {}".format(self.ban_person, self.ban_group))
def first_time_init(self, mirai_http_api_config: dict): if hasattr(banlist, "enable_private"):
"""热重载后不再运行此函数""" self.enable_private = banlist.enable_private
if hasattr(banlist, "enable_group"):
self.enable_group = banlist.enable_group
if 'adapter' not in mirai_http_api_config or mirai_http_api_config['adapter'] == "WebSocketAdapter": config = context.get_config_manager().data
bot = Mirai( if os.path.exists("sensitive.json") \
qq=mirai_http_api_config['qq'], and config['sensitive_word_filter'] is not None \
adapter=WebSocketAdapter( and config['sensitive_word_filter']:
verify_key=mirai_http_api_config['verifyKey'], with open("sensitive.json", "r", encoding="utf-8") as f:
host=mirai_http_api_config['host'], sensitive_json = json.load(f)
port=mirai_http_api_config['port'] self.reply_filter = qqbot_filter.ReplyFilter(
sensitive_words=sensitive_json['words'],
mask=sensitive_json['mask'] if 'mask' in sensitive_json else '*',
mask_word=sensitive_json['mask_word'] if 'mask_word' in sensitive_json else ''
) )
)
elif mirai_http_api_config['adapter'] == "HTTPAdapter":
bot = Mirai(
qq=mirai_http_api_config['qq'],
adapter=HTTPAdapter(
verify_key=mirai_http_api_config['verifyKey'],
host=mirai_http_api_config['host'],
port=mirai_http_api_config['port']
)
)
else: else:
raise Exception("未知的适配器类型") self.reply_filter = qqbot_filter.ReplyFilter([])
self.bot = bot def send(self, event, msg, check_quote=True, check_at_sender=True):
config = context.get_config_manager().data
def send(self, event, msg, check_quote=True): if check_at_sender and config['at_sender']:
config = pkg.utils.context.get_config() msg.insert(
asyncio.run( 0,
self.bot.send(event, msg, quote=True if hasattr(config, Plain(" \n")
"quote_origin") and config.quote_origin and check_quote else False)) )
# 当回复的正文中包含换行时,quote可能会自带at,此时就不再单独添加at,只添加换行
if "\n" not in str(msg[1]) or config['msg_source_adapter'] == 'nakuru':
msg.insert(
0,
At(
event.sender.id
)
)
self.adapter.reply_message(
event,
msg,
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 = ''
if event.sender.id == self.bot.qq: config = context.get_config_manager().data
if not self.enable_private:
logging.debug("已在banlist.py中禁用所有私聊")
elif event.sender.id == self.bot_account_id:
pass pass
else: else:
if Image in event.message_chain: if Image in event.message_chain:
@@ -247,7 +303,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,
@@ -256,37 +312,38 @@ 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 = ["[bot]err:请求超时"] reply = [tips_custom.reply_message]
if reply: if reply:
return self.send(event, reply, check_quote=False) return self.send(event, reply, check_quote=False, check_at_sender=False)
# 群消息处理 # 群消息处理
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.qq) in event.message_chain: if At(self.bot_account_id) in event.message_chain:
event.message_chain.remove(At(self.bot.qq)) event.message_chain.remove(At(self.bot_account_id))
# 超时则重试,重试超过次数则放弃 # 超时则重试,重试超过次数则放弃
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,
@@ -296,34 +353,36 @@ 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 = ["[bot]err:请求超时"] replys = [tips_custom.replys_message]
return replys return replys
if Image in event.message_chain: if not self.enable_group:
logging.debug("已在banlist.py中禁用所有群聊")
elif Image in event.message_chain:
pass pass
else: else:
if At(self.bot.qq) in event.message_chain and response_at(): if At(self.bot_account_id) in event.message_chain and response_at(event.group.id):
# 直接调用 # 直接调用
reply = process() reply = process()
else: else:
check, result = check_response_rule(str(event.message_chain).strip()) check, result = check_response_rule(event.group.id, str(event.message_chain).strip())
if check: if check:
reply = process(result.strip()) reply = process(result.strip())
# 检查是否随机响应 # 检查是否随机响应
elif random_responding(): elif random_responding(event.group.id):
logging.info("随机响应group_{}消息".format(event.group.id)) logging.info("随机响应group_{}消息".format(event.group.id))
reply = process() reply = process()
@@ -332,26 +391,37 @@ 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 hasattr(config, "admin_qq") and 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:
send_task = self.bot.send_friend_message(config.admin_qq, "[bot]{}".format(message)) self.adapter.send_message(
threading.Thread(target=asyncio.run, args=(send_task,)).start() "person",
config['admin_qq'],
MessageChain([Plain("[bot]{}".format(message))])
)
else: else:
for adm in config.admin_qq: for adm in config['admin_qq']:
send_task = self.bot.send_friend_message(adm, "[bot]{}".format(message)) self.adapter.send_message(
threading.Thread(target=asyncio.run, args=(send_task,)).start() "person",
adm,
MessageChain([Plain("[bot]{}".format(message))])
)
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 hasattr(config, "admin_qq") and 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:
send_task = self.bot.send_friend_message(config.admin_qq, message) self.adapter.send_message(
threading.Thread(target=asyncio.run, args=(send_task,)).start() "person",
config['admin_qq'],
message
)
else: else:
for adm in config.admin_qq: for adm in config['admin_qq']:
send_task = self.bot.send_friend_message(adm, message) self.adapter.send_message(
threading.Thread(target=asyncio.run, args=(send_task,)).start() "person",
adm,
message
)
+40 -36
View File
@@ -1,35 +1,33 @@
# 普通消息处理模块 # 普通消息处理模块
import logging import logging
import time
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
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 hasattr(config, 'hide_exce_info_to_user') and config.hide_exce_info_to_user: if config['hide_exce_info_to_user']:
if hasattr(config, 'alter_tip_message'): return [tips_custom.alter_tip_message] if tips_custom.alter_tip_message else []
return [config.alter_tip_message] if config.alter_tip_message else []
else:
return ["[bot]出错了,请重试或联系管理员"]
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
@@ -41,9 +39,9 @@ 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 hasattr(config, "show_prefix") and config.show_prefix else "" prefix = "[GPT]" if config['show_prefix'] else ""
text = session.append(text_message) text, finish_reason, funcs = session.query(text_message)
# 触发插件事件 # 触发插件事件
args = { args = {
@@ -52,10 +50,12 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
"sender_id": sender_id, "sender_id": sender_id,
"session": session, "session": session,
"prefix": prefix, "prefix": prefix,
"response_text": text "response_text": text,
"finish_reason": finish_reason,
"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")
@@ -64,43 +64,44 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
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 = blob.check_text(prefix + text) reply = [prefix + text]
except openai.error.APIConnectionError as e:
except openai.APIConnectionError as e:
err_msg = str(e) err_msg = str(e)
if err_msg.__contains__('Error communicating with OpenAI'): if err_msg.__contains__('Error communicating with OpenAI'):
reply = handle_exception("{}会话调用API失败:{}\n请尝试关闭网络代理来解决此问题。".format(session_name, e), reply = handle_exception("{}会话调用API失败:{}\n您的网络无法访问OpenAI接口或网络代理不正常".format(session_name, e),
"[bot]err:调用API失败,请重试或联系管理员,或等待修复") "[bot]err:调用API失败,请重试或联系管理员,或等待修复")
else: else:
reply = handle_exception("{}会话调用API失败:{}".format(session_name, e), "[bot]err:调用API失败,请重试或联系管理员,或等待修复") reply = handle_exception("{}会话调用API失败:{}".format(session_name, e), "[bot]err:调用API失败,请重试或联系管理员,或等待修复")
except openai.error.RateLimitError as e: except openai.RateLimitError as e:
logging.debug(type(e)) logging.debug(type(e))
logging.debug(e.error['message']) logging.debug(e.error['message'])
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
@@ -116,11 +117,14 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
else: else:
reply = handle_exception("{}会话调用API失败:{}".format(session_name, e), reply = handle_exception("{}会话调用API失败:{}".format(session_name, e),
"[bot]err:RateLimitError,请重试或联系作者,或等待修复") "[bot]err:RateLimitError,请重试或联系作者,或等待修复")
except openai.error.InvalidRequestError as e: except openai.BadRequestError as e:
reply = handle_exception("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或" if config['auto_reset'] and "This model's maximum context length is" in str(e):
"completion_api_params中的max_tokens参数数值过大导致的,请尝试将其降低".format( session.reset(persist=True)
reply = [tips_custom.session_auto_reset_message]
else:
reply = handle_exception("{}API调用参数错误:{}\n".format(
session_name, e), "[bot]err:API调用参数错误,请联系管理员,或等待修复") session_name, e), "[bot]err:API调用参数错误,请联系管理员,或等待修复")
except openai.error.ServiceUnavailableError as e: except openai.APIStatusError as e:
reply = handle_exception("{}API调用服务不可用:{}".format(session_name, e), "[bot]err:API调用服务不可用,请重试或联系管理员,或等待修复") reply = handle_exception("{}API调用服务不可用:{}".format(session_name, e), "[bot]err:API调用服务不可用,请重试或联系管理员,或等待修复")
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
+67 -44
View File
@@ -1,49 +1,46 @@
# 此模块提供了消息处理的具体逻辑的接口 # 此模块提供了消息处理的具体逻辑的接口
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
from ..qqbot import blob
import tips as tips_custom
processing = [] 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)
@@ -57,35 +54,38 @@ 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 []
config = context.get_config_manager().data
if not config['wait_last_done'] and session_name in processing:
return mirai.MessageChain([mirai.Plain(tips_custom.message_drop_tip)])
# 检查是否被禁言 # 检查是否被禁言
if launcher_type == 'group': if launcher_type == 'group':
result = mgr.bot.member_info(target=launcher_id, member_id=mgr.bot.qq).get() is_muted = mgr.adapter.is_muted(launcher_id)
result = asyncio.run(result) if is_muted:
if result.mute_time_remaining > 0: logging.info("机器人被禁言,跳过消息处理(group_{})".format(launcher_id))
logging.info("机器人被禁言,跳过消息处理(group_{},剩余{}s)".format(launcher_id,
result.mute_time_remaining))
return reply return reply
import config if config['income_msg_check']:
if hasattr(config, 'income_msg_check') and 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()
# 为强制消息延迟计时
start_time = time.time()
# 处理消息 # 处理消息
try: try:
if session_name in processing:
pkg.openai.session.get_session(session_name).release_response_lock()
return MessageChain([Plain("[bot]err:正在处理中,请稍后再试")])
config = pkg.utils.context.get_config()
processing.append(session_name) processing.append(session_name)
try: try:
if text_message.startswith('!') or text_message.startswith(""): # 指令 msg_type = ''
if text_message.startswith('!') or text_message.startswith(""): # 命令
msg_type = 'command'
# 触发插件事件 # 触发插件事件
args = { args = {
'launcher_type': launcher_type, 'launcher_type': launcher_type,
@@ -108,16 +108,18 @@ 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: # 消息
msg_type = 'message'
# 限速丢弃检查 # 限速丢弃检查
# print(ratelimit.__crt_minute_usage__[session_name]) # print(ratelimit.__crt_minute_usage__[session_name])
if hasattr(config, "rate_limitation") and 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]"+config.rate_limit_drop_tip]) if hasattr(config, "rate_limit_drop_tip") and config.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()
# 触发插件事件 # 触发插件事件
@@ -139,14 +141,13 @@ 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 hasattr(config, "rate_limitation") and 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))
if hasattr(config, "rate_limitation"):
ratelimit.add_usage(session_name) ratelimit.add_usage(session_name)
if reply is not None and len(reply) > 0 and (type(reply[0]) == str or type(reply[0]) == mirai.Plain): if reply is not None and len(reply) > 0 and (type(reply[0]) == str or type(reply[0]) == mirai.Plain):
@@ -156,13 +157,35 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
"回复[{}]文字消息:{}".format(session_name, "回复[{}]文字消息:{}".format(session_name,
reply[0][:min(100, len(reply[0]))] + ( reply[0][:min(100, len(reply[0]))] + (
"..." if len(reply[0]) > 100 else ""))) "..." if len(reply[0]) > 100 else "")))
if msg_type == 'message':
reply = [mgr.reply_filter.process(reply[0])] reply = [mgr.reply_filter.process(reply[0])]
reply = blob.check_text(reply[0])
else: else:
logging.info("回复[{}]消息".format(session_name)) logging.info("回复[{}]消息".format(session_name))
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()
return MessageChain(reply) # 检查延迟时间
if config['force_delay_range'][1] == 0:
delay_time = 0
else:
import random
# 从延迟范围中随机取一个值(浮点)
rdm = random.uniform(config['force_delay_range'][0], config['force_delay_range'][1])
spent = time.time() - start_time
# 如果花费时间小于延迟时间,则延迟
delay_time = rdm - spent if rdm - spent > 0 else 0
# 延迟
if delay_time > 0:
logging.info("[风控] 强制延迟{:.2f}秒(如需关闭,请到config.py修改force_delay_range字段)".format(delay_time))
time.sleep(delay_time)
return mirai.MessageChain(reply)
+15 -12
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__ = {}
"""当前分钟每个会话的对话次数""" """当前分钟每个会话的对话次数"""
@@ -10,6 +13,16 @@ __crt_minute_usage__ = {}
__timer_thr__: threading.Thread = None __timer_thr__: threading.Thread = None
def get_limitation(session_name: str) -> int:
"""获取会话的限制次数"""
config = context.get_config_manager().data
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): def add_usage(session_name: str):
"""增加会话的对话次数""" """增加会话的对话次数"""
global __crt_minute_usage__ global __crt_minute_usage__
@@ -56,12 +69,7 @@ def get_rest_wait_time(session_name: str, spent: float) -> float:
"""获取会话此回合的剩余等待时间""" """获取会话此回合的剩余等待时间"""
global __crt_minute_usage__ global __crt_minute_usage__
import config min_seconds_per_round = 60.0 / get_limitation(session_name)
if not hasattr(config, 'rate_limitation'):
return 0
min_seconds_per_round = 60.0 / config.rate_limitation
if session_name in __crt_minute_usage__: if session_name in __crt_minute_usage__:
return max(0, min_seconds_per_round - spent) return max(0, min_seconds_per_round - spent)
@@ -73,13 +81,8 @@ def is_reach_limit(session_name: str) -> bool:
"""判断会话是否超过限制""" """判断会话是否超过限制"""
global __crt_minute_usage__ global __crt_minute_usage__
import config
if not hasattr(config, 'rate_limitation'):
return False
if session_name in __crt_minute_usage__: if session_name in __crt_minute_usage__:
return __crt_minute_usage__[session_name] >= config.rate_limitation return __crt_minute_usage__[session_name] >= get_limitation(session_name)
else: else:
return False return False
View File
+328
View File
@@ -0,0 +1,328 @@
import asyncio
import typing
import traceback
import logging
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(adapter_model.MessageConverter):
"""消息转换器"""
@staticmethod
def yiri2target(message_chain: mirai.MessageChain) -> list:
msg_list = []
if type(message_chain) is mirai.MessageChain:
msg_list = message_chain.__root__
elif type(message_chain) is list:
msg_list = message_chain
else:
raise Exception("Unknown message type: " + str(message_chain) + str(type(message_chain)))
nakuru_msg_list = []
# 遍历并转换
for component in msg_list:
if type(component) is mirai.Plain:
nakuru_msg_list.append(nkc.Plain(component.text, False))
elif type(component) is mirai.Image:
if component.url is not None:
nakuru_msg_list.append(nkc.Image.fromURL(component.url))
elif component.base64 is not None:
nakuru_msg_list.append(nkc.Image.fromBase64(component.base64))
elif component.path is not None:
nakuru_msg_list.append(nkc.Image.fromFileSystem(component.path))
elif type(component) is mirai.Face:
nakuru_msg_list.append(nkc.Face(id=component.face_id))
elif type(component) is mirai.At:
nakuru_msg_list.append(nkc.At(qq=component.target))
elif type(component) is mirai.AtAll:
nakuru_msg_list.append(nkc.AtAll())
elif type(component) is mirai.Voice:
if component.url is not None:
nakuru_msg_list.append(nkc.Record.fromURL(component.url))
elif component.path is not None:
nakuru_msg_list.append(nkc.Record.fromFileSystem(component.path))
elif type(component) is blob.Forward:
# 转发消息
yiri_forward_node_list = component.node_list
nakuru_forward_node_list = []
# 遍历并转换
for yiri_forward_node in yiri_forward_node_list:
try:
content_list = NakuruProjectMessageConverter.yiri2target(yiri_forward_node.message_chain)
nakuru_forward_node = nkc.Node(
name=yiri_forward_node.sender_name,
uin=yiri_forward_node.sender_id,
time=int(yiri_forward_node.time.timestamp()) if yiri_forward_node.time is not None else None,
content=content_list
)
nakuru_forward_node_list.append(nakuru_forward_node)
except Exception as e:
import traceback
traceback.print_exc()
nakuru_msg_list.append(nakuru_forward_node_list)
else:
nakuru_msg_list.append(nkc.Plain(str(component)))
return nakuru_msg_list
@staticmethod
def target2yiri(message_chain: typing.Any, message_id: int = -1) -> mirai.MessageChain:
"""将Yiri的消息链转换为YiriMirai的消息链"""
assert type(message_chain) is list
yiri_msg_list = []
import datetime
# 添加Source组件以标记message_id等信息
yiri_msg_list.append(mirai.models.message.Source(id=message_id, time=datetime.datetime.now()))
for component in message_chain:
if type(component) is nkc.Plain:
yiri_msg_list.append(mirai.Plain(text=component.text))
elif type(component) is nkc.Image:
yiri_msg_list.append(mirai.Image(url=component.url))
elif type(component) is nkc.Face:
yiri_msg_list.append(mirai.Face(face_id=component.id))
elif type(component) is nkc.At:
yiri_msg_list.append(mirai.At(target=component.qq))
elif type(component) is nkc.AtAll:
yiri_msg_list.append(mirai.AtAll())
else:
pass
logging.debug("转换后的消息链: " + str(yiri_msg_list))
chain = mirai.MessageChain(yiri_msg_list)
return chain
class NakuruProjectEventConverter(adapter_model.EventConverter):
"""事件转换器"""
@staticmethod
def yiri2target(event: typing.Type[mirai.Event]):
if event is mirai.GroupMessage:
return nakuru.GroupMessage
elif event is mirai.FriendMessage:
return nakuru.FriendMessage
else:
raise Exception("未支持转换的事件类型: " + str(event))
@staticmethod
def target2yiri(event: typing.Any) -> mirai.Event:
yiri_chain = NakuruProjectMessageConverter.target2yiri(event.message, event.message_id)
if type(event) is nakuru.FriendMessage: # 私聊消息事件
return mirai.FriendMessage(
sender=mirai.models.entities.Friend(
id=event.sender.user_id,
nickname=event.sender.nickname,
remark=event.sender.nickname
),
message_chain=yiri_chain,
time=event.time
)
elif type(event) is nakuru.GroupMessage: # 群聊消息事件
permission = "MEMBER"
if event.sender.role == "admin":
permission = "ADMINISTRATOR"
elif event.sender.role == "owner":
permission = "OWNER"
import mirai.models.entities as entities
return mirai.GroupMessage(
sender=mirai.models.entities.GroupMember(
id=event.sender.user_id,
member_name=event.sender.nickname,
permission=permission,
group=mirai.models.entities.Group(
id=event.group_id,
name=event.sender.nickname,
permission=entities.Permission.Member
),
special_title=event.sender.title,
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=yiri_chain,
time=event.time
)
else:
raise Exception("未支持转换的事件类型: " + str(event))
class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
"""nakuru-project适配器"""
bot: nakuru.CQHTTP
bot_account_id: int
message_converter: NakuruProjectMessageConverter = NakuruProjectMessageConverter()
event_converter: NakuruProjectEventConverter = NakuruProjectEventConverter()
listener_list: list[dict]
def __init__(self, cfg: dict):
"""初始化nakuru-project的对象"""
self.bot = nakuru.CQHTTP(**cfg)
self.listener_list = []
# nakuru库有bug,这个接口没法带access_token,会失败
# 所以目前自行发请求
config = context.get_config_manager().data
import requests
resp = requests.get(
url="http://{}:{}/get_login_info".format(config['nakuru_config']['host'], config['nakuru_config']['http_port']),
headers={
'Authorization': "Bearer " + config['nakuru_config']['token'] if 'token' in config['nakuru_config']else ""
},
timeout=5,
proxies=None
)
if resp.status_code == 403:
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匹配")
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(
self,
target_type: str,
target_id: str,
message: typing.Union[mirai.MessageChain, list],
converted: bool = False
):
task = None
converted_msg = self.message_converter.yiri2target(message) if not converted else message
# 检查是否有转发消息
has_forward = False
for msg in converted_msg:
if type(msg) is list: # 转发消息,仅回复此消息组件
has_forward = True
converted_msg = msg
break
if has_forward:
if target_type == "group":
task = self.bot.sendGroupForwardMessage(int(target_id), converted_msg)
elif target_type == "person":
task = self.bot.sendPrivateForwardMessage(int(target_id), converted_msg)
else:
raise Exception("Unknown target type: " + target_type)
else:
if target_type == "group":
task = self.bot.sendGroupMessage(int(target_id), converted_msg)
elif target_type == "person":
task = self.bot.sendFriendMessage(int(target_id), converted_msg)
else:
raise Exception("Unknown target type: " + target_type)
asyncio.run(task)
def reply_message(
self,
message_source: mirai.MessageEvent,
message: mirai.MessageChain,
quote_origin: bool = False
):
message = self.message_converter.yiri2target(message)
if quote_origin:
# 在前方添加引用组件
message.insert(0, nkc.Reply(
id=message_source.message_chain.message_id,
)
)
if type(message_source) is mirai.GroupMessage:
self.send_message(
"group",
message_source.sender.group.id,
message,
converted=True
)
elif type(message_source) is mirai.FriendMessage:
self.send_message(
"person",
message_source.sender.id,
message,
converted=True
)
else:
raise Exception("Unknown message source type: " + str(type(message_source)))
def is_muted(self, group_id: int) -> bool:
import time
# 检查是否被禁言
group_member_info = asyncio.run(self.bot.getGroupMemberInfo(group_id, self.bot_account_id))
return group_member_info.shut_up_timestamp > int(time.time())
def register_listener(
self,
event_type: typing.Type[mirai.Event],
callback: typing.Callable[[mirai.Event], None]
):
try:
logging.debug("注册监听器: " + str(event_type) + " -> " + str(callback))
# 包装函数
async def listener_wrapper(app: nakuru.CQHTTP, source: NakuruProjectAdapter.event_converter.yiri2target(event_type)):
callback(self.event_converter.target2yiri(source))
# 将包装函数和原函数的对应关系存入列表
self.listener_list.append(
{
"event_type": event_type,
"callable": callback,
"wrapper": listener_wrapper,
}
)
# 注册监听器
self.bot.receiver(self.event_converter.yiri2target(event_type).__name__)(listener_wrapper)
logging.debug("注册完成")
except Exception as e:
traceback.print_exc()
raise e
def unregister_listener(
self,
event_type: typing.Type[mirai.Event],
callback: typing.Callable[[mirai.Event], None]
):
nakuru_event_name = self.event_converter.yiri2target(event_type).__name__
new_event_list = []
# 从本对象的监听器列表中查找并删除
target_wrapper = None
for listener in self.listener_list:
if listener["event_type"] == event_type and listener["callable"] == callback:
target_wrapper = listener["wrapper"]
self.listener_list.remove(listener)
break
if target_wrapper is None:
raise Exception("未找到对应的监听器")
for func in self.bot.event[nakuru_event_name]:
if func.callable != target_wrapper:
new_event_list.append(func)
self.bot.event[nakuru_event_name] = new_event_list
def run_sync(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.bot.run()
def kill(self) -> bool:
return False
+123
View File
@@ -0,0 +1,123 @@
import asyncio
import typing
import mirai
import mirai.models.bus
from mirai.bot import MiraiRunner
from .. import adapter as adapter_model
class YiriMiraiAdapter(adapter_model.MessageSourceAdapter):
"""YiriMirai适配器"""
bot: mirai.Mirai
def __init__(self, config: dict):
"""初始化YiriMirai的对象"""
if 'adapter' not in config or \
config['adapter'] == 'WebSocketAdapter':
self.bot = mirai.Mirai(
qq=config['qq'],
adapter=mirai.WebSocketAdapter(
host=config['host'],
port=config['port'],
verify_key=config['verifyKey']
)
)
elif config['adapter'] == 'HTTPAdapter':
self.bot = mirai.Mirai(
qq=config['qq'],
adapter=mirai.HTTPAdapter(
host=config['host'],
port=config['port'],
verify_key=config['verifyKey']
)
)
else:
raise Exception('Unknown adapter for YiriMirai: ' + config['adapter'])
def send_message(
self,
target_type: str,
target_id: str,
message: mirai.MessageChain
):
"""发送消息
Args:
target_type (str): 目标类型,`person`或`group`
target_id (str): 目标ID
message (mirai.MessageChain): YiriMirai库的消息链
"""
task = None
if target_type == 'person':
task = self.bot.send_friend_message(int(target_id), message)
elif target_type == 'group':
task = self.bot.send_group_message(int(target_id), message)
else:
raise Exception('Unknown target type: ' + target_type)
asyncio.run(task)
def reply_message(
self,
message_source: mirai.MessageEvent,
message: mirai.MessageChain,
quote_origin: bool = False
):
"""回复消息
Args:
message_source (mirai.MessageEvent): YiriMirai消息源事件
message (mirai.MessageChain): YiriMirai库的消息链
quote_origin (bool, optional): 是否引用原消息. Defaults to False.
"""
asyncio.run(self.bot.send(message_source, message, quote_origin))
def is_muted(self, group_id: int) -> bool:
result = self.bot.member_info(target=group_id, member_id=self.bot.qq).get()
result = asyncio.run(result)
if result.mute_time_remaining > 0:
return True
return False
def register_listener(
self,
event_type: typing.Type[mirai.Event],
callback: typing.Callable[[mirai.Event], None]
):
"""注册事件监听器
Args:
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
"""
self.bot.on(event_type)(callback)
def unregister_listener(
self,
event_type: typing.Type[mirai.Event],
callback: typing.Callable[[mirai.Event], None]
):
"""注销事件监听器
Args:
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
"""
assert isinstance(self.bot, mirai.Mirai)
bus = self.bot.bus
assert isinstance(bus, mirai.models.bus.ModelEventBus)
bus.unsubscribe(event_type, callback)
def run_sync(self):
"""运行YiriMirai"""
# 创建新的
loop = asyncio.new_event_loop()
loop.run_until_complete(MiraiRunner(self.bot)._run())
def kill(self) -> bool:
return False
+1
View File
@@ -0,0 +1 @@
from .threadctl import ThreadCtl
+68
View File
@@ -0,0 +1,68 @@
import base64
import os
import json
import requests
def read_latest() -> list:
import pkg.utils.network as network
resp = requests.get(
url="https://api.github.com/repos/RockChinQ/QChatGPT/contents/res/announcement.json",
proxies=network.wrapper_proxies()
)
obj_json = resp.json()
b64_content = obj_json["content"]
# 解码
content = base64.b64decode(b64_content).decode("utf-8")
return json.loads(content)
def read_saved() -> list:
# 已保存的在res/announcement_saved
# 检查是否存在
if not os.path.exists("res/announcement_saved.json"):
with open("res/announcement_saved.json", "w", encoding="utf-8") as f:
f.write("[]")
with open("res/announcement_saved.json", "r", encoding="utf-8") as f:
content = f.read()
return json.loads(content)
def write_saved(content: list):
# 已保存的在res/announcement_saved
with open("res/announcement_saved.json", "w", encoding="utf-8") as f:
f.write(json.dumps(content, indent=4, ensure_ascii=False))
def fetch_new() -> list:
latest = read_latest()
saved = read_saved()
to_show: list = []
for item in latest:
# 遍历saved检查是否有相同id的公告
for saved_item in saved:
if saved_item["id"] == item["id"]:
break
else:
# 没有相同id的公告
to_show.append(item)
write_saved(latest)
return to_show
if __name__ == '__main__':
resp = requests.get(
url="https://api.github.com/repos/RockChinQ/QChatGPT/contents/res/announcement.json",
)
obj_json = resp.json()
b64_content = obj_json["content"]
# 解码
content = base64.b64decode(b64_content).decode("utf-8")
print(json.dumps(json.loads(content), indent=4, ensure_ascii=False))
View File
+88
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
+55
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,
}
}
)
+65
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,
}
}
)
+88
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
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
+94 -14
View File
@@ -1,50 +1,130 @@
from __future__ import annotations
import threading
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
from .center import v2 as center_v2
context = { context = {
'inst': { 'inst': {
'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,
'logger_handler': None, 'logger_handler': None,
'config': None, 'config': None,
'plugin_host': None, 'plugin_host': None,
} }
context_lock = threading.Lock()
### context耦合度非常高,需要大改 ###
def set_config(inst): def set_config(inst):
context_lock.acquire()
context['config'] = inst context['config'] = inst
context_lock.release()
def get_config(): def get_config():
return context['config'] context_lock.acquire()
t = context['config']
context_lock.release()
return t
def set_database_manager(inst): def set_database_manager(inst: db_mgr.DatabaseManager):
context_lock.acquire()
context['inst']['database.manager.DatabaseManager'] = inst context['inst']['database.manager.DatabaseManager'] = inst
context_lock.release()
def get_database_manager(): def get_database_manager() -> db_mgr.DatabaseManager:
return context['inst']['database.manager.DatabaseManager'] context_lock.acquire()
t = context['inst']['database.manager.DatabaseManager']
context_lock.release()
return t
def set_openai_manager(inst): def set_openai_manager(inst: openai_mgr.OpenAIInteract):
context_lock.acquire()
context['inst']['openai.manager.OpenAIInteract'] = inst context['inst']['openai.manager.OpenAIInteract'] = inst
context_lock.release()
def get_openai_manager(): def get_openai_manager() -> openai_mgr.OpenAIInteract:
return context['inst']['openai.manager.OpenAIInteract'] context_lock.acquire()
t = context['inst']['openai.manager.OpenAIInteract']
context_lock.release()
return t
def set_qqbot_manager(inst): def set_qqbot_manager(inst: qqbot_mgr.QQBotManager):
context_lock.acquire()
context['inst']['qqbot.manager.QQBotManager'] = inst context['inst']['qqbot.manager.QQBotManager'] = inst
context_lock.release()
def get_qqbot_manager(): def get_qqbot_manager() -> qqbot_mgr.QQBotManager:
return context['inst']['qqbot.manager.QQBotManager'] context_lock.acquire()
t = context['inst']['qqbot.manager.QQBotManager']
context_lock.release()
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['plugin_host'] = inst context['plugin_host'] = inst
context_lock.release()
def get_plugin_host(): def get_plugin_host() -> plugin_host.PluginHost:
return context['plugin_host'] context_lock.acquire()
t = context['plugin_host']
context_lock.release()
return t
def set_thread_ctl(inst: threadctl.ThreadCtl):
context_lock.acquire()
context['pool_ctl'] = inst
context_lock.release()
def get_thread_ctl() -> threadctl.ThreadCtl:
context_lock.acquire()
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
-13
View File
@@ -1,13 +0,0 @@
# OpenAI账号免费额度剩余查询
import requests
def fetch_credit_data(api_key: str) -> dict:
"""OpenAI账号免费额度剩余查询"""
resp = requests.get(
url="https://api.openai.com/dashboard/billing/credit_grants",
headers={
"Authorization": "Bearer {}".format(api_key),
}
)
return resp.json()
+67
View File
@@ -0,0 +1,67 @@
import os
import time
import logging
import shutil
from . import context
log_file_name = "qchatgpt.log"
log_colors_config = {
'DEBUG': 'green', # cyan white
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'cyan',
}
def init_runtime_log_file():
"""为此次运行生成日志文件
格式: qchatgpt-yyyy-MM-dd-HH-mm-ss.log
"""
global log_file_name
# 检查logs目录是否存在
if not os.path.exists("logs"):
os.mkdir("logs")
log_file_name = "logs/qchatgpt-%s.log" % time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
def reset_logging():
global log_file_name
import pkg.utils.context
import colorlog
if pkg.utils.context.context['logger_handler'] is not None:
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
for handler in logging.getLogger().handlers:
logging.getLogger().removeHandler(handler)
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日志输出的文件位置和文件名
format="[%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s",
# 日志输出的格式
# -8表示占位符,让输出左对齐,输出长度都为8位
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
)
sh = logging.StreamHandler()
sh.setLevel(logging_level)
sh.setFormatter(colorlog.ColoredFormatter(
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
"%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors=log_colors_config
))
logging.getLogger().addHandler(sh)
pkg.utils.context.context['logger_handler'] = sh
return sh
+11
View File
@@ -0,0 +1,11 @@
from . import context
def wrapper_proxies() -> dict:
"""获取代理"""
config = context.get_config_manager().data
return {
"http": config['openai_config']['proxy'],
"https": config['openai_config']['proxy']
} if 'proxy' in config['openai_config'] and (config['openai_config']['proxy'] is not None) else None
+11 -5
View File
@@ -1,21 +1,27 @@
from pip._internal import main as pipmain from pip._internal import main as pipmain
import main from . import log
def install(package): def install(package):
pipmain(['install', package]) pipmain(['install', package])
main.reset_logging() log.reset_logging()
def install_upgrade(package):
pipmain(['install', '--upgrade', package, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
log.reset_logging()
def run_pip(params: list): def run_pip(params: list):
pipmain(params) pipmain(params)
main.reset_logging() log.reset_logging()
def install_requirements(file): def install_requirements(file):
pipmain(['install', '-r', file, "--upgrade"]) pipmain(['install', '-r', file, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
main.reset_logging() "--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
log.reset_logging()
def ensure_dulwich(): def ensure_dulwich():
+7
View File
@@ -0,0 +1,7 @@
import os
import sys
def get_platform() -> str:
"""获取当前平台"""
return sys.platform
+32 -11
View File
@@ -1,10 +1,10 @@
import logging import logging
import threading
import importlib import importlib
import pkgutil import pkgutil
import pkg.utils.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,36 +15,57 @@ 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=['']))
def reload_all(notify=True): def reload_all(notify=True):
# 解除bot的事件注册 # 解除bot的事件注册
import pkg import pkg
pkg.utils.context.get_qqbot_manager().unsubscribe_all() context.get_qqbot_manager().unsubscribe_all()
# 执行关闭流程 # 执行关闭流程
logging.info("执行程序关闭流程") logging.info("执行程序关闭流程")
import main import main
main.stop() main.stop()
# 删除所有已注册的命令
import pkg.qqbot.cmds.aamgr as cmdsmgr
cmdsmgr.__command_list__ = {}
cmdsmgr.__tree_index__ = {}
# 重载所有模块 # 重载所有模块
pkg.utils.context.context['exceeded_keys'] = pkg.utils.context.get_openai_manager().key_mgr.exceeded context.context['exceeded_keys'] = context.get_openai_manager().key_mgr.exceeded
context = pkg.utils.context.context this_context = context.context
walk(pkg) walk(pkg)
importlib.reload(__import__("config-template"))
importlib.reload(__import__('config')) importlib.reload(__import__('config'))
importlib.reload(__import__('main')) importlib.reload(__import__('main'))
importlib.reload(__import__('banlist')) importlib.reload(__import__('banlist'))
pkg.utils.context.context = context importlib.reload(__import__('tips'))
context.context = this_context
# 重载插件 # 重载插件
import plugins import plugins
walk(plugins) walk(plugins)
# 初始化相关文件
main.check_file()
# 执行启动流程 # 执行启动流程
logging.info("执行程序启动流程") logging.info("执行程序启动流程")
threading.Thread(target=main.main, args=(False,), daemon=False).start()
context.get_thread_ctl().reload(
admin_pool_num=4,
user_pool_num=8
)
def run_wrapper():
asyncio.run(main.start_process(False))
context.get_thread_ctl().submit_sys_task(
run_wrapper
)
logging.info('程序启动完成') logging.info('程序启动完成')
if notify: if notify:
pkg.utils.context.get_qqbot_manager().notify_admin("重载完成") context.get_qqbot_manager().notify_admin("重载完成")

Some files were not shown because too many files have changed in this diff Show More