Compare commits

..

158 Commits

Author SHA1 Message Date
Junyan Qin
5311e78776 chore: release v3.4.7.1 2025-02-12 15:16:02 +08:00
Junyan Qin
35721c1340 doc(README): update comment of aliyun bailian 2025-02-12 13:47:01 +08:00
Junyan Qin (Chin)
a76df22cab Merge pull request #1066 from lyg09270/master
阿里云百炼平台通用模型和自定义模型应用API支持
2025-02-12 13:44:19 +08:00
Junyan Qin
a90f996b24 chore: related configuration of dashscope runner 2025-02-12 13:33:07 +08:00
Junyan Qin (Chin)
c96d4456ea Merge pull request #1035 from wanjiaju3108/master
阿里云大模型服务适配
2025-02-12 11:27:05 +08:00
Junyan Qin (Chin)
d1df6d993f Merge branch 'master' into master 2025-02-12 11:26:35 +08:00
Junyan Qin
191f8866ae chore(bailian): related configuration 2025-02-12 11:25:28 +08:00
Junyan Qin
e17da4e2ee chore: remove models of MaaS from llm-models.json 2025-02-12 11:11:07 +08:00
Junyan Qin
e89c6b68c9 fix: f the stream resp 2025-02-11 21:19:15 +08:00
Civic_Crab
51cca31f04 去除qwen请求器 2025-02-11 18:52:27 +08:00
Civic_Crab
e51950aa75 修改llm-model.json,去除舍弃的qwen功能 2025-02-11 18:50:56 +08:00
Civic_Crab
4c344e0636 阿里云百炼平台应用API支持 2025-02-11 18:48:21 +08:00
Civic_Crab
90261d1f55 Merge branch 'master' of https://github.com/lyg09270/LangBot 2025-02-11 18:40:13 +08:00
Junyan Qin
fabf93f741 chore: release v3.4.7 2025-02-11 12:56:13 +08:00
Junyan Qin
ab8ef01c76 docs: update trendshift badge link 2025-02-11 12:28:11 +08:00
Junyan Qin (Chin)
e463d3a8fe Merge pull request #1057 from eltociear/add-japanese-readme
docs: add Japanese README
2025-02-11 12:19:39 +08:00
Junyan Qin
a6bc617a3b docs: add discord link 2025-02-11 12:19:12 +08:00
Ikko Eltociear Ashimine
1b1ccdd733 docs: add Japanese README
I created Japanese translated README.
2025-02-11 03:07:31 +09:00
Junyan Qin
8d00e710d5 doc(README): add official account compatibility comment 2025-02-11 00:26:26 +08:00
Junyan Qin (Chin)
de9e3bdbd5 Merge pull request #1055 from wangcham/feat/wxoa
feat: add support for wechat official account
2025-02-11 00:24:17 +08:00
Junyan Qin
b6e054a73f chore: migrations for officialaccount adapter 2025-02-11 00:23:38 +08:00
Junyan Qin (Chin)
a078b2cf12 Merge branch 'master' into feat/wxoa 2025-02-11 00:02:33 +08:00
wangcham
6f32bf9621 fix: wecom userid 2025-02-10 10:01:48 -05:00
wangcham
ac628b26d9 feat:add support for wechat official account 2025-02-10 09:16:33 -05:00
wangcham
7ba655902b fix: wecom userid couldn't pass correctly 2025-02-10 09:11:27 -05:00
wangcham
05c1fdaa9e feat: add adapter for 微信公众号 2025-02-10 06:08:59 -05:00
Junyan Qin (Chin)
d7687913a9 doc(README.md): update trendingshift badge 2025-02-10 11:04:57 +08:00
Civic_Crab
9e718a2e8a 新增dashscope依赖 2025-02-09 06:39:39 +08:00
Civic_Crab
cbec2f6d02 新增dashscope依赖 2025-02-09 06:37:55 +08:00
Civic_Crab
52eb37d13d 支持阿里云百炼的通用模型和自定义大模型应用 2025-02-09 06:32:49 +08:00
wanjiaju
8e9f43885a 阿里云百炼适配
新增阿里云请求器配置、阿里云模型配置、阿里云令牌配置

新增硅基模型配置
2025-02-08 10:30:19 +08:00
wanjiaju
9eefbcb6f2 阿里云百炼适配
新增阿里云请求器配置、阿里云模型配置、阿里云令牌配置

新增硅基模型配置
2025-02-08 10:27:19 +08:00
Junyan Qin
4d8ebc8c38 chore: release v3.4.6.2 2025-02-08 00:05:12 +08:00
Junyan Qin
21cfb6ee6f fix: some field may not exist in chatcmplchunk 2025-02-07 23:57:51 +08:00
WangCham
c72ad2b242 Merge pull request #1026 from 7emotions/patch-1
fix: remove fatal clearance to message from QQWebhook
2025-02-07 23:16:22 +08:00
Lorenzo Feng
e83b0a7825 fix: remove fatal clearance to message from QQWebhook 2025-02-07 21:19:47 +08:00
Junyan Qin (Chin)
a8f2438288 Merge pull request #1024 from RockChinQ/dependabot/npm_and_yarn/web/jsonpath-plus-10.2.0
chore(deps): bump jsonpath-plus from 10.1.0 to 10.2.0 in /web
2025-02-07 11:16:28 +08:00
dependabot[bot]
d0ceaff6ed chore(deps): bump jsonpath-plus from 10.1.0 to 10.2.0 in /web
Bumps [jsonpath-plus](https://github.com/s3u/JSONPath) from 10.1.0 to 10.2.0.
- [Release notes](https://github.com/s3u/JSONPath/releases)
- [Changelog](https://github.com/JSONPath-Plus/JSONPath/blob/main/CHANGES.md)
- [Commits](https://github.com/s3u/JSONPath/compare/v10.1.0...v10.2.0)

---
updated-dependencies:
- dependency-name: jsonpath-plus
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 02:56:43 +00:00
Junyan Qin
dbe6272bd8 chore: release v3.4.6.1 2025-02-06 14:52:01 +08:00
Junyan Qin
eceaf85807 feat: use stream req in the chatcmpl (#992) 2025-02-06 14:48:43 +08:00
Junyan Qin
d0606b79b0 chore: update issue template 2025-02-05 22:10:50 +08:00
Junyan Qin
412f290606 fix(wrapper): potential tool_calls misjudgment 2025-02-05 21:55:10 +08:00
Junyan Qin
21e1acc4f5 doc: add README_EN.md 2025-02-04 22:13:16 +08:00
Junyan Qin
326aad3c00 chore: release v3.4.6 2025-02-04 20:57:06 +08:00
Junyan Qin
493c2e9a16 chore: update readme 2025-02-04 20:50:56 +08:00
Junyan Qin (Chin)
51a87e28e2 Merge pull request #1016 from wangcham/bugfix-branch
fix: add support for qq official webhook
2025-02-04 20:49:36 +08:00
Junyan Qin
be2ff20f4b chore: migration for qqofficial 2025-02-04 20:48:47 +08:00
Junyan Qin (Chin)
19c6b2fc32 Merge branch 'master' into bugfix-branch 2025-02-04 20:39:52 +08:00
Junyan Qin (Chin)
5d249f441b Merge pull request #1015 from RockChinQ/feat/gewechat
feat: add `gewechat` adapter
2025-02-04 20:38:05 +08:00
Junyan Qin
852254eaef feat: add gewechat adapter 2025-02-04 19:37:40 +08:00
wangcham
43ea64befa fix: add support for webhook qq official 2025-02-04 06:35:51 -05:00
Junyan Qin
0f2cb58897 fix: forward msg send fail in lark and discord 2025-02-04 12:07:15 +08:00
Junyan Qin
dbece6af7f chore: release v3.4.5.2 2025-02-04 00:17:46 +08:00
Junyan Qin (Chin)
b1e68182bd Merge pull request #1013 from RockChinQ/feat/marketplace
feat: add marketplace
2025-02-04 00:17:09 +08:00
Junyan Qin
45a64bea78 feat: add marketplace 2025-02-04 00:14:45 +08:00
Junyan Qin
aec8735388 chore: release v3.4.5.1 2025-02-03 01:36:21 +08:00
Junyan Qin (Chin)
1d91faaa49 fix(platform.json): discord enabled by default 2025-02-03 01:33:29 +08:00
Junyan Qin (Chin)
e1e21c0063 Update README.md 2025-02-02 17:12:48 +08:00
Junyan Qin
e775499080 chore: release v3.4.5 2025-02-02 17:10:12 +08:00
Junyan Qin
735aad5a91 doc: README 2025-02-02 16:32:40 +08:00
Junyan Qin
fb4e106f69 doc: update README 2025-02-02 16:31:32 +08:00
Junyan Qin (Chin)
e5659db535 Merge pull request #1002 from RockChinQ/feat/discord
feat: add `discord` adapter
2025-02-02 16:30:40 +08:00
Junyan Qin
5381e09a6c chore: config for discord 2025-02-02 16:28:21 +08:00
Junyan Qin
21f16ecd68 feat: discord adapter 2025-02-02 12:18:18 +08:00
Junyan Qin (Chin)
12fc76b326 Update README.md 2025-02-02 11:11:49 +08:00
Junyan Qin (Chin)
d7f87dd269 更新 README.md 2025-02-02 00:10:41 +08:00
Junyan Qin (Chin)
56227f3713 更新 README.md 2025-02-02 00:10:04 +08:00
Junyan Qin (Chin)
f492fee486 Merge pull request #1000 from RockChinQ/feat/siliconflow
feat: siliconflow provider
2025-02-01 14:19:43 +08:00
Junyan Qin
41a7814615 feat: siliconflow provider 2025-02-01 14:19:21 +08:00
Junyan Qin
8644f2c166 chore: update 2025-02-01 13:53:20 +08:00
Junyan Qin
e4a9365caf chore: update issue template 2025-02-01 12:13:20 +08:00
Junyan Qin (Chin)
9fc7af1295 Merge pull request #999 from RockChinQ/feat/lm-studio
feat: add supports for LM Studio
2025-02-01 12:01:45 +08:00
Junyan Qin
d0eeb2b304 feat: add supports for LM Studio 2025-02-01 12:01:07 +08:00
Junyan Qin
e4518ebcf1 chore: release v3.4.4.1 2025-01-31 17:42:12 +08:00
Junyan Qin
7604cefd0f fix: dify agent type not in schema 2025-01-30 22:07:03 +08:00
Junyan Qin
71729d4784 doc(README): update qq group number 2025-01-30 11:11:24 +08:00
Junyan Qin
1d16bc4968 perf: default value for requester args 2025-01-30 00:30:01 +08:00
Junyan Qin
de2bf79004 chore: release v3.4.4 2025-01-30 00:16:33 +08:00
Junyan Qin (Chin)
83ed7a9f38 Merge pull request #991 from RockChinQ/feat/lark
feat: add adapter `lark`
2025-01-30 00:15:27 +08:00
Junyan Qin
c326e72758 fix: migration not imported 2025-01-29 23:43:32 +08:00
Junyan Qin
ac9cef82cc chore: migrations 2025-01-29 23:41:29 +08:00
Junyan Qin
ea254d57d2 feat: lark adapter 2025-01-29 23:31:40 +08:00
Junyan Qin
a661f24ae0 doc: add contributors graph 2025-01-29 16:53:09 +08:00
Junyan Qin
afabf9256b chore: add model info deepseek-reasoner 2025-01-28 15:14:23 +08:00
Junyan Qin
74a8f9c9e2 fix: deps Crypto not checked 2025-01-27 21:33:10 +08:00
Junyan Qin
1d11e448f9 doc(README): update slogan 2025-01-26 10:15:14 +08:00
Junyan Qin
e3e23cbccb chore: release v3.4.3.2 2025-01-25 17:25:06 +08:00
Junyan Qin (Chin)
79132aa11d Merge pull request #988 from wangcham/bugfix-branch
fix:修复了企业微信的accesstoken问题
2025-01-25 17:23:19 +08:00
wangcham
7bb9e6e951 fix:修复了企业微信的accesstoken问题 2025-01-25 04:17:01 -05:00
Junyan Qin
37dc5b4135 chore: release v3.4.3.1 2025-01-23 13:32:51 +08:00
Junyan Qin
d588faf470 fix(httpx): deprecated proxies param 2025-01-23 13:32:27 +08:00
Junyan Qin
8b51a81158 doc(README): update qq group badge 2025-01-22 00:11:43 +08:00
Junyan Qin
9f125974bf doc: update qq group 2025-01-22 00:07:16 +08:00
Junyan Qin
d0aed48ca9 chore: release v3.4.3 2025-01-21 10:47:53 +08:00
Junyan Qin (Chin)
bf548df6ae Merge pull request #977 from wangcham/master
feat: add supports for wecom
2025-01-21 10:40:20 +08:00
Junyan Qin (Chin)
a3fe105f8e Merge branch 'master' into master 2025-01-21 10:38:04 +08:00
wangcham
5add1d71bc feat: migration for wecom config 2025-01-20 21:34:34 -05:00
wangcham
7a01cff0c8 perf(wecom): add supports for images 2025-01-20 21:24:46 -05:00
Junyan Qin
e8602f7134 doc(README): link title image to website 2025-01-20 20:29:54 +08:00
Junyan Qin
e9aad2c8d7 doc(README): update platforms 2025-01-20 20:05:45 +08:00
wangcham
60d4f3d77c feat: add supports for wecom 2025-01-12 05:09:53 -05:00
Junyan Qin
9b8c5a3499 chore: release v3.4.2.1 2025-01-06 21:32:42 +08:00
Junyan Qin
53dde0607d Merge pull request #972 from RockChinQ/fix/dify-back-image
fix(dify): display agent image
2025-01-06 21:29:26 +08:00
Junyan Qin
7f034b4ffa fix(dify): display agent image 2025-01-06 21:28:36 +08:00
Junyan Qin
599ab83100 doc(README): perf llm comments 2025-01-06 20:33:35 +08:00
Junyan Qin
f4a3508ec2 Merge pull request #971 from RockChinQ/feat/zhipuai
feat: add supports for zhipuai(chatglm)
2025-01-06 20:29:26 +08:00
Junyan Qin
44b92909eb feat: add supports for zhipuai(chatglm) 2025-01-06 20:27:10 +08:00
Junyan Qin
8ed07b8d1a feat: add langbot scope plugin api 2025-01-06 19:49:32 +08:00
Junyan Qin
2ff9ced15e doc(README): add go-cqhttp 2025-01-06 09:53:56 +08:00
Junyan Qin
641b8d71ed doc(README): add compability comment 2025-01-06 09:51:40 +08:00
Junyan Qin
a31b450f54 chore: release v3.4.2 2025-01-04 23:07:52 +08:00
Junyan Qin
97bb24c5b9 feat: supports for provider reloading 2025-01-04 23:07:10 +08:00
Junyan Qin
5e5a3639d1 Merge pull request #958 from zhihuanwang/master
增加xAI模型支持
2025-01-04 22:25:51 +08:00
Junyan Qin
0a68a77e28 feat: refactor 2025-01-04 22:24:05 +08:00
kevin
11a0c4142e 增加xAI模型支持
推荐llm-models.json新增
```json
,
        {
            "name": "grok-2-vision-1212",
            "model_name": "grok-2-vision-1212",
            "requester": "grok-chat-completions",
            "token_mgr": "grok",
            "vision_supported": true
        }
```
provider.json requester增加
```json
,
        "grok-chat-completions": {
            "args": {},
            "base-url": "https://api.x.ai/v1",
            "timeout": 120
        }
```
keys增加:
```json
,
"grok": [
            "xai-your-key"
        ]
```
2025-01-04 22:13:47 +08:00
Junyan Qin
d214d80579 Update README.md 2025-01-04 11:11:57 +08:00
Junyan Qin
ed719fd44e ci: perf workflows 2025-01-02 11:03:40 +08:00
Junyan Qin
5dc6bed0d1 Merge pull request #969 from RockChinQ/perf/unified-persistence-dir
perf: move label file to data dir
2025-01-02 10:52:16 +08:00
Junyan Qin
b1244a4d4e perf: move label file to data dir 2025-01-02 10:51:09 +08:00
Junyan Qin
6aa325a4b1 perf: no exit after files created 2025-01-02 10:41:52 +08:00
Junyan Qin
88a11561f9 chore: remove stranger message callback method 2024-12-29 21:31:47 +08:00
Junyan Qin
fd30022065 fix: potential vulnerabilities in CI 2024-12-27 22:54:48 +08:00
Junyan Qin
9486312737 chore: release v3.4.1.5 2024-12-26 22:08:38 +08:00
Junyan Qin
e37070a985 fix(requester): unmatched params (#967) 2024-12-26 15:14:06 +08:00
Junyan Qin
ffb98ecca2 chore: release v3.4.1.4 2024-12-24 23:37:23 +08:00
Junyan Qin
29bd69ef97 fix: bad await in aiocqhttp adapter 2024-12-24 23:37:02 +08:00
Junyan Qin
e46c9530cc 更新 README.md 2024-12-24 20:48:18 +08:00
Junyan Qin
7ddd303e2d Update README.md 2024-12-24 20:46:40 +08:00
Junyan Qin
66798a1d0f Update README.md 2024-12-24 20:41:21 +08:00
Junyan Qin
bd05afdf14 Update README.md 2024-12-24 20:23:31 +08:00
Junyan Qin
136e48f7ee Update README.md 2024-12-24 20:19:41 +08:00
Junyan Qin
facb5f177a Update README.md 2024-12-24 20:16:20 +08:00
Junyan Qin
10ce31cc46 chore: release v3.4.1.3 2024-12-24 19:26:39 +08:00
Junyan Qin
3b4f3c516b Update README.md 2024-12-24 19:08:35 +08:00
Junyan Qin
a1e3981ce4 chore: 更新issue模板 2024-12-24 15:51:19 +08:00
Junyan Qin
89f26781fe chore: 更新issue模板 2024-12-24 15:50:41 +08:00
Junyan Qin
914292a80b chore: 修改issue模板 2024-12-24 15:47:59 +08:00
Junyan Qin
8227e3299b Merge pull request #963 from RockChinQ/feat/dl-image-by-adapters
fix: 下载 QQ 图片时的400问题
2024-12-24 11:28:31 +08:00
Junyan Qin
07ca48d652 fix: 无法传递qq图片的问题 2024-12-24 11:26:33 +08:00
Junyan Qin
243f45c7db fix: 使用 header 避免400 2024-12-24 11:09:45 +08:00
Junyan Qin
12cfce3622 feat: 重构图片消息传递逻辑 (#957, #955) 2024-12-24 10:57:17 +08:00
Junyan Qin
535c4a8a11 fix: anthropic sdk删除proxies导致无法启动 (#962, #960) 2024-12-23 21:35:16 +08:00
Junyan Qin
6606c671b2 doc: README添加demo 2024-12-23 10:43:52 +08:00
Junyan Qin
242f24840d fix: 为dify agent设置项更新schema 2024-12-17 16:24:00 +08:00
Junyan Qin
486f636b2d doc(README): 添加 railway 部署按钮 2024-12-17 14:49:41 +08:00
Junyan Qin
b293d7a7cd doc: README 2024-12-17 01:30:39 +08:00
Junyan Qin
f4fa0b42a6 chore: release v3.4.1.2 2024-12-17 01:22:31 +08:00
Junyan Qin
209e89712d Merge pull request #953 from RockChinQ/perf/dify-sv-api
perf: 完善 dify api runner
2024-12-17 01:21:01 +08:00
Junyan Qin
3314a7a9e9 fix: 在设置model为非视觉模型时,非local-agent的runner无法获得图片消息 (#948) 2024-12-17 01:17:57 +08:00
Junyan Qin
793d64303e perf: 完善dify api runner 2024-12-17 01:04:08 +08:00
Junyan Qin
6642498f00 feat: 添加对 agent 应用的支持 (#951) 2024-12-17 00:41:28 +08:00
Junyan Qin
32b400dcb1 fix: dify的timeout无法自定义 (#949) 2024-12-16 23:54:56 +08:00
Junyan Qin
0dcd2d8179 doc: 添加 zeabur 部署方式 2024-12-16 20:02:04 +08:00
Junyan Qin
736f8b613c feat: 为 ollama 支持视觉和函数调用 (#950) 2024-12-15 17:05:56 +08:00
Junyan Qin
9e7d9a937d chore: release v3.4.1.1 2024-12-15 12:18:41 +08:00
Junyan Qin
4767983279 deps: 限制 taskgroup==0.0.0a4 2024-12-15 11:54:40 +08:00
Junyan Qin
e37f35d95a doc: 修改使用文档站的social.png 2024-12-14 19:31:31 +08:00
Junyan Qin
ad1e609fb9 doc: 优化 README (#947)
* doc: update readme

* doc: update readme

* doc: replace banner

* doc: update social

* Update README.md

* perf: 优化 features

* Update README.md

* doc: update

* Update README.md
2024-12-14 19:28:29 +08:00
109 changed files with 6102 additions and 451 deletions

View File

@@ -6,26 +6,24 @@ body:
- type: dropdown - type: dropdown
attributes: attributes:
label: 消息平台适配器 label: 消息平台适配器
description: "连接QQ使用的框架" description: "接入的消息平台类型"
options: options:
- 其他(或暂未使用)
- Nakurugo-cqhttp - Nakurugo-cqhttp
- aiocqhttp使用 OneBot 协议接入的) - aiocqhttp使用 OneBot 协议接入的)
- qq-botpyQQ官方API - qq-botpyQQ官方API WebSocket
- 其他 - qqofficialQQ官方API Webhook
validations: - lark飞书
required: false - wecom企业微信
- type: input - gewechat个人微信
attributes: - discord
label: 运行环境
description: 操作系统、系统架构、**Python版本**、**主机地理位置**
placeholder: 例如: CentOS x64 Python 3.10.3、Docker 的直接写 Docker 就行
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: LangBot 版本 label: 运行环境
description: LangBot (QChatGPT) 版本号 description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
placeholder: 例如v3.3.0,可以使用`!version`命令查看,或者到 pkg/utils/constants.py 查看 placeholder: 例如v3.3.0、CentOS x64 Python 3.10.3、Docker 的系统直接写 Docker 就行
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -34,6 +32,12 @@ body:
description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。** description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。**
validations: validations:
required: true required: true
- type: textarea
attributes:
label: 复现步骤
description: 如何重现这个问题,越详细越好;请贴上所有相关的配置文件和元数据文件(注意隐去敏感信息)
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: 启用的插件 label: 启用的插件

View File

@@ -7,9 +7,14 @@ on:
jobs: jobs:
build-dev-image: build-dev-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# 如果是tag则跳过
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
persist-credentials: false
- name: Generate Tag - name: Generate Tag
id: generate_tag id: generate_tag
run: | run: |

View File

@@ -13,6 +13,9 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
persist-credentials: false
- name: judge has env GITHUB_REF # 如果没有GITHUB_REF环境变量则把github.ref变量赋值给GITHUB_REF - name: judge has env GITHUB_REF # 如果没有GITHUB_REF环境变量则把github.ref变量赋值给GITHUB_REF
run: | run: |
if [ -z "$GITHUB_REF" ]; then if [ -z "$GITHUB_REF" ]; then

View File

@@ -12,6 +12,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
persist-credentials: false
- name: Check version - name: Check version
id: check_version id: check_version
@@ -50,3 +52,11 @@ jobs:
with: with:
name: langbot-${{ steps.check_version.outputs.version }}-all name: langbot-${{ steps.check_version.outputs.version }}-all
path: . path: .
- name: Upload To Release
env:
GH_TOKEN: ${{ secrets.RELEASE_UPLOAD_GITHUB_TOKEN }}
run: |
# 本目录下所有文件打包成zip
zip -r langbot-${{ steps.check_version.outputs.version }}-all.zip .
gh release upload ${{ github.event.release.tag_name }} langbot-${{ steps.check_version.outputs.version }}-all.zip

View File

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

11
.gitignore vendored
View File

@@ -2,7 +2,6 @@
.idea/ .idea/
__pycache__/ __pycache__/
database.db database.db
qchatgpt.log
langbot.log langbot.log
/banlist.py /banlist.py
/plugins/ /plugins/
@@ -17,8 +16,7 @@ scenario/
!scenario/default-template.json !scenario/default-template.json
override.json override.json
cookies.json cookies.json
res/announcement_saved data/labels/announcement_saved.json
res/announcement_saved.json
cmdpriv.json cmdpriv.json
tips.py tips.py
.venv .venv
@@ -32,8 +30,11 @@ claude.json
bard.json bard.json
/*yaml /*yaml
!/docker-compose.yaml !/docker-compose.yaml
res/instance_id.json data/labels/instance_id.json
.DS_Store .DS_Store
/data /data
botpy.log* botpy.log*
/poc /poc
/libs/wecom_api/test.py
/venv
/jp-tyo-churros-05.rockchin.top

133
README.md
View File

@@ -1,26 +1,12 @@
<p align="center"> <p align="center">
<img src="https://docs.langbot.app/langbot-logo-0.5x.png" alt="QChatGPT" width="180" /> <a href="https://langbot.app">
</p> <img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a>
<div align="center"> <div align="center">
# LangBot <a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<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/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="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://docs.langbot.app">项目主页</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/intro.htmll">功能介绍</a>
@@ -29,12 +15,109 @@
<a href="https://docs.langbot.app/plugin/plugin-intro.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> <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>
## 相关链接 <div align="center">
😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖
</div>
<a href="https://github.com/RockChinQ/qcg-installer">安装器源码</a> <br/>
<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> [![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-1030838208-blue)](https://qm.qq.com/q/PF9OuQCCcM)
[![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)
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
</div>
</p>
## ✨ 特性
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、飞书、Discord、个人微信后续还将支持 WhatsApp、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
## 📦 开始使用
> [!IMPORTANT]
>
> 在您开始任何方式部署之前,请务必阅读[新手指引](https://docs.langbot.app/insight/guide.html)。
#### Docker Compose 部署
适合熟悉 Docker 的用户,查看文档[Docker 部署](https://docs.langbot.app/deploy/langbot/docker.html)。
#### 宝塔面板部署
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/deploy/langbot/one-click/bt.html)使用。
#### Zeabur 云部署
社区贡献的 Zeabur 模板。
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railway 云部署
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### 手动部署
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/deploy/langbot/manual.html)。
## 📸 效果展示
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/> <img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
</div>
- WebUI Demo: https://demo.langbot.dev/
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
- 注意仅展示webui效果公开环境请不要在其中填入您的任何敏感信息。
## 🔌 组件兼容性
### 消息平台
| 平台 | 状态 | 备注 |
| --- | --- | --- |
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | |
| 微信公众号 | ✅ | |
| 飞书 | ✅ | |
| Discord | ✅ | |
| 个人微信 | ✅ | 使用 [Gewechat](https://github.com/Devo919/Gewechat) 接入 |
| Telegram | 🚧 | |
| WhatsApp | 🚧 | |
| 钉钉 | 🚧 | |
🚧: 正在开发中
### 大模型
| 模型 | 状态 | 备注 |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
## 😘 社区贡献
LangBot 离不开以下贡献者和社区内所有人的贡献,我们欢迎任何形式的贡献和反馈。
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>

124
README_EN.md Normal file
View File

@@ -0,0 +1,124 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://docs.langbot.app">Home</a>
<a href="https://docs.langbot.app/insight/intro.htmll">Features</a>
<a href="https://docs.langbot.app/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/usage/faq.html">FAQ</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">Plugin</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">Submit Plugin</a>
<div align="center">
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![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=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
</div>
</p>
## ✨ Features
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Deeply integrates with [Dify](https://dify.ai). Currently supports QQ, QQ Channel, WeCom, Lark, Discord, personal WeChat, and will support WhatsApp, Telegram, etc. in the future.
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods.
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Rich ecology, currently has dozens of [plugins](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web UI: Support management LangBot instance through the browser, for details, see [documentation](https://docs.langbot.app/webui/intro.html)
## 📦 Getting Started
> [!IMPORTANT]
>
> - Before you start deploying in any way, please read the [New User Guide](https://docs.langbot.app/insight/guide.html).
> - All documentation is in Chinese, we will provide i18n version in the near future.
#### Docker Compose Deployment
Suitable for users familiar with Docker, see the [Docker Deployment](https://docs.langbot.app/deploy/langbot/docker.html) documentation.
#### One-click Deployment on BTPanel
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/deploy/langbot/one-click/bt.html) to use it.
#### Zeabur Cloud Deployment
Community contributed Zeabur template.
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railway Cloud Deployment
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### Other Deployment Methods
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/deploy/langbot/manual.html) documentation.
## 📸 Demo
<img alt="Reply Effect (with Internet Plugin)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUI Demo: https://demo.langbot.dev/
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
- Note: Only the WebUI effect is shown, please do not fill in any sensitive information in the public environment.
## 🔌 Component Compatibility
### Message Platform
| Platform | Status | Remarks |
| --- | --- | --- |
| Personal QQ | ✅ | |
| QQ Official API | ✅ | |
| WeCom | ✅ | |
| WeChat Official Account | ✅ | |
| Lark | ✅ | |
| Discord | ✅ | |
| Personal WeChat | ✅ | Use [Gewechat](https://github.com/Devo919/Gewechat) to access |
| Telegram | 🚧 | |
| WhatsApp | 🚧 | |
| DingTalk | 🚧 | |
🚧: In development
### LLMs
| LLM | Status | Remarks |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
## 🤝 Community Contribution
Thanks to the following contributors and everyone in the community for their contributions.
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>

122
README_JP.md Normal file
View File

@@ -0,0 +1,122 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></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">FAQ</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>
<div align="center">
😎高い安定性、🧩拡張サポート、🦄マルチモーダル - LLMネイティブインスタントメッセージングボットプラットフォーム🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![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=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
</div>
</p>
## ✨ 機能
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQチャンネル、WeCom、Lark、Discord、個人WeChatをサポートし、将来的にはWhatsApp、Telegramなどもサポート予定。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。豊富なエコシステム、現在数十の[プラグイン](https://docs.langbot.app/plugin/plugin-intro.html)が存在。
- 😻 [新機能] Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。詳細は[ドキュメント](https://docs.langbot.app/webui/intro.html)を参照。
## 📦 始め方
> [!IMPORTANT]
>
> - どのデプロイ方法を始める前に、必ず[新規ユーザーガイド](https://docs.langbot.app/insight/guide.html)をお読みください。
> - すべてのドキュメントは中国語で提供されています。近い将来、i18nバージョンを提供する予定です。
#### Docker Compose デプロイ
Dockerに慣れているユーザーに適しています。[Dockerデプロイ](https://docs.langbot.app/deploy/langbot/docker.html)のドキュメントを参照してください。
#### BTPanelでのワンクリックデプロイ
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/deploy/langbot/one-click/bt.html)を使用して使用できます。
#### Zeaburクラウドデプロイ
コミュニティが提供するZeaburテンプレート。
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railwayクラウドデプロイ
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### その他のデプロイ方法
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/deploy/langbot/manual.html)のドキュメントを参照してください。
## 📸 デモ
<img alt="返信効果(インターネットプラグイン付き)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUIデモ: https://demo.langbot.dev/
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
- 注意: WebUIの効果のみを示しています。公開環境では、機密情報を入力しないでください。
## 🔌 コンポーネントの互換性
### メッセージプラットフォーム
| プラットフォーム | ステータス | 備考 |
| --- | --- | --- |
| 個人QQ | ✅ | |
| QQ公式API | ✅ | |
| WeCom | ✅ | |
| Lark | ✅ | |
| Discord | ✅ | |
| 個人WeChat | ✅ | [Gewechat](https://github.com/Devo919/Gewechat)を使用して接続 |
| Telegram | 🚧 | |
| WhatsApp | 🚧 | |
| DingTalk | 🚧 | |
🚧: 開発中
### LLMs
| LLM | ステータス | 備考 |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
## 🤝 コミュニティ貢献
以下の貢献者とコミュニティの皆さんの貢献に感謝します。
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>

View File

@@ -10,8 +10,8 @@ class TestDifyClient:
async def test_chat_messages(self): async def test_chat_messages(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL")) cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL"))
resp = await cln.chat_messages(inputs={}, query="Who are you?", user="test") async for chunk in cln.chat_messages(inputs={}, query="调用工具查看现在几点?", user="test"):
print(json.dumps(resp, ensure_ascii=False, indent=4)) print(json.dumps(chunk, ensure_ascii=False, indent=4))
async def test_upload_file(self): async def test_upload_file(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL")) cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL"))
@@ -41,4 +41,4 @@ class TestDifyClient:
print(json.dumps(chunks, ensure_ascii=False, indent=4)) print(json.dumps(chunks, ensure_ascii=False, indent=4))
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(TestDifyClient().test_workflow_run()) asyncio.run(TestDifyClient().test_chat_messages())

View File

@@ -26,21 +26,22 @@ class AsyncDifyServiceClient:
inputs: dict[str, typing.Any], inputs: dict[str, typing.Any],
query: str, query: str,
user: str, user: str,
response_mode: str = "blocking", # 当前不支持 streaming response_mode: str = "streaming", # 当前不支持 blocking
conversation_id: str = "", conversation_id: str = "",
files: list[dict[str, typing.Any]] = [], files: list[dict[str, typing.Any]] = [],
timeout: float = 30.0, timeout: float = 30.0,
) -> dict[str, typing.Any]: ) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
"""发送消息""" """发送消息"""
if response_mode != "blocking": if response_mode != "streaming":
raise DifyAPIError("当前仅支持 blocking 模式") raise DifyAPIError("当前仅支持 streaming 模式")
async with httpx.AsyncClient( async with httpx.AsyncClient(
base_url=self.base_url, base_url=self.base_url,
trust_env=True, trust_env=True,
timeout=timeout, timeout=timeout,
) as client: ) as client:
response = await client.post( async with client.stream(
"POST",
"/chat-messages", "/chat-messages",
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}, headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json={ json={
@@ -51,12 +52,14 @@ class AsyncDifyServiceClient:
"conversation_id": conversation_id, "conversation_id": conversation_id,
"files": files, "files": files,
}, },
) ) as r:
async for chunk in r.aiter_lines():
if response.status_code != 200: if r.status_code != 200:
raise DifyAPIError(f"{response.status_code} {response.text}") raise DifyAPIError(f"{r.status_code} {chunk}")
if chunk.strip() == "":
return response.json() continue
if chunk.startswith("data:"):
yield json.loads(chunk[5:])
async def workflow_run( async def workflow_run(
self, self,
@@ -88,6 +91,8 @@ class AsyncDifyServiceClient:
}, },
) as r: ) as r:
async for chunk in r.aiter_lines(): async for chunk in r.aiter_lines():
if r.status_code != 200:
raise DifyAPIError(f"{r.status_code} {chunk}")
if chunk.strip() == "": if chunk.strip() == "":
continue continue
if chunk.startswith("data:"): if chunk.startswith("data:"):
@@ -100,10 +105,6 @@ class AsyncDifyServiceClient:
timeout: float = 30.0, timeout: float = 30.0,
) -> str: ) -> str:
"""上传文件""" """上传文件"""
# curl -X POST 'http://dify.rockchin.top/v1/files/upload' \
# --header 'Authorization: Bearer {api_key}' \
# --form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \
# --form 'user=abc-123'
async with httpx.AsyncClient( async with httpx.AsyncClient(
base_url=self.base_url, base_url=self.base_url,
trust_env=True, trust_env=True,

View File

View File

@@ -0,0 +1,176 @@
# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件
import time
import traceback
from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
import xml.etree.ElementTree as ET
from quart import Quart,request
import hashlib
from typing import Callable, Dict, Any
from .oaevent import OAEvent
import httpx
import asyncio
import time
import xml.etree.ElementTree as ET
xml_template = """
<xml>
<ToUserName><![CDATA[{to_user}]]></ToUserName>
<FromUserName><![CDATA[{from_user}]]></FromUserName>
<CreateTime>{create_time}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{content}]]></Content>
</xml>
"""
class OAClient():
def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str):
self.token = token
self.aes = EncodingAESKey
self.appid = AppID
self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com'
self.access_token = ''
self.app = Quart(__name__)
self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'])
self._message_handlers = {
"example":[],
}
self.access_token_expiry_time = None
self.msg_id_map = {}
async def handle_callback_request(self):
try:
# 每隔100毫秒查询是否生成ai回答
start_time = time.time()
signature = request.args.get("signature", "")
timestamp = request.args.get("timestamp", "")
nonce = request.args.get("nonce", "")
echostr = request.args.get("echostr", "")
msg_signature = request.args.get("msg_signature","")
if msg_signature is None:
raise Exception("msg_signature不在请求体中")
if request.method == 'GET':
# 校验签名
check_str = "".join(sorted([self.token, timestamp, nonce]))
check_signature = hashlib.sha1(check_str.encode("utf-8")).hexdigest()
if check_signature == signature:
return echostr # 验证成功返回echostr
else:
raise Exception("拒绝请求")
elif request.method == "POST":
encryt_msg = await request.data
wxcpt = WXBizMsgCrypt(self.token,self.aes,self.appid)
ret,xml_msg = wxcpt.DecryptMsg(encryt_msg,msg_signature,timestamp,nonce)
xml_msg = xml_msg.decode('utf-8')
if ret != 0:
raise Exception("消息解密失败")
message_data = await self.get_message(xml_msg)
if message_data :
event = OAEvent.from_payload(message_data)
if event:
await self._handle_message(event)
root = ET.fromstring(xml_msg)
from_user = root.find("FromUserName").text # 发送者
to_user = root.find("ToUserName").text # 机器人
from pkg.platform.sources import officialaccount
timeout = 4.80
interval = 0.1
while True:
content = officialaccount.generated_content.pop(message_data["MsgId"], None)
if content:
response_xml = xml_template.format(
to_user=from_user,
from_user=to_user,
create_time=int(time.time()),
content = content
)
return response_xml
if time.time() - start_time >= timeout:
break
await asyncio.sleep(interval)
if self.msg_id_map.get(message_data["MsgId"], 1) == 3:
# response_xml = xml_template.format(
# to_user=from_user,
# from_user=to_user,
# create_time=int(time.time()),
# content = "请求失效暂不支持公众号超过15秒的请求如有需求请联系 LangBot 团队。"
# )
print("请求失效暂不支持公众号超过15秒的请求如有需求请联系 LangBot 团队。")
return ''
except Exception as e:
traceback.print_exc()
async def get_message(self, xml_msg: str):
root = ET.fromstring(xml_msg)
message_data = {
"ToUserName": root.find("ToUserName").text,
"FromUserName": root.find("FromUserName").text,
"CreateTime": int(root.find("CreateTime").text),
"MsgType": root.find("MsgType").text,
"Content": root.find("Content").text if root.find("Content") is not None else None,
"MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None,
}
return message_data
async def run_task(self, host: str, port: int, *args, **kwargs):
"""
启动 Quart 应用。
"""
await self.app.run_task(host=host, port=port, *args, **kwargs)
def on_message(self, msg_type: str):
"""
注册消息类型处理器。
"""
def decorator(func: Callable[[OAEvent], None]):
if msg_type not in self._message_handlers:
self._message_handlers[msg_type] = []
self._message_handlers[msg_type].append(func)
return func
return decorator
async def _handle_message(self, event: OAEvent):
"""
处理消息事件。
"""
message_id = event.message_id
if message_id in self.msg_id_map.keys():
self.msg_id_map[message_id] += 1
return
self.msg_id_map[message_id] = 1
msg_type = event.type
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
await handler(event)

View File

@@ -0,0 +1,167 @@
from typing import Dict, Any, Optional
class OAEvent(dict):
"""
封装从微信公众号收到的事件数据对象(字典),提供属性以获取其中的字段。
除 `type` 和 `detail_type` 属性对于任何事件都有效外,其它属性是否存在(若不存在则返回 `None`)依事件类型不同而不同。
"""
@staticmethod
def from_payload(payload: Dict[str, Any]) -> Optional["OAEvent"]:
"""
从微信公众号事件数据构造 `WecomEvent` 对象。
Args:
payload (Dict[str, Any]): 解密后的微信事件数据。
Returns:
Optional[OAEvent]: 如果事件数据合法,则返回 OAEvent 对象;否则返回 None。
"""
try:
event = OAEvent(payload)
_ = event.type, event.detail_type # 确保必须字段存在
return event
except KeyError:
return None
@property
def type(self) -> str:
"""
事件类型,例如 "message""event""text" 等。
Returns:
str: 事件类型。
"""
return self.get("MsgType", "")
@property
def picurl(self) -> str:
"""
图片链接
"""
return self.get("PicUrl","")
@property
def detail_type(self) -> str:
"""
事件详细类型,依 `type` 的不同而不同。例如:
- 消息事件: "text", "image", "voice", 等
- 事件通知: "subscribe", "unsubscribe", "click", 等
Returns:
str: 事件详细类型。
"""
if self.type == "event":
return self.get("Event", "")
return self.type
@property
def name(self) -> str:
"""
事件名,对于消息事件是 `type.detail_type`,对于其他事件是 `event_type`。
Returns:
str: 事件名。
"""
return f"{self.type}.{self.detail_type}"
@property
def user_id(self) -> Optional[str]:
"""
发送方账号
"""
return self.get("FromUserName")
@property
def receiver_id(self) -> Optional[str]:
"""
接收者 ID例如机器人自身的公众号微信 ID。
Returns:
Optional[str]: 接收者 ID。
"""
return self.get("ToUserName")
@property
def message_id(self) -> Optional[str]:
"""
消息 ID仅在消息类型事件中存在。
Returns:
Optional[str]: 消息 ID。
"""
return self.get("MsgId")
@property
def message(self) -> Optional[str]:
"""
消息内容,仅在消息类型事件中存在。
Returns:
Optional[str]: 消息内容。
"""
return self.get("Content")
@property
def media_id(self) -> Optional[str]:
"""
媒体文件 ID仅在图片、语音等消息类型中存在。
Returns:
Optional[str]: 媒体文件 ID。
"""
return self.get("MediaId")
@property
def timestamp(self) -> Optional[int]:
"""
事件发生的时间戳。
Returns:
Optional[int]: 时间戳。
"""
return self.get("CreateTime")
@property
def event_key(self) -> Optional[str]:
"""
事件的 Key 值,例如点击菜单时的 `EventKey`。
Returns:
Optional[str]: 事件 Key。
"""
return self.get("EventKey")
def __getattr__(self, key: str) -> Optional[Any]:
"""
允许通过属性访问数据中的任意字段。
Args:
key (str): 字段名。
Returns:
Optional[Any]: 字段值。
"""
return self.get(key)
def __setattr__(self, key: str, value: Any) -> None:
"""
允许通过属性设置数据中的任意字段。
Args:
key (str): 字段名。
value (Any): 字段值。
"""
self[key] = value
def __repr__(self) -> str:
"""
生成事件对象的字符串表示。
Returns:
str: 字符串表示。
"""
return f"<WecomEvent {super().__repr__()}>"

View File

274
libs/qq_official_api/api.py Normal file
View File

@@ -0,0 +1,274 @@
import time
from quart import request
import base64
import binascii
import httpx
from quart import Quart
import xml.etree.ElementTree as ET
from typing import Callable, Dict, Any
from pkg.platform.types import events as platform_events, message as platform_message
import aiofiles
from .qqofficialevent import QQOfficialEvent
import json
import hmac
import base64
import hashlib
import traceback
from cryptography.hazmat.primitives.asymmetric import ed25519
from .qqofficialevent import QQOfficialEvent
def handle_validation(body: dict, bot_secret: str):
# bot正确的secert是32位的此处仅为了适配演示demo
while len(bot_secret) < 32:
bot_secret = bot_secret * 2
bot_secret = bot_secret[:32]
# 实际使用场景中以上三行内容可清除
seed_bytes = bot_secret.encode()
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed_bytes)
msg = body['d']['event_ts'] + body['d']['plain_token']
msg_bytes = msg.encode()
signature = signing_key.sign(msg_bytes)
signature_hex = signature.hex()
response = {
"plain_token": body['d']['plain_token'],
"signature": signature_hex
}
return response
class QQOfficialClient:
def __init__(self, secret: str, token: str, app_id: str):
self.app = Quart(__name__)
self.app.add_url_rule(
"/callback/command",
"handle_callback",
self.handle_callback_request,
methods=["GET", "POST"],
)
self.secret = secret
self.token = token
self.app_id = app_id
self._message_handlers = {
}
self.base_url = "https://api.sgroup.qq.com"
self.access_token = ""
self.access_token_expiry_time = None
async def check_access_token(self):
"""检查access_token是否存在"""
if not self.access_token or await self.is_token_expired():
return False
return bool(self.access_token and self.access_token.strip())
async def get_access_token(self):
"""获取access_token"""
url = "https://bots.qq.com/app/getAppAccessToken"
async with httpx.AsyncClient() as client:
params = {
"appId":self.app_id,
"clientSecret":self.secret,
}
headers = {
"content-type":"application/json",
}
try:
response = await client.post(url,json=params,headers=headers)
if response.status_code == 200:
response_data = response.json()
access_token = response_data.get("access_token")
expires_in = int(response_data.get("expires_in",7200))
self.access_token_expiry_time = time.time() + expires_in - 60
if access_token:
self.access_token = access_token
except Exception as e:
raise Exception(f"获取access_token失败: {e}")
async def handle_callback_request(self):
"""处理回调请求"""
try:
# 读取请求数据
body = await request.get_data()
payload = json.loads(body)
# 验证是否为回调验证请求
if payload.get("op") == 13:
# 生成签名
response = handle_validation(payload, self.secret)
return response
if payload.get("op") == 0:
message_data = await self.get_message(payload)
if message_data:
event = QQOfficialEvent.from_payload(message_data)
await self._handle_message(event)
return {"code": 0, "message": "success"}
except Exception as e:
traceback.print_exc()
return {"error": str(e)}, 400
async def run_task(self, host: str, port: int, *args, **kwargs):
"""启动 Quart 应用"""
await self.app.run_task(host=host, port=port, *args, **kwargs)
def on_message(self, msg_type: str):
"""注册消息类型处理器"""
def decorator(func: Callable[[platform_events.Event], None]):
if msg_type not in self._message_handlers:
self._message_handlers[msg_type] = []
self._message_handlers[msg_type].append(func)
return func
return decorator
async def _handle_message(self, event:QQOfficialEvent):
"""处理消息事件"""
msg_type = event.t
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
await handler(event)
async def get_message(self,msg:dict) -> Dict[str,Any]:
"""获取消息"""
message_data = {
"t": msg.get("t",{}),
"user_openid": msg.get("d",{}).get("author",{}).get("user_openid",{}),
"timestamp": msg.get("d",{}).get("timestamp",{}),
"d_author_id": msg.get("d",{}).get("author",{}).get("id",{}),
"content": msg.get("d",{}).get("content",{}),
"d_id": msg.get("d",{}).get("id",{}),
"id": msg.get("id",{}),
"channel_id": msg.get("d",{}).get("channel_id",{}),
"username": msg.get("d",{}).get("author",{}).get("username",{}),
"guild_id": msg.get("d",{}).get("guild_id",{}),
"member_openid": msg.get("d",{}).get("author",{}).get("openid",{}),
"group_openid": msg.get("d",{}).get("group_openid",{})
}
attachments = msg.get("d", {}).get("attachments", [])
image_attachments = [attachment['url'] for attachment in attachments if await self.is_image(attachment)]
image_attachments_type = [attachment['content_type'] for attachment in attachments if await self.is_image(attachment)]
if image_attachments:
message_data["image_attachments"] = image_attachments[0]
message_data["content_type"] = image_attachments_type[0]
else:
message_data["image_attachments"] = None
return message_data
async def is_image(self,attachment:dict) -> bool:
"""判断是否为图片附件"""
content_type = attachment.get("content_type","")
return content_type.startswith("image/")
async def send_private_text_msg(self,user_openid:str,content:str,msg_id:str):
"""发送私聊消息"""
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + "/v2/users/" + user_openid + "/messages"
async with httpx.AsyncClient() as client:
headers = {
"Authorization": f"QQBot {self.access_token}",
"Content-Type": "application/json",
}
data = {
"content": content,
"msg_type": 0,
"msg_id": msg_id,
}
response = await client.post(url,headers=headers,json=data)
if response.status_code == 200:
return
else:
raise ValueError(response)
async def send_group_text_msg(self,group_openid:str,content:str,msg_id:str):
"""发送群聊消息"""
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + "/v2/groups/" + group_openid + "/messages"
async with httpx.AsyncClient() as client:
headers = {
"Authorization": f"QQBot {self.access_token}",
"Content-Type": "application/json",
}
data = {
"content": content,
"msg_type": 0,
"msg_id": msg_id,
}
response = await client.post(url,headers=headers,json=data)
if response.status_code == 200:
return
else:
raise Exception(response.read().decode())
async def send_channle_group_text_msg(self,channel_id:str,content:str,msg_id:str):
"""发送频道群聊消息"""
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + "/channels/" + channel_id + "/messages"
async with httpx.AsyncClient() as client:
headers = {
"Authorization": f"QQBot {self.access_token}",
"Content-Type": "application/json",
}
params = {
"content": content,
"msg_type": 0,
"msg_id": msg_id,
}
response = await client.post(url,headers=headers,json=params)
if response.status_code == 200:
return True
else:
raise Exception(response)
async def send_channle_private_text_msg(self,guild_id:str,content:str,msg_id:str):
"""发送频道私聊消息"""
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + "/dms/" + guild_id + "/messages"
async with httpx.AsyncClient() as client:
headers = {
"Authorization": f"QQBot {self.access_token}",
"Content-Type": "application/json",
}
params = {
"content": content,
"msg_type": 0,
"msg_id": msg_id,
}
response = await client.post(url,headers=headers,json=params)
if response.status_code == 200:
return True
else:
raise Exception(response)
async def is_token_expired(self):
"""检查token是否过期"""
if self.access_token_expiry_time is None:
return True
return time.time() > self.access_token_expiry_time

View File

@@ -0,0 +1,114 @@
from typing import Dict, Any, Optional
class QQOfficialEvent(dict):
@staticmethod
def from_payload(payload: Dict[str, Any]) -> Optional["QQOfficialEvent"]:
try:
event = QQOfficialEvent(payload)
return event
except KeyError:
return None
@property
def t(self) -> str:
"""
事件类型
"""
return self.get("t", "")
@property
def user_openid(self) -> str:
"""
用户openid
"""
return self.get("user_openid",{})
@property
def timestamp(self) -> str:
"""
时间戳
"""
return self.get("timestamp",{})
@property
def d_author_id(self) -> str:
"""
作者id
"""
return self.get("id",{})
@property
def content(self) -> str:
"""
内容
"""
return self.get("content",'')
@property
def d_id(self) -> str:
"""
d_id
"""
return self.get("d_id",{})
@property
def id(self) -> str:
"""
消息idmsg_id
"""
return self.get("id",{})
@property
def channel_id(self) -> str:
"""
频道id
"""
return self.get("channel_id",{})
@property
def username(self) -> str:
"""
用户名
"""
return self.get("username",{})
@property
def guild_id(self) -> str:
"""
频道id
"""
return self.get("guild_id",{})
@property
def member_openid(self) -> str:
"""
成员openid
"""
return self.get("openid",{})
@property
def attachments(self) -> str:
"""
附件url
"""
url = self.get("image_attachments", "")
if url and not url.startswith("https://"):
url = "https://" + url
return url
@property
def group_openid(self) -> str:
"""
群组id
"""
return self.get("group_openid",{})
@property
def content_type(self) -> str:
"""
文件类型
"""
return self.get("content_type","")

View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python
# -*- encoding:utf-8 -*-
""" 对企业微信发送给企业后台的消息加解密示例代码.
@copyright: Copyright (c) 1998-2014 Tencent Inc.
"""
# ------------------------------------------------------------------------
import logging
import base64
import random
import hashlib
import time
import struct
from Crypto.Cipher import AES
import xml.etree.cElementTree as ET
import socket
from . import ierror
"""
Crypto.Cipher包已不再维护开发者可以通过以下命令下载安装最新版的加解密工具包
pip install pycryptodome
"""
class FormatException(Exception):
pass
def throw_exception(message, exception_class=FormatException):
"""my define raise exception function"""
raise exception_class(message)
class SHA1:
"""计算企业微信的消息签名接口"""
def getSHA1(self, token, timestamp, nonce, encrypt):
"""用SHA1算法生成安全签名
@param token: 票据
@param timestamp: 时间戳
@param encrypt: 密文
@param nonce: 随机字符串
@return: 安全签名
"""
try:
sortlist = [token, timestamp, nonce, encrypt]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist).encode())
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
class XMLParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# xml消息模板
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""
def extract(self, xmltext):
"""提取出xml数据包中的加密消息
@param xmltext: 待提取的xml字符串
@return: 提取出的加密消息字符串
"""
try:
xml_tree = ET.fromstring(xmltext)
encrypt = xml_tree.find("Encrypt")
return ierror.WXBizMsgCrypt_OK, encrypt.text
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_ParseXml_Error, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成xml消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的xml字符串
"""
resp_dict = {
'msg_encrypt': encrypt,
'msg_signaturet': signature,
'timestamp': timestamp,
'nonce': nonce,
}
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_xml
class PKCS7Encoder():
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + (pad * amount_to_pad).encode()
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad < 1 or pad > 32:
pad = 0
return decrypted[:-pad]
class Prpcrypt(object):
"""提供接收和推送给企业微信消息的加解密接口"""
def __init__(self, key):
# self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self, text, receiveid):
"""对明文进行加密
@param text: 需要加密的明文
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = text.encode()
text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode()
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
cryptor = AES.new(self.key, self.mode, self.key[:16])
try:
ciphertext = cryptor.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self, text, receiveid):
"""对解密后的明文进行补位删除
@param text: 密文
@return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key, self.mode, self.key[:16])
# 使用BASE64对密文进行解码然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_DecryptAES_Error, None
try:
pad = plain_text[-1]
# 去掉补位字符串
# pkcs7 = PKCS7Encoder()
# plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
xml_content = content[4: xml_len + 4]
from_receiveid = content[xml_len + 4:]
except Exception as e:
logger = logging.getLogger()
logger.error(e)
return ierror.WXBizMsgCrypt_IllegalBuffer, None
if from_receiveid.decode('utf8') != receiveid:
return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
return 0, xml_content
def get_random_str(self):
""" 随机生成16位字符串
@return: 16位字符串
"""
return str(random.randint(1000000000000000, 9999999999999999)).encode()
class WXBizMsgCrypt(object):
# 构造函数
def __init__(self, sToken, sEncodingAESKey, sReceiveId):
try:
self.key = base64.b64decode(sEncodingAESKey + "=")
assert len(self.key) == 32
except:
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
self.m_sToken = sToken
self.m_sReceiveId = sReceiveId
# 验证URL
# @param sMsgSignature: 签名串对应URL参数的msg_signature
# @param sTimeStamp: 时间戳对应URL参数的timestamp
# @param sNonce: 随机串对应URL参数的nonce
# @param sEchoStr: 随机串对应URL参数的echostr
# @param sReplyEchoStr: 解密之后的echostr当return返回0时有效
# @return成功0失败返回对应的错误码
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId)
return ret, sReplyEchoStr
def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
# 将企业回复用户的消息加密打包
# @param sReplyMsg: 企业号待回复用户的消息xml格式的字符串
# @param sTimeStamp: 时间戳可以自己生成也可以用URL参数的timestamp,如为None则自动用当前时间
# @param sNonce: 随机串可以自己生成也可以用URL参数的nonce
# sEncryptMsg: 加密后的可以直接回复用户的密文包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
# return成功0sEncryptMsg,失败返回对应的错误码None
pc = Prpcrypt(self.key)
ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
encrypt = encrypt.decode('utf8')
if ret != 0:
return ret, None
if timestamp is None:
timestamp = str(int(time.time()))
# 生成安全签名
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
if ret != 0:
return ret, None
xmlParse = XMLParse()
return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
# 检验消息的真实性,并且获取解密后的明文
# @param sMsgSignature: 签名串对应URL参数的msg_signature
# @param sTimeStamp: 时间戳对应URL参数的timestamp
# @param sNonce: 随机串对应URL参数的nonce
# @param sPostData: 密文对应POST请求的数据
# xml_content: 解密后的原文当return返回0时有效
# @return: 成功0失败返回对应的错误码
# 验证安全签名
xmlParse = XMLParse()
ret, encrypt = xmlParse.extract(sPostData)
if ret != 0:
return ret, None
sha1 = SHA1()
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
if ret != 0:
return ret, None
if not signature == sMsgSignature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId)
return ret, xml_content

View File

319
libs/wecom_api/api.py Normal file
View File

@@ -0,0 +1,319 @@
from quart import request
from .WXBizMsgCrypt3 import WXBizMsgCrypt
import base64
import binascii
import httpx
from quart import Quart
import xml.etree.ElementTree as ET
from typing import Callable, Dict, Any
from .wecomevent import WecomEvent
from pkg.platform.types import events as platform_events, message as platform_message
import aiofiles
class WecomClient():
def __init__(self,corpid:str,secret:str,token:str,EncodingAESKey:str,contacts_secret:str):
self.corpid = corpid
self.secret = secret
self.access_token_for_contacts =''
self.token = token
self.aes = EncodingAESKey
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
self.access_token = ''
self.secret_for_contacts = contacts_secret
self.app = Quart(__name__)
self.wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'])
self._message_handlers = {
"example":[],
}
#access——token操作
async def check_access_token(self):
return bool(self.access_token and self.access_token.strip())
async def check_access_token_for_contacts(self):
return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip())
async def get_access_token(self,secret):
url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}'
async with httpx.AsyncClient() as client:
response = await client.get(url)
data = response.json()
if 'access_token' in data:
return data['access_token']
else:
raise Exception(f"未获取access token: {data}")
async def get_users(self):
if not self.check_access_token_for_contacts():
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
url = self.base_url+'/user/list_id?access_token='+self.access_token_for_contacts
async with httpx.AsyncClient() as client:
params = {
"cursor":"",
"limit":10000,
}
response = await client.post(url,json=params)
data = response.json()
if data['errcode'] == 0:
dept_users = data['dept_user']
userid = []
for user in dept_users:
userid.append(user["userid"])
return userid
else:
raise Exception("未获取用户")
async def send_to_all(self,content:str):
if not self.check_access_token_for_contacts():
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
url = self.base_url+'/message/send?access_token='+self.access_token_for_contacts
user_ids = await self.get_users()
user_ids_string = "|".join(user_ids)
async with httpx.AsyncClient() as client:
params = {
"touser" : user_ids_string,
"msgtype" : "text",
"agentid" : 1000002,
"text" : {
"content" : content,
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
response = await client.post(url,json=params)
data = response.json()
if data['errcode'] != 0:
raise Exception("Failed to send message: "+str(data))
async def send_image(self,user_id:str,agent_id:int,media_id:str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url+'/media/upload?access_token='+self.access_token
async with httpx.AsyncClient() as client:
params = {
"touser" : user_id,
"toparty" : "",
"totag":"",
"agentid" : agent_id,
"msgtype" : "image",
"image" : {
"media_id" : media_id,
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
try:
response = await client.post(url,json=params)
data = response.json()
except Exception as e:
raise Exception("Failed to send image: "+str(e))
# 企业微信错误码40014和42001代表accesstoken问题
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
return await self.send_image(user_id,agent_id,media_id)
if data['errcode'] != 0:
raise Exception("Failed to send image: "+str(data))
async def send_private_msg(self,user_id:str, agent_id:int,content:str):
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url+'/message/send?access_token='+self.access_token
async with httpx.AsyncClient() as client:
params={
"touser" : user_id,
"msgtype" : "text",
"agentid" : agent_id,
"text" : {
"content" : content,
},
"safe":0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
response = await client.post(url,json=params)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
return await self.send_private_msg(user_id,agent_id,content)
if data['errcode'] != 0:
raise Exception("Failed to send message: "+str(data))
async def handle_callback_request(self):
"""
处理回调请求,包括 GET 验证和 POST 消息接收。
"""
try:
msg_signature = request.args.get("msg_signature")
timestamp = request.args.get("timestamp")
nonce = request.args.get("nonce")
if request.method == "GET":
echostr = request.args.get("echostr")
ret, reply_echo_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret != 0:
raise Exception(f"验证失败,错误码: {ret}")
return reply_echo_str
elif request.method == "POST":
encrypt_msg = await request.data
ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
if ret != 0:
raise Exception(f"消息解密失败,错误码: {ret}")
# 解析消息并处理
message_data = await self.get_message(xml_msg)
if message_data:
event = WecomEvent.from_payload(message_data) # 转换为 WecomEvent 对象
if event:
await self._handle_message(event)
return "success"
except Exception as e:
return f"Error processing request: {str(e)}", 400
async def run_task(self, host: str, port: int, *args, **kwargs):
"""
启动 Quart 应用。
"""
await self.app.run_task(host=host, port=port, *args, **kwargs)
def on_message(self, msg_type: str):
"""
注册消息类型处理器。
"""
def decorator(func: Callable[[WecomEvent], None]):
if msg_type not in self._message_handlers:
self._message_handlers[msg_type] = []
self._message_handlers[msg_type].append(func)
return func
return decorator
async def _handle_message(self, event: WecomEvent):
"""
处理消息事件。
"""
msg_type = event.type
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
await handler(event)
async def get_message(self, xml_msg: str) -> Dict[str, Any]:
"""
解析微信返回的 XML 消息并转换为字典。
"""
root = ET.fromstring(xml_msg)
message_data = {
"ToUserName": root.find("ToUserName").text,
"FromUserName": root.find("FromUserName").text,
"CreateTime": int(root.find("CreateTime").text),
"MsgType": root.find("MsgType").text,
"Content": root.find("Content").text if root.find("Content") is not None else None,
"MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None,
"AgentID": int(root.find("AgentID").text) if root.find("AgentID") is not None else None,
}
if message_data["MsgType"] == "image":
message_data["MediaId"] = root.find("MediaId").text if root.find("MediaId") is not None else None
message_data["PicUrl"] = root.find("PicUrl").text if root.find("PicUrl") is not None else None
return message_data
@staticmethod
async def get_image_type(image_bytes: bytes) -> str:
"""
通过图片的magic numbers判断图片类型
"""
magic_numbers = {
b'\xFF\xD8\xFF': 'jpg',
b'\x89\x50\x4E\x47': 'png',
b'\x47\x49\x46': 'gif',
b'\x42\x4D': 'bmp',
b'\x00\x00\x01\x00': 'ico'
}
for magic, ext in magic_numbers.items():
if image_bytes.startswith(magic):
return ext
return 'jpg' # 默认返回jpg
async def upload_to_work(self, image: platform_message.Image):
"""
获取 media_id
"""
if not await self.check_access_token():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file'
file_bytes = None
file_name = "uploaded_file.txt"
# 获取文件的二进制数据
if image.path:
async with aiofiles.open(image.path, 'rb') as f:
file_bytes = await f.read()
file_name = image.path.split('/')[-1]
elif image.url:
file_bytes = await self.download_image_to_bytes(image.url)
file_name = image.url.split('/')[-1]
elif image.base64:
try:
base64_data = image.base64
if ',' in base64_data:
base64_data = base64_data.split(',', 1)[1]
padding = 4 - (len(base64_data) % 4) if len(base64_data) % 4 else 0
padded_base64 = base64_data + '=' * padding
file_bytes = base64.b64decode(padded_base64)
except binascii.Error as e:
raise ValueError(f"Invalid base64 string: {str(e)}")
else:
raise ValueError("image对象出错")
# 设置 multipart/form-data 格式的文件
boundary = "-------------------------acebdf13572468"
headers = {
'Content-Type': f'multipart/form-data; boundary={boundary}'
}
body = (
f"--{boundary}\r\n"
f"Content-Disposition: form-data; name=\"media\"; filename=\"{file_name}\"; filelength={len(file_bytes)}\r\n"
f"Content-Type: application/octet-stream\r\n\r\n"
).encode('utf-8') + file_bytes + f"\r\n--{boundary}--\r\n".encode('utf-8')
# 上传文件
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, content=body)
data = response.json()
if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret)
media_id = await self.upload_to_work(image)
if data.get('errcode', 0) != 0:
raise Exception("failed to upload file")
media_id = data.get('media_id')
return media_id
async def download_image_to_bytes(self,url:str) -> bytes:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.content
#进行media_id的获取
async def get_media_id(self, image: platform_message.Image):
media_id = await self.upload_to_work(image=image)
return media_id

20
libs/wecom_api/ierror.py Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseXml_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateCorpid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011

View File

@@ -0,0 +1,179 @@
from typing import Dict, Any, Optional
class WecomEvent(dict):
"""
封装从企业微信收到的事件数据对象(字典),提供属性以获取其中的字段。
除 `type` 和 `detail_type` 属性对于任何事件都有效外,其它属性是否存在(若不存在则返回 `None`)依事件类型不同而不同。
"""
@staticmethod
def from_payload(payload: Dict[str, Any]) -> Optional["WecomEvent"]:
"""
从企业微信事件数据构造 `WecomEvent` 对象。
Args:
payload (Dict[str, Any]): 解密后的企业微信事件数据。
Returns:
Optional[WecomEvent]: 如果事件数据合法,则返回 WecomEvent 对象;否则返回 None。
"""
try:
event = WecomEvent(payload)
_ = event.type, event.detail_type # 确保必须字段存在
return event
except KeyError:
return None
@property
def type(self) -> str:
"""
事件类型,例如 "message""event""text" 等。
Returns:
str: 事件类型。
"""
return self.get("MsgType", "")
@property
def picurl(self) -> str:
"""
图片链接
"""
return self.get("PicUrl")
@property
def detail_type(self) -> str:
"""
事件详细类型,依 `type` 的不同而不同。例如:
- 消息事件: "text", "image", "voice", 等
- 事件通知: "subscribe", "unsubscribe", "click", 等
Returns:
str: 事件详细类型。
"""
if self.type == "event":
return self.get("Event", "")
return self.type
@property
def name(self) -> str:
"""
事件名,对于消息事件是 `type.detail_type`,对于其他事件是 `event_type`。
Returns:
str: 事件名。
"""
return f"{self.type}.{self.detail_type}"
@property
def user_id(self) -> Optional[str]:
"""
用户 ID例如消息的发送者或事件的触发者。
Returns:
Optional[str]: 用户 ID。
"""
return self.get("FromUserName")
@property
def agent_id(self) -> Optional[int]:
"""
机器人 ID仅在消息类型事件中存在。
Returns:
Optional[int]: 机器人 ID。
"""
return self.get("AgentID")
@property
def receiver_id(self) -> Optional[str]:
"""
接收者 ID例如机器人自身的企业微信 ID。
Returns:
Optional[str]: 接收者 ID。
"""
return self.get("ToUserName")
@property
def message_id(self) -> Optional[str]:
"""
消息 ID仅在消息类型事件中存在。
Returns:
Optional[str]: 消息 ID。
"""
return self.get("MsgId")
@property
def message(self) -> Optional[str]:
"""
消息内容,仅在消息类型事件中存在。
Returns:
Optional[str]: 消息内容。
"""
return self.get("Content")
@property
def media_id(self) -> Optional[str]:
"""
媒体文件 ID仅在图片、语音等消息类型中存在。
Returns:
Optional[str]: 媒体文件 ID。
"""
return self.get("MediaId")
@property
def timestamp(self) -> Optional[int]:
"""
事件发生的时间戳。
Returns:
Optional[int]: 时间戳。
"""
return self.get("CreateTime")
@property
def event_key(self) -> Optional[str]:
"""
事件的 Key 值,例如点击菜单时的 `EventKey`。
Returns:
Optional[str]: 事件 Key。
"""
return self.get("EventKey")
def __getattr__(self, key: str) -> Optional[Any]:
"""
允许通过属性访问数据中的任意字段。
Args:
key (str): 字段名。
Returns:
Optional[Any]: 字段值。
"""
return self.get(key)
def __setattr__(self, key: str, value: Any) -> None:
"""
允许通过属性设置数据中的任意字段。
Args:
key (str): 字段名。
value (Any): 字段值。
"""
self[key] = value
def __repr__(self) -> str:
"""
生成事件对象的字符串表示。
Returns:
str: 字符串表示。
"""
return f"<WecomEvent {super().__repr__()}>"

View File

@@ -49,12 +49,10 @@ async def main_entry(loop: asyncio.AbstractEventLoop):
generated_files = await files.generate_files() generated_files = await files.generate_files()
if generated_files: if generated_files:
print("以下文件不存在,已自动生成,请按需修改配置文件后重启") print("以下文件不存在,已自动生成:")
for file in generated_files: for file in generated_files:
print("-", file) print("-", file)
sys.exit(0)
from pkg.core import boot from pkg.core import boot
await boot.main(loop) await boot.main(loop)

View File

@@ -12,7 +12,7 @@ from .. import group
class LogsRouterGroup(group.RouterGroup): class LogsRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('', methods=['GET']) @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
start_page_number = int(quart.request.args.get('start_page_number', 0)) start_page_number = int(quart.request.args.get('start_page_number', 0))

View File

@@ -13,7 +13,7 @@ from .. import group
class PluginsRouterGroup(group.RouterGroup): class PluginsRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('', methods=['GET']) @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
plugins = self.ap.plugin_mgr.plugins() plugins = self.ap.plugin_mgr.plugins()
@@ -23,14 +23,14 @@ class PluginsRouterGroup(group.RouterGroup):
'plugins': plugins_data 'plugins': plugins_data
}) })
@self.route('/<author>/<plugin_name>/toggle', methods=['PUT']) @self.route('/<author>/<plugin_name>/toggle', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN)
async def _(author: str, plugin_name: str) -> str: async def _(author: str, plugin_name: str) -> str:
data = await quart.request.json data = await quart.request.json
target_enabled = data.get('target_enabled') target_enabled = data.get('target_enabled')
await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled) await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled)
return self.success() return self.success()
@self.route('/<author>/<plugin_name>/update', methods=['POST']) @self.route('/<author>/<plugin_name>/update', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _(author: str, plugin_name: str) -> str: async def _(author: str, plugin_name: str) -> str:
ctx = taskmgr.TaskContext.new() ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task( wrapper = self.ap.task_mgr.create_user_task(
@@ -44,7 +44,7 @@ class PluginsRouterGroup(group.RouterGroup):
'task_id': wrapper.id 'task_id': wrapper.id
}) })
@self.route('/<author>/<plugin_name>', methods=['DELETE']) @self.route('/<author>/<plugin_name>', methods=['DELETE'], auth_type=group.AuthType.USER_TOKEN)
async def _(author: str, plugin_name: str) -> str: async def _(author: str, plugin_name: str) -> str:
ctx = taskmgr.TaskContext.new() ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task( wrapper = self.ap.task_mgr.create_user_task(
@@ -59,13 +59,13 @@ class PluginsRouterGroup(group.RouterGroup):
'task_id': wrapper.id 'task_id': wrapper.id
}) })
@self.route('/reorder', methods=['PUT']) @self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
data = await quart.request.json data = await quart.request.json
await self.ap.plugin_mgr.reorder_plugins(data.get('plugins')) await self.ap.plugin_mgr.reorder_plugins(data.get('plugins'))
return self.success() return self.success()
@self.route('/install/github', methods=['POST']) @self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
data = await quart.request.json data = await quart.request.json

View File

@@ -9,7 +9,7 @@ class SettingsRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('', methods=['GET']) @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
return self.success( return self.success(
data={ data={
@@ -23,7 +23,7 @@ class SettingsRouterGroup(group.RouterGroup):
} }
) )
@self.route('/<manager_name>', methods=['GET']) @self.route('/<manager_name>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _(manager_name: str) -> str: async def _(manager_name: str) -> str:
manager = self.ap.settings_mgr.get_manager(manager_name) manager = self.ap.settings_mgr.get_manager(manager_name)
@@ -44,7 +44,7 @@ class SettingsRouterGroup(group.RouterGroup):
} }
) )
@self.route('/<manager_name>/data', methods=['PUT']) @self.route('/<manager_name>/data', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN)
async def _(manager_name: str) -> str: async def _(manager_name: str) -> str:
data = await quart.request.json data = await quart.request.json
manager = self.ap.settings_mgr.get_manager(manager_name) manager = self.ap.settings_mgr.get_manager(manager_name)

View File

@@ -9,7 +9,7 @@ from .. import group
class StatsRouterGroup(group.RouterGroup): class StatsRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('/basic', methods=['GET']) @self.route('/basic', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
conv_count = 0 conv_count = 0

View File

@@ -20,7 +20,7 @@ class SystemRouterGroup(group.RouterGroup):
} }
) )
@self.route('/tasks', methods=['GET']) @self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
task_type = quart.request.args.get("type") task_type = quart.request.args.get("type")
@@ -31,7 +31,7 @@ class SystemRouterGroup(group.RouterGroup):
data=self.ap.task_mgr.get_tasks_dict(task_type) data=self.ap.task_mgr.get_tasks_dict(task_type)
) )
@self.route('/tasks/<task_id>', methods=['GET']) @self.route('/tasks/<task_id>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _(task_id: str) -> str: async def _(task_id: str) -> str:
task = self.ap.task_mgr.get_task_by_id(int(task_id)) task = self.ap.task_mgr.get_task_by_id(int(task_id))
@@ -40,7 +40,7 @@ class SystemRouterGroup(group.RouterGroup):
return self.success(data=task.to_dict()) return self.success(data=task.to_dict())
@self.route('/reload', methods=['POST']) @self.route('/reload', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
json_data = await quart.request.json json_data = await quart.request.json
@@ -51,7 +51,7 @@ class SystemRouterGroup(group.RouterGroup):
) )
return self.success() return self.success()
@self.route('/_debug/exec', methods=['POST']) @self.route('/_debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
if not constants.debug_mode: if not constants.debug_mode:
return self.http_status(403, 403, "Forbidden") return self.http_status(403, 403, "Forbidden")

View File

@@ -13,14 +13,14 @@ identifier = {
'instance_create_ts': 0, 'instance_create_ts': 0,
} }
HOST_ID_FILE = os.path.expanduser('~/.qchatgpt/host_id.json') HOST_ID_FILE = os.path.expanduser('~/.langbot/host_id.json')
INSTANCE_ID_FILE = 'res/instance_id.json' INSTANCE_ID_FILE = 'data/labels/instance_id.json'
def init(): def init():
global identifier global identifier
if not os.path.exists(os.path.expanduser('~/.qchatgpt')): if not os.path.exists(os.path.expanduser('~/.langbot')):
os.mkdir(os.path.expanduser('~/.qchatgpt')) os.mkdir(os.path.expanduser('~/.langbot'))
if not os.path.exists(HOST_ID_FILE): if not os.path.exists(HOST_ID_FILE):
new_host_id = 'host_'+str(uuid.uuid4()) new_host_id = 'host_'+str(uuid.uuid4())

View File

@@ -143,9 +143,7 @@ class Application:
self.logger.warning("WebUI 文件缺失请根据文档获取https://docs.langbot.app/webui/intro.html") self.logger.warning("WebUI 文件缺失请根据文档获取https://docs.langbot.app/webui/intro.html")
return return
import socket host_ip = "127.0.0.1"
host_ip = socket.gethostbyname(socket.gethostname())
public_ip = await ip.get_myip() public_ip = await ip.get_myip()
@@ -199,5 +197,27 @@ class Application:
await self.plugin_mgr.load_plugins() await self.plugin_mgr.load_plugins()
await self.plugin_mgr.initialize_plugins() await self.plugin_mgr.initialize_plugins()
case core_entities.LifecycleControlScope.PROVIDER.value:
self.logger.info("执行热重载 scope="+scope)
llm_model_mgr_inst = llm_model_mgr.ModelManager(self)
await llm_model_mgr_inst.initialize()
self.model_mgr = llm_model_mgr_inst
llm_session_mgr_inst = llm_session_mgr.SessionManager(self)
await llm_session_mgr_inst.initialize()
self.sess_mgr = llm_session_mgr_inst
llm_prompt_mgr_inst = llm_prompt_mgr.PromptManager(self)
await llm_prompt_mgr_inst.initialize()
self.prompt_mgr = llm_prompt_mgr_inst
llm_tool_mgr_inst = llm_tool_mgr.ToolManager(self)
await llm_tool_mgr_inst.initialize()
self.tool_mgr = llm_tool_mgr_inst
runner_mgr_inst = runnermgr.RunnerManager(self)
await runner_mgr_inst.initialize()
self.runner_mgr = runner_mgr_inst
case _: case _:
pass pass

View File

@@ -1,5 +1,7 @@
import pip import pip
# 检查依赖,防止用户未安装
# 左边为引入名称,右边为依赖名称
required_deps = { required_deps = {
"requests": "requests", "requests": "requests",
"openai": "openai", "openai": "openai",
@@ -23,6 +25,12 @@ required_deps = {
"aioshutil": "aioshutil", "aioshutil": "aioshutil",
"argon2": "argon2-cffi", "argon2": "argon2-cffi",
"jwt": "pyjwt", "jwt": "pyjwt",
"Crypto": "pycryptodome",
"lark_oapi": "lark-oapi",
"discord": "discord.py",
"cryptography": "cryptography",
"gewechat_client": "gewechat-client",
"dashscope": "dashscope",
} }

View File

@@ -24,6 +24,7 @@ required_paths = [
"data/scenario", "data/scenario",
"data/logs", "data/logs",
"data/config", "data/config",
"data/labels",
"plugins" "plugins"
] ]

View File

@@ -23,6 +23,7 @@ class LifecycleControlScope(enum.Enum):
APPLICATION = "application" APPLICATION = "application"
PLATFORM = "platform" PLATFORM = "platform"
PLUGIN = "plugin" PLUGIN = "plugin"
PROVIDER = "provider"
class LauncherTypes(enum.Enum): class LauncherTypes(enum.Enum):
@@ -44,10 +45,10 @@ class Query(pydantic.BaseModel):
launcher_type: LauncherTypes launcher_type: LauncherTypes
"""会话类型platform处理阶段设置""" """会话类型platform处理阶段设置"""
launcher_id: int launcher_id: typing.Union[int, str]
"""会话IDplatform处理阶段设置""" """会话IDplatform处理阶段设置"""
sender_id: int sender_id: typing.Union[int, str]
"""发送者IDplatform处理阶段设置""" """发送者IDplatform处理阶段设置"""
message_event: platform_events.MessageEvent message_event: platform_events.MessageEvent
@@ -113,9 +114,9 @@ class Session(pydantic.BaseModel):
"""会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}""" """会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}"""
launcher_type: LauncherTypes launcher_type: LauncherTypes
launcher_id: int launcher_id: typing.Union[int, str]
sender_id: typing.Optional[int] = 0 sender_id: typing.Optional[typing.Union[int, str]] = 0
use_prompt_name: typing.Optional[str] = 'default' use_prompt_name: typing.Optional[str] = 'default'

View File

@@ -0,0 +1,24 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("dify-api-timeout-params", 17)
class DifyAPITimeoutParamsMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'timeout' not in self.ap.provider_cfg.data['dify-service-api']['chat'] or 'timeout' not in self.ap.provider_cfg.data['dify-service-api']['workflow'] \
or 'agent' not in self.ap.provider_cfg.data['dify-service-api']
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['dify-service-api']['chat']['timeout'] = 120
self.ap.provider_cfg.data['dify-service-api']['workflow']['timeout'] = 120
self.ap.provider_cfg.data['dify-service-api']['agent'] = {
"api-key": "app-1234567890",
"timeout": 120
}
await self.ap.provider_cfg.dump_config()

View File

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

View File

@@ -0,0 +1,25 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("zhipuai-config", 19)
class ZhipuaiConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'zhipuai-chat-completions' not in self.ap.provider_cfg.data['requester']
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['requester']['zhipuai-chat-completions'] = {
"base-url": "https://open.bigmodel.cn/api/paas/v4",
"args": {},
"timeout": 120
}
self.ap.provider_cfg.data['keys']['zhipuai'] = [
"xxxxxxx"
]
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("wecom-config", 20)
class WecomConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'wecom':
return False
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters'].append({
"adapter": "wecom",
"enable": False,
"host": "0.0.0.0",
"port": 2290,
"corpid": "",
"secret": "",
"token": "",
"EncodingAESKey": "",
"contacts_secret": ""
})
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,29 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("lark-config", 21)
class LarkConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'lark':
return False
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters'].append({
"adapter": "lark",
"enable": False,
"app_id": "cli_abcdefgh",
"app_secret": "XXXXXXXXXX",
"bot_name": "LangBot"
})
await self.ap.platform_cfg.dump_config()

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("discord-config", 24)
class DiscordConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'discord':
return False
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters'].append({
"adapter": "discord",
"enable": False,
"client_id": "1234567890",
"token": "XXXXXXXXXX"
})
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("gewechat-config", 25)
class GewechatConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'gewechat':
return False
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters'].append({
"adapter": "gewechat",
"enable": False,
"gewechat_url": "http://your-gewechat-server:2531",
"port": 2286,
"callback_url": "http://your-callback-url:2286/gewechat/callback",
"app_id": "",
"token": ""
})
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("qqofficial-config", 26)
class QQOfficialConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'qqofficial':
return False
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters'].append({
"adapter": "qqofficial",
"enable": False,
"appid": "",
"secret": "",
"port": 2284,
"token": ""
})
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("wx-official-account-config", 27)
class WXOfficialAccountConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
return False
return True
async def run(self):
"""执行迁移"""
self.ap.platform_cfg.data['platform-adapters'].append({
"adapter": "officialaccount",
"enable": False,
"token": "",
"EncodingAESKey": "",
"AppID": "",
"AppSecret": "",
"host": "0.0.0.0",
"port": 2287
})
await self.ap.platform_cfg.dump_config()

View File

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

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("dashscope-app-api-config", 29)
class DashscopeAppAPICfgMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'dashscope-app-api' not in self.ap.provider_cfg.data
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['dashscope-app-api'] = {
"app-type": "agent",
"api-key": "sk-1234567890",
"agent": {
"app-id": "Your_app_id",
"references_quote": "参考资料来自:"
},
"workflow": {
"app-id": "Your_app_id",
"references_quote": "参考资料来自:",
"biz_params": {
"city": "北京",
"date": "2023-08-10"
}
}
}
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import typing
import os
import sys
import logging
from .. import note, app
@note.note_class("PrintVersion", 3)
class PrintVersion(note.LaunchNote):
"""打印版本信息
"""
async def need_show(self) -> bool:
return True
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
yield f"当前版本:{self.ap.ver_mgr.get_current_version()}", logging.INFO

View File

@@ -7,9 +7,13 @@ from .. import migration
from ..migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion from ..migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion
from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg
from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config
from ..migrations import m015_gitee_ai_config, m016_dify_service_api from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_api_timeout_params, m018_xai_config, m019_zhipuai_config
from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config
from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config
from ..migrations import m029_dashscope_app_api_config
@stage.stage_class("MigrationStage") @stage.stage_class("MigrationStage")
class MigrationStage(stage.BootingStage): class MigrationStage(stage.BootingStage):
"""迁移阶段 """迁移阶段

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from .. import stage, app, note from .. import stage, app, note
from ..notes import n001_classic_msgs, n002_selection_mode_on_windows from ..notes import n001_classic_msgs, n002_selection_mode_on_windows, n003_print_version
@stage.stage_class("ShowNotesStage") @stage.stage_class("ShowNotesStage")

View File

@@ -68,7 +68,7 @@ class Controller:
except Exception as e: except Exception as e:
# traceback.print_exc() # traceback.print_exc()
self.ap.logger.error(f"控制器循环出错: {e}") self.ap.logger.error(f"控制器循环出错: {e}")
self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") self.ap.logger.error(f"Traceback: {traceback.format_exc()}")
async def _check_output(self, query: entities.Query, result: pipeline_entities.StageProcessResult): async def _check_output(self, query: entities.Query, result: pipeline_entities.StageProcessResult):
"""检查输出 """检查输出
@@ -163,29 +163,30 @@ class Controller:
async def process_query(self, query: entities.Query): async def process_query(self, query: entities.Query):
"""处理请求 """处理请求
""" """
# ======== 触发 MessageReceived 事件 ========
event_type = events.PersonMessageReceived if query.launcher_type == entities.LauncherTypes.PERSON else events.GroupMessageReceived
event_ctx = await self.ap.plugin_mgr.emit_event(
event=event_type(
launcher_type=query.launcher_type.value,
launcher_id=query.launcher_id,
sender_id=query.sender_id,
message_chain=query.message_chain,
query=query
)
)
if event_ctx.is_prevented_default():
return
self.ap.logger.debug(f"Processing query {query}")
try: try:
# ======== 触发 MessageReceived 事件 ========
event_type = events.PersonMessageReceived if query.launcher_type == entities.LauncherTypes.PERSON else events.GroupMessageReceived
event_ctx = await self.ap.plugin_mgr.emit_event(
event=event_type(
launcher_type=query.launcher_type.value,
launcher_id=query.launcher_id,
sender_id=query.sender_id,
message_chain=query.message_chain,
query=query
)
)
if event_ctx.is_prevented_default():
return
self.ap.logger.debug(f"Processing query {query}")
await self._execute_from_stage(0, query) await self._execute_from_stage(0, query)
except Exception as e: except Exception as e:
self.ap.logger.error(f"处理请求时出错 query_id={query.query_id} stage={query.current_stage.inst_name} : {e}") inst_name = query.current_stage.inst_name if query.current_stage else 'unknown'
self.ap.logger.error(f"处理请求时出错 query_id={query.query_id} stage={inst_name} : {e}")
self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") self.ap.logger.debug(f"Traceback: {traceback.format_exc()}")
finally: finally:
self.ap.logger.debug(f"Query {query} processed") self.ap.logger.debug(f"Query {query} processed")

View File

@@ -9,30 +9,8 @@ from ....core import entities as core_entities
from ....platform.types import message as platform_message from ....platform.types import message as platform_message
class ForwardMessageDiaplay(pydantic.BaseModel): ForwardMessageDiaplay = platform_message.ForwardMessageDiaplay
title: str = "群聊的聊天记录" Forward = platform_message.Forward
brief: str = "[聊天记录]"
source: str = "聊天记录"
preview: typing.List[str] = []
summary: str = "查看x条转发消息"
class Forward(platform_message.MessageComponent):
"""合并转发。"""
type: str = "Forward"
"""消息组件类型。"""
display: ForwardMessageDiaplay
"""显示信息"""
node_list: typing.List[platform_message.ForwardMessageNode]
"""转发消息节点列表。"""
def __init__(self, *args, **kwargs):
if len(args) == 1:
self.node_list = args[0]
super().__init__(**kwargs)
super().__init__(*args, **kwargs)
def __str__(self):
return '[聊天记录]'
@strategy_model.strategy_class("forward") @strategy_model.strategy_class("forward")

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import typing
from ..core import entities from ..core import entities
from ..platform import adapter as msadapter from ..platform import adapter as msadapter
@@ -29,8 +29,8 @@ class QueryPool:
async def add_query( async def add_query(
self, self,
launcher_type: entities.LauncherTypes, launcher_type: entities.LauncherTypes,
launcher_id: int, launcher_id: typing.Union[int, str],
sender_id: int, sender_id: typing.Union[int, str],
message_event: platform_events.MessageEvent, message_event: platform_events.MessageEvent,
message_chain: platform_message.MessageChain, message_chain: platform_message.MessageChain,
adapter: msadapter.MessageSourceAdapter adapter: msadapter.MessageSourceAdapter

View File

@@ -45,7 +45,7 @@ class PreProcessor(stage.PipelineStage):
# 检查vision是否启用没启用就删除所有图片 # 检查vision是否启用没启用就删除所有图片
if not self.ap.provider_cfg.data['enable-vision'] or not query.use_model.vision_supported: if not self.ap.provider_cfg.data['enable-vision'] or (self.ap.provider_cfg.data['runner'] == 'local-agent' and not query.use_model.vision_supported):
for msg in query.messages: for msg in query.messages:
if isinstance(msg.content, list): if isinstance(msg.content, list):
for me in msg.content: for me in msg.content:
@@ -60,13 +60,13 @@ class PreProcessor(stage.PipelineStage):
llm_entities.ContentElement.from_text(me.text) llm_entities.ContentElement.from_text(me.text)
) )
elif isinstance(me, platform_message.Image): elif isinstance(me, platform_message.Image):
if self.ap.provider_cfg.data['enable-vision'] and query.use_model.vision_supported: if self.ap.provider_cfg.data['enable-vision'] and (self.ap.provider_cfg.data['runner'] != 'local-agent' or query.use_model.vision_supported):
if me.url is not None: if me.base64 is not None:
content_list.append( content_list.append(
llm_entities.ContentElement.from_image_url(str(me.url)) llm_entities.ContentElement.from_image_base64(me.base64)
) )
query.user_message = llm_entities.Message( # TODO 适配多模态输入 query.user_message = llm_entities.Message(
role='user', role='user',
content=content_list content=content_list
) )

View File

@@ -91,7 +91,7 @@ class ChatMessageHandler(handler.MessageHandler):
query.session.using_conversation.messages.extend(query.resp_messages) query.session.using_conversation.messages.extend(query.resp_messages)
except Exception as e: except Exception as e:
self.ap.logger.error(f'对话({query.query_id})请求失败: {str(e)}') self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}')
yield entities.StageProcessResult( yield entities.StageProcessResult(
result_type=entities.ResultType.INTERRUPT, result_type=entities.ResultType.INTERRUPT,
@@ -105,7 +105,7 @@ class ChatMessageHandler(handler.MessageHandler):
await self.ap.ctr_mgr.usage.post_query_record( await self.ap.ctr_mgr.usage.post_query_record(
session_type=query.session.launcher_type.value, session_type=query.session.launcher_type.value,
session_id=str(query.session.launcher_id), session_id=str(query.session.launcher_id),
query_ability_provider="QChatGPT.Chat", query_ability_provider="LangBot.Chat",
usage=text_length, usage=text_length,
model_name=query.use_model.name, model_name=query.use_model.name,
response_seconds=int(time.time() - start_time), response_seconds=int(time.time() - start_time),

View File

@@ -31,7 +31,7 @@ class ReteLimitAlgo(metaclass=abc.ABCMeta):
pass pass
@abc.abstractmethod @abc.abstractmethod
async def require_access(self, launcher_type: str, launcher_id: int) -> bool: async def require_access(self, launcher_type: str, launcher_id: typing.Union[int, str]) -> bool:
"""进入处理流程 """进入处理流程
这个方法对等待是友好的,意味着算法可以实现在这里等待一段时间以控制速率。 这个方法对等待是友好的,意味着算法可以实现在这里等待一段时间以控制速率。
@@ -46,7 +46,7 @@ class ReteLimitAlgo(metaclass=abc.ABCMeta):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
async def release_access(self, launcher_type: str, launcher_id: int): async def release_access(self, launcher_type: str, launcher_id: typing.Union[int, str]):
"""退出处理流程 """退出处理流程
Args: Args:

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import time import time
import typing
from .. import algo from .. import algo
# 固定窗口算法 # 固定窗口算法
@@ -29,7 +30,7 @@ class FixedWindowAlgo(algo.ReteLimitAlgo):
self.containers_lock = asyncio.Lock() self.containers_lock = asyncio.Lock()
self.containers = {} self.containers = {}
async def require_access(self, launcher_type: str, launcher_id: int) -> bool: async def require_access(self, launcher_type: str, launcher_id: typing.Union[int, str]) -> bool:
# 加锁,找容器 # 加锁,找容器
container: SessionContainer = None container: SessionContainer = None
@@ -83,5 +84,5 @@ class FixedWindowAlgo(algo.ReteLimitAlgo):
# 返回True # 返回True
return True return True
async def release_access(self, launcher_type: str, launcher_id: int): async def release_access(self, launcher_type: str, launcher_id: typing.Union[int, str]):
pass pass

View File

@@ -102,7 +102,7 @@ class ResponseWrapper(stage.PipelineStage):
new_query=query new_query=query
) )
if result.tool_calls is not None: # 有函数调用 if result.tool_calls is not None and len(result.tool_calls) > 0: # 有函数调用
function_names = [tc.function.name for tc in result.tool_calls] function_names = [tc.function.name for tc in result.tool_calls]

View File

@@ -52,7 +52,7 @@ class MessageSourceAdapter(metaclass=abc.ABCMeta):
self.config = config self.config = config
self.ap = ap self.ap = ap
async def send_message( async def send_message(
self, self,
target_type: str, target_type: str,
target_id: str, target_id: str,

View File

@@ -7,6 +7,8 @@ import logging
import asyncio import asyncio
import traceback import traceback
from .sources import qqofficial
# FriendMessage, Image, MessageChain, Plain # FriendMessage, Image, MessageChain, Plain
from ..platform import adapter as msadapter from ..platform import adapter as msadapter
@@ -37,7 +39,7 @@ class PlatformManager:
async def initialize(self): async def initialize(self):
from .sources import nakuru, aiocqhttp, qqbotpy from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat, officialaccount
async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter): async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter):
@@ -50,17 +52,6 @@ class PlatformManager:
adapter=adapter adapter=adapter
) )
async def on_stranger_message(event: platform_events.StrangerMessage, adapter: msadapter.MessageSourceAdapter):
await self.ap.query_pool.add_query(
launcher_type=core_entities.LauncherTypes.PERSON,
launcher_id=event.sender.id,
sender_id=event.sender.id,
message_event=event,
message_chain=event.message_chain,
adapter=adapter
)
async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessageSourceAdapter): async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessageSourceAdapter):
await self.ap.query_pool.add_query( await self.ap.query_pool.add_query(
@@ -96,12 +87,6 @@ class PlatformManager:
) )
self.adapters.append(adapter_inst) self.adapters.append(adapter_inst)
if adapter_name == 'yiri-mirai':
adapter_inst.register_listener(
platform_events.StrangerMessage,
on_stranger_message
)
adapter_inst.register_listener( adapter_inst.register_listener(
platform_events.FriendMessage, platform_events.FriendMessage,
on_friend_message on_friend_message
@@ -117,6 +102,35 @@ class PlatformManager:
if len(self.adapters) == 0: if len(self.adapters) == 0:
self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。') self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。')
async def write_back_config(self, adapter_inst: msadapter.MessageSourceAdapter, config: dict):
index = -2
for i, adapter in enumerate(self.adapters):
if adapter == adapter_inst:
index = i
break
if index == -2:
raise Exception('平台适配器未找到')
# 只修改启用的适配器
real_index = -1
for i, adapter in enumerate(self.ap.platform_cfg.data['platform-adapters']):
if adapter['enable']:
index -= 1
if index == -1:
real_index = i
break
new_cfg = {
'adapter': adapter_inst.name,
'enable': True,
**config
}
self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg
await self.ap.platform_cfg.dump_config()
async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessageSourceAdapter): async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessageSourceAdapter):
if self.ap.platform_cfg.data['at-sender'] and isinstance(event, platform_events.GroupMessage): if self.ap.platform_cfg.data['at-sender'] and isinstance(event, platform_events.GroupMessage):

View File

@@ -6,6 +6,7 @@ import time
import datetime import datetime
import aiocqhttp import aiocqhttp
import aiohttp
from .. import adapter from .. import adapter
from ...pipeline.longtext.strategies import forward from ...pipeline.longtext.strategies import forward
@@ -13,12 +14,12 @@ from ...core import app
from ..types import message as platform_message from ..types import message as platform_message
from ..types import events as platform_events from ..types import events as platform_events
from ..types import entities as platform_entities from ..types import entities as platform_entities
from ...utils import image
class AiocqhttpMessageConverter(adapter.MessageConverter): class AiocqhttpMessageConverter(adapter.MessageConverter):
@staticmethod @staticmethod
def yiri2target(message_chain: platform_message.MessageChain) -> typing.Tuple[list, int, datetime.datetime]: async def yiri2target(message_chain: platform_message.MessageChain) -> typing.Tuple[list, int, datetime.datetime]:
msg_list = aiocqhttp.Message() msg_list = aiocqhttp.Message()
msg_id = 0 msg_id = 0
@@ -59,7 +60,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif type(msg) is forward.Forward: elif type(msg) is forward.Forward:
for node in msg.node_list: for node in msg.node_list:
msg_list.extend(AiocqhttpMessageConverter.yiri2target(node.message_chain)[0]) msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0])
else: else:
msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) msg_list.append(aiocqhttp.MessageSegment.text(str(msg)))
@@ -67,7 +68,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
return msg_list, msg_id, msg_time return msg_list, msg_id, msg_time
@staticmethod @staticmethod
def target2yiri(message: str, message_id: int = -1): async def target2yiri(message: str, message_id: int = -1):
message = aiocqhttp.Message(message) message = aiocqhttp.Message(message)
yiri_msg_list = [] yiri_msg_list = []
@@ -89,7 +90,8 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif msg.type == "text": elif msg.type == "text":
yiri_msg_list.append(platform_message.Plain(text=msg.data["text"])) yiri_msg_list.append(platform_message.Plain(text=msg.data["text"]))
elif msg.type == "image": elif msg.type == "image":
yiri_msg_list.append(platform_message.Image(url=msg.data["url"])) image_base64, image_format = await image.qq_image_url_to_base64(msg.data['url'])
yiri_msg_list.append(platform_message.Image(base64=f"data:image/{image_format};base64,{image_base64}"))
chain = platform_message.MessageChain(yiri_msg_list) chain = platform_message.MessageChain(yiri_msg_list)
@@ -99,9 +101,9 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
class AiocqhttpEventConverter(adapter.EventConverter): class AiocqhttpEventConverter(adapter.EventConverter):
@staticmethod @staticmethod
def yiri2target(event: platform_events.Event, bot_account_id: int): async def yiri2target(event: platform_events.Event, bot_account_id: int):
msg, msg_id, msg_time = AiocqhttpMessageConverter.yiri2target(event.message_chain) msg, msg_id, msg_time = await AiocqhttpMessageConverter.yiri2target(event.message_chain)
if type(event) is platform_events.GroupMessage: if type(event) is platform_events.GroupMessage:
role = "member" role = "member"
@@ -164,8 +166,8 @@ class AiocqhttpEventConverter(adapter.EventConverter):
return aiocqhttp.Event.from_payload(payload) return aiocqhttp.Event.from_payload(payload)
@staticmethod @staticmethod
def target2yiri(event: aiocqhttp.Event): async def target2yiri(event: aiocqhttp.Event):
yiri_chain = AiocqhttpMessageConverter.target2yiri( yiri_chain = await AiocqhttpMessageConverter.target2yiri(
event.message, event.message_id event.message, event.message_id
) )
@@ -242,7 +244,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
async def send_message( async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain self, target_type: str, target_id: str, message: platform_message.MessageChain
): ):
aiocq_msg = AiocqhttpMessageConverter.yiri2target(message)[0] aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0]
if target_type == "group": if target_type == "group":
await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg) await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg)
@@ -255,8 +257,8 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
message: platform_message.MessageChain, message: platform_message.MessageChain,
quote_origin: bool = False, quote_origin: bool = False,
): ):
aiocq_event = AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id) aiocq_event = await AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id)
aiocq_msg = AiocqhttpMessageConverter.yiri2target(message)[0] aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0]
if quote_origin: if quote_origin:
aiocq_msg = aiocqhttp.MessageSegment.reply(aiocq_event.message_id) + aiocq_msg aiocq_msg = aiocqhttp.MessageSegment.reply(aiocq_event.message_id) + aiocq_msg
@@ -276,7 +278,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
async def on_message(event: aiocqhttp.Event): async def on_message(event: aiocqhttp.Event):
self.bot_account_id = event.self_id self.bot_account_id = event.self_id
try: try:
return await callback(self.event_converter.target2yiri(event), self) return await callback(await self.event_converter.target2yiri(event), self)
except: except:
traceback.print_exc() traceback.print_exc()

View File

@@ -0,0 +1,269 @@
from __future__ import annotations
import discord
import typing
import asyncio
import traceback
import time
import re
import base64
import uuid
import json
import os
import datetime
import aiohttp
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...utils import image
class DiscordMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain
) -> typing.Tuple[str, typing.List[discord.File]]:
for ele in message_chain:
if isinstance(ele, platform_message.At):
message_chain.remove(ele)
break
text_string = ""
image_files = []
for ele in message_chain:
if isinstance(ele, platform_message.Image):
image_bytes = None
if ele.base64:
image_bytes = base64.b64decode(ele.base64)
elif ele.url:
async with aiohttp.ClientSession() as session:
async with session.get(ele.url) as response:
image_bytes = await response.read()
elif ele.path:
with open(ele.path, "rb") as f:
image_bytes = f.read()
image_files.append(discord.File(fp=image_bytes, filename=f"{uuid.uuid4()}.png"))
elif isinstance(ele, platform_message.Plain):
text_string += ele.text
elif isinstance(ele, platform_message.Forward):
for node in ele.node_list:
text_string, image_files = await DiscordMessageConverter.yiri2target(node.message_chain)
text_string += text_string
image_files.extend(image_files)
return text_string, image_files
@staticmethod
async def target2yiri(
message: discord.Message
) -> platform_message.MessageChain:
lb_msg_list = []
msg_create_time = datetime.datetime.fromtimestamp(
int(message.created_at.timestamp())
)
lb_msg_list.append(
platform_message.Source(id=message.id, time=msg_create_time)
)
element_list = []
def text_element_recur(text_ele: str) -> list[platform_message.MessageComponent]:
if text_ele == "":
return []
# <@1234567890>
# @everyone
# @here
at_pattern = re.compile(r"(@everyone|@here|<@[\d]+>)")
at_matches = at_pattern.findall(text_ele)
if len(at_matches) > 0:
mid_at = at_matches[0]
text_split = text_ele.split(mid_at)
mid_at_component = []
if mid_at == "@everyone" or mid_at == "@here":
mid_at_component.append(platform_message.AtAll())
else:
mid_at_component.append(platform_message.At(target=mid_at[2:-1]))
return text_element_recur(text_split[0]) + \
mid_at_component + \
text_element_recur(text_split[1])
else:
return [platform_message.Plain(text=text_ele)]
element_list.extend(text_element_recur(message.content))
# attachments
for attachment in message.attachments:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(attachment.url) as response:
image_data = await response.read()
image_base64 = base64.b64encode(image_data).decode("utf-8")
image_format = response.headers["Content-Type"]
element_list.append(platform_message.Image(base64=f"data:{image_format};base64,{image_base64}"))
return platform_message.MessageChain(element_list)
class DiscordEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(
event: platform_events.Event
) -> discord.Message:
pass
@staticmethod
async def target2yiri(
event: discord.Message
) -> platform_events.Event:
message_chain = await DiscordMessageConverter.target2yiri(event)
if type(event.channel) == discord.DMChannel:
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event.author.id,
nickname=event.author.name,
remark=event.channel.id,
),
message_chain=message_chain,
time=event.created_at.timestamp(),
source_platform_object=event,
)
elif type(event.channel) == discord.TextChannel:
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=event.author.id,
member_name=event.author.name,
permission=platform_entities.Permission.Member,
group=platform_entities.Group(
id=event.channel.id,
name=event.channel.name,
permission=platform_entities.Permission.Member,
),
special_title="",
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=message_chain,
time=event.created_at.timestamp(),
source_platform_object=event,
)
@adapter.adapter_class("discord")
class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter):
bot: discord.Client
bot_account_id: str # 用于在流水线中识别at是否是本bot直接以bot_name作为标识
config: dict
ap: app.Application
message_converter: DiscordMessageConverter = DiscordMessageConverter()
event_converter: DiscordEventConverter = DiscordEventConverter()
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
self.bot_account_id = self.config["client_id"]
adapter_self = self
class MyClient(discord.Client):
async def on_message(self: discord.Client, message: discord.Message):
if message.author.id == self.user.id or message.author.bot:
return
lb_event = await adapter_self.event_converter.target2yiri(message)
await adapter_self.listeners[type(lb_event)](lb_event, adapter_self)
intents = discord.Intents.default()
intents.message_content = True
args = {}
if os.getenv("http_proxy"):
args["proxy"] = os.getenv("http_proxy")
self.bot = MyClient(intents=intents, **args)
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
msg_to_send, image_files = await self.message_converter.yiri2target(message)
assert isinstance(message_source.source_platform_object, discord.Message)
args = {
"content": msg_to_send,
}
if len(image_files) > 0:
args["files"] = image_files
if quote_origin:
args["reference"] = message_source.source_platform_object
if message.has(platform_message.At):
args["mention_author"] = True
await message_source.source_platform_object.channel.send(**args)
async def is_muted(self, group_id: int) -> bool:
return False
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
):
self.listeners.pop(event_type)
async def run_async(self):
async with self.bot:
await self.bot.start(self.config["token"], reconnect=True)
async def kill(self) -> bool:
await self.bot.close()
return True

View File

@@ -0,0 +1,285 @@
from __future__ import annotations
import gewechat_client
import typing
import asyncio
import traceback
import time
import re
import base64
import uuid
import json
import os
import copy
import datetime
import threading
import quart
import aiohttp
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...utils import image
class GewechatMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain
) -> list[dict]:
content_list = []
for component in message_chain:
if isinstance(component, platform_message.At):
content_list.append({"type": "at", "target": component.target})
elif isinstance(component, platform_message.Plain):
content_list.append({"type": "text", "content": component.text})
elif isinstance(component, platform_message.Image):
# content_list.append({"type": "image", "image_id": component.image_id})
pass
elif isinstance(component, platform_message.Forward):
for node in component.node_list:
content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain))
return content_list
@staticmethod
async def target2yiri(
message: dict,
bot_account_id: str
) -> platform_message.MessageChain:
if message["Data"]["MsgType"] == 1:
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉
regex = re.compile(r"^wxid_.*:")
line_split = message["Data"]["Content"]["string"].split("\n")
if len(line_split) > 0 and regex.match(line_split[0]):
message["Data"]["Content"]["string"] = "\n".join(line_split[1:])
at_string = f'@{bot_account_id}'
content_list = []
if at_string in message["Data"]["Content"]["string"]:
content_list.append(platform_message.At(target=bot_account_id))
content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, "", 1)))
else:
content_list = [platform_message.Plain(message["Data"]["Content"]["string"])]
return platform_message.MessageChain(content_list)
elif message["Data"]["MsgType"] == 3:
image_base64 = message["Data"]["ImgBuf"]["buffer"]
return platform_message.MessageChain(
[platform_message.Image(base64=f"data:image/jpeg;base64,{image_base64}")]
)
class GewechatEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(
event: platform_events.MessageEvent
) -> dict:
pass
@staticmethod
async def target2yiri(
event: dict,
bot_account_id: str
) -> platform_events.MessageEvent:
message_chain = await GewechatMessageConverter.target2yiri(copy.deepcopy(event), bot_account_id)
if not message_chain:
return None
if '@chatroom' in event["Data"]["FromUserName"]["string"]:
# 找出开头的 wxid_ 字符串,以:结尾
sender_wxid = event["Data"]["Content"]["string"].split(":")[0]
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=sender_wxid,
member_name=event["Data"]["FromUserName"]["string"],
permission=platform_entities.Permission.Member,
group=platform_entities.Group(
id=event["Data"]["FromUserName"]["string"],
name=event["Data"]["FromUserName"]["string"],
permission=platform_entities.Permission.Member,
),
special_title="",
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=message_chain,
time=event["Data"]["CreateTime"],
source_platform_object=event,
)
elif 'wxid_' in event["Data"]["FromUserName"]["string"]:
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event["Data"]["FromUserName"]["string"],
nickname=event["Data"]["FromUserName"]["string"],
remark='',
),
message_chain=message_chain,
time=event["Data"]["CreateTime"],
source_platform_object=event,
)
@adapter.adapter_class("gewechat")
class GewechatMessageSourceAdapter(adapter.MessageSourceAdapter):
bot: gewechat_client.GewechatClient
quart_app: quart.Quart
bot_account_id: str
config: dict
ap: app.Application
message_converter: GewechatMessageConverter = GewechatMessageConverter()
event_converter: GewechatEventConverter = GewechatEventConverter()
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
self.quart_app = quart.Quart(__name__)
@self.quart_app.route('/gewechat/callback', methods=['POST'])
async def gewechat_callback():
data = await quart.request.json
# print(json.dumps(data, indent=4, ensure_ascii=False))
if 'testMsg' in data:
return 'ok'
elif 'TypeName' in data and data['TypeName'] == 'AddMsg':
try:
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
except Exception as e:
traceback.print_exc()
if event.__class__ in self.listeners:
await self.listeners[event.__class__](event, self)
return 'ok'
async def send_message(
self,
target_type: str,
target_id: str,
message: platform_message.MessageChain
):
pass
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False
):
content_list = await self.message_converter.yiri2target(message)
ats = [item["target"] for item in content_list if item["type"] == "at"]
for msg in content_list:
if msg["type"] == "text":
if ats:
member_info = self.bot.get_chatroom_member_detail(
self.config["app_id"],
message_source.source_platform_object["Data"]["FromUserName"]["string"],
ats[::-1]
)["data"]
for member in member_info:
msg['content'] = f'@{member["nickName"]} {msg["content"]}'
self.bot.post_text(
app_id=self.config["app_id"],
to_wxid=message_source.source_platform_object["Data"]["FromUserName"]["string"],
content=msg["content"],
ats=','.join(ats)
)
async def is_muted(self, group_id: int) -> bool:
pass
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None]
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None]
):
pass
async def run_async(self):
if not self.config["token"]:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.config['gewechat_url']}/v2/api/tools/getTokenId",
json={"app_id": self.config["app_id"]}
) as response:
if response.status != 200:
raise Exception(f"获取gewechat token失败: {await response.text()}")
self.config["token"] = (await response.json())["data"]
self.bot = gewechat_client.GewechatClient(
f"{self.config['gewechat_url']}/v2/api",
self.config["token"]
)
app_id, error_msg = self.bot.login(self.config["app_id"])
if error_msg:
raise Exception(f"Gewechat 登录失败: {error_msg}")
self.config["app_id"] = app_id
self.ap.logger.info(f"Gewechat 登录成功app_id: {app_id}")
await self.ap.platform_mgr.write_back_config(self, self.config)
# 获取 nickname
profile = self.bot.get_profile(self.config["app_id"])
self.bot_account_id = profile["data"]["nickName"]
def thread_set_callback():
time.sleep(3)
ret = self.bot.set_callback(self.config["token"], self.config["callback_url"])
print('设置 Gewechat 回调:', ret)
threading.Thread(target=thread_set_callback).start()
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
await self.quart_app.run_task(
host='0.0.0.0',
port=self.config["port"],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool:
pass

View File

@@ -0,0 +1,407 @@
from __future__ import annotations
import lark_oapi
import typing
import asyncio
import traceback
import time
import re
import base64
import uuid
import json
import datetime
import aiohttp
import lark_oapi.ws.exception
from lark_oapi.api.im.v1 import *
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...utils import image
class LarkMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain, api_client: lark_oapi.Client
) -> typing.Tuple[list]:
message_elements = []
pending_paragraph = []
for msg in message_chain:
if isinstance(msg, platform_message.Plain):
pending_paragraph.append({"tag": "md", "text": msg.text})
elif isinstance(msg, platform_message.At):
pending_paragraph.append(
{"tag": "at", "user_id": msg.target, "style": []}
)
elif isinstance(msg, platform_message.AtAll):
pending_paragraph.append({"tag": "at", "user_id": "all", "style": []})
elif isinstance(msg, platform_message.Image):
image_bytes = None
if msg.base64:
image_bytes = base64.b64decode(msg.base64)
elif msg.url:
async with aiohttp.ClientSession() as session:
async with session.get(msg.url) as response:
image_bytes = await response.read()
elif msg.path:
with open(msg.path, "rb") as f:
image_bytes = f.read()
request: CreateImageRequest = (
CreateImageRequest.builder()
.request_body(
CreateImageRequestBody.builder()
.image_type("message")
.image(image_bytes)
.build()
)
.build()
)
response: CreateImageResponse = await api_client.im.v1.image.acreate(
request
)
if not response.success():
raise Exception(
f"client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}"
)
image_key = response.data.image_key
message_elements.append(pending_paragraph)
message_elements.append(
[
{
"tag": "img",
"image_key": image_key,
}
]
)
pending_paragraph = []
elif isinstance(msg, platform_message.Forward):
for node in msg.node_list:
message_elements.extend(await LarkMessageConverter.yiri2target(node.message_chain, api_client))
if pending_paragraph:
message_elements.append(pending_paragraph)
return message_elements
@staticmethod
async def target2yiri(
message: lark_oapi.api.im.v1.model.event_message.EventMessage,
api_client: lark_oapi.Client,
) -> platform_message.MessageChain:
message_content = json.loads(message.content)
lb_msg_list = []
msg_create_time = datetime.datetime.fromtimestamp(
int(message.create_time) / 1000
)
lb_msg_list.append(
platform_message.Source(id=message.message_id, time=msg_create_time)
)
if message.message_type == "text":
element_list = []
def text_element_recur(text_ele: dict) -> list[dict]:
if text_ele["text"] == "":
return []
at_pattern = re.compile(r"@_user_[\d]+")
at_matches = at_pattern.findall(text_ele["text"])
name_mapping = {}
for mathc in at_matches:
for mention in message.mentions:
if mention.key == mathc:
name_mapping[mathc] = mention.name
break
if len(name_mapping.keys()) == 0:
return [text_ele]
# 只处理第一个,剩下的递归处理
text_split = text_ele["text"].split(list(name_mapping.keys())[0])
new_list = []
left_text = text_split[0]
right_text = text_split[1]
new_list.extend(
text_element_recur({"tag": "text", "text": left_text, "style": []})
)
new_list.append(
{
"tag": "at",
"user_id": list(name_mapping.keys())[0],
"user_name": name_mapping[list(name_mapping.keys())[0]],
"style": [],
}
)
new_list.extend(
text_element_recur({"tag": "text", "text": right_text, "style": []})
)
return new_list
element_list = text_element_recur(
{"tag": "text", "text": message_content["text"], "style": []}
)
message_content = {"title": "", "content": element_list}
elif message.message_type == "post":
new_list = []
for ele in message_content["content"]:
if type(ele) is dict:
new_list.append(ele)
elif type(ele) is list:
new_list.extend(ele)
message_content["content"] = new_list
elif message.message_type == "image":
message_content["content"] = [
{"tag": "img", "image_key": message_content["image_key"], "style": []}
]
for ele in message_content["content"]:
if ele["tag"] == "text":
lb_msg_list.append(platform_message.Plain(text=ele["text"]))
elif ele["tag"] == "at":
lb_msg_list.append(platform_message.At(target=ele["user_name"]))
elif ele["tag"] == "img":
image_key = ele["image_key"]
request: GetMessageResourceRequest = (
GetMessageResourceRequest.builder()
.message_id(message.message_id)
.file_key(image_key)
.type("image")
.build()
)
response: GetMessageResourceResponse = (
await api_client.im.v1.message_resource.aget(request)
)
if not response.success():
raise Exception(
f"client.im.v1.message_resource.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}"
)
image_bytes = response.file.read()
image_base64 = base64.b64encode(image_bytes).decode()
image_format = response.raw.headers["content-type"]
lb_msg_list.append(
platform_message.Image(
base64=f"data:{image_format};base64,{image_base64}"
)
)
return platform_message.MessageChain(lb_msg_list)
class LarkEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(
event: platform_events.MessageEvent,
) -> lark_oapi.im.v1.P2ImMessageReceiveV1:
pass
@staticmethod
async def target2yiri(
event: lark_oapi.im.v1.P2ImMessageReceiveV1, api_client: lark_oapi.Client
) -> platform_events.Event:
message_chain = await LarkMessageConverter.target2yiri(
event.event.message, api_client
)
if event.event.message.chat_type == "p2p":
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event.event.sender.sender_id.open_id,
nickname=event.event.sender.sender_id.union_id,
remark="",
),
message_chain=message_chain,
time=event.event.message.create_time,
)
elif event.event.message.chat_type == "group":
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=event.event.sender.sender_id.open_id,
member_name=event.event.sender.sender_id.union_id,
permission=platform_entities.Permission.Member,
group=platform_entities.Group(
id=event.event.message.chat_id,
name="",
permission=platform_entities.Permission.Member,
),
special_title="",
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=message_chain,
time=event.event.message.create_time,
)
@adapter.adapter_class("lark")
class LarkMessageSourceAdapter(adapter.MessageSourceAdapter):
bot: lark_oapi.ws.Client
api_client: lark_oapi.Client
bot_account_id: str # 用于在流水线中识别at是否是本bot直接以bot_name作为标识
lark_tenant_key: str # 飞书企业key
message_converter: LarkMessageConverter = LarkMessageConverter()
event_converter: LarkEventConverter = LarkEventConverter()
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
] = {}
config: dict
ap: app.Application
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
lb_event = await self.event_converter.target2yiri(event, self.api_client)
await self.listeners[type(lb_event)](lb_event, self)
def sync_on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
asyncio.create_task(on_message(event))
event_handler = (
lark_oapi.EventDispatcherHandler.builder("", "")
.register_p2_im_message_receive_v1(sync_on_message)
.build()
)
self.bot_account_id = config["bot_name"]
self.bot = lark_oapi.ws.Client(
config["app_id"], config["app_secret"], event_handler=event_handler
)
self.api_client = (
lark_oapi.Client.builder()
.app_id(config["app_id"])
.app_secret(config["app_secret"])
.build()
)
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
# 不再需要了因为message_id已经被包含到message_chain中
# lark_event = await self.event_converter.yiri2target(message_source)
lark_message = await self.message_converter.yiri2target(
message, self.api_client
)
final_content = {
"zh_cn": {
"title": "",
"content": lark_message,
},
}
request: ReplyMessageRequest = (
ReplyMessageRequest.builder()
.message_id(message_source.message_chain.message_id)
.request_body(
ReplyMessageRequestBody.builder()
.content(json.dumps(final_content))
.msg_type("post")
.reply_in_thread(False)
.uuid(str(uuid.uuid4()))
.build()
)
.build()
)
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(
request
)
if not response.success():
raise Exception(
f"client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}"
)
async def is_muted(self, group_id: int) -> bool:
return False
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
],
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
],
):
self.listeners.pop(event_type)
async def run_async(self):
try:
await self.bot._connect()
except lark_oapi.ws.exception.ClientException as e:
raise e
except Exception as e:
await self.bot._disconnect()
if self.bot._auto_reconnect:
await self.bot._reconnect()
else:
raise e
async def kill(self) -> bool:
return False

View File

@@ -0,0 +1,155 @@
from __future__ import annotations
import typing
import asyncio
import traceback
import time
import datetime
from pkg.core import app
from pkg.platform.adapter import MessageSourceAdapter
from pkg.platform.types import events as platform_events, message as platform_message
import aiocqhttp
import aiohttp
from libs.official_account_api.oaevent import OAEvent
from pkg.platform.adapter import MessageSourceAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from libs.official_account_api.api import OAClient
from pkg.core import app
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...command.errors import ParamNotEnoughError
# 生成的ai回答
generated_content = {}
class OAMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(message_chain: platform_message.MessageChain):
for msg in message_chain:
if type(msg) is platform_message.Plain:
return msg.text
@staticmethod
async def target2yiri(message:str,message_id =-1):
yiri_msg_list = []
yiri_msg_list.append(
platform_message.Source(id=message_id, time=datetime.datetime.now())
)
yiri_msg_list.append(platform_message.Plain(text=message))
chain = platform_message.MessageChain(yiri_msg_list)
return chain
class OAEventConverter(adapter.EventConverter):
@staticmethod
async def target2yiri(event:OAEvent):
if event.type == "text":
yiri_chain = await OAMessageConverter.target2yiri(
event.message, event.message_id
)
friend = platform_entities.Friend(
id=event.user_id,
nickname=str(event.user_id),
remark="",
)
return platform_events.FriendMessage(
sender=friend, message_chain=yiri_chain, time=event.timestamp, source_platform_object=event
)
else:
return None
@adapter.adapter_class("officialaccount")
class OfficialAccountAdapter(adapter.MessageSourceAdapter):
bot : OAClient
ap : app.Application
bot_account_id: str
message_converter: OAMessageConverter = OAMessageConverter()
event_converter: OAEventConverter = OAEventConverter()
config: dict
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
required_keys = [
"token",
"EncodingAESKey",
"AppSecret",
"AppID",
]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
raise ParamNotEnoughError("企业微信缺少相关配置项,请查看文档或联系管理员")
self.bot = OAClient(
token=config['token'],
EncodingAESKey=config['EncodingAESKey'],
Appsecret=config['AppSecret'],
AppID=config['AppID'],
)
async def reply_message(self, message_source: platform_events.FriendMessage, message: platform_message.MessageChain, quote_origin: bool = False):
global generated_content
content = await OAMessageConverter.yiri2target(
message
)
generated_content[message_source.message_chain.message_id] = content
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
def register_listener(self, event_type: type, callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None]):
async def on_message(event: OAEvent):
self.bot_account_id = event.receiver_id
try:
return await callback(
await self.event_converter.target2yiri(event), self
)
except:
traceback.print_exc()
if event_type == platform_events.FriendMessage:
self.bot.on_message("text")(on_message)
elif event_type == platform_events.GroupMessage:
pass
async def run_async(self):
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
await self.bot.run_task(
host=self.config["host"],
port=self.config["port"],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool:
return False
async def unregister_listener(
self,
event_type: type,
callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -0,0 +1,256 @@
from __future__ import annotations
import typing
import asyncio
import traceback
import time
import datetime
import aiocqhttp
import aiohttp
from pkg.platform.adapter import MessageSourceAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from pkg.core import app
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...command.errors import ParamNotEnoughError
from libs.qq_official_api.api import QQOfficialClient
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
from ...utils import image
class QQOfficialMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(message_chain: platform_message.MessageChain):
content_list = []
#只实现了发文字
for msg in message_chain:
if type(msg) is platform_message.Plain:
content_list.append({
"type":"text",
"content":msg.text,
})
return content_list
@staticmethod
async def target2yiri(message:str,message_id:str,pic_url:str,content_type):
yiri_msg_list = []
yiri_msg_list.append(
platform_message.Source(id=message_id,time=datetime.datetime.now())
)
if pic_url is not None:
base64_url = await image.get_qq_official_image_base64(pic_url=pic_url,content_type=content_type)
yiri_msg_list.append(
platform_message.Image(base64=base64_url)
)
yiri_msg_list.append(platform_message.Plain(text=message))
chain = platform_message.MessageChain(yiri_msg_list)
return chain
class QQOfficialEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(event:platform_events.MessageEvent) -> QQOfficialEvent:
return event.source_platform_object
@staticmethod
async def target2yiri(event:QQOfficialEvent):
"""
QQ官方消息转换为LB对象
"""
yiri_chain = await QQOfficialMessageConverter.target2yiri(
message=event.content,message_id=event.d_id,pic_url=event.attachments,content_type=event.content_type
)
if event.t == 'C2C_MESSAGE_CREATE':
friend = platform_entities.Friend(
id = event.user_openid,
nickname = event.t,
remark = "",
)
return platform_events.FriendMessage(
sender = friend,message_chain = yiri_chain,time = event.timestamp,
source_platform_object=event
)
if event.t == 'DIRECT_MESSAGE_CREATE':
friend = platform_entities.Friend(
id = event.guild_id,
nickname = event.t,
remark = "",
)
return platform_events.FriendMessage(
sender = friend,message_chain = yiri_chain,
source_platform_object=event
)
if event.t == 'GROUP_AT_MESSAGE_CREATE':
yiri_chain.insert(0, platform_message.At(target="justbot"))
sender = platform_entities.GroupMember(
id = event.group_openid,
member_name= event.t,
permission= 'MEMBER',
group = platform_entities.Group(
id = 0,
name = 'MEMBER',
permission= platform_entities.Permission.Member
),
special_title='',
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0
)
time = event.timestamp
return platform_events.GroupMessage(
sender = sender,
message_chain=yiri_chain,
time = time,
source_platform_object=event
)
if event.t =='AT_MESSAGE_CREATE':
yiri_chain.insert(0, platform_message.At(target="justbot"))
sender = platform_entities.GroupMember(
id = event.channel_id,
member_name=event.t,
permission= 'MEMBER',
group = platform_entities.Group(
id = 0,
name = 'MEMBER',
permission=platform_entities.Permission.Member
),
special_title='',
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0
)
time = event.timestamp,
return platform_events.GroupMessage(
sender =sender,
message_chain = yiri_chain,
time = time,
source_platform_object=event
)
@adapter.adapter_class("qqofficial")
class QQOfficialAdapter(adapter.MessageSourceAdapter):
bot:QQOfficialClient
ap:app.Application
config:dict
bot_account_id:str
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
def __init__(self, config:dict, ap:app.Application):
self.config = config
self.ap = ap
required_keys = [
"appid",
"secret",
]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
raise ParamNotEnoughError("QQ官方机器人缺少相关配置项请查看文档或联系管理员")
self.bot = QQOfficialClient(
app_id=config["appid"],
secret=config["secret"],
token=config["token"],
)
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
qq_official_event = await QQOfficialEventConverter.yiri2target(
message_source,
)
content_list = await QQOfficialMessageConverter.yiri2target(message)
#私聊消息
if qq_official_event.t == 'C2C_MESSAGE_CREATE':
for content in content_list:
if content["type"] == 'text':
await self.bot.send_private_text_msg(qq_official_event.user_openid,content['content'],qq_official_event.d_id)
#群聊消息
if qq_official_event.t == 'GROUP_AT_MESSAGE_CREATE':
for content in content_list:
if content["type"] == 'text':
await self.bot.send_group_text_msg(qq_official_event.group_openid,content['content'],qq_official_event.d_id)
#频道群聊
if qq_official_event.t == 'AT_MESSAGE_CREATE':
for content in content_list:
if content["type"] == 'text':
await self.bot.send_channle_group_text_msg(qq_official_event.channel_id,content['content'],qq_official_event.d_id)
#频道私聊
if qq_official_event.t == 'DIRECT_MESSAGE_CREATE':
for content in content_list:
if content["type"] == 'text':
await self.bot.send_channle_private_text_msg(qq_official_event.guild_id,content['content'],qq_official_event.d_id)
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
],
):
async def on_message(event:QQOfficialEvent):
self.bot_account_id = "justbot"
try:
return await callback(
await self.event_converter.target2yiri(event),self
)
except:
traceback.print_exc()
if event_type == platform_events.FriendMessage:
self.bot.on_message("DIRECT_MESSAGE_CREATE")(on_message)
self.bot.on_message("C2C_MESSAGE_CREATE")(on_message)
elif event_type == platform_events.GroupMessage:
self.bot.on_message("GROUP_AT_MESSAGE_CREATE")(on_message)
self.bot.on_message("AT_MESSAGE_CREATE")(on_message)
async def run_async(self):
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
await self.bot.run_task(
host='0.0.0.0',
port=self.config["port"],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool:
return False
def unregister_listener(
self,
event_type: type,
callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -0,0 +1,247 @@
from __future__ import annotations
import typing
import asyncio
import traceback
import time
import datetime
import aiocqhttp
import aiohttp
from libs.wecom_api.api import WecomClient
from pkg.platform.adapter import MessageSourceAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from libs.wecom_api.wecomevent import WecomEvent
from pkg.core import app
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...command.errors import ParamNotEnoughError
from ...utils import image
class WecomMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain, bot: WecomClient
):
content_list = []
for msg in message_chain:
if type(msg) is platform_message.Plain:
content_list.append({
"type": "text",
"content": msg.text,
})
elif type(msg) is platform_message.Image:
content_list.append({
"type": "image",
"media_id": await bot.get_media_id(msg),
})
elif type(msg) is platform_message.Forward:
for node in msg.node_list:
content_list.extend((await WecomMessageConverter.yiri2target(node.message_chain, bot)))
else:
content_list.append({
"type": "text",
"content": str(msg),
})
return content_list
@staticmethod
async def target2yiri(message: str, message_id: int = -1):
yiri_msg_list = []
yiri_msg_list.append(
platform_message.Source(id=message_id, time=datetime.datetime.now())
)
yiri_msg_list.append(platform_message.Plain(text=message))
chain = platform_message.MessageChain(yiri_msg_list)
return chain
@staticmethod
async def target2yiri_image(picurl: str, message_id: int = -1):
yiri_msg_list = []
yiri_msg_list.append(
platform_message.Source(id=message_id, time=datetime.datetime.now())
)
image_base64, image_format = await image.get_wecom_image_base64(pic_url=picurl)
yiri_msg_list.append(platform_message.Image(base64=f"data:image/{image_format};base64,{image_base64}"))
chain = platform_message.MessageChain(yiri_msg_list)
return chain
class WecomEventConverter:
@staticmethod
async def yiri2target(
event: platform_events.Event, bot_account_id: int, bot: WecomClient
) -> WecomEvent:
# only for extracting user information
if type(event) is platform_events.GroupMessage:
pass
if type(event) is platform_events.FriendMessage:
payload = {
"MsgType": "text",
"Content": '',
"FromUserName": event.sender.id,
"ToUserName": bot_account_id,
"CreateTime": int(datetime.datetime.now().timestamp()),
"AgentID": event.sender.nickname,
}
wecom_event = WecomEvent.from_payload(payload=payload)
if not wecom_event:
raise ValueError("无法从 message_data 构造 WecomEvent 对象")
return wecom_event
@staticmethod
async def target2yiri(event: WecomEvent):
"""
将 WecomEvent 转换为平台的 FriendMessage 对象。
Args:
event (WecomEvent): 企业微信事件。
Returns:
platform_events.FriendMessage: 转换后的 FriendMessage 对象。
"""
# 转换消息链
if event.type == "text":
yiri_chain = await WecomMessageConverter.target2yiri(
event.message, event.message_id
)
friend = platform_entities.Friend(
id=f"u{event.user_id}",
nickname=str(event.agent_id),
remark="",
)
return platform_events.FriendMessage(
sender=friend, message_chain=yiri_chain, time=event.timestamp
)
elif event.type == "image":
friend = platform_entities.Friend(
id=event.user_id,
nickname=str(event.agent_id),
remark="",
)
yiri_chain = await WecomMessageConverter.target2yiri_image(
picurl=event.picurl, message_id=event.message_id
)
return platform_events.FriendMessage(
sender=friend, message_chain=yiri_chain, time=event.timestamp
)
@adapter.adapter_class("wecom")
class WecomeAdapter(adapter.MessageSourceAdapter):
bot: WecomClient
ap: app.Application
bot_account_id: str
message_converter: WecomMessageConverter = WecomMessageConverter()
event_converter: WecomEventConverter = WecomEventConverter()
config: dict
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
required_keys = [
"corpid",
"secret",
"token",
"EncodingAESKey",
"contacts_secret",
]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
raise ParamNotEnoughError("企业微信缺少相关配置项,请查看文档或联系管理员")
self.bot = WecomClient(
corpid=config["corpid"],
secret=config["secret"],
token=config["token"],
EncodingAESKey=config["EncodingAESKey"],
contacts_secret=config["contacts_secret"],
)
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
Wecom_event = await WecomEventConverter.yiri2target(
message_source, self.bot_account_id, self.bot
)
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
fixed_user_id = Wecom_event.user_id
# 删掉开头的u
fixed_user_id = fixed_user_id[1:]
for content in content_list:
if content["type"] == "text":
await self.bot.send_private_msg(fixed_user_id, Wecom_event.agent_id, content["content"])
elif content["type"] == "image":
await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content["media_id"])
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
],
):
async def on_message(event: WecomEvent):
self.bot_account_id = event.receiver_id
try:
return await callback(
await self.event_converter.target2yiri(event), self
)
except:
traceback.print_exc()
if event_type == platform_events.FriendMessage:
self.bot.on_message("text")(on_message)
self.bot.on_message("image")(on_message)
elif event_type == platform_events.GroupMessage:
pass
async def run_async(self):
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
await self.bot.run_task(
host=self.config["host"],
port=self.config["port"],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool:
return False
async def unregister_listener(
self,
event_type: type,
callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -25,7 +25,7 @@ class Entity(pydantic.BaseModel):
class Friend(Entity): class Friend(Entity):
"""好友。""" """好友。"""
id: int id: typing.Union[int, str]
"""QQ 号。""" """QQ 号。"""
nickname: typing.Optional[str] nickname: typing.Optional[str]
"""昵称。""" """昵称。"""
@@ -38,6 +38,7 @@ class Friend(Entity):
return self.nickname or self.remark or '' return self.nickname or self.remark or ''
class Permission(str, Enum): class Permission(str, Enum):
"""群成员身份权限。""" """群成员身份权限。"""
Member = "MEMBER" Member = "MEMBER"
@@ -52,7 +53,7 @@ class Permission(str, Enum):
class Group(Entity): class Group(Entity):
"""群。""" """群。"""
id: int id: typing.Union[int, str]
"""群号。""" """群号。"""
name: str name: str
"""群名称。""" """群名称。"""
@@ -67,7 +68,7 @@ class Group(Entity):
class GroupMember(Entity): class GroupMember(Entity):
"""群成员。""" """群成员。"""
id: int id: typing.Union[int, str]
"""QQ 号。""" """QQ 号。"""
member_name: str member_name: str
"""群成员名称。""" """群成员名称。"""
@@ -92,7 +93,7 @@ class GroupMember(Entity):
class Client(Entity): class Client(Entity):
"""来自其他客户端的用户。""" """来自其他客户端的用户。"""
id: int id: typing.Union[int, str]
"""识别 id。""" """识别 id。"""
platform: str platform: str
"""来源平台。""" """来源平台。"""
@@ -105,7 +106,7 @@ class Client(Entity):
class Subject(pydantic.BaseModel): class Subject(pydantic.BaseModel):
"""另一种实体类型表示。""" """另一种实体类型表示。"""
id: int id: typing.Union[int, str]
"""QQ 号或群号。""" """QQ 号或群号。"""
kind: typing.Literal['Friend', 'Group', 'Stranger'] kind: typing.Literal['Friend', 'Group', 'Stranger']
"""类型。""" """类型。"""

View File

@@ -72,6 +72,11 @@ class MessageEvent(Event):
message_chain: platform_message.MessageChain message_chain: platform_message.MessageChain
"""消息内容。""" """消息内容。"""
source_platform_object: typing.Optional[typing.Any] = None
"""原消息平台对象。
供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息,
那么可以将其存到这个字段以供之后取出使用。"""
class FriendMessage(MessageEvent): class FriendMessage(MessageEvent):
"""好友消息。 """好友消息。

View File

@@ -460,7 +460,7 @@ class Source(MessageComponent):
"""源。包含消息的基本信息。""" """源。包含消息的基本信息。"""
type: str = "Source" type: str = "Source"
"""消息组件类型。""" """消息组件类型。"""
id: int id: typing.Union[int, str]
"""消息的识别号用于引用回复Source 类型永远为 MessageChain 的第一个元素)。""" """消息的识别号用于引用回复Source 类型永远为 MessageChain 的第一个元素)。"""
time: datetime time: datetime
"""消息时间。""" """消息时间。"""
@@ -485,11 +485,11 @@ class Quote(MessageComponent):
"""消息组件类型。""" """消息组件类型。"""
id: typing.Optional[int] = None id: typing.Optional[int] = None
"""被引用回复的原消息的 message_id。""" """被引用回复的原消息的 message_id。"""
group_id: typing.Optional[int] = None group_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息所接收的群号当为好友消息时为0。""" """被引用回复的原消息所接收的群号当为好友消息时为0。"""
sender_id: typing.Optional[int] = None sender_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的发送者的QQ号。""" """被引用回复的原消息的发送者的QQ号。"""
target_id: typing.Optional[int] = None target_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的接收者者的QQ号或群号""" """被引用回复的原消息的接收者者的QQ号或群号"""
origin: MessageChain origin: MessageChain
"""被引用回复的原消息的消息链对象。""" """被引用回复的原消息的消息链对象。"""
@@ -503,7 +503,7 @@ class At(MessageComponent):
"""At某人。""" """At某人。"""
type: str = "At" type: str = "At"
"""消息组件类型。""" """消息组件类型。"""
target: int target: typing.Union[int, str]
"""群员 QQ 号。""" """群员 QQ 号。"""
display: typing.Optional[str] = None display: typing.Optional[str] = None
"""At时显示的文字发送消息时无效自动使用群名片。""" """At时显示的文字发送消息时无效自动使用群名片。"""
@@ -749,7 +749,7 @@ class Voice(MessageComponent):
class ForwardMessageNode(pydantic.BaseModel): class ForwardMessageNode(pydantic.BaseModel):
"""合并转发中的一条消息。""" """合并转发中的一条消息。"""
sender_id: typing.Optional[int] = None sender_id: typing.Optional[typing.Union[int, str]] = None
"""发送人QQ号。""" """发送人QQ号。"""
sender_name: typing.Optional[str] = None sender_name: typing.Optional[str] = None
"""显示名称。""" """显示名称。"""
@@ -785,10 +785,20 @@ class ForwardMessageNode(pydantic.BaseModel):
) )
class ForwardMessageDiaplay(pydantic.BaseModel):
title: str = "群聊的聊天记录"
brief: str = "[聊天记录]"
source: str = "聊天记录"
preview: typing.List[str] = []
summary: str = "查看x条转发消息"
class Forward(MessageComponent): class Forward(MessageComponent):
"""合并转发。""" """合并转发。"""
type: str = "Forward" type: str = "Forward"
"""消息组件类型。""" """消息组件类型。"""
display: ForwardMessageDiaplay
"""显示信息"""
node_list: typing.List[ForwardMessageNode] node_list: typing.List[ForwardMessageNode]
"""转发消息节点列表。""" """转发消息节点列表。"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -9,6 +9,7 @@ from . import events
from ..provider.tools import entities as tools_entities from ..provider.tools import entities as tools_entities
from ..core import app from ..core import app
from ..platform.types import message as platform_message from ..platform.types import message as platform_message
from ..platform import adapter as platform_adapter
def register( def register(
@@ -113,6 +114,37 @@ class APIHost:
async def initialize(self): async def initialize(self):
pass pass
# ========== 插件可调用的 API主程序API ==========
def get_platform_adapters(self) -> list[platform_adapter.MessageSourceAdapter]:
"""获取已启用的消息平台适配器列表
Returns:
list[platform.adapter.MessageSourceAdapter]: 已启用的消息平台适配器列表
"""
return self.ap.platform_mgr.adapters
async def send_active_message(
self,
adapter: platform_adapter.MessageSourceAdapter,
target_type: str,
target_id: str,
message: platform_message.MessageChain,
):
"""发送主动消息
Args:
adapter (platform.adapter.MessageSourceAdapter): 消息平台适配器对象,调用 host.get_platform_adapters() 获取并取用其中某个
target_type (str): 目标类型,`person`或`group`
target_id (str): 目标ID
message (platform.types.MessageChain): 消息链
"""
await adapter.send_message(
target_type=target_type,
target_id=target_id,
message=message,
)
def require_ver( def require_ver(
self, self,
ge: str, ge: str,
@@ -127,16 +159,16 @@ class APIHost:
Returns: Returns:
bool: 是否满足要求, False时为无法获取版本号True时为满足要求报错为不满足要求 bool: 是否满足要求, False时为无法获取版本号True时为满足要求报错为不满足要求
""" """
qchatgpt_version = "" langbot_version = ""
try: try:
qchatgpt_version = self.ap.ver_mgr.get_current_version() # 从updater模块获取版本号 langbot_version = self.ap.ver_mgr.get_current_version() # 从updater模块获取版本号
except: except:
return False return False
if self.ap.ver_mgr.compare_version_str(qchatgpt_version, ge) < 0 or \ if self.ap.ver_mgr.compare_version_str(langbot_version, ge) < 0 or \
(self.ap.ver_mgr.compare_version_str(qchatgpt_version, le) > 0): (self.ap.ver_mgr.compare_version_str(langbot_version, le) > 0):
raise Exception("LangBot 版本不满足要求,某些功能(可能是由插件提供的)无法正常使用。(要求版本:{}-{},但当前版本:{}".format(ge, le, qchatgpt_version)) raise Exception("LangBot 版本不满足要求,某些功能(可能是由插件提供的)无法正常使用。(要求版本:{}-{},但当前版本:{}".format(ge, le, langbot_version))
return True return True

View File

@@ -25,10 +25,10 @@ class PersonMessageReceived(BaseEventModel):
launcher_type: str launcher_type: str
"""发起对象类型(group/person)""" """发起对象类型(group/person)"""
launcher_id: int launcher_id: typing.Union[int, str]
"""发起对象ID(群号/QQ号)""" """发起对象ID(群号/QQ号)"""
sender_id: int sender_id: typing.Union[int, str]
"""发送者ID(QQ号)""" """发送者ID(QQ号)"""
message_chain: platform_message.MessageChain message_chain: platform_message.MessageChain
@@ -39,9 +39,9 @@ class GroupMessageReceived(BaseEventModel):
launcher_type: str launcher_type: str
launcher_id: int launcher_id: typing.Union[int, str]
sender_id: int sender_id: typing.Union[int, str]
message_chain: platform_message.MessageChain message_chain: platform_message.MessageChain
@@ -51,9 +51,9 @@ class PersonNormalMessageReceived(BaseEventModel):
launcher_type: str launcher_type: str
launcher_id: int launcher_id: typing.Union[int, str]
sender_id: int sender_id: typing.Union[int, str]
text_message: str text_message: str
@@ -69,9 +69,9 @@ class PersonCommandSent(BaseEventModel):
launcher_type: str launcher_type: str
launcher_id: int launcher_id: typing.Union[int, str]
sender_id: int sender_id: typing.Union[int, str]
command: str command: str
@@ -93,9 +93,9 @@ class GroupNormalMessageReceived(BaseEventModel):
launcher_type: str launcher_type: str
launcher_id: int launcher_id: typing.Union[int, str]
sender_id: int sender_id: typing.Union[int, str]
text_message: str text_message: str
@@ -111,9 +111,9 @@ class GroupCommandSent(BaseEventModel):
launcher_type: str launcher_type: str
launcher_id: int launcher_id: typing.Union[int, str]
sender_id: int sender_id: typing.Union[int, str]
command: str command: str
@@ -135,9 +135,9 @@ class NormalMessageResponded(BaseEventModel):
launcher_type: str launcher_type: str
launcher_id: int launcher_id: typing.Union[int, str]
sender_id: int sender_id: typing.Union[int, str]
session: core_entities.Session session: core_entities.Session
"""会话对象""" """会话对象"""

View File

@@ -38,6 +38,8 @@ class ContentElement(pydantic.BaseModel):
image_url: typing.Optional[ImageURLContentObject] = None image_url: typing.Optional[ImageURLContentObject] = None
image_base64: typing.Optional[str] = None
def __str__(self): def __str__(self):
if self.type == 'text': if self.type == 'text':
return self.text return self.text
@@ -53,6 +55,10 @@ class ContentElement(pydantic.BaseModel):
@classmethod @classmethod
def from_image_url(cls, image_url: str): def from_image_url(cls, image_url: str):
return cls(type='image_url', image_url=ImageURLContentObject(url=image_url)) return cls(type='image_url', image_url=ImageURLContentObject(url=image_url))
@classmethod
def from_image_base64(cls, image_base64: str):
return cls(type='image_base64', image_base64=image_base64)
class Message(pydantic.BaseModel): class Message(pydantic.BaseModel):

View File

@@ -6,7 +6,7 @@ from . import entities, requester
from ...core import app from ...core import app
from . import token from . import token
from .requesters import chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl
FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list" FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list"
@@ -109,4 +109,4 @@ class ModelManager:
self.model_list.append(model_info) self.model_list.append(model_info)
except Exception as e: except Exception as e:
self.ap.logger.error(f"初始化模型 {model['name']} 失败: {e} ,请检查配置文件") self.ap.logger.error(f"初始化模型 {model['name']} 失败: {type(e)} {e} ,请检查配置文件")

View File

@@ -48,6 +48,7 @@ class LLMAPIRequester(metaclass=abc.ABCMeta):
@abc.abstractmethod @abc.abstractmethod
async def call( async def call(
self, self,
query: core_entities.Query,
model: modelmgr_entities.LLMModelInfo, model: modelmgr_entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,

View File

@@ -2,8 +2,10 @@ from __future__ import annotations
import typing import typing
import traceback import traceback
import base64
import anthropic import anthropic
import httpx
from .. import entities, errors, requester from .. import entities, errors, requester
@@ -21,15 +23,24 @@ class AnthropicMessages(requester.LLMAPIRequester):
client: anthropic.AsyncAnthropic client: anthropic.AsyncAnthropic
async def initialize(self): async def initialize(self):
httpx_client = anthropic._base_client.AsyncHttpxClientWrapper(
base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'],
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=typing.cast(httpx.Timeout, self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout']),
limits=anthropic._constants.DEFAULT_CONNECTION_LIMITS,
follow_redirects=True,
trust_env=True,
)
self.client = anthropic.AsyncAnthropic( self.client = anthropic.AsyncAnthropic(
api_key="", api_key="",
base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'], http_client=httpx_client,
timeout=self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout'],
proxies=self.ap.proxy_mgr.get_forward_proxies()
) )
async def call( async def call(
self, self,
query: core_entities.Query,
model: entities.LLMModelInfo, model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,
@@ -61,24 +72,20 @@ class AnthropicMessages(requester.LLMAPIRequester):
if isinstance(m.content, str) and m.content.strip() != "": if isinstance(m.content, str) and m.content.strip() != "":
req_messages.append(m.dict(exclude_none=True)) req_messages.append(m.dict(exclude_none=True))
elif isinstance(m.content, list): elif isinstance(m.content, list):
# m.content = [
# c for c in m.content if c.type == "text"
# ]
# if len(m.content) > 0:
# req_messages.append(m.dict(exclude_none=True))
msg_dict = m.dict(exclude_none=True) msg_dict = m.dict(exclude_none=True)
for i, ce in enumerate(m.content): for i, ce in enumerate(m.content):
if ce.type == "image_url":
base64_image, image_format = await image.qq_image_url_to_base64(ce.image_url.url) if ce.type == "image_base64":
image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
alter_image_ele = { alter_image_ele = {
"type": "image", "type": "image",
"source": { "source": {
"type": "base64", "type": "base64",
"media_type": f"image/{image_format}", "media_type": f"image/{image_format}",
"data": base64_image "data": image_b64
} }
} }
msg_dict["content"][i] = alter_image_ele msg_dict["content"][i] = alter_image_ele

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from . import chatcmpl
from .. import requester
from ....core import app
@requester.requester_class("bailian-chat-completions")
class BailianChatCompletions(chatcmpl.OpenAIChatCompletions):
"""阿里云百炼大模型平台 ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['bailian-chat-completions']

View File

@@ -8,6 +8,7 @@ from typing import AsyncGenerator
import openai import openai
import openai.types.chat.chat_completion as chat_completion import openai.types.chat.chat_completion as chat_completion
import openai.types.chat.chat_completion_message_tool_call as chat_completion_message_tool_call
import httpx import httpx
import aiohttp import aiohttp
import async_lru import async_lru
@@ -39,7 +40,8 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
base_url=self.requester_cfg['base-url'], base_url=self.requester_cfg['base-url'],
timeout=self.requester_cfg['timeout'], timeout=self.requester_cfg['timeout'],
http_client=httpx.AsyncClient( http_client=httpx.AsyncClient(
proxies=self.ap.proxy_mgr.get_forward_proxies() trust_env=True,
timeout=self.requester_cfg['timeout']
) )
) )
@@ -47,7 +49,70 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
self, self,
args: dict, args: dict,
) -> chat_completion.ChatCompletion: ) -> chat_completion.ChatCompletion:
return await self.client.chat.completions.create(**args) args["stream"] = True
chunk = None
pending_content = ""
tool_calls = []
resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args)
async for chunk in resp_gen:
# print(chunk)
if not chunk or not chunk.id or not chunk.choices or not chunk.choices[0] or not chunk.choices[0].delta:
continue
if chunk.choices[0].delta.content is not None:
pending_content += chunk.choices[0].delta.content
if chunk.choices[0].delta.tool_calls is not None:
for tool_call in chunk.choices[0].delta.tool_calls:
for tc in tool_calls:
if tc.index == tool_call.index:
tc.function.arguments += tool_call.function.arguments
break
else:
tool_calls.append(tool_call)
if chunk.choices[0].finish_reason is not None:
break
real_tool_calls = []
for tc in tool_calls:
function = chat_completion_message_tool_call.Function(
name=tc.function.name,
arguments=tc.function.arguments
)
real_tool_calls.append(chat_completion_message_tool_call.ChatCompletionMessageToolCall(
id=tc.id,
function=function,
type="function"
))
return chat_completion.ChatCompletion(
id=chunk.id,
object="chat.completion",
created=chunk.created,
choices=[
chat_completion.Choice(
index=0,
message=chat_completion.ChatCompletionMessage(
role="assistant",
content=pending_content,
tool_calls=real_tool_calls if len(real_tool_calls) > 0 else None
),
finish_reason=chunk.choices[0].finish_reason if hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason is not None else 'stop',
logprobs=chunk.choices[0].logprobs,
)
],
model=chunk.model,
service_tier=chunk.service_tier if hasattr(chunk, 'service_tier') else None,
system_fingerprint=chunk.system_fingerprint if hasattr(chunk, 'system_fingerprint') else None,
usage=chunk.usage if hasattr(chunk, 'usage') else None
) if chunk else None
async def _make_msg( async def _make_msg(
self, self,
@@ -65,6 +130,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
async def _closure( async def _closure(
self, self,
query: core_entities.Query,
req_messages: list[dict], req_messages: list[dict],
use_model: entities.LLMModelInfo, use_model: entities.LLMModelInfo,
use_funcs: list[tools_entities.LLMFunction] = None, use_funcs: list[tools_entities.LLMFunction] = None,
@@ -87,8 +153,12 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
for msg in messages: for msg in messages:
if 'content' in msg and isinstance(msg["content"], list): if 'content' in msg and isinstance(msg["content"], list):
for me in msg["content"]: for me in msg["content"]:
if me["type"] == "image_url": if me["type"] == "image_base64":
me["image_url"]['url'] = await self.get_base64_str(me["image_url"]['url']) me["image_url"] = {
"url": me["image_base64"]
}
me["type"] = "image_url"
del me["image_base64"]
args["messages"] = messages args["messages"] = messages
@@ -102,6 +172,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
async def call( async def call(
self, self,
query: core_entities.Query,
model: entities.LLMModelInfo, model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,
@@ -118,7 +189,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
req_messages.append(msg_dict) req_messages.append(msg_dict)
try: try:
return await self._closure(req_messages, model, funcs) return await self._closure(query=query, req_messages=req_messages, use_model=model, use_funcs=funcs)
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise errors.RequesterError('请求超时') raise errors.RequesterError('请求超时')
except openai.BadRequestError as e: except openai.BadRequestError as e:
@@ -134,11 +205,3 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}')
except openai.APIError as e: except openai.APIError as e:
raise errors.RequesterError(f'请求错误: {e.message}') raise errors.RequesterError(f'请求错误: {e.message}')
@async_lru.alru_cache(maxsize=128)
async def get_base64_str(
self,
original_url: str,
) -> str:
base64_image, image_format = await image.qq_image_url_to_base64(original_url)
return f"data:image/{image_format};base64,{base64_image}"

View File

@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
from ....core import app
from . import chatcmpl from . import chatcmpl
from .. import entities, errors, requester from .. import entities, errors, requester
from ....core import entities as core_entities, app from ....core import entities as core_entities, app
@@ -19,6 +17,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
async def _closure( async def _closure(
self, self,
query: core_entities.Query,
req_messages: list[dict], req_messages: list[dict],
use_model: entities.LLMModelInfo, use_model: entities.LLMModelInfo,
use_funcs: list[tools_entities.LLMFunction] = None, use_funcs: list[tools_entities.LLMFunction] = None,
@@ -47,6 +46,9 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
# 发送请求 # 发送请求
resp = await self._req(args) resp = await self._req(args)
if resp is None:
raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常')
# 处理请求结果 # 处理请求结果
message = await self._make_msg(resp) message = await self._make_msg(resp)

View File

@@ -8,7 +8,7 @@ import typing
from . import chatcmpl from . import chatcmpl
from .. import entities, errors, requester from .. import entities, errors, requester
from ....core import app from ....core import app, entities as core_entities
from ... import entities as llm_entities from ... import entities as llm_entities
from ...tools import entities as tools_entities from ...tools import entities as tools_entities
from .. import entities as modelmgr_entities from .. import entities as modelmgr_entities
@@ -24,6 +24,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
async def _closure( async def _closure(
self, self,
query: core_entities.Query,
req_messages: list[dict], req_messages: list[dict],
use_model: entities.LLMModelInfo, use_model: entities.LLMModelInfo,
use_funcs: list[tools_entities.LLMFunction] = None, use_funcs: list[tools_entities.LLMFunction] = None,

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from . import chatcmpl
from .. import requester
from ....core import app
@requester.requester_class("lmstudio-chat-completions")
class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions):
"""LMStudio ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['lmstudio-chat-completions']

View File

@@ -19,6 +19,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
async def _closure( async def _closure(
self, self,
query: core_entities.Query,
req_messages: list[dict], req_messages: list[dict],
use_model: entities.LLMModelInfo, use_model: entities.LLMModelInfo,
use_funcs: list[tools_entities.LLMFunction] = None, use_funcs: list[tools_entities.LLMFunction] = None,

View File

@@ -4,6 +4,9 @@ import asyncio
import os import os
import typing import typing
from typing import Union, Mapping, Any, AsyncIterator from typing import Union, Mapping, Any, AsyncIterator
import uuid
import json
import base64
import async_lru import async_lru
import ollama import ollama
@@ -11,7 +14,7 @@ import ollama
from .. import entities, errors, requester from .. import entities, errors, requester
from ... import entities as llm_entities from ... import entities as llm_entities
from ...tools import entities as tools_entities from ...tools import entities as tools_entities
from ....core import app from ....core import app, entities as core_entities
from ....utils import image from ....utils import image
REQUESTER_NAME: str = "ollama-chat" REQUESTER_NAME: str = "ollama-chat"
@@ -41,7 +44,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
**args **args
) )
async def _closure(self, req_messages: list[dict], use_model: entities.LLMModelInfo, async def _closure(self, query: core_entities.Query, req_messages: list[dict], use_model: entities.LLMModelInfo,
user_funcs: list[tools_entities.LLMFunction] = None) -> ( user_funcs: list[tools_entities.LLMFunction] = None) -> (
llm_entities.Message): llm_entities.Message):
args: Any = self.request_cfg['args'].copy() args: Any = self.request_cfg['args'].copy()
@@ -55,30 +58,59 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
for me in msg["content"]: for me in msg["content"]:
if me["type"] == "text": if me["type"] == "text":
text_content.append(me["text"]) text_content.append(me["text"])
elif me["type"] == "image_url": elif me["type"] == "image_base64":
image_url = await self.get_base64_str(me["image_url"]['url']) image_urls.append(me["image_base64"])
image_urls.append(image_url)
msg["content"] = "\n".join(text_content) msg["content"] = "\n".join(text_content)
msg["images"] = [url.split(',')[1] for url in image_urls] msg["images"] = [url.split(',')[1] for url in image_urls]
if 'tool_calls' in msg: # LangBot 内部以 str 存储 tool_calls 的参数,这里需要转换为 dict
for tool_call in msg['tool_calls']:
tool_call['function']['arguments'] = json.loads(tool_call['function']['arguments'])
args["messages"] = messages args["messages"] = messages
resp: Mapping[str, Any] | AsyncIterator[Mapping[str, Any]] = await self._req(args) args["tools"] = []
if user_funcs:
tools = await self.ap.tool_mgr.generate_tools_for_openai(user_funcs)
if tools:
args["tools"] = tools
resp = await self._req(args)
message: llm_entities.Message = await self._make_msg(resp) message: llm_entities.Message = await self._make_msg(resp)
return message return message
async def _make_msg( async def _make_msg(
self, self,
chat_completions: Union[Mapping[str, Any], AsyncIterator[Mapping[str, Any]]]) -> llm_entities.Message: chat_completions: ollama.ChatResponse) -> llm_entities.Message:
message: Any = chat_completions.pop('message', None) message: ollama.Message = chat_completions.message
if message is None: if message is None:
raise ValueError("chat_completions must contain a 'message' field") raise ValueError("chat_completions must contain a 'message' field")
message.update(chat_completions) ret_msg: llm_entities.Message = None
ret_msg: llm_entities.Message = llm_entities.Message(**message)
if message.content is not None:
ret_msg = llm_entities.Message(
role="assistant",
content=message.content
)
if message.tool_calls is not None and len(message.tool_calls) > 0:
tool_calls: list[llm_entities.ToolCall] = []
for tool_call in message.tool_calls:
tool_calls.append(llm_entities.ToolCall(
id=uuid.uuid4().hex,
type="function",
function=llm_entities.FunctionCall(
name=tool_call.function.name,
arguments=json.dumps(tool_call.function.arguments)
)
))
ret_msg.tool_calls = tool_calls
return ret_msg return ret_msg
async def call( async def call(
self, self,
query: core_entities.Query,
model: entities.LLMModelInfo, model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message], messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None, funcs: typing.List[tools_entities.LLMFunction] = None,
@@ -92,14 +124,6 @@ class OllamaChatCompletions(requester.LLMAPIRequester):
msg_dict["content"] = "\n".join(part["text"] for part in content) msg_dict["content"] = "\n".join(part["text"] for part in content)
req_messages.append(msg_dict) req_messages.append(msg_dict)
try: try:
return await self._closure(req_messages, model) return await self._closure(query, req_messages, model, funcs)
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise errors.RequesterError('请求超时') raise errors.RequesterError('请求超时')
@async_lru.alru_cache(maxsize=128)
async def get_base64_str(
self,
original_url: str,
) -> str:
base64_image, image_format = await image.qq_image_url_to_base64(original_url)
return f"data:image/{image_format};base64,{base64_image}"

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from . import chatcmpl
from .. import requester
from ....core import app
@requester.requester_class("siliconflow-chat-completions")
class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions):
"""SiliconFlow ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['siliconflow-chat-completions']

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from . import chatcmpl
from .. import requester
from ....core import app
@requester.requester_class("xai-chat-completions")
class XaiChatCompletions(chatcmpl.OpenAIChatCompletions):
"""xAI ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['xai-chat-completions']

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from ....core import app
from . import chatcmpl
from .. import requester
@requester.requester_class("zhipuai-chat-completions")
class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions):
"""智谱AI ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['zhipuai-chat-completions']

View File

@@ -5,6 +5,7 @@ from ..core import app
from .runners import localagent from .runners import localagent
from .runners import difysvapi from .runners import difysvapi
from .runners import dashscopeapi
class RunnerManager: class RunnerManager:
@@ -22,6 +23,8 @@ class RunnerManager:
self.using_runner = r(self.ap) self.using_runner = r(self.ap)
await self.using_runner.initialize() await self.using_runner.initialize()
break break
else:
raise ValueError(f"未找到请求运行器: {self.ap.provider_cfg.data['runner']}")
def get_runner(self) -> runner.RequestRunner: def get_runner(self) -> runner.RequestRunner:
return self.using_runner return self.using_runner

View File

@@ -0,0 +1,236 @@
from __future__ import annotations
import typing
import json
import base64
import re
import dashscope
from .. import runner
from ...core import entities as core_entities
from .. import entities as llm_entities
from ...utils import image
class DashscopeAPIError(Exception):
"""Dashscope API 请求失败"""
def __init__(self, message: str):
self.message = message
super().__init__(self.message)
@runner.runner_class("dashscope-app-api")
class DashScopeAPIRunner(runner.RequestRunner):
"阿里云百炼DashsscopeAPI对话请求器"
# 运行器内部使用的配置
app_type: str # 应用类型
app_id: str # 应用ID
api_key: str # API Key
references_quote: str # 引用资料提示当展示回答来源功能开启时这个变量会作为引用资料名前的提示可在provider.json中配置
biz_params: dict = {} # 工作流应用参数(仅在工作流应用中生效)
async def initialize(self):
"""初始化"""
valid_app_types = ["agent", "workflow"]
self.app_type = self.ap.provider_cfg.data["dashscope-app-api"]["app-type"]
#检查配置文件中使用的应用类型是否支持
if (self.app_type not in valid_app_types):
raise DashscopeAPIError(
f"不支持的 Dashscope 应用类型: {self.app_type}"
)
#初始化Dashscope 参数配置
self.app_id = self.ap.provider_cfg.data["dashscope-app-api"][self.app_type]["app-id"]
self.api_key = self.ap.provider_cfg.data["dashscope-app-api"]["api-key"]
self.references_quote = self.ap.provider_cfg.data["dashscope-app-api"][self.app_type]["references_quote"]
self.biz_params = self.ap.provider_cfg.data["dashscope-app-api"]["workflow"]["biz_params"]
def _replace_references(self, text, references_dict):
"""阿里云百炼平台的自定义应用支持资料引用,此函数可以将引用标签替换为参考资料"""
# 匹配 <ref>[index_id]</ref> 形式的字符串
pattern = re.compile(r'<ref>\[(.*?)\]</ref>')
def replacement(match):
# 获取引用编号
ref_key = match.group(1)
if ref_key in references_dict:
# 如果有对应的参考资料按照provider.json中的reference_quote返回提示来自哪个参考资料文件
return f"({self.references_quote} {references_dict[ref_key]})"
else:
# 如果没有对应的参考资料,保留原样
return match.group(0)
# 使用 re.sub() 进行替换
return pattern.sub(replacement, text)
async def _preprocess_user_message(
self, query: core_entities.Query
) -> tuple[str, list[str]]:
"""预处理用户消息,提取纯文本,阿里云提供的上传文件方法过于复杂,暂不支持上传文件(包括图片)"""
plain_text = ""
image_ids = []
if isinstance(query.user_message.content, list):
for ce in query.user_message.content:
if ce.type == "text":
plain_text += ce.text
# 暂时不支持上传图片,保留代码以便后续扩展
# elif ce.type == "image_base64":
# image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
# file_bytes = base64.b64decode(image_b64)
# file = ("img.png", file_bytes, f"image/{image_format}")
# file_upload_resp = await self.dify_client.upload_file(
# file,
# f"{query.session.launcher_type.value}_{query.session.launcher_id}",
# )
# image_id = file_upload_resp["id"]
# image_ids.append(image_id)
elif isinstance(query.user_message.content, str):
plain_text = query.user_message.content
return plain_text, image_ids
async def _agent_messages(
self, query: core_entities.Query
) -> typing.AsyncGenerator[llm_entities.Message, None]:
"""Dashscope 智能体对话请求"""
#局部变量
chunk = None # 流式传输的块
pending_content = "" # 待处理的Agent输出内容
references_dict = {} # 用于存储引用编号和对应的参考资料
plain_text = "" # 用户输入的纯文本信息
image_ids = [] # 用户输入的图片ID列表 (暂不支持)
plain_text, image_ids = await self._preprocess_user_message(query)
#发送对话请求
response = dashscope.Application.call(
api_key=self.api_key, # 智能体应用的API Key
app_id=self.app_id, # 智能体应用的ID
prompt=plain_text, # 用户输入的文本信息
stream=True, # 流式输出
incremental_output=True, # 增量输出,使用流式输出需要开启增量输出
session_id=query.session.using_conversation.uuid, # 会话ID用于多轮对话
# rag_options={ # 主要用于文件交互,暂不支持
# "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个
# }
)
for chunk in response:
if chunk.get("status_code") != 200:
raise DashscopeAPIError(
f"Dashscope API 请求失败: status_code={chunk.get('status_code')} message={chunk.get('message')} request_id={chunk.get('request_id')} "
)
if not chunk:
continue
#获取流式传输的output
stream_output = chunk.get("output", {})
if stream_output.get("text") is not None:
pending_content += stream_output.get("text")
#保存当前会话的session_id用于下次对话的语境
query.session.using_conversation.uuid = stream_output.get("session_id")
#获取模型传出的参考资料列表
references_dict_list = stream_output.get("doc_references", [])
#从模型传出的参考资料信息中提取用于替换的字典
if references_dict_list is not None:
for doc in references_dict_list:
if doc.get("index_id") is not None:
references_dict[doc.get("index_id")] = doc.get("doc_name")
#将参考资料替换到文本中
pending_content = self._replace_references(pending_content, references_dict)
yield llm_entities.Message(
role="assistant",
content=pending_content,
)
async def _workflow_messages(
self, query: core_entities.Query
) -> typing.AsyncGenerator[llm_entities.Message, None]:
"""Dashscope 工作流对话请求"""
#局部变量
chunk = None # 流式传输的块
pending_content = "" # 待处理的Agent输出内容
references_dict = {} # 用于存储引用编号和对应的参考资料
plain_text = "" # 用户输入的纯文本信息
image_ids = [] # 用户输入的图片ID列表 (暂不支持)
plain_text, image_ids = await self._preprocess_user_message(query)
#发送对话请求
response = dashscope.Application.call(
api_key=self.api_key, # 智能体应用的API Key
app_id=self.app_id, # 智能体应用的ID
prompt=plain_text, # 用户输入的文本信息
stream=True, # 流式输出
incremental_output=True, # 增量输出,使用流式输出需要开启增量输出
session_id=query.session.using_conversation.uuid, # 会话ID用于多轮对话
biz_params=self.biz_params # 工作流应用的自定义输入参数传递
# rag_options={ # 主要用于文件交互,暂不支持
# "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个
# }
)
#处理API返回的流式输出
for chunk in response:
if chunk.get("status_code") != 200:
raise DashscopeAPIError(
f"Dashscope API 请求失败: status_code={chunk.get('status_code')} message={chunk.get('message')} request_id={chunk.get('request_id')} "
)
if not chunk:
continue
#获取流式传输的output
stream_output = chunk.get("output", {})
if stream_output.get("text") is not None:
pending_content += stream_output.get("text")
#保存当前会话的session_id用于下次对话的语境
query.session.using_conversation.uuid = stream_output.get("session_id")
#获取模型传出的参考资料列表
references_dict_list = stream_output.get("doc_references", [])
#从模型传出的参考资料信息中提取用于替换的字典
if references_dict_list is not None:
for doc in references_dict_list:
if doc.get("index_id") is not None:
references_dict[doc.get("index_id")] = doc.get("doc_name")
#将参考资料替换到文本中
pending_content = self._replace_references(pending_content, references_dict)
yield llm_entities.Message(
role="assistant",
content=pending_content,
)
async def run(
self, query: core_entities.Query
) -> typing.AsyncGenerator[llm_entities.Message, None]:
"""运行"""
if self.ap.provider_cfg.data["dashscope-app-api"]["app-type"] == "agent":
async for msg in self._agent_messages(query):
yield msg
elif self.ap.provider_cfg.data["dashscope-app-api"]["app-type"] == "workflow":
async for msg in self._workflow_messages(query):
yield msg
else:
raise DashscopeAPIError(
f"不支持的 Dashscope 应用类型: {self.ap.provider_cfg.data['dashscope-app-api']['app-type']}"
)

View File

@@ -3,6 +3,9 @@ from __future__ import annotations
import typing import typing
import json import json
import uuid import uuid
import base64
import aiohttp
from .. import runner from .. import runner
from ...core import entities as core_entities from ...core import entities as core_entities
@@ -20,125 +23,274 @@ class DifyServiceAPIRunner(runner.RequestRunner):
async def initialize(self): async def initialize(self):
"""初始化""" """初始化"""
valid_app_types = ['chat', 'workflow'] valid_app_types = ["chat", "agent", "workflow"]
if self.ap.provider_cfg.data['dify-service-api']['app-type'] not in valid_app_types: if (
raise errors.DifyAPIError(f"不支持的 Dify 应用类型: {self.ap.provider_cfg.data['dify-service-api']['app-type']}") self.ap.provider_cfg.data["dify-service-api"]["app-type"]
not in valid_app_types
):
raise errors.DifyAPIError(
f"不支持的 Dify 应用类型: {self.ap.provider_cfg.data['dify-service-api']['app-type']}"
)
api_key = self.ap.provider_cfg.data['dify-service-api'][self.ap.provider_cfg.data['dify-service-api']['app-type']]['api-key'] api_key = self.ap.provider_cfg.data["dify-service-api"][
self.ap.provider_cfg.data["dify-service-api"]["app-type"]
]["api-key"]
self.dify_client = client.AsyncDifyServiceClient( self.dify_client = client.AsyncDifyServiceClient(
api_key=api_key, api_key=api_key,
base_url=self.ap.provider_cfg.data['dify-service-api']['base-url'] base_url=self.ap.provider_cfg.data["dify-service-api"]["base-url"],
) )
async def _preprocess_user_message(self, query: core_entities.Query) -> tuple[str, list[str]]: async def _preprocess_user_message(
self, query: core_entities.Query
) -> tuple[str, list[str]]:
"""预处理用户消息,提取纯文本,并将图片上传到 Dify 服务 """预处理用户消息,提取纯文本,并将图片上传到 Dify 服务
Returns: Returns:
tuple[str, list[str]]: 纯文本和图片的 Dify 服务图片 ID tuple[str, list[str]]: 纯文本和图片的 Dify 服务图片 ID
""" """
plain_text = '' plain_text = ""
image_ids = [] image_ids = []
if isinstance(query.user_message.content, list): if isinstance(query.user_message.content, list):
for ce in query.user_message.content: for ce in query.user_message.content:
if ce.type == 'text': if ce.type == "text":
plain_text += ce.text plain_text += ce.text
elif ce.type == 'image_url': elif ce.type == "image_base64":
file_bytes, image_format = await image.get_qq_image_bytes(ce.image_url.url) image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
file_bytes = base64.b64decode(image_b64)
file = ("img.png", file_bytes, f"image/{image_format}") file = ("img.png", file_bytes, f"image/{image_format}")
file_upload_resp = await self.dify_client.upload_file(file, f"{query.session.launcher_type.value}_{query.session.launcher_id}") file_upload_resp = await self.dify_client.upload_file(
image_id = file_upload_resp['id'] file,
f"{query.session.launcher_type.value}_{query.session.launcher_id}",
)
image_id = file_upload_resp["id"]
image_ids.append(image_id) image_ids.append(image_id)
elif isinstance(query.user_message.content, str): elif isinstance(query.user_message.content, str):
plain_text = query.user_message.content plain_text = query.user_message.content
return plain_text, image_ids return plain_text, image_ids
async def _chat_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: async def _chat_messages(
self, query: core_entities.Query
) -> typing.AsyncGenerator[llm_entities.Message, None]:
"""调用聊天助手""" """调用聊天助手"""
cov_id = query.session.using_conversation.uuid or "" cov_id = query.session.using_conversation.uuid or ""
plain_text, image_ids = await self._preprocess_user_message(query) plain_text, image_ids = await self._preprocess_user_message(query)
files = [{ files = [
'type': 'image', {
'transfer_method': 'local_file', "type": "image",
'upload_file_id': image_id, "transfer_method": "local_file",
} for image_id in image_ids] "upload_file_id": image_id,
}
for image_id in image_ids
]
resp = await self.dify_client.chat_messages(inputs={}, query=plain_text, user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", conversation_id=cov_id, files=files) mode = "basic" # 标记是基础编排还是工作流编排
msg = llm_entities.Message( basic_mode_pending_chunk = ''
role='assistant',
content=resp['answer'],
)
yield msg async for chunk in self.dify_client.chat_messages(
inputs={},
query=plain_text,
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
conversation_id=cov_id,
files=files,
timeout=self.ap.provider_cfg.data["dify-service-api"]["chat"]["timeout"],
):
self.ap.logger.debug("dify-chat-chunk: " + str(chunk))
query.session.using_conversation.uuid = resp['conversation_id'] if chunk['event'] == 'workflow_started':
mode = "workflow"
async def _workflow_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: if mode == "workflow":
if chunk['event'] == 'node_finished':
if chunk['data']['node_type'] == 'answer':
yield llm_entities.Message(
role="assistant",
content=chunk['data']['outputs']['answer'],
)
elif mode == "basic":
if chunk['event'] == 'message':
basic_mode_pending_chunk += chunk['answer']
elif chunk['event'] == 'message_end':
yield llm_entities.Message(
role="assistant",
content=basic_mode_pending_chunk,
)
basic_mode_pending_chunk = ''
query.session.using_conversation.uuid = chunk["conversation_id"]
async def _agent_chat_messages(
self, query: core_entities.Query
) -> typing.AsyncGenerator[llm_entities.Message, None]:
"""调用聊天助手"""
cov_id = query.session.using_conversation.uuid or ""
plain_text, image_ids = await self._preprocess_user_message(query)
files = [
{
"type": "image",
"transfer_method": "local_file",
"upload_file_id": image_id,
}
for image_id in image_ids
]
ignored_events = ["agent_message"]
async for chunk in self.dify_client.chat_messages(
inputs={},
query=plain_text,
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
response_mode="streaming",
conversation_id=cov_id,
files=files,
timeout=self.ap.provider_cfg.data["dify-service-api"]["chat"]["timeout"],
):
self.ap.logger.debug("dify-agent-chunk: " + str(chunk))
if chunk["event"] in ignored_events:
continue
if chunk["event"] == "agent_thought":
if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过
continue
if chunk['thought'].strip() != '': # 文字回复内容
msg = llm_entities.Message(
role="assistant",
content=chunk["thought"],
)
yield msg
if chunk['tool']:
msg = llm_entities.Message(
role="assistant",
tool_calls=[
llm_entities.ToolCall(
id=chunk['id'],
type="function",
function=llm_entities.FunctionCall(
name=chunk["tool"],
arguments=json.dumps({}),
),
)
],
)
yield msg
if chunk['event'] == 'message_file':
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
base_url = self.dify_client.base_url
if base_url.endswith('/v1'):
base_url = base_url[:-3]
image_url = base_url + chunk['url']
yield llm_entities.Message(
role="assistant",
content=[llm_entities.ContentElement.from_image_url(image_url)],
)
query.session.using_conversation.uuid = chunk["conversation_id"]
async def _workflow_messages(
self, query: core_entities.Query
) -> typing.AsyncGenerator[llm_entities.Message, None]:
"""调用工作流""" """调用工作流"""
if not query.session.using_conversation.uuid: if not query.session.using_conversation.uuid:
query.session.using_conversation.uuid = str(uuid.uuid4()) query.session.using_conversation.uuid = str(uuid.uuid4())
cov_id = query.session.using_conversation.uuid cov_id = query.session.using_conversation.uuid
plain_text, image_ids = await self._preprocess_user_message(query) plain_text, image_ids = await self._preprocess_user_message(query)
files = [{ files = [
'type': 'image', {
'transfer_method': 'local_file', "type": "image",
'upload_file_id': image_id, "transfer_method": "local_file",
} for image_id in image_ids] "upload_file_id": image_id,
}
for image_id in image_ids
]
ignored_events = ['text_chunk', 'workflow_started'] ignored_events = ["text_chunk", "workflow_started"]
async for chunk in self.dify_client.workflow_run(inputs={ async for chunk in self.dify_client.workflow_run(
"langbot_user_message_text": plain_text, inputs={
"langbot_session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}", "langbot_user_message_text": plain_text,
"langbot_conversation_id": cov_id, "langbot_session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}",
}, user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", files=files): "langbot_conversation_id": cov_id,
if chunk['event'] in ignored_events: },
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
files=files,
timeout=self.ap.provider_cfg.data["dify-service-api"]["workflow"]["timeout"],
):
self.ap.logger.debug("dify-workflow-chunk: " + str(chunk))
if chunk["event"] in ignored_events:
continue continue
if chunk['event'] == 'node_started': if chunk["event"] == "node_started":
if chunk['data']['node_type'] == 'start' or chunk['data']['node_type'] == 'end': if (
chunk["data"]["node_type"] == "start"
or chunk["data"]["node_type"] == "end"
):
continue continue
msg = llm_entities.Message( msg = llm_entities.Message(
role='assistant', role="assistant",
content=None, content=None,
tool_calls=[llm_entities.ToolCall( tool_calls=[
id=chunk['data']['node_id'], llm_entities.ToolCall(
type='function', id=chunk["data"]["node_id"],
function=llm_entities.FunctionCall( type="function",
name=chunk['data']['title'], function=llm_entities.FunctionCall(
arguments=json.dumps({}), name=chunk["data"]["title"],
), arguments=json.dumps({}),
)], ),
)
],
) )
yield msg yield msg
elif chunk['event'] == 'workflow_finished': elif chunk["event"] == "workflow_finished":
if chunk['data']['error']:
raise errors.DifyAPIError(chunk['data']['error'])
msg = llm_entities.Message( msg = llm_entities.Message(
role='assistant', role="assistant",
content=chunk['data']['outputs'][self.ap.provider_cfg.data['dify-service-api']['workflow']['output-key']], content=chunk["data"]["outputs"][
self.ap.provider_cfg.data["dify-service-api"]["workflow"][
"output-key"
]
],
) )
yield msg yield msg
async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: async def run(
self, query: core_entities.Query
) -> typing.AsyncGenerator[llm_entities.Message, None]:
"""运行请求""" """运行请求"""
if self.ap.provider_cfg.data['dify-service-api']['app-type'] == 'chat': if self.ap.provider_cfg.data["dify-service-api"]["app-type"] == "chat":
async for msg in self._chat_messages(query): async for msg in self._chat_messages(query):
yield msg yield msg
elif self.ap.provider_cfg.data['dify-service-api']['app-type'] == 'workflow': elif self.ap.provider_cfg.data["dify-service-api"]["app-type"] == "agent":
async for msg in self._agent_chat_messages(query):
yield msg
elif self.ap.provider_cfg.data["dify-service-api"]["app-type"] == "workflow":
async for msg in self._workflow_messages(query): async for msg in self._workflow_messages(query):
yield msg yield msg
else: else:
raise errors.DifyAPIError(f"不支持的 Dify 应用类型: {self.ap.provider_cfg.data['dify-service-api']['app-type']}") raise errors.DifyAPIError(
f"不支持的 Dify 应用类型: {self.ap.provider_cfg.data['dify-service-api']['app-type']}"
)

View File

@@ -23,7 +23,7 @@ class LocalAgentRunner(runner.RequestRunner):
req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message]
# 首次请求 # 首次请求
msg = await query.use_model.requester.call(query.use_model, req_messages, query.use_funcs) msg = await query.use_model.requester.call(query, query.use_model, req_messages, query.use_funcs)
yield msg yield msg
@@ -61,7 +61,7 @@ class LocalAgentRunner(runner.RequestRunner):
req_messages.append(err_msg) req_messages.append(err_msg)
# 处理完所有调用,再次请求 # 处理完所有调用,再次请求
msg = await query.use_model.requester.call(query.use_model, req_messages, query.use_funcs) msg = await query.use_model.requester.call(query, query.use_model, req_messages, query.use_funcs)
yield msg yield msg

View File

@@ -62,11 +62,11 @@ class AnnouncementManager:
async def fetch_saved( async def fetch_saved(
self self
) -> list[Announcement]: ) -> list[Announcement]:
if not os.path.exists("res/announcement_saved.json"): if not os.path.exists("data/labels/announcement_saved.json"):
with open("res/announcement_saved.json", "w", encoding="utf-8") as f: with open("data/labels/announcement_saved.json", "w", encoding="utf-8") as f:
f.write("[]") f.write("[]")
with open("res/announcement_saved.json", "r", encoding="utf-8") as f: with open("data/labels/announcement_saved.json", "r", encoding="utf-8") as f:
content = f.read() content = f.read()
if not content: if not content:
@@ -79,7 +79,7 @@ class AnnouncementManager:
content: list[Announcement] content: list[Announcement]
): ):
with open("res/announcement_saved.json", "w", encoding="utf-8") as f: with open("data/labels/announcement_saved.json", "w", encoding="utf-8") as f:
f.write(json.dumps([ f.write(json.dumps([
item.to_dict() for item in content item.to_dict() for item in content
], indent=4, ensure_ascii=False)) ], indent=4, ensure_ascii=False))

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.1" semantic_version = "v3.4.7.1"
debug_mode = False debug_mode = False

View File

@@ -1,9 +1,49 @@
import base64 import base64
import typing import typing
import io
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import ssl import ssl
import aiohttp import aiohttp
import PIL.Image
import httpx
async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]:
"""
下载企业微信图片并转换为 base64
:param pic_url: 企业微信图片URL
:return: (base64_str, image_format)
"""
async with aiohttp.ClientSession() as session:
async with session.get(pic_url) as response:
if response.status != 200:
raise Exception(f"Failed to download image: {response.status}")
# 读取图片数据
image_data = await response.read()
# 获取图片格式
content_type = response.headers.get('Content-Type', '')
image_format = content_type.split('/')[-1] # 例如 'image/jpeg' -> 'jpeg'
# 转换为 base64
import base64
image_base64 = base64.b64encode(image_data).decode('utf-8')
return image_base64, image_format
async def get_qq_official_image_base64(pic_url:str,content_type:str) -> tuple[str,str]:
"""
下载QQ官方图片
并且转换为base64格式
"""
async with httpx.AsyncClient() as client:
response = await client.get(pic_url)
response.raise_for_status() # 确保请求成功
image_data = response.content
base64_data = base64.b64encode(image_data).decode('utf-8')
return f"data:{content_type};base64,{base64_data}"
def get_qq_image_downloadable_url(image_url: str) -> tuple[str, dict]: def get_qq_image_downloadable_url(image_url: str) -> tuple[str, dict]:
@@ -13,9 +53,10 @@ def get_qq_image_downloadable_url(image_url: str) -> tuple[str, dict]:
return f"http://{parsed.netloc}{parsed.path}", query return f"http://{parsed.netloc}{parsed.path}", query
async def get_qq_image_bytes(image_url: str) -> tuple[bytes, str]: async def get_qq_image_bytes(image_url: str, query: dict={}) -> tuple[bytes, str]:
"""获取QQ图片的bytes""" """[弃用]获取QQ图片的bytes"""
image_url, query = get_qq_image_downloadable_url(image_url) image_url, query_in_url = get_qq_image_downloadable_url(image_url)
query = {**query, **query_in_url}
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE ssl_context.verify_mode = ssl.CERT_NONE
@@ -24,8 +65,11 @@ async def get_qq_image_bytes(image_url: str) -> tuple[bytes, str]:
resp.raise_for_status() resp.raise_for_status()
file_bytes = await resp.read() file_bytes = await resp.read()
content_type = resp.headers.get('Content-Type') content_type = resp.headers.get('Content-Type')
if not content_type or not content_type.startswith('image/'): if not content_type:
image_format = 'jpeg' image_format = 'jpeg'
elif not content_type.startswith('image/'):
pil_img = PIL.Image.open(io.BytesIO(file_bytes))
image_format = pil_img.format.lower()
else: else:
image_format = content_type.split('/')[-1] image_format = content_type.split('/')[-1]
return file_bytes, image_format return file_bytes, image_format
@@ -34,7 +78,7 @@ async def get_qq_image_bytes(image_url: str) -> tuple[bytes, str]:
async def qq_image_url_to_base64( async def qq_image_url_to_base64(
image_url: str image_url: str
) -> typing.Tuple[str, str]: ) -> typing.Tuple[str, str]:
"""将QQ图片URL转为base64并返回图片格式 """[弃用]将QQ图片URL转为base64并返回图片格式
Args: Args:
image_url (str): QQ图片URL image_url (str): QQ图片URL
@@ -47,8 +91,18 @@ async def qq_image_url_to_base64(
# Flatten the query dictionary # Flatten the query dictionary
query = {k: v[0] for k, v in query.items()} query = {k: v[0] for k, v in query.items()}
file_bytes, image_format = await get_qq_image_bytes(image_url) file_bytes, image_format = await get_qq_image_bytes(image_url, query)
base64_str = base64.b64encode(file_bytes).decode() base64_str = base64.b64encode(file_bytes).decode()
return base64_str, image_format return base64_str, image_format
async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, str]:
"""提取base64编码和图片格式
data:image/jpeg;base64,xxx
提取出base64编码和图片格式
"""
base64_str = image_base64_data.split(',')[-1]
image_format = image_base64_data.split(':')[-1].split(';')[0].split('/')[-1]
return base64_str, image_format

View File

@@ -2,7 +2,7 @@ import aiohttp
async def get_myip() -> str: async def get_myip() -> str:
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
async with session.get("https://ip.useragentinfo.com/myip") as response: async with session.get("https://ip.useragentinfo.com/myip") as response:
return await response.text() return await response.text()
except Exception as e: except Exception as e:

View File

@@ -22,13 +22,3 @@ def install_requirements(file):
pipmain(['install', '-r', file, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple", pipmain(['install', '-r', file, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"]) "--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
# log.reset_logging() # log.reset_logging()
if __name__ == "__main__":
try:
install("openai11")
except Exception as e:
print(111)
print(e)
print(222)

View File

@@ -1,3 +1,4 @@
# direct
requests requests
openai>1.0.0 openai>1.0.0
anthropic anthropic
@@ -22,4 +23,13 @@ quart-cors
aiofiles aiofiles
aioshutil aioshutil
argon2-cffi argon2-cffi
pyjwt pyjwt
pycryptodome
lark-oapi
discord.py
cryptography
gewechat-client
dashscope
# indirect
taskgroup==0.0.0a4

View File

@@ -0,0 +1 @@
[]

1
res/instance_id.json Normal file
View File

@@ -0,0 +1 @@
{"host_id": "host_9b4a220d-3bb6-42fc-aec3-41188ce0a41c", "instance_id": "instance_61d8f262-b98a-4165-8e77-85fb6262529e", "instance_create_ts": 1736824678}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -115,6 +115,102 @@
"name": "deepseek-coder", "name": "deepseek-coder",
"requester": "deepseek-chat-completions", "requester": "deepseek-chat-completions",
"token_mgr": "deepseek" "token_mgr": "deepseek"
},
{
"name": "deepseek-reasoner",
"requester": "deepseek-chat-completions",
"token_mgr": "deepseek"
},
{
"name": "grok-2-latest",
"requester": "xai-chat-completions",
"token_mgr": "xai"
},
{
"name": "grok-2",
"requester": "xai-chat-completions",
"token_mgr": "xai"
},
{
"name": "grok-2-vision-1212",
"requester": "xai-chat-completions",
"token_mgr": "xai",
"vision_supported": true
},
{
"name": "grok-2-1212",
"requester": "xai-chat-completions",
"token_mgr": "xai"
},
{
"name": "grok-vision-beta",
"requester": "xai-chat-completions",
"token_mgr": "xai",
"vision_supported": true
},
{
"name": "grok-beta",
"requester": "xai-chat-completions",
"token_mgr": "xai"
},
{
"name": "glm-4-plus",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
},
{
"name": "glm-4-0520",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
},
{
"name": "glm-4-air",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
},
{
"name": "glm-4-airx",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
},
{
"name": "glm-4-long",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
},
{
"name": "glm-4-flashx",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
},
{
"name": "glm-4-flash",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
},
{
"name": "glm-4v-plus",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
},
{
"name": "glm-4v",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
},
{
"name": "glm-4v-flash",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
},
{
"name": "glm-zero-preview",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
} }
] ]
} }

View File

@@ -24,6 +24,57 @@
"public_guild_messages", "public_guild_messages",
"direct_message" "direct_message"
] ]
},
{
"adapter": "qqofficial",
"enable": false,
"appid": "1234567890",
"secret": "xxxxxxx",
"port": 2284,
"token": "abcdefg"
},
{
"adapter": "wecom",
"enable": false,
"host": "0.0.0.0",
"port": 2290,
"corpid": "",
"secret": "",
"token": "",
"EncodingAESKey": "",
"contacts_secret": ""
},
{
"adapter": "lark",
"enable": false,
"app_id": "cli_abcdefgh",
"app_secret": "XXXXXXXXXX",
"bot_name": "LangBot"
},
{
"adapter": "discord",
"enable": false,
"client_id": "1234567890",
"token": "XXXXXXXXXX"
},
{
"adapter": "gewechat",
"enable": false,
"gewechat_url": "http://your-gewechat-server:2531",
"port": 2286,
"callback_url": "http://your-callback-url:2286/gewechat/callback",
"app_id": "",
"token": ""
},
{
"adapter":"officialaccount",
"enable": false,
"token": "",
"EncodingAESKey":"",
"AppID":"",
"AppSecret":"",
"host": "0.0.0.0",
"port": 2287
} }
], ],
"track-function-calls": true, "track-function-calls": true,
@@ -34,7 +85,7 @@
"max": 0 "max": 0
}, },
"long-text-process": { "long-text-process": {
"threshold": 256, "threshold": 2560,
"strategy": "forward", "strategy": "forward",
"font-path": "" "font-path": ""
}, },

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