Compare commits

..

435 Commits

Author SHA1 Message Date
Junyan Qin
0094056def chore: release v3.4.0.1 2024-11-22 23:55:24 +08:00
Junyan Qin
a9f305a1c6 feat: 添加对pydantic v1的兼容性 2024-11-22 23:37:46 +08:00
Junyan Qin
e8cc048901 deps: bump pydantic to v2 2024-11-22 23:29:12 +08:00
Junyan Qin
05da43f606 chore: 更新模型信息 2024-11-22 22:33:05 +08:00
Junyan Qin
a81faa7d8e fix: gitee ai 配置 schema 2024-11-22 20:01:35 +08:00
Junyan Qin
18ba7d1da7 Merge pull request #929 from RockChinQ/feat/gitee-ai
feat: 添加对 Gitee AI 的支持
2024-11-22 19:59:25 +08:00
Junyan Qin
875adfcbaa feat: 添加对 Gitee AI 的支持 2024-11-21 23:28:19 +08:00
Junyan Qin
6e9c213893 fix: 登录失败时无提示 2024-11-20 21:03:02 +08:00
Junyan Qin
753066ccb9 fix: webui 访问提示在Windows上的编码问题 2024-11-19 19:33:58 +08:00
Junyan Qin
8b36782c25 chore: 更新 docker-compose.yaml 2024-11-18 23:31:49 +08:00
Junyan Qin
da9dde6bd2 doc: update README 2024-11-17 21:30:53 +08:00
Junyan Qin
07f6e69b93 doc: update README.md 2024-11-17 21:11:21 +08:00
Junyan Qin
31a7503df3 chore: release v3.4.0 2024-11-17 20:48:45 +08:00
Junyan Qin
11db8d8d17 Merge pull request #904 from RockChinQ/version/3.4.0
Version/3.4.0
2024-11-17 20:47:25 +08:00
Junyan Qin
93ee8d51bc Merge branch 'master' into version/3.4.0 2024-11-17 20:45:24 +08:00
Junyan Qin
83e80f324e perf: webui 文件存在性检查 2024-11-17 20:43:40 +08:00
Junyan Qin
c51eac717e doc: 修复 README 的死链 2024-11-17 20:37:42 +08:00
Junyan Qin
db7d5dcce3 chore: 替换多处 qchatgpt.rockchin.top 2024-11-17 20:35:39 +08:00
Junyan Qin
0d25578e22 perf: 配置文件页放到单独组件 2024-11-17 19:54:07 +08:00
Junyan Qin
1a457be823 Merge pull request #921 from RockChinQ/feat/authenticating
Feat: 用户鉴权
2024-11-17 19:13:11 +08:00
Junyan Qin
20e3edba8f feat: 用户账户系统 2024-11-17 19:11:44 +08:00
Junyan Qin
036c2182a5 chore: 修改aiocqhttp适配器默认端口为2280 2024-11-17 10:18:56 +08:00
Junyan Qin
6238f430e8 Merge pull request #900 from RockChinQ/feat/webui
Feat: webui
2024-11-16 19:27:35 +08:00
Junyan Qin
9fc891ec01 chore: Hello LangBot ! 2024-11-16 17:57:39 +08:00
Junyan Qin
491d977d9e ci: fix 2024-11-16 17:47:50 +08:00
Junyan Qin
9a4bcda9bc ci: 添加 build-artifacts 工作流在 release 分布时执行 2024-11-16 17:44:02 +08:00
Junyan Qin
2c2374a763 ci: fix 2024-11-16 17:39:34 +08:00
Junyan Qin
a76e0b287e ci: typo 2024-11-16 17:36:33 +08:00
Junyan Qin
1d6f1e3c7c fix: chore 2024-11-16 17:34:15 +08:00
Junyan Qin
896fd982a1 ci: upload artifacts 2024-11-16 17:33:24 +08:00
Junyan Qin
c031ab20da Merge pull request #920 from RockChinQ/feat/lifetime-controlling
Feat: 生命周期和热重载
2024-11-16 17:19:42 +08:00
Junyan Qin
318b6e6bf1 typo: minor fix 2024-11-16 17:16:40 +08:00
Junyan Qin
ca3999d251 feat: 插件文件更改热重载 2024-11-16 16:45:13 +08:00
Junyan Qin
658eb278c4 refactor: 重构部分插件管理逻辑 2024-11-16 16:13:02 +08:00
Junyan Qin
bb219889e5 feat: 消息平台热重载 2024-11-16 12:40:57 +08:00
Junyan Qin
3239c9ec3f feat: 彻底移除 yirimirai 2024-11-15 20:03:49 +08:00
Junyan Qin
16153dc573 perf: 设置页标题改为小写 2024-11-12 20:04:03 +08:00
Junyan Qin
e0d9a295ab perf: 优化部分 UI 显示 2024-11-12 18:57:43 +08:00
Junyan Qin
eabdda5eb1 feat: 改为 WebHashHistory 2024-11-12 18:29:37 +08:00
Junyan Qin
43f45f9184 feat: 修改 apibase 2024-11-12 18:14:53 +08:00
Junyan Qin
7c19785a17 fix: http_proxy 环境变量为空检查 2024-11-12 17:56:59 +08:00
Junyan Qin
78005f8b4e ci: 删除 refs-heads 2024-11-12 17:49:00 +08:00
Junyan Qin
0d4784d098 feat: 代理设置同步到环境变量 2024-11-11 19:12:30 +08:00
Junyan Qin
805454e037 chore: 更新 docker-compose.yaml 2024-11-10 16:37:44 +08:00
Junyan Qin
bf383bbf9c ci: fix 2024-11-10 16:29:40 +08:00
Junyan Qin
73ffd67792 ci: 添加构建 ci 2024-11-10 16:27:50 +08:00
Junyan Qin
54bbfc8eda perf: dashboard 添加图表更新提示 2024-11-10 15:38:06 +08:00
Junyan Qin
a3e234c979 perf: debug模式改为绿色 2024-11-10 12:03:34 +08:00
Junyan Qin
9336abff8b perf: 使用 json-editor-vue 作为json编辑器 2024-11-10 11:46:41 +08:00
Junyan Qin
0fe161cd7f pref: 优化日志显示 2024-11-10 11:04:29 +08:00
Junyan Qin
7cc55eab3e feat: 仪表盘基本数据 2024-11-10 00:05:47 +08:00
Junyan Qin
15482e398b feat: 插件删除功能 2024-11-09 23:25:26 +08:00
Junyan Qin
601fa0ac7f feat: 关于 LangBot 对话框 2024-11-09 22:44:56 +08:00
Junyan Qin
2819da5f2f fix: github下载未使用环境变量代理 2024-11-09 18:09:39 +08:00
Junyan Qin
3cb3562477 doc(README): fix deadlinks 2024-11-08 23:14:52 +08:00
Junyan Qin
cee205994f doc: update logo 2024-11-05 15:42:48 +08:00
Junyan Qin
e44df0a3dd feat: dashboard 基本组件 2024-11-04 21:54:02 +08:00
Junyan Qin
84a51cb26d feat: 插件安装支持 2024-11-04 00:01:07 +08:00
Junyan Qin
db02d9c126 feat: 完成任务列表功能 2024-11-03 23:22:33 +08:00
Junyan Qin
709b86b724 refactor: 使插件更新过程全异步 2024-11-03 22:27:31 +08:00
Junyan Qin
68184b0e47 Merge pull request #911 from RockChinQ/feat/trackable-async-tasks
Feat: 用户级任务系统
2024-11-01 22:42:11 +08:00
Junyan Qin
6d2a4c038d feat: 完成异步任务跟踪架构基础 2024-11-01 22:41:26 +08:00
Junyan Qin
2f05f5b456 feat: 添加任务列表框架 2024-10-24 18:28:57 +08:00
Junyan Qin
d5e3120350 chore: 确保 pydantic<2.0 2024-10-24 14:26:18 +08:00
Junyan Qin
a4589327a6 feat: 添加 python 版本检查 2024-10-22 18:17:09 +08:00
Junyan Qin
c151665419 feat: 添加任务管理模块 2024-10-22 18:09:18 +08:00
Junyan Qin
947790e8d1 Update README.md 2024-10-22 13:51:05 +08:00
Junyan Qin
26770439bb fix: 关闭编排对话框时错误的插件顺序 2024-10-21 19:18:40 +08:00
Junyan Qin
7da9171dde feat: 插件优先级更改功能 2024-10-20 22:20:35 +08:00
Junyan Qin
16b386eaf7 feat: 插件页展示功能 2024-10-19 18:38:01 +08:00
Junyan Qin
c330aab48b Merge pull request #902 from RockChinQ/feat/settings-form-render
Feat: 设置项可视化编辑器
2024-10-16 22:32:40 +08:00
Junyan Qin
5f998a0852 perf: settings 页面的一些提示 2024-10-16 22:24:15 +08:00
Junyan Qin
c3dfbb64a6 feat: 异常处理 2024-10-16 21:55:55 +08:00
Junyan Qin
3db52282b8 fix: 修复子字段值为空时导致字段丢失的问题 2024-10-16 16:08:58 +08:00
Junyan Qin
a313ae5f97 feat: 添加多个可视化编辑schema 2024-10-16 15:34:30 +08:00
Junyan Qin
18cce189a4 feat: 完成 pipeline 的可视化配置 2024-10-16 13:57:41 +08:00
Junyan Qin
fb308d576b fix(settings): 切换tab时的异步问题 2024-10-16 12:58:52 +08:00
Junyan Qin
8c976303a4 feat: system.json 的可视化编辑 2024-10-15 21:42:05 +08:00
Junyan Qin
12f1f3609d feat: 引入 vjsf 渲染表单 2024-10-15 16:16:39 +08:00
Junyan Qin
661fdeb6a1 perf: 重新切换到 settings tab 时加载之前编辑的内容 2024-10-15 14:28:06 +08:00
Junyan Qin
d52f9b9543 feat(settings): json 编辑器 2024-10-15 14:23:56 +08:00
Junyan Qin
7174742886 feat: settings 基础组件 2024-10-15 00:07:40 +08:00
Junyan Qin
cd0a8fb24b perf: 使内容背景稍微灰一些 2024-10-14 21:30:10 +08:00
Junyan Qin
1fbc92bc6d perf: 首页展示版本信息 2024-10-14 21:18:36 +08:00
Junyan Qin
231dca956d feat: 日志页面 2024-10-14 18:52:28 +08:00
RockChinQ
0dd74c825b feat: 前端基础框架 2024-10-13 22:34:35 +08:00
RockChinQ
9703fc0366 perf: 优化日志增量获取逻辑 2024-10-13 22:33:51 +08:00
RockChinQ
7c3557e943 feat: 持久化和 web 接口基础架构 2024-10-11 22:27:53 +08:00
RockChinQ
21f153e5c3 chore: webui 前端模板 2024-10-11 22:23:08 +08:00
Junyan Qin
ea6a0af5a7 Merge pull request #890 from RockChinQ/feat/more-platforms
Refactor: 移除 YiriMirai 组件
2024-09-26 14:41:03 +08:00
RockChinQ
c53ffaca6c fix: 处理插件 import mirai 时的兼容性问题 2024-09-26 14:38:18 +08:00
RockChinQ
3469515e04 feat: 删除代码中对 mirai 的引用 2024-09-26 13:01:45 +08:00
RockChinQ
e8da26cb8a fix: missing break 2024-09-26 11:23:37 +08:00
RockChinQ
1235fc1339 chore: release v3.3.1.1 2024-09-26 10:39:35 +08:00
Junyan Qin
47e308b99d Merge pull request #889 from YunZLu/add-check-role
Fix: Add Role Check to Prevent Validation Error
2024-09-26 09:31:25 +08:00
RockChinQ
fdba470e9a perf: 将 platform 的 组件导入包 __init__ 中 2024-09-26 00:28:57 +08:00
Junyan Qin
a1ccceefd2 Merge branch 'master' into feat/more-platforms 2024-09-26 00:26:17 +08:00
RockChinQ
1c4a700d92 refactor: 将 yirimirai 的组件集成进 platform 包 2024-09-26 00:23:03 +08:00
YunZL
81c2c3c0e5 Add Role Check to Prevent Validation Error 2024-09-23 23:25:54 +08:00
Junyan Qin
3c2db5097a Merge pull request #888 from Tigrex-Dai/master
fix: 添加了针对报错内容对event.sender中'role'的存在性检查
2024-09-22 16:50:55 +08:00
Tigrex Dai
ce56f79687 Update aiocqhttp.py
针对报错对"role"做存在性检查
2024-09-22 15:39:48 +08:00
RockChinQ
ee0d6dcdae chore: release v3.3.1.0 2024-09-08 15:14:24 +08:00
Junyan Qin
bcf1d92f73 Merge pull request #881 from RockChinQ/version/3.3.1.0
Version/3.3.1.0
2024-09-08 15:13:39 +08:00
RockChinQ
ffdec16ce6 docs: wiki 所有页面加上已弃用说明 2024-09-08 14:52:35 +08:00
RockChinQ
b2f6e84adc typo: 优化插件执行日志信息 2024-09-08 14:51:39 +08:00
Junyan Qin
f76c457e1f Update README.md 2024-09-03 20:07:41 +08:00
RockChinQ
80bd0a20df doc: 修复 README 中的logo图片 2024-08-30 14:48:23 +08:00
RockChinQ
efeaf73339 doc: 修改README图片链接 2024-08-30 11:13:04 +08:00
Junyan Qin
91b5100a24 Merge pull request #872 from RockChinQ/feat/config-file-api
Feat: 添加yaml配置文件的支持
2024-08-24 20:55:19 +08:00
RockChinQ
d1a06f4730 feat: 添加yaml配置文件的支持 2024-08-24 20:54:36 +08:00
Junyan Qin
b0b186e951 Merge pull request #871 from RockChinQ/feat/qq-c2c
Feat: 添加对 QQ 官方 API 私聊场景的支持
2024-08-24 17:04:41 +08:00
RockChinQ
4c8fedef6e feat: QQ官方api群聊和私聊支持图片 2024-08-24 17:01:35 +08:00
RockChinQ
718c221d01 feat: 支持官方机器人私信接口 2024-08-24 16:26:47 +08:00
Junyan Qin
077e77eee5 Merge pull request #869 from ligen131/lg/fix_image_format
fix: 发送正确的图片格式而不是默认的 `image/jpeg`
2024-08-24 15:47:55 +08:00
ligen131
b51ca06c7c fix: 发送正确的图片格式而不是默认的 image/jpeg 2024-08-19 00:00:29 +08:00
RockChinQ
2f092f4a87 chore: release v3.3.0.2 2024-08-01 23:14:07 +08:00
Junyan Qin
f1ff9c05c4 Merge pull request #864 from RockChinQ/version/3.3.0.2
fix: 消息忽略规则失效 (#854)
2024-08-01 23:12:33 +08:00
RockChinQ
c9c8603ccc fix: 消息忽略规则失效 (#854) 2024-08-01 23:01:28 +08:00
RockChinQ
47e281fb61 chore: release v3.3.0.1 2024-07-28 22:47:49 +08:00
RockChinQ
dc625647eb fix: ollama 依赖检查 2024-07-28 22:47:19 +08:00
RockChinQ
66cf1b05be chore: 优化issue和pr模板 2024-07-28 21:32:22 +08:00
RockChinQ
622cc89414 chore: release v3.3.0 2024-07-28 20:58:29 +08:00
Junyan Qin
78d98c40b1 Merge pull request #847 from RockChinQ/version/3.3
Release: 3.3
2024-07-28 20:57:26 +08:00
RockChinQ
1c5f06d9a9 feat: 添加 reply 和 send_message 两个插件api方法 2024-07-28 20:23:52 +08:00
Junyan Qin
998fe5a980 Merge pull request #857 from RockChinQ/feat/runner-abstraction
Feat: Runner 组件抽象
2024-07-28 18:47:38 +08:00
RockChinQ
8cad4089a7 feat: runner 层抽象 (#839) 2024-07-28 18:45:27 +08:00
RockChinQ
48cc3656bd feat: 允许自定义命令前缀 2024-07-28 16:01:58 +08:00
RockChinQ
68ddb3a6e1 feat: 添加 model 命令 2024-07-28 15:46:09 +08:00
ElvisChenML
70583f5ba0 Fixed aiocqhttp mirai.Voice类型无法正确传递url及base64的异常 2024-07-28 15:08:33 +08:00
Junyan Qin
5bebe01dd0 Update README.md 2024-07-28 15:08:33 +08:00
Junyan Qin
4dd976c9c5 Merge pull request #856 from ElvisChenML/pr
Fixed aiocqhttp mirai.Voice类型无法正确传递url及base64的异常
2024-07-28 13:05:06 +08:00
ElvisChenML
221b310485 Fixed aiocqhttp mirai.Voice类型无法正确传递url及base64的异常 2024-07-25 16:14:24 +08:00
Junyan Qin
dd1cec70c0 Update README.md 2024-07-13 09:15:18 +08:00
Junyan Qin
7656443b28 Merge pull request #845 from ElvisChenML/pr
fixed pkg\provider\entities.py\get_content_mirai_message_chain中ce.type图片类型不正确的异常
2024-07-10 00:13:48 +08:00
Junyan Qin
9d91c13b12 Merge pull request #844 from canyuan0801/pr
Feat: Ollama平台集成
2024-07-10 00:09:48 +08:00
RockChinQ
7c06141ce2 perf(ollama): 优化命令显示细节 2024-07-10 00:07:32 +08:00
RockChinQ
3dc413638b feat(ollama): 配置文件迁移 2024-07-09 23:37:34 +08:00
RockChinQ
bdb8baeddd perf(ollama): 修改请求器名称以适配请求路径 2024-07-09 23:37:19 +08:00
ElvisChenML
21966bfb69 fixed pkg\provider\entities.py\get_content_mirai_message_chain中ce.type图片类型不正确的异常 2024-07-09 17:04:11 +08:00
canyuan
e78c82e999 mod: merge ollama cmd 2024-07-09 16:19:09 +08:00
canyuan
2bdc3468d1 add ollama cmd 2024-07-09 14:57:39 +08:00
canyuan
987b3dc4ef add ollama chat 2024-07-09 14:57:28 +08:00
RockChinQ
45a10b4ac7 chore: release v3.2.4 2024-07-05 18:19:10 +08:00
RockChinQ
b5d33ef629 perf: 优化 pipeline 处理时的报错 2024-07-04 13:03:58 +08:00
RockChinQ
d3629916bf fix: user_notice 处理时为对齐为 MessageChain (#809) 2024-07-04 12:47:55 +08:00
RockChinQ
c5cb26d295 fix: GroupNormalMessageReceived事件设置 alter 无效 (#803) 2024-07-03 23:16:16 +08:00
RockChinQ
4b2785c5eb fix: QQ 官方 API 图片识别功能不正常 (#825) 2024-07-03 22:36:35 +08:00
RockChinQ
7ed190e6d2 doc: 删除广告 2024-07-03 17:50:58 +08:00
Junyan Qin
eac041cdd2 Merge pull request #834 from RockChinQ/feat/env-reminder
Feat: 添加启动信息阶段
2024-07-03 17:45:56 +08:00
RockChinQ
05527cfc01 feat: 添加 windows 下针对选择模式的提示 2024-07-03 17:44:10 +08:00
RockChinQ
61e2af4a14 feat: 添加启动信息阶段 2024-07-03 17:34:23 +08:00
RockChinQ
79804b6ecd chore: release 3.2.3 2024-06-26 10:55:21 +08:00
Junyan Qin
76434b2f4e Merge pull request #829 from RockChinQ/version/3.2.3
Release 3.2.3
2024-06-26 10:54:42 +08:00
RockChinQ
ec8bd4922e fix: 错误地resprule选择逻辑 (#810) 2024-06-26 10:37:08 +08:00
RockChinQ
4ffa773fac fix: 前缀响应时图片被错误地转换为文字 (#820) 2024-06-26 10:15:21 +08:00
Junyan Qin
ea8b7bc8aa Merge pull request #818 from Huoyuuu/master
fix: ensure content is string in chatcmpl call method
2024-06-24 17:12:46 +08:00
RockChinQ
39ce5646f6 perf: content元素拼接时使用换行符间隔 2024-06-24 17:04:50 +08:00
Huoyuuu
5092a82739 Update chatcmpl.py 2024-06-19 19:13:00 +08:00
Huoyuuu
3bba0b6d9a Merge pull request #1 from Huoyuuu/fix/issue-817-ensure-content-string
fix: ensure content is string in chatcmpl call method
2024-06-19 17:32:30 +08:00
Huoyuuu
7a19dd503d fix: ensure content is string in chatcmpl call method
fix: ensure content is string in chatcmpl call method

- Ensure user message content is a string instead of an array
- Updated `call` method in `chatcmpl.py` to guarantee content is a string
- Resolves compatibility issue with the yi-large model
2024-06-19 17:26:06 +08:00
RockChinQ
9e6a01fefd chore: release v3.2.2 2024-05-31 19:20:34 +08:00
RockChinQ
933471b4d9 perf: 启动失败时输出完整traceback (#799) 2024-05-31 15:37:56 +08:00
RockChinQ
f81808d239 perf: 添加JSON配置文件语法检查 (#796) 2024-05-29 21:11:21 +08:00
RockChinQ
96832b6f7d perf: 忽略空的 assistant content 消息 (#795) 2024-05-29 21:00:48 +08:00
Junyan Qin
e2eb0a84b0 Merge pull request #797 from RockChinQ/feat/context-truncater
Feat: 消息截断器
2024-05-29 20:38:14 +08:00
RockChinQ
c8eb2e3376 feat: 消息截断器 2024-05-29 20:34:49 +08:00
Junyan Qin
21fe5822f9 Merge pull request #794 from RockChinQ/perf/advanced-fixwin
Feat: fixwin限速支持设置窗口大小
2024-05-26 10:33:49 +08:00
RockChinQ
d49cc9a7a3 feat: fixwin限速支持设置窗口大小 (#791) 2024-05-26 10:29:10 +08:00
Junyan Qin
910d0bfae1 Update README.md 2024-05-25 12:27:27 +08:00
RockChinQ
d6761949ca chore: release v3.2.1 2024-05-23 16:29:26 +08:00
RockChinQ
6afac1f593 feat: 允许指定遥测服务器url 2024-05-23 16:25:51 +08:00
RockChinQ
4d1a270d22 doc: 添加qcg-center源码链接 2024-05-23 16:16:13 +08:00
Junyan Qin
a7888f5536 Merge pull request #787 from RockChinQ/perf/claude-ability
Perf: Claude 的能力完善支持
2024-05-22 20:33:39 +08:00
RockChinQ
b9049e91cf chore: 同步 llm-models.json 2024-05-22 20:31:46 +08:00
RockChinQ
7db56c8e77 feat: claude 支持视觉 2024-05-22 20:09:29 +08:00
Junyan Qin
50563cb957 Merge pull request #785 from RockChinQ/fix/msg-chain-compability
Fix: 修复 query.resp_messages 对插件reply的兼容性
2024-05-18 20:13:50 +08:00
RockChinQ
18ae2299a7 fix: 修复 query.resp_messages 对插件reply的兼容性 2024-05-18 20:08:48 +08:00
RockChinQ
7463e0aab9 perf: 删除多个地方残留的 config.py 字段 (#781) 2024-05-18 18:52:45 +08:00
Junyan Qin
c92d47bb95 Merge pull request #779 from jerryliang122/master
修复aiocqhttp的图片错误
2024-05-17 17:05:58 +08:00
RockChinQ
0b1af7df91 perf: 统一判断方式 2024-05-17 17:05:20 +08:00
jerryliang122
a9104eb2da 通过base64编码发送,修复cqhttp无法发送图片 2024-05-17 08:20:06 +00:00
RockChinQ
abbd15d5cc chore: release v3.2.0.1 2024-05-17 09:48:20 +08:00
RockChinQ
aadfa14d59 fix: claude 请求失败 2024-05-17 09:46:06 +08:00
Junyan Qin
4cd10bbe25 Update README.md 2024-05-16 22:17:46 +08:00
RockChinQ
1d4a6b71ab chore: release v3.2.0 2024-05-16 21:22:40 +08:00
Junyan Qin
a7f830dd73 Merge pull request #773 from RockChinQ/feat/multi-modal
Feat: 多模态
2024-05-16 21:13:15 +08:00
RockChinQ
bae86ac05c chore: 恢复版本号 2024-05-16 21:03:56 +08:00
RockChinQ
a3706bfe21 perf: 细节优化 2024-05-16 21:02:59 +08:00
RockChinQ
91e23b8c11 perf: 为图片base64函数添加lru 2024-05-16 20:52:17 +08:00
RockChinQ
37ef1c9fab feat: 删除oss相关代码 2024-05-16 20:32:30 +08:00
RockChinQ
6bc6f77af1 feat: 通过 base64 传输图片 2024-05-16 20:25:51 +08:00
RockChinQ
2c478ccc25 feat: 模型vision支持性参数 2024-05-16 20:11:54 +08:00
RockChinQ
404e5492a3 chore: 同步现有模型信息 2024-05-16 18:29:23 +08:00
RockChinQ
d5b5d667a5 feat: 模型视觉多模态支持 2024-05-15 21:40:18 +08:00
RockChinQ
8807f02f36 perf: resp_message_chain 改为 list 类型 (#770) 2024-05-14 23:08:49 +08:00
RockChinQ
269e561497 perf: messages 存回 conversation 应该仅在成功执行本次请求时执行 (#769) 2024-05-14 22:41:39 +08:00
RockChinQ
527ad81d38 feat: 解藕chat的处理器和请求器 (#772) 2024-05-14 22:20:31 +08:00
Junyan Qin
972d3c18af Update README.md 2024-05-08 21:49:45 +08:00
Junyan Qin
3cbfc078fc doc(README.md): 更新 社区四群群号 2024-05-08 21:46:19 +08:00
RockChinQ
fde6822b5c chore: release v3.1.1 2024-05-08 02:28:40 +00:00
Junyan Qin
930321bcf1 Merge pull request #762 from RockChinQ/feat/deepseek
Feat: 支持 deepseek 模型
2024-05-07 22:48:37 +08:00
RockChinQ
c45931363a feat: deepseek配置迁移 2024-05-07 14:45:59 +00:00
RockChinQ
9c6491e5ee feat: 支持 deepseek 的模型 2024-05-07 14:28:52 +00:00
RockChinQ
9bc248f5bc feat: 删除submit-messages-tokens配置项 2024-05-07 12:32:54 +00:00
Junyan Qin
becac2fde5 doc(README.md): 添加 GitHub Trending 徽标 2024-04-29 21:00:22 +08:00
RockChinQ
1e1a103882 feat: aiocqhttp允许使用图片链接作为参数 2024-04-11 03:26:12 +00:00
RockChinQ
e5cffb7c9b chore: release v3.1.0.4 2024-04-06 16:51:15 +08:00
RockChinQ
e2becf7777 feat: 删除父进程判断 (#750) 2024-04-06 16:50:35 +08:00
RockChinQ
a6b875a242 fix: GroupMessageReceived 事件参数错误 2024-04-04 16:50:45 +08:00
RockChinQ
b5e67f3df8 fix: 内容函数调用时错误地传递了RuntimeContainer 2024-04-04 15:08:40 +08:00
RockChinQ
2093fb16a7 chore: release v3.1.0.3 2024-04-02 22:33:36 +08:00
RockChinQ
fc9a9d2386 fix: 缺失的 psutil 依赖 2024-04-02 22:33:06 +08:00
RockChinQ
5e69f78f7e chore: 不再支持python 3.9 2024-04-01 18:16:49 +08:00
RockChinQ
6919bece77 chore: release v3.1.0.2 2024-03-31 14:41:32 +08:00
RockChinQ
8b003739f1 feat: message.content 支持 mirai.MessageChain 对象 (#741) 2024-03-31 14:38:15 +08:00
RockChinQ
2e9229a6ad fix: 工作目录必须在 main.py 目录 2024-03-30 21:34:22 +08:00
RockChinQ
5a3e7fe8ee perf: 禁止双击运行 2024-03-30 21:28:42 +08:00
RockChinQ
7b3d7e7bd6 fix: json配置文件错误的加载流程 2024-03-30 19:01:59 +08:00
Junyan Qin
fdd7c1864d feat(chatcmpl): 对函数调用进行异常捕获 (#749) 2024-03-30 09:45:30 +00:00
Junyan Qin
cac5a5adff fix(qq-botpy): 群内单query多回复时msg_seq重复问题 2024-03-30 02:58:37 +00:00
RockChinQ
63307633c2 feat: chatcmpl请求时也忽略空的 system prompt message (#745) 2024-03-29 17:34:09 +08:00
RockChinQ
387dfa39ff fix: 内容过滤无效 (#743) 2024-03-29 17:24:42 +08:00
Junyan Qin
1f797f899c doc(README.md): 添加使用量计数徽标 2024-03-26 15:25:08 +08:00
RockChinQ
092bb0a1e2 chore: release v3.1.0.1 2024-03-23 22:50:54 +08:00
RockChinQ
2c3399e237 perf: 敏感词迁移的双条件检查 2024-03-23 22:41:21 +08:00
RockChinQ
835275b47f fix: 多处对 launcher_type 枚举的不当比较 (#736) 2024-03-23 22:39:42 +08:00
Junyan Qin
7b060ce3f9 doc(README.md): 更新wakapi路径 2024-03-23 19:14:43 +08:00
RockChinQ
1fb69311b0 chore: release v3.1.0 2024-03-22 17:17:16 +08:00
Junyan Qin
995d1f61d2 Merge pull request #735 from RockChinQ/feat/plugin-api
Feat: 插件异步 API
2024-03-22 17:10:06 +08:00
RockChinQ
80258e9182 perf: 修改platform_mgr名称 2024-03-22 17:09:43 +08:00
RockChinQ
bd6a32e08e doc: 为可扩展组件添加注释 2024-03-22 16:41:46 +08:00
RockChinQ
5f138de75b doc: 完善query对象的注释 2024-03-22 11:05:58 +08:00
RockChinQ
d0b0f2209a fix: chat处理过程的插件返回值目标错误 2024-03-20 23:32:28 +08:00
RockChinQ
0752698c1d chore: 完善plugin对外对象的注释 2024-03-20 18:43:52 +08:00
RockChinQ
9855c6b8f5 feat: 新的引入路径 2024-03-20 15:48:11 +08:00
RockChinQ
52a7c25540 feat: 异步风格插件方法注册器 2024-03-20 15:09:47 +08:00
RockChinQ
fa823de6b0 perf: 初始化config对象时支持传递dict作为模板 2024-03-20 14:20:56 +08:00
RockChinQ
f53070d8b6 feat: 插件加载阶段前置 (#681) 2024-03-19 22:48:02 +08:00
Junyan Qin
7677672691 Merge pull request #734 from RockChinQ/feat/moonshot
Feat: 添加对 moonshot 模型的支持
2024-03-19 22:41:40 +08:00
RockChinQ
dead8fa168 feat: 添加对 moonshot 模型的支持 2024-03-19 22:39:45 +08:00
RockChinQ
c6347bea45 fix: full-scenario 命名和目录名错误问题 (#731) 2024-03-18 21:05:54 +08:00
RockChinQ
32bd194bfc chore: anthropic 的配置补全迁移 2024-03-18 21:04:09 +08:00
Junyan Qin
cca48a394d Merge pull request #732 from RockChinQ/feat/claude-3
Feat: 接入 claude 3 系列模型
2024-03-18 11:27:22 +08:00
RockChinQ
a723c8ce37 perf: claude 的接口异常处理 2024-03-17 23:22:26 -04:00
RockChinQ
327b2509f6 perf: 忽略用户空消息 2024-03-17 23:06:40 -04:00
RockChinQ
1dae7bd655 feat: 对 claude api 的基本支持 2024-03-17 12:44:45 -04:00
RockChinQ
550a131685 deps: 添加 anthropic 依赖库 2024-03-17 12:03:25 -04:00
RockChinQ
0cfb8bb29f fix: 获取模型列表时未传递version参数 2024-03-16 22:23:02 +08:00
Junyan Qin
9c32420a95 Merge pull request #730 from RockChinQ/feat/customized-model
Feat: 允许自定义模型信息
2024-03-16 22:19:27 +08:00
RockChinQ
867093cc88 chore: 更改 provider.json 格式 2024-03-16 22:12:13 +08:00
RockChinQ
82763f8ec5 chore: 删除默认prompt 2024-03-16 21:43:45 +08:00
RockChinQ
97449065df feat: 通过元数据生成模型列表 2024-03-16 21:43:09 +08:00
Junyan Qin
9489783846 Merge pull request #729 from RockChinQ/feat/migration-stage
Feat: 配置文件迁移功能
2024-03-16 20:34:29 +08:00
RockChinQ
f91c9015bc feat: 添加配置文件迁移阶段 2024-03-16 20:27:17 +08:00
RockChinQ
302d86056d refactor: 所有的 json 加载统一到启动阶段中 2024-03-16 15:41:59 +08:00
Junyan Qin
98bebfddaa Merge pull request #728 from RockChinQ/feat/active-message
Feat: aiocqhttp 和 qq-botpy 适配器的主动消息发送接口
2024-03-16 15:18:27 +08:00
RockChinQ
dab20e3187 feat: aiocqhttp和qq-botpy的主动消息发送接口 2024-03-16 15:16:46 +08:00
RockChinQ
09e72f7c5f chore: 删除注释的代码 2024-03-14 17:24:36 +08:00
Junyan Qin
2028d85f84 Merge pull request #726 from RockChinQ/feat/qq-botpy-cache
Feat: qq-botpy 适配器对 member 和 group 的 openid 进行静态缓存
2024-03-14 16:05:14 +08:00
RockChinQ
ed3c0d9014 feat: qq-botpy 适配器对 member 和 group 的 openid 进行静态缓存 2024-03-14 16:00:22 +08:00
RockChinQ
be06150990 chore: aiocqhttp添加默认access-token参数 2024-03-13 16:53:30 +08:00
Junyan Qin
afb3fb4a31 Merge pull request #725 from RockChinQ/feat/aiocqhttp-access-token
Feat: aiocqhttp支持access-token
2024-03-13 16:49:56 +08:00
RockChinQ
d66577e6c3 feat: aiocqhttp支持access-token 2024-03-13 16:49:11 +08:00
Junyan Qin
6a4ea5446a Merge pull request #724 from RockChinQ/fix/at-resp
Fix: 回复并at机器人时会多一个at组件
2024-03-13 16:31:54 +08:00
RockChinQ
74e84c744a fix: 回复并at机器人时会多一个at组件 2024-03-13 16:31:06 +08:00
Junyan Qin
5ad2446cf3 Update bug-report.yml 2024-03-13 16:13:14 +08:00
Junyan Qin
63303bb5c0 Merge pull request #712 from RockChinQ/feat/component-extensibility
Feat: 更多组件的可扩展性
2024-03-13 00:32:26 +08:00
Junyan Qin
13393b6624 feat: 限速算法的扩展性 2024-03-12 16:31:54 +00:00
Junyan Qin
b9fa11c0c3 feat: prompt 加载器的扩展性 2024-03-12 16:22:07 +00:00
RockChinQ
8c6ce1f030 feat: 群响应规则的扩展性 2024-03-12 23:34:13 +08:00
RockChinQ
1d963d0f0c feat: 不再预先计算前文token数而是在报错时提醒用户重置 2024-03-12 16:04:11 +08:00
Junyan Qin
0ee383be27 Update announcement.json 2024-03-08 22:35:17 +08:00
RockChinQ
53d09129b4 fix: 命令事件的command参数处理错误 (#713) 2024-03-08 21:10:43 +08:00
RockChinQ
a398c6f311 feat: 消息平台适配器可扩展性 2024-03-08 20:40:54 +08:00
RockChinQ
4347ddd42a feat: 长消息处理策略可扩展性 2024-03-08 20:31:22 +08:00
RockChinQ
22cb8a6a06 feat: 内容过滤器的可扩展性 2024-03-08 20:22:06 +08:00
RockChinQ
7f554fd862 feat: command支持扩展命令类 2024-03-08 19:56:57 +08:00
Junyan Qin
a82bfa8a56 perf: 为命令装饰器添加断言 2024-03-08 11:38:26 +00:00
RockChinQ
95784debbf perf: 支持识别docker环境 2024-03-07 15:55:02 +08:00
Junyan Qin
2471c5bf0f Merge pull request #709 from RockChinQ/doc/comments
Doc: 补全部分注释
2024-03-03 16:35:31 +08:00
RockChinQ
2fe6d731b8 doc: 补全部分注释 2024-03-03 16:34:59 +08:00
RockChinQ
ce881372ee chore: release v3.0.2 2024-03-02 21:03:04 +08:00
Junyan Qin
171ea7c375 Merge pull request #708 from RockChinQ/fix/llonebot-not-supported
Fix: 修复使用llonebot时的协议问题
2024-03-02 20:59:41 +08:00
RockChinQ
1e9a6f813f fix: 修复使用llonebot时的协议问题 2024-03-02 20:58:58 +08:00
Junyan Qin
39a7f3b2b9 Merge pull request #707 from RockChinQ/feat/booting-stages
Feat: 分阶段启动
2024-03-02 20:27:51 +08:00
RockChinQ
8d375a02db fix: 未导入问题 2024-03-02 20:05:23 +08:00
RockChinQ
cac8a0a414 perf: 优化导入 2024-03-02 16:39:29 +08:00
RockChinQ
c89623967e refactor: 应用初始化流程初步分阶段 2024-03-02 16:37:30 +08:00
RockChinQ
92aa9c1711 perf: 配置文件生成步骤移动到main.py 2024-03-02 14:57:55 +08:00
Junyan Qin
71f2a58acb feat: 依赖检查移动到main.py 2024-02-29 11:10:30 +00:00
RockChinQ
1f07a8a9e3 refactor: 移动pool到pipeline包 2024-02-29 03:38:38 +00:00
RockChinQ
cacd21bde7 refactor: 移动控制器到pipeline包 2024-02-29 03:38:38 +00:00
RockChinQ
a060ec66c3 deps: 整理依赖 2024-02-29 11:03:11 +08:00
Junyan Qin
fd10db3c75 ci: fix 2024-02-21 13:56:38 +00:00
Junyan Qin
db4c658980 chore: test 2024-02-21 13:52:54 +00:00
Junyan Qin
0ee88674f8 ci: update 2024-02-21 13:52:33 +00:00
Junyan Qin
3540759682 chore: release v3.0.1.1 2024-02-21 13:46:38 +00:00
Junyan Qin
44cc8f15b4 Merge pull request #695 from RockChinQ/ci/arm-image
CI: 构建arm64镜像
2024-02-21 21:45:40 +08:00
Junyan Qin
59f821bf0a ci: 构建arm64镜像 2024-02-21 13:44:07 +00:00
RockChinQ
80858672b0 perf: 控制台输出请求响应过程 2024-02-20 22:56:42 +08:00
RockChinQ
3258d5b255 chore: aiocqhttp默认监听地址改为0.0.0.0 2024-02-20 20:13:46 +08:00
RockChinQ
e8c8cc0a9c chore: release v3.0.1 2024-02-20 11:48:26 +08:00
Junyan Qin
570c19f29f Merge pull request #693 from RockChinQ/fix/3.9-compability
Fix: 针对python3.9的兼容性
2024-02-20 11:47:49 +08:00
RockChinQ
ee93fd8636 hotfix: 针对python3.9的兼容性 2024-02-20 11:47:04 +08:00
RockChinQ
1e6c32ffc7 fix: 'VersionManager' object has no attribute 'get_release_list' 2024-02-20 09:54:02 +08:00
RockChinQ
3ef2fb958c chore: release v3.0.0 2024-02-19 22:04:41 +08:00
RockChinQ
97edfe7cd7 doc: 整理README 2024-02-19 22:03:27 +08:00
Junyan Qin
1bdc96f8b2 Merge pull request #669 from RockChinQ/feat/asyncio
Refactor: 异步架构
2024-02-19 21:59:41 +08:00
RockChinQ
4ef285aee9 chore: 删除无用文件 2024-02-19 21:51:56 +08:00
RockChinQ
6ccee3b7cf chore: 删除 README_en.md 2024-02-19 21:48:52 +08:00
RockChinQ
082731ba32 fix: !version 命令获取最新版本失败时导致命令失败 2024-02-19 21:47:51 +08:00
RockChinQ
0bf85fb644 fix: msg_source无法通过审计接口发给center 2024-02-19 21:41:53 +08:00
RockChinQ
5ce1759dd9 fix: 启动后未进行对话时,!list会 越界异常 2024-02-19 21:40:34 +08:00
RockChinQ
1e016dfa24 ci: 修改工作流文件名 2024-02-19 20:37:40 +08:00
RockChinQ
7b3bb53f06 ci: 更换基础镜像 2024-02-19 20:36:26 +08:00
RockChinQ
53d0059848 perf: 不再需要exit来退出程序 2024-02-19 19:27:42 +08:00
RockChinQ
9a85178a29 deps: 重新添加nakuru 2024-02-19 19:17:18 +08:00
RockChinQ
d74681a128 deps: 删除无用依赖 2024-02-19 18:59:52 +08:00
RockChinQ
06c8773975 perf: 优化控制台输出 2024-02-16 14:11:22 +08:00
RockChinQ
ae358dd6d0 fix: 昨天错误的shutdown_trigger逻辑 2024-02-16 13:08:26 +08:00
RockChinQ
7174cbf41f feat: 支持 ctrl+c 退出 2024-02-15 22:21:56 +08:00
RockChinQ
f73d69e814 perf: 添加未启用适配器时的警告 2024-02-15 16:12:42 +08:00
Junyan Qin
8af174127d Merge pull request #685 from RockChinQ/feat/run-multi-adapter
Feat: 支持同时运行多个适配器
2024-02-12 13:38:56 +08:00
RockChinQ
991a0aa5f6 fix: 修复nakuru无法运行的问题 2024-02-12 13:37:41 +08:00
RockChinQ
abc19e78b8 feat: 命令行退出方式 2024-02-11 23:35:05 +08:00
RockChinQ
836df87e18 feat: 删除过时配置 2024-02-11 23:11:13 +08:00
RockChinQ
9cad94e961 feat: 支持同时运行多个平台适配器 2024-02-11 23:07:38 +08:00
Junyan Qin
b9568eb558 doc(README.md): 更新社区群群号 2024-02-11 09:47:21 +08:00
RockChinQ
f951625025 chore: 修改推荐的docker-compose.yaml配置 2024-02-08 13:45:26 +08:00
RockChinQ
c2b3b53c12 chore: 修改启动相关 2024-02-08 13:40:25 +08:00
RockChinQ
d95e18c202 chore: 整理代码 2024-02-08 13:37:27 +08:00
Junyan Qin
e705e707e5 Merge pull request #680 from RockChinQ/feat/nakuru
Feat: 恢复nakuru使用
2024-02-08 13:14:53 +08:00
RockChinQ
2fa5d7608f chore: 删除无效代码 2024-02-08 13:13:35 +08:00
RockChinQ
f9a3e99795 feat: 恢复nakuru使用 2024-02-08 13:12:33 +08:00
RockChinQ
d86ad25f86 feat: 正向代理支持 2024-02-07 23:58:22 +08:00
Junyan Qin
cf583486e3 Merge pull request #679 from RockChinQ/feat/botpy-qq
Feat: 接入 QQ 官方 API
2024-02-07 23:29:56 +08:00
RockChinQ
7366ca59c7 chore: 忽略botpy.log 2024-02-07 23:27:10 +08:00
RockChinQ
12820e6c64 feat: 支持qq-botpy 2024-02-07 23:21:32 +08:00
Junyan Qin
71b54fd684 Merge pull request #678 from RockChinQ/feat/aiocqhttp
Feat: 适配aiocqhttp
2024-02-07 20:23:43 +08:00
RockChinQ
aeb1912db6 feat: 适配aiocqhttp 2024-02-07 20:03:46 +08:00
Junyan Qin
84b2867148 Merge pull request #677 from RockChinQ/refactor/asyncio/config
Refactor: 配置文件重构
2024-02-07 00:09:23 +08:00
RockChinQ
5880dacad8 ci: 修改dockerfile 2024-02-07 00:07:55 +08:00
RockChinQ
b5b67ad958 refactor: 恢复命令权限设置 2024-02-06 23:57:21 +08:00
RockChinQ
2a913ed24c chore: 删除过时文件 2024-02-06 21:29:31 +08:00
RockChinQ
aab56294ba chore: 删除字体文件 2024-02-06 21:28:24 +08:00
RockChinQ
26912ef976 chore: 删除多余文件 2024-02-06 21:28:01 +08:00
RockChinQ
c1fed3410b chore: 删除过时的配置文件 2024-02-06 21:27:14 +08:00
RockChinQ
c853bba4ba refactor: 配置文件均改为json 2024-02-06 21:26:03 +08:00
RockChinQ
f340a44abf feat: 恢复ratelimit 2024-02-01 18:38:20 +08:00
RockChinQ
0dec10ddf2 chore: 删除tests目录 2024-02-01 18:38:04 +08:00
RockChinQ
7026abe56a perf: 完善openai异常处理 2024-02-01 18:11:47 +08:00
RockChinQ
a9d92115f8 feat: chat前的前文剪裁逻辑 2024-02-01 17:42:51 +08:00
RockChinQ
6f2d7d96d0 perf: 完善历史消息处理逻辑 2024-02-01 16:43:44 +08:00
RockChinQ
532a713355 refactor: 独立出预处理阶段 2024-02-01 16:35:00 +08:00
RockChinQ
976a9de39c refactor: 分隔LLM请求过程和消息封装过程 2024-02-01 15:48:26 +08:00
RockChinQ
32162afa65 refactor: 恢复所有审计API调用 2024-01-31 00:02:19 +08:00
RockChinQ
c1c751a9ab feat: 更新操作 2024-01-30 22:50:52 +08:00
RockChinQ
b749ba587d feat: 恢复强制消息延迟 2024-01-30 21:56:25 +08:00
GitHub Actions
b2741686fd Update override-all.json 2024-01-30 13:45:50 +00:00
RockChinQ
94bf7739a0 chore: 默认回复函数响应 2024-01-30 21:45:31 +08:00
RockChinQ
33d600fb6b refactor: 恢复插件事件调用 2024-01-30 21:45:17 +08:00
RockChinQ
e2de3d0102 feat: 删除部分插件事件 2024-01-30 17:47:03 +08:00
RockChinQ
6b76adc00e feat: 添加事件对象 2024-01-30 17:24:22 +08:00
RockChinQ
61f4cb2f65 perf: 完善模型信息 2024-01-30 16:58:11 +08:00
RockChinQ
28bd232dda feat: 添加更多LLM模型 2024-01-30 16:29:54 +08:00
RockChinQ
e9e458c877 feat: 公告和更新检查 2024-01-30 16:13:33 +08:00
RockChinQ
437971ded8 feat: 应用层异常处理 2024-01-30 14:58:34 +08:00
RockChinQ
3945ac95d1 refactor: 审计api改为异步 2024-01-29 21:58:47 +08:00
RockChinQ
13ab647dc0 perf: 完善插件加载流程 2024-01-29 21:41:20 +08:00
RockChinQ
c75b0ce8fb perf: 优化代码声明 2024-01-29 21:31:11 +08:00
RockChinQ
6cc4688660 refactor: 重构插件系统 2024-01-29 21:22:27 +08:00
RockChinQ
b730f17eb6 chore: 修改包名 2024-01-28 19:20:10 +08:00
RockChinQ
698782c537 chore: 整理文件 2024-01-28 18:45:18 +08:00
Junyan Qin
2b0faea8ec Merge pull request #673 from RockChinQ/refactor/asyncio/control-flow
Refactor: 请求处理控制流
2024-01-28 18:41:59 +08:00
RockChinQ
d130c376f4 chore: 删除命令权限同步脚本 2024-01-28 18:40:10 +08:00
RockChinQ
238c55a40e chore: 删除已弃用的文件 2024-01-28 18:38:47 +08:00
RockChinQ
b5924bb34f refactor: 添加更新命令 2024-01-28 18:27:48 +08:00
RockChinQ
1368ee22b2 refactor: 命令基本完成 2024-01-28 18:21:43 +08:00
RockChinQ
2a0cf57303 refactor: 命令处理基础 2024-01-28 00:16:42 +08:00
RockChinQ
f10af09bd2 refactor: AI对话基本完成 2024-01-27 21:50:40 +08:00
RockChinQ
850a4eeb7c refactor: 重构openai包基础组件架构 2024-01-27 00:06:38 +08:00
RockChinQ
411034902a feat: 启动时展示asciiart 2024-01-27 00:05:55 +08:00
RockChinQ
1900ddacbb chore: 删除 qqbot 包中的流程代码 2024-01-26 15:54:24 +08:00
RockChinQ
8d084427d2 refactor: 请求处理控制流基础架构 2024-01-26 15:51:49 +08:00
Junyan Qin
a064c24f60 Merge pull request #670 from RockChinQ/refactor/asyncio/simplify-qqbot-mgr
Refactor: 简化和调整qqbot包架构
2024-01-25 22:39:25 +08:00
RockChinQ
b43882aad0 refactor: 独立ratelimiter包 2024-01-25 22:35:15 +08:00
RockChinQ
f4ead5ec5c refactor: 独立resprule为单独的包 2024-01-25 18:07:28 +08:00
RockChinQ
ea9ae85428 refactor: 独立长消息处理为longtext包 2024-01-25 17:05:09 +08:00
RockChinQ
a9a798b19d refactor: filter和ignore独立成新的cntfilter包 2024-01-25 15:28:23 +08:00
RockChinQ
f4ae9df3bf refactor: 重构会话封禁功能处理逻辑 2024-01-24 23:38:13 +08:00
RockChinQ
f3bcff1261 chore: banlist模版移至根目录 2024-01-24 23:33:48 +08:00
RockChinQ
b4bd86549e chore: banlist模版移至根目录 2024-01-24 23:33:19 +08:00
RockChinQ
a975718a64 refactor: 暂时删除对热重载的支持 2024-01-24 22:29:19 +08:00
RockChinQ
3d06a18bcb refactor: 简化私聊群聊共同处理代码 2024-01-24 17:00:56 +08:00
RockChinQ
a236089785 refactor: 独立resprule模块 2024-01-24 16:11:56 +08:00
RockChinQ
2f877965cf chore: 删除部分注释代码 2024-01-23 23:27:55 +08:00
RockChinQ
ad5ef95e65 refactor: yirimirai 适配器实现异步 2024-01-23 22:28:30 +08:00
RockChinQ
8d35ecd711 refactor: 基本启动流程 2024-01-23 20:55:20 +08:00
RockChinQ
e63c6ac723 feat: 删除main.py中init_db函数 2024-01-23 15:42:23 +08:00
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
391 changed files with 23259 additions and 10667 deletions

View File

@@ -5,57 +5,35 @@ labels: ["bug?"]
body:
- type: dropdown
attributes:
label: 部署方式
description: "主程序使用的部署方式"
options:
- 手动部署
- 安装器部署
- 一键安装包部署
- Docker部署
validations:
required: true
- type: dropdown
attributes:
label: 登录框架
label: 消息平台适配器
description: "连接QQ使用的框架"
options:
- Mirai
- go-cqhttp
- Nakurugo-cqhttp
- aiocqhttp(使用 OneBot 协议接入的)
- qq-botpyQQ官方API
- yiri-miraiMirai
validations:
required: false
- type: input
attributes:
label: 系统环境
description: 操作系统、系统架构、**主机地理位置**,地理位置最好写清楚,涉及网络问题排查。
placeholder: 例如: CentOS x64 中国大陆、Windows11 美国
label: 运行环境
description: 操作系统、系统架构、**Python版本**、**主机地理位置**
placeholder: 例如: CentOS x64 Python 3.10.3、Docker 的直接写 Docker 就行
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`命令查看
label: LangBot 版本
description: LangBot (QChatGPT) 版本
placeholder: 例如:v3.3.0,可以使用`!version`命令查看,或者到 pkg/utils/constants.py 查看
validations:
required: true
- type: textarea
attributes:
label: 异常情况
description: 完整描述异常情况,什么时候发生的、发生了什么,尽可能详细
description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。**
validations:
required: true
- type: textarea
attributes:
label: 日志信息
description: 请提供完整的 **登录框架 和 QChatGPT控制台**的相关日志信息(若有),不提供日志信息**无法**为您排查问题,请尽可能详细
validations:
required: false
- type: textarea
attributes:
label: 启用的插件

View File

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

View File

@@ -2,24 +2,19 @@
实现/解决/优化的内容:
### 事务
## 检查清单
- [ ] 已阅读仓库[贡献指引](https://github.com/RockChinQ/QChatGPT/blob/master/CONTRIBUTING.md)
- [ ] 已与维护者在issues或其他平台沟通此PR大致内容
### PR 作者完成
## 以下内容可在起草PR后、合并PR前逐步完成
*请在方括号间写`x`以打勾
### 功能
- [ ] 阅读仓库[贡献指引](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)了吗?
- [ ] 与项目所有者沟通过了吗?
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。
- [ ] 已编写完善的配置文件字段说明(若有新增)
- [ ] 已编写面向用户的新功能说明(若有必要)
- [ ] 已测试新功能或更改
### 项目所有者完成
### 兼容性
- [ ] 已处理版本兼容性
- [ ] 已处理插件兼容问题
### 风险
可能导致或已知的问题:
- [ ] 相关 issues 链接了吗?
- [ ] 配置项写好了吗?迁移写好了吗?生效了吗?
- [ ] 依赖写到 requirements.txt 和 core/bootutils/deps.py 了吗
- [ ] 文档编写了吗?

24
.github/workflows/build-dev-image.yaml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Build Dev Image
on:
push:
workflow_dispatch:
jobs:
build-dev-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Generate Tag
id: generate_tag
run: |
# 获取分支名称,把/替换为-
echo ${{ github.ref }} | sed 's/refs\/heads\///g' | sed 's/\//-/g'
echo ::set-output name=tag::$(echo ${{ github.ref }} | sed 's/refs\/heads\///g' | sed 's/\//-/g')
- name: Login to Registry
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
- name: Build Docker Image
run: |
docker buildx create --name mybuilder --use
docker build -t rockchin/langbot:${{ steps.generate_tag.outputs.tag }} . --push

View File

@@ -0,0 +1,42 @@
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 }}
echo $GITHUB_REF
fi
- name: Check version
id: check_version
run: |
echo $GITHUB_REF
# 如果是tag则去掉refs/tags/前缀
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "It's a tag"
echo $GITHUB_REF
echo $GITHUB_REF | awk -F '/' '{print $3}'
echo ::set-output name=version::$(echo $GITHUB_REF | awk -F '/' '{print $3}')
else
echo "It's not a tag"
echo $GITHUB_REF
echo ::set-output name=version::${GITHUB_REF}
fi
- name: Login to Registry
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
- name: Create Buildx
run: docker buildx create --name mybuilder --use
- name: Build # image name: rockchin/langbot:<VERSION>
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} -t rockchin/langbot:latest . --push

View File

@@ -0,0 +1,52 @@
name: Build Release Artifacts
on:
workflow_dispatch:
## 发布release的时候会自动构建
release:
types: [published]
jobs:
build-artifacts:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Check version
id: check_version
run: |
echo $GITHUB_REF
# 如果是tag则去掉refs/tags/前缀
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "It's a tag"
echo $GITHUB_REF
echo $GITHUB_REF | awk -F '/' '{print $3}'
echo ::set-output name=version::$(echo $GITHUB_REF | awk -F '/' '{print $3}')
else
echo "It's not a tag"
echo $GITHUB_REF
echo ::set-output name=version::${GITHUB_REF}
fi
- name: Make Temp Directory
run: |
mkdir -p /tmp/langbot_build_web
cp -r . /tmp/langbot_build_web
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '22'
- name: Build Web
run: |
cd /tmp/langbot_build_web/web
npm install
npm run build
- name: Package Output
run: |
cp -r /tmp/langbot_build_web/web/dist ./web
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: langbot-${{ steps.check_version.outputs.version }}-all
path: .

View File

@@ -1,38 +0,0 @@
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

View File

@@ -1,48 +0,0 @@
name: Update Wiki
on:
pull_request:
branches:
- master
paths:
- 'res/wiki/**'
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
.github/workflows/test-pr.yml vendored Normal file
View File

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

View File

@@ -1,58 +0,0 @@
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

View File

@@ -1,52 +0,0 @@
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

11
.gitignore vendored
View File

@@ -3,9 +3,10 @@
__pycache__/
database.db
qchatgpt.log
langbot.log
/banlist.py
plugins/
!plugins/__init__.py
/plugins/
!/plugins/__init__.py
/revcfg.py
prompts/
logs/
@@ -31,4 +32,8 @@ claude.json
bard.json
/*yaml
!/docker-compose.yaml
res/instance_id.json
res/instance_id.json
.DS_Store
/data
botpy.log*
/poc

View File

@@ -1,15 +1,22 @@
FROM python:3.10.13-bullseye
WORKDIR /QChatGPT
FROM node:22-alpine AS node
COPY . /QChatGPT/
WORKDIR /app
RUN ls
COPY web ./web
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 cd web && npm install && npm run build
# 生成配置文件
RUN python main.py
FROM python:3.10.13-slim
WORKDIR /app
COPY . .
COPY --from=node /app/web/dist ./web/dist
RUN apt update \
&& apt install gcc -y \
&& python -m pip install -r requirements.txt \
&& touch /.dockerenv
CMD [ "python", "main.py" ]

View File

@@ -1,40 +1,40 @@
<p align="center">
<img src="https://qchatgpt.rockchin.top/logo.png" alt="QChatGPT" width="180" />
<img src="https://docs.langbot.app/langbot-logo-0.5x.png" alt="QChatGPT" width="180" />
</p>
<div align="center">
# QChatGPT
# LangBot
<blockquote> 🥳 QChatGPT 一周年啦,感谢大家的支持!欢迎前往<a href="https://github.com/RockChinQ/QChatGPT/discussions/627">讨论</a>。</blockquote>
<a href="https://trendshift.io/repositories/6187" target="_blank"><img src="https://trendshift.io/api/badge/repositories/6187" alt="RockChinQ%2FQChatGPT | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/QChatGPT)](https://github.com/RockChinQ/QChatGPT/releases/latest)
<a href="https://hub.docker.com/repository/docker/rockchin/qchatgpt">
<img src="https://img.shields.io/docker/pulls/rockchin/qchatgpt?color=blue" alt="docker pull">
</a>
![Wakapi Count](https://wakapi.dev/api/badge/RockChinQ/interval:any/project:QChatGPT)
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=%E4%BD%BF%E7%94%A8%E9%87%8F%EF%BC%887%E6%97%A5%EF%BC%89)
![Wakapi Count](https://wakapi.rockchin.top/api/badge/RockChinQ/interval:any/project:QChatGPT)
<br/>
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=66-aWvn8cbP4c1ut_1YYkvvGVeEtyTH8&authKey=pTaKBK5C%2B8dFzQ4XlENf6MHTCLaHnlKcCRx7c14EeVVlpX2nRSaS8lJm8YeM4mCU&noverify=0&group_code=195992197">
<img alt="Static Badge" src="https://img.shields.io/badge/%E5%AE%98%E6%96%B9%E7%BE%A4-195992197-purple">
</a>
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nC80H57wmKPwRDLFeQrDDjVl81XuC21P&authKey=2wTUTfoQ5v%2BD4C5zfpuR%2BSPMDqdXgDXA%2FS2wHI1NxTfWIG%2B%2FqK08dgyjMMOzhXa9&noverify=0&group_code=738382634">
<img alt="Static Badge" src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA%E7%BE%A4-738382634-purple">
</a>
<a href="https://www.bilibili.com/video/BV14h4y1w7TC">
<img alt="Static Badge" src="https://img.shields.io/badge/%E8%A7%86%E9%A2%91%E6%95%99%E7%A8%8B-208647">
</a>
<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">
<a href="https://qm.qq.com/q/PClALFK242">
<img alt="Static Badge" src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA%E7%BE%A4-619154800-purple">
</a>
## 使用文档
<a href="https://qchatgpt.rockchin.top">项目主页</a>
<a href="https://qchatgpt.rockchin.top/posts/feature.html">功能介绍</a>
<a href="https://qchatgpt.rockchin.top/posts/deploy/">部署文档</a>
<a href="https://qchatgpt.rockchin.top/posts/error/">常见问题</a>
<a href="https://qchatgpt.rockchin.top/posts/plugin/intro.html">插件介绍</a>
<a href="https://github.com/RockChinQ/QChatGPT/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
<a href="https://docs.langbot.app">项目主页</a>
<a href="https://docs.langbot.app/insight/intro.htmll">功能介绍</a>
<a href="https://docs.langbot.app/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/usage/faq.html">常见问题</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/RockChinQ/LangBot/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>
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/>
## 相关链接
<a href="https://github.com/RockChinQ/qcg-installer">安装器源码</a>
<a href="https://github.com/RockChinQ/qcg-tester">测试工程源码</a>
<a href="https://github.com/RockChinQ/qcg-center">遥测服务端源码</a>
<a href="https://github.com/the-lazy-me/QChatGPT-Wiki">官方文档储存库</a>
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
</div>

View File

@@ -1,215 +0,0 @@
# 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"/> -->

View File

@@ -1,377 +0,0 @@
# 配置文件: 注释里标[必需]的参数必须修改, 其他参数根据需要修改, 但请勿删除
import logging
# 消息处理协议适配器
# 目前支持以下适配器:
# - "yirimirai": mirai的通信框架YiriMirai框架适配器, 请同时填写下方mirai_http_api_config
# - "nakuru": go-cqhttp通信框架请同时填写下方nakuru_config
msg_source_adapter = "yirimirai"
# [必需(与nakuru二选一取决于msg_source_adapter)] Mirai的配置
# 请到配置mirai的步骤中的教程查看每个字段的信息
# adapter: 选择适配器目前支持HTTPAdapter和WebSocketAdapter
# host: 运行mirai的主机地址
# port: 运行mirai的主机端口
# verifyKey: mirai-api-http的verifyKey
# qq: 机器人的QQ号
#
# 注意: QQ机器人配置不支持热重载及热更新
mirai_http_api_config = {
"adapter": "WebSocketAdapter",
"host": "localhost",
"port": 8080,
"verifyKey": "yirimirai",
"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的配置
# api_key: OpenAI的API Key
# http_proxy: 请求OpenAI时使用的代理None为不使用https和socks5暂不能使用
# 若只有一个api-key请直接修改以下内容中的"openai_api_key"为你的api-key
#
# 如准备了多个api-key可以以字典的形式填写程序会自动选择可用的api-key
# 例如
# openai_config = {
# "api_key": {
# "default": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
# "key1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
# "key2": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
# },
# "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 = {
"api_key": {
"default": "openai_api_key"
},
"http_proxy": None,
"reverse_proxy": None
}
# api-key切换策略
# active每次请求时都会切换api-key
# passive仅当api-key超额时才会切换api-key
switch_strategy = "active"
# [必需] 管理员QQ号用于接收报错等通知及执行管理员级别指令
# 支持多个管理员可以使用list形式设置例如
# admin_qq = [12345678, 87654321]
admin_qq = 0
# 情景预设(机器人人格)
# 每个会话的预设信息,影响所有会话,无视指令重置
# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令
# 例如:
# default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”"
# 这样用户在不知所措的时候机器人就会提示其输入!help获取帮助
# 可参考 https://github.com/PlexPt/awesome-chatgpt-prompts-zh
#
# 如果需要多个情景预设,并在运行期间方便切换,请使用字典的形式填写,例如
# default_prompt = {
# "default": "如果我之后想获取帮助,请你说“输入!help获取帮助”",
# "linux-terminal": "我想让你充当 Linux 终端。我将输入命令,您将回复终端应显示的内容。",
# "en-dict": "我想让你充当英英词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈。",
# }
#
# 在使用期间即可通过指令:
# !reset [名称]
# 来使用指定的情景预设重置会话
# 例如:
# !reset linux-terminal
# 若不指定名称,则使用默认情景预设
#
# 也可以使用指令:
# !default <名称>
# 将指定的情景预设设置为默认情景预设
# 例如:
# !default linux-terminal
# 之后的会话重置时若不指定名称则使用linux-terminal情景预设
#
# 还可以加载文件中的预设文字使用方法请查看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": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。",
}
# 情景预设格式
# 参考值默认方式normal | 完整情景full_scenario
# 默认方式 的格式为上述default_prompt中的内容或prompts目录下的文件名
# 完整情景方式 的格式为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消息、随机响应概率
# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式(regexp)匹配的消息不会删除匹配的部分
# 前缀匹配优先级高于正则表达式匹配
# 正则表达式简明教程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 = {
"default": {
"at": True, # 是否响应at机器人的消息
"prefix": ["/ai", "!ai", "ai", "ai"],
"regexp": [], # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办"
"random_rate": 0.0, # 随机响应概率0.0-1.00.0为不随机响应1.0为响应所有消息, 仅在前几项判断不通过时生效
},
}
# 消息忽略规则
# 适用于私聊及群聊
# 符合此规则的消息将不会被响应
# 支持消息前缀匹配及正则表达式匹配
# 此设置优先级高于response_rules
# 用以过滤mirai等其他层级的指令
# @see https://github.com/RockChinQ/QChatGPT/issues/165
ignore_rules = {
"prefix": ["/"],
"regexp": []
}
# 是否检查收到的消息中是否包含敏感词
# 若收到的消息无法通过下方指定的敏感词检查策略,则发送提示信息
income_msg_check = False
# 敏感词过滤开关,以同样数量的*代替敏感词回复
# 请在sensitive.json中添加敏感词
sensitive_word_filter = True
# 是否启用百度云内容安全审核
# 注册方式查看 https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy
baidu_check = False
# 百度云API_KEY 24位英文数字字符串
baidu_api_key = ""
# 百度云SECRET_KEY 32位的英文数字字符串
baidu_secret_key = ""
# 不合规消息自定义返回
inappropriate_message_tips = "[百度云]请珍惜机器人,当前返回内容不合规"
# 启动时是否发送赞赏码
# 仅当使用量已经超过2048字时发送
encourage_sponsor_at_start = True
# 每次向OpenAI接口发送对话记录上下文的字符数
# 最大不超过(4096 - max_tokens)个字符max_tokens为下方completion_api_params中的max_tokens
# 注意较大的prompt_submit_length会导致OpenAI账户额度消耗更快
prompt_submit_length = 2048
# 是否在token超限报错时自动重置会话
# 可在tips.py中编辑提示语
auto_reset = True
# OpenAI补全API的参数
# 请在下方填写模型,程序自动选择接口
# 模型文档https://platform.openai.com/docs/models
# 现已支持的模型有:
#
# ChatCompletions 接口:
# # GPT 4 系列
# "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
#
# Completions接口
# "text-davinci-003", # legacy
# "text-davinci-002", # legacy
# "code-davinci-002", # legacy
# "code-cushman-001", # legacy
# "text-curie-001", # legacy
# "text-babbage-001", # legacy
# "text-ada-001", # legacy
# "gpt-3.5-turbo-instruct",
#
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create
# 请将内容修改到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 = {
"model": "gpt-3.5-turbo",
"temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1]
}
# OpenAI的Image API的参数
# 具体请查看OpenAI的文档: https://platform.openai.com/docs/api-reference/images/create
image_api_params = {
"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 = False
# 群内回复消息时是否at发送者
at_sender = False
# 回复绘图时是否包含图片描述
include_image_description = True
# 消息处理的超时时间,单位为秒
process_message_timeout = 120
# 回复消息时是否显示[GPT]前缀
show_prefix = False
# 回复前的强制延迟时间,降低机器人被腾讯风控概率
# *此机制对命令和消息、私聊及群聊均生效
# 每次处理时从以下的范围取一个随机秒数,
# 当此次消息处理时间低于此秒数时,将会强制延迟至此秒数
# 例如:[1.5, 3]则每次处理时会随机取一个1.5-3秒的随机数若处理时间低于此随机数则强制延迟至此随机秒数
# 若您不需要此功能请将force_delay_range设置为[0, 0]
force_delay_range = [0, 0]
# 应用长消息处理策略的阈值
# 当回复消息长度超过此值时,将使用长消息处理策略
blob_message_threshold = 256
# 长消息处理策略
# - "image": 将长消息转换为图片发送
# - "forward": 将长消息转换为转发消息组件发送
blob_message_strategy = "forward"
# 允许等待
# 同一会话内,是否等待上一条消息处理完成后再处理下一条消息
# 若设置为False若上一条未处理完时收到了新消息将会丢弃新消息
# 丢弃消息时的提示信息可以在tips.py中修改
wait_last_done = True
# 文字转图片时使用的字体文件路径
# 当策略为"image"时生效
# 若在Windows系统下程序会自动使用Windows自带的微软雅黑字体
# 若未填写或不存在且不是Windows将禁用文字转图片功能改为使用转发消息组件
font_path = ""
# 消息处理超时重试次数
retry_times = 3
# 消息处理出错时是否向用户隐藏错误详细信息
# 设置为True时仅向管理员发送错误详细信息
# 设置为False时向用户及管理员发送错误详细信息
hide_exce_info_to_user = False
# 每个会话的过期时间,单位为秒
# 默认值20分钟
session_expire_time = 1200
# 会话限速
# 单会话内每分钟可进行的对话次数
# 若不需要限速,可以设置为一个很大的值
# 默认值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": 每次对话获取到回复时,等待一定时间再发送回复,保证其不会超过限速均值
# - "drop": 此分钟内,若对话次数超过限速次数,则丢弃之后的对话,每自然分钟重置
rate_limit_strategy = "drop"
# 是否在启动时进行依赖库更新
upgrade_dependencies = False
# 是否上报统计信息
# 用于统计机器人的使用情况,不会收集任何用户信息
# 仅上报时间、字数使用量、绘图使用量,其他信息不会上报
report_usage = True
# 日志级别
logging_level = logging.INFO

View File

@@ -1,18 +1,14 @@
version: "3"
services:
qchatgpt:
image: rockchin/qchatgpt:latest
langbot:
image: rockchin/langbot:latest
container_name: langbot
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
# 根据具体环境配置网络
- ./data:/app/data
- ./plugins:/app/plugins
restart: on-failure
ports:
- 5300:5300 # 供 WebUI 使用
- 2280-2290:2280-2290 # 供消息平台适配器方向连接
# 根据具体环境配置网络

541
main.py
View File

@@ -1,496 +1,89 @@
import importlib
import json
import os
import shutil
import threading
import time
# LangBot 终端启动入口
# 在此层级解决依赖项检查。
# LangBot/main.py
asciiart = r"""
_ ___ _
| | __ _ _ _ __ _| _ ) ___| |_
| |__/ _` | ' \/ _` | _ \/ _ \ _|
|____\__,_|_||_\__, |___/\___/\__|
|___/
⭐️开源地址: https://github.com/RockChinQ/LangBot
📖文档地址: https://docs.langbot.app
"""
import logging
import sys
import traceback
import asyncio
sys.path.append(".")
async def main_entry(loop: asyncio.AbstractEventLoop):
print(asciiart)
def check_file():
# 检查是否有banlist.py,如果没有就把banlist-template.py复制一份
if not os.path.exists('banlist.py'):
shutil.copy('res/templates/banlist-template.py', 'banlist.py')
import sys
# 检查是否有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")
from pkg.core.bootutils import deps
# 检查cmdpriv.json
if not os.path.exists("cmdpriv.json"):
shutil.copy("res/templates/cmdpriv-template.json", "cmdpriv.json")
missing_deps = await deps.check_deps()
# 检查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中填写配置')
if missing_deps:
print("以下依赖包未安装,将自动安装,请完成后重启程序:")
for dep in missing_deps:
print("-", dep)
await deps.install_deps(missing_deps)
print("已自动安装缺失的依赖包,请重启程序。")
sys.exit(0)
# 检查pydantic版本如果没有 pydantic.v1则把 pydantic 映射为 v1
import pydantic.version
if pydantic.version.VERSION < '2.0':
import pydantic
sys.modules['pydantic.v1'] = pydantic
# 初始化相关文件
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
from pkg.core.bootutils import files
generated_files = await files.generate_files()
try:
import colorlog
except ImportError:
# 尝试安装
import pkg.utils.pkgmgr as pkgmgr
try:
pkgmgr.install_requirements("requirements.txt")
import colorlog
except ImportError:
print("依赖不满足,请查看 https://github.com/RockChinQ/qcg-installer/issues/15")
sys.exit(1)
import colorlog
if generated_files:
print("以下文件不存在,已自动生成,请按需修改配置文件后重启:")
for file in generated_files:
print("-", file)
import requests
import websockets.exceptions
from urllib3.exceptions import InsecureRequestWarning
import pkg.utils.context
# 是否使用override.json覆盖配置
# 仅在启动时提供 --override 或 -r 参数时生效
use_override = False
def init_db():
import pkg.database.manager
database = pkg.database.manager.DatabaseManager()
database.initialize_database()
def ensure_dependencies():
import pkg.utils.pkgmgr as pkgmgr
pkgmgr.run_pip(["install", "openai", "Pillow", "nakuru-project-idk", "CallingGPT", "tiktoken", "--upgrade",
"-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
known_exception_caught = False
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 complete_tips():
"""根据tips-custom-template模块补全tips模块的属性"""
non_exist_keys = []
is_integrity = True
logging.debug("检查tips模块完整性.")
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
if not is_integrity:
logging.warning("以下提示语字段不存在: {}".format(", ".join(non_exist_keys)))
logging.warning("tips模块不完整您可以依据tips-custom-template.py检查tips.py")
logging.warning("以上配置已被设为默认值将在3秒后继续启动... ")
time.sleep(3)
async def start_process(first_time_init=False):
"""启动流程reload之后会被执行"""
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
# 更新openai库到最新版本
if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']:
print("正在更新依赖库,请等待...")
if 'upgrade_dependencies' not in cfg:
print("这个操作不是必须的,如果不想更新,请在config.py中添加upgrade_dependencies=False")
else:
print("这个操作不是必须的,如果不想更新,请在config.py中将upgrade_dependencies设置为False")
try:
ensure_dependencies()
except Exception as e:
print("更新openai库失败:{}, 请忽略或自行更新".format(e))
# 初始化文字转图片
from pkg.utils import text2img
text2img.initialize()
known_exception_caught = False
try:
try:
sh = reset_logging()
pkg.utils.context.context['logger_handler'] = sh
# 检查是否设置了管理员
if cfg['admin_qq'] == 0:
# logging.warning("未设置管理员QQ,管理员权限指令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
while True:
try:
cfg['admin_qq'] = int(input("未设置管理员QQ,管理员权限指令及运行告警将无法使用,请输入管理员QQ号: "))
# 写入到文件
# 读取文件
config_file_str = ""
with open("config.py", "r", encoding="utf-8") as f:
config_file_str = f.read()
# 替换
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:
f.write(config_file_str)
print("管理员QQ已设置如需修改请修改config.py中的admin_qq字段")
time.sleep(4)
break
except ValueError:
print("请输入数字")
# 初始化中央服务器 API 交互实例
from pkg.utils.center import apigroup
from pkg.utils.center import v2 as center_v2
center_v2_api = center_v2.V2CenterAPI(
basic_info={
"host_id": pkg.audit.identifier.identifier['host_id'],
"instance_id": pkg.audit.identifier.identifier['instance_id'],
"semantic_version": pkg.utils.updater.get_current_tag(),
"platform": sys.platform,
},
runtime_info={
"admin_id": "{}".format(cfg['admin_qq']),
"msg_source": cfg['msg_source_adapter'],
}
)
pkg.utils.context.set_center_v2_api(center_v2_api)
import pkg.openai.manager
import pkg.database.manager
import pkg.openai.session
import pkg.qqbot.manager
import pkg.openai.dprompt
import pkg.qqbot.cmds.aamgr
try:
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"]
# 主启动流程
database = pkg.database.manager.DatabaseManager()
database.initialize_database()
openai_interact = pkg.openai.manager.OpenAIInteract(cfg['openai_config']['api_key'])
# 加载所有未超时的session
pkg.openai.session.load_sessions()
# 初始化qq机器人
qqbot = pkg.qqbot.manager.QQBotManager(first_time_init=first_time_init)
# 加载插件
import pkg.plugin.host
pkg.plugin.host.load_plugins()
pkg.plugin.host.initialize_plugins()
if first_time_init: # 不是热重载之后的启动,则启动新的bot线程
import mirai.exceptions
def run_bot_wrapper():
global known_exception_caught
try:
logging.debug("使用账号: {}".format(qqbot.bot_account_id))
qqbot.adapter.run_sync()
except TypeError as e:
if str(e).__contains__("argument 'debug'"):
logging.error(
"连接bot失败:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/82".format(e))
known_exception_caught = True
elif str(e).__contains__("As of 3.10, the *loop*"):
logging.error(
"Websockets版本过低:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/5".format(e))
known_exception_caught = True
except websockets.exceptions.InvalidStatus as e:
logging.error(
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
e))
known_exception_caught = True
except mirai.exceptions.NetworkError as e:
logging.error("连接mirai-api-http失败:{}, 请检查是否已按照文档启动mirai".format(e))
known_exception_caught = True
except Exception as e:
if str(e).__contains__("404"):
logging.error(
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
e))
known_exception_caught = True
elif str(e).__contains__("signal only works in main thread"):
logging.error(
"hypercorn异常:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/86".format(
e))
known_exception_caught = True
elif str(e).__contains__("did not receive a valid HTTP"):
logging.error(
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
e))
else:
import traceback
traceback.print_exc()
logging.error(
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/QChatGPT/issues 查找或提issue".format(e))
known_exception_caught = True
raise e
finally:
time.sleep(12)
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:
# 判断若是Windows输出选择模式可能会暂停程序的警告
if os.name == 'nt':
time.sleep(2)
logging.info("您正在使用Windows系统若命令行窗口处于“选择”模式程序可能会被暂停此时请右键点击窗口空白区域使其取消选择模式。")
time.sleep(12)
if first_time_init:
if not known_exception_caught:
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')
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:
sys.exit(1)
else:
logging.info('热重载完成')
# 发送赞赏码
if cfg['encourage_sponsor_at_start'] \
and pkg.utils.context.get_openai_manager().audit_mgr.get_total_text_length() >= 2048:
logging.info("发送赞赏码")
from mirai import MessageChain, Plain, Image
import pkg.utils.constants
message_chain = MessageChain([
Plain("自2022年12月初以来开发者已经花费了大量时间和精力来维护本项目如果您觉得本项目对您有帮助欢迎赞赏开发者"
"以支持项目稳定运行😘"),
Image(base64=pkg.utils.constants.alipay_qr_b64),
Image(base64=pkg.utils.constants.wechat_qr_b64),
Plain("BTC: 3N4Azee63vbBB9boGv9Rjf4N5SocMe5eCq\nXMR: 89LS21EKQuDGkyQoe2nDupiuWXk4TVD6FALvSKv5owfmeJEPFpHeMsZLYtLiJ6GxLrhsRe5gMs6MyMSDn4GNQAse2Mae4KE\n\n"),
Plain("(本消息仅在启动时发送至管理员如果您不想再看到此消息请在config.py中将encourage_sponsor_at_start设置为False)")
])
pkg.utils.context.get_qqbot_manager().notify_admin_message_chain(message_chain)
time.sleep(5)
import pkg.utils.updater
try:
if pkg.utils.updater.is_new_version_available():
logging.info("新版本可用,请发送 !update 进行自动更新\n更新日志:\n{}".format("\n".join(pkg.utils.updater.get_rls_notes())))
else:
# logging.info("当前已是最新版本")
pass
except Exception as 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
def stop():
import pkg.qqbot.manager
import pkg.openai.session
try:
import pkg.plugin.host
pkg.plugin.host.unload_plugins()
qqbot_inst = pkg.utils.context.get_qqbot_manager()
assert isinstance(qqbot_inst, pkg.qqbot.manager.QQBotManager)
for session in pkg.openai.session.sessions:
logging.info('持久化session: %s', session)
pkg.openai.session.sessions[session].persistence()
pkg.utils.context.get_database_manager().close()
except Exception as e:
if not isinstance(e, KeyboardInterrupt):
raise e
def main():
global use_override
# 检查是否携带了 --override 或 -r 参数
if '--override' in sys.argv or '-r' in sys.argv:
use_override = True
# 初始化logging
init_runtime_log_file()
pkg.utils.context.context['logger_handler'] = reset_logging()
# 配置线程池
from pkg.utils import ThreadCtl
thread_ctl = ThreadCtl(
sys_pool_num=8,
admin_pool_num=4,
user_pool_num=8
)
# 存进上下文
pkg.utils.context.set_thread_ctl(thread_ctl)
# 启动指令处理
if len(sys.argv) > 1 and sys.argv[1] == 'init_db':
init_db()
sys.exit(0)
elif len(sys.argv) > 1 and sys.argv[1] == 'update':
print("正在进行程序更新...")
import pkg.utils.updater as updater
updater.update_all(cli=True)
sys.exit(0)
# 关闭urllib的http警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
def run_wrapper():
asyncio.run(start_process(True))
pkg.utils.context.get_thread_ctl().submit_sys_task(
run_wrapper
)
# 主线程循环
while True:
try:
time.sleep(0xFF)
except:
stop()
pkg.utils.context.get_thread_ctl().shutdown()
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)
from pkg.core import boot
await boot.main(loop)
if __name__ == '__main__':
main()
import os
import sys
# 必须大于 3.10.1
if sys.version_info < (3, 10, 1):
print("需要 Python 3.10.1 及以上版本,当前 Python 版本为:", sys.version)
input("按任意键退出...")
exit(1)
# 检查本目录是否有main.py且包含LangBot字符串
invalid_pwd = False
if not os.path.exists('main.py'):
invalid_pwd = True
else:
with open('main.py', 'r', encoding='utf-8') as f:
content = f.read()
if "LangBot/main.py" not in content:
invalid_pwd = True
if invalid_pwd:
print("请在 LangBot 项目根目录下以命令形式运行此程序。")
input("按任意键退出...")
exit(1)
loop = asyncio.new_event_loop()
loop.run_until_complete(main_entry(loop))

View File

@@ -1,90 +0,0 @@
{
"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": 2048,
"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
}

View File

@@ -0,0 +1,107 @@
from __future__ import annotations
import abc
import typing
import enum
import quart
from quart.typing import RouteCallable
from ....core import app
preregistered_groups: list[type[RouterGroup]] = []
"""RouterGroup 的预注册列表"""
def group_class(name: str, path: str) -> None:
"""注册一个 RouterGroup"""
def decorator(cls: typing.Type[RouterGroup]) -> typing.Type[RouterGroup]:
cls.name = name
cls.path = path
preregistered_groups.append(cls)
return cls
return decorator
class AuthType(enum.Enum):
"""认证类型"""
NONE = 'none'
USER_TOKEN = 'user-token'
class RouterGroup(abc.ABC):
name: str
path: str
ap: app.Application
quart_app: quart.Quart
def __init__(self, ap: app.Application, quart_app: quart.Quart) -> None:
self.ap = ap
self.quart_app = quart_app
@abc.abstractmethod
async def initialize(self) -> None:
pass
def route(self, rule: str, auth_type: AuthType = AuthType.USER_TOKEN, **options: typing.Any) -> typing.Callable[[RouteCallable], RouteCallable]: # decorator
"""注册一个路由"""
def decorator(f: RouteCallable) -> RouteCallable:
nonlocal rule
rule = self.path + rule
async def handler_error(*args, **kwargs):
if auth_type == AuthType.USER_TOKEN:
# 从Authorization头中获取token
token = quart.request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return self.http_status(401, -1, '未提供有效的用户令牌')
try:
user_email = await self.ap.user_service.verify_jwt_token(token)
# 检查f是否接受user_email参数
if 'user_email' in f.__code__.co_varnames:
kwargs['user_email'] = user_email
except Exception as e:
return self.http_status(401, -1, str(e))
try:
return await f(*args, **kwargs)
except Exception as e: # 自动 500
return self.http_status(500, -2, str(e))
new_f = handler_error
new_f.__name__ = (self.name + rule).replace('/', '__')
new_f.__doc__ = f.__doc__
self.quart_app.route(rule, **options)(new_f)
return f
return decorator
def success(self, data: typing.Any = None) -> quart.Response:
"""返回一个 200 响应"""
return quart.jsonify({
'code': 0,
'msg': 'ok',
'data': data,
})
def fail(self, code: int, msg: str) -> quart.Response:
"""返回一个异常响应"""
return quart.jsonify({
'code': code,
'msg': msg,
})
def http_status(self, status: int, code: int, msg: str) -> quart.Response:
"""返回一个指定状态码的响应"""
return self.fail(code, msg), status

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
import traceback
import quart
from .....core import app
from .. import group
@group.group_class('logs', '/api/v1/logs')
class LogsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET'])
async def _() -> str:
start_page_number = int(quart.request.args.get('start_page_number', 0))
start_offset = int(quart.request.args.get('start_offset', 0))
logs_str, end_page_number, end_offset = self.ap.log_cache.get_log_by_pointer(
start_page_number=start_page_number,
start_offset=start_offset
)
return self.success(
data={
"logs": logs_str,
"end_page_number": end_page_number,
"end_offset": end_offset
}
)

View File

@@ -0,0 +1,84 @@
from __future__ import annotations
import traceback
import quart
from .....core import app, taskmgr
from .. import group
@group.group_class('plugins', '/api/v1/plugins')
class PluginsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET'])
async def _() -> str:
plugins = self.ap.plugin_mgr.plugins()
plugins_data = [plugin.model_dump() for plugin in plugins]
return self.success(data={
'plugins': plugins_data
})
@self.route('/<author>/<plugin_name>/toggle', methods=['PUT'])
async def _(author: str, plugin_name: str) -> str:
data = await quart.request.json
target_enabled = data.get('target_enabled')
await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled)
return self.success()
@self.route('/<author>/<plugin_name>/update', methods=['POST'])
async def _(author: str, plugin_name: str) -> str:
ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_mgr.update_plugin(plugin_name, task_context=ctx),
kind="plugin-operation",
name=f"plugin-update-{plugin_name}",
label=f"更新插件 {plugin_name}",
context=ctx
)
return self.success(data={
'task_id': wrapper.id
})
@self.route('/<author>/<plugin_name>', methods=['DELETE'])
async def _(author: str, plugin_name: str) -> str:
ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx),
kind="plugin-operation",
name=f'plugin-remove-{plugin_name}',
label=f'删除插件 {plugin_name}',
context=ctx
)
return self.success(data={
'task_id': wrapper.id
})
@self.route('/reorder', methods=['PUT'])
async def _() -> str:
data = await quart.request.json
await self.ap.plugin_mgr.reorder_plugins(data.get('plugins'))
return self.success()
@self.route('/install/github', methods=['POST'])
async def _() -> str:
data = await quart.request.json
ctx = taskmgr.TaskContext.new()
short_source_str = data['source'][-8:]
wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx),
kind="plugin-operation",
name=f'plugin-install-github',
label=f'安装插件 ...{short_source_str}',
context=ctx
)
return self.success(data={
'task_id': wrapper.id
})

View File

@@ -0,0 +1,62 @@
import quart
from .....core import app
from .. import group
@group.group_class('settings', '/api/v1/settings')
class SettingsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET'])
async def _() -> str:
return self.success(
data={
"managers": [
{
"name": m.name,
"description": m.description,
}
for m in self.ap.settings_mgr.get_manager_list()
]
}
)
@self.route('/<manager_name>', methods=['GET'])
async def _(manager_name: str) -> str:
manager = self.ap.settings_mgr.get_manager(manager_name)
if manager is None:
return self.fail(1, '配置管理器不存在')
return self.success(
data={
"manager": {
"name": manager.name,
"description": manager.description,
"schema": manager.schema,
"file": manager.file.config_file_name,
"data": manager.data,
"doc_link": manager.doc_link
}
}
)
@self.route('/<manager_name>/data', methods=['PUT'])
async def _(manager_name: str) -> str:
data = await quart.request.json
manager = self.ap.settings_mgr.get_manager(manager_name)
if manager is None:
return self.fail(code=1, msg='配置管理器不存在')
# manager.data = data['data']
for k, v in data['data'].items():
manager.data[k] = v
await manager.dump_config()
return self.success(data={
"data": manager.data
})

View File

@@ -0,0 +1,23 @@
import quart
import asyncio
from .....core import app, taskmgr
from .. import group
@group.group_class('stats', '/api/v1/stats')
class StatsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/basic', methods=['GET'])
async def _() -> str:
conv_count = 0
for session in self.ap.sess_mgr.session_list:
conv_count += len(session.conversations if session.conversations is not None else [])
return self.success(data={
'active_session_count': len(self.ap.sess_mgr.session_list),
'conversation_count': conv_count,
'query_count': self.ap.query_pool.query_id_counter,
})

View File

@@ -0,0 +1,63 @@
import quart
import asyncio
from .....core import app, taskmgr
from .. import group
from .....utils import constants
@group.group_class('system', '/api/v1/system')
class SystemRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
async def _() -> str:
return self.success(
data={
"version": constants.semantic_version,
"debug": constants.debug_mode,
"enabled_platform_count": len(self.ap.platform_mgr.adapters)
}
)
@self.route('/tasks', methods=['GET'])
async def _() -> str:
task_type = quart.request.args.get("type")
if task_type == '':
task_type = None
return self.success(
data=self.ap.task_mgr.get_tasks_dict(task_type)
)
@self.route('/tasks/<task_id>', methods=['GET'])
async def _(task_id: str) -> str:
task = self.ap.task_mgr.get_task_by_id(int(task_id))
if task is None:
return self.http_status(404, 404, "Task not found")
return self.success(data=task.to_dict())
@self.route('/reload', methods=['POST'])
async def _() -> str:
json_data = await quart.request.json
scope = json_data.get("scope")
await self.ap.reload(
scope=scope
)
return self.success()
@self.route('/_debug/exec', methods=['POST'])
async def _() -> str:
if not constants.debug_mode:
return self.http_status(403, 403, "Forbidden")
py_code = await quart.request.data
ap = self.ap
return self.success(data=exec(py_code, {"ap": ap}))

View File

@@ -0,0 +1,47 @@
import quart
import sqlalchemy
import argon2
from .. import group
from .....persistence.entities import user
@group.group_class('user', '/api/v1/user')
class UserRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/init', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
async def _() -> str:
if quart.request.method == 'GET':
return self.success(data={
'initialized': await self.ap.user_service.is_initialized()
})
if await self.ap.user_service.is_initialized():
return self.fail(1, '系统已初始化')
json_data = await quart.request.json
user_email = json_data['user']
password = json_data['password']
await self.ap.user_service.create_user(user_email, password)
return self.success()
@self.route('/auth', methods=['POST'], auth_type=group.AuthType.NONE)
async def _() -> str:
json_data = await quart.request.json
try:
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
except argon2.exceptions.VerifyMismatchError:
return self.fail(1, '用户名或密码错误')
return self.success(data={
'token': token
})
@self.route('/check-token', methods=['GET'])
async def _() -> str:
return self.success()

View File

@@ -0,0 +1,73 @@
from __future__ import annotations
import asyncio
import os
import quart
import quart_cors
from ....core import app, entities as core_entities
from .groups import logs, system, settings, plugins, stats, user
from . import group
class HTTPController:
ap: app.Application
quart_app: quart.Quart
def __init__(self, ap: app.Application) -> None:
self.ap = ap
self.quart_app = quart.Quart(__name__)
quart_cors.cors(self.quart_app, allow_origin="*")
async def initialize(self) -> None:
await self.register_routes()
async def run(self) -> None:
if self.ap.system_cfg.data["http-api"]["enable"]:
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
async def exception_handler(*args, **kwargs):
try:
await self.quart_app.run_task(
*args, **kwargs
)
except Exception as e:
self.ap.logger.error(f"启动 HTTP 服务失败: {e}")
self.ap.task_mgr.create_task(
exception_handler(
host=self.ap.system_cfg.data["http-api"]["host"],
port=self.ap.system_cfg.data["http-api"]["port"],
shutdown_trigger=shutdown_trigger_placeholder,
),
name="http-api-quart",
scopes=[core_entities.LifecycleControlScope.APPLICATION],
)
# await asyncio.sleep(5)
async def register_routes(self) -> None:
@self.quart_app.route("/healthz")
async def healthz():
return {"code": 0, "msg": "ok"}
for g in group.preregistered_groups:
ginst = g(self.ap, self.quart_app)
await ginst.initialize()
frontend_path = "web/dist"
@self.quart_app.route("/")
async def index():
return await quart.send_from_directory(frontend_path, "index.html")
@self.quart_app.route("/<path:path>")
async def static_file(path: str):
return await quart.send_from_directory(frontend_path, path)

View File

@@ -0,0 +1,73 @@
from __future__ import annotations
import sqlalchemy
import argon2
import jwt
import datetime
from ....core import app
from ....persistence.entities import user
from ....utils import constants
class UserService:
ap: app.Application
def __init__(self, ap: app.Application) -> None:
self.ap = ap
async def is_initialized(self) -> bool:
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(user.User).limit(1)
)
result_list = result.all()
return result_list is not None and len(result_list) > 0
async def create_user(self, user_email: str, password: str) -> None:
ph = argon2.PasswordHasher()
hashed_password = ph.hash(password)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(user.User).values(
user=user_email,
password=hashed_password
)
)
async def authenticate(self, user_email: str, password: str) -> str | None:
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(user.User).where(user.User.user == user_email)
)
result_list = result.all()
if result_list is None or len(result_list) == 0:
raise ValueError('用户不存在')
user_obj = result_list[0]
ph = argon2.PasswordHasher()
ph.verify(user_obj.password, password)
return await self.generate_jwt_token(user_email)
async def generate_jwt_token(self, user_email: str) -> str:
jwt_secret = self.ap.instance_secret_meta.data['jwt_secret']
jwt_expire = self.ap.system_cfg.data['http-api']['jwt-expire']
payload = {
'user': user_email,
'iss': 'LangBot-'+constants.edition,
'exp': datetime.datetime.now() + datetime.timedelta(seconds=jwt_expire)
}
return jwt.encode(payload, jwt_secret, algorithm='HS256')
async def verify_jwt_token(self, token: str) -> str:
jwt_secret = self.ap.instance_secret_meta.data['jwt_secret']
return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user']

View File

@@ -0,0 +1,88 @@
from __future__ import annotations
import abc
import uuid
import json
import logging
import asyncio
import aiohttp
import requests
from ...core import app, entities as core_entities
class APIGroup(metaclass=abc.ABCMeta):
"""API 组抽象类"""
_basic_info: dict = None
_runtime_info: dict = None
prefix = None
ap: app.Application
def __init__(self, prefix: str, ap: app.Application):
self.prefix = prefix
self.ap = ap
async def _do(
self,
method: str,
path: str,
data: dict = None,
params: dict = None,
headers: dict = {},
**kwargs,
):
"""
执行请求
"""
self._runtime_info["account_id"] = "-1"
url = self.prefix + path
data = json.dumps(data)
headers["Content-Type"] = "application/json"
try:
async with aiohttp.ClientSession() as session:
async with session.request(
method, url, data=data, params=params, headers=headers, **kwargs
) as resp:
self.ap.logger.debug("data: %s", data)
self.ap.logger.debug("ret: %s", await resp.text())
except Exception as e:
self.ap.logger.debug(f"上报失败: {e}")
async def do(
self,
method: str,
path: str,
data: dict = None,
params: dict = None,
headers: dict = {},
**kwargs,
) -> asyncio.Task:
"""执行请求"""
return self.ap.task_mgr.create_task(
self._do(method, path, data, params, headers, **kwargs),
kind="telemetry-operation",
name=f"{method} {path}",
scopes=[core_entities.LifecycleControlScope.APPLICATION],
).task
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

@@ -1,15 +1,22 @@
from __future__ import annotations
from .. import apigroup
from ....core import app
class V2MainDataAPI(apigroup.APIGroup):
"""主程序相关 数据API"""
def __init__(self, prefix: str):
super().__init__(prefix+"/main")
def __init__(self, prefix: str, ap: app.Application):
self.ap = ap
super().__init__(prefix+"/main", ap)
def post_update_record(
async def do(self, *args, **kwargs):
if not self.ap.system_cfg.data['report-usage']:
return None
return await super().do(*args, **kwargs)
async def post_update_record(
self,
spent_seconds: int,
infer_reason: str,
@@ -17,7 +24,7 @@ class V2MainDataAPI(apigroup.APIGroup):
new_version: str,
):
"""提交更新记录"""
return self.do(
return await self.do(
"POST",
"/update",
data={
@@ -31,12 +38,12 @@ class V2MainDataAPI(apigroup.APIGroup):
}
)
def post_announcement_showed(
async def post_announcement_showed(
self,
ids: list[int],
):
"""提交公告已阅"""
return self.do(
return await self.do(
"POST",
"/announcement",
data={

View File

@@ -1,20 +1,27 @@
from __future__ import annotations
from ....core import app
from .. import apigroup
class V2PluginDataAPI(apigroup.APIGroup):
"""插件数据相关 API"""
def __init__(self, prefix: str):
super().__init__(prefix+"/plugin")
def __init__(self, prefix: str, ap: app.Application):
self.ap = ap
super().__init__(prefix+"/plugin", ap)
def post_install_record(
async def do(self, *args, **kwargs):
if not self.ap.system_cfg.data['report-usage']:
return None
return await super().do(*args, **kwargs)
async def post_install_record(
self,
plugin: dict
):
"""提交插件安装记录"""
return self.do(
return await self.do(
"POST",
"/install",
data={
@@ -23,12 +30,12 @@ class V2PluginDataAPI(apigroup.APIGroup):
}
)
def post_remove_record(
async def post_remove_record(
self,
plugin: dict
):
"""提交插件卸载记录"""
return self.do(
return await self.do(
"POST",
"/remove",
data={
@@ -37,14 +44,14 @@ class V2PluginDataAPI(apigroup.APIGroup):
}
)
def post_update_record(
async def post_update_record(
self,
plugin: dict,
old_version: str,
new_version: str,
):
"""提交插件更新记录"""
return self.do(
return await self.do(
"POST",
"/update",
data={

View File

@@ -1,15 +1,22 @@
from __future__ import annotations
from .. import apigroup
from ....core import app
class V2UsageDataAPI(apigroup.APIGroup):
"""使用量数据相关 API"""
def __init__(self, prefix: str):
super().__init__(prefix+"/usage")
def post_query_record(
def __init__(self, prefix: str, ap: app.Application):
self.ap = ap
super().__init__(prefix+"/usage", ap)
async def do(self, *args, **kwargs):
if not self.ap.system_cfg.data['report-usage']:
return None
return await super().do(*args, **kwargs)
async def post_query_record(
self,
session_type: str,
session_id: str,
@@ -20,7 +27,7 @@ class V2UsageDataAPI(apigroup.APIGroup):
retry_times: int,
):
"""提交请求记录"""
return self.do(
return await self.do(
"POST",
"/query",
data={
@@ -40,13 +47,13 @@ class V2UsageDataAPI(apigroup.APIGroup):
}
)
def post_event_record(
async def post_event_record(
self,
plugins: list[dict],
event_name: str,
):
"""提交事件触发记录"""
return self.do(
return await self.do(
"POST",
"/event",
data={
@@ -59,14 +66,14 @@ class V2UsageDataAPI(apigroup.APIGroup):
}
)
def post_function_record(
async def post_function_record(
self,
plugin: dict,
function_name: str,
function_description: str,
):
"""提交内容函数使用记录"""
return self.do(
return await self.do(
"POST",
"/function",
data={

View File

@@ -6,10 +6,9 @@ from . import apigroup
from .groups import main
from .groups import usage
from .groups import plugin
from ...core import app
BACKEND_URL = "https://api.qchatgpt.rockchin.top/api/v2"
class V2CenterAPI:
"""中央服务器 v2 API 交互类"""
@@ -22,7 +21,7 @@ class V2CenterAPI:
plugin: plugin.V2PluginDataAPI = None
"""插件 API 组"""
def __init__(self, basic_info: dict = None, runtime_info: dict = None):
def __init__(self, ap: app.Application, backend_url: str, basic_info: dict = None, runtime_info: dict = None):
"""初始化"""
logging.debug("basic_info: %s, runtime_info: %s", basic_info, runtime_info)
@@ -30,6 +29,7 @@ class V2CenterAPI:
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)
self.main = main.V2MainDataAPI(backend_url, ap)
self.usage = usage.V2UsageDataAPI(backend_url, ap)
self.plugin = plugin.V2PluginDataAPI(backend_url, ap)

View File

@@ -1,139 +0,0 @@
"""
使用量统计以及数据上报功能实现
"""
import hashlib
import json
import logging
import threading
import requests
from ..utils import context
from ..utils import updater
class DataGatherer:
"""数据收集器"""
usage = {}
"""各api-key的使用量
以key值md5为key,{
"text": {
"text-davinci-003": 文字量:int,
},
"image": {
"256x256": 图片数量:int,
}
}为值的字典"""
version_str = "undetermined"
def __init__(self):
self.load_from_db()
try:
self.version_str = updater.get_current_tag() # 从updater模块获取版本号
except:
pass
def report_to_server(self, subservice_name: str, count: int):
"""向中央服务器报告使用量
只会报告此次请求的使用量,不会报告总量。
不包含除版本号、使用类型、使用量以外的任何信息,仅供开发者分析使用情况。
"""
def thread_func():
try:
config = context.get_config_manager().data
if not config['report_usage']:
return
res = requests.get("http://reports.rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}&msg_source={}".format(subservice_name, self.version_str, count, config['msg_source_adapter']))
if res.status_code != 200 or res.text != "ok":
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
except:
return
threading.Thread(target=thread_func).start()
def get_usage(self, key_md5):
return self.usage[key_md5] if key_md5 in self.usage else {}
def report_text_model_usage(self, model, total_tokens):
"""调用方报告文字模型请求文字使用量"""
key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5() # 以key的md5进行储存
if key_md5 not in self.usage:
self.usage[key_md5] = {}
if "text" not in self.usage[key_md5]:
self.usage[key_md5]["text"] = {}
if model not in self.usage[key_md5]["text"]:
self.usage[key_md5]["text"][model] = 0
length = total_tokens
self.usage[key_md5]["text"][model] += length
self.dump_to_db()
self.report_to_server("text", length)
def report_image_model_usage(self, size):
"""调用方报告图片模型请求图片使用量"""
key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5()
if key_md5 not in self.usage:
self.usage[key_md5] = {}
if "image" not in self.usage[key_md5]:
self.usage[key_md5]["image"] = {}
if size not in self.usage[key_md5]["image"]:
self.usage[key_md5]["image"][size] = 0
self.usage[key_md5]["image"][size] += 1
self.dump_to_db()
self.report_to_server("image", 1)
def get_text_length_of_key(self, key):
"""获取指定api-key (明文) 的文字总使用量(本地记录)"""
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
if key_md5 not in self.usage:
return 0
if "text" not in self.usage[key_md5]:
return 0
# 遍历其中所有模型,求和
return sum(self.usage[key_md5]["text"].values())
def get_image_count_of_key(self, key):
"""获取指定api-key (明文) 的图片总使用量(本地记录)"""
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
if key_md5 not in self.usage:
return 0
if "image" not in self.usage[key_md5]:
return 0
# 遍历其中所有模型,求和
return sum(self.usage[key_md5]["image"].values())
def get_total_text_length(self):
"""获取所有api-key的文字总使用量(本地记录)"""
total = 0
for key in self.usage:
if "text" not in self.usage[key]:
continue
total += sum(self.usage[key]["text"].values())
return total
def dump_to_db(self):
context.get_database_manager().dump_usage_json(self.usage)
def load_from_db(self):
json_str = context.get_database_manager().load_usage_json()
if json_str is not None:
self.usage = json.loads(json_str)

View File

@@ -1,3 +1,5 @@
# 实例 识别码 控制
import os
import uuid
import json

129
pkg/command/cmdmgr.py Normal file
View File

@@ -0,0 +1,129 @@
from __future__ import annotations
import typing
from ..core import app, entities as core_entities
from ..provider import entities as llm_entities
from . import entities, operator, errors
from ..config import manager as cfg_mgr
# 引入所有算子以便注册
from .operators import func, plugin, default, reset, list as list_cmd, last, next, delc, resend, prompt, cmd, help, version, update, ollama, model
class CommandManager:
"""命令管理器
"""
ap: app.Application
cmd_list: list[operator.CommandOperator]
"""
运行时命令列表,扁平存储,各个对象包含对应的子节点引用
"""
def __init__(self, ap: app.Application):
self.ap = ap
async def initialize(self):
# 设置各个类的路径
def set_path(cls: operator.CommandOperator, ancestors: list[str]):
cls.path = '.'.join(ancestors + [cls.name])
for op in operator.preregistered_operators:
if op.parent_class == cls:
set_path(op, ancestors + [cls.name])
for cls in operator.preregistered_operators:
if cls.parent_class is None:
set_path(cls, [])
# 应用命令权限配置
for cls in operator.preregistered_operators:
if cls.path in self.ap.command_cfg.data['privilege']:
cls.lowest_privilege = self.ap.command_cfg.data['privilege'][cls.path]
# 实例化所有类
self.cmd_list = [cls(self.ap) for cls in operator.preregistered_operators]
# 设置所有类的子节点
for cmd in self.cmd_list:
cmd.children = [child for child in self.cmd_list if child.parent_class == cmd.__class__]
# 初始化所有类
for cmd in self.cmd_list:
await cmd.initialize()
async def _execute(
self,
context: entities.ExecuteContext,
operator_list: list[operator.CommandOperator],
operator: operator.CommandOperator = None
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""执行命令
"""
found = False
if len(context.crt_params) > 0: # 查找下一个参数是否对应此节点的某个子节点名
for oper in operator_list:
if (context.crt_params[0] == oper.name \
or context.crt_params[0] in oper.alias) \
and (oper.parent_class is None or oper.parent_class == operator.__class__):
found = True
context.crt_command = context.crt_params[0]
context.crt_params = context.crt_params[1:]
async for ret in self._execute(
context,
oper.children,
oper
):
yield ret
break
if not found: # 如果下一个参数未在此节点的子节点中找到,则执行此节点或者报错
if operator is None:
yield entities.CommandReturn(
error=errors.CommandNotFoundError(context.crt_params[0])
)
else:
if operator.lowest_privilege > context.privilege:
yield entities.CommandReturn(
error=errors.CommandPrivilegeError(operator.name)
)
else:
async for ret in operator.execute(context):
yield ret
async def execute(
self,
command_text: str,
query: core_entities.Query,
session: core_entities.Session
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""执行命令
"""
privilege = 1
if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.system_cfg.data['admin-sessions']:
privilege = 2
ctx = entities.ExecuteContext(
query=query,
session=session,
command_text=command_text,
command='',
crt_command='',
params=command_text.split(' '),
crt_params=command_text.split(' '),
privilege=privilege
)
async for ret in self._execute(
ctx,
self.cmd_list
):
yield ret

76
pkg/command/entities.py Normal file
View File

@@ -0,0 +1,76 @@
from __future__ import annotations
import typing
import pydantic.v1 as pydantic
from ..core import app, entities as core_entities
from . import errors, operator
from ..platform.types import message as platform_message
class CommandReturn(pydantic.BaseModel):
"""命令返回值
"""
text: typing.Optional[str] = None
"""文本
"""
image: typing.Optional[platform_message.Image] = None
"""弃用"""
image_url: typing.Optional[str] = None
"""图片链接
"""
error: typing.Optional[errors.CommandError]= None
"""错误
"""
class Config:
arbitrary_types_allowed = True
class ExecuteContext(pydantic.BaseModel):
"""单次命令执行上下文
"""
query: core_entities.Query
"""本次消息的请求对象"""
session: core_entities.Session
"""本次消息所属的会话对象"""
command_text: str
"""命令完整文本"""
command: str
"""命令名称"""
crt_command: str
"""当前命令
多级命令中crt_command为当前命令command为根命令。
例如:!plugin on Webwlkr
处理到plugin时command为plugincrt_command为plugin
处理到on时command为plugincrt_command为on
"""
params: list[str]
"""命令参数
整个命令以空格分割后的参数列表
"""
crt_params: list[str]
"""当前命令参数
多级命令中crt_params为当前命令参数params为根命令参数。
例如:!plugin on Webwlkr
处理到plugin时params为['on', 'Webwlkr']crt_params为['on', 'Webwlkr']
处理到on时params为['on', 'Webwlkr']crt_params为['Webwlkr']
"""
privilege: int
"""发起人权限"""

33
pkg/command/errors.py Normal file
View File

@@ -0,0 +1,33 @@
class CommandError(Exception):
def __init__(self, message: str = None):
self.message = message
def __str__(self):
return self.message
class CommandNotFoundError(CommandError):
def __init__(self, message: str = None):
super().__init__("未知命令: "+message)
class CommandPrivilegeError(CommandError):
def __init__(self, message: str = None):
super().__init__("权限不足: "+message)
class ParamNotEnoughError(CommandError):
def __init__(self, message: str = None):
super().__init__("参数不足: "+message)
class CommandOperationError(CommandError):
def __init__(self, message: str = None):
super().__init__("操作失败: "+message)

113
pkg/command/operator.py Normal file
View File

@@ -0,0 +1,113 @@
from __future__ import annotations
import typing
import abc
from ..core import app, entities as core_entities
from . import entities
preregistered_operators: list[typing.Type[CommandOperator]] = []
"""预注册命令算子列表。在初始化时,所有算子类会被注册到此列表中。"""
def operator_class(
name: str,
help: str = "",
usage: str = None,
alias: list[str] = [],
privilege: int=1, # 1为普通用户2为管理员
parent_class: typing.Type[CommandOperator] = None
) -> typing.Callable[[typing.Type[CommandOperator]], typing.Type[CommandOperator]]:
"""命令类装饰器
Args:
name (str): 名称
help (str, optional): 帮助信息. Defaults to "".
usage (str, optional): 使用说明. Defaults to None.
alias (list[str], optional): 别名. Defaults to [].
privilege (int, optional): 权限1为普通用户可用2为仅管理员可用. Defaults to 1.
parent_class (typing.Type[CommandOperator], optional): 父节点若为None则为顶级命令. Defaults to None.
Returns:
typing.Callable[[typing.Type[CommandOperator]], typing.Type[CommandOperator]]: 装饰器
"""
def decorator(cls: typing.Type[CommandOperator]) -> typing.Type[CommandOperator]:
assert issubclass(cls, CommandOperator)
cls.name = name
cls.alias = alias
cls.help = help
cls.usage = usage
cls.parent_class = parent_class
cls.lowest_privilege = privilege
preregistered_operators.append(cls)
return cls
return decorator
class CommandOperator(metaclass=abc.ABCMeta):
"""命令算子抽象类
以下的参数均不需要在子类中设置,只需要在使用装饰器注册类时作为参数传递即可。
命令支持级联,即一个命令可以有多个子命令,子命令可以有子命令,以此类推。
处理命令时,若有子命令,会以当前参数列表的第一个参数去匹配子命令,若匹配成功,则转移到子命令中执行。
若没有匹配成功或没有子命令,则执行当前命令。
"""
ap: app.Application
name: str
"""名称,搜索到时若符合则使用"""
path: str
"""路径所有父节点的name的连接用于定义命令权限由管理器在初始化时自动设置。
"""
alias: list[str]
"""同name"""
help: str
"""此节点的帮助信息"""
usage: str = None
"""用法"""
parent_class: typing.Union[typing.Type[CommandOperator], None] = None
"""父节点类。标记以供管理器在初始化时编织父子关系。"""
lowest_privilege: int = 0
"""最低权限。若权限低于此值,则不予执行。"""
children: list[CommandOperator]
"""子节点。解析命令时,若节点有子节点,则以下一个参数去匹配子节点,
若有匹配中的,转移到子节点中执行,若没有匹配中的或没有子节点,执行此节点。"""
def __init__(self, ap: app.Application):
self.ap = ap
self.children = []
async def initialize(self):
pass
@abc.abstractmethod
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""实现此方法以执行命令
支持多次yield以返回多个结果。
例如:一个安装插件的命令,可能会有下载、解压、安装等多个步骤,每个步骤都可以返回一个结果。
Args:
context (entities.ExecuteContext): 命令执行上下文
Yields:
entities.CommandReturn: 命令返回封装
"""
pass

View File

@@ -0,0 +1,50 @@
from __future__ import annotations
import typing
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="cmd",
help='显示命令列表',
usage='!cmd\n!cmd <命令名称>'
)
class CmdOperator(operator.CommandOperator):
"""命令列表
"""
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""执行
"""
if len(context.crt_params) == 0:
reply_str = "当前所有命令: \n\n"
for cmd in self.ap.cmd_mgr.cmd_list:
if cmd.parent_class is None:
reply_str += f"{cmd.name}: {cmd.help}\n"
reply_str += "\n使用 !cmd <命令名称> 查看命令的详细帮助"
yield entities.CommandReturn(text=reply_str.strip())
else:
cmd_name = context.crt_params[0]
cmd = None
for _cmd in self.ap.cmd_mgr.cmd_list:
if (cmd_name == _cmd.name or cmd_name in _cmd.alias) and (_cmd.parent_class is None):
cmd = _cmd
break
if cmd is None:
yield entities.CommandReturn(error=errors.CommandNotFoundError(cmd_name))
else:
reply_str = f"{cmd.name}: {cmd.help}\n\n"
reply_str += f"使用方法: \n{cmd.usage}"
yield entities.CommandReturn(text=reply_str.strip())

View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import typing
import traceback
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="default",
help="操作情景预设",
usage='!default\n!default set <指定情景预设为默认>'
)
class DefaultOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
reply_str = "当前所有情景预设: \n\n"
for prompt in self.ap.prompt_mgr.get_all_prompts():
content = ""
for msg in prompt.messages:
content += f" {msg.readable_str()}\n"
reply_str += f"名称: {prompt.name}\n内容: \n{content}\n\n"
reply_str += f"当前会话使用的是: {context.session.use_prompt_name}"
yield entities.CommandReturn(text=reply_str.strip())
@operator.operator_class(
name="set",
help="设置当前会话默认情景预设",
parent_class=DefaultOperator
)
class DefaultSetOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供情景预设名称'))
else:
prompt_name = context.crt_params[0]
try:
prompt = await self.ap.prompt_mgr.get_prompt_by_prefix(prompt_name)
if prompt is None:
yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: 未找到情景预设 {}".format(prompt_name)))
else:
context.session.use_prompt_name = prompt.name
yield entities.CommandReturn(text=f"已设置当前会话默认情景预设为 {prompt_name}, !reset 后生效")
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("设置当前会话默认情景预设失败: "+str(e)))

View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import typing
import datetime
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="del",
help="删除当前会话的历史记录",
usage='!del <序号>\n!del all'
)
class DelOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if context.session.conversations:
delete_index = 0
if len(context.crt_params) > 0:
try:
delete_index = int(context.crt_params[0])
except:
yield entities.CommandReturn(error=errors.CommandOperationError('索引必须是整数'))
return
if delete_index < 0 or delete_index >= len(context.session.conversations):
yield entities.CommandReturn(error=errors.CommandOperationError('索引超出范围'))
return
# 倒序
to_delete_index = len(context.session.conversations)-1-delete_index
if context.session.conversations[to_delete_index] == context.session.using_conversation:
context.session.using_conversation = None
del context.session.conversations[to_delete_index]
yield entities.CommandReturn(text=f"已删除对话: {delete_index}")
else:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
@operator.operator_class(
name="all",
help="删除此会话的所有历史记录",
parent_class=DelOperator
)
class DelAllOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
context.session.conversations = []
context.session.using_conversation = None
yield entities.CommandReturn(text="已删除所有对话")

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from typing import AsyncGenerator
from .. import operator, entities, cmdmgr
from ...plugin import context as plugin_context
@operator.operator_class(name="func", help="查看所有已注册的内容函数", usage='!func')
class FuncOperator(operator.CommandOperator):
async def execute(
self, context: entities.ExecuteContext
) -> AsyncGenerator[entities.CommandReturn, None]:
reply_str = "当前已启用的内容函数: \n\n"
index = 1
all_functions = await self.ap.tool_mgr.get_all_functions(
plugin_enabled=True,
plugin_status=plugin_context.RuntimeContainerStatus.INITIALIZED,
)
for func in all_functions:
reply_str += "{}. {}:\n{}\n\n".format(
index,
func.name,
func.description,
)
index += 1
yield entities.CommandReturn(text=reply_str)

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
import typing
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name='help',
help='显示帮助',
usage='!help\n!help <命令名称>'
)
class HelpOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
help = self.ap.system_cfg.data['help-message']
help += '\n发送命令 !cmd 可查看命令列表'
yield entities.CommandReturn(text=help)

View File

@@ -0,0 +1,36 @@
from __future__ import annotations
import typing
import datetime
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="last",
help="切换到前一个对话",
usage='!last'
)
class LastOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if context.session.conversations:
# 找到当前会话的上一个会话
for index in range(len(context.session.conversations)-1, -1, -1):
if context.session.conversations[index] == context.session.using_conversation:
if index == 0:
yield entities.CommandReturn(error=errors.CommandOperationError('已经是第一个对话了'))
return
else:
context.session.using_conversation = context.session.conversations[index-1]
time_str = context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")
yield entities.CommandReturn(text=f"已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}")
return
else:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))

View File

@@ -0,0 +1,56 @@
from __future__ import annotations
import typing
import datetime
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="list",
help="列出此会话中的所有历史对话",
usage='!list\n!list <页码>'
)
class ListOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
page = 0
if len(context.crt_params) > 0:
try:
page = int(context.crt_params[0]-1)
except:
yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数'))
return
record_per_page = 10
content = ''
index = 0
using_conv_index = 0
for conv in context.session.conversations[::-1]:
time_str = conv.create_time.strftime("%Y-%m-%d %H:%M:%S")
if conv == context.session.using_conversation:
using_conv_index = index
if index >= page * record_per_page and index < (page + 1) * record_per_page:
content += f"{index} {time_str}: {conv.messages[0].readable_str() if len(conv.messages) > 0 else '无内容'}\n"
index += 1
if content == '':
content = ''
else:
if context.session.using_conversation is None:
content += "\n当前处于新会话"
else:
content += f"\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else '无内容'}"
yield entities.CommandReturn(text=f"{page + 1} 页 (时间倒序):\n{content}")

View File

@@ -0,0 +1,86 @@
from __future__ import annotations
import typing
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="model",
help='显示和切换模型列表',
usage='!model\n!model show <模型名>\n!model set <模型名>',
privilege=2
)
class ModelOperator(operator.CommandOperator):
"""Model命令"""
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
content = '模型列表:\n'
model_list = self.ap.model_mgr.model_list
for model in model_list:
content += f"\n名称: {model.name}\n"
content += f"请求器: {model.requester.name}\n"
content += f"\n当前对话使用模型: {context.query.use_model.name}\n"
content += f"新对话默认使用模型: {self.ap.provider_cfg.data.get('model')}\n"
yield entities.CommandReturn(text=content.strip())
@operator.operator_class(
name="show",
help='显示模型详情',
privilege=2,
parent_class=ModelOperator
)
class ModelShowOperator(operator.CommandOperator):
"""Model Show命令"""
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
model_name = context.crt_params[0]
model = None
for _model in self.ap.model_mgr.model_list:
if model_name == _model.name:
model = _model
break
if model is None:
yield entities.CommandReturn(error=errors.CommandError(f"未找到模型 {model_name}"))
else:
content = f"模型详情\n"
content += f"名称: {model.name}\n"
if model.model_name is not None:
content += f"请求模型名称: {model.model_name}\n"
content += f"请求器: {model.requester.name}\n"
content += f"密钥组: {model.token_mgr.provider}\n"
content += f"支持视觉: {model.vision_supported}\n"
content += f"支持工具: {model.tool_call_supported}\n"
yield entities.CommandReturn(text=content.strip())
@operator.operator_class(
name="set",
help='设置默认使用模型',
privilege=2,
parent_class=ModelOperator
)
class ModelSetOperator(operator.CommandOperator):
"""Model Set命令"""
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
model_name = context.crt_params[0]
model = None
for _model in self.ap.model_mgr.model_list:
if model_name == _model.name:
model = _model
break
if model is None:
yield entities.CommandReturn(error=errors.CommandError(f"未找到模型 {model_name}"))
else:
self.ap.provider_cfg.data['model'] = model_name
await self.ap.provider_cfg.dump_config()
yield entities.CommandReturn(text=f"已设置当前使用模型为 {model_name},重置会话以生效")

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
import typing
import datetime
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="next",
help="切换到后一个对话",
usage='!next'
)
class NextOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if context.session.conversations:
# 找到当前会话的下一个会话
for index in range(len(context.session.conversations)):
if context.session.conversations[index] == context.session.using_conversation:
if index == len(context.session.conversations)-1:
yield entities.CommandReturn(error=errors.CommandOperationError('已经是最后一个对话了'))
return
else:
context.session.using_conversation = context.session.conversations[index+1]
time_str = context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")
yield entities.CommandReturn(text=f"已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}")
return
else:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))

View File

@@ -0,0 +1,121 @@
from __future__ import annotations
import json
import typing
import traceback
import ollama
from .. import operator, entities, errors
@operator.operator_class(
name="ollama",
help="ollama平台操作",
usage="!ollama\n!ollama show <模型名>\n!ollama pull <模型名>\n!ollama del <模型名>"
)
class OllamaOperator(operator.CommandOperator):
async def execute(
self, context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
try:
content: str = '模型列表:\n'
model_list: list = ollama.list().get('models', [])
for model in model_list:
content += f"名称: {model['name']}\n"
content += f"修改时间: {model['modified_at']}\n"
content += f"大小: {bytes_to_mb(model['size'])}MB\n\n"
yield entities.CommandReturn(text=f"{content.strip()}")
except ollama.ResponseError as e:
yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型列表,请确认 Ollama 服务正常"))
def bytes_to_mb(num_bytes):
mb: float = num_bytes / 1024 / 1024
return format(mb, '.2f')
@operator.operator_class(
name="show",
help="ollama模型详情",
privilege=2,
parent_class=OllamaOperator
)
class OllamaShowOperator(operator.CommandOperator):
async def execute(
self, context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
content: str = '模型详情:\n'
try:
show: dict = ollama.show(model=context.crt_params[0])
model_info: dict = show.get('model_info', {})
ignore_show: str = 'too long to show...'
for key in ['license', 'modelfile']:
show[key] = ignore_show
for key in ['tokenizer.chat_template.rag', 'tokenizer.chat_template.tool_use']:
model_info[key] = ignore_show
content += json.dumps(show, indent=4)
yield entities.CommandReturn(text=content.strip())
except ollama.ResponseError as e:
yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型详情,请确认 Ollama 服务正常"))
@operator.operator_class(
name="pull",
help="ollama模型拉取",
privilege=2,
parent_class=OllamaOperator
)
class OllamaPullOperator(operator.CommandOperator):
async def execute(
self, context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
try:
model_list: list = ollama.list().get('models', [])
if context.crt_params[0] in [model['name'] for model in model_list]:
yield entities.CommandReturn(text="模型已存在")
return
except ollama.ResponseError as e:
yield entities.CommandReturn(error=errors.CommandError(f"无法获取模型列表,请确认 Ollama 服务正常"))
return
on_progress: bool = False
progress_count: int = 0
try:
for resp in ollama.pull(model=context.crt_params[0], stream=True):
total: typing.Any = resp.get('total')
if not on_progress:
if total is not None:
on_progress = True
yield entities.CommandReturn(text=resp.get('status'))
else:
if total is None:
on_progress = False
completed: typing.Any = resp.get('completed')
if isinstance(completed, int) and isinstance(total, int):
percentage_completed = (completed / total) * 100
if percentage_completed > progress_count:
progress_count += 10
yield entities.CommandReturn(
text=f"下载进度: {completed}/{total} ({percentage_completed:.2f}%)")
except ollama.ResponseError as e:
yield entities.CommandReturn(text=f"拉取失败: {e.error}")
@operator.operator_class(
name="del",
help="ollama模型删除",
privilege=2,
parent_class=OllamaOperator
)
class OllamaDelOperator(operator.CommandOperator):
async def execute(
self, context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
try:
ret: str = ollama.delete(model=context.crt_params[0])['status']
except ollama.ResponseError as e:
ret = f"{e.error}"
yield entities.CommandReturn(text=ret)

View File

@@ -0,0 +1,219 @@
from __future__ import annotations
import typing
import traceback
from .. import operator, entities, cmdmgr, errors
from ...core import app
@operator.operator_class(
name="plugin",
help="插件操作",
usage="!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>"
)
class PluginOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
plugin_list = self.ap.plugin_mgr.plugins()
reply_str = "所有插件({}):\n".format(len(plugin_list))
idx = 0
for plugin in plugin_list:
reply_str += "\n#{} {} {}\n{}\nv{}\n作者: {}\n"\
.format((idx+1), plugin.plugin_name,
"[已禁用]" if not plugin.enabled else "",
plugin.plugin_description,
plugin.plugin_version, plugin.plugin_author)
# TODO 从元数据调远程地址
idx += 1
yield entities.CommandReturn(text=reply_str)
@operator.operator_class(
name="get",
help="安装插件",
privilege=2,
parent_class=PluginOperator
)
class PluginGetOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件仓库地址'))
else:
repo = context.crt_params[0]
yield entities.CommandReturn(text="正在安装插件...")
try:
await self.ap.plugin_mgr.install_plugin(repo)
yield entities.CommandReturn(text="插件安装成功,请重启程序以加载插件")
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("插件安装失败: "+str(e)))
@operator.operator_class(
name="update",
help="更新插件",
privilege=2,
parent_class=PluginOperator
)
class PluginUpdateOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
else:
plugin_name = context.crt_params[0]
try:
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is not None:
yield entities.CommandReturn(text="正在更新插件...")
await self.ap.plugin_mgr.update_plugin(plugin_name)
yield entities.CommandReturn(text="插件更新成功,请重启程序以加载插件")
else:
yield entities.CommandReturn(error=errors.CommandError("插件更新失败: 未找到插件"))
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("插件更新失败: "+str(e)))
@operator.operator_class(
name="all",
help="更新所有插件",
privilege=2,
parent_class=PluginUpdateOperator
)
class PluginUpdateAllOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
try:
plugins = [
p.plugin_name
for p in self.ap.plugin_mgr.plugins()
]
if plugins:
yield entities.CommandReturn(text="正在更新插件...")
updated = []
try:
for plugin_name in plugins:
await self.ap.plugin_mgr.update_plugin(plugin_name)
updated.append(plugin_name)
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("插件更新失败: "+str(e)))
yield entities.CommandReturn(text="已更新插件: {}".format(", ".join(updated)))
else:
yield entities.CommandReturn(text="没有可更新的插件")
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("插件更新失败: "+str(e)))
@operator.operator_class(
name="del",
help="删除插件",
privilege=2,
parent_class=PluginOperator
)
class PluginDelOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
else:
plugin_name = context.crt_params[0]
try:
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is not None:
yield entities.CommandReturn(text="正在删除插件...")
await self.ap.plugin_mgr.uninstall_plugin(plugin_name)
yield entities.CommandReturn(text="插件删除成功,请重启程序以加载插件")
else:
yield entities.CommandReturn(error=errors.CommandError("插件删除失败: 未找到插件"))
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("插件删除失败: "+str(e)))
@operator.operator_class(
name="on",
help="启用插件",
privilege=2,
parent_class=PluginOperator
)
class PluginEnableOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
else:
plugin_name = context.crt_params[0]
try:
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, True):
yield entities.CommandReturn(text="已启用插件: {}".format(plugin_name))
else:
yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name)))
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: "+str(e)))
@operator.operator_class(
name="off",
help="禁用插件",
privilege=2,
parent_class=PluginOperator
)
class PluginDisableOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
else:
plugin_name = context.crt_params[0]
try:
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, False):
yield entities.CommandReturn(text="已禁用插件: {}".format(plugin_name))
else:
yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name)))
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: "+str(e)))

View File

@@ -0,0 +1,29 @@
from __future__ import annotations
import typing
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="prompt",
help="查看当前对话的前文",
usage='!prompt'
)
class PromptOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""执行
"""
if context.session.using_conversation is None:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
else:
reply_str = '当前对话所有内容:\n\n'
for msg in context.session.using_conversation.messages:
reply_str += f"{msg.role}: {msg.content}\n"
yield entities.CommandReturn(text=reply_str)

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
import typing
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="resend",
help="重发当前会话的最后一条消息",
usage='!resend'
)
class ResendOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
# 回滚到最后一条用户message前
if context.session.using_conversation is None:
yield entities.CommandReturn(error=errors.CommandError("当前没有对话"))
else:
conv_msg = context.session.using_conversation.messages
# 倒序一直删到最后一条用户message
while len(conv_msg) > 0 and conv_msg[-1].role != 'user':
conv_msg.pop()
if len(conv_msg) > 0:
# 删除最后一条用户message
conv_msg.pop()
# 不重发了,提示用户已删除就行了
yield entities.CommandReturn(text="已删除最后一次请求记录")

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
import typing
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="reset",
help="重置当前会话",
usage='!reset'
)
class ResetOperator(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""执行
"""
context.session.using_conversation = None
yield entities.CommandReturn(text="已重置当前会话")

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
import typing
import traceback
from .. import operator, entities, cmdmgr, errors
@operator.operator_class(
name="update",
help="更新程序",
usage='!update',
privilege=2
)
class UpdateCommand(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
try:
yield entities.CommandReturn(text="正在进行更新...")
if await self.ap.ver_mgr.update_all():
yield entities.CommandReturn(text="更新完成,请重启程序以应用更新")
else:
yield entities.CommandReturn(text="当前已是最新版本")
except Exception as e:
traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError("更新失败: "+str(e)))

View File

@@ -0,0 +1,27 @@
from __future__ import annotations
import typing
from .. import operator, cmdmgr, entities, errors
@operator.operator_class(
name="version",
help="显示版本信息",
usage='!version'
)
class VersionCommand(operator.CommandOperator):
async def execute(
self,
context: entities.ExecuteContext
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
reply_str = f"当前版本: \n{self.ap.ver_mgr.get_current_version()}"
try:
if await self.ap.ver_mgr.is_new_version_available():
reply_str += "\n\n有新版本可用。"
except:
pass
yield entities.CommandReturn(text=reply_str.strip())

59
pkg/config/impls/json.py Normal file
View File

@@ -0,0 +1,59 @@
import os
import shutil
import json
from .. import model as file_model
class JSONConfigFile(file_model.ConfigFile):
"""JSON配置文件"""
def __init__(
self, config_file_name: str, template_file_name: str = None, template_data: dict = None
) -> None:
self.config_file_name = config_file_name
self.template_file_name = template_file_name
self.template_data = template_data
def exists(self) -> bool:
return os.path.exists(self.config_file_name)
async def create(self):
if self.template_file_name is not None:
shutil.copyfile(self.template_file_name, self.config_file_name)
elif self.template_data is not None:
with open(self.config_file_name, "w", encoding="utf-8") as f:
json.dump(self.template_data, f, indent=4, ensure_ascii=False)
else:
raise ValueError("template_file_name or template_data must be provided")
async def load(self, completion: bool=True) -> dict:
if not self.exists():
await self.create()
if self.template_file_name is not None:
with open(self.template_file_name, "r", encoding="utf-8") as f:
self.template_data = json.load(f)
with open(self.config_file_name, "r", encoding="utf-8") as f:
try:
cfg = json.load(f)
except json.JSONDecodeError as e:
raise Exception(f"配置文件 {self.config_file_name} 语法错误: {e}")
if completion:
for key in self.template_data:
if key not in cfg:
cfg[key] = self.template_data[key]
return cfg
async def save(self, cfg: dict):
with open(self.config_file_name, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=4, ensure_ascii=False)
def save_sync(self, cfg: dict):
with open(self.config_file_name, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=4, ensure_ascii=False)

View File

@@ -25,7 +25,7 @@ class PythonModuleConfigFile(file_model.ConfigFile):
async def create(self):
shutil.copyfile(self.template_file_name, self.config_file_name)
async def load(self) -> dict:
async def load(self, completion: bool=True) -> dict:
module_name = os.path.splitext(os.path.basename(self.config_file_name))[0]
module = importlib.import_module(module_name)
@@ -43,20 +43,24 @@ class PythonModuleConfigFile(file_model.ConfigFile):
cfg[key] = getattr(module, key)
# 从模板模块文件中进行补全
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
module = importlib.import_module(module_name)
if completion:
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
for key in dir(module):
if key.startswith('__'):
continue
if not isinstance(getattr(module, key), allowed_types):
continue
if not isinstance(getattr(module, key), allowed_types):
continue
if key not in cfg:
cfg[key] = getattr(module, key)
if key not in cfg:
cfg[key] = getattr(module, key)
return cfg
async def save(self, data: dict):
logging.warning('Python模块配置文件不支持保存')
def save_sync(self, data: dict):
logging.warning('Python模块配置文件不支持保存')

59
pkg/config/impls/yaml.py Normal file
View File

@@ -0,0 +1,59 @@
import os
import shutil
import yaml
from .. import model as file_model
class YAMLConfigFile(file_model.ConfigFile):
"""YAML配置文件"""
def __init__(
self, config_file_name: str, template_file_name: str = None, template_data: dict = None
) -> None:
self.config_file_name = config_file_name
self.template_file_name = template_file_name
self.template_data = template_data
def exists(self) -> bool:
return os.path.exists(self.config_file_name)
async def create(self):
if self.template_file_name is not None:
shutil.copyfile(self.template_file_name, self.config_file_name)
elif self.template_data is not None:
with open(self.config_file_name, "w", encoding="utf-8") as f:
yaml.dump(self.template_data, f, indent=4, allow_unicode=True)
else:
raise ValueError("template_file_name or template_data must be provided")
async def load(self, completion: bool=True) -> dict:
if not self.exists():
await self.create()
if self.template_file_name is not None:
with open(self.template_file_name, "r", encoding="utf-8") as f:
self.template_data = yaml.load(f, Loader=yaml.FullLoader)
with open(self.config_file_name, "r", encoding="utf-8") as f:
try:
cfg = yaml.load(f, Loader=yaml.FullLoader)
except yaml.YAMLError as e:
raise Exception(f"配置文件 {self.config_file_name} 语法错误: {e}")
if completion:
for key in self.template_data:
if key not in cfg:
cfg[key] = self.template_data[key]
return cfg
async def save(self, cfg: dict):
with open(self.config_file_name, "w", encoding="utf-8") as f:
yaml.dump(cfg, f, indent=4, allow_unicode=True)
def save_sync(self, cfg: dict):
with open(self.config_file_name, "w", encoding="utf-8") as f:
yaml.dump(cfg, f, indent=4, allow_unicode=True)

View File

@@ -1,9 +1,22 @@
from __future__ import annotations
from . import model as file_model
from ..utils import context
from .impls import pymodule, json as json_file, yaml as yaml_file
class ConfigManager:
"""配置文件管理器"""
name: str = None
"""配置管理器名"""
description: str = None
"""配置管理器描述"""
schema: dict = None
"""配置文件 schema
需要符合 JSON Schema Draft 7 规范
"""
file: file_model.ConfigFile = None
"""配置文件实例"""
@@ -11,13 +24,85 @@ class ConfigManager:
data: dict = None
"""配置数据"""
doc_link: str = 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 load_config(self, completion: bool=True):
self.data = await self.file.load(completion=completion)
async def dump_config(self):
await self.file.save(self.data)
def dump_config_sync(self):
self.file.save_sync(self.data)
async def load_python_module_config(config_name: str, template_name: str, completion: bool=True) -> ConfigManager:
"""加载Python模块配置文件
Args:
config_name (str): 配置文件名
template_name (str): 模板文件名
completion (bool): 是否自动补全内存中的配置文件
Returns:
ConfigManager: 配置文件管理器
"""
cfg_inst = pymodule.PythonModuleConfigFile(
config_name,
template_name
)
cfg_mgr = ConfigManager(cfg_inst)
await cfg_mgr.load_config(completion=completion)
return cfg_mgr
async def load_json_config(config_name: str, template_name: str=None, template_data: dict=None, completion: bool=True) -> ConfigManager:
"""加载JSON配置文件
Args:
config_name (str): 配置文件名
template_name (str): 模板文件名
template_data (dict): 模板数据
completion (bool): 是否自动补全内存中的配置文件
"""
cfg_inst = json_file.JSONConfigFile(
config_name,
template_name,
template_data
)
cfg_mgr = ConfigManager(cfg_inst)
await cfg_mgr.load_config(completion=completion)
return cfg_mgr
async def load_yaml_config(config_name: str, template_name: str=None, template_data: dict=None, completion: bool=True) -> ConfigManager:
"""加载YAML配置文件
Args:
config_name (str): 配置文件名
template_name (str): 模板文件名
template_data (dict): 模板数据
completion (bool): 是否自动补全内存中的配置文件
Returns:
ConfigManager: 配置文件管理器
"""
cfg_inst = yaml_file.YAMLConfigFile(
config_name,
template_name,
template_data
)
cfg_mgr = ConfigManager(cfg_inst)
await cfg_mgr.load_config(completion=completion)
return cfg_mgr

View File

@@ -10,6 +10,9 @@ class ConfigFile(metaclass=abc.ABCMeta):
template_file_name: str = None
"""模板文件名"""
template_data: dict = None
"""模板数据"""
@abc.abstractmethod
def exists(self) -> bool:
pass
@@ -19,9 +22,13 @@ class ConfigFile(metaclass=abc.ABCMeta):
pass
@abc.abstractmethod
async def load(self) -> dict:
async def load(self, completion: bool=True) -> dict:
pass
@abc.abstractmethod
async def save(self, data: dict):
pass
@abc.abstractmethod
def save_sync(self, data: dict):
pass

75
pkg/config/settings.py Normal file
View File

@@ -0,0 +1,75 @@
from __future__ import annotations
from . import manager as config_manager
from ..core import app
class SettingsManager:
"""设置管理器
保存、管理多个配置文件管理器
"""
ap: app.Application
managers: list[config_manager.ConfigManager] = []
"""配置文件管理器列表"""
def __init__(self, ap: app.Application) -> None:
self.ap = ap
self.managers = []
async def initialize(self) -> None:
pass
def register_manager(
self,
name: str,
description: str,
manager: config_manager.ConfigManager,
schema: dict=None,
doc_link: str=None,
) -> None:
"""注册配置管理器
Args:
name (str): 配置管理器名
description (str): 配置管理器描述
manager (ConfigManager): 配置管理器
schema (dict): 配置文件 schema符合 JSON Schema Draft 7 规范
"""
for m in self.managers:
if m.name == name:
raise ValueError(f'配置管理器名 {name} 已存在')
manager.name = name
manager.description = description
manager.schema = schema
manager.doc_link = doc_link
self.managers.append(manager)
def get_manager(self, name: str) -> config_manager.ConfigManager | None:
"""获取配置管理器
Args:
name (str): 配置管理器名
Returns:
ConfigManager: 配置管理器
"""
for m in self.managers:
if m.name == name:
return m
return None
def get_manager_list(self) -> list[config_manager.ConfigManager]:
"""获取配置管理器列表
Returns:
list[ConfigManager]: 配置管理器列表
"""
return self.managers

203
pkg/core/app.py Normal file
View File

@@ -0,0 +1,203 @@
from __future__ import annotations
import logging
import asyncio
import threading
import traceback
import enum
import sys
import os
from ..platform import manager as im_mgr
from ..provider.session import sessionmgr as llm_session_mgr
from ..provider.modelmgr import modelmgr as llm_model_mgr
from ..provider.sysprompt import sysprompt as llm_prompt_mgr
from ..provider.tools import toolmgr as llm_tool_mgr
from ..provider import runnermgr
from ..config import manager as config_mgr
from ..config import settings as settings_mgr
from ..audit.center import v2 as center_mgr
from ..command import cmdmgr
from ..plugin import manager as plugin_mgr
from ..pipeline import pool
from ..pipeline import controller, stagemgr
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
from ..persistence import mgr as persistencemgr
from ..api.http.controller import main as http_controller
from ..api.http.service import user as user_service
from ..utils import logcache, ip
from . import taskmgr
from . import entities as core_entities
class Application:
"""运行时应用对象和上下文"""
event_loop: asyncio.AbstractEventLoop = None
# asyncio_tasks: list[asyncio.Task] = []
task_mgr: taskmgr.AsyncTaskManager = None
platform_mgr: im_mgr.PlatformManager = None
cmd_mgr: cmdmgr.CommandManager = None
sess_mgr: llm_session_mgr.SessionManager = None
model_mgr: llm_model_mgr.ModelManager = None
prompt_mgr: llm_prompt_mgr.PromptManager = None
tool_mgr: llm_tool_mgr.ToolManager = None
runner_mgr: runnermgr.RunnerManager = None
settings_mgr: settings_mgr.SettingsManager = None
# ======= 配置管理器 =======
command_cfg: config_mgr.ConfigManager = None
pipeline_cfg: config_mgr.ConfigManager = None
platform_cfg: config_mgr.ConfigManager = None
provider_cfg: config_mgr.ConfigManager = None
system_cfg: config_mgr.ConfigManager = None
# ======= 元数据配置管理器 =======
sensitive_meta: config_mgr.ConfigManager = None
adapter_qq_botpy_meta: config_mgr.ConfigManager = None
plugin_setting_meta: config_mgr.ConfigManager = None
llm_models_meta: config_mgr.ConfigManager = None
instance_secret_meta: config_mgr.ConfigManager = None
# =========================
ctr_mgr: center_mgr.V2CenterAPI = None
plugin_mgr: plugin_mgr.PluginManager = None
query_pool: pool.QueryPool = None
ctrl: controller.Controller = None
stage_mgr: stagemgr.StageManager = None
ver_mgr: version_mgr.VersionManager = None
ann_mgr: announce_mgr.AnnouncementManager = None
proxy_mgr: proxy_mgr.ProxyManager = None
logger: logging.Logger = None
persistence_mgr: persistencemgr.PersistenceManager = None
http_ctrl: http_controller.HTTPController = None
log_cache: logcache.LogCache = None
# ========= HTTP Services =========
user_service: user_service.UserService = None
def __init__(self):
pass
async def initialize(self):
pass
async def run(self):
try:
await self.plugin_mgr.initialize_plugins()
# 后续可能会允许动态重启其他任务
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
async def never_ending():
while True:
await asyncio.sleep(1)
self.task_mgr.create_task(self.platform_mgr.run(), name="platform-manager", scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM])
self.task_mgr.create_task(self.ctrl.run(), name="query-controller", scopes=[core_entities.LifecycleControlScope.APPLICATION])
self.task_mgr.create_task(self.http_ctrl.run(), name="http-api-controller", scopes=[core_entities.LifecycleControlScope.APPLICATION])
self.task_mgr.create_task(never_ending(), name="never-ending-task", scopes=[core_entities.LifecycleControlScope.APPLICATION])
await self.print_web_access_info()
await self.task_mgr.wait_all()
except asyncio.CancelledError:
pass
except Exception as e:
self.logger.error(f"应用运行致命异常: {e}")
self.logger.debug(f"Traceback: {traceback.format_exc()}")
async def print_web_access_info(self):
"""打印访问 webui 的提示"""
if not os.path.exists(os.path.join(".", "web/dist")):
self.logger.warning("WebUI 文件缺失请根据文档获取https://docs.langbot.app/webui/intro.html")
return
import socket
host_ip = socket.gethostbyname(socket.gethostname())
public_ip = await ip.get_myip()
port = self.system_cfg.data['http-api']['port']
tips = f"""
=======================================
✨ 您可通过以下方式访问管理面板
🏠 本地地址http://{host_ip}:{port}/
🌐 公网地址http://{public_ip}:{port}/
📌 如果您在容器中运行此程序,请确保容器的 {port} 端口已对外暴露
🔗 若要使用公网地址访问,请阅读以下须知
1. 公网地址仅供参考,请以您的主机公网 IP 为准;
2. 要使用公网地址访问,请确保您的主机具有公网 IP并且系统防火墙已放行 {port} 端口;
🤯 WebUI 仍处于 Beta 测试阶段,如有问题或建议请反馈到 https://github.com/RockChinQ/LangBot/issues
=======================================
""".strip()
for line in tips.split("\n"):
self.logger.info(line)
async def reload(
self,
scope: core_entities.LifecycleControlScope,
):
match scope:
case core_entities.LifecycleControlScope.PLATFORM.value:
self.logger.info("执行热重载 scope="+scope)
await self.platform_mgr.shutdown()
self.platform_mgr = im_mgr.PlatformManager(self)
await self.platform_mgr.initialize()
self.task_mgr.create_task(self.platform_mgr.run(), name="platform-manager", scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM])
case core_entities.LifecycleControlScope.PLUGIN.value:
self.logger.info("执行热重载 scope="+scope)
await self.plugin_mgr.destroy_plugins()
# 删除 sys.module 中所有的 plugins/* 下的模块
for mod in list(sys.modules.keys()):
if mod.startswith("plugins."):
del sys.modules[mod]
self.plugin_mgr = plugin_mgr.PluginManager(self)
await self.plugin_mgr.initialize()
await self.plugin_mgr.initialize_plugins()
await self.plugin_mgr.load_plugins()
await self.plugin_mgr.initialize_plugins()
case _:
pass

69
pkg/core/boot.py Normal file
View File

@@ -0,0 +1,69 @@
from __future__ import print_function
import traceback
import asyncio
import os
from . import app
from ..audit import identifier
from . import stage
from ..utils import constants
# 引入启动阶段实现以便注册
from .stages import load_config, setup_logger, build_app, migrate, show_notes
stage_order = [
"LoadConfigStage",
"MigrationStage",
"SetupLoggerStage",
"BuildAppStage",
"ShowNotesStage"
]
async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
# 生成标识符
identifier.init()
# 确定是否为调试模式
if "DEBUG" in os.environ and os.environ["DEBUG"] in ["true", "1"]:
constants.debug_mode = True
ap = app.Application()
ap.event_loop = loop
# 执行启动阶段
for stage_name in stage_order:
stage_cls = stage.preregistered_stages[stage_name]
stage_inst = stage_cls()
await stage_inst.run(ap)
await ap.initialize()
return ap
async def main(loop: asyncio.AbstractEventLoop):
try:
# 挂系统信号处理
import signal
ap: app.Application
def signal_handler(sig, frame):
print("[Signal] 程序退出.")
# ap.shutdown()
os._exit(0)
signal.signal(signal.SIGINT, signal_handler)
app_inst = await make_app(loop)
ap = app_inst
await app_inst.run()
except Exception as e:
traceback.print_exc()

View File

@@ -0,0 +1,10 @@
from __future__ import annotations
import json
from ...config import manager as config_mgr
from ...config.impls import pymodule
load_python_module_config = config_mgr.load_python_module_config
load_json_config = config_mgr.load_json_config

View File

@@ -0,0 +1,44 @@
import pip
required_deps = {
"requests": "requests",
"openai": "openai",
"anthropic": "anthropic",
"colorlog": "colorlog",
"aiocqhttp": "aiocqhttp",
"botpy": "qq-botpy-rc",
"PIL": "pillow",
"nakuru": "nakuru-project-idk",
"tiktoken": "tiktoken",
"yaml": "pyyaml",
"aiohttp": "aiohttp",
"psutil": "psutil",
"async_lru": "async-lru",
"ollama": "ollama",
"quart": "quart",
"quart_cors": "quart-cors",
"sqlalchemy": "sqlalchemy[asyncio]",
"aiosqlite": "aiosqlite",
"aiofiles": "aiofiles",
"aioshutil": "aioshutil",
"argon2": "argon2-cffi",
"jwt": "pyjwt",
}
async def check_deps() -> list[str]:
global required_deps
missing_deps = []
for dep in required_deps:
try:
__import__(dep)
except ImportError:
missing_deps.append(dep)
return missing_deps
async def install_deps(deps: list[str]):
global required_deps
for dep in deps:
pip.main(["install", required_deps[dep]])

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
import os
import shutil
import sys
required_files = {
"plugins/__init__.py": "templates/__init__.py",
"plugins/plugins.json": "templates/plugin-settings.json",
"data/config/command.json": "templates/command.json",
"data/config/pipeline.json": "templates/pipeline.json",
"data/config/platform.json": "templates/platform.json",
"data/config/provider.json": "templates/provider.json",
"data/config/system.json": "templates/system.json",
"data/scenario/default.json": "templates/scenario-template.json",
}
required_paths = [
"temp",
"data",
"data/metadata",
"data/prompts",
"data/scenario",
"data/logs",
"data/config",
"plugins"
]
async def generate_files() -> list[str]:
global required_files, required_paths
for required_paths in required_paths:
if not os.path.exists(required_paths):
os.mkdir(required_paths)
generated_files = []
for file in required_files:
if not os.path.exists(file):
shutil.copyfile(required_files[file], file)
generated_files.append(file)
return generated_files

67
pkg/core/bootutils/log.py Normal file
View File

@@ -0,0 +1,67 @@
import logging
import os
import sys
import time
import colorlog
from ...utils import constants
log_colors_config = {
"DEBUG": "green", # cyan white
"INFO": "white",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "cyan",
}
async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.Logger:
# 删除所有现有的logger
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
level = logging.INFO
if constants.debug_mode:
level = logging.DEBUG
log_file_name = "data/logs/langbot-%s.log" % time.strftime(
"%Y-%m-%d", time.localtime()
)
qcg_logger = logging.getLogger("qcg")
qcg_logger.setLevel(level)
color_formatter = colorlog.ColoredFormatter(
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
datefmt="%m-%d %H:%M:%S",
log_colors=log_colors_config,
)
stream_handler = logging.StreamHandler(sys.stdout)
# stream_handler.setLevel(level)
# stream_handler.setFormatter(color_formatter)
stream_handler.stream = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
log_handlers: list[logging.Handler] = [stream_handler, logging.FileHandler(log_file_name, encoding='utf-8')]
log_handlers += extra_handlers if extra_handlers is not None else []
for handler in log_handlers:
handler.setLevel(level)
handler.setFormatter(color_formatter)
qcg_logger.addHandler(handler)
qcg_logger.debug("日志初始化完成,日志级别:%s" % level)
logging.basicConfig(
level=logging.CRITICAL, # 设置日志输出格式
format="[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s",
# 日志输出的格式
# -8表示占位符让输出左对齐输出长度都为8位
datefmt="%Y-%m-%d %H:%M:%S", # 时间输出的格式
handlers=[logging.NullHandler()],
)
return qcg_logger

131
pkg/core/entities.py Normal file
View File

@@ -0,0 +1,131 @@
from __future__ import annotations
import enum
import typing
import datetime
import asyncio
import pydantic.v1 as pydantic
from ..provider import entities as llm_entities
from ..provider.modelmgr import entities
from ..provider.sysprompt import entities as sysprompt_entities
from ..provider.tools import entities as tools_entities
from ..platform import adapter as msadapter
from ..platform.types import message as platform_message
from ..platform.types import events as platform_events
from ..platform.types import entities as platform_entities
class LifecycleControlScope(enum.Enum):
APPLICATION = "application"
PLATFORM = "platform"
PLUGIN = "plugin"
class LauncherTypes(enum.Enum):
"""一个请求的发起者类型"""
PERSON = 'person'
"""私聊"""
GROUP = 'group'
"""群聊"""
class Query(pydantic.BaseModel):
"""一次请求的信息封装"""
query_id: int
"""请求ID添加进请求池时生成"""
launcher_type: LauncherTypes
"""会话类型platform处理阶段设置"""
launcher_id: int
"""会话IDplatform处理阶段设置"""
sender_id: int
"""发送者IDplatform处理阶段设置"""
message_event: platform_events.MessageEvent
"""事件platform收到的原始事件"""
message_chain: platform_message.MessageChain
"""消息链platform收到的原始消息链"""
adapter: msadapter.MessageSourceAdapter
"""消息平台适配器对象单个app中可能启用了多个消息平台适配器此对象表明发起此query的适配器"""
session: typing.Optional[Session] = None
"""会话对象,由前置处理器阶段设置"""
messages: typing.Optional[list[llm_entities.Message]] = []
"""历史消息列表,由前置处理器阶段设置"""
prompt: typing.Optional[sysprompt_entities.Prompt] = None
"""情景预设内容,由前置处理器阶段设置"""
user_message: typing.Optional[llm_entities.Message] = None
"""此次请求的用户消息对象,由前置处理器阶段设置"""
use_model: typing.Optional[entities.LLMModelInfo] = None
"""使用的模型,由前置处理器阶段设置"""
use_funcs: typing.Optional[list[tools_entities.LLMFunction]] = None
"""使用的函数,由前置处理器阶段设置"""
resp_messages: typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] = []
"""由Process阶段生成的回复消息对象列表"""
resp_message_chain: typing.Optional[list[platform_message.MessageChain]] = None
"""回复消息链从resp_messages包装而得"""
# ======= 内部保留 =======
current_stage: "pkg.pipeline.stagemgr.StageInstContainer" = None
class Config:
arbitrary_types_allowed = True
class Conversation(pydantic.BaseModel):
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation但只有一个当前使用的 Conversation"""
prompt: sysprompt_entities.Prompt
messages: list[llm_entities.Message]
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
use_model: entities.LLMModelInfo
use_funcs: typing.Optional[list[tools_entities.LLMFunction]]
class Session(pydantic.BaseModel):
"""会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}"""
launcher_type: LauncherTypes
launcher_id: int
sender_id: typing.Optional[int] = 0
use_prompt_name: typing.Optional[str] = 'default'
using_conversation: typing.Optional[Conversation] = None
conversations: typing.Optional[list[Conversation]] = pydantic.Field(default_factory=list)
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
semaphore: typing.Optional[asyncio.Semaphore] = None
"""当前会话的信号量,用于限制并发"""
class Config:
arbitrary_types_allowed = True

47
pkg/core/migration.py Normal file
View File

@@ -0,0 +1,47 @@
from __future__ import annotations
import abc
import typing
from . import app
preregistered_migrations: list[typing.Type[Migration]] = []
"""当前阶段暂不支持扩展"""
def migration_class(name: str, number: int):
"""注册一个迁移
"""
def decorator(cls: typing.Type[Migration]) -> typing.Type[Migration]:
cls.name = name
cls.number = number
preregistered_migrations.append(cls)
return cls
return decorator
class Migration(abc.ABC):
"""一个版本的迁移
"""
name: str
number: int
ap: app.Application
def __init__(self, ap: app.Application):
self.ap = ap
@abc.abstractmethod
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
pass
@abc.abstractmethod
async def run(self):
"""执行迁移
"""
pass

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
import os
import sys
from .. import migration
@migration.migration_class("sensitive-word-migration", 1)
class SensitiveWordMigration(migration.Migration):
"""敏感词迁移
"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
return os.path.exists("data/config/sensitive-words.json") and not os.path.exists("data/metadata/sensitive-words.json")
async def run(self):
"""执行迁移
"""
# 移动文件
os.rename("data/config/sensitive-words.json", "data/metadata/sensitive-words.json")
# 重新加载配置
await self.ap.sensitive_meta.load_config()

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("openai-config-migration", 2)
class OpenAIConfigMigration(migration.Migration):
"""OpenAI配置迁移
"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
return 'openai-config' in self.ap.provider_cfg.data
async def run(self):
"""执行迁移
"""
old_openai_config = self.ap.provider_cfg.data['openai-config'].copy()
if 'keys' not in self.ap.provider_cfg.data:
self.ap.provider_cfg.data['keys'] = {}
if 'openai' not in self.ap.provider_cfg.data['keys']:
self.ap.provider_cfg.data['keys']['openai'] = []
self.ap.provider_cfg.data['keys']['openai'] = old_openai_config['api-keys']
self.ap.provider_cfg.data['model'] = old_openai_config['chat-completions-params']['model']
del old_openai_config['chat-completions-params']['model']
if 'requester' not in self.ap.provider_cfg.data:
self.ap.provider_cfg.data['requester'] = {}
if 'openai-chat-completions' not in self.ap.provider_cfg.data['requester']:
self.ap.provider_cfg.data['requester']['openai-chat-completions'] = {}
self.ap.provider_cfg.data['requester']['openai-chat-completions'] = {
'base-url': old_openai_config['base_url'],
'args': old_openai_config['chat-completions-params'],
'timeout': old_openai_config['request-timeout'],
}
del self.ap.provider_cfg.data['openai-config']
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("anthropic-requester-config-completion", 3)
class AnthropicRequesterConfigCompletionMigration(migration.Migration):
"""OpenAI配置迁移
"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
return 'anthropic-messages' not in self.ap.provider_cfg.data['requester'] \
or 'anthropic' not in self.ap.provider_cfg.data['keys']
async def run(self):
"""执行迁移
"""
if 'anthropic-messages' not in self.ap.provider_cfg.data['requester']:
self.ap.provider_cfg.data['requester']['anthropic-messages'] = {
'base-url': 'https://api.anthropic.com',
'args': {
'max_tokens': 1024
},
'timeout': 120,
}
if 'anthropic' not in self.ap.provider_cfg.data['keys']:
self.ap.provider_cfg.data['keys']['anthropic'] = []
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("moonshot-config-completion", 4)
class MoonshotConfigCompletionMigration(migration.Migration):
"""OpenAI配置迁移
"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
return 'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester'] \
or 'moonshot' not in self.ap.provider_cfg.data['keys']
async def run(self):
"""执行迁移
"""
if 'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester']:
self.ap.provider_cfg.data['requester']['moonshot-chat-completions'] = {
'base-url': 'https://api.moonshot.cn/v1',
'args': {},
'timeout': 120,
}
if 'moonshot' not in self.ap.provider_cfg.data['keys']:
self.ap.provider_cfg.data['keys']['moonshot'] = []
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("deepseek-config-completion", 5)
class DeepseekConfigCompletionMigration(migration.Migration):
"""OpenAI配置迁移
"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
return 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester'] \
or 'deepseek' not in self.ap.provider_cfg.data['keys']
async def run(self):
"""执行迁移
"""
if 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester']:
self.ap.provider_cfg.data['requester']['deepseek-chat-completions'] = {
'base-url': 'https://api.deepseek.com',
'args': {},
'timeout': 120,
}
if 'deepseek' not in self.ap.provider_cfg.data['keys']:
self.ap.provider_cfg.data['keys']['deepseek'] = []
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,19 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("vision-config", 6)
class VisionConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return "enable-vision" not in self.ap.provider_cfg.data
async def run(self):
"""执行迁移"""
if "enable-vision" not in self.ap.provider_cfg.data:
self.ap.provider_cfg.data["enable-vision"] = False
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,20 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("qcg-center-url-config", 7)
class QCGCenterURLConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return "qcg-center-url" not in self.ap.system_cfg.data
async def run(self):
"""执行迁移"""
if "qcg-center-url" not in self.ap.system_cfg.data:
self.ap.system_cfg.data["qcg-center-url"] = "https://api.qchatgpt.rockchin.top/api/v2"
await self.ap.system_cfg.dump_config()

View File

@@ -0,0 +1,29 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("ad-fixwin-cfg-migration", 8)
class AdFixwinConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return isinstance(
self.ap.pipeline_cfg.data["rate-limit"]["fixwin"]["default"],
int
)
async def run(self):
"""执行迁移"""
for session_name in self.ap.pipeline_cfg.data["rate-limit"]["fixwin"]:
temp_dict = {
"window-size": 60,
"limit": self.ap.pipeline_cfg.data["rate-limit"]["fixwin"][session_name]
}
self.ap.pipeline_cfg.data["rate-limit"]["fixwin"][session_name] = temp_dict
await self.ap.pipeline_cfg.dump_config()

View File

@@ -0,0 +1,24 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("msg-truncator-cfg-migration", 9)
class MsgTruncatorConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'msg-truncate' not in self.ap.pipeline_cfg.data
async def run(self):
"""执行迁移"""
self.ap.pipeline_cfg.data['msg-truncate'] = {
'method': 'round',
'round': {
'max-round': 10
}
}
await self.ap.pipeline_cfg.dump_config()

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("ollama-requester-config", 10)
class MsgTruncatorConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'ollama-chat' not in self.ap.provider_cfg.data['requester']
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['requester']['ollama-chat'] = {
"base-url": "http://127.0.0.1:11434",
"args": {},
"timeout": 600
}
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("command-prefix-config", 11)
class CommandPrefixConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'command-prefix' not in self.ap.command_cfg.data
async def run(self):
"""执行迁移"""
self.ap.command_cfg.data['command-prefix'] = [
"!", ""
]
await self.ap.command_cfg.dump_config()

View File

@@ -0,0 +1,19 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("runner-config", 12)
class RunnerConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'runner' not in self.ap.provider_cfg.data
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['runner'] = 'local-agent'
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("http-api-config", 13)
class HttpApiConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'http-api' not in self.ap.system_cfg.data or "persistence" not in self.ap.system_cfg.data
async def run(self):
"""执行迁移"""
self.ap.system_cfg.data['http-api'] = {
"enable": True,
"host": "0.0.0.0",
"port": 5300,
"jwt-expire": 604800
}
self.ap.system_cfg.data['persistence'] = {
"sqlite": {
"path": "data/persistence.db"
},
"use": "sqlite"
}
await self.ap.system_cfg.dump_config()

View File

@@ -0,0 +1,22 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("force-delay-config", 14)
class ForceDelayConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return type(self.ap.platform_cfg.data['force-delay']) == list
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['force-delay'] = {
"min": self.ap.platform_cfg.data['force-delay'][0],
"max": self.ap.platform_cfg.data['force-delay'][1]
}
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("gitee-ai-config", 15)
class GiteeAIConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'gitee-ai-chat-completions' not in self.ap.provider_cfg.data['requester'] or 'gitee-ai' not in self.ap.provider_cfg.data['keys']
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['requester']['gitee-ai-chat-completions'] = {
"base-url": "https://ai.gitee.com/v1",
"args": {},
"timeout": 120
}
self.ap.provider_cfg.data['keys']['gitee-ai'] = [
"XXXXX"
]
await self.ap.provider_cfg.dump_config()

44
pkg/core/note.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
import abc
import typing
from . import app
preregistered_notes: list[typing.Type[LaunchNote]] = []
def note_class(name: str, number: int):
"""注册一个启动信息
"""
def decorator(cls: typing.Type[LaunchNote]) -> typing.Type[LaunchNote]:
cls.name = name
cls.number = number
preregistered_notes.append(cls)
return cls
return decorator
class LaunchNote(abc.ABC):
"""启动信息
"""
name: str
number: int
ap: app.Application
def __init__(self, ap: app.Application):
self.ap = ap
@abc.abstractmethod
async def need_show(self) -> bool:
"""判断当前环境是否需要显示此启动信息
"""
pass
@abc.abstractmethod
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
"""生成启动信息
"""
pass

View File

@@ -0,0 +1,20 @@
from __future__ import annotations
import typing
from .. import note, app
@note.note_class("ClassicNotes", 1)
class ClassicNotes(note.LaunchNote):
"""经典启动信息
"""
async def need_show(self) -> bool:
return True
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
yield await self.ap.ann_mgr.show_announcements()
yield await self.ap.ver_mgr.show_version_update()

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