Compare commits

..

84 Commits

Author SHA1 Message Date
Junyan Qin (Chin)
629ebae0e9 chore: release v3.4.11.1 (#1244) 2025-03-28 23:48:09 +08:00
Junyan Qin (Chin)
394d4b3c1b fix: static_file sent with wrong mimetype (#1243) 2025-03-28 23:46:24 +08:00
Junyan Qin (Chin)
a0fd152d19 doc(README): add 3.13 in python version badge (#1232) 2025-03-24 15:43:46 +08:00
Junyan Qin (Chin)
1a62e08bab chore: update gitignore (#1231) 2025-03-24 15:40:51 +08:00
Junyan Qin (Chin)
edbc59c117 perf: use source_platform_object to pass source event in aiocqhttp (#1230) 2025-03-24 14:03:44 +08:00
WangCham
cfdd0f8cb2 fix: Format the code in a standardized way (#1222) 2025-03-21 14:04:13 +08:00
Junyan Qin (Chin)
808f30675d doc(README): contributors (#1223) 2025-03-21 10:47:32 +08:00
Junyan Qin (Chin)
46072abb41 doc(README): add planning platforms comments (#1219) 2025-03-19 23:23:56 +08:00
Junyan Qin (Chin)
27bbb2297a Merge pull request #1217 from RockChinQ/version/3.4.11
chore: bump version 3.4.11
2025-03-19 22:50:50 +08:00
Junyan Qin
0d235aaef8 chore: bump version 3.4.11 2025-03-19 22:50:15 +08:00
Junyan Qin (Chin)
dda8c637d8 Merge pull request #1216 from RockChinQ/doc/mcp-readme
doc(README): add mcp comments
2025-03-19 13:10:30 +08:00
Junyan Qin
e6d7aaa440 doc(README): add mcp comments 2025-03-19 13:10:01 +08:00
Junyan Qin (Chin)
028458b33c Merge pull request #1210 from fdc310/master
处理at全员的atall,以及修复回复公众号和微信团队的问题
2025-03-19 13:03:07 +08:00
Junyan Qin (Chin)
9c7d8099cb Merge pull request #1215 from RockChinQ/feat/mcp-tools
feat: add supports for loading mcp server as LLM tools provider
2025-03-19 13:01:09 +08:00
Junyan Qin
5640dc332d feat(mcp): available for provider reloading 2025-03-19 12:41:04 +08:00
Junyan Qin
40275c3ef1 feat: add supports for loading mcp server as LLM tools provider 2025-03-19 12:08:47 +08:00
Junyan Qin (Chin)
ebe0b2f335 Merge pull request #1214 from RockChinQ/feat/tool-loaders
feat: tool loader abstraction
2025-03-19 09:37:10 +08:00
Junyan Qin
97603e8441 feat: tool loader abstraction 2025-03-19 09:36:03 +08:00
Dong_master
72cd444861 处理at全员的atall,以及修复回复公众号和微信团队的问题 2025-03-18 23:14:55 +08:00
Dong_master
955b859f2c 处理at全员的atall,以及修复回复公众号和微信团队的问题 2025-03-18 23:14:33 +08:00
Junyan Qin
dea5cc9c0c stash 2025-03-18 21:59:53 +08:00
Junyan Qin (Chin)
d13ab1703e Merge pull request #1209 from wangcham/feat/wxoa-notice-msg
feat: add support for loading message in wxoa
2025-03-18 20:39:22 +08:00
Junyan Qin
61ab6a009b chore: migration for wxoa loading message 2025-03-18 20:38:33 +08:00
wangcham
a9ae36d362 feat: add support for loading message in wxoa 2025-03-18 06:58:35 -04:00
Junyan Qin (Chin)
f518395ce5 Merge pull request #1204 from fdc310/master
增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。
2025-03-18 12:04:09 +08:00
Junyan Qin (Chin)
20b17fe378 Merge pull request #1203 from IGCrystal/master
fix: Fix SSL certificate verification error during GitHub plugin installation
2025-03-17 23:54:25 +08:00
Junyan Qin
572182180c deps: add certifi 2025-03-17 23:53:29 +08:00
Dong_master
de261099aa 增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。 2025-03-17 23:33:44 +08:00
Dong_master
50f0122955 增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。 2025-03-17 23:31:13 +08:00
Dong_master
fe9eff923e 增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。 2025-03-17 23:23:21 +08:00
冰苷晶
dd36278032 fix: add certifi to requirement 2025-03-17 23:16:51 +08:00
冰苷晶
a079821976 fix: fix SSL certificateverification error during GitHub plugin installation.
- Create a custom SSL context using certifi for proper HTTPS certificate verification, meow - Add the ssl parameter to aiohttp requests to prevent download failure due to missing root certificates, meow - Improve error messages and enhance the overall plugin installation process, meow!
2025-03-17 23:12:23 +08:00
Junyan Qin
fa233e0a24 fix(variables): user_message_text not provided 2025-03-17 22:04:00 +08:00
Junyan Qin (Chin)
22306cb4ea Merge pull request #1199 from fdc310/master
Fixed bot sending messages to others without PushContent field causing error in judgment, and fixed bot sending messages to reply to themselves
2025-03-16 22:12:59 +08:00
Dong_master
f2d45a3668 将bot账号手动发送消息拦截提前到转换层 2025-03-16 21:43:07 +08:00
Dong_master
db91ff12f7 修复bot自己给别人发消息时没有PushContent字段导致判断错误,并修复bot自己发消息自己回复问题 2025-03-16 19:10:07 +08:00
Dong_master
eb841fb73e 修复bot自己给别人发消息时没有PushContent字段导致判断错误,并修复bot自己发消息自己回复问题 2025-03-16 19:08:56 +08:00
Junyan Qin
bd0438df76 chore: release v3.4.10.4 2025-03-16 17:05:10 +08:00
Junyan Qin (Chin)
9ca1fc59ef Merge pull request #1198 from fdc310/master
当机器人群名称改名后群聊单独at机器人时候替换@信息为空
2025-03-16 16:24:52 +08:00
Dong_master
84a80a5ec8 当是单独群聊at机器人时候替换@信息为空 2025-03-15 22:21:21 +08:00
Dong_master
4b2e248646 当是单独群聊at机器人时候替换@信息为空 2025-03-15 22:13:15 +08:00
Dong_master
b90e45590a 当微信群有at消息时删除第一个at已达到能激活管理员模式 2025-03-15 01:15:56 +08:00
Junyan Qin (Chin)
ff93d563a8 Merge pull request #1194 from fdc310/master
'增加了主动发送at信息'
2025-03-14 22:41:41 +08:00
Junyan Qin (Chin)
53228498ed Merge pull request #1191 from wangcham/master
fix: eliminate critical message queue blockage in wxoa
2025-03-14 22:29:03 +08:00
Dong_master
8ece82e43a '增加了主动发送at信息' 2025-03-14 02:33:52 +08:00
wangcham
8b4684675e fix: eliminate critical message queue blockage in wxoa 2025-03-13 10:44:09 -04:00
Junyan Qin (Chin)
8cca12fff2 Merge pull request #1190 from wangcham/master
feat: add support for longer response in wxoa
2025-03-13 17:30:19 +08:00
Junyan Qin
a74111612e chore: config for wxoa mode 2025-03-13 17:29:05 +08:00
wangcham
c7799a65c4 fix: update config in wxoa 2025-03-13 05:15:03 -04:00
wangcham
aabb01c50f feat: add support for longer response in wxoa 2025-03-12 23:39:43 -04:00
Junyan Qin
95e2ada965 fix(schema): adapt str type session id 2025-03-12 21:32:19 +08:00
Junyan Qin (Chin)
3fe7d53c76 Merge pull request #1188 from RockChinQ/feat/query-variables
feat: add query variables
2025-03-12 21:21:50 +08:00
Junyan Qin
e8634bb1ab feat(variables): add api for plugin 2025-03-12 20:57:42 +08:00
Junyan Qin
dbe46b5770 feat: add query variables 2025-03-12 19:13:04 +08:00
Junyan Qin (Chin)
6d9fba30b1 Merge pull request #1187 from wangcham/master
feat: add support for sending active messages in wecom
2025-03-12 16:49:47 +08:00
wangcham
6a866bf871 feat: add support for sending active messages in wecom 2025-03-12 04:03:02 -04:00
Junyan Qin (Chin)
3c961e4652 Merge pull request #1184 from wangcham/master
feat: add support for sending active messages in dingtalk
2025-03-11 16:42:29 +08:00
wangcham
7abd999420 feat: add support for sending active messages in dingtalk 2025-03-11 04:27:17 -04:00
Junyan Qin
fca8fbb135 perf: no longer add slash as ignored prefix as default 2025-03-11 12:06:37 +08:00
Junyan Qin
c67caf18df chore: release v3.4.10.3 2025-03-10 22:58:23 +08:00
Junyan Qin (Chin)
fe956fe4a5 Merge pull request #1181 from wangcham/master
fix: wrong group id in qqofficial web hook
2025-03-10 19:38:00 +08:00
wangcham
0e52f679a2 fix: wrong group id in webhook qq 2025-03-10 06:56:15 -04:00
Junyan Qin
b9500283ec fix(gewechat): remove necessary await in send_message 2025-03-10 15:16:10 +08:00
Junyan Qin (Chin)
441b69b528 Merge pull request #1174 from Muffeter/master 2025-03-07 18:37:18 +08:00
sonmu
898bcdc96b typo: fix README 2025-03-07 18:33:27 +08:00
Junyan Qin
02bc1fc45e chore: release v3.4.10.2 2025-03-05 21:12:52 +08:00
Junyan Qin (Chin)
5585981dc3 Merge pull request #1165 from fdc310/master
修复因为gewe将字段修改回原版而导致的我的判断逻辑问题
2025-03-05 21:12:21 +08:00
Dong_master
a4777f194b 修复因为gewe将字段修改回原版而导致的我的判断逻辑问题 2025-03-05 19:56:28 +08:00
Junyan Qin
41aeda8dc0 chore: release v3.4.10.1 2025-03-05 17:16:05 +08:00
Junyan Qin (Chin)
2ed522667e Merge pull request #1162 from fdc310/master
'修复了gewe更新回调参数data和typename字段改变造成的不回复的问题'
2025-03-05 17:14:27 +08:00
Dong_master
1932444666 '修复了gewe更新回调参数data和typename字段改变造成的不回复的问题' 2025-03-05 16:48:46 +08:00
Dong_master
b49b7e963d '修复了gewe更新回调参数data和typename字段改变造成的不回复的问题' 2025-03-05 00:54:39 +08:00
Junyan Qin
435c11ff27 doc(README): add more model in README 2025-03-03 21:26:39 +08:00
Junyan Qin
2e93600437 feat: update llm-models.json template 2025-03-03 21:02:48 +08:00
Junyan Qin (Chin)
faecb70d0f Merge pull request #1154 from Yi-Lyu/master
将微信消息时间戳传递给 dify,便于 dify 通过消息时间戳来做业务逻辑。
2025-03-02 20:21:08 +08:00
Junyan Qin
92e1ac5c3a feat: add supports for passing time to dify workflow 2025-03-02 20:18:33 +08:00
Junyan Qin
8963a2117b feat: add field time in MessageEvent 2025-03-02 20:16:34 +08:00
Ethan
aa300258ab feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:45:10 +08:00
Ethan
48841daff5 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:30:07 +08:00
Ethan
8878f1ed87 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:20:10 +08:00
Ethan
f6205d79c0 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:18:26 +08:00
Ethan
d6d5dac6b3 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:10:53 +08:00
Ethan
05b979e68a feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:10:09 +08:00
Ethan
9f7d9e4c0d feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 18:49:32 +08:00
46 changed files with 1129 additions and 260 deletions

2
.gitignore vendored
View File

@@ -19,7 +19,7 @@ cookies.json
data/labels/announcement_saved.json data/labels/announcement_saved.json
cmdpriv.json cmdpriv.json
tips.py tips.py
.venv venv*
bin/ bin/
.vscode .vscode
test_* test_*

View File

@@ -9,7 +9,7 @@
<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/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">项目主页</a>
<a href="https://docs.langbot.app/insight/intro.htmll">功能介绍</a> <a href="https://docs.langbot.app/insight/intro.html">功能介绍</a>
<a href="https://docs.langbot.app/insight/guide.html">部署文档</a> <a href="https://docs.langbot.app/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/usage/faq.html">常见问题</a> <a href="https://docs.langbot.app/usage/faq.html">常见问题</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">插件介绍</a> <a href="https://docs.langbot.app/plugin/plugin-intro.html">插件介绍</a>
@@ -26,7 +26,7 @@
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum) [![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest) [![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) ![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"> <img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot) [![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) [简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
@@ -39,7 +39,7 @@
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。 - 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。 - 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html) - 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/)目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html) - 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
## 📦 开始使用 ## 📦 开始使用
@@ -93,11 +93,13 @@
| 钉钉 | ✅ | | | 钉钉 | ✅ | |
| Discord | ✅ | | | Discord | ✅ | |
| Telegram | ✅ | | | Telegram | ✅ | |
| Slack | 🚧 | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | | | WhatsApp | 🚧 | |
🚧: 正在开发中 🚧: 正在开发中
### 大模型 ### 大模型能力
| 模型 | 状态 | 备注 | | 模型 | 状态 | 备注 |
| --- | --- | --- | | --- | --- | --- |
@@ -114,12 +116,33 @@
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 | | [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 | | [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 | | [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
### TTS
| 平台/模型 | 备注 |
| --- | --- |
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
### 文生图
| 平台/模型 | 备注 |
| --- | --- |
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
## 😘 社区贡献 ## 😘 社区贡献
LangBot 离不开以下贡献者和社区内所有人的贡献,我们欢迎任何形式的贡献和反馈。 感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors"> <a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" /> <img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a> </a>
以及 LangBot 核心团队成员:
- [RockChinQ](https://github.com/RockChinQ)
- [the-lazy-me](https://github.com/the-lazy-me)
- [wangcham](https://github.com/wangcham)
- [KaedeSAMA](https://github.com/KaedeSAMA)

View File

@@ -8,7 +8,7 @@
<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/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">Home</a>
<a href="https://docs.langbot.app/insight/intro.htmll">Features</a> <a href="https://docs.langbot.app/insight/intro.html">Features</a>
<a href="https://docs.langbot.app/insight/guide.html">Deployment</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/usage/faq.html">FAQ</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">Plugin</a> <a href="https://docs.langbot.app/plugin/plugin-intro.html">Plugin</a>
@@ -24,7 +24,7 @@
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87) [![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) [![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)) ![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"> <img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) [简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
@@ -36,7 +36,7 @@
- 💬 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, personal WeChat, Lark, DingTalk, Discord, Telegram, etc. - 💬 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, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. - 🛠️ 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) - 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); 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) - 😻 [New] Web UI: Support management LangBot instance through the browser, for details, see [documentation](https://docs.langbot.app/webui/intro.html)
## 📦 Getting Started ## 📦 Getting Started
@@ -90,6 +90,8 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| DingTalk | ✅ | | | DingTalk | ✅ | |
| Discord | ✅ | | | Discord | ✅ | |
| Telegram | ✅ | | | Telegram | ✅ | |
| Slack | 🚧 | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | | | WhatsApp | 🚧 | |
🚧: In development 🚧: In development
@@ -111,14 +113,19 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) | | [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform | | [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform | | [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
## 🤝 Community Contribution ## 🤝 Community Contribution
Thanks to the following contributors and everyone in the community for their contributions. Thank you for the following [code contributors](https://github.com/RockChinQ/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors"> <a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" /> <img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a> </a>
And the core team members of LangBot:
- [RockChinQ](https://github.com/RockChinQ)
- [the-lazy-me](https://github.com/the-lazy-me)
- [wangcham](https://github.com/wangcham)
- [KaedeSAMA](https://github.com/KaedeSAMA)

View File

@@ -8,7 +8,7 @@
<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/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">ホーム</a>
<a href="https://docs.langbot.app/insight/intro.htmll">機能</a> <a href="https://docs.langbot.app/insight/intro.html">機能</a>
<a href="https://docs.langbot.app/insight/guide.html">デプロイ</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/usage/faq.html">FAQ</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">プラグイン</a> <a href="https://docs.langbot.app/plugin/plugin-intro.html">プラグイン</a>
@@ -23,7 +23,7 @@
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87) [![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) [![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)) ![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"> <img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) [简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
@@ -35,7 +35,7 @@
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。 - 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。 - 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。豊富なエコシステム、現在数十の[プラグイン](https://docs.langbot.app/plugin/plugin-intro.html)が存在。 - 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/)豊富なエコシステム、現在数十の[プラグイン](https://docs.langbot.app/plugin/plugin-intro.html)が存在。
- 😻 [新機能] Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。詳細は[ドキュメント](https://docs.langbot.app/webui/intro.html)を参照。 - 😻 [新機能] Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。詳細は[ドキュメント](https://docs.langbot.app/webui/intro.html)を参照。
## 📦 始め方 ## 📦 始め方
@@ -89,6 +89,8 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| DingTalk | ✅ | | | DingTalk | ✅ | |
| Discord | ✅ | | | Discord | ✅ | |
| Telegram | ✅ | | | Telegram | ✅ | |
| Slack | 🚧 | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | | | WhatsApp | 🚧 | |
🚧: 開発中 🚧: 開発中
@@ -110,14 +112,19 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) | | [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム | | [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム | | [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
## 🤝 コミュニティ貢献 ## 🤝 コミュニティ貢献
以下の貢献者とコミュニティの皆さんの貢献に感謝します。 LangBot への貢献に対して、以下の [コード貢献者](https://github.com/RockChinQ/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors"> <a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" /> <img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a> </a>
LangBot の核心チームメンバー:
- [RockChinQ](https://github.com/RockChinQ)
- [the-lazy-me](https://github.com/the-lazy-me)
- [wangcham](https://github.com/wangcham)
- [KaedeSAMA](https://github.com/KaedeSAMA)

View File

@@ -23,6 +23,7 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler):
"""异步等待消息的到来""" """异步等待消息的到来"""
while self.incoming_message is None: while self.incoming_message is None:
await asyncio.sleep(0.1) # 异步等待,避免阻塞 await asyncio.sleep(0.1) # 异步等待,避免阻塞
return self.incoming_message return self.incoming_message
async def get_dingtalk_client(client_id, client_secret): async def get_dingtalk_client(client_id, client_secret):

View File

@@ -157,6 +157,7 @@ class DingTalkClient:
async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage): async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage):
try: try:
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False)) # print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
message_data = { message_data = {
"IncomingMessage":incoming_message, "IncomingMessage":incoming_message,
@@ -197,6 +198,53 @@ class DingTalkClient:
traceback.print_exc() traceback.print_exc()
return message_data return message_data
async def send_proactive_message_to_one(self,target_id:str,content:str):
if not await self.check_access_token():
await self.get_access_token()
url = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'
headers ={
"x-acs-dingtalk-access-token":self.access_token,
"Content-Type":"application/json",
}
data ={
"robotCode":self.robot_code,
"userIds":[target_id],
"msgKey": "sampleText",
"msgParam": json.dumps({"content":content}),
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(url,headers=headers,json=data)
except Exception:
traceback.print_exc()
async def send_proactive_message_to_group(self,target_id:str,content:str):
if not await self.check_access_token():
await self.get_access_token()
url = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send'
headers ={
"x-acs-dingtalk-access-token":self.access_token,
"Content-Type":"application/json",
}
data ={
"robotCode":self.robot_code,
"openConversationId":target_id,
"msgKey": "sampleText",
"msgParam": json.dumps({"content":content}),
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(url,headers=headers,json=data)
except Exception:
traceback.print_exc()
async def start(self): async def start(self):
"""启动 WebSocket 连接,监听消息""" """启动 WebSocket 连接,监听消息"""

View File

@@ -1,4 +1,5 @@
# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件 # 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件
from collections import deque
import time import time
import traceback import traceback
from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
@@ -12,6 +13,7 @@ import httpx
import asyncio import asyncio
import time import time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from pkg.platform.sources import officialaccount as oa
@@ -25,6 +27,7 @@ xml_template = """
</xml> </xml>
""" """
class OAClient(): class OAClient():
def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str): def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str):
@@ -41,6 +44,7 @@ class OAClient():
} }
self.access_token_expiry_time = None self.access_token_expiry_time = None
self.msg_id_map = {} self.msg_id_map = {}
self.generated_content = {}
async def handle_callback_request(self): async def handle_callback_request(self):
@@ -83,12 +87,10 @@ class OAClient():
from_user = root.find("FromUserName").text # 发送者 from_user = root.find("FromUserName").text # 发送者
to_user = root.find("ToUserName").text # 机器人 to_user = root.find("ToUserName").text # 机器人
from pkg.platform.sources import officialaccount
timeout = 4.80 timeout = 4.80
interval = 0.1 interval = 0.1
while True: while True:
content = officialaccount.generated_content.pop(message_data["MsgId"], None) content = self.generated_content.pop(message_data["MsgId"], None)
if content: if content:
response_xml = xml_template.format( response_xml = xml_template.format(
to_user=from_user, to_user=from_user,
@@ -168,6 +170,168 @@ class OAClient():
for handler in self._message_handlers[msg_type]: for handler in self._message_handlers[msg_type]:
await handler(event) await handler(event)
async def set_message(self,msg_id:int,content:str):
self.generated_content[msg_id] = content
class OAClientForLongerResponse():
def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str,LoadingMessage: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.loading_message = LoadingMessage
self.msg_queue = {}
self.user_msg_queue = {}
async def handle_callback_request(self):
try:
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()
return echostr if check_signature == signature else "拒绝请求"
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("消息解密失败")
# 解析 XML
root = ET.fromstring(xml_msg)
from_user = root.find("FromUserName").text
to_user = root.find("ToUserName").text
if self.msg_queue.get(from_user) and self.msg_queue[from_user][0]["content"]:
queue_top = self.msg_queue[from_user].pop(0)
queue_content = queue_top["content"]
# 弹出用户消息
if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user]:
self.user_msg_queue[from_user].pop(0)
response_xml = xml_template.format(
to_user=from_user,
from_user=to_user,
create_time=int(time.time()),
content=queue_content
)
return response_xml
else:
response_xml = xml_template.format(
to_user=from_user,
from_user=to_user,
create_time=int(time.time()),
content=self.loading_message
)
if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user][0]["content"]:
return response_xml
else:
message_data = await self.get_message(xml_msg)
if message_data:
event = OAEvent.from_payload(message_data)
if event:
self.user_msg_queue.setdefault(from_user,[]).append(
{
"content":event.message,
}
)
await self._handle_message(event)
return response_xml
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):
"""
处理消息事件。
"""
msg_type = event.type
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
await handler(event)
async def set_message(self,from_user:int,message_id:int,content:str):
if from_user not in self.msg_queue:
self.msg_queue[from_user] = []
self.msg_queue[from_user].append(
{
"msg_id":message_id,
"content":content,
}
)

View File

@@ -66,7 +66,7 @@ class WecomClient():
else: else:
raise Exception("未获取用户") raise Exception("未获取用户")
async def send_to_all(self,content:str): async def send_to_all(self,content:str,agent_id:int):
if not self.check_access_token_for_contacts(): if not self.check_access_token_for_contacts():
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts) self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
@@ -77,7 +77,7 @@ class WecomClient():
params = { params = {
"touser" : user_ids_string, "touser" : user_ids_string,
"msgtype" : "text", "msgtype" : "text",
"agentid" : 1000002, "agentid" : agent_id,
"text" : { "text" : {
"content" : content, "content" : content,
}, },

View File

@@ -66,8 +66,42 @@ class HTTPController:
@self.quart_app.route("/") @self.quart_app.route("/")
async def index(): async def index():
return await quart.send_from_directory(frontend_path, "index.html") return await quart.send_from_directory(
frontend_path,
"index.html",
mimetype="text/html"
)
@self.quart_app.route("/<path:path>") @self.quart_app.route("/<path:path>")
async def static_file(path: str): async def static_file(path: str):
return await quart.send_from_directory(frontend_path, path)
mimetype = None
if path.endswith(".html"):
mimetype = "text/html"
elif path.endswith(".js"):
mimetype = "application/javascript"
elif path.endswith(".css"):
mimetype = "text/css"
elif path.endswith(".png"):
mimetype = "image/png"
elif path.endswith(".jpg"):
mimetype = "image/jpeg"
elif path.endswith(".jpeg"):
mimetype = "image/jpeg"
elif path.endswith(".gif"):
mimetype = "image/gif"
elif path.endswith(".svg"):
mimetype = "image/svg+xml"
elif path.endswith(".ico"):
mimetype = "image/x-icon"
elif path.endswith(".json"):
mimetype = "application/json"
elif path.endswith(".txt"):
mimetype = "text/plain"
return await quart.send_from_directory(
frontend_path,
path,
mimetype=mimetype
)

View File

@@ -16,7 +16,6 @@ class FuncOperator(operator.CommandOperator):
all_functions = await self.ap.tool_mgr.get_all_functions( all_functions = await self.ap.tool_mgr.get_all_functions(
plugin_enabled=True, plugin_enabled=True,
plugin_status=plugin_context.RuntimeContainerStatus.INITIALIZED,
) )
for func in all_functions: for func in all_functions:

View File

@@ -204,6 +204,8 @@ class Application:
case core_entities.LifecycleControlScope.PROVIDER.value: case core_entities.LifecycleControlScope.PROVIDER.value:
self.logger.info("执行热重载 scope="+scope) self.logger.info("执行热重载 scope="+scope)
await self.tool_mgr.shutdown()
latest_llm_model_config = await config.load_json_config("data/metadata/llm-models.json", "templates/metadata/llm-models.json") latest_llm_model_config = await config.load_json_config("data/metadata/llm-models.json", "templates/metadata/llm-models.json")
self.llm_models_meta = latest_llm_model_config self.llm_models_meta = latest_llm_model_config
llm_model_mgr_inst = llm_model_mgr.ModelManager(self) llm_model_mgr_inst = llm_model_mgr.ModelManager(self)

View File

@@ -33,6 +33,8 @@ required_deps = {
"dingtalk_stream": "dingtalk_stream", "dingtalk_stream": "dingtalk_stream",
"dashscope": "dashscope", "dashscope": "dashscope",
"telegram": "python-telegram-bot", "telegram": "python-telegram-bot",
"certifi": "certifi",
"mcp": "mcp",
} }

View File

@@ -72,6 +72,9 @@ class Query(pydantic.BaseModel):
user_message: typing.Optional[llm_entities.Message] = None user_message: typing.Optional[llm_entities.Message] = None
"""此次请求的用户消息对象,由前置处理器阶段设置""" """此次请求的用户消息对象,由前置处理器阶段设置"""
variables: typing.Optional[dict[str, typing.Any]] = None
"""变量由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。"""
use_model: typing.Optional[entities.LLMModelInfo] = None use_model: typing.Optional[entities.LLMModelInfo] = None
"""使用的模型,由前置处理器阶段设置""" """使用的模型,由前置处理器阶段设置"""
@@ -86,10 +89,31 @@ class Query(pydantic.BaseModel):
# ======= 内部保留 ======= # ======= 内部保留 =======
current_stage: "pkg.pipeline.stagemgr.StageInstContainer" = None current_stage: "pkg.pipeline.stagemgr.StageInstContainer" = None
"""当前所处阶段"""
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
# ========== 插件可调用的 API请求 API ==========
def set_variable(self, key: str, value: typing.Any):
"""设置变量"""
if self.variables is None:
self.variables = {}
self.variables[key] = value
def get_variable(self, key: str) -> typing.Any:
"""获取变量"""
if self.variables is None:
return None
return self.variables.get(key)
def get_variables(self) -> dict[str, typing.Any]:
"""获取所有变量"""
if self.variables is None:
return {}
return self.variables
class Conversation(pydantic.BaseModel): class Conversation(pydantic.BaseModel):
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation但只有一个当前使用的 Conversation""" """对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation但只有一个当前使用的 Conversation"""

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("wxoa-mode", 35)
class WxoaModeMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'Mode' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'Mode' not in adapter:
adapter['Mode'] = 'drop'
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("wxoa-loading-message", 36)
class WxoaLoadingMessageMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'LoadingMessage' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'LoadingMessage' not in adapter:
adapter['LoadingMessage'] = 'AI正在思考中请发送任意内容获取回复。'
await self.ap.platform_cfg.dump_config()

View File

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

View File

@@ -11,7 +11,9 @@ from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_
from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_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 m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config
from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config
from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message
from ..migrations import m037_mcp_config
@stage.stage_class("MigrationStage") @stage.stage_class("MigrationStage")
class MigrationStage(stage.BootingStage): class MigrationStage(stage.BootingStage):

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import datetime
from .. import stage, entities, stagemgr from .. import stage, entities, stagemgr
from ...core import entities as core_entities from ...core import entities as core_entities
@@ -34,7 +35,7 @@ class PreProcessor(stage.PipelineStage):
conversation = await self.ap.sess_mgr.get_conversation(session) conversation = await self.ap.sess_mgr.get_conversation(session)
# 从会话取出消息和情景预设到query # 设置query
query.session = session query.session = session
query.prompt = conversation.prompt.copy() query.prompt = conversation.prompt.copy()
query.messages = conversation.messages.copy() query.messages = conversation.messages.copy()
@@ -43,6 +44,11 @@ class PreProcessor(stage.PipelineStage):
query.use_funcs = conversation.use_funcs if query.use_model.tool_call_supported else None query.use_funcs = conversation.use_funcs if query.use_model.tool_call_supported else None
query.variables = {
"session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}",
"conversation_id": conversation.uuid,
"msg_create_time": int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp()),
}
# 检查vision是否启用没启用就删除所有图片 # 检查vision是否启用没启用就删除所有图片
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): 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):
@@ -54,11 +60,14 @@ class PreProcessor(stage.PipelineStage):
content_list = [] content_list = []
plain_text = ""
for me in query.message_chain: for me in query.message_chain:
if isinstance(me, platform_message.Plain): if isinstance(me, platform_message.Plain):
content_list.append( content_list.append(
llm_entities.ContentElement.from_text(me.text) llm_entities.ContentElement.from_text(me.text)
) )
plain_text += me.text
elif isinstance(me, platform_message.Image): elif isinstance(me, platform_message.Image):
if self.ap.provider_cfg.data['enable-vision'] and (self.ap.provider_cfg.data['runner'] != 'local-agent' or 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.base64 is not None: if me.base64 is not None:
@@ -66,6 +75,8 @@ class PreProcessor(stage.PipelineStage):
llm_entities.ContentElement.from_image_base64(me.base64) llm_entities.ContentElement.from_image_base64(me.base64)
) )
query.variables['user_message_text'] = plain_text
query.user_message = llm_entities.Message( query.user_message = llm_entities.Message(
role='user', role='user',
content=content_list content=content_list

View File

@@ -170,7 +170,8 @@ class PlatformManager:
self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") self.ap.logger.debug(f"Traceback: {traceback.format_exc()}")
tasks.append(exception_wrapper(adapter)) tasks.append(exception_wrapper(adapter))
for task in tasks: for task in tasks:
self.ap.task_mgr.create_task( self.ap.task_mgr.create_task(
task, task,

View File

@@ -57,7 +57,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif msg.path: elif msg.path:
arg = msg.path arg = msg.path
msg_list.append(aiocqhttp.MessageSegment.record(msg.path)) msg_list.append(aiocqhttp.MessageSegment.record(msg.path))
elif type(msg) is forward.Forward: elif type(msg) is platform_message.Forward:
for node in msg.node_list: for node in msg.node_list:
msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0]) msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0])
@@ -101,69 +101,8 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
class AiocqhttpEventConverter(adapter.EventConverter): class AiocqhttpEventConverter(adapter.EventConverter):
@staticmethod @staticmethod
async def yiri2target(event: platform_events.Event, bot_account_id: int): async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int):
return event.source_platform_object
msg, msg_id, msg_time = await AiocqhttpMessageConverter.yiri2target(event.message_chain)
if type(event) is platform_events.GroupMessage:
role = "member"
if event.sender.permission == "ADMINISTRATOR":
role = "admin"
elif event.sender.permission == "OWNER":
role = "owner"
payload = {
"post_type": "message",
"message_type": "group",
"time": int(msg_time.timestamp()),
"self_id": bot_account_id,
"sub_type": "normal",
"anonymous": None,
"font": 0,
"message": str(msg),
"raw_message": str(msg),
"sender": {
"age": 0,
"area": "",
"card": "",
"level": "",
"nickname": event.sender.member_name,
"role": role,
"sex": "unknown",
"title": "",
"user_id": event.sender.id,
},
"user_id": event.sender.id,
"message_id": msg_id,
"group_id": event.group.id,
"message_seq": 0,
}
return aiocqhttp.Event.from_payload(payload)
elif type(event) is platform_events.FriendMessage:
payload = {
"post_type": "message",
"message_type": "private",
"time": int(msg_time.timestamp()),
"self_id": bot_account_id,
"sub_type": "friend",
"target_id": bot_account_id,
"message": str(msg),
"raw_message": str(msg),
"font": 0,
"sender": {
"age": 0,
"nickname": event.sender.nickname,
"sex": "unknown",
"user_id": event.sender.id,
},
"message_id": msg_id,
"user_id": event.sender.id,
}
return aiocqhttp.Event.from_payload(payload)
@staticmethod @staticmethod
async def target2yiri(event: aiocqhttp.Event): async def target2yiri(event: aiocqhttp.Event):
@@ -196,6 +135,7 @@ class AiocqhttpEventConverter(adapter.EventConverter):
), ),
message_chain=yiri_chain, message_chain=yiri_chain,
time=event.time, time=event.time,
source_platform_object=event
) )
return converted_event return converted_event
elif event.message_type == "private": elif event.message_type == "private":
@@ -207,6 +147,7 @@ class AiocqhttpEventConverter(adapter.EventConverter):
), ),
message_chain=yiri_chain, message_chain=yiri_chain,
time=event.time, time=event.time,
source_platform_object=event
) )

View File

@@ -77,7 +77,7 @@ class DingTalkEventConverter(adapter.EventConverter):
remark="" remark=""
), ),
message_chain = message_chain, message_chain = message_chain,
time = datetime.datetime.now(), time = event.incoming_message.create_at,
source_platform_object=event, source_platform_object=event,
) )
elif event.conversation == 'GroupMessage': elif event.conversation == 'GroupMessage':
@@ -95,7 +95,7 @@ class DingTalkEventConverter(adapter.EventConverter):
last_speak_timestamp=0, last_speak_timestamp=0,
mute_time_remaining=0 mute_time_remaining=0
) )
time = datetime.datetime.now(), time = event.incoming_message.create_at
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender =sender, sender =sender,
message_chain = message_chain, message_chain = message_chain,
@@ -152,7 +152,11 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
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
): ):
pass content = await DingTalkMessageConverter.yiri2target(message)
if target_type == 'person':
await self.bot.send_proactive_message_to_one(target_id,content)
if target_type == 'group':
await self.bot.send_proactive_message_to_group(target_id,content)
def register_listener( def register_listener(
self, self,

View File

@@ -25,6 +25,7 @@ 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 from ...utils import image
import xml.etree.ElementTree as ET
class GewechatMessageConverter(adapter.MessageConverter): class GewechatMessageConverter(adapter.MessageConverter):
@@ -62,20 +63,33 @@ class GewechatMessageConverter(adapter.MessageConverter):
bot_account_id: str bot_account_id: str
) -> platform_message.MessageChain: ) -> platform_message.MessageChain:
if message["Data"]["MsgType"] == 1: if message["Data"]["MsgType"] == 1:
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉
regex = re.compile(r"^wxid_.*:") regex = re.compile(r"^wxid_.*:")
# print(message)
line_split = message["Data"]["Content"]["string"].split("\n") line_split = message["Data"]["Content"]["string"].split("\n")
if len(line_split) > 0 and regex.match(line_split[0]): if len(line_split) > 0 and regex.match(line_split[0]):
message["Data"]["Content"]["string"] = "\n".join(line_split[1:]) message["Data"]["Content"]["string"] = "\n".join(line_split[1:])
at_string = f'@{bot_account_id}'
# 正则表达式模式,匹配'@'后跟任意数量的非空白字符
pattern = r'@\S+'
at_string = f"@{bot_account_id}"
content_list = [] content_list = []
if at_string in message["Data"]["Content"]["string"]: if at_string in message["Data"]["Content"]["string"]:
content_list.append(platform_message.At(target=bot_account_id)) content_list.append(platform_message.At(target=bot_account_id))
content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, "", 1))) content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, '', 1)))
# 更优雅的替换改名后@机器人仅仅限于单独AT的情况
elif "PushContent" in message['Data'] and '在群聊中@了你' in message["Data"]["PushContent"]:
if '@所有人' in message["Data"]["Content"]["string"]: # at全员时候传入atll不当作at自己
content_list.append(platform_message.AtAll())
else:
content_list.append(platform_message.At(target=bot_account_id))
content_list.append(platform_message.Plain(re.sub(pattern, '', message["Data"]["Content"]["string"])))
else: else:
content_list = [platform_message.Plain(message["Data"]["Content"]["string"])] content_list = [platform_message.Plain(message["Data"]["Content"]["string"])]
@@ -118,17 +132,78 @@ class GewechatMessageConverter(adapter.MessageConverter):
# 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 # 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递
try: try:
content = message["Data"]["Content"]["string"] content = message["Data"]["Content"]["string"]
# 有三种可能的消息结构weid开头私聊直接<?xml>和直接<msg>
if content.startswith('wxid'):
xml_list = content.split('\n')[2:]
xml_data = '\n'.join(xml_list)
elif content.startswith('<?xml'):
xml_list = content.split('\n')[1:]
xml_data = '\n'.join(xml_list)
else:
xml_data = content
try: content_data = ET.fromstring(xml_data)
content_bytes = content.encode('utf-8') # print(xml_data)
decoded_content = base64.b64decode(content_bytes) # 拿到细分消息类型按照gewe接口中描述
'''
小程序33/36
引用消息57
转账消息2000
红包消息2001
视频号消息51
'''
appmsg_data = content_data.find('.//appmsg')
data_type = appmsg_data.find('.//type').text
if data_type == '57':
user_data = appmsg_data.find('.//title').text # 拿到用户消息
quote_data = appmsg_data.find('.//refermsg').find('.//content').text # 引用原文
sender_id = appmsg_data.find('.//refermsg').find('.//chatusr').text # 引用用户id
from_name = message['Data']['FromUserName']['string']
message_list =[]
if message['Wxid'] == sender_id and from_name.endswith('@chatroom'): # 因为引用机制暂时无法响应用户所以当引用用户是机器人是构建一个at激活机器人
message_list.append(platform_message.At(target=bot_account_id))
message_list.append(platform_message.Quote(
sender_id=sender_id,
origin=platform_message.MessageChain(
[platform_message.Plain(quote_data)]
)))
message_list.append(platform_message.Plain(user_data))
return platform_message.MessageChain(message_list)
elif data_type == '51':
return platform_message.MessageChain( return platform_message.MessageChain(
[platform_message.Unknown(content=decoded_content)] [platform_message.Plain(text=f'[视频号消息]')]
) )
except Exception as e: # print(content_data)
elif data_type == '2000':
return platform_message.MessageChain( return platform_message.MessageChain(
[platform_message.Plain(text=content)] [platform_message.Plain(text=f'[转账消息]')]
) )
elif data_type == '2001':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[红包消息]')]
)
elif data_type == '5':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[公众号消息]')]
)
elif data_type == '33' or data_type == '36':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[小程序消息]')]
)
# print(data_type.text)
else:
try:
content_bytes = content.encode('utf-8')
decoded_content = base64.b64decode(content_bytes)
return platform_message.MessageChain(
[platform_message.Unknown(content=decoded_content)]
)
except Exception as e:
return platform_message.MessageChain(
[platform_message.Plain(text=content)]
)
except Exception as e: except Exception as e:
print(f"Error processing type 49 message: {str(e)}") print(f"Error processing type 49 message: {str(e)}")
return platform_message.MessageChain( return platform_message.MessageChain(
@@ -152,6 +227,14 @@ class GewechatEventConverter(adapter.EventConverter):
event: dict, event: dict,
bot_account_id: str bot_account_id: str
) -> platform_events.MessageEvent: ) -> platform_events.MessageEvent:
# print(event)
# 排除自己发消息回调回答问题
if event['Wxid'] == event['Data']['FromUserName']['string']:
return None
# 排除公众号以及微信团队消息
if event['Data']['FromUserName']['string'].startswith('gh_')\
or event['Data']['FromUserName']['string'].startswith('weixin'):
return None
message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id) message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id)
if not message_chain: if not message_chain:
@@ -227,6 +310,13 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
async def gewechat_callback(): async def gewechat_callback():
data = await quart.request.json data = await quart.request.json
# print(json.dumps(data, indent=4, ensure_ascii=False)) # print(json.dumps(data, indent=4, ensure_ascii=False))
if 'data' in data:
data['Data'] = data['data']
if 'type_name' in data:
data['TypeName'] = data['type_name']
# print(json.dumps(data, indent=4, ensure_ascii=False))
if 'testMsg' in data: if 'testMsg' in data:
return 'ok' return 'ok'
@@ -248,17 +338,29 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
target_id: str, target_id: str,
message: platform_message.MessageChain message: platform_message.MessageChain
): ):
geweap_msg = await GewechatMessageConverter.yiri2target(message) geweap_msg = await self.message_converter.yiri2target(message)
# 此处加上群消息at处理 # 此处加上群消息at处理
# ats = [item["target"] for item in geweap_msg if item["type"] == "at"] ats = [item["target"] for item in geweap_msg if item["type"] == "at"]
for msg in geweap_msg: for msg in geweap_msg:
# at主动发送消息
if msg['type'] == 'text': if msg['type'] == 'text':
await self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id, content=msg['content']) if ats:
member_info = self.bot.get_chatroom_member_detail(
self.config["app_id"],
target_id,
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=target_id, content=msg['content'],
ats=",".join(ats))
elif msg['type'] == 'image': elif msg['type'] == 'image':
await self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"])
@@ -289,7 +391,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
app_id=self.config["app_id"], app_id=self.config["app_id"],
to_wxid=message_source.source_platform_object["Data"]["FromUserName"]["string"], to_wxid=message_source.source_platform_object["Data"]["FromUserName"]["string"],
content=msg["content"], content=msg["content"],
ats=','.join(ats) ats=",".join(ats)
) )
async def is_muted(self, group_id: int) -> bool: async def is_muted(self, group_id: int) -> bool:

View File

@@ -2,21 +2,19 @@ from __future__ import annotations
import typing import typing
import asyncio import asyncio
import traceback import traceback
import time
import datetime import datetime
from pkg.core import app from pkg.core import app
from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message from pkg.platform.types import events as platform_events, message as platform_message
from collections import deque
import aiocqhttp
import aiohttp
from libs.official_account_api.oaevent import OAEvent from libs.official_account_api.oaevent import OAEvent
from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message from pkg.platform.types import events as platform_events, message as platform_message
from libs.official_account_api.api import OAClient from libs.official_account_api.api import OAClient
from libs.official_account_api.api import OAClientForLongerResponse
from pkg.core import app from pkg.core import app
from .. import adapter from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app 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
@@ -24,9 +22,6 @@ from ..types import entities as platform_entities
from ...command.errors import ParamNotEnoughError from ...command.errors import ParamNotEnoughError
# 生成的ai回答
generated_content = {}
class OAMessageConverter(adapter.MessageConverter): class OAMessageConverter(adapter.MessageConverter):
@staticmethod @staticmethod
async def yiri2target(message_chain: platform_message.MessageChain): async def yiri2target(message_chain: platform_message.MessageChain):
@@ -70,7 +65,7 @@ class OAEventConverter(adapter.EventConverter):
class OfficialAccountAdapter(adapter.MessagePlatformAdapter): class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
bot : OAClient bot : OAClient | OAClientForLongerResponse
ap : app.Application ap : app.Application
bot_account_id: str bot_account_id: str
message_converter: OAMessageConverter = OAMessageConverter() message_converter: OAMessageConverter = OAMessageConverter()
@@ -88,27 +83,44 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
"EncodingAESKey", "EncodingAESKey",
"AppSecret", "AppSecret",
"AppID", "AppID",
"Mode",
] ]
missing_keys = [key for key in required_keys if key not in config] missing_keys = [key for key in required_keys if key not in config]
if missing_keys: if missing_keys:
raise ParamNotEnoughError("企业微信缺少相关配置项,请查看文档或联系管理员") raise ParamNotEnoughError("微信公众号缺少相关配置项,请查看文档或联系管理员")
self.bot = OAClient(
token=config['token'], if self.config['Mode'] == "drop":
EncodingAESKey=config['EncodingAESKey'], self.bot = OAClient(
Appsecret=config['AppSecret'], token=config['token'],
AppID=config['AppID'], EncodingAESKey=config['EncodingAESKey'],
) Appsecret=config['AppSecret'],
AppID=config['AppID'],
)
elif self.config['Mode'] == "passive":
self.bot = OAClientForLongerResponse(
token=config['token'],
EncodingAESKey=config['EncodingAESKey'],
Appsecret=config['AppSecret'],
AppID=config['AppID'],
LoadingMessage=config['LoadingMessage']
)
else:
raise KeyError("请设置微信公众号通信模式")
async def reply_message(self, message_source: platform_events.FriendMessage, message: platform_message.MessageChain, quote_origin: bool = False): 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( content = await OAMessageConverter.yiri2target(
message message
) )
if type(self.bot) == OAClient:
await self.bot.set_message(message_source.message_chain.message_id,content)
if type(self.bot) == OAClientForLongerResponse:
from_user = message_source.sender.id
await self.bot.set_message(from_user,message_source.message_chain.message_id,content)
generated_content[message_source.message_chain.message_id] = content
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
): ):

View File

@@ -2,15 +2,14 @@ from __future__ import annotations
import typing import typing
import asyncio import asyncio
import traceback import traceback
import time
import datetime import datetime
import aiocqhttp
import aiohttp
from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message from pkg.platform.types import events as platform_events, message as platform_message
from pkg.core import app from pkg.core import app
from .. import adapter from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app 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
@@ -74,7 +73,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
remark = "", remark = "",
) )
return platform_events.FriendMessage( return platform_events.FriendMessage(
sender = friend,message_chain = yiri_chain,time = event.timestamp, sender = friend,message_chain = yiri_chain,time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
),
source_platform_object=event source_platform_object=event
) )
@@ -96,7 +99,7 @@ class QQOfficialEventConverter(adapter.EventConverter):
member_name= event.t, member_name= event.t,
permission= 'MEMBER', permission= 'MEMBER',
group = platform_entities.Group( group = platform_entities.Group(
id = 0, id = event.group_openid,
name = 'MEMBER', name = 'MEMBER',
permission= platform_entities.Permission.Member permission= platform_entities.Permission.Member
), ),
@@ -105,7 +108,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
last_speak_timestamp=0, last_speak_timestamp=0,
mute_time_remaining=0 mute_time_remaining=0
) )
time = event.timestamp time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
)
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender = sender, sender = sender,
message_chain=yiri_chain, message_chain=yiri_chain,
@@ -119,7 +126,7 @@ class QQOfficialEventConverter(adapter.EventConverter):
member_name=event.t, member_name=event.t,
permission= 'MEMBER', permission= 'MEMBER',
group = platform_entities.Group( group = platform_entities.Group(
id = 0, id = event.channel_id,
name = 'MEMBER', name = 'MEMBER',
permission=platform_entities.Permission.Member permission=platform_entities.Permission.Member
), ),
@@ -128,7 +135,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
last_speak_timestamp=0, last_speak_timestamp=0,
mute_time_remaining=0 mute_time_remaining=0
) )
time = event.timestamp, time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
)
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender =sender, sender =sender,
message_chain = yiri_chain, message_chain = yiri_chain,

View File

@@ -2,18 +2,15 @@ from __future__ import annotations
import typing import typing
import asyncio import asyncio
import traceback import traceback
import time
import datetime import datetime
import aiocqhttp
import aiohttp
from libs.wecom_api.api import WecomClient from libs.wecom_api.api import WecomClient
from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message from pkg.platform.types import events as platform_events, message as platform_message
from libs.wecom_api.wecomevent import WecomEvent from libs.wecom_api.wecomevent import WecomEvent
from pkg.core import app from pkg.core import app
from .. import adapter from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app 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
@@ -200,7 +197,19 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
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
): ):
pass """企业微信目前只有发送给个人的方法,
构造target_id的方式为前半部分为账户id后半部分为agent_id,中间使用“|”符号隔开。
"""
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
parts = target_id.split("|")
user_id = parts[0]
agent_id = int(parts[1])
if target_type == 'person':
for content in content_list:
if content["type"] == "text":
await self.bot.send_private_msg(user_id,agent_id,content["content"])
if content["type"] == "image":
await self.bot.send_image(user_id,agent_id,content["media"])
def register_listener( def register_listener(
self, self,

View File

@@ -57,6 +57,9 @@ class MessageEvent(Event):
message_chain: platform_message.MessageChain message_chain: platform_message.MessageChain
"""消息内容。""" """消息内容。"""
time: float | None = None
"""消息发送时间戳。"""
source_platform_object: typing.Optional[typing.Any] = None source_platform_object: typing.Optional[typing.Any] = None
"""原消息平台对象。 """原消息平台对象。
供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息, 供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息,

View File

@@ -4,6 +4,8 @@ import re
import os import os
import shutil import shutil
import zipfile import zipfile
import ssl
import certifi
import aiohttp import aiohttp
import aiofiles import aiofiles
@@ -21,44 +23,39 @@ class GitHubRepoInstaller(installer.PluginInstaller):
def get_github_plugin_repo_label(self, repo_url: str) -> list[str]: def get_github_plugin_repo_label(self, repo_url: str) -> list[str]:
"""获取username, repo""" """获取username, repo"""
# 提取 username/repo , 正则表达式
repo = re.findall( repo = re.findall(
r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)", r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)",
repo_url, repo_url,
) )
if len(repo) > 0:
if len(repo) > 0: # github
return repo[0].split("/") return repo[0].split("/")
else: else:
return None return None
async def download_plugin_source_code(self, repo_url: str, target_path: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder()) -> str: async def download_plugin_source_code(self, repo_url: str, target_path: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder()) -> str:
"""下载插件源码(全异步)""" """下载插件源码(全异步)"""
# 提取 username/repo , 正则表达式
repo = self.get_github_plugin_repo_label(repo_url) repo = self.get_github_plugin_repo_label(repo_url)
target_path += repo[1]
if repo is None: if repo is None:
raise errors.PluginInstallerError('仅支持GitHub仓库地址') raise errors.PluginInstallerError('仅支持GitHub仓库地址')
target_path += repo[1]
self.ap.logger.debug("正在下载源码...") self.ap.logger.debug("正在下载源码...")
task_context.trace("下载源码...", "download-plugin-source-code") task_context.trace("下载源码...", "download-plugin-source-code")
zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD" zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD"
zip_resp: bytes = None zip_resp: bytes = None
# 创建自定义SSL上下文使用certifi提供的根证书
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get( async with session.get(
url=zipball_url, url=zipball_url,
timeout=aiohttp.ClientTimeout(total=300) timeout=aiohttp.ClientTimeout(total=300),
ssl=ssl_context # 使用自定义SSL上下文来验证证书
) as resp: ) as resp:
if resp.status != 200: if resp.status != 200:
raise errors.PluginInstallerError(f"下载源码失败: {resp.text}") raise errors.PluginInstallerError(f"下载源码失败: {await resp.text()}")
zip_resp = await resp.read() zip_resp = await resp.read()
if await aiofiles_os.path.exists("temp/" + target_path): if await aiofiles_os.path.exists("temp/" + target_path):
@@ -80,15 +77,11 @@ class GitHubRepoInstaller(installer.PluginInstaller):
await aiofiles_os.remove("temp/" + target_path + "/source.zip") await aiofiles_os.remove("temp/" + target_path + "/source.zip")
import glob import glob
unzip_dir = glob.glob("temp/" + target_path + "/*")[0] unzip_dir = glob.glob("temp/" + target_path + "/*")[0]
await aioshutil.copytree(unzip_dir, target_path + "/") await aioshutil.copytree(unzip_dir, target_path + "/")
await aioshutil.rmtree(unzip_dir) await aioshutil.rmtree(unzip_dir)
self.ap.logger.debug("源码下载完成。") self.ap.logger.debug("源码下载完成。")
return repo[1] return repo[1]
async def install_requirements(self, path: str): async def install_requirements(self, path: str):
@@ -100,20 +93,14 @@ class GitHubRepoInstaller(installer.PluginInstaller):
plugin_source: str, plugin_source: str,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
): ):
"""安装插件 """安装插件"""
"""
task_context.trace("下载插件源码...", "install-plugin") task_context.trace("下载插件源码...", "install-plugin")
repo_label = await self.download_plugin_source_code(plugin_source, "plugins/", task_context) repo_label = await self.download_plugin_source_code(plugin_source, "plugins/", task_context)
task_context.trace("安装插件依赖...", "install-plugin") task_context.trace("安装插件依赖...", "install-plugin")
await self.install_requirements("plugins/" + repo_label) await self.install_requirements("plugins/" + repo_label)
task_context.trace("完成.", "install-plugin") task_context.trace("完成.", "install-plugin")
await self.ap.plugin_mgr.setting.record_installed_plugin_source( await self.ap.plugin_mgr.setting.record_installed_plugin_source(
"plugins/"+repo_label+'/', plugin_source "plugins/" + repo_label + '/', plugin_source
) )
async def uninstall_plugin( async def uninstall_plugin(
@@ -121,10 +108,8 @@ class GitHubRepoInstaller(installer.PluginInstaller):
plugin_name: str, plugin_name: str,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
): ):
"""卸载插件 """卸载插件"""
"""
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is None: if plugin_container is None:
raise errors.PluginInstallerError('插件不存在或未成功加载') raise errors.PluginInstallerError('插件不存在或未成功加载')
else: else:
@@ -135,24 +120,18 @@ class GitHubRepoInstaller(installer.PluginInstaller):
async def update_plugin( async def update_plugin(
self, self,
plugin_name: str, plugin_name: str,
plugin_source: str=None, plugin_source: str = None,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
): ):
"""更新插件 """更新插件"""
"""
task_context.trace("更新插件...", "update-plugin") task_context.trace("更新插件...", "update-plugin")
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is None: if plugin_container is None:
raise errors.PluginInstallerError('插件不存在或未成功加载') raise errors.PluginInstallerError('插件不存在或未成功加载')
else: else:
if plugin_container.plugin_source: if plugin_container.plugin_source:
plugin_source = plugin_container.plugin_source plugin_source = plugin_container.plugin_source
task_context.trace("转交安装任务.", "update-plugin") task_context.trace("转交安装任务.", "update-plugin")
await self.install_plugin(plugin_source, task_context) await self.install_plugin(plugin_source, task_context)
else: else:
raise errors.PluginInstallerError('插件无源码信息,无法更新') raise errors.PluginInstallerError('插件无源码信息,无法更新')

View File

@@ -167,6 +167,10 @@ class DashScopeAPIRunner(runner.RequestRunner):
image_ids = [] # 用户输入的图片ID列表 (暂不支持) image_ids = [] # 用户输入的图片ID列表 (暂不支持)
plain_text, image_ids = await self._preprocess_user_message(query) plain_text, image_ids = await self._preprocess_user_message(query)
biz_params = {}
biz_params.update(self.biz_params)
biz_params.update(query.variables)
#发送对话请求 #发送对话请求
response = dashscope.Application.call( response = dashscope.Application.call(
@@ -176,7 +180,7 @@ class DashScopeAPIRunner(runner.RequestRunner):
stream=True, # 流式输出 stream=True, # 流式输出
incremental_output=True, # 增量输出,使用流式输出需要开启增量输出 incremental_output=True, # 增量输出,使用流式输出需要开启增量输出
session_id=query.session.using_conversation.uuid, # 会话ID用于多轮对话 session_id=query.session.using_conversation.uuid, # 会话ID用于多轮对话
biz_params=self.biz_params # 工作流应用的自定义输入参数传递 biz_params=biz_params, # 工作流应用的自定义输入参数传递
# rag_options={ # 主要用于文件交互,暂不支持 # rag_options={ # 主要用于文件交互,暂不支持
# "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个 # "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个
# } # }

View File

@@ -5,6 +5,7 @@ import json
import uuid import uuid
import re import re
import base64 import base64
import datetime
import aiohttp import aiohttp
@@ -69,6 +70,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
""" """
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":
@@ -109,8 +111,12 @@ class DifyServiceAPIRunner(runner.RequestRunner):
basic_mode_pending_chunk = '' basic_mode_pending_chunk = ''
inputs = {}
inputs.update(query.variables)
async for chunk in self.dify_client.chat_messages( async for chunk in self.dify_client.chat_messages(
inputs={}, inputs=inputs,
query=plain_text, query=plain_text,
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
conversation_id=cov_id, conversation_id=cov_id,
@@ -160,8 +166,12 @@ class DifyServiceAPIRunner(runner.RequestRunner):
ignored_events = ["agent_message"] ignored_events = ["agent_message"]
inputs = {}
inputs.update(query.variables)
async for chunk in self.dify_client.chat_messages( async for chunk in self.dify_client.chat_messages(
inputs={}, inputs=inputs,
query=plain_text, query=plain_text,
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
response_mode="streaming", response_mode="streaming",
@@ -225,8 +235,8 @@ class DifyServiceAPIRunner(runner.RequestRunner):
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 query.variables["conversation_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)
@@ -241,12 +251,17 @@ class DifyServiceAPIRunner(runner.RequestRunner):
ignored_events = ["text_chunk", "workflow_started"] ignored_events = ["text_chunk", "workflow_started"]
inputs = { # these variables are legacy variables, we need to keep them for compatibility
"langbot_user_message_text": plain_text,
"langbot_session_id": query.variables["session_id"],
"langbot_conversation_id": query.variables["conversation_id"],
"langbot_msg_create_time": query.variables["msg_create_time"],
}
inputs.update(query.variables)
async for chunk in self.dify_client.workflow_run( async for chunk in self.dify_client.workflow_run(
inputs={ inputs=inputs,
"langbot_user_message_text": plain_text,
"langbot_session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}",
"langbot_conversation_id": cov_id,
},
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
files=files, files=files,
timeout=self.ap.provider_cfg.data["dify-service-api"]["workflow"]["timeout"], timeout=self.ap.provider_cfg.data["dify-service-api"]["workflow"]["timeout"],

View File

@@ -54,7 +54,6 @@ class SessionManager:
use_model=await self.ap.model_mgr.get_model_by_name(self.ap.provider_cfg.data['model']), use_model=await self.ap.model_mgr.get_model_by_name(self.ap.provider_cfg.data['model']),
use_funcs=await self.ap.tool_mgr.get_all_functions( use_funcs=await self.ap.tool_mgr.get_all_functions(
plugin_enabled=True, plugin_enabled=True,
plugin_status=plugin_context.RuntimeContainerStatus.INITIALIZED,
), ),
) )
session.conversations.append(conversation) session.conversations.append(conversation)

View File

@@ -0,0 +1,54 @@
from __future__ import annotations
import abc
import typing
from ...core import app, entities as core_entities
from . import entities as tools_entities
preregistered_loaders: list[typing.Type[ToolLoader]] = []
def loader_class(name: str):
"""注册一个工具加载器
"""
def decorator(cls: typing.Type[ToolLoader]) -> typing.Type[ToolLoader]:
cls.name = name
preregistered_loaders.append(cls)
return cls
return decorator
class ToolLoader(abc.ABC):
"""工具加载器"""
name: str = None
ap: app.Application
def __init__(self, ap: app.Application):
self.ap = ap
async def initialize(self):
pass
@abc.abstractmethod
async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]:
"""获取所有工具"""
pass
@abc.abstractmethod
async def has_tool(self, name: str) -> bool:
"""检查工具是否存在"""
pass
@abc.abstractmethod
async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any:
"""执行工具调用"""
pass
@abc.abstractmethod
async def shutdown(self):
"""关闭工具"""
pass

View File

View File

@@ -0,0 +1,161 @@
from __future__ import annotations
import typing
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.sse import sse_client
from .. import loader, entities as tools_entities
from ....core import app, entities as core_entities
class RuntimeMCPSession:
"""运行时 MCP 会话"""
ap: app.Application
server_name: str
server_config: dict
session: ClientSession
exit_stack: AsyncExitStack
functions: list[tools_entities.LLMFunction] = []
def __init__(self, server_name: str, server_config: dict, ap: app.Application):
self.server_name = server_name
self.server_config = server_config
self.ap = ap
self.session = None
self.exit_stack = AsyncExitStack()
self.functions = []
async def _init_stdio_python_server(self):
server_params = StdioServerParameters(
command=self.server_config["command"],
args=self.server_config["args"],
env=self.server_config["env"],
)
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
stdio, write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(stdio, write)
)
await self.session.initialize()
async def _init_sse_server(self):
sse_transport = await self.exit_stack.enter_async_context(
sse_client(
self.server_config["url"],
headers=self.server_config.get("headers", {}),
timeout=self.server_config.get("timeout", 10),
)
)
sseio, write = sse_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(sseio, write)
)
await self.session.initialize()
async def initialize(self):
self.ap.logger.debug(f"初始化 MCP 会话: {self.server_name} {self.server_config}")
if self.server_config["mode"] == "stdio":
await self._init_stdio_python_server()
elif self.server_config["mode"] == "sse":
await self._init_sse_server()
else:
raise ValueError(f"无法识别 MCP 服务器类型: {self.server_name}: {self.server_config}")
tools = await self.session.list_tools()
self.ap.logger.debug(f"获取 MCP 工具: {tools}")
for tool in tools.tools:
async def func(query: core_entities.Query, **kwargs):
result = await self.session.call_tool(tool.name, kwargs)
if result.isError:
raise Exception(result.content[0].text)
return result.content[0].text
func.__name__ = tool.name
self.functions.append(tools_entities.LLMFunction(
name=tool.name,
human_desc=tool.description,
description=tool.description,
parameters=tool.inputSchema,
func=func,
))
async def shutdown(self):
"""关闭工具"""
await self.session._exit_stack.aclose()
@loader.loader_class("mcp")
class MCPLoader(loader.ToolLoader):
"""MCP 工具加载器。
在此加载器中管理所有与 MCP Server 的连接。
"""
sessions: dict[str, RuntimeMCPSession] = {}
_last_listed_functions: list[tools_entities.LLMFunction] = []
def __init__(self, ap: app.Application):
super().__init__(ap)
self.sessions = {}
self._last_listed_functions = []
async def initialize(self):
for server_config in self.ap.provider_cfg.data.get("mcp", {}).get("servers", []):
if not server_config["enable"]:
continue
session = RuntimeMCPSession(server_config["name"], server_config, self.ap)
await session.initialize()
# self.ap.event_loop.create_task(session.initialize())
self.sessions[server_config["name"]] = session
async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]:
all_functions = []
for session in self.sessions.values():
all_functions.extend(session.functions)
self._last_listed_functions = all_functions
return all_functions
async def has_tool(self, name: str) -> bool:
return name in [f.name for f in self._last_listed_functions]
async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any:
for server_name, session in self.sessions.items():
for function in session.functions:
if function.name == name:
return await function.func(query, **parameters)
raise ValueError(f"未找到工具: {name}")
async def shutdown(self):
"""关闭工具"""
for session in self.sessions.values():
await session.shutdown()

View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import typing
import traceback
from .. import loader, entities as tools_entities
from ....core import app, entities as core_entities
from ....plugin import context as plugin_context
@loader.loader_class("plugin-tool-loader")
class PluginToolLoader(loader.ToolLoader):
"""插件工具加载器。
本加载器中不存储工具信息,仅负责从插件系统中获取工具信息。
"""
async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]:
# 从插件系统获取工具(内容函数)
all_functions: list[tools_entities.LLMFunction] = []
for plugin in self.ap.plugin_mgr.plugins(
enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
all_functions.extend(plugin.content_functions)
return all_functions
async def has_tool(self, name: str) -> bool:
"""检查工具是否存在"""
for plugin in self.ap.plugin_mgr.plugins(
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
for function in plugin.content_functions:
if function.name == name:
return True
return False
async def _get_function_and_plugin(
self, name: str
) -> typing.Tuple[tools_entities.LLMFunction, plugin_context.BasePlugin]:
"""获取函数和插件实例"""
for plugin in self.ap.plugin_mgr.plugins(
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
for function in plugin.content_functions:
if function.name == name:
return function, plugin.plugin_inst
return None, None
async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any:
try:
function, plugin = await self._get_function_and_plugin(name)
if function is None:
return None
parameters = parameters.copy()
parameters = {"query": query, **parameters}
return await function.func(plugin, **parameters)
except Exception as e:
self.ap.logger.error(f"执行函数 {name} 时发生错误: {e}")
traceback.print_exc()
return f"error occurred when executing function {name}: {e}"
finally:
plugin = None
for p in self.ap.plugin_mgr.plugins():
if function in p.content_functions:
plugin = p
break
if plugin is not None:
await self.ap.ctr_mgr.usage.post_function_record(
plugin={
"name": plugin.plugin_name,
"remote": plugin.plugin_source,
"version": plugin.plugin_version,
"author": plugin.plugin_author,
},
function_name=function.name,
function_description=function.description,
)
async def shutdown(self):
"""关闭工具"""
pass

View File

@@ -4,8 +4,9 @@ import typing
import traceback import traceback
from ...core import app, entities as core_entities from ...core import app, entities as core_entities
from . import entities from . import entities, loader as tools_loader
from ...plugin import context as plugin_context from ...plugin import context as plugin_context
from .loaders import plugin, mcp
class ToolManager: class ToolManager:
@@ -13,33 +14,26 @@ class ToolManager:
ap: app.Application ap: app.Application
loaders: list[tools_loader.ToolLoader]
def __init__(self, ap: app.Application): def __init__(self, ap: app.Application):
self.ap = ap self.ap = ap
self.all_functions = [] self.all_functions = []
self.loaders = []
async def initialize(self): async def initialize(self):
pass
async def get_function_and_plugin( for loader_cls in tools_loader.preregistered_loaders:
self, name: str loader_inst = loader_cls(self.ap)
) -> typing.Tuple[entities.LLMFunction, plugin_context.BasePlugin]: await loader_inst.initialize()
"""获取函数和插件实例""" self.loaders.append(loader_inst)
for plugin in self.ap.plugin_mgr.plugins(
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
for function in plugin.content_functions:
if function.name == name:
return function, plugin.plugin_inst
return None, None
async def get_all_functions(self, plugin_enabled: bool=None, plugin_status: plugin_context.RuntimeContainerStatus=None) -> list[entities.LLMFunction]: async def get_all_functions(self, plugin_enabled: bool=None) -> list[entities.LLMFunction]:
"""获取所有函数""" """获取所有函数"""
all_functions: list[entities.LLMFunction] = [] all_functions: list[entities.LLMFunction] = []
for plugin in self.ap.plugin_mgr.plugins( for loader in self.loaders:
enabled=plugin_enabled, status=plugin_status all_functions.extend(await loader.get_tools(plugin_enabled))
):
all_functions.extend(plugin.content_functions)
return all_functions return all_functions
@@ -102,38 +96,13 @@ class ToolManager:
) -> typing.Any: ) -> typing.Any:
"""执行函数调用""" """执行函数调用"""
try: for loader in self.loaders:
if await loader.has_tool(name):
return await loader.invoke_tool(query, name, parameters)
else:
raise ValueError(f"未找到工具: {name}")
function, plugin = await self.get_function_and_plugin(name) async def shutdown(self):
if function is None: """关闭所有工具"""
return None for loader in self.loaders:
await loader.shutdown()
parameters = parameters.copy()
parameters = {"query": query, **parameters}
return await function.func(plugin, **parameters)
except Exception as e:
self.ap.logger.error(f"执行函数 {name} 时发生错误: {e}")
traceback.print_exc()
return f"error occurred when executing function {name}: {e}"
finally:
plugin = None
for p in self.ap.plugin_mgr.plugins():
if function in p.content_functions:
plugin = p
break
if plugin is not None:
await self.ap.ctr_mgr.usage.post_function_record(
plugin={
"name": plugin.plugin_name,
"remote": plugin.plugin_source,
"version": plugin.plugin_version,
"author": plugin.plugin_author,
},
function_name=function.name,
function_description=function.description,
)

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.10" semantic_version = "v3.4.11.1"
debug_mode = False debug_mode = False

View File

@@ -15,6 +15,9 @@ import asyncio
from urllib.parse import urlparse from urllib.parse import urlparse
async def get_gewechat_image_base64( async def get_gewechat_image_base64(
gewechat_url: str, gewechat_url: str,
gewechat_file_url: str, gewechat_file_url: str,
@@ -67,6 +70,7 @@ async def get_gewechat_image_base64(
} }
) as response: ) as response:
if response.status != 200: if response.status != 200:
# print(response)
raise Exception(f"获取gewechat图片下载失败: {await response.text()}") raise Exception(f"获取gewechat图片下载失败: {await response.text()}")
resp_data = await response.json() resp_data = await response.json()
@@ -108,6 +112,9 @@ async def get_gewechat_image_base64(
raise Exception(f"获取图片失败: {str(e)}") from e raise Exception(f"获取图片失败: {str(e)}") from e
async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]: async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]:
""" """
下载企业微信图片并转换为 base64 下载企业微信图片并转换为 base64

View File

@@ -32,6 +32,8 @@ gewechat-client
dingtalk_stream dingtalk_stream
dashscope dashscope
python-telegram-bot python-telegram-bot
certifi
mcp
# indirect # indirect
taskgroup==0.0.0a4 taskgroup==0.0.0a4

View File

@@ -166,61 +166,72 @@
{ {
"name": "glm-4-plus", "name": "glm-4-plus",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-0520", "name": "glm-4-0520",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-air", "name": "glm-4-air",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-airx", "name": "glm-4-airx",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-long", "name": "glm-4-long",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-flashx", "name": "glm-4-flashx",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-flash", "name": "glm-4-flash",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4v-plus", "name": "glm-4v-plus",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "glm-4v", "name": "glm-4v",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "glm-4v-flash", "name": "glm-4v-flash",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "glm-zero-preview", "name": "glm-zero-preview",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
} }
] ]
} }

View File

@@ -16,7 +16,7 @@
}, },
"income-msg-check": true, "income-msg-check": true,
"ignore-rules": { "ignore-rules": {
"prefix": ["/"], "prefix": [],
"regexp": [] "regexp": []
}, },
"check-sensitive-words": true, "check-sensitive-words": true,

View File

@@ -77,6 +77,8 @@
"EncodingAESKey":"", "EncodingAESKey":"",
"AppID":"", "AppID":"",
"AppSecret":"", "AppSecret":"",
"Mode":"drop",
"LoadingMessage":"AI正在思考中请发送任意内容获取回复。",
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 2287 "port": 2287
}, },

View File

@@ -138,5 +138,8 @@
"date": "2023-08-10" "date": "2023-08-10"
} }
} }
},
"mcp": {
"servers": []
} }
} }

View File

@@ -23,7 +23,7 @@
"items": { "items": {
"type": "string", "type": "string",
"format": "regex", "format": "regex",
"pattern": "^(person|group)_(\\d)*$" "pattern": "^(person|group)_.*$"
}, },
"default": [] "default": []
}, },
@@ -34,7 +34,7 @@
"items": { "items": {
"type": "string", "type": "string",
"format": "regex", "format": "regex",
"pattern": "^(person|group)_(\\d)*$" "pattern": "^(person|group)_.*$"
}, },
"default": [] "default": []
} }
@@ -99,7 +99,7 @@
} }
}, },
"patternProperties": { "patternProperties": {
"^\\d+$": { "^.*$": {
"type": "object", "type": "object",
"properties": { "properties": {
"at": { "at": {

View File

@@ -385,6 +385,17 @@
"default": "", "default": "",
"description": "微信公众号的AppSecret" "description": "微信公众号的AppSecret"
}, },
"Mode": {
"type": "string",
"default": "drop",
"description": "对于超过15s的响应的处理模式",
"enum": ["drop", "passive"]
},
"LoadingMessage": {
"type": "string",
"default": "AI正在思考中请发送任意内容获取回复。",
"description": "当使用被动模式时,显示给用户的提示信息"
},
"host": { "host": {
"type": "string", "type": "string",
"default": "0.0.0.0", "default": "0.0.0.0",

View File

@@ -520,6 +520,87 @@
} }
} }
} }
},
"mcp": {
"type": "object",
"title": "MCP 配置",
"properties": {
"servers": {
"type": "array",
"title": "MCP 服务器配置",
"default": [],
"items": {
"type": "object",
"oneOf": [
{
"title": "Stdio 模式服务器",
"properties": {
"mode": {
"type": "string",
"title": "模式",
"const": "stdio"
},
"enable": {
"type": "boolean",
"title": "启用"
},
"name": {
"type": "string",
"title": "名称"
},
"command": {
"type": "string",
"title": "启动命令"
},
"args": {
"type": "array",
"title": "启动参数",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"default": {}
}
}
},
{
"title": "SSE 模式服务器",
"properties": {
"mode": {
"type": "string",
"title": "模式",
"const": "sse"
},
"enable": {
"type": "boolean",
"title": "启用"
},
"name": {
"type": "string",
"title": "名称"
},
"url": {
"type": "string",
"title": "URL"
},
"headers": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "请求超时时间",
"default": 10
}
}
}
]
}
}
}
} }
} }
} }

View File

@@ -9,7 +9,7 @@
"items": { "items": {
"type": "string", "type": "string",
"format": "regex", "format": "regex",
"pattern": "^(person|group)_(\\d+)$" "pattern": "^(person|group)_.*$"
}, },
"default": [] "default": []
}, },
@@ -52,7 +52,7 @@
} }
}, },
"patternProperties": { "patternProperties": {
"^(person|group)_(\\d+)$": { "^(person|group)_.*$": {
"type": "integer" "type": "integer"
} }
} }