Compare commits

...

74 Commits

Author SHA1 Message Date
Junyan Qin
02bc1fc45e chore: release v3.4.10.2 2025-03-05 21:12:52 +08:00
Junyan Qin (Chin)
5585981dc3 Merge pull request #1165 from fdc310/master
修复因为gewe将字段修改回原版而导致的我的判断逻辑问题
2025-03-05 21:12:21 +08:00
Dong_master
a4777f194b 修复因为gewe将字段修改回原版而导致的我的判断逻辑问题 2025-03-05 19:56:28 +08:00
Junyan Qin
41aeda8dc0 chore: release v3.4.10.1 2025-03-05 17:16:05 +08:00
Junyan Qin (Chin)
2ed522667e Merge pull request #1162 from fdc310/master
'修复了gewe更新回调参数data和typename字段改变造成的不回复的问题'
2025-03-05 17:14:27 +08:00
Dong_master
1932444666 '修复了gewe更新回调参数data和typename字段改变造成的不回复的问题' 2025-03-05 16:48:46 +08:00
Dong_master
b49b7e963d '修复了gewe更新回调参数data和typename字段改变造成的不回复的问题' 2025-03-05 00:54:39 +08:00
Junyan Qin
435c11ff27 doc(README): add more model in README 2025-03-03 21:26:39 +08:00
Junyan Qin
2e93600437 feat: update llm-models.json template 2025-03-03 21:02:48 +08:00
Junyan Qin (Chin)
faecb70d0f Merge pull request #1154 from Yi-Lyu/master
将微信消息时间戳传递给 dify,便于 dify 通过消息时间戳来做业务逻辑。
2025-03-02 20:21:08 +08:00
Junyan Qin
92e1ac5c3a feat: add supports for passing time to dify workflow 2025-03-02 20:18:33 +08:00
Junyan Qin
8963a2117b feat: add field time in MessageEvent 2025-03-02 20:16:34 +08:00
Ethan
aa300258ab feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:45:10 +08:00
Ethan
48841daff5 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:30:07 +08:00
Ethan
8878f1ed87 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:20:10 +08:00
Ethan
f6205d79c0 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:18:26 +08:00
Ethan
d6d5dac6b3 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:10:53 +08:00
Ethan
05b979e68a feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:10:09 +08:00
Ethan
9f7d9e4c0d feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 18:49:32 +08:00
Junyan Qin
98a9fed726 chore: release v3.4.10 2025-03-02 18:08:59 +08:00
Junyan Qin
720a218259 perf: simplify platform entities 2025-03-02 17:59:13 +08:00
Junyan Qin (Chin)
60c0adc6f9 Merge pull request #1152 from RockChinQ/feat/dingtalk-audio
feat(dingtalk): add supports for audio receiving
2025-03-02 17:38:19 +08:00
Junyan Qin
bc8c346e68 fix(dingtalk): group and person id not distinguished 2025-03-02 17:35:06 +08:00
Junyan Qin
a198b6da0b feat(dingtalk): add supports for audio receiving 2025-03-02 17:03:19 +08:00
Junyan Qin (Chin)
0f3dc35df4 Merge pull request #1150 from Tigrex-Dai/master
添加针对Anthropic新模型的thinking开关
2025-03-02 15:39:58 +08:00
Junyan Qin
7b6e6b046a perf(claude): simplify the thinking resp processing 2025-03-02 15:24:08 +08:00
Tigrex Dai
9e503191d6 Update anthropicmsgs.py 2025-03-01 17:27:01 +01:00
Tigrex Dai
1fd23a0d8d Merge branch 'RockChinQ:master' into master 2025-03-01 16:53:23 +01:00
Junyan Qin
3811700a78 chore: update llm-models.json template 2025-03-01 21:33:47 +08:00
Junyan Qin
8762ba3d9c feat(anthropic): add supports for tool use #763 2025-03-01 20:34:22 +08:00
Junyan Qin
c42b5aab5a feat: update components.yaml 2025-03-01 11:45:15 +08:00
Junyan Qin (Chin)
d724899ec0 Merge pull request #1148 from RockChinQ/feat/requester-manifests
feat: add manifests for all requesters
2025-03-01 00:12:55 +08:00
Junyan Qin
81aacdd76e refactor: lookup requester from discover engine 2025-03-01 00:12:23 +08:00
Junyan Qin
0aa072b4e8 feat: add manifests for all requesters 2025-02-28 22:47:34 +08:00
Tigrex Dai
6335e9dd8b Update anthropicmsgs.py 2025-02-28 13:02:06 +01:00
Tigrex Dai
a785289ac9 Update entities.py 2025-02-28 13:00:44 +01:00
Junyan Qin (Chin)
f8bace040c Merge pull request #1142 from fdc310/master
个人微信中主动发送图片的修改,但是只能发送链接
2025-02-28 11:33:43 +08:00
Dong_master
d62d597695 '个人微信中主动发送图片的修改,但是只能发送链接' 2025-02-28 01:31:59 +08:00
Dong_master
d938129884 '删除先' 2025-02-28 01:30:55 +08:00
Dong_master
327f448321 Resolved merge conflict in gewechat.py 2025-02-28 01:22:15 +08:00
Dong_master
19af3740c1 '个人微信中主动发送图片的修改,但是只能发送链接' 2025-02-28 01:17:25 +08:00
Junyan Qin
11b1110eed chore: release v3.4.9.5 2025-02-27 17:04:54 +08:00
Junyan Qin (Chin)
682b897e21 Merge pull request #1130 from fdc310/master
'个人微信中主动发送信息send_message的修改'
2025-02-26 15:54:02 +08:00
Junyan Qin
998ad7623c perf(gewechat): simplify 2025-02-26 15:53:26 +08:00
Junyan Qin (Chin)
4f1db33abc Merge pull request #1131 from shockno1/master
Update gewechat.py 添加gewe微信接口中voice语音的处理
2025-02-26 15:38:56 +08:00
shockno1
ca6cb60bdd Update gewechat.py 添加gewe微信接口中voice语音的处理
添加gewe微信接口中voice语音的处理
2025-02-26 12:45:28 +08:00
Dong_master
133e48a5a9 '个人微信中主动发送信息send_message的修改' 2025-02-26 02:54:42 +08:00
Junyan Qin
d659d01b1e chore: release v3.4.9.4 2025-02-25 17:03:00 +08:00
Junyan Qin
34f73fd84b fix: typo 2025-02-25 17:02:36 +08:00
Junyan Qin (Chin)
54b87ff79d Merge pull request #1128 from wang149876/master
让llm重载可以直接获取本地最新的llm_models.json里面的内容
2025-02-25 16:54:53 +08:00
wang149876
6c2843e7c1 精简为直接复制给llm_models_meta 2025-02-25 16:52:00 +08:00
Junyan Qin (Chin)
6761a31982 Merge pull request #1127 from Yi-Lyu/master
围绕 Gewechat 修改,1)支持聊天记录的消息; 2)图片消息改为图片常规尺寸图片放弃原来的缩略图
2025-02-25 16:15:17 +08:00
Junyan Qin
9401a79b2b feat: update file download url 2025-02-25 16:12:45 +08:00
wang149876
7a4905d943 让llm重载可以直接获取本地最新的llm_models.json里面的内容 2025-02-25 12:56:00 +08:00
Ethan
4db1d2b3a3 fix: comment out debug print statement in gewechat callback 2025-02-25 11:53:23 +08:00
Ethan
2ffe2967d6 feat: add download image port configuration and improve image retrieval process 2025-02-25 11:32:35 +08:00
Ethan
0875c0f266 Merge branch 'RockChinQ:master' into master 2025-02-25 08:48:01 +08:00
Junyan Qin
68c7de5199 chore: release v3.4.9.3 2025-02-24 23:01:10 +08:00
Junyan Qin
4dfb8597ae fix: #1124 2025-02-24 23:00:19 +08:00
Ethan
e21a27ff23 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:36:03 +08:00
Ethan
91ad7944de 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:18:35 +08:00
Ethan
c86602ebaf 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:17:15 +08:00
Ethan
f75ac292db 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:11:27 +08:00
Ethan
2742c249bf 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:09:11 +08:00
Ethan
36f04849ab Merge remote-tracking branch 'origin/master'
# Conflicts:
#	pkg/platform/sources/gewechat.py
2025-02-24 20:03:18 +08:00
Ethan
a60c896e89 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:02:49 +08:00
Ethan
c442320c7f 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 19:53:43 +08:00
Ethan
6aeae7e9f5 解决运行报错(base LangBot v3.4.9.2):
[02-24 05:46:37.616] manager.py (169) - [ERROR] : 平台适配器运行出错: 'GeWeChatAdapter' object has no attribute 'name'
2025-02-24 18:53:29 +08:00
Ethan
cae79aac48 添加微信消息类型 49(聊天记录)的支持,支持处理聊天记录类型的微信消息。
微信聊天记录是 xml 数据格式,本质上也是字符串,可以按照字符串Plain类型来处理。
2025-02-24 18:09:02 +08:00
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
53 changed files with 971 additions and 297 deletions

View File

@@ -115,6 +115,20 @@
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 | | [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 | | [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
### TTS
| 平台/模型 | 备注 |
| --- | --- |
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
### 文生图
| 平台/模型 | 备注 |
| --- | --- |
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
## 😘 社区贡献 ## 😘 社区贡献
LangBot 离不开以下贡献者和社区内所有人的贡献,我们欢迎任何形式的贡献和反馈。 LangBot 离不开以下贡献者和社区内所有人的贡献,我们欢迎任何形式的贡献和反馈。

View File

@@ -10,6 +10,10 @@ spec:
ComponentTemplate: ComponentTemplate:
fromFiles: fromFiles:
- pkg/platform/adapter.yaml - pkg/platform/adapter.yaml
- pkg/provider/modelmgr/requester.yaml
MessagePlatformAdapter: MessagePlatformAdapter:
fromDirs: fromDirs:
- path: pkg/platform/sources/ - path: pkg/platform/sources/
LLMAPIRequester:
fromDirs:
- path: pkg/provider/modelmgr/requesters/

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import json
import dingtalk_stream import dingtalk_stream
from dingtalk_stream import AckMessage from dingtalk_stream import AckMessage
@@ -17,7 +18,7 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler):
await self.client.update_incoming_message(incoming_message) await self.client.update_incoming_message(incoming_message)
return AckMessage.STATUS_OK, 'OK' return AckMessage.STATUS_OK, 'OK'
async def get_incoming_message(self): async def get_incoming_message(self):
"""异步等待消息的到来""" """异步等待消息的到来"""
while self.incoming_message is None: while self.incoming_message is None:

View File

@@ -1,4 +1,5 @@
import base64 import base64
import json
import time import time
from typing import Callable from typing import Callable
import dingtalk_stream import dingtalk_stream
@@ -92,8 +93,31 @@ class DingTalkClient:
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式 base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
return base64_str return base64_str
else: else:
raise Exception("获取图片失败") raise Exception("获取文件失败")
async def get_audio_url(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")
if download_url:
return await self.download_url_to_base64(download_url)
else:
raise Exception("获取音频失败")
else:
raise Exception(f"Error: {response.status_code}, {response.text}")
async def update_incoming_message(self, message): async def update_incoming_message(self, message):
"""异步更新 DingTalkClient 中的 incoming_message""" """异步更新 DingTalkClient 中的 incoming_message"""
message_data = await self.get_message(message) message_data = await self.get_message(message)
@@ -133,6 +157,7 @@ class DingTalkClient:
async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage): async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage):
try: try:
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
message_data = { message_data = {
"IncomingMessage":incoming_message, "IncomingMessage":incoming_message,
} }
@@ -160,10 +185,14 @@ class DingTalkClient:
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0]) message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
message_data['Type'] = 'image' message_data['Type'] = 'image'
elif incoming_message.message_type == 'audio':
message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode'])
# 删掉开头的@消息 message_data['Type'] = 'audio'
if message_data["Content"].startswith("@"+self.robot_name):
message_data["Content"][len("@"+self.robot_name):] copy_message_data = message_data.copy()
del copy_message_data['IncomingMessage']
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()

View File

@@ -1,4 +1,5 @@
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import dingtalk_stream
class DingTalkEvent(dict): class DingTalkEvent(dict):
@staticmethod @staticmethod
@@ -15,7 +16,7 @@ class DingTalkEvent(dict):
return self.get("Content","") return self.get("Content","")
@property @property
def incoming_message(self): def incoming_message(self) -> Optional["dingtalk_stream.chatbot.ChatbotMessage"]:
return self.get("IncomingMessage") return self.get("IncomingMessage")
@property @property
@@ -25,6 +26,10 @@ class DingTalkEvent(dict):
@property @property
def picture(self): def picture(self):
return self.get("Picture","") return self.get("Picture","")
@property
def audio(self):
return self.get("Audio","")
@property @property
def conversation(self): def conversation(self):
@@ -61,4 +66,4 @@ class DingTalkEvent(dict):
Returns: Returns:
str: 字符串表示。 str: 字符串表示。
""" """
return f"<WecomEvent {super().__repr__()}>" return f"<DingTalkEvent {super().__repr__()}>"

View File

@@ -29,6 +29,7 @@ from ..discover import engine as discover_engine
from ..utils import logcache, ip from ..utils import logcache, ip
from . import taskmgr from . import taskmgr
from . import entities as core_entities from . import entities as core_entities
from .bootutils import config
class Application: class Application:
@@ -203,6 +204,8 @@ class Application:
case core_entities.LifecycleControlScope.PROVIDER.value: case core_entities.LifecycleControlScope.PROVIDER.value:
self.logger.info("执行热重载 scope="+scope) self.logger.info("执行热重载 scope="+scope)
latest_llm_model_config = await config.load_json_config("data/metadata/llm-models.json", "templates/metadata/llm-models.json")
self.llm_models_meta = latest_llm_model_config
llm_model_mgr_inst = llm_model_mgr.ModelManager(self) llm_model_mgr_inst = llm_model_mgr.ModelManager(self)
await llm_model_mgr_inst.initialize() await llm_model_mgr_inst.initialize()
self.model_mgr = llm_model_mgr_inst self.model_mgr = llm_model_mgr_inst

View File

@@ -23,6 +23,7 @@ class GewechatConfigMigration(migration.Migration):
"adapter": "gewechat", "adapter": "gewechat",
"enable": False, "enable": False,
"gewechat_url": "http://your-gewechat-server:2531", "gewechat_url": "http://your-gewechat-server:2531",
"gewechat_file_url": "http://your-gewechat-server:2532",
"port": 2286, "port": 2286,
"callback_url": "http://your-callback-url:2286/gewechat/callback", "callback_url": "http://your-callback-url:2286/gewechat/callback",
"app_id": "", "app_id": "",

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

@@ -0,0 +1,29 @@
from __future__ import annotations
from urllib.parse import urlparse
from .. import migration
@migration.migration_class("gewechat-file-url-config", 34)
class GewechatFileUrlConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'gewechat':
if 'gewechat_file_url' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'gewechat':
if 'gewechat_file_url' not in adapter:
parsed_url = urlparse(adapter['gewechat_url'])
adapter['gewechat_file_url'] = f"{parsed_url.scheme}://{parsed_url.hostname}:2532"
await self.ap.platform_cfg.dump_config()

View File

@@ -11,7 +11,7 @@ from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_
from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config
from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config
from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config
from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config
@stage.stage_class("MigrationStage") @stage.stage_class("MigrationStage")
class MigrationStage(stage.BootingStage): class MigrationStage(stage.BootingStage):

View File

@@ -139,7 +139,7 @@ class ComponentDiscoveryEngine:
def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component: def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component:
"""加载组件清单""" """加载组件清单"""
with open(path, 'r') as f: with open(path, 'r', encoding='utf-8') as f:
manifest = yaml.safe_load(f) manifest = yaml.safe_load(f)
comp = Component( comp = Component(
owner=owner, owner=owner,
@@ -187,6 +187,9 @@ class ComponentDiscoveryEngine:
if name == 'ComponentTemplate': if name == 'ComponentTemplate':
continue continue
components[name] = self.load_blueprint_comp_group(component, owner) components[name] = self.load_blueprint_comp_group(component, owner)
self.ap.logger.debug(f'Components: {components}')
return blueprint_manifest, components return blueprint_manifest, components

View File

@@ -110,7 +110,7 @@ class PlatformManager:
if len(self.adapters) == 0: if len(self.adapters) == 0:
self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。') self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。')
async def write_back_config(self, adapter_inst: msadapter.MessagePlatformAdapter, config: dict): async def write_back_config(self, adapter_name: str, adapter_inst: msadapter.MessagePlatformAdapter, config: dict):
index = -2 index = -2
for i, adapter in enumerate(self.adapters): for i, adapter in enumerate(self.adapters):
@@ -132,7 +132,7 @@ class PlatformManager:
break break
new_cfg = { new_cfg = {
'adapter': adapter_inst.name, 'adapter': adapter_name,
'enable': True, 'enable': True,
**config **config
} }

View File

@@ -28,16 +28,23 @@ class DingTalkMessageConverter(adapter.MessageConverter):
return msg.text return msg.text
@staticmethod @staticmethod
async def target2yiri(event:DingTalkEvent): async def target2yiri(event:DingTalkEvent, bot_name:str):
yiri_msg_list = [] yiri_msg_list = []
yiri_msg_list.append( yiri_msg_list.append(
platform_message.Source(id = '0',time=datetime.datetime.now()) platform_message.Source(id = event.incoming_message.message_id,time=datetime.datetime.now())
) )
for atUser in event.incoming_message.at_users:
if atUser.dingtalk_id == event.incoming_message.chatbot_user_id:
yiri_msg_list.append(platform_message.At(target=bot_name))
if event.content: if event.content:
yiri_msg_list.append(platform_message.Plain(text=event.content)) text_content = event.content.replace("@"+bot_name, '')
yiri_msg_list.append(platform_message.Plain(text=text_content))
if event.picture: if event.picture:
yiri_msg_list.append(platform_message.Image(base64=event.picture)) yiri_msg_list.append(platform_message.Image(base64=event.picture))
if event.audio:
yiri_msg_list.append(platform_message.Voice(base64=event.audio))
chain = platform_message.MessageChain(yiri_msg_list) chain = platform_message.MessageChain(yiri_msg_list)
@@ -54,33 +61,33 @@ class DingTalkEventConverter(adapter.EventConverter):
@staticmethod @staticmethod
async def target2yiri( async def target2yiri(
event:DingTalkEvent event:DingTalkEvent,
bot_name:str
): ):
message_chain = await DingTalkMessageConverter.target2yiri(event) message_chain = await DingTalkMessageConverter.target2yiri(event, bot_name)
if event.conversation == 'FriendMessage': if event.conversation == 'FriendMessage':
return platform_events.FriendMessage( return platform_events.FriendMessage(
sender=platform_entities.Friend( sender=platform_entities.Friend(
id= 0, id=event.incoming_message.sender_id,
nickname ='nickname', nickname = event.incoming_message.sender_nick,
remark="" remark=""
), ),
message_chain = message_chain, message_chain = message_chain,
time = datetime.datetime.now(), time = event.incoming_message.create_at,
source_platform_object=event, source_platform_object=event,
) )
elif event.conversation == 'GroupMessage': elif event.conversation == 'GroupMessage':
message_chain.insert(0, platform_message.At(target="justbot"))
sender = platform_entities.GroupMember( sender = platform_entities.GroupMember(
id = 111, id = event.incoming_message.sender_id,
member_name="name", member_name=event.incoming_message.sender_nick,
permission= 'MEMBER', permission= 'MEMBER',
group = platform_entities.Group( group = platform_entities.Group(
id = 111, id = event.incoming_message.conversation_id,
name = 'MEMBER', name = event.incoming_message.conversation_title,
permission=platform_entities.Permission.Member permission=platform_entities.Permission.Member
), ),
special_title='', special_title='',
@@ -88,7 +95,7 @@ class DingTalkEventConverter(adapter.EventConverter):
last_speak_timestamp=0, last_speak_timestamp=0,
mute_time_remaining=0 mute_time_remaining=0
) )
time = datetime.datetime.now(), time = event.incoming_message.create_at
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender =sender, sender =sender,
message_chain = message_chain, message_chain = message_chain,
@@ -117,6 +124,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
missing_keys = [key for key in required_keys if key not in config] missing_keys = [key for key in required_keys if key not in config]
if missing_keys: if missing_keys:
raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员") raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员")
self.bot_account_id = self.config["robot_name"]
self.bot = DingTalkClient( self.bot = DingTalkClient(
client_id=config["client_id"], client_id=config["client_id"],
@@ -153,10 +162,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
], ],
): ):
async def on_message(event: DingTalkEvent): async def on_message(event: DingTalkEvent):
self.bot_account_id = 'justbot'
try: try:
return await callback( return await callback(
await self.event_converter.target2yiri(event), self await self.event_converter.target2yiri(event, self.config["robot_name"]), self
) )
except: except:
traceback.print_exc() traceback.print_exc()
@@ -167,7 +175,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
self.bot.on_message("GroupMessage")(on_message) self.bot.on_message("GroupMessage")(on_message)
async def run_async(self): async def run_async(self):
await self.bot.start() await self.bot.start()
async def kill(self) -> bool: async def kill(self) -> bool:

View File

@@ -28,7 +28,10 @@ from ...utils import image
class GewechatMessageConverter(adapter.MessageConverter): class GewechatMessageConverter(adapter.MessageConverter):
def __init__(self, config: dict):
self.config = config
@staticmethod @staticmethod
async def yiri2target( async def yiri2target(
message_chain: platform_message.MessageChain message_chain: platform_message.MessageChain
@@ -40,20 +43,25 @@ class GewechatMessageConverter(adapter.MessageConverter):
elif isinstance(component, platform_message.Plain): elif isinstance(component, platform_message.Plain):
content_list.append({"type": "text", "content": component.text}) content_list.append({"type": "text", "content": component.text})
elif isinstance(component, platform_message.Image): elif isinstance(component, platform_message.Image):
# content_list.append({"type": "image", "image_id": component.image_id}) if not component.url:
pass pass
content_list.append({"type": "image", "image": component.url})
elif isinstance(component, platform_message.Voice):
content_list.append({"type": "voice", "url": component.url, "length": component.length})
elif isinstance(component, platform_message.Forward): elif isinstance(component, platform_message.Forward):
for node in component.node_list: for node in component.node_list:
content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain))
return content_list return content_list
@staticmethod
async def target2yiri( async def target2yiri(
self,
message: dict, message: dict,
bot_account_id: str bot_account_id: str
) -> platform_message.MessageChain: ) -> platform_message.MessageChain:
if message["Data"]["MsgType"] == 1: if message["Data"]["MsgType"] == 1:
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉
regex = re.compile(r"^wxid_.*:") regex = re.compile(r"^wxid_.*:")
@@ -74,25 +82,77 @@ class GewechatMessageConverter(adapter.MessageConverter):
return platform_message.MessageChain(content_list) return platform_message.MessageChain(content_list)
elif message["Data"]["MsgType"] == 3: elif message["Data"]["MsgType"] == 3:
image_base64 = message["Data"]["ImgBuf"]["buffer"] image_xml = message["Data"]["Content"]["string"]
if not image_xml:
return platform_message.MessageChain([
platform_message.Plain(text="[图片内容为空]")
])
try:
base64_str, image_format = await image.get_gewechat_image_base64(
gewechat_url=self.config["gewechat_url"],
gewechat_file_url=self.config["gewechat_file_url"],
app_id=self.config["app_id"],
xml_content=image_xml,
token=self.config["token"],
image_type=2,
)
return platform_message.MessageChain([
platform_message.Image(
base64=f"data:image/{image_format};base64,{base64_str}"
)
])
except Exception as e:
print(f"处理图片消息失败: {str(e)}")
return platform_message.MessageChain([
platform_message.Plain(text=f"[图片处理失败]")
])
elif message["Data"]["MsgType"] == 34:
audio_base64 = message["Data"]["ImgBuf"]["buffer"]
return platform_message.MessageChain( return platform_message.MessageChain(
[platform_message.Image(base64=f"data:image/jpeg;base64,{image_base64}")] [platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")]
) )
elif message["Data"]["MsgType"] == 49:
# 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递
try:
content = message["Data"]["Content"]["string"]
try:
content_bytes = content.encode('utf-8')
decoded_content = base64.b64decode(content_bytes)
return platform_message.MessageChain(
[platform_message.Unknown(content=decoded_content)]
)
except Exception as e:
return platform_message.MessageChain(
[platform_message.Plain(text=content)]
)
except Exception as e:
print(f"Error processing type 49 message: {str(e)}")
return platform_message.MessageChain(
[platform_message.Plain(text="[无法解析的消息]")]
)
class GewechatEventConverter(adapter.EventConverter): class GewechatEventConverter(adapter.EventConverter):
def __init__(self, config: dict):
self.config = config
self.message_converter = GewechatMessageConverter(config)
@staticmethod @staticmethod
async def yiri2target( async def yiri2target(
event: platform_events.MessageEvent event: platform_events.MessageEvent
) -> dict: ) -> dict:
pass pass
@staticmethod
async def target2yiri( async def target2yiri(
self,
event: dict, event: dict,
bot_account_id: str bot_account_id: str
) -> platform_events.MessageEvent: ) -> platform_events.MessageEvent:
message_chain = await GewechatMessageConverter.target2yiri(copy.deepcopy(event), bot_account_id) message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id)
if not message_chain: if not message_chain:
return None return None
@@ -120,7 +180,7 @@ class GewechatEventConverter(adapter.EventConverter):
time=event["Data"]["CreateTime"], time=event["Data"]["CreateTime"],
source_platform_object=event, source_platform_object=event,
) )
elif 'wxid_' in event["Data"]["FromUserName"]["string"]: else:
return platform_events.FriendMessage( return platform_events.FriendMessage(
sender=platform_entities.Friend( sender=platform_entities.Friend(
id=event["Data"]["FromUserName"]["string"], id=event["Data"]["FromUserName"]["string"],
@@ -134,7 +194,9 @@ class GewechatEventConverter(adapter.EventConverter):
class GeWeChatAdapter(adapter.MessagePlatformAdapter): class GeWeChatAdapter(adapter.MessagePlatformAdapter):
name: str = "gewechat" # 定义适配器名称
bot: gewechat_client.GewechatClient bot: gewechat_client.GewechatClient
quart_app: quart.Quart quart_app: quart.Quart
@@ -144,8 +206,8 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
ap: app.Application ap: app.Application
message_converter: GewechatMessageConverter = GewechatMessageConverter() message_converter: GewechatMessageConverter
event_converter: GewechatEventConverter = GewechatEventConverter() event_converter: GewechatEventConverter
listeners: typing.Dict[ listeners: typing.Dict[
typing.Type[platform_events.Event], typing.Type[platform_events.Event],
@@ -158,10 +220,20 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
self.quart_app = quart.Quart(__name__) self.quart_app = quart.Quart(__name__)
self.message_converter = GewechatMessageConverter(config)
self.event_converter = GewechatEventConverter(config)
@self.quart_app.route('/gewechat/callback', methods=['POST']) @self.quart_app.route('/gewechat/callback', methods=['POST'])
async def gewechat_callback(): async def gewechat_callback():
data = await quart.request.json data = await quart.request.json
# print(json.dumps(data, indent=4, ensure_ascii=False)) # print(json.dumps(data, indent=4, ensure_ascii=False))
if 'data' in data:
data['Data'] = data['data']
if 'type_name' in data:
data['TypeName'] = data['type_name']
# print(json.dumps(data, indent=4, ensure_ascii=False))
if 'testMsg' in data: if 'testMsg' in data:
return 'ok' return 'ok'
@@ -183,7 +255,19 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
target_id: str, target_id: str,
message: platform_message.MessageChain message: platform_message.MessageChain
): ):
pass geweap_msg = await self.message_converter.yiri2target(message)
# 此处加上群消息at处理
# ats = [item["target"] for item in geweap_msg if item["type"] == "at"]
for msg in geweap_msg:
if msg['type'] == 'text':
await self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id, content=msg['content'])
elif msg['type'] == 'image':
await self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"])
async def reply_message( async def reply_message(
self, self,
@@ -257,7 +341,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
self.ap.logger.info(f"Gewechat 登录成功app_id: {app_id}") self.ap.logger.info(f"Gewechat 登录成功app_id: {app_id}")
await self.ap.platform_mgr.write_back_config(self, self.config) await self.ap.platform_mgr.write_back_config('gewechat', self, self.config)
# 获取 nickname # 获取 nickname
profile = self.bot.get_profile(self.config["app_id"]) profile = self.bot.get_profile(self.config["app_id"])
@@ -281,4 +365,4 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
) )
async def kill(self) -> bool: async def kill(self) -> bool:
pass pass

View File

@@ -17,6 +17,13 @@ spec:
type: string type: string
required: true required: true
default: "" default: ""
- name: gewechat_file_url
label:
en_US: GeWeChat file download URL
zh_CN: GeWeChat 文件下载URL
type: string
required: true
default: ""
- name: port - name: port
label: label:
en_US: Port en_US: Port

View File

@@ -74,7 +74,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
remark = "", remark = "",
) )
return platform_events.FriendMessage( return platform_events.FriendMessage(
sender = friend,message_chain = yiri_chain,time = event.timestamp, sender = friend,message_chain = yiri_chain,time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
),
source_platform_object=event source_platform_object=event
) )
@@ -105,7 +109,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
last_speak_timestamp=0, last_speak_timestamp=0,
mute_time_remaining=0 mute_time_remaining=0
) )
time = event.timestamp time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
)
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender = sender, sender = sender,
message_chain=yiri_chain, message_chain=yiri_chain,
@@ -128,7 +136,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
last_speak_timestamp=0, last_speak_timestamp=0,
mute_time_remaining=0 mute_time_remaining=0
) )
time = event.timestamp, time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
),
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender =sender, sender =sender,
message_chain = yiri_chain, message_chain = yiri_chain,

View File

@@ -13,27 +13,20 @@ import pydantic.v1 as pydantic
class Entity(pydantic.BaseModel): class Entity(pydantic.BaseModel):
"""实体,表示一个用户或群。""" """实体,表示一个用户或群。"""
id: int id: int
"""QQ 号或群号""" """ID"""
@abc.abstractmethod
def get_avatar_url(self) -> str:
"""头像图片链接。"""
@abc.abstractmethod @abc.abstractmethod
def get_name(self) -> str: def get_name(self) -> str:
"""名称。""" """名称。"""
class Friend(Entity): class Friend(Entity):
"""好友""" """私聊对象"""
id: typing.Union[int, str] id: typing.Union[int, str]
"""QQ 号""" """ID"""
nickname: typing.Optional[str] nickname: typing.Optional[str]
"""昵称。""" """昵称。"""
remark: typing.Optional[str] remark: typing.Optional[str]
"""备注。""" """备注。"""
def get_avatar_url(self) -> str:
return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140'
def get_name(self) -> str: def get_name(self) -> str:
return self.nickname or self.remark or '' return self.nickname or self.remark or ''
@@ -59,8 +52,6 @@ class Group(Entity):
"""群名称。""" """群名称。"""
permission: Permission permission: Permission
"""Bot 在群中的权限。""" """Bot 在群中的权限。"""
def get_avatar_url(self) -> str:
return f'https://p.qlogo.cn/gh/{self.id}/{self.id}/'
def get_name(self) -> str: def get_name(self) -> str:
return self.name return self.name
@@ -69,11 +60,11 @@ class Group(Entity):
class GroupMember(Entity): class GroupMember(Entity):
"""群成员。""" """群成员。"""
id: typing.Union[int, str] id: typing.Union[int, str]
"""QQ 号""" """群员 ID"""
member_name: str member_name: str
"""员名称。""" """群员名称。"""
permission: Permission permission: Permission
"""Bot 在群中的权限。""" """在群中的权限。"""
group: Group group: Group
"""群。""" """群。"""
special_title: str = '' special_title: str = ''
@@ -84,61 +75,6 @@ class GroupMember(Entity):
"""最后一次发言的时间。""" """最后一次发言的时间。"""
mute_time_remaining: int = 0 mute_time_remaining: int = 0
"""禁言剩余时间。""" """禁言剩余时间。"""
def get_avatar_url(self) -> str:
return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140'
def get_name(self) -> str: def get_name(self) -> str:
return self.member_name return self.member_name
class Client(Entity):
"""来自其他客户端的用户。"""
id: typing.Union[int, str]
"""识别 id。"""
platform: str
"""来源平台。"""
def get_avatar_url(self) -> str:
raise NotImplementedError
def get_name(self) -> str:
return self.platform
class Subject(pydantic.BaseModel):
"""另一种实体类型表示。"""
id: typing.Union[int, str]
"""QQ 号或群号。"""
kind: typing.Literal['Friend', 'Group', 'Stranger']
"""类型。"""
class Config(pydantic.BaseModel):
"""配置项类型。"""
def modify(self, **kwargs) -> 'Config':
"""修改部分设置。"""
for k, v in kwargs.items():
if k in self.__fields__:
setattr(self, k, v)
else:
raise ValueError(f'未知配置项: {k}')
return self
class GroupConfigModel(Config):
"""群配置。"""
name: str
"""群名称。"""
confess_talk: bool
"""是否允许坦白说。"""
allow_member_invite: bool
"""是否允许成员邀请好友入群。"""
auto_approve: bool
"""是否开启自动审批入群。"""
anonymous_chat: bool
"""是否开启匿名聊天。"""
announcement: str = ''
"""群公告。"""
class MemberInfoModel(Config, GroupMember):
"""群成员信息。"""

View File

@@ -43,21 +43,6 @@ class Event(pydantic.BaseModel):
return Event return Event
###############################
# Bot Event
class BotEvent(Event):
"""Bot 自身事件。
Args:
type: 事件名。
qq: Bot 的 QQ 号。
"""
type: str
"""事件名。"""
qq: int
"""Bot 的 QQ 号。"""
############################### ###############################
# Message Event # Message Event
class MessageEvent(Event): class MessageEvent(Event):
@@ -72,6 +57,9 @@ class MessageEvent(Event):
message_chain: platform_message.MessageChain message_chain: platform_message.MessageChain
"""消息内容。""" """消息内容。"""
time: float | None = None
"""消息发送时间戳。"""
source_platform_object: typing.Optional[typing.Any] = None source_platform_object: typing.Optional[typing.Any] = None
"""原消息平台对象。 """原消息平台对象。
供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息, 供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息,
@@ -79,7 +67,7 @@ class MessageEvent(Event):
class FriendMessage(MessageEvent): class FriendMessage(MessageEvent):
"""好友消息。 """私聊消息。
Args: Args:
type: 事件名。 type: 事件名。
@@ -111,19 +99,3 @@ class GroupMessage(MessageEvent):
@property @property
def group(self) -> platform_entities.Group: def group(self) -> platform_entities.Group:
return self.sender.group return self.sender.group
class StrangerMessage(MessageEvent):
"""陌生人消息。
Args:
type: 事件名。
sender: 发送消息的人。
message_chain: 消息内容。
"""
type: str = 'StrangerMessage'
"""事件名。"""
sender: platform_entities.Friend
"""发送消息的人。"""
message_chain: platform_message.MessageChain
"""消息内容。"""

View File

@@ -116,18 +116,6 @@ class MessageChain(PlatformBaseModel):
print('At Me') print('At Me')
``` ```
消息链对索引操作进行了增强。以消息组件类型为索引,获取消息链中的全部该类型的消息组件。
```py
plain_list = message_chain[Plain]
'[Plain("Hello World!")]'
```
可以用加号连接两个消息链。
```py
MessageChain(['Hello World!']) + MessageChain(['Goodbye World!'])
# 返回 MessageChain([Plain("Hello World!"), Plain("Goodbye World!")])
```
""" """
__root__: typing.List[MessageComponent] __root__: typing.List[MessageComponent]
@@ -488,9 +476,9 @@ class Quote(MessageComponent):
group_id: typing.Optional[typing.Union[int, str]] = None group_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息所接收的群号当为好友消息时为0。""" """被引用回复的原消息所接收的群号当为好友消息时为0。"""
sender_id: typing.Optional[typing.Union[int, str]] = None sender_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的发送者的QQ号""" """被引用回复的原消息的发送者的ID"""
target_id: typing.Optional[typing.Union[int, str]] = None target_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的接收者者的QQ号或群号""" """被引用回复的原消息的接收者者的ID或群ID"""
origin: MessageChain origin: MessageChain
"""被引用回复的原消息的消息链对象。""" """被引用回复的原消息的消息链对象。"""
@@ -504,7 +492,7 @@ class At(MessageComponent):
type: str = "At" type: str = "At"
"""消息组件类型。""" """消息组件类型。"""
target: typing.Union[int, str] target: typing.Union[int, str]
"""群员 QQ 号""" """群员 ID"""
display: typing.Optional[str] = None display: typing.Optional[str] = None
"""At时显示的文字发送消息时无效自动使用群名片。""" """At时显示的文字发送消息时无效自动使用群名片。"""
def __eq__(self, other): def __eq__(self, other):
@@ -527,9 +515,9 @@ class Image(MessageComponent):
type: str = "Image" type: str = "Image"
"""消息组件类型。""" """消息组件类型。"""
image_id: typing.Optional[str] = None image_id: typing.Optional[str] = None
"""图片的 image_id群图片与好友图片格式不同。不为空时将忽略 url 属性。""" """图片的 image_id不为空时将忽略 url 属性。"""
url: typing.Optional[pydantic.HttpUrl] = None url: typing.Optional[pydantic.HttpUrl] = None
"""图片的 URL发送时可作网络图片的链接接收时为腾讯图片服务器的链接,可用于图片下载。""" """图片的 URL发送时可作网络图片的链接接收时为图片的链接,可用于图片下载。"""
path: typing.Union[str, Path, None] = None path: typing.Union[str, Path, None] = None
"""图片的路径,发送本地图片。""" """图片的路径,发送本地图片。"""
base64: typing.Optional[str] = None base64: typing.Optional[str] = None
@@ -663,7 +651,7 @@ class Voice(MessageComponent):
voice_id: typing.Optional[str] = None voice_id: typing.Optional[str] = None
"""语音的 voice_id不为空时将忽略 url 属性。""" """语音的 voice_id不为空时将忽略 url 属性。"""
url: typing.Optional[str] = None url: typing.Optional[str] = None
"""语音的 URL发送时可作网络语音的链接接收时为腾讯语音服务器的链接,可用于语音下载。""" """语音的 URL发送时可作网络语音的链接接收时为语音文件的链接,可用于语音下载。"""
path: typing.Optional[str] = None path: typing.Optional[str] = None
"""语音的路径,发送本地语音。""" """语音的路径,发送本地语音。"""
base64: typing.Optional[str] = None base64: typing.Optional[str] = None
@@ -691,8 +679,6 @@ class Voice(MessageComponent):
): ):
"""下载语音到本地。 """下载语音到本地。
语音采用 silk v3 格式silk 格式的编码解码请使用 [graiax-silkcoder](https://pypi.org/project/graiax-silkcoder/)。
Args: Args:
filename: 下载到本地的文件路径。与 `directory` 二选一。 filename: 下载到本地的文件路径。与 `directory` 二选一。
directory: 下载到本地的文件夹路径。与 `filename` 二选一。 directory: 下载到本地的文件夹路径。与 `filename` 二选一。
@@ -750,13 +736,13 @@ class Voice(MessageComponent):
class ForwardMessageNode(pydantic.BaseModel): class ForwardMessageNode(pydantic.BaseModel):
"""合并转发中的一条消息。""" """合并转发中的一条消息。"""
sender_id: typing.Optional[typing.Union[int, str]] = None sender_id: typing.Optional[typing.Union[int, str]] = None
"""发送人QQ号""" """发送人ID"""
sender_name: typing.Optional[str] = None sender_name: typing.Optional[str] = None
"""显示名称。""" """显示名称。"""
message_chain: typing.Optional[MessageChain] = None message_chain: typing.Optional[MessageChain] = None
"""消息内容。""" """消息内容。"""
message_id: typing.Optional[int] = None message_id: typing.Optional[int] = None
"""消息的 message_id,可以只使用此属性,从缓存中读取消息内容""" """消息的 message_id。"""
time: typing.Optional[datetime] = None time: typing.Optional[datetime] = None
"""发送时间。""" """发送时间。"""
@pydantic.validator('message_chain', check_fields=False) @pydantic.validator('message_chain', check_fields=False)

View File

@@ -4,7 +4,7 @@ import aiohttp
from . import entities, requester from . import entities, requester
from ...core import app from ...core import app
from ...discover import engine
from . import token from . import token
from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl
@@ -16,6 +16,8 @@ class ModelManager:
ap: app.Application ap: app.Application
requester_components: list[engine.Component]
model_list: list[entities.LLMModelInfo] model_list: list[entities.LLMModelInfo]
requesters: dict[str, requester.LLMAPIRequester] requesters: dict[str, requester.LLMAPIRequester]
@@ -38,14 +40,21 @@ class ModelManager:
async def initialize(self): async def initialize(self):
self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester')
# 初始化token_mgr, requester # 初始化token_mgr, requester
for k, v in self.ap.provider_cfg.data['keys'].items(): for k, v in self.ap.provider_cfg.data['keys'].items():
self.token_mgrs[k] = token.TokenManager(k, v) self.token_mgrs[k] = token.TokenManager(k, v)
for api_cls in requester.preregistered_requesters: # for api_cls in requester.preregistered_requesters:
# api_inst = api_cls(self.ap)
# await api_inst.initialize()
# self.requesters[api_inst.name] = api_inst
for component in self.requester_components:
api_cls = component.get_python_component_class()
api_inst = api_cls(self.ap) api_inst = api_cls(self.ap)
await api_inst.initialize() await api_inst.initialize()
self.requesters[api_inst.name] = api_inst self.requesters[component.metadata.name] = api_inst
# 尝试从api获取最新的模型信息 # 尝试从api获取最新的模型信息
try: try:

View File

@@ -10,18 +10,6 @@ from . import entities as modelmgr_entities
from ..tools import entities as tools_entities from ..tools import entities as tools_entities
preregistered_requesters: list[typing.Type[LLMAPIRequester]] = []
def requester_class(name: str):
def decorator(cls: typing.Type[LLMAPIRequester]) -> typing.Type[LLMAPIRequester]:
cls.name = name
preregistered_requesters.append(cls)
return cls
return decorator
class LLMAPIRequester(metaclass=abc.ABCMeta): class LLMAPIRequester(metaclass=abc.ABCMeta):
"""LLM API请求器 """LLM API请求器
""" """

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
import json
import traceback import traceback
import base64 import base64
@@ -16,7 +17,6 @@ from ...tools import entities as tools_entities
from ....utils import image from ....utils import image
@requester.requester_class("anthropic-messages")
class AnthropicMessages(requester.LLMAPIRequester): class AnthropicMessages(requester.LLMAPIRequester):
"""Anthropic Messages API 请求器""" """Anthropic Messages API 请求器"""
@@ -69,11 +69,32 @@ class AnthropicMessages(requester.LLMAPIRequester):
req_messages = [] req_messages = []
for m in messages: for m in messages:
if isinstance(m.content, str) and m.content.strip() != "": if m.role == 'tool':
req_messages.append(m.dict(exclude_none=True)) tool_call_id = m.tool_call_id
elif isinstance(m.content, list):
msg_dict = m.dict(exclude_none=True) req_messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_call_id,
"content": m.content
}
]
})
continue
msg_dict = m.dict(exclude_none=True)
if isinstance(m.content, str) and m.content.strip() != "":
msg_dict["content"] = [
{
"type": "text",
"text": m.content
}
]
elif isinstance(m.content, list):
for i, ce in enumerate(m.content): for i, ce in enumerate(m.content):
@@ -90,25 +111,60 @@ class AnthropicMessages(requester.LLMAPIRequester):
} }
msg_dict["content"][i] = alter_image_ele msg_dict["content"][i] = alter_image_ele
req_messages.append(msg_dict) if m.tool_calls:
for tool_call in m.tool_calls:
msg_dict["content"].append({
"type": "tool_use",
"id": tool_call.id,
"name": tool_call.function.name,
"input": json.loads(tool_call.function.arguments)
})
del msg_dict["tool_calls"]
req_messages.append(msg_dict)
args["messages"] = req_messages args["messages"] = req_messages
if funcs:
tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs)
# anthropic的tools处在beta阶段sdk不稳定故暂时不支持 if tools:
# args["tools"] = tools
# if funcs:
# tools = await self.ap.tool_mgr.generate_tools_for_openai(funcs)
# if tools:
# args["tools"] = tools
try: try:
# print(json.dumps(args, indent=4, ensure_ascii=False))
resp = await self.client.messages.create(**args) resp = await self.client.messages.create(**args)
return llm_entities.Message( args = {
content=resp.content[0].text, 'content': '',
role=resp.role 'role': resp.role,
) }
assert type(resp) is anthropic.types.message.Message
for block in resp.content:
if block.type == 'thinking':
args['content'] = '<think>' + block.thinking + '</think>\n' + args['content']
elif block.type == 'text':
args['content'] += block.text
elif block.type == 'tool_use':
assert type(block) is anthropic.types.tool_use_block.ToolUseBlock
tool_call = llm_entities.ToolCall(
id=block.id,
type="function",
function=llm_entities.FunctionCall(
name=block.name,
arguments=json.dumps(block.input)
)
)
if 'tool_calls' not in args:
args['tool_calls'] = []
args['tool_calls'].append(tool_call)
return llm_entities.Message(**args)
except anthropic.AuthenticationError as e: except anthropic.AuthenticationError as e:
raise errors.RequesterError(f'api-key 无效: {e.message}') raise errors.RequesterError(f'api-key 无效: {e.message}')
except anthropic.BadRequestError as e: except anthropic.BadRequestError as e:

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: anthropic-messages
label:
en_US: Anthropic
zh_CN: Anthropic
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.anthropic.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./anthropicmsgs.py
attr: AnthropicMessages

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("bailian-chat-completions")
class BailianChatCompletions(chatcmpl.OpenAIChatCompletions): class BailianChatCompletions(chatcmpl.OpenAIChatCompletions):
"""阿里云百炼大模型平台 ChatCompletion API 请求器""" """阿里云百炼大模型平台 ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: bailian-chat-completions
label:
en_US: Aliyun Bailian
zh_CN: 阿里云百炼
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://dashscope.aliyuncs.com/compatible-mode/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./bailianchatcmpl.py
attr: BailianChatCompletions

View File

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

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: openai-chat-completions
label:
en_US: OpenAI
zh_CN: OpenAI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.openai.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./chatcmpl.py
attr: OpenAIChatCompletions

View File

@@ -7,7 +7,6 @@ from ... import entities as llm_entities
from ...tools import entities as tools_entities from ...tools import entities as tools_entities
@requester.requester_class("deepseek-chat-completions")
class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Deepseek ChatCompletion API 请求器""" """Deepseek ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: deepseek-chat-completions
label:
en_US: DeepSeek
zh_CN: 深度求索
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.deepseek.com"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./deepseekchatcmpl.py
attr: DeepseekChatCompletions

View File

@@ -14,7 +14,6 @@ from ...tools import entities as tools_entities
from .. import entities as modelmgr_entities from .. import entities as modelmgr_entities
@requester.requester_class("gitee-ai-chat-completions")
class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Gitee AI ChatCompletions API 请求器""" """Gitee AI ChatCompletions API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: gitee-ai-chat-completions
label:
en_US: Gitee AI
zh_CN: Gitee AI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://ai.gitee.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./giteeaichatcmpl.py
attr: GiteeAIChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("lmstudio-chat-completions")
class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions): class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions):
"""LMStudio ChatCompletion API 请求器""" """LMStudio ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: lmstudio-chat-completions
label:
en_US: LM Studio
zh_CN: LM Studio
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "http://127.0.0.1:1234/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./lmstudiochatcmpl.py
attr: LmStudioChatCompletions

View File

@@ -9,7 +9,6 @@ from ... import entities as llm_entities
from ...tools import entities as tools_entities from ...tools import entities as tools_entities
@requester.requester_class("moonshot-chat-completions")
class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Moonshot ChatCompletion API 请求器""" """Moonshot ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: moonshot-chat-completions
label:
en_US: Moonshot
zh_CN: 月之暗面
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.moonshot.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./moonshotchatcmpl.py
attr: MoonshotChatCompletions

View File

@@ -20,7 +20,6 @@ from ....utils import image
REQUESTER_NAME: str = "ollama-chat" REQUESTER_NAME: str = "ollama-chat"
@requester.requester_class(REQUESTER_NAME)
class OllamaChatCompletions(requester.LLMAPIRequester): class OllamaChatCompletions(requester.LLMAPIRequester):
"""Ollama平台 ChatCompletion API请求器""" """Ollama平台 ChatCompletion API请求器"""
client: ollama.AsyncClient client: ollama.AsyncClient

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: ollama-chat
label:
en_US: Ollama
zh_CN: Ollama
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "http://127.0.0.1:11434"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./ollamachat.py
attr: OllamaChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("siliconflow-chat-completions")
class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions): class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions):
"""SiliconFlow ChatCompletion API 请求器""" """SiliconFlow ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: siliconflow-chat-completions
label:
en_US: SiliconFlow
zh_CN: 硅基流动
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.siliconflow.cn/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./siliconflowchatcmpl.py
attr: SiliconFlowChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("volcark-chat-completions")
class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions): class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions):
"""火山方舟大模型平台 ChatCompletion API 请求器""" """火山方舟大模型平台 ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: volcark-chat-completions
label:
en_US: Volc Engine Ark
zh_CN: 火山方舟
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://ark.cn-beijing.volces.com/api/v3"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./volcarkchatcmpl.py
attr: VolcArkChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app from ....core import app
@requester.requester_class("xai-chat-completions")
class XaiChatCompletions(chatcmpl.OpenAIChatCompletions): class XaiChatCompletions(chatcmpl.OpenAIChatCompletions):
"""xAI ChatCompletion API 请求器""" """xAI ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: xai-chat-completions
label:
en_US: xAI
zh_CN: xAI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.x.ai/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./xaichatcmpl.py
attr: XaiChatCompletions

View File

@@ -7,7 +7,6 @@ from . import chatcmpl
from .. import requester from .. import requester
@requester.requester_class("zhipuai-chat-completions")
class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions): class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions):
"""智谱AI ChatCompletion API 请求器""" """智谱AI ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: zhipuai-chat-completions
label:
en_US: ZhipuAI
zh_CN: 智谱 AI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://open.bigmodel.cn/api/paas/v4"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./zhipuaichatcmpl.py
attr: ZhipuAIChatCompletions

View File

@@ -3,7 +3,9 @@ from __future__ import annotations
import typing import typing
import json import json
import uuid import uuid
import re
import base64 import base64
import datetime
import aiohttp import aiohttp
@@ -41,6 +43,23 @@ class DifyServiceAPIRunner(runner.RequestRunner):
base_url=self.ap.provider_cfg.data["dify-service-api"]["base-url"], 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( async def _preprocess_user_message(
self, query: core_entities.Query self, query: core_entities.Query
) -> tuple[str, list[str]]: ) -> tuple[str, list[str]]:
@@ -51,6 +70,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
""" """
plain_text = "" plain_text = ""
image_ids = [] image_ids = []
if isinstance(query.user_message.content, list): if isinstance(query.user_message.content, list):
for ce in query.user_message.content: for ce in query.user_message.content:
if ce.type == "text": if ce.type == "text":
@@ -109,7 +129,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
if chunk['data']['node_type'] == 'answer': if chunk['data']['node_type'] == 'answer':
yield llm_entities.Message( yield llm_entities.Message(
role="assistant", role="assistant",
content=chunk['data']['outputs']['answer'], content=self._try_convert_thinking(chunk['data']['outputs']['answer']),
) )
elif mode == "basic": elif mode == "basic":
if chunk['event'] == 'message': if chunk['event'] == 'message':
@@ -117,7 +137,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
elif chunk['event'] == 'message_end': elif chunk['event'] == 'message_end':
yield llm_entities.Message( yield llm_entities.Message(
role="assistant", role="assistant",
content=basic_mode_pending_chunk, content=self._try_convert_thinking(basic_mode_pending_chunk),
) )
basic_mode_pending_chunk = '' basic_mode_pending_chunk = ''
@@ -212,6 +232,9 @@ class DifyServiceAPIRunner(runner.RequestRunner):
plain_text, image_ids = await self._preprocess_user_message(query) plain_text, image_ids = await self._preprocess_user_message(query)
# 尝试获取 CreateTime
create_time = int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
files = [ files = [
{ {
"type": "image", "type": "image",
@@ -228,6 +251,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
"langbot_user_message_text": plain_text, "langbot_user_message_text": plain_text,
"langbot_session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}", "langbot_session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}",
"langbot_conversation_id": cov_id, "langbot_conversation_id": cov_id,
"langbot_msg_create_time": create_time,
}, },
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}", user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
files=files, files=files,

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.9" semantic_version = "v3.4.10.2"
debug_mode = False debug_mode = False

View File

@@ -8,6 +8,106 @@ import aiohttp
import PIL.Image import PIL.Image
import httpx import httpx
import os
import aiofiles
import pathlib
import asyncio
from urllib.parse import urlparse
async def get_gewechat_image_base64(
gewechat_url: str,
gewechat_file_url: str,
app_id: str,
xml_content: str,
token: str,
image_type: int = 2,
) -> typing.Tuple[str, str]:
"""从gewechat服务器获取图片并转换为base64格式
Args:
gewechat_url (str): gewechat服务器地址用于获取图片URL
gewechat_file_url (str): gewechat文件下载服务地址
app_id (str): gewechat应用ID
xml_content (str): 图片的XML内容
token (str): Gewechat API Token
image_type (int, optional): 图片类型. Defaults to 2.
Returns:
typing.Tuple[str, str]: (base64编码, 图片格式)
Raises:
aiohttp.ClientTimeout: 请求超时15秒或连接超时2秒
Exception: 其他错误
"""
headers = {
'X-GEWE-TOKEN': token,
'Content-Type': 'application/json'
}
# 设置超时
timeout = aiohttp.ClientTimeout(
total=15.0, # 总超时时间15秒
connect=2.0, # 连接超时2秒
sock_connect=2.0, # socket连接超时2秒
sock_read=15.0 # socket读取超时15秒
)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
# 获取图片下载链接
try:
async with session.post(
f"{gewechat_url}/v2/api/message/downloadImage",
headers=headers,
json={
"appId": app_id,
"type": image_type,
"xml": xml_content
}
) as response:
if response.status != 200:
raise Exception(f"获取gewechat图片下载失败: {await response.text()}")
resp_data = await response.json()
if resp_data.get("ret") != 200:
raise Exception(f"获取gewechat图片下载链接失败: {resp_data}")
file_url = resp_data['data']['fileUrl']
except asyncio.TimeoutError:
raise Exception("获取图片下载链接超时")
except aiohttp.ClientError as e:
raise Exception(f"获取图片下载链接网络错误: {str(e)}")
# 解析原始URL并替换端口
base_url = gewechat_file_url
download_url = f"{base_url}/download/{file_url}"
# 下载图片
try:
async with session.get(download_url) as img_response:
if img_response.status != 200:
raise Exception(f"下载图片失败: {await img_response.text()}, URL: {download_url}")
image_data = await img_response.read()
content_type = img_response.headers.get('Content-Type', '')
if content_type:
image_format = content_type.split('/')[-1]
else:
image_format = file_url.split('.')[-1]
base64_str = base64.b64encode(image_data).decode('utf-8')
return base64_str, image_format
except asyncio.TimeoutError:
raise Exception(f"下载图片超时, URL: {download_url}")
except aiohttp.ClientError as e:
raise Exception(f"下载图片网络错误: {str(e)}, URL: {download_url}")
except Exception as e:
raise Exception(f"获取图片失败: {str(e)}") from e
async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]: async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]:
""" """
下载企业微信图片并转换为 base64 下载企业微信图片并转换为 base64

View File

@@ -74,19 +74,29 @@
"name": "claude-3-opus-latest", "name": "claude-3-opus-latest",
"requester": "anthropic-messages", "requester": "anthropic-messages",
"token_mgr": "anthropic", "token_mgr": "anthropic",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "claude-3-5-sonnet-latest", "name": "claude-3-5-sonnet-latest",
"requester": "anthropic-messages", "requester": "anthropic-messages",
"token_mgr": "anthropic", "token_mgr": "anthropic",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "claude-3-5-haiku-latest", "name": "claude-3-5-haiku-latest",
"requester": "anthropic-messages", "requester": "anthropic-messages",
"token_mgr": "anthropic", "token_mgr": "anthropic",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
},
{
"name": "claude-3-7-sonnet-latest",
"requester": "anthropic-messages",
"token_mgr": "anthropic",
"vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "moonshot-v1-8k", "name": "moonshot-v1-8k",
@@ -156,61 +166,72 @@
{ {
"name": "glm-4-plus", "name": "glm-4-plus",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-0520", "name": "glm-4-0520",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-air", "name": "glm-4-air",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-airx", "name": "glm-4-airx",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-long", "name": "glm-4-long",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-flashx", "name": "glm-4-flashx",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4-flash", "name": "glm-4-flash",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai" "token_mgr": "zhipuai",
"tool_call_supported": true
}, },
{ {
"name": "glm-4v-plus", "name": "glm-4v-plus",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "glm-4v", "name": "glm-4v",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "glm-4v-flash", "name": "glm-4v-flash",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
}, },
{ {
"name": "glm-zero-preview", "name": "glm-zero-preview",
"requester": "zhipuai-chat-completions", "requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai", "token_mgr": "zhipuai",
"vision_supported": true "vision_supported": true,
"tool_call_supported": true
} }
] ]
} }

View File

@@ -64,6 +64,7 @@
"adapter": "gewechat", "adapter": "gewechat",
"enable": false, "enable": false,
"gewechat_url": "http://your-gewechat-server:2531", "gewechat_url": "http://your-gewechat-server:2531",
"gewechat_file_url": "http://your-gewechat-server:2532",
"port": 2286, "port": 2286,
"callback_url": "http://your-callback-url:2286/gewechat/callback", "callback_url": "http://your-callback-url:2286/gewechat/callback",
"app_id": "", "app_id": "",

View File

@@ -106,6 +106,9 @@
"dify-service-api": { "dify-service-api": {
"base-url": "https://api.dify.ai/v1", "base-url": "https://api.dify.ai/v1",
"app-type": "chat", "app-type": "chat",
"options": {
"convert-thinking-tips": "plain"
},
"chat": { "chat": {
"api-key": "app-1234567890", "api-key": "app-1234567890",
"timeout": 120 "timeout": 120

View File

@@ -325,6 +325,11 @@
"default": "", "default": "",
"description": "gewechat 的 url" "description": "gewechat 的 url"
}, },
"gewechat_file_url": {
"type": "string",
"default": "",
"description": "gewechat 文件下载URL"
},
"port": { "port": {
"type": "integer", "type": "integer",
"default": 2286, "default": 2286,

View File

@@ -404,6 +404,20 @@
"enum": ["chat", "workflow", "agent"], "enum": ["chat", "workflow", "agent"],
"default": "chat" "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": { "chat": {
"type": "object", "type": "object",
"title": "聊天助手 API 参数", "title": "聊天助手 API 参数",