Compare commits

..

33 Commits

Author SHA1 Message Date
Junyan Qin
0623f4009a chore: release v3.4.9.2 2025-02-24 15:01:00 +08:00
Junyan Qin
06adeb72c4 fix: components.yaml encoding error on windows 2025-02-24 15:00:17 +08:00
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
lipu
2c3fdb4fdc feat(lark):enable lark callback 2025-02-11 21:37:07 +08:00
65 changed files with 1956 additions and 209 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

@@ -23,10 +23,11 @@
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-1030838208-blue)](https://qm.qq.com/q/PF9OuQCCcM)
[![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_JP.md)
@@ -36,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)
@@ -86,13 +87,13 @@
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | |
| 个人微信 | ✅ | 使用 [Gewechat](https://github.com/Devo919/Gewechat) 接入 |
| 微信公众号 | ✅ | |
| 飞书 | ✅ | |
| 钉钉 | ✅ | |
| Discord | ✅ | |
| 个人微信 | ✅ | 使用 [Gewechat](https://github.com/Devo919/Gewechat) 接入 |
| Telegram | 🚧 | |
| Telegram | ✅ | |
| WhatsApp | 🚧 | |
| 钉钉 | 🚧 | |
🚧: 正在开发中
@@ -112,6 +113,7 @@
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
## 😘 社区贡献

View File

@@ -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,13 +85,12 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| Personal QQ | ✅ | |
| QQ Official API | ✅ | |
| WeCom | ✅ | |
| WeChat Official Account | ✅ | |
| Lark | ✅ | |
| Discord | ✅ | |
| Personal WeChat | ✅ | Use [Gewechat](https://github.com/Devo919/Gewechat) to access |
| Telegram | 🚧 | |
| Lark | | |
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| WhatsApp | 🚧 | |
| DingTalk | 🚧 | |
🚧: In development
@@ -111,6 +110,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [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

View File

@@ -33,7 +33,7 @@
## ✨ 機能
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQチャンネル、WeCom、Lark、Discord、個人WeChatをサポートし、将来的にはWhatsApp、Telegramなどもサポート予定
- 💬 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)を参照。
@@ -84,12 +84,12 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| 個人QQ | ✅ | |
| QQ公式API | ✅ | |
| WeCom | ✅ | |
| Lark | ✅ | |
| Discord | ✅ | |
| 個人WeChat | ✅ | [Gewechat](https://github.com/Devo919/Gewechat)を使用して接続 |
| Telegram | 🚧 | |
| Lark | | |
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| WhatsApp | 🚧 | |
| DingTalk | 🚧 | |
🚧: 開発中
@@ -109,6 +109,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| [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プラットフォーム |
## 🤝 コミュニティ貢献

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

@@ -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,

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

@@ -30,7 +30,9 @@ required_deps = {
"discord": "discord.py",
"cryptography": "cryptography",
"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

@@ -10,11 +10,12 @@ 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
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
# if adapter['adapter'] == 'officialaccount':
# return False
return True
# return True
return False
async def run(self):
"""执行迁移"""

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

@@ -10,9 +10,8 @@ from ..migrations import m010_ollama_requester_config, m011_command_prefix_confi
from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_api_timeout_params, m018_xai_config, m019_zhipuai_config
from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config
from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config
from ..migrations import m029_dashscope_app_api_config
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', encoding='utf-8') 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, officialaccount
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

@@ -5,13 +5,13 @@ import traceback
import time
import datetime
from pkg.core import app
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
import aiocqhttp
import aiohttp
from libs.official_account_api.oaevent import OAEvent
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.official_account_api.api import OAClient
from pkg.core import app
@@ -68,8 +68,7 @@ class OAEventConverter(adapter.EventConverter):
else:
return None
@adapter.adapter_class("officialaccount")
class OfficialAccountAdapter(adapter.MessageSourceAdapter):
class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
bot : OAClient
ap : app.Application
@@ -116,7 +115,7 @@ class OfficialAccountAdapter(adapter.MessageSourceAdapter):
pass
def register_listener(self, event_type: type, callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None]):
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:
@@ -148,7 +147,7 @@ class OfficialAccountAdapter(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,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
@@ -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
@@ -130,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="",
)
@@ -144,8 +144,7 @@ class WecomEventConverter:
)
@adapter.adapter_class("wecom")
class WecomeAdapter(adapter.MessageSourceAdapter):
class WecomAdapter(adapter.MessagePlatformAdapter):
bot: WecomClient
ap: app.Application
@@ -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

@@ -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 bailianchatcmpl, 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

@@ -49,70 +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 or not chunk.id or not chunk.choices or not chunk.choices[0] or not chunk.choices[0].delta:
continue
if chunk.choices[0].delta.content is not None:
pending_content += chunk.choices[0].delta.content
if chunk.choices[0].delta.tool_calls is not None:
for tool_call in chunk.choices[0].delta.tool_calls:
for tc in tool_calls:
if tc.index == tool_call.index:
tc.function.arguments += tool_call.function.arguments
break
else:
tool_calls.append(tool_call)
if chunk.choices[0].finish_reason is not None:
break
real_tool_calls = []
for tc in tool_calls:
function = chat_completion_message_tool_call.Function(
name=tc.function.name,
arguments=tc.function.arguments
)
real_tool_calls.append(chat_completion_message_tool_call.ChatCompletionMessageToolCall(
id=tc.id,
function=function,
type="function"
))
return chat_completion.ChatCompletion(
id=chunk.id,
object="chat.completion",
created=chunk.created,
choices=[
chat_completion.Choice(
index=0,
message=chat_completion.ChatCompletionMessage(
role="assistant",
content=pending_content,
tool_calls=real_tool_calls if len(real_tool_calls) > 0 else None
),
finish_reason=chunk.choices[0].finish_reason if hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason is not None else 'stop',
logprobs=chunk.choices[0].logprobs,
)
],
model=chunk.model,
service_tier=chunk.service_tier if hasattr(chunk, 'service_tier') else None,
system_fingerprint=chunk.system_fingerprint if hasattr(chunk, 'system_fingerprint') else None,
usage=chunk.usage if hasattr(chunk, 'usage') else None
) if chunk else None
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

@@ -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.7.1"
semantic_version = "v3.4.9.2"
debug_mode = False

View File

@@ -29,7 +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",
@@ -75,6 +78,19 @@
"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

@@ -28,6 +28,9 @@
],
"bailian": [
"sk-xxxxxxx"
],
"volcark": [
"xxxxxxxx"
]
},
"requester": {
@@ -87,6 +90,11 @@
"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",
@@ -98,6 +106,9 @@
"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

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": "设置加密密钥"
}
}
},
@@ -376,6 +391,67 @@
"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

@@ -90,6 +90,14 @@
"type": "string"
},
"default": []
},
"volcark": {
"type": "array",
"title": "火山引擎大模型平台 API 密钥",
"items": {
"type": "string"
},
"default": []
}
}
},
@@ -316,6 +324,26 @@
"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
}
}
}
}
},
@@ -376,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 参数",