Compare commits

..

63 Commits

Author SHA1 Message Date
Junyan Qin
ef044f4fc7 chore: release v3.4.9.1 2025-02-24 12:23:08 +08:00
Junyan Qin
7cd4e904ca perf: add converting options for dify thinking tips (#1108) 2025-02-24 12:17:33 +08:00
Junyan Qin
c724494ee7 fix: revert streaming resp in chatcmpl 2025-02-24 11:07:42 +08:00
Junyan Qin
cdb2db348e chore: release v3.4.9 2025-02-23 23:06:40 +08:00
Junyan Qin (Chin)
5873d4696f Merge pull request #1118 from RockChinQ/feat/volcengine
feat: supports for `volcark`
2025-02-23 23:05:16 +08:00
Junyan Qin
613787f49c doc: bad url in README 2025-02-23 23:02:07 +08:00
Junyan Qin
f620874251 chore: rename volcengine to volcark 2025-02-23 22:52:50 +08:00
Junyan Qin
1f08082a58 feat: add supports for volcengine (#1114) 2025-02-23 22:42:20 +08:00
Junyan Qin (Chin)
8f5da1677b Merge pull request #1113 from RockChinQ/feat/component-manifest
feat: component discovering engine
2025-02-23 22:16:38 +08:00
Junyan Qin
5439a3a31f feat: add manifest for LLMAPIRequester 2025-02-22 21:33:35 +08:00
Junyan Qin
d92ee23764 feat: discover engine & manifests for platform adapters 2025-02-22 14:49:05 +08:00
Junyan Qin
71ecfc2566 doc(README): update community qq group number 2025-02-18 20:02:25 +08:00
Junyan Qin
c0787e0bb6 doc(README): add GitCode badge for CN README 2025-02-18 14:08:38 +08:00
Junyan Qin
357da2d236 doc: update README 2025-02-14 13:46:24 +08:00
Junyan Qin
6071241872 chore: release v3.4.8 2025-02-14 13:36:59 +08:00
Junyan Qin
ab93c67081 doc(README): telegram comment 2025-02-14 13:36:26 +08:00
Junyan Qin (Chin)
7af6b833df Merge pull request #1079 from RockChinQ/feat/telegram
feat: add adapter `telegram`
2025-02-14 13:34:38 +08:00
Junyan Qin
3e4b85aeb5 chore: configurations 2025-02-14 13:12:49 +08:00
Junyan Qin
2b6be04c5d feat: telegram adapter 2025-02-14 12:55:48 +08:00
Junyan Qin
b2d1c82196 stash 2025-02-14 00:10:21 +08:00
Junyan Qin
a19da7b923 doc(README): comments for DingTalk 2025-02-14 00:04:55 +08:00
Junyan Qin (Chin)
4a9a78d07b Merge pull request #1077 from wangcham/feat/dingtalk
feat: add support for dingtalk
2025-02-14 00:02:07 +08:00
Junyan Qin (Chin)
300dbd076f Merge branch 'master' into feat/dingtalk 2025-02-14 00:01:03 +08:00
Junyan Qin
ddf52524a8 chore: migrations 2025-02-13 20:03:06 +08:00
wangcham
7dcc44b4fc feat: add support for dingtalk 2025-02-13 03:47:45 -05:00
Junyan Qin
75af631c17 chore: release v3.4.7.2 2025-02-13 00:49:19 +08:00
WangCham
04dd4fce68 Update wecom.py
fix the bug that wecom couldnt send message when accept an image.
2025-02-12 22:04:16 +08:00
Junyan Qin (Chin)
2776a95a40 Merge pull request #1068 from leeAx/feat/lark_http
feat(lark):enable lark callback
2025-02-12 21:17:34 +08:00
Junyan Qin (Chin)
dc93b37fd6 Merge branch 'master' into feat/lark_http 2025-02-12 21:13:54 +08:00
Junyan Qin
6502a64cab feat(lark): supports for encrypted message 2025-02-12 21:12:53 +08:00
Junyan Qin
5311e78776 chore: release v3.4.7.1 2025-02-12 15:16:02 +08:00
Junyan Qin
35721c1340 doc(README): update comment of aliyun bailian 2025-02-12 13:47:01 +08:00
Junyan Qin (Chin)
a76df22cab Merge pull request #1066 from lyg09270/master
阿里云百炼平台通用模型和自定义模型应用API支持
2025-02-12 13:44:19 +08:00
Junyan Qin
a90f996b24 chore: related configuration of dashscope runner 2025-02-12 13:33:07 +08:00
Junyan Qin (Chin)
c96d4456ea Merge pull request #1035 from wanjiaju3108/master
阿里云大模型服务适配
2025-02-12 11:27:05 +08:00
Junyan Qin (Chin)
d1df6d993f Merge branch 'master' into master 2025-02-12 11:26:35 +08:00
Junyan Qin
191f8866ae chore(bailian): related configuration 2025-02-12 11:25:28 +08:00
Junyan Qin
e17da4e2ee chore: remove models of MaaS from llm-models.json 2025-02-12 11:11:07 +08:00
lipu
2c3fdb4fdc feat(lark):enable lark callback 2025-02-11 21:37:07 +08:00
Junyan Qin
e89c6b68c9 fix: f the stream resp 2025-02-11 21:19:15 +08:00
Civic_Crab
51cca31f04 去除qwen请求器 2025-02-11 18:52:27 +08:00
Civic_Crab
e51950aa75 修改llm-model.json,去除舍弃的qwen功能 2025-02-11 18:50:56 +08:00
Civic_Crab
4c344e0636 阿里云百炼平台应用API支持 2025-02-11 18:48:21 +08:00
Civic_Crab
90261d1f55 Merge branch 'master' of https://github.com/lyg09270/LangBot 2025-02-11 18:40:13 +08:00
Junyan Qin
fabf93f741 chore: release v3.4.7 2025-02-11 12:56:13 +08:00
Junyan Qin
ab8ef01c76 docs: update trendshift badge link 2025-02-11 12:28:11 +08:00
Junyan Qin (Chin)
e463d3a8fe Merge pull request #1057 from eltociear/add-japanese-readme
docs: add Japanese README
2025-02-11 12:19:39 +08:00
Junyan Qin
a6bc617a3b docs: add discord link 2025-02-11 12:19:12 +08:00
Ikko Eltociear Ashimine
1b1ccdd733 docs: add Japanese README
I created Japanese translated README.
2025-02-11 03:07:31 +09:00
Junyan Qin
8d00e710d5 doc(README): add official account compatibility comment 2025-02-11 00:26:26 +08:00
Junyan Qin (Chin)
de9e3bdbd5 Merge pull request #1055 from wangcham/feat/wxoa
feat: add support for wechat official account
2025-02-11 00:24:17 +08:00
Junyan Qin
b6e054a73f chore: migrations for officialaccount adapter 2025-02-11 00:23:38 +08:00
Junyan Qin (Chin)
a078b2cf12 Merge branch 'master' into feat/wxoa 2025-02-11 00:02:33 +08:00
wangcham
6f32bf9621 fix: wecom userid 2025-02-10 10:01:48 -05:00
wangcham
ac628b26d9 feat:add support for wechat official account 2025-02-10 09:16:33 -05:00
wangcham
7ba655902b fix: wecom userid couldn't pass correctly 2025-02-10 09:11:27 -05:00
wangcham
05c1fdaa9e feat: add adapter for 微信公众号 2025-02-10 06:08:59 -05:00
Junyan Qin (Chin)
d7687913a9 doc(README.md): update trendingshift badge 2025-02-10 11:04:57 +08:00
Civic_Crab
9e718a2e8a 新增dashscope依赖 2025-02-09 06:39:39 +08:00
Civic_Crab
cbec2f6d02 新增dashscope依赖 2025-02-09 06:37:55 +08:00
Civic_Crab
52eb37d13d 支持阿里云百炼的通用模型和自定义大模型应用 2025-02-09 06:32:49 +08:00
wanjiaju
8e9f43885a 阿里云百炼适配
新增阿里云请求器配置、阿里云模型配置、阿里云令牌配置

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

新增硅基模型配置
2025-02-08 10:27:19 +08:00
74 changed files with 3097 additions and 206 deletions

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ qcapi
claude.json
bard.json
/*yaml
!components.yaml
!/docker-compose.yaml
data/labels/instance_id.json
.DS_Store

View File

@@ -6,7 +6,7 @@
<div align="center">
<a href="https://trendshift.io/repositories/6187" target="_blank"><img src="https://trendshift.io/api/badge/repositories/6187" alt="RockChinQ%2FQChatGPT | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://docs.langbot.app">项目主页</a>
<a href="https://docs.langbot.app/insight/intro.htmll">功能介绍</a>
@@ -22,12 +22,14 @@
<br/>
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-1030838208-blue)](https://qm.qq.com/q/PF9OuQCCcM)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-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)
![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">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
[简体中文](README.md) / [English](README_EN.md)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
</div>
@@ -35,7 +37,7 @@
## ✨ 特性
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、飞书、Discord、个人微信,后续还将支持 WhatsApp、Telegram 等平台。
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
@@ -85,12 +87,13 @@
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | |
| 飞书 | ✅ | |
| Discord | ✅ | |
| 个人微信 | ✅ | 使用 [Gewechat](https://github.com/Devo919/Gewechat) 接入 |
| Telegram | 🚧 | |
| 微信公众号 | | |
| 飞书 | ✅ | |
| 钉钉 | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| WhatsApp | 🚧 | |
| 钉钉 | 🚧 | |
🚧: 正在开发中
@@ -109,7 +112,8 @@
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
## 😘 社区贡献

View File

@@ -5,7 +5,7 @@
<div align="center">
<a href="https://trendshift.io/repositories/6187" target="_blank"><img src="https://trendshift.io/api/badge/repositories/6187" alt="RockChinQ%2FQChatGPT | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://docs.langbot.app">Home</a>
<a href="https://docs.langbot.app/insight/intro.htmll">Features</a>
@@ -21,12 +21,12 @@
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
</div>
@@ -34,7 +34,7 @@
## ✨ Features
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Deeply integrates with [Dify](https://dify.ai). Currently supports QQ, QQ Channel, WeCom, Lark, Discord, personal WeChat, and will support WhatsApp, Telegram, etc. in the future.
- 💬 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.
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Rich ecology, currently has dozens of [plugins](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web UI: Support management LangBot instance through the browser, for details, see [documentation](https://docs.langbot.app/webui/intro.html)
@@ -85,12 +85,12 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| Personal QQ | ✅ | |
| QQ Official API | ✅ | |
| WeCom | ✅ | |
| Lark | ✅ | |
| Discord | ✅ | |
| Personal WeChat | ✅ | Use [Gewechat](https://github.com/Devo919/Gewechat) to access |
| Telegram | 🚧 | |
| Lark | | |
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| WhatsApp | 🚧 | |
| DingTalk | 🚧 | |
🚧: In development
@@ -109,7 +109,8 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS) |
| [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 |
## 🤝 Community Contribution

123
README_JP.md Normal file
View File

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

15
components.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Blueprint
metadata:
name: builtin-components
label:
en_US: Builtin Components
zh_CN: 内置组件
spec:
components:
ComponentTemplate:
fromFiles:
- pkg/platform/adapter.yaml
MessagePlatformAdapter:
fromDirs:
- path: pkg/platform/sources/

View File

@@ -0,0 +1,29 @@
import asyncio
import dingtalk_stream
from dingtalk_stream import AckMessage
class EchoTextHandler(dingtalk_stream.ChatbotHandler):
def __init__(self, client):
self.msg_id = ''
self.incoming_message = None
self.client = client # 用于更新 DingTalkClient 中的 incoming_message
"""处理钉钉消息"""
async def process(self, callback: dingtalk_stream.CallbackMessage):
incoming_message = dingtalk_stream.ChatbotMessage.from_dict(callback.data)
if incoming_message.message_id != self.msg_id:
self.msg_id = incoming_message.message_id
await self.client.update_incoming_message(incoming_message)
return AckMessage.STATUS_OK, 'OK'
async def get_incoming_message(self):
"""异步等待消息的到来"""
while self.incoming_message is None:
await asyncio.sleep(0.1) # 异步等待,避免阻塞
return self.incoming_message
async def get_dingtalk_client(client_id, client_secret):
from api import DingTalkClient # 延迟导入,避免循环导入
return DingTalkClient(client_id, client_secret)

View File

174
libs/dingtalk_api/api.py Normal file
View File

@@ -0,0 +1,174 @@
import base64
import time
from typing import Callable
import dingtalk_stream
from .EchoHandler import EchoTextHandler
from .dingtalkevent import DingTalkEvent
import httpx
import traceback
class DingTalkClient:
def __init__(self, client_id: str, client_secret: str,robot_name:str,robot_code:str):
"""初始化 WebSocket 连接并自动启动"""
self.credential = dingtalk_stream.Credential(client_id, client_secret)
self.client = dingtalk_stream.DingTalkStreamClient(self.credential)
self.key = client_id
self.secret = client_secret
# 在 DingTalkClient 中传入自己作为参数,避免循环导入
self.EchoTextHandler = EchoTextHandler(self)
self.client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC, self.EchoTextHandler)
self._message_handlers = {
"example":[],
}
self.access_token = ''
self.robot_name = robot_name
self.robot_code = robot_code
self.access_token_expiry_time = ''
async def get_access_token(self):
url = "https://api.dingtalk.com/v1.0/oauth2/accessToken"
headers = {
"Content-Type": "application/json"
}
data = {
"appKey": self.key,
"appSecret": self.secret
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(url,json=data,headers=headers)
if response.status_code == 200:
response_data = response.json()
self.access_token = response_data.get("accessToken")
expires_in = int(response_data.get("expireIn",7200))
self.access_token_expiry_time = time.time() + expires_in - 60
except Exception as e:
raise Exception(e)
async def is_token_expired(self):
"""检查token是否过期"""
if self.access_token_expiry_time is None:
return True
return time.time() > self.access_token_expiry_time
async def check_access_token(self):
if not self.access_token or await self.is_token_expired():
return False
return bool(self.access_token and self.access_token.strip())
async def download_image(self,download_code:str):
if not await self.check_access_token():
await self.get_access_token()
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
params = {
"downloadCode":download_code,
"robotCode":self.robot_code
}
headers ={
"x-acs-dingtalk-access-token": self.access_token
}
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, json=params)
if response.status_code == 200:
result = response.json()
download_url = result.get("downloadUrl")
else:
raise Exception(f"Error: {response.status_code}, {response.text}")
if download_url:
return await self.download_url_to_base64(download_url)
async def download_url_to_base64(self,download_url):
async with httpx.AsyncClient() as client:
response = await client.get(download_url)
if response.status_code == 200:
file_bytes = response.content
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
return base64_str
else:
raise Exception("获取图片失败")
async def update_incoming_message(self, message):
"""异步更新 DingTalkClient 中的 incoming_message"""
message_data = await self.get_message(message)
if message_data:
event = DingTalkEvent.from_payload(message_data)
if event:
await self._handle_message(event)
async def send_message(self,content:str,incoming_message):
self.EchoTextHandler.reply_text(content,incoming_message)
async def get_incoming_message(self):
"""获取收到的消息"""
return await self.EchoTextHandler.get_incoming_message()
def on_message(self, msg_type: str):
def decorator(func: Callable[[DingTalkEvent], 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: DingTalkEvent):
"""
处理消息事件。
"""
msg_type = event.conversation
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
await handler(event)
async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage):
try:
message_data = {
"IncomingMessage":incoming_message,
}
if str(incoming_message.conversation_type) == '1':
message_data["conversation_type"] = 'FriendMessage'
elif str(incoming_message.conversation_type) == '2':
message_data["conversation_type"] = 'GroupMessage'
if incoming_message.message_type == 'richText':
data = incoming_message.rich_text_content.to_dict()
for item in data['richText']:
if 'text' in item:
message_data["Content"] = item['text']
if incoming_message.get_image_list()[0]:
message_data["Picture"] = await self.download_image(incoming_message.get_image_list()[0])
message_data["Type"] = 'text'
elif incoming_message.message_type == 'text':
message_data['Content'] = incoming_message.get_text_list()[0]
message_data["Type"] = 'text'
elif incoming_message.message_type == 'picture':
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
message_data['Type'] = 'image'
# 删掉开头的@消息
if message_data["Content"].startswith("@"+self.robot_name):
message_data["Content"][len("@"+self.robot_name):]
except Exception:
traceback.print_exc()
return message_data
async def start(self):
"""启动 WebSocket 连接,监听消息"""
await self.client.start()

View File

@@ -0,0 +1,64 @@
from typing import Dict, Any, Optional
class DingTalkEvent(dict):
@staticmethod
def from_payload(payload: Dict[str, Any]) -> Optional["DingTalkEvent"]:
try:
event = DingTalkEvent(payload)
return event
except KeyError:
return None
@property
def content(self):
return self.get("Content","")
@property
def incoming_message(self):
return self.get("IncomingMessage")
@property
def type(self):
return self.get("Type","")
@property
def picture(self):
return self.get("Picture","")
@property
def conversation(self):
return self.get("conversation_type","")
def __getattr__(self, key: str) -> Optional[Any]:
"""
允许通过属性访问数据中的任意字段。
Args:
key (str): 字段名。
Returns:
Optional[Any]: 字段值。
"""
return self.get(key)
def __setattr__(self, key: str, value: Any) -> None:
"""
允许通过属性设置数据中的任意字段。
Args:
key (str): 字段名。
value (Any): 字段值。
"""
self[key] = value
def __repr__(self) -> str:
"""
生成事件对象的字符串表示。
Returns:
str: 字符串表示。
"""
return f"<WecomEvent {super().__repr__()}>"

View File

View File

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

View File

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

View File

@@ -129,7 +129,6 @@ class WecomClient():
self.access_token = await self.get_access_token(self.secret)
url = self.base_url+'/message/send?access_token='+self.access_token
async with httpx.AsyncClient() as client:
params={
"touser" : user_id,
@@ -228,7 +227,7 @@ class WecomClient():
if message_data["MsgType"] == "image":
message_data["MediaId"] = root.find("MediaId").text if root.find("MediaId") is not None else None
message_data["PicUrl"] = root.find("PicUrl").text if root.find("PicUrl") is not None else None
return message_data
@staticmethod

View File

@@ -25,6 +25,7 @@ from ..utils import version as version_mgr, proxy as proxy_mgr, announce as anno
from ..persistence import mgr as persistencemgr
from ..api.http.controller import main as http_controller
from ..api.http.service import user as user_service
from ..discover import engine as discover_engine
from ..utils import logcache, ip
from . import taskmgr
from . import entities as core_entities
@@ -38,6 +39,8 @@ class Application:
# asyncio_tasks: list[asyncio.Task] = []
task_mgr: taskmgr.AsyncTaskManager = None
discover: discover_engine.ComponentDiscoveryEngine = None
platform_mgr: im_mgr.PlatformManager = None
cmd_mgr: cmdmgr.CommandManager = None

View File

@@ -29,7 +29,10 @@ required_deps = {
"lark_oapi": "lark-oapi",
"discord": "discord.py",
"cryptography": "cryptography",
"gewechat_client": "gewechat-client"
"gewechat_client": "gewechat-client",
"dingtalk_stream": "dingtalk_stream",
"dashscope": "dashscope",
"telegram": "python-telegram-bot",
}

View File

@@ -57,7 +57,7 @@ class Query(pydantic.BaseModel):
message_chain: platform_message.MessageChain
"""消息链platform收到的原始消息链"""
adapter: msadapter.MessageSourceAdapter
adapter: msadapter.MessagePlatformAdapter
"""消息平台适配器对象单个app中可能启用了多个消息平台适配器此对象表明发起此query的适配器"""
session: typing.Optional[Session] = None

View File

@@ -10,11 +10,12 @@ class WecomConfigMigration(migration.Migration):
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'wecom':
return False
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
# if adapter['adapter'] == 'wecom':
# return False
return True
# return True
return False
async def run(self):
"""执行迁移"""

View File

@@ -10,11 +10,12 @@ class LarkConfigMigration(migration.Migration):
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'lark':
return False
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
# if adapter['adapter'] == 'lark':
# return False
return True
# return True
return False
async def run(self):
"""执行迁移"""
@@ -23,7 +24,10 @@ class LarkConfigMigration(migration.Migration):
"enable": False,
"app_id": "cli_abcdefgh",
"app_secret": "XXXXXXXXXX",
"bot_name": "LangBot"
"bot_name": "LangBot",
"enable-webhook": False,
"port": 2285,
"encrypt-key": "xxxxxxxxx"
})
await self.ap.platform_cfg.dump_config()

View File

@@ -10,11 +10,12 @@ class DiscordConfigMigration(migration.Migration):
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'discord':
return False
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
# if adapter['adapter'] == 'discord':
# return False
return True
# return True
return False
async def run(self):
"""执行迁移"""

View File

@@ -10,11 +10,12 @@ class GewechatConfigMigration(migration.Migration):
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'gewechat':
return False
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
# if adapter['adapter'] == 'gewechat':
# return False
return True
# return True
return False
async def run(self):
"""执行迁移"""

View File

@@ -10,11 +10,12 @@ class QQOfficialConfigMigration(migration.Migration):
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'qqofficial':
return False
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
# if adapter['adapter'] == 'qqofficial':
# return False
return True
# return True
return False
async def run(self):
"""执行迁移"""

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("lark-config-cmpl", 30)
class LarkConfigCmplMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'lark':
if 'enable-webhook' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'lark':
if 'enable-webhook' not in adapter:
adapter['enable-webhook'] = False
if 'port' not in adapter:
adapter['port'] = 2285
if 'encrypt-key' not in adapter:
adapter['encrypt-key'] = "xxxxxxxxx"
await self.ap.platform_cfg.dump_config()

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("dify-thinking-config", 33)
class DifyThinkingConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
if 'options' not in self.ap.provider_cfg.data["dify-service-api"]:
return True
if 'convert-thinking-tips' not in self.ap.provider_cfg.data["dify-service-api"]["options"]:
return True
return False
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data["dify-service-api"]["options"] = {
"convert-thinking-tips": "plain"
}
await self.ap.provider_cfg.dump_config()

View File

@@ -18,6 +18,7 @@ from ...platform import manager as im_mgr
from ...persistence import mgr as persistencemgr
from ...api.http.controller import main as http_controller
from ...api.http.service import user as user_service
from ...discover import engine as discover_engine
from ...utils import logcache
from .. import taskmgr
@@ -32,6 +33,12 @@ class BuildAppStage(stage.BootingStage):
"""
ap.task_mgr = taskmgr.AsyncTaskManager(ap)
discover = discover_engine.ComponentDiscoveryEngine(ap)
discover.discover_blueprint(
"components.yaml"
)
ap.discover = discover
proxy_mgr = proxy.ProxyManager(ap)
await proxy_mgr.initialize()
ap.proxy_mgr = proxy_mgr

View File

@@ -9,7 +9,9 @@ from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_
from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config
from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_api_timeout_params, m018_xai_config, m019_zhipuai_config
from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config
from ..migrations import m026_qqofficial_config
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 m033_dify_thinking_config
@stage.stage_class("MigrationStage")
class MigrationStage(stage.BootingStage):

0
pkg/discover/__init__.py Normal file
View File

197
pkg/discover/engine.py Normal file
View File

@@ -0,0 +1,197 @@
from __future__ import annotations
import typing
import importlib
import os
import inspect
import yaml
import pydantic
from ..core import app
class I18nString(pydantic.BaseModel):
"""国际化字符串"""
en_US: str
"""英文"""
zh_CN: typing.Optional[str] = None
"""中文"""
ja_JP: typing.Optional[str] = None
"""日文"""
class Metadata(pydantic.BaseModel):
"""元数据"""
name: str
"""名称"""
label: I18nString
"""标签"""
description: typing.Optional[I18nString] = None
"""描述"""
icon: typing.Optional[str] = None
"""图标"""
class PythonExecution(pydantic.BaseModel):
"""Python执行"""
path: str
"""路径"""
attr: str
"""属性"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.path.startswith('./'):
self.path = self.path[2:]
class Execution(pydantic.BaseModel):
"""执行"""
python: PythonExecution
"""Python执行"""
class Component(pydantic.BaseModel):
"""组件清单"""
owner: str
"""组件所属"""
manifest: typing.Dict[str, typing.Any]
"""组件清单内容"""
rel_path: str
"""组件清单相对main.py的路径"""
_metadata: Metadata
"""组件元数据"""
_spec: typing.Dict[str, typing.Any]
"""组件规格"""
_execution: Execution
"""组件执行"""
def __init__(self, owner: str, manifest: typing.Dict[str, typing.Any], rel_path: str):
super().__init__(
owner=owner,
manifest=manifest,
rel_path=rel_path
)
self._metadata = Metadata(**manifest['metadata'])
self._spec = manifest['spec']
self._execution = Execution(**manifest['execution']) if 'execution' in manifest else None
@property
def kind(self) -> str:
"""组件类型"""
return self.manifest['kind']
@property
def metadata(self) -> Metadata:
"""组件元数据"""
return self._metadata
@property
def spec(self) -> typing.Dict[str, typing.Any]:
"""组件规格"""
return self._spec
@property
def execution(self) -> Execution:
"""组件执行"""
return self._execution
def get_python_component_class(self) -> typing.Type[typing.Any]:
"""获取Python组件类"""
parent_path = os.path.dirname(self.rel_path)
module_path = os.path.join(parent_path, self.execution.python.path)
if module_path.endswith('.py'):
module_path = module_path[:-3]
module_path = module_path.replace('/', '.').replace('\\', '.')
module = importlib.import_module(module_path)
return getattr(module, self.execution.python.attr)
class ComponentDiscoveryEngine:
"""组件发现引擎"""
ap: app.Application
"""应用实例"""
components: typing.Dict[str, typing.List[Component]] = {}
"""组件列表"""
def __init__(self, ap: app.Application):
self.ap = ap
def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component:
"""加载组件清单"""
with open(path, 'r') as f:
manifest = yaml.safe_load(f)
comp = Component(
owner=owner,
manifest=manifest,
rel_path=path
)
if not no_save:
if comp.kind not in self.components:
self.components[comp.kind] = []
self.components[comp.kind].append(comp)
return comp
def load_component_manifests_in_dir(self, path: str, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]:
"""加载目录中的组件清单"""
components: typing.List[Component] = []
for file in os.listdir(path):
if file.endswith('.yaml') or file.endswith('.yml'):
components.append(self.load_component_manifest(os.path.join(path, file), owner, no_save))
return components
def load_blueprint_comp_group(self, group: dict, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]:
"""加载蓝图组件组"""
components: typing.List[Component] = []
if 'fromFiles' in group:
for file in group['fromFiles']:
components.append(self.load_component_manifest(file, owner, no_save))
if 'fromDirs' in group:
for dir in group['fromDirs']:
path = dir['path']
# depth = dir['depth']
components.extend(self.load_component_manifests_in_dir(path, owner, no_save))
return components
def discover_blueprint(self, blueprint_manifest_path: str, owner: str = 'builtin'):
"""发现蓝图"""
blueprint_manifest = self.load_component_manifest(blueprint_manifest_path, owner, no_save=True)
assert blueprint_manifest.kind == 'Blueprint', '`Kind` must be `Blueprint`'
components: typing.Dict[str, typing.List[Component]] = {}
# load ComponentTemplate first
if 'ComponentTemplate' in blueprint_manifest.spec['components']:
components['ComponentTemplate'] = self.load_blueprint_comp_group(blueprint_manifest.spec['components']['ComponentTemplate'], owner)
for name, component in blueprint_manifest.spec['components'].items():
if name == 'ComponentTemplate':
continue
components[name] = self.load_blueprint_comp_group(component, owner)
return blueprint_manifest, components
def get_components_by_kind(self, kind: str) -> typing.List[Component]:
"""获取指定类型的组件"""
if kind not in self.components:
raise ValueError(f'No components found for kind: {kind}')
return self.components[kind]

View File

@@ -33,7 +33,7 @@ class QueryPool:
sender_id: typing.Union[int, str],
message_event: platform_events.MessageEvent,
message_chain: platform_message.MessageChain,
adapter: msadapter.MessageSourceAdapter
adapter: msadapter.MessagePlatformAdapter
) -> entities.Query:
async with self.condition:
query = entities.Query(

View File

@@ -10,27 +10,7 @@ from .types import message as platform_message
from .types import events as platform_events
preregistered_adapters: list[typing.Type[MessageSourceAdapter]] = []
def adapter_class(
name: str
):
"""消息平台适配器类装饰器
Args:
name (str): 适配器名称
Returns:
typing.Callable[[typing.Type[MessageSourceAdapter]], typing.Type[MessageSourceAdapter]]: 装饰器
"""
def decorator(cls: typing.Type[MessageSourceAdapter]) -> typing.Type[MessageSourceAdapter]:
cls.name = name
preregistered_adapters.append(cls)
return cls
return decorator
class MessageSourceAdapter(metaclass=abc.ABCMeta):
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
"""消息平台适配器基类"""
name: str
@@ -89,7 +69,7 @@ class MessageSourceAdapter(metaclass=abc.ABCMeta):
def register_listener(
self,
event_type: typing.Type[platform_message.Event],
callback: typing.Callable[[platform_message.Event, MessageSourceAdapter], None]
callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None]
):
"""注册事件监听器
@@ -102,7 +82,7 @@ class MessageSourceAdapter(metaclass=abc.ABCMeta):
def unregister_listener(
self,
event_type: typing.Type[platform_message.Event],
callback: typing.Callable[[platform_message.Event, MessageSourceAdapter], None]
callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None]
):
"""注销事件监听器

14
pkg/platform/adapter.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: ComponentTemplate
metadata:
name: MessagePlatformAdapter
label:
en_US: Message Platform Adapter
zh_CN: 消息平台适配器模板类
spec:
type:
- python
execution:
python:
path: ./adapter.py
attr: MessagePlatformAdapter

View File

@@ -18,6 +18,8 @@ from .types import message as platform_message
from .types import events as platform_events
from .types import entities as platform_entities
from ..discover import engine
# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题
from . import types as mirai
sys.modules['mirai'] = mirai
@@ -27,7 +29,9 @@ sys.modules['mirai'] = mirai
class PlatformManager:
# adapter: msadapter.MessageSourceAdapter = None
adapters: list[msadapter.MessageSourceAdapter] = []
adapters: list[msadapter.MessagePlatformAdapter] = []
message_platform_adapter_components: list[engine.Component] = []
# modern
ap: app.Application = None
@@ -39,9 +43,13 @@ class PlatformManager:
async def initialize(self):
from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat
components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter')
async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter):
self.message_platform_adapter_components = components
# from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat, officialaccount, telegram, dingtalk
async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessagePlatformAdapter):
await self.ap.query_pool.add_query(
launcher_type=core_entities.LauncherTypes.PERSON,
@@ -52,7 +60,7 @@ class PlatformManager:
adapter=adapter
)
async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessageSourceAdapter):
async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessagePlatformAdapter):
await self.ap.query_pool.add_query(
launcher_type=core_entities.LauncherTypes.GROUP,
@@ -76,10 +84,10 @@ class PlatformManager:
found = False
for adapter in msadapter.preregistered_adapters:
if adapter.name == adapter_name:
for adapter in self.message_platform_adapter_components:
if adapter.metadata.name == adapter_name:
found = True
adapter_cls = adapter
adapter_cls = adapter.get_python_component_class()
adapter_inst = adapter_cls(
cfg_copy,
@@ -102,7 +110,7 @@ class PlatformManager:
if len(self.adapters) == 0:
self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。')
async def write_back_config(self, adapter_inst: msadapter.MessageSourceAdapter, config: dict):
async def write_back_config(self, adapter_inst: msadapter.MessagePlatformAdapter, config: dict):
index = -2
for i, adapter in enumerate(self.adapters):
@@ -131,7 +139,7 @@ class PlatformManager:
self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg
await self.ap.platform_cfg.dump_config()
async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessageSourceAdapter):
async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessagePlatformAdapter):
if self.ap.platform_cfg.data['at-sender'] and isinstance(event, platform_events.GroupMessage):
@@ -152,7 +160,7 @@ class PlatformManager:
try:
tasks = []
for adapter in self.adapters:
async def exception_wrapper(adapter: msadapter.MessageSourceAdapter):
async def exception_wrapper(adapter: msadapter.MessagePlatformAdapter):
try:
await adapter.run_async()
except Exception as e:
@@ -167,7 +175,7 @@ class PlatformManager:
self.ap.task_mgr.create_task(
task,
kind="platform-adapter",
name=f"platform-adapter-{adapter.name}",
name=f"platform-adapter-{adapter.__class__.__name__}",
scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM],
)

View File

@@ -210,8 +210,7 @@ class AiocqhttpEventConverter(adapter.EventConverter):
)
@adapter.adapter_class("aiocqhttp")
class AiocqhttpAdapter(adapter.MessageSourceAdapter):
class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
bot: aiocqhttp.CQHttp
@@ -273,7 +272,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
):
async def on_message(event: aiocqhttp.Event):
self.bot_account_id = event.self_id
@@ -290,7 +289,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter):
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -0,0 +1,37 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: aiocqhttp
label:
en_US: OneBot v11 Adapter
zh_CN: OneBot v11 适配器
description:
en_US: OneBot v11 Adapter
zh_CN: OneBot v11 适配器
spec:
config:
- name: host
label:
en_US: Host
zh_CN: 主机
type: string
required: true
default: 0.0.0.0
- name: port
label:
en_US: Port
zh_CN: 端口
type: int
required: true
default: 2280
- name: access-token
label:
en_US: Access Token
zh_CN: 访问令牌
type: string
required: false
default: ""
execution:
python:
path: ./aiocqhttp.py
attr: AiocqhttpAdapter

View File

@@ -0,0 +1,182 @@
import traceback
import typing
from libs.dingtalk_api.dingtalkevent import DingTalkEvent
from pkg.platform.types import message as platform_message
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from pkg.core import app
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...command.errors import ParamNotEnoughError
from libs.dingtalk_api.api import DingTalkClient
import datetime
class DingTalkMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(
message_chain:platform_message.MessageChain
):
for msg in message_chain:
if type(msg) is platform_message.Plain:
return msg.text
@staticmethod
async def target2yiri(event:DingTalkEvent):
yiri_msg_list = []
yiri_msg_list.append(
platform_message.Source(id = '0',time=datetime.datetime.now())
)
if event.content:
yiri_msg_list.append(platform_message.Plain(text=event.content))
if event.picture:
yiri_msg_list.append(platform_message.Image(base64=event.picture))
chain = platform_message.MessageChain(yiri_msg_list)
return chain
class DingTalkEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(
event:platform_events.MessageEvent
):
return event.source_platform_object
@staticmethod
async def target2yiri(
event:DingTalkEvent
):
message_chain = await DingTalkMessageConverter.target2yiri(event)
if event.conversation == 'FriendMessage':
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id= 0,
nickname ='nickname',
remark=""
),
message_chain = message_chain,
time = datetime.datetime.now(),
source_platform_object=event,
)
elif event.conversation == 'GroupMessage':
message_chain.insert(0, platform_message.At(target="justbot"))
sender = platform_entities.GroupMember(
id = 111,
member_name="name",
permission= 'MEMBER',
group = platform_entities.Group(
id = 111,
name = 'MEMBER',
permission=platform_entities.Permission.Member
),
special_title='',
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0
)
time = datetime.datetime.now(),
return platform_events.GroupMessage(
sender =sender,
message_chain = message_chain,
time = time,
source_platform_object=event
)
class DingTalkAdapter(adapter.MessagePlatformAdapter):
bot: DingTalkClient
ap: app.Application
bot_account_id: str
message_converter: DingTalkMessageConverter = DingTalkMessageConverter()
event_converter: DingTalkEventConverter = DingTalkEventConverter()
config: dict
def __init__(self,config:dict,ap:app.Application):
self.config = config
self.ap = ap
required_keys = [
"client_id",
"client_secret",
"robot_name",
"robot_code",
]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员")
self.bot = DingTalkClient(
client_id=config["client_id"],
client_secret=config["client_secret"],
robot_name = config["robot_name"],
robot_code=config["robot_code"]
)
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
event = await DingTalkEventConverter.yiri2target(
message_source,
)
incoming_message = event.incoming_message
content = await DingTalkMessageConverter.yiri2target(message)
await self.bot.send_message(content,incoming_message)
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessagePlatformAdapter], None
],
):
async def on_message(event: DingTalkEvent):
self.bot_account_id = 'justbot'
try:
return await callback(
await self.event_converter.target2yiri(event), self
)
except:
traceback.print_exc()
if event_type == platform_events.FriendMessage:
self.bot.on_message("FriendMessage")(on_message)
elif event_type == platform_events.GroupMessage:
self.bot.on_message("GroupMessage")(on_message)
async def run_async(self):
await self.bot.start()
async def kill(self) -> bool:
return False
async def unregister_listener(
self,
event_type: type,
callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -0,0 +1,44 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: dingtalk
label:
en_US: DingTalk
zh_CN: 钉钉
description:
en_US: DingTalk Adapter
zh_CN: 钉钉适配器
spec:
config:
- name: client_id
label:
en_US: Client ID
zh_CN: 客户端ID
type: string
required: true
default: ""
- name: client_secret
label:
en_US: Client Secret
zh_CN: 客户端密钥
type: string
required: true
default: ""
- name: robot_code
label:
en_US: Robot Code
zh_CN: 机器人代码
type: string
required: true
default: ""
- name: robot_name
label:
en_US: Robot Name
zh_CN: 机器人名称
type: string
required: true
default: ""
execution:
python:
path: ./dingtalk.py
attr: DingTalkAdapter

View File

@@ -168,8 +168,8 @@ class DiscordEventConverter(adapter.EventConverter):
source_platform_object=event,
)
@adapter.adapter_class("discord")
class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter):
class DiscordAdapter(adapter.MessagePlatformAdapter):
bot: discord.Client
@@ -184,7 +184,7 @@ class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter):
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application):
@@ -249,14 +249,14 @@ class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter):
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
):
self.listeners.pop(event_type)

View File

@@ -0,0 +1,30 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: discord
label:
en_US: Discord
zh_CN: Discord
description:
en_US: Discord Adapter
zh_CN: Discord 适配器
spec:
config:
- name: client_id
label:
en_US: Client ID
zh_CN: 客户端ID
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
execution:
python:
path: ./discord.py
attr: DiscordAdapter

View File

@@ -133,8 +133,7 @@ class GewechatEventConverter(adapter.EventConverter):
)
@adapter.adapter_class("gewechat")
class GewechatMessageSourceAdapter(adapter.MessageSourceAdapter):
class GeWeChatAdapter(adapter.MessagePlatformAdapter):
bot: gewechat_client.GewechatClient
quart_app: quart.Quart
@@ -150,7 +149,7 @@ class GewechatMessageSourceAdapter(adapter.MessageSourceAdapter):
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application):
@@ -222,14 +221,14 @@ class GewechatMessageSourceAdapter(adapter.MessageSourceAdapter):
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None]
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None]
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None]
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None]
):
pass

View File

@@ -0,0 +1,51 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: gewechat
label:
en_US: GeWeChat
zh_CN: GeWeChat个人微信
description:
en_US: GeWeChat Adapter
zh_CN: GeWeChat 适配器
spec:
config:
- name: gewechat_url
label:
en_US: GeWeChat URL
zh_CN: GeWeChat URL
type: string
required: true
default: ""
- name: port
label:
en_US: Port
zh_CN: 端口
type: int
required: true
default: 2286
- name: callback_url
label:
en_US: Callback URL
zh_CN: 回调URL
type: string
required: true
default: ""
- name: app_id
label:
en_US: App ID
zh_CN: 应用ID
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
execution:
python:
path: ./gewechat.py
attr: GeWeChatAdapter

View File

@@ -11,10 +11,16 @@ import base64
import uuid
import json
import datetime
import hashlib
import base64
from Crypto.Cipher import AES
import aiohttp
import lark_oapi.ws.exception
import quart
from flask import jsonify
from lark_oapi.api.im.v1 import *
from lark_oapi.api.verification.v1 import GetVerificationRequest
from .. import adapter
from ...pipeline.longtext.strategies import forward
@@ -25,6 +31,28 @@ from ..types import entities as platform_entities
from ...utils import image
class AESCipher(object):
def __init__(self, key):
self.bs = AES.block_size
self.key=hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
@staticmethod
def str_to_bytes(data):
u_type = type(b"".decode('utf8'))
if isinstance(data, u_type):
return data.encode('utf8')
return data
@staticmethod
def _unpad(s):
return s[:-ord(s[len(s) - 1:])]
def decrypt(self, enc):
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:]))
def decrypt_string(self, enc):
enc = base64.b64decode(enc)
return self.decrypt(enc).decode('utf8')
class LarkMessageConverter(adapter.MessageConverter):
@staticmethod
@@ -270,8 +298,7 @@ class LarkEventConverter(adapter.EventConverter):
)
@adapter.adapter_class("lark")
class LarkMessageSourceAdapter(adapter.MessageSourceAdapter):
class LarkAdapter(adapter.MessagePlatformAdapter):
bot: lark_oapi.ws.Client
api_client: lark_oapi.Client
@@ -284,16 +311,61 @@ class LarkMessageSourceAdapter(adapter.MessageSourceAdapter):
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None],
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
] = {}
config: dict
quart_app: quart.Quart
ap: app.Application
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
self.quart_app = quart.Quart(__name__)
@self.quart_app.route('/lark/callback', methods=['POST'])
async def lark_callback():
try:
data = await quart.request.json
if 'encrypt' in data:
cipher = AESCipher(self.config['encrypt-key'])
data = cipher.decrypt_string(data['encrypt'])
data = json.loads(data)
type = data.get("type")
if type is None :
context = EventContext(data)
type = context.header.event_type
if 'url_verification' == type:
print(data.get("challenge"))
# todo 验证verification token
return {
"challenge": data.get("challenge")
}
context = EventContext(data)
type = context.header.event_type
p2v1 = P2ImMessageReceiveV1()
p2v1.header = context.header
event = P2ImMessageReceiveV1Data()
event.message = EventMessage(context.event['message'])
event.sender = EventSender(context.event['sender'])
p2v1.event = event
p2v1.schema = context.schema
if 'im.message.receive_v1' == type:
try:
event = await self.event_converter.target2yiri(p2v1, self.api_client)
except Exception as e:
traceback.print_exc()
if event.__class__ in self.listeners:
await self.listeners[event.__class__](event, self)
return {"code": 200, "message": "ok"}
except Exception as e:
traceback.print_exc()
return {"code": 500, "message": "error"}
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
@@ -377,7 +449,7 @@ class LarkMessageSourceAdapter(adapter.MessageSourceAdapter):
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
[platform_events.Event, adapter.MessagePlatformAdapter], None
],
):
self.listeners[event_type] = callback
@@ -386,22 +458,35 @@ class LarkMessageSourceAdapter(adapter.MessageSourceAdapter):
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
[platform_events.Event, adapter.MessagePlatformAdapter], None
],
):
self.listeners.pop(event_type)
async def run_async(self):
try:
await self.bot._connect()
except lark_oapi.ws.exception.ClientException as e:
raise e
except Exception as e:
await self.bot._disconnect()
if self.bot._auto_reconnect:
await self.bot._reconnect()
else:
raise e
port = self.config['port']
enable_webhook = self.config['enable-webhook']
if not enable_webhook:
try:
await self.bot._connect()
except lark_oapi.ws.exception.ClientException as e:
raise e
except Exception as e:
await self.bot._disconnect()
if self.bot._auto_reconnect:
await self.bot._reconnect()
else:
raise e
else:
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
await self.quart_app.run_task(
host='0.0.0.0',
port=port,
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool:
return False

View File

@@ -0,0 +1,58 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: lark
label:
en_US: Lark
zh_CN: 飞书
description:
en_US: Lark Adapter
zh_CN: 飞书适配器
spec:
config:
- name: app_id
label:
en_US: App ID
zh_CN: 应用ID
type: string
required: true
default: ""
- name: app_secret
label:
en_US: App Secret
zh_CN: 应用密钥
type: string
required: true
default: ""
- name: bot_name
label:
en_US: Bot Name
zh_CN: 机器人名称
type: string
required: true
default: ""
- name: enable-webhook
label:
en_US: Enable Webhook Mode
zh_CN: 启用Webhook模式
type: boolean
required: true
default: false
- name: port
label:
en_US: Webhook Port
zh_CN: Webhook端口
type: int
required: true
default: 2285
- name: encrypt-key
label:
en_US: Encrypt Key
zh_CN: 加密密钥
type: string
required: true
default: ""
execution:
python:
path: ./lark.py
attr: LarkAdapter

View File

@@ -158,8 +158,7 @@ class NakuruProjectEventConverter(adapter_model.EventConverter):
raise Exception("未支持转换的事件类型: " + str(event))
@adapter_model.adapter_class("nakuru")
class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
class NakuruAdapter(adapter_model.MessagePlatformAdapter):
"""nakuru-project适配器"""
bot: nakuru.CQHTTP
bot_account_id: int
@@ -256,7 +255,7 @@ class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter_model.MessageSourceAdapter], None]
callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None]
):
try:
@@ -284,7 +283,7 @@ class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter_model.MessageSourceAdapter], None]
callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None]
):
nakuru_event_name = self.event_converter.yiri2target(event_type).__name__

View File

@@ -0,0 +1,44 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: nakuru
label:
en_US: Nakuru
zh_CN: Nakuru
description:
en_US: Nakuru Adapter
zh_CN: Nakuru 适配器(go-cqhttp)
spec:
config:
- name: host
label:
en_US: Host
zh_CN: 主机
type: string
required: true
default: "127.0.0.1"
- name: http_port
label:
en_US: HTTP Port
zh_CN: HTTP端口
type: int
required: true
default: 5700
- name: ws_port
label:
en_US: WebSocket Port
zh_CN: WebSocket端口
type: int
required: true
default: 8080
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
execution:
python:
path: ./nakuru.py
attr: NakuruAdapter

View File

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

View File

@@ -0,0 +1,58 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: officialaccount
label:
en_US: Official Account
zh_CN: 微信公众号
description:
en_US: Official Account Adapter
zh_CN: 微信公众号适配器
spec:
config:
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
- name: EncodingAESKey
label:
en_US: EncodingAESKey
zh_CN: 消息加解密密钥
type: string
required: true
default: ""
- name: AppID
label:
en_US: App ID
zh_CN: 应用ID
type: string
required: true
default: ""
- name: AppSecret
label:
en_US: App Secret
zh_CN: 应用密钥
type: string
required: true
default: ""
- name: host
label:
en_US: Host
zh_CN: 监听主机
type: string
required: true
default: 0.0.0.0
- name: port
label:
en_US: Port
zh_CN: 监听端口
type: int
required: true
default: 2287
execution:
python:
path: ./officialaccount.py
attr: OfficialAccountAdapter

View File

@@ -360,8 +360,7 @@ class OfficialEventConverter(adapter_model.EventConverter):
)
@adapter_model.adapter_class("qq-botpy")
class OfficialAdapter(adapter_model.MessageSourceAdapter):
class OfficialAdapter(adapter_model.MessagePlatformAdapter):
"""QQ 官方消息适配器"""
bot: botpy.Client = None
@@ -535,7 +534,7 @@ class OfficialAdapter(adapter_model.MessageSourceAdapter):
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter_model.MessageSourceAdapter], None
[platform_events.Event, adapter_model.MessagePlatformAdapter], None
],
):
@@ -561,7 +560,7 @@ class OfficialAdapter(adapter_model.MessageSourceAdapter):
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter_model.MessageSourceAdapter], None
[platform_events.Event, adapter_model.MessagePlatformAdapter], None
],
):
delattr(self.bot, event_handler_mapping[event_type])

View File

@@ -0,0 +1,37 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: qq-botpy
label:
en_US: QQBotPy
zh_CN: QQBotPy
description:
en_US: QQ Official API (WebSocket)
zh_CN: QQ 官方 API (WebSocket)
spec:
config:
- name: appid
label:
en_US: App ID
zh_CN: 应用ID
type: string
required: true
default: ""
- name: secret
label:
en_US: Secret
zh_CN: 密钥
type: string
required: true
default: ""
- name: intents
label:
en_US: Intents
zh_CN: 权限
type: array[string]
required: true
default: []
execution:
python:
path: ./qqbotpy.py
attr: OfficialAdapter

View File

@@ -6,7 +6,7 @@ import time
import datetime
import aiocqhttp
import aiohttp
from pkg.platform.adapter import MessageSourceAdapter
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from pkg.core import app
from .. import adapter
@@ -47,7 +47,7 @@ class QQOfficialMessageConverter(adapter.MessageConverter):
yiri_msg_list.append(
platform_message.Image(base64=base64_url)
)
yiri_msg_list.append(platform_message.Plain(text=message))
chain = platform_message.MessageChain(yiri_msg_list)
return chain
@@ -137,10 +137,7 @@ class QQOfficialEventConverter(adapter.EventConverter):
)
@adapter.adapter_class("qqofficial")
class QQOfficialAdapter(adapter.MessageSourceAdapter):
class QQOfficialAdapter(adapter.MessagePlatformAdapter):
bot:QQOfficialClient
ap:app.Application
config:dict
@@ -213,7 +210,7 @@ class QQOfficialAdapter(adapter.MessageSourceAdapter):
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
[platform_events.Event, adapter.MessagePlatformAdapter], None
],
):
async def on_message(event:QQOfficialEvent):
@@ -250,7 +247,7 @@ class QQOfficialAdapter(adapter.MessageSourceAdapter):
def unregister_listener(
self,
event_type: type,
callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None],
callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -0,0 +1,44 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: qqofficial
label:
en_US: QQ Official API
zh_CN: QQ 官方 API
description:
en_US: QQ Official API (Webhook)
zh_CN: QQ 官方 API (Webhook)
spec:
config:
- name: appid
label:
en_US: App ID
zh_CN: 应用ID
type: string
required: true
default: ""
- name: secret
label:
en_US: Secret
zh_CN: 密钥
type: string
required: true
default: ""
- name: port
label:
en_US: Port
zh_CN: 监听端口
type: int
required: true
default: 2284
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
execution:
python:
path: ./qqofficial.py
attr: QQOfficialAdapter

View File

@@ -0,0 +1,242 @@
from __future__ import annotations
import telegram
import telegram.ext
from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters
import typing
import asyncio
import traceback
import time
import re
import base64
import uuid
import json
import datetime
import hashlib
import base64
import aiohttp
from Crypto.Cipher import AES
from flask import jsonify
from lark_oapi.api.im.v1 import *
from lark_oapi.api.verification.v1 import GetVerificationRequest
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...utils import image
class TelegramMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(message_chain: platform_message.MessageChain, bot: telegram.Bot) -> list[dict]:
components = []
for component in message_chain:
if isinstance(component, platform_message.Plain):
components.append({
"type": "text",
"text": component.text
})
elif isinstance(component, platform_message.Image):
photo_bytes = None
if component.base64:
photo_bytes = base64.b64decode(component.base64)
elif component.url:
async with aiohttp.ClientSession() as session:
async with session.get(component.url) as response:
photo_bytes = await response.read()
elif component.path:
with open(component.path, "rb") as f:
photo_bytes = f.read()
components.append({
"type": "photo",
"photo": photo_bytes
})
elif isinstance(component, platform_message.Forward):
for node in component.node_list:
components.extend(await TelegramMessageConverter.yiri2target(node.message_chain, bot))
return components
@staticmethod
async def target2yiri(message: telegram.Message, bot: telegram.Bot, bot_account_id: str):
message_components = []
def parse_message_text(text: str) -> list[platform_message.MessageComponent]:
msg_components = []
if f'@{bot_account_id}' in text:
msg_components.append(platform_message.At(target=bot_account_id))
text = text.replace(f'@{bot_account_id}', '')
msg_components.append(platform_message.Plain(text=text))
return msg_components
if message.text:
message_text = message.text
message_components.extend(parse_message_text(message_text))
if message.photo:
message_components.extend(parse_message_text(message.caption))
file = await message.photo[-1].get_file()
file_bytes = None
file_format = ''
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(file.file_path) as response:
file_bytes = await response.read()
file_format = 'image/jpeg'
message_components.append(platform_message.Image(base64=f"data:{file_format};base64,{base64.b64encode(file_bytes).decode('utf-8')}"))
return platform_message.MessageChain(message_components)
class TelegramEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(event: platform_events.MessageEvent, bot: telegram.Bot):
return event.source_platform_object
@staticmethod
async def target2yiri(event: Update, bot: telegram.Bot, bot_account_id: str):
lb_message = await TelegramMessageConverter.target2yiri(event.message, bot, bot_account_id)
if event.effective_chat.type == 'private':
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event.effective_chat.id,
nickname=event.effective_chat.first_name,
remark=event.effective_chat.id,
),
message_chain=lb_message,
time=event.message.date.timestamp(),
source_platform_object=event
)
elif event.effective_chat.type == 'group':
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=event.effective_chat.id,
member_name=event.effective_chat.title,
permission=platform_entities.Permission.Member,
group=platform_entities.Group(
id=event.effective_chat.id,
name=event.effective_chat.title,
permission=platform_entities.Permission.Member,
),
special_title="",
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=lb_message,
time=event.message.date.timestamp(),
source_platform_object=event
)
class TelegramAdapter(adapter.MessagePlatformAdapter):
bot: telegram.Bot
application: telegram.ext.Application
bot_account_id: str
message_converter: TelegramMessageConverter = TelegramMessageConverter()
event_converter: TelegramEventConverter = TelegramEventConverter()
config: dict
ap: app.Application
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.message.from_user.is_bot:
return
try:
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
await self.listeners[type(lb_event)](lb_event, self)
except Exception as e:
print(traceback.format_exc())
self.application = ApplicationBuilder().token(self.config['token']).build()
self.bot = self.application.bot
self.application.add_handler(MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO , telegram_callback))
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
assert isinstance(message_source.source_platform_object, Update)
components = await TelegramMessageConverter.yiri2target(message, self.bot)
for component in components:
if component['type'] == 'text':
args = {
"chat_id": message_source.source_platform_object.effective_chat.id,
"text": component['text'],
}
if quote_origin:
args['reply_to_message_id'] = message_source.source_platform_object.message.id
await self.bot.send_message(**args)
async def is_muted(self, group_id: int) -> bool:
return False
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
):
self.listeners.pop(event_type)
async def run_async(self):
await self.application.initialize()
self.bot_account_id = (await self.bot.get_me()).username
await self.application.updater.start_polling(
allowed_updates=Update.ALL_TYPES
)
await self.application.start()
async def kill(self) -> bool:
await self.application.stop()
return True

View File

@@ -0,0 +1,23 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: telegram
label:
en_US: Telegram
zh_CN: 电报
description:
en_US: Telegram Adapter
zh_CN: 电报适配器
spec:
config:
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
execution:
python:
path: ./telegram.py
attr: TelegramAdapter

View File

@@ -8,7 +8,7 @@ import datetime
import aiocqhttp
import aiohttp
from libs.wecom_api.api import WecomClient
from pkg.platform.adapter import MessageSourceAdapter
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from libs.wecom_api.wecomevent import WecomEvent
from pkg.core import app
@@ -119,9 +119,8 @@ class WecomEventConverter:
yiri_chain = await WecomMessageConverter.target2yiri(
event.message, event.message_id
)
friend = platform_entities.Friend(
id=event.user_id,
id=f"u{event.user_id}",
nickname=str(event.agent_id),
remark="",
)
@@ -131,7 +130,7 @@ class WecomEventConverter:
)
elif event.type == "image":
friend = platform_entities.Friend(
id=event.user_id,
id=f"u{event.user_id}",
nickname=str(event.agent_id),
remark="",
)
@@ -145,8 +144,7 @@ class WecomEventConverter:
)
@adapter.adapter_class("wecom")
class WecomeAdapter(adapter.MessageSourceAdapter):
class WecomAdapter(adapter.MessagePlatformAdapter):
bot: WecomClient
ap: app.Application
@@ -154,7 +152,6 @@ class WecomeAdapter(adapter.MessageSourceAdapter):
message_converter: WecomMessageConverter = WecomMessageConverter()
event_converter: WecomEventConverter = WecomEventConverter()
config: dict
ap: app.Application
def __init__(self, config: dict, ap: app.Application):
self.config = config
@@ -191,12 +188,14 @@ class WecomeAdapter(adapter.MessageSourceAdapter):
message_source, self.bot_account_id, self.bot
)
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
fixed_user_id = Wecom_event.user_id
# 删掉开头的u
fixed_user_id = fixed_user_id[1:]
for content in content_list:
if content["type"] == "text":
await self.bot.send_private_msg(Wecom_event.user_id, Wecom_event.agent_id, content["content"])
await self.bot.send_private_msg(fixed_user_id, Wecom_event.agent_id, content["content"])
elif content["type"] == "image":
await self.bot.send_image(Wecom_event.user_id, Wecom_event.agent_id, content["media_id"])
await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content["media_id"])
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
@@ -207,7 +206,7 @@ class WecomeAdapter(adapter.MessageSourceAdapter):
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessageSourceAdapter], None
[platform_events.Event, adapter.MessagePlatformAdapter], None
],
):
async def on_message(event: WecomEvent):
@@ -242,6 +241,6 @@ class WecomeAdapter(adapter.MessageSourceAdapter):
async def unregister_listener(
self,
event_type: type,
callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None],
callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -0,0 +1,65 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: wecom
label:
en_US: WeCom
zh_CN: 企业微信
description:
en_US: WeCom Adapter
zh_CN: 企业微信适配器
spec:
config:
- name: host
label:
en_US: Host
zh_CN: 监听主机
type: string
required: true
default: "0.0.0.0"
- name: port
label:
en_US: Port
zh_CN: 监听端口
type: int
required: true
default: 2290
- name: corpid
label:
en_US: Corpid
zh_CN: 企业ID
type: string
required: true
default: ""
- name: secret
label:
en_US: Secret
zh_CN: 密钥
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
- name: EncodingAESKey
label:
en_US: EncodingAESKey
zh_CN: 消息加解密密钥
type: string
required: true
default: ""
- name: contacts_secret
label:
en_US: Contacts Secret
zh_CN: 通讯录密钥
type: string
required: true
default: ""
execution:
python:
path: ./wecom.py
attr: WecomAdapter

View File

@@ -38,6 +38,7 @@ class Friend(Entity):
return self.nickname or self.remark or ''
class Permission(str, Enum):
"""群成员身份权限。"""
Member = "MEMBER"

View File

@@ -116,7 +116,7 @@ class APIHost:
# ========== 插件可调用的 API主程序API ==========
def get_platform_adapters(self) -> list[platform_adapter.MessageSourceAdapter]:
def get_platform_adapters(self) -> list[platform_adapter.MessagePlatformAdapter]:
"""获取已启用的消息平台适配器列表
Returns:
@@ -126,7 +126,7 @@ class APIHost:
async def send_active_message(
self,
adapter: platform_adapter.MessageSourceAdapter,
adapter: platform_adapter.MessagePlatformAdapter,
target_type: str,
target_id: str,
message: platform_message.MessageChain,

View File

@@ -6,7 +6,7 @@ from . import entities, requester
from ...core import app
from . import token
from .requesters import chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl
from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl
FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list"

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: ComponentTemplate
metadata:
name: LLMAPIRequester
label:
en_US: LLM API Requester
zh_CN: LLM API 请求器
spec:
type:
- python
execution:
python:
path: ./requester.py
attr: LLMAPIRequester

View File

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

View File

@@ -49,67 +49,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
self,
args: dict,
) -> chat_completion.ChatCompletion:
args["stream"] = True
chunk = None
pending_content = ""
tool_calls = []
resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args)
async for chunk in resp_gen:
# print(chunk)
if not chunk:
continue
if chunk.choices[0].delta.content is not None:
pending_content += chunk.choices[0].delta.content
if chunk.choices[0].delta.tool_calls is not None:
for tool_call in chunk.choices[0].delta.tool_calls:
for tc in tool_calls:
if tc.index == tool_call.index:
tc.function.arguments += tool_call.function.arguments
break
else:
tool_calls.append(tool_call)
real_tool_calls = []
for tc in tool_calls:
function = chat_completion_message_tool_call.Function(
name=tc.function.name,
arguments=tc.function.arguments
)
real_tool_calls.append(chat_completion_message_tool_call.ChatCompletionMessageToolCall(
id=tc.id,
function=function,
type="function"
))
return chat_completion.ChatCompletion(
id=chunk.id,
object="chat.completion",
created=chunk.created,
choices=[
chat_completion.Choice(
index=0,
message=chat_completion.ChatCompletionMessage(
role="assistant",
content=pending_content,
tool_calls=real_tool_calls if len(real_tool_calls) > 0 else None
),
finish_reason=chunk.choices[0].finish_reason if hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason is not None else 'stop',
logprobs=chunk.choices[0].logprobs,
)
],
model=chunk.model,
service_tier=chunk.service_tier if hasattr(chunk, 'service_tier') else None,
system_fingerprint=chunk.system_fingerprint if hasattr(chunk, 'system_fingerprint') else None,
usage=chunk.usage if hasattr(chunk, 'usage') else None
) if chunk else None
return await self.client.chat.completions.create(**args)
async def _make_msg(
self,

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import openai
from . import chatcmpl
from .. import requester
from ....core import app
@requester.requester_class("volcark-chat-completions")
class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions):
"""火山方舟大模型平台 ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['volcark-chat-completions']

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import typing
import json
import uuid
import re
import base64
import aiohttp
@@ -41,6 +42,23 @@ class DifyServiceAPIRunner(runner.RequestRunner):
base_url=self.ap.provider_cfg.data["dify-service-api"]["base-url"],
)
def _try_convert_thinking(self, resp_text: str) -> str:
"""尝试转换 Dify 的思考提示"""
if not resp_text.startswith("<details style=\"color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;\" open> <summary> Thinking... </summary>"):
return resp_text
if self.ap.provider_cfg.data["dify-service-api"]["options"]["convert-thinking-tips"] == "original":
return resp_text
if self.ap.provider_cfg.data["dify-service-api"]["options"]["convert-thinking-tips"] == "remove":
return re.sub(r'<details style="color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;" open> <summary> Thinking... </summary>.*?</details>', '', resp_text, flags=re.DOTALL)
if self.ap.provider_cfg.data["dify-service-api"]["options"]["convert-thinking-tips"] == "plain":
pattern = r'<details style="color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;" open> <summary> Thinking... </summary>(.*?)</details>'
thinking_text = re.search(pattern, resp_text, flags=re.DOTALL)
content_text = re.sub(pattern, '', resp_text, flags=re.DOTALL)
return f"<think>{thinking_text.group(1)}</think>\n{content_text}"
async def _preprocess_user_message(
self, query: core_entities.Query
) -> tuple[str, list[str]]:
@@ -109,7 +127,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
if chunk['data']['node_type'] == 'answer':
yield llm_entities.Message(
role="assistant",
content=chunk['data']['outputs']['answer'],
content=self._try_convert_thinking(chunk['data']['outputs']['answer']),
)
elif mode == "basic":
if chunk['event'] == 'message':
@@ -117,7 +135,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
elif chunk['event'] == 'message_end':
yield llm_entities.Message(
role="assistant",
content=basic_mode_pending_chunk,
content=self._try_convert_thinking(basic_mode_pending_chunk),
)
basic_mode_pending_chunk = ''

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.6.2"
semantic_version = "v3.4.9.1"
debug_mode = False

View File

@@ -29,6 +29,9 @@ lark-oapi
discord.py
cryptography
gewechat-client
dingtalk_stream
dashscope
python-telegram-bot
# indirect
taskgroup==0.0.0a4

View File

@@ -49,7 +49,10 @@
"enable": false,
"app_id": "cli_abcdefgh",
"app_secret": "XXXXXXXXXX",
"bot_name": "LangBot"
"bot_name": "LangBot",
"enable-webhook": false,
"port": 2285,
"encrypt-key": "xxxxxxxxx"
},
{
"adapter": "discord",
@@ -65,6 +68,29 @@
"callback_url": "http://your-callback-url:2286/gewechat/callback",
"app_id": "",
"token": ""
},
{
"adapter":"officialaccount",
"enable": false,
"token": "",
"EncodingAESKey":"",
"AppID":"",
"AppSecret":"",
"host": "0.0.0.0",
"port": 2287
},
{
"adapter":"dingtalk",
"enable": false,
"client_id":"",
"client_secret":"",
"robot_code":"",
"robot_name":""
},
{
"adapter":"telegram",
"enable": false,
"token":""
}
],
"track-function-calls": true,

View File

@@ -25,6 +25,12 @@
],
"siliconflow": [
"xxxxxxx"
],
"bailian": [
"sk-xxxxxxx"
],
"volcark": [
"xxxxxxxx"
]
},
"requester": {
@@ -79,17 +85,30 @@
"base-url": "https://api.siliconflow.cn/v1",
"args": {},
"timeout": 120
},
"bailian-chat-completions": {
"args": {},
"base-url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"timeout": 120
},
"volcark-chat-completions": {
"args": {},
"base-url": "https://ark.cn-beijing.volces.com/api/v3",
"timeout": 120
}
},
"model": "gpt-4o",
"prompt-mode": "normal",
"prompt": {
"default": ""
"default": ""
},
"runner": "local-agent",
"dify-service-api": {
"base-url": "https://api.dify.ai/v1",
"app-type": "chat",
"options": {
"convert-thinking-tips": "plain"
},
"chat": {
"api-key": "app-1234567890",
"timeout": 120
@@ -103,5 +122,21 @@
"output-key": "summary",
"timeout": 120
}
},
"dashscope-app-api": {
"app-type": "agent",
"api-key": "sk-1234567890",
"agent": {
"app-id": "Your_app_id",
"references_quote": "参考资料来自:"
},
"workflow": {
"app-id": "Your_app_id",
"references_quote": "参考资料来自:",
"biz_params": {
"city": "北京",
"date": "2023-08-10"
}
}
}
}

View File

@@ -252,6 +252,21 @@
"type": "string",
"default": "",
"description": "飞书的bot_name"
},
"enable-webhook": {
"type": "boolean",
"default": false,
"description": "是否启用webhook模式"
},
"port": {
"type": "integer",
"description": "设置监听的端口,开启callback event时需要设置",
"default": 2285
},
"encrypt-key": {
"type": "string",
"default": "",
"description": "设置加密密钥"
}
}
},
@@ -331,6 +346,112 @@
"description": "gewechat 的 token"
}
}
},
{
"title": "微信公众号适配器",
"description": "用于接入微信公众号",
"properties": {
"adapter": {
"type": "string",
"const": "officialaccount"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器"
},
"token": {
"type": "string",
"default": "",
"description": "微信公众号的token"
},
"EncodingAESKey": {
"type": "string",
"default": "",
"description": "微信公众号的EncodingAESKey"
},
"AppID": {
"type": "string",
"default": "",
"description": "微信公众号的AppID"
},
"AppSecret": {
"type": "string",
"default": "",
"description": "微信公众号的AppSecret"
},
"host": {
"type": "string",
"default": "0.0.0.0",
"description": "监听的IP地址"
},
"port": {
"type": "integer",
"default": 2287,
"description": "监听的端口"
}
}
},
{
"title": "钉钉适配器",
"description": "用于接入钉钉",
"properties": {
"adapter": {
"type": "string",
"const": "dingtalk"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器",
"layout": {
"comp": "switch",
"props": {
"color": "primary"
}
}
},
"client_id": {
"type": "string",
"default": "",
"description": "钉钉的client_id"
},
"client_secret": {
"type": "string",
"default": "",
"description": "钉钉的client_secret"
},
"robot_code": {
"type": "string",
"default": "",
"description": "钉钉的robot_code"
},
"robot_name": {
"type": "string",
"default": "",
"description": "钉钉的robot_name"
}
}
},
{
"title": "Telegram 适配器",
"description": "用于接入 Telegram",
"properties": {
"adapter": {
"type": "string",
"const": "telegram"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器"
},
"token": {
"type": "string",
"default": "",
"description": "Telegram 的 token"
}
}
}
]
}

View File

@@ -82,6 +82,22 @@
"type": "string"
},
"default": []
},
"bailian": {
"type": "array",
"title": "阿里云百炼大模型平台 API 密钥",
"items": {
"type": "string"
},
"default": []
},
"volcark": {
"type": "array",
"title": "火山引擎大模型平台 API 密钥",
"items": {
"type": "string"
},
"default": []
}
}
},
@@ -288,6 +304,46 @@
"default": 120
}
}
},
"bailian-chat-completions": {
"type": "object",
"title": "阿里云百炼大模型平台 API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
},
"volcark-chat-completions": {
"type": "object",
"title": "火山方舟大模型平台 API 请求配置",
"description": "仅可编辑 URL 和 超时时间,额外请求参数不支持可视化编辑,请到编辑器编辑",
"properties": {
"base-url": {
"type": "string",
"title": "API URL"
},
"args": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "API 请求超时时间",
"default": 120
}
}
}
}
},
@@ -348,6 +404,20 @@
"enum": ["chat", "workflow", "agent"],
"default": "chat"
},
"options": {
"type": "object",
"title": "Dify Service API 配置选项",
"properties": {
"convert-thinking-tips": {
"type": "string",
"title": "转换思考提示",
"description": "设置转换思考提示。值为 original 时,不转换思考提示;值为 plain 时,将思考提示转换为类似 DeepSeek 官方的<think>...</think>格式;值为 remove 时,删除思考提示",
"enum": ["original", "plain", "remove"],
"default": "plain"
}
}
},
"chat": {
"type": "object",
"title": "聊天助手 API 参数",
@@ -397,6 +467,59 @@
}
}
}
},
"dashscope-app-api": {
"type": "object",
"title": "阿里百炼平台自建应用 API 配置",
"properties": {
"app-type": {
"type": "string",
"title": "应用类型",
"description": "支持 workflow 和 agentworkflow智能体编排agent普通智能体请填写下方对应的应用类型 API 参数",
"enum": ["workflow", "agent"],
"default": "agent"
},
"api-key": {
"type": "string",
"title": "API 密钥"
},
"agent": {
"type": "object",
"title": "Agent API 参数",
"properties": {
"app-id": {
"type": "string",
"title": "应用 ID"
},
"references_quote": {
"type": "string",
"title": "参考资料引用",
"description": "设置参考资料引用,用于从 Dashscope App API 结束节点返回的 JSON 数据中提取引用内容",
"default": "参考资料来自:"
}
}
},
"workflow": {
"type": "object",
"title": "工作流 API 参数",
"properties": {
"app-id": {
"type": "string",
"title": "应用 ID"
},
"references_quote": {
"type": "string",
"title": "参考资料引用",
"default": "参考资料来自:"
},
"biz_params": {
"type": "object",
"title": "传入参数",
"default": {}
}
}
}
}
}
}
}