From c597c6482a6e7ee376f9324e60e5f961c70f6f9c Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 19 Mar 2025 20:46:56 +0800 Subject: [PATCH 01/73] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E8=BD=AC=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 4 ++++ pkg/platform/types/message.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 869e05c6..0fb00fa9 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -47,6 +47,8 @@ class GewechatMessageConverter(adapter.MessageConverter): if not component.url: pass content_list.append({"type": "image", "image": component.url}) + elif isinstance(component, platform_message.MiniPrograms): + content_list.append({"type": 'MiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) elif isinstance(component, platform_message.Voice): @@ -361,6 +363,8 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): elif msg['type'] == 'image': self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) + elif msg['type'] == 'MiniPrograms': + self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index b99a28b3..ad519481 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -643,6 +643,14 @@ class Unknown(MessageComponent): text: str """文本。""" +class MiniPrograms(MessageComponent): + """小程序?""" + type: str = 'MiniPrograms' + """xml数据""" + xml_data: str + """首页图片""" + image_url: typing.Optional[str] = None + class Voice(MessageComponent): """语音。""" From 3697afd9d6507d054fbb0e3b5839ce1074f20f27 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 19 Mar 2025 21:55:36 +0800 Subject: [PATCH 02/73] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=8F=91=E9=80=81=EF=BC=8C=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E8=BD=AC=E5=8F=91=E6=9B=B4=E5=90=8D=E4=B8=BAForwardMiniProgram?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 10 +++++++--- pkg/platform/types/message.py | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 0fb00fa9..f78cdfba 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -48,8 +48,10 @@ class GewechatMessageConverter(adapter.MessageConverter): pass content_list.append({"type": "image", "image": component.url}) elif isinstance(component, platform_message.MiniPrograms): - content_list.append({"type": 'MiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) - + # content_list.append({"type": 'MiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) + content_list.append({"type": 'MiniPrograms', 'mini_app_id': component.mini_app_id, 'display_name': component.display_name, + 'page_path': component.page_path, 'cover_img_url': component.image_url, 'title': component.title, + 'user_name': component.user_name}) elif isinstance(component, platform_message.Voice): content_list.append({"type": "voice", "url": component.url, "length": component.length}) @@ -364,7 +366,9 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) elif msg['type'] == 'MiniPrograms': - self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) + self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id'] + , display_name=msg['display_name'], page_path=msg['page_path'] + , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index ad519481..314657d3 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -643,9 +643,29 @@ class Unknown(MessageComponent): text: str """文本。""" + + + class MiniPrograms(MessageComponent): - """小程序?""" + """小程序""" type: str = 'MiniPrograms' + """小程序id""" + mini_app_id: str + """小程序归属用户id""" + user_name: str + """小程序名称""" + display_name: typing.Optional[str] = '' + """打开地址""" + page_path: typing.Optional[str] = 'None' + """小程序标题""" + title: typing.Optional[str] = 'None' + """首页图片""" + image_url: typing.Optional[str] = 'None' + + +class ForwardMiniPrograms(MessageComponent): + """转发小程序""" + type: str = 'ForwardMiniPrograms' """xml数据""" xml_data: str """首页图片""" From c136e790ef7ea678e7af05e93e3d2a61c1d2a433 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 19 Mar 2025 21:56:13 +0800 Subject: [PATCH 03/73] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=8F=91=E9=80=81=EF=BC=8C=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E8=BD=AC=E5=8F=91=E6=9B=B4=E5=90=8D=E4=B8=BAForwardMiniProgram?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/types/message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index 314657d3..f42dfb9c 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -656,11 +656,11 @@ class MiniPrograms(MessageComponent): """小程序名称""" display_name: typing.Optional[str] = '' """打开地址""" - page_path: typing.Optional[str] = 'None' + page_path: typing.Optional[str] = '' """小程序标题""" - title: typing.Optional[str] = 'None' + title: typing.Optional[str] = '' """首页图片""" - image_url: typing.Optional[str] = 'None' + image_url: typing.Optional[str] = '' class ForwardMiniPrograms(MessageComponent): From e22c804debcce54ce8a853123677acf8e511ded9 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 19 Mar 2025 22:47:10 +0800 Subject: [PATCH 04/73] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81emoji?= =?UTF-8?q?=E8=A1=A8=E6=83=85=EF=BC=9F=EF=BC=88=E5=A5=BD=E5=83=8F=E6=B2=A1?= =?UTF-8?q?=E5=95=A5=E7=94=A8=EF=BC=89=E5=92=8C=E5=8F=91=E9=80=81=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 18 +++++++++++++++++- pkg/platform/types/message.py | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index f78cdfba..db7971e1 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -48,10 +48,17 @@ class GewechatMessageConverter(adapter.MessageConverter): pass content_list.append({"type": "image", "image": component.url}) elif isinstance(component, platform_message.MiniPrograms): - # content_list.append({"type": 'MiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) content_list.append({"type": 'MiniPrograms', 'mini_app_id': component.mini_app_id, 'display_name': component.display_name, 'page_path': component.page_path, 'cover_img_url': component.image_url, 'title': component.title, 'user_name': component.user_name}) + elif isinstance(component, platform_message.ForwardMiniPrograms): + content_list.append({"type": 'ForwardMiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) + elif isinstance(component, platform_message.EmoJi): + content_list.append({'type': 'emoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size}) + elif isinstance(component, platform_message.Link): + content_list.append({'type': 'Link', 'link_title': component.link_title, 'link_desc': component.link_desc, + 'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url}) + elif isinstance(component, platform_message.Voice): content_list.append({"type": "voice", "url": component.url, "length": component.length}) @@ -369,6 +376,15 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id'] , display_name=msg['display_name'], page_path=msg['page_path'] , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) + elif msg['type'] == 'ForwardMiniPrograms': + self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['inage_url']) + elif msg['type'] == 'emoji': + self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, + emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) + elif msg['type'] == 'Link': + self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id + ,title=msg['link_title'], desc=msg['link_desc'] + , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index f42dfb9c..f75bb945 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -672,6 +672,29 @@ class ForwardMiniPrograms(MessageComponent): image_url: typing.Optional[str] = None +class EmoJi(MessageComponent): + """emoji表情""" + type: str = 'EmoJi' + """emojimd5""" + emoji_md5: str + """emoji大小""" + emoji_size: int + + +class Link(MessageComponent): + """发送链接""" + type: str = 'Link' + """标题""" + link_title: str = '' + """链接描述""" + link_desc: str = '' + """链接地址""" + link_url: str = '' + """链接略缩图""" + link_thumb_url: str = '' + + + class Voice(MessageComponent): """语音。""" type: str = "Voice" From 432440d6bf5adf8d016ea920a6f457662c3a43de Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 27 Mar 2025 00:01:05 +0800 Subject: [PATCH 05/73] =?UTF-8?q?=E6=96=B0=E5=A2=9Ereply=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8F=8A=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index db7971e1..d11f64b3 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -397,6 +397,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): content_list = await self.message_converter.yiri2target(message) ats = [item["target"] for item in content_list if item["type"] == "at"] + target_id = message_source.source_platform_object["Data"]["FromUserName"]["string"] for msg in content_list: if msg["type"] == "text": @@ -417,6 +418,22 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): content=msg["content"], ats=",".join(ats) ) + elif msg['type'] == 'image': + + self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) + elif msg['type'] == 'MiniPrograms': + self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id'] + , display_name=msg['display_name'], page_path=msg['page_path'] + , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) + elif msg['type'] == 'ForwardMiniPrograms': + self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['inage_url']) + elif msg['type'] == 'emoji': + self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, + emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) + elif msg['type'] == 'Link': + self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id + , title=msg['link_title'], desc=msg['link_desc'] + , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) async def is_muted(self, group_id: int) -> bool: pass From 394d4b3c1b538b152678aad47387c9d400911b5d Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Fri, 28 Mar 2025 23:46:24 +0800 Subject: [PATCH 06/73] fix: static_file sent with wrong mimetype (#1243) --- pkg/api/http/controller/main.py | 38 +++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/pkg/api/http/controller/main.py b/pkg/api/http/controller/main.py index acbfa104..70a1a546 100644 --- a/pkg/api/http/controller/main.py +++ b/pkg/api/http/controller/main.py @@ -66,8 +66,42 @@ class HTTPController: @self.quart_app.route("/") async def index(): - return await quart.send_from_directory(frontend_path, "index.html") + return await quart.send_from_directory( + frontend_path, + "index.html", + mimetype="text/html" + ) @self.quart_app.route("/") async def static_file(path: str): - return await quart.send_from_directory(frontend_path, path) + + mimetype = None + + if path.endswith(".html"): + mimetype = "text/html" + elif path.endswith(".js"): + mimetype = "application/javascript" + elif path.endswith(".css"): + mimetype = "text/css" + elif path.endswith(".png"): + mimetype = "image/png" + elif path.endswith(".jpg"): + mimetype = "image/jpeg" + elif path.endswith(".jpeg"): + mimetype = "image/jpeg" + elif path.endswith(".gif"): + mimetype = "image/gif" + elif path.endswith(".svg"): + mimetype = "image/svg+xml" + elif path.endswith(".ico"): + mimetype = "image/x-icon" + elif path.endswith(".json"): + mimetype = "application/json" + elif path.endswith(".txt"): + mimetype = "text/plain" + + return await quart.send_from_directory( + frontend_path, + path, + mimetype=mimetype + ) \ No newline at end of file From 629ebae0e972dfcb57cebd2b6b1912854e9939e3 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Fri, 28 Mar 2025 23:48:09 +0800 Subject: [PATCH 07/73] chore: release v3.4.11.1 (#1244) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 14b2b74c..1e84a22e 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.11" +semantic_version = "v3.4.11.1" debug_mode = False From cd4a06b692cdfc8d04e7abfb2f845a0757970c8b Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 29 Mar 2025 01:18:30 +0800 Subject: [PATCH 08/73] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9B=A0=E4=B8=BA?= =?UTF-8?q?=E6=89=8B=E8=AF=AF=E7=9A=84=E5=8F=82=E6=95=B0=E5=90=8D=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E4=BB=A5=E5=8F=8A=E7=B1=BB=E5=90=8D=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 6 +++--- pkg/platform/types/message.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index d11f64b3..acbf92e6 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -53,7 +53,7 @@ class GewechatMessageConverter(adapter.MessageConverter): 'user_name': component.user_name}) elif isinstance(component, platform_message.ForwardMiniPrograms): content_list.append({"type": 'ForwardMiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) - elif isinstance(component, platform_message.EmoJi): + elif isinstance(component, platform_message.Emoji): content_list.append({'type': 'emoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size}) elif isinstance(component, platform_message.Link): content_list.append({'type': 'Link', 'link_title': component.link_title, 'link_desc': component.link_desc, @@ -377,7 +377,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): , display_name=msg['display_name'], page_path=msg['page_path'] , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) elif msg['type'] == 'ForwardMiniPrograms': - self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['inage_url']) + self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) elif msg['type'] == 'emoji': self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) @@ -426,7 +426,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): , display_name=msg['display_name'], page_path=msg['page_path'] , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) elif msg['type'] == 'ForwardMiniPrograms': - self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['inage_url']) + self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) elif msg['type'] == 'emoji': self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index f75bb945..8b509b17 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -672,7 +672,7 @@ class ForwardMiniPrograms(MessageComponent): image_url: typing.Optional[str] = None -class EmoJi(MessageComponent): +class Emoji(MessageComponent): """emoji表情""" type: str = 'EmoJi' """emojimd5""" @@ -858,6 +858,7 @@ class File(MessageComponent): """文件名称。""" size: int """文件大小。""" + def __str__(self): return f'[文件]{self.name}' From 23a0dba4709324ed65a08b1241cd65333c63e4b2 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sun, 30 Mar 2025 23:04:46 +0800 Subject: [PATCH 09/73] feat(dify): throw error event (#1251) --- pkg/provider/runners/difysvapi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 81ceddee..a33a3c0b 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -225,6 +225,8 @@ class DifyServiceAPIRunner(runner.RequestRunner): role="assistant", content=[llm_entities.ContentElement.from_image_url(image_url)], ) + if chunk['event'] == 'error': + raise errors.DifyAPIError("dify 服务错误: " + chunk['message']) query.session.using_conversation.uuid = chunk["conversation_id"] From f5e98d4ebb263e2abbf8934f62665f1681280a5a Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sun, 30 Mar 2025 23:14:56 +0800 Subject: [PATCH 10/73] fix(gewe): should not block main launching process (#1163) (#1252) --- pkg/platform/manager.py | 4 ++-- pkg/platform/sources/gewechat.py | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/pkg/platform/manager.py b/pkg/platform/manager.py index acdf0990..96c50602 100644 --- a/pkg/platform/manager.py +++ b/pkg/platform/manager.py @@ -110,7 +110,7 @@ class PlatformManager: if len(self.adapters) == 0: self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。') - async def write_back_config(self, adapter_name: str, adapter_inst: msadapter.MessagePlatformAdapter, config: dict): + def write_back_config(self, adapter_name: str, adapter_inst: msadapter.MessagePlatformAdapter, config: dict): index = -2 for i, adapter in enumerate(self.adapters): @@ -137,7 +137,7 @@ class PlatformManager: **config } self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg - await self.ap.platform_cfg.dump_config() + self.ap.platform_cfg.dump_config_sync() async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessagePlatformAdapter): diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 869e05c6..ad96971e 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -428,26 +428,28 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self.config["token"] ) - app_id, error_msg = self.bot.login(self.config["app_id"]) - if error_msg: - raise Exception(f"Gewechat 登录失败: {error_msg}") + def gewechat_login_process(): - self.config["app_id"] = app_id + app_id, error_msg = self.bot.login(self.config["app_id"]) + if error_msg: + raise Exception(f"Gewechat 登录失败: {error_msg}") - self.ap.logger.info(f"Gewechat 登录成功,app_id: {app_id}") + self.config["app_id"] = app_id - await self.ap.platform_mgr.write_back_config('gewechat', self, self.config) + self.ap.logger.info(f"Gewechat 登录成功,app_id: {app_id}") - # 获取 nickname - profile = self.bot.get_profile(self.config["app_id"]) - self.bot_account_id = profile["data"]["nickName"] + self.ap.platform_mgr.write_back_config('gewechat', self, self.config) + + # 获取 nickname + profile = self.bot.get_profile(self.config["app_id"]) + self.bot_account_id = profile["data"]["nickName"] + + time.sleep(2) - def thread_set_callback(): - time.sleep(3) ret = self.bot.set_callback(self.config["token"], self.config["callback_url"]) print('设置 Gewechat 回调:', ret) - threading.Thread(target=thread_set_callback).start() + threading.Thread(target=gewechat_login_process).start() async def shutdown_trigger_placeholder(): while True: From 8b56f946671666c8ded025edade82455cb4bbbdd Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sun, 30 Mar 2025 23:23:31 +0800 Subject: [PATCH 11/73] perf: add debugging msg for webhook style adapters (#1253) --- pkg/platform/sources/gewechat.py | 3 +++ pkg/platform/sources/lark.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index ad96971e..2b070757 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -310,6 +310,9 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): async def gewechat_callback(): data = await quart.request.json # print(json.dumps(data, indent=4, ensure_ascii=False)) + self.ap.logger.debug( + f"Gewechat callback event: {data}" + ) if 'data' in data: data['Data'] = data['data'] diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 4c87640b..a04ceb4e 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -328,6 +328,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter): try: data = await quart.request.json + self.ap.logger.debug( + f"Lark callback event: {data}" + ) + if 'encrypt' in data: cipher = AESCipher(self.config['encrypt-key']) data = cipher.decrypt_string(data['encrypt']) From 73414351279dfc639e0ffe6c19af04cece8e32f9 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sun, 30 Mar 2025 23:43:45 +0800 Subject: [PATCH 12/73] perf(chatcmpl): use `extra_body` to pass args (#1254) --- pkg/provider/modelmgr/requesters/chatcmpl.py | 7 ++++--- pkg/provider/modelmgr/requesters/deepseekchatcmpl.py | 4 ++-- pkg/provider/modelmgr/requesters/giteeaichatcmpl.py | 4 ++-- pkg/provider/modelmgr/requesters/moonshotchatcmpl.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 7bf83377..c675618e 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -47,8 +47,9 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def _req( self, args: dict, + extra_body: dict = {}, ) -> chat_completion.ChatCompletion: - return await self.client.chat.completions.create(**args) + return await self.client.chat.completions.create(**args, extra_body=extra_body) async def _make_msg( self, @@ -73,7 +74,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = self.requester_cfg['args'].copy() + args = {} args["model"] = use_model.name if use_model.model_name is None else use_model.model_name if use_funcs: @@ -99,7 +100,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): args["messages"] = messages # 发送请求 - resp = await self._req(args) + resp = await self._req(args, extra_body=self.requester_cfg['args']) # 处理请求结果 message = await self._make_msg(resp) diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index f6453a19..eb466b65 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -23,7 +23,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = self.requester_cfg['args'].copy() + args = {} args["model"] = use_model.name if use_model.model_name is None else use_model.model_name if use_funcs: @@ -43,7 +43,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): args["messages"] = messages # 发送请求 - resp = await self._req(args) + resp = await self._req(args, extra_body=self.requester_cfg['args']) if resp is None: raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常') diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 4beb6ba8..fd9f66c8 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -30,7 +30,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = self.requester_cfg['args'].copy() + args = {} args["model"] = use_model.name if use_model.model_name is None else use_model.model_name if use_funcs: @@ -46,7 +46,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): args["messages"] = req_messages - resp = await self._req(args) + resp = await self._req(args, extra_body=self.requester_cfg['args']) message = await self._make_msg(resp) diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index 2e94fd04..d863049b 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -25,7 +25,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() - args = self.requester_cfg['args'].copy() + args = {} args["model"] = use_model.name if use_model.model_name is None else use_model.model_name if use_funcs: @@ -48,7 +48,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): args["messages"] = messages # 发送请求 - resp = await self._req(args) + resp = await self._req(args, extra_body=self.requester_cfg['args']) # 处理请求结果 message = await self._make_msg(resp) From e04d46db2ce50f999892ded7f0e49f9959a00686 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sun, 30 Mar 2025 23:51:53 +0800 Subject: [PATCH 13/73] perf(claude): ensure system message removed (#867) (#1255) --- pkg/provider/modelmgr/requesters/anthropicmsgs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index b03e536d..a6164257 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -59,9 +59,11 @@ class AnthropicMessages(requester.LLMAPIRequester): if m.role == "system": system_role_message = m - messages.pop(i) break + if system_role_message: + messages.pop(i) + if isinstance(system_role_message, llm_entities.Message) \ and isinstance(system_role_message.content, str): args['system'] = system_role_message.content From e20b79b0edc58c9a3a2c98fe0209aa05750614f6 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sun, 30 Mar 2025 23:59:55 +0800 Subject: [PATCH 14/73] perf(chatcmpl): remove `space` from `base-url` (#1256) --- pkg/provider/modelmgr/requesters/anthropicmsgs.py | 2 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index a6164257..6bbe4bf1 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -25,7 +25,7 @@ class AnthropicMessages(requester.LLMAPIRequester): async def initialize(self): httpx_client = anthropic._base_client.AsyncHttpxClientWrapper( - base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'], + base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'].replace(' ', ''), # cast to a valid type because mypy doesn't understand our type narrowing timeout=typing.cast(httpx.Timeout, self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout']), limits=anthropic._constants.DEFAULT_CONNECTION_LIMITS, diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index c675618e..e65d908b 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -36,7 +36,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): self.client = openai.AsyncClient( api_key="", - base_url=self.requester_cfg['base-url'], + base_url=self.requester_cfg['base-url'].replace(' ', ''), timeout=self.requester_cfg['timeout'], http_client=httpx.AsyncClient( trust_env=True, From ffe9c3e0f876eefc5fef82cae5f3f255e5c3d4b4 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Mon, 31 Mar 2025 00:02:54 +0800 Subject: [PATCH 15/73] chore: release v3.4.11.2 (#1257) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 1e84a22e..5830d883 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.11.1" +semantic_version = "v3.4.11.2" debug_mode = False From c0dbf6fd134d92b7f424f46fe7331ebe0ca9129b Mon Sep 17 00:00:00 2001 From: wangcham Date: Sun, 30 Mar 2025 12:53:48 -0400 Subject: [PATCH 16/73] feat:add support for slack --- .gitignore | 1 + libs/slack_api/__init__.py | 0 libs/slack_api/api.py | 104 +++++++++++++++++ libs/slack_api/slackevent.py | 66 +++++++++++ pkg/platform/sources/slack.py | 200 ++++++++++++++++++++++++++++++++ pkg/platform/sources/slack.yaml | 44 +++++++ pkg/utils/image.py | 6 +- requirements.txt | 2 +- templates/platform.json | 8 ++ 9 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 libs/slack_api/__init__.py create mode 100644 libs/slack_api/api.py create mode 100644 libs/slack_api/slackevent.py create mode 100644 pkg/platform/sources/slack.py create mode 100644 pkg/platform/sources/slack.yaml diff --git a/.gitignore b/.gitignore index 17271201..1c2147a8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ botpy.log* /libs/wecom_api/test.py /venv /jp-tyo-churros-05.rockchin.top +test.py \ No newline at end of file diff --git a/libs/slack_api/__init__.py b/libs/slack_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py new file mode 100644 index 00000000..b9e39b9f --- /dev/null +++ b/libs/slack_api/api.py @@ -0,0 +1,104 @@ +import json +from quart import Quart, jsonify,request +from slack_sdk.web.async_client import AsyncWebClient +from .slackevent import SlackEvent +from typing import Callable, Dict, Any +from pkg.platform.types import events as platform_events, message as platform_message + +class SlackClient(): + + def __init__(self,bot_token:str,signing_secret:str): + + self.bot_token = bot_token + self.signing_secret = signing_secret + self.app = Quart(__name__) + self.client = AsyncWebClient(self.bot_token) + self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']) + self._message_handlers = { + "example":[], + } + self.bot_user_id = None # avoid block + + async def handle_callback_request(self): + try: + body = await request.get_data() + data = json.loads(body) + print("shoudao:") + print(data) + bot_user_id = data.get("event",{}).get("bot_id","") + + if self.bot_user_id and bot_user_id == self.bot_user_id: + return jsonify({'status': 'ok'}) + + if data and data.get("event", {}).get("channel_type") in ["im", "channel"]: + event = SlackEvent.from_payload(data) + await self._handle_message(event) + return jsonify({'status': 'ok'}) + + except Exception as e: + raise(e) + + + + async def _handle_message(self, event: SlackEvent): + """ + 处理消息事件。 + """ + msg_type = event.type + if msg_type in self._message_handlers: + for handler in self._message_handlers[msg_type]: + await handler(event) + + def on_message(self, msg_type: str): + """注册消息类型处理器""" + def decorator(func: Callable[[platform_events.Event], None]): + if msg_type not in self._message_handlers: + self._message_handlers[msg_type] = [] + self._message_handlers[msg_type].append(func) + return func + return decorator + + async def send_message_to_channle(self,text:str,channel_id:str): + try: + response = await self.client.chat_postMessage( + channel=channel_id, + text=text + ) + if self.bot_user_id is None and response.get("ok"): + self.bot_user_id = response["message"]["bot_id"] + print("bot_id:") + print(self.bot_user_id) + print("fanhui:") + print(response) + return + except Exception as e: + raise e + + async def send_message_to_one(self,text:str,user_id:str): + try: + response = await self.client.chat_postMessage( + channel = '@'+user_id, + text= text + ) + if self.bot_user_id is None and response.get("ok"): + self.bot_user_id = response["message"]["bot_id"] + print("bot_id:") + print(self.bot_user_id) + + return + except Exception as e: + raise e + + async def run_task(self, host: str, port: int, *args, **kwargs): + """ + 启动 Quart 应用。 + """ + await self.app.run_task(host=host, port=port, *args, **kwargs) + + + + + + + + diff --git a/libs/slack_api/slackevent.py b/libs/slack_api/slackevent.py new file mode 100644 index 00000000..9eb7137d --- /dev/null +++ b/libs/slack_api/slackevent.py @@ -0,0 +1,66 @@ +from typing import Dict, Any, Optional + +class SlackEvent(dict): + @staticmethod + def from_payload(payload: Dict[str, Any]) -> Optional["SlackEvent"]: + try: + event = SlackEvent(payload) + return event + except KeyError: + return None + + @property + def text(self) -> str: + if self.get("event", {}).get("channel_type") == "im": + elements = self["event"]["blocks"][0]["elements"][0]["elements"] + for el in elements: + if el.get("type") == "text": + return el.get("text", "") + + if self.get("event",{}).get("channel_type") == "channel": + elements = self["event"]["blocks"][0]["elements"][0]["elements"] + text_result = next((el["text"] for el in elements if el["type"] == "text"), "") + return text_result + + + return "" + + + @property + def user_id(self) -> Optional[str]: + return self.get("event", {}).get("user","") + + @property + def channel_id(self) -> Optional[str]: + return self.get("event", {}).get("channel","") + + @property + def type(self) -> str: + """ message对应私聊,app_mention对应频道at """ + return self.get("event", {}).get("channel_type", "") + + @property + def message_id(self) -> str: + return self.get("event_id","") + + @property + def pic_url(self) -> str: + """提取 Slack 事件中的图片 URL""" + files = self.get("event", {}).get("files", []) + if files: + return files[0].get("url_private", "") + return "" + + + @property + def sender_name(self) -> str: + return self.get("event", {}).get("user","") + + def __getattr__(self, key: str) -> Optional[Any]: + return self.get(key) + + def __setattr__(self, key: str, value: Any) -> None: + self[key] = value + + def __repr__(self) -> str: + return f"" diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py new file mode 100644 index 00000000..e4eafe17 --- /dev/null +++ b/pkg/platform/sources/slack.py @@ -0,0 +1,200 @@ +from __future__ import annotations +import typing +import asyncio +import traceback + +import datetime + +from libs.slack_api.api import SlackClient +from pkg.platform.adapter import MessagePlatformAdapter +from pkg.platform.types import events as platform_events, message as platform_message +from libs.slack_api.slackevent import SlackEvent +from pkg.core import app +from .. import adapter +from ...core import app +from ..types import message as platform_message +from ..types import events as platform_events +from ..types import entities as platform_entities +from ...command.errors import ParamNotEnoughError +from ...utils import image + +class SlackMessageConverter(adapter.MessageConverter): + + @staticmethod + async def yiri2target(message_chain:platform_message.MessageChain): + content_list = [] + for msg in message_chain: + if type(msg) is platform_message.Plain: + content_list.append({ + "content":msg.text, + }) + + return content_list + + @staticmethod + async def target2yiri(message:str,message_id:str,pic_url:str): + yiri_msg_list = [] + yiri_msg_list.append( + platform_message.Source(id=message_id,time=datetime.datetime.now()) + ) + if pic_url is not None: + base64_url = await image.get_slack_image_to_base64(pic_url=pic_url) + yiri_msg_list.append( + platform_message.Image(base64=base64_url) + ) + + yiri_msg_list.append(platform_message.Plain(text=message)) + chain = platform_message.MessageChain(yiri_msg_list) + return chain + + +class SlackEventConverter(adapter.EventConverter): + + @staticmethod + async def yiri2target(event:platform_events.MessageEvent) -> SlackEvent: + return event.source_platform_object + + @staticmethod + async def target2yiri(event:SlackEvent): + yiri_chain = await SlackMessageConverter.target2yiri( + message=event.text,message_id=event.message_id,pic_url=event.pic_url + ) + + if event.type == 'channel': + yiri_chain.insert(0, platform_message.At(target="SlackBot")) + + sender = platform_entities.GroupMember( + id = event.user_id, + member_name= str(event.sender_name), + permission= 'MEMBER', + group = platform_entities.Group( + id = event.channel_id, + name = 'MEMBER', + permission= platform_entities.Permission.Member + ), + special_title='', + join_timestamp=0, + last_speak_timestamp=0, + mute_time_remaining=0 + ) + time = int(datetime.datetime.utcnow().timestamp()) + return platform_events.GroupMessage( + sender = sender, + message_chain=yiri_chain, + time = time, + source_platform_object=event + ) + + if event.type == 'im': + return platform_events.FriendMessage( + sender=platform_entities.Friend( + id=event.user_id, + nickname = event.sender_name, + remark="" + ), + message_chain = yiri_chain, + time = float(datetime.datetime.now().timestamp()), + source_platform_object=event, + ) + + + + +class SlackAdapter(adapter.MessagePlatformAdapter): + bot: SlackClient + ap: app.Application + bot_account_id: str + message_converter: SlackMessageConverter = SlackMessageConverter() + event_converter: SlackEventConverter = SlackEventConverter() + config: dict + + def __init__(self,config:dict,ap:app.Application): + self.config = config + self.ap = app.Application + required_keys = [ + "bot_token", + "signing_secret", + ] + missing_keys = [key for key in required_keys if key not in config] + if missing_keys: + raise ParamNotEnoughError("Slack机器人缺少相关配置项,请查看文档或联系管理员") + + self.bot = SlackClient( + bot_token=self.config["bot_token"], + signing_secret=self.config["signing_secret"] + ) + + async def reply_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + slack_event = await SlackEventConverter.yiri2target( + message_source + ) + + content_list = await SlackMessageConverter.yiri2target(message) + + for content in content_list: + if slack_event.type == 'channel': + print("fasong1") + await self.bot.send_message_to_channle( + content['content'],slack_event.channel_id + ) + if slack_event.type == 'im': + await self.bot.send_message_to_one( + content['content'],slack_event.user_id + ) + + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): + pass + + + def register_listener( + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[ + [platform_events.Event, adapter.MessagePlatformAdapter], None + ], + ): + async def on_message(event:SlackEvent): + self.bot_account_id = "SlackBot" + 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("im")(on_message) + elif event_type == platform_events.GroupMessage: + self.bot.on_message("channel")(on_message) + + + async def run_async(self): + async def shutdown_trigger_placeholder(): + while True: + await asyncio.sleep(1) + + await self.bot.run_task( + host=self.config["host"], + port=self.config["port"], + shutdown_trigger=shutdown_trigger_placeholder, + ) + + async def kill(self) -> bool: + return False + + async def unregister_listener( + self, + event_type: type, + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + ): + return super().unregister_listener(event_type, callback) + + + + + diff --git a/pkg/platform/sources/slack.yaml b/pkg/platform/sources/slack.yaml new file mode 100644 index 00000000..ffc924e3 --- /dev/null +++ b/pkg/platform/sources/slack.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: slack + label: + en_US: Slack API + zh_CN: Slack API + description: + en_US: Slack API + zh_CN: Slack API +spec: + config: + - name: bot_token + label: + en_US: Bot Token + zh_CN: 机器人令牌 + type: string + required: true + default: "" + - name: signing_secret + label: + en_US: signing_secret + zh_CN: 密钥 + type: string + required: true + default: "" + - name: port + label: + en_US: Port + zh_CN: 监听端口 + type: int + required: true + default: 2288 + - name: host + label: + en_US: Host + zh_CN: 监听主机 + type: string + required: true + default: 0.0.0.0 +execution: + python: + path: ./slack.py + attr: SlackAdapter \ No newline at end of file diff --git a/pkg/utils/image.py b/pkg/utils/image.py index 760c2128..2aedbfd7 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -212,4 +212,8 @@ async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, st """ base64_str = image_base64_data.split(',')[-1] image_format = image_base64_data.split(':')[-1].split(';')[0].split('/')[-1] - return base64_str, image_format \ No newline at end of file + return base64_str, image_format + + +async def get_slack_image_to_base64(pic_url:str): + pass \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 243d2da7..cd82211c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,6 +34,6 @@ dashscope python-telegram-bot certifi mcp - +slack_sdk # indirect taskgroup==0.0.0a4 \ No newline at end of file diff --git a/templates/platform.json b/templates/platform.json index fe39947c..ddb9f045 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -94,6 +94,14 @@ "adapter":"telegram", "enable": false, "token":"" + }, + { + "adapter":"slack", + "enable":true, + "bot_token":"", + "signing_secret":"", + "host":"0.0.0.0", + "port":2288 } ], "track-function-calls": true, From be1328cee9c64d06fe6ed0a37b8d60865aeb2abd Mon Sep 17 00:00:00 2001 From: wangcham Date: Sun, 30 Mar 2025 22:24:53 -0400 Subject: [PATCH 17/73] feat: add support for slack --- libs/slack_api/api.py | 24 ++++++++++++++---------- libs/slack_api/slackevent.py | 10 ++++------ pkg/platform/sources/slack.py | 11 +++++------ pkg/utils/image.py | 19 +++++++++++++++++-- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py index b9e39b9f..5988e3e7 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -17,23 +17,33 @@ class SlackClient(): self._message_handlers = { "example":[], } - self.bot_user_id = None # avoid block + self.bot_user_id = None # 避免机器人回复自己的消息 async def handle_callback_request(self): try: body = await request.get_data() data = json.loads(body) - print("shoudao:") - print(data) + if 'type' in data: + if data['type'] == 'url_verification': + return data['challenge'] + bot_user_id = data.get("event",{}).get("bot_id","") if self.bot_user_id and bot_user_id == self.bot_user_id: return jsonify({'status': 'ok'}) - if data and data.get("event", {}).get("channel_type") in ["im", "channel"]: + # 处理私信 + if data and data.get("event", {}).get("channel_type") in ["im"]: event = SlackEvent.from_payload(data) await self._handle_message(event) return jsonify({'status': 'ok'}) + + #处理群聊 + if data.get("event",{}).get("type") == 'app_mention': + data.setdefault("event", {})["channel_type"] = "channel" + event = SlackEvent.from_payload(data) + await self._handle_message(event) + return jsonify({'status':'ok'}) except Exception as e: raise(e) @@ -66,10 +76,6 @@ class SlackClient(): ) if self.bot_user_id is None and response.get("ok"): self.bot_user_id = response["message"]["bot_id"] - print("bot_id:") - print(self.bot_user_id) - print("fanhui:") - print(response) return except Exception as e: raise e @@ -82,8 +88,6 @@ class SlackClient(): ) if self.bot_user_id is None and response.get("ok"): self.bot_user_id = response["message"]["bot_id"] - print("bot_id:") - print(self.bot_user_id) return except Exception as e: diff --git a/libs/slack_api/slackevent.py b/libs/slack_api/slackevent.py index 9eb7137d..63c949e0 100644 --- a/libs/slack_api/slackevent.py +++ b/libs/slack_api/slackevent.py @@ -16,11 +16,9 @@ class SlackEvent(dict): for el in elements: if el.get("type") == "text": return el.get("text", "") - - if self.get("event",{}).get("channel_type") == "channel": - elements = self["event"]["blocks"][0]["elements"][0]["elements"] - text_result = next((el["text"] for el in elements if el["type"] == "text"), "") - return text_result + if self.get("event",{}).get("channel_type") == 'channel': + message_text = next((el["text"] for block in self.get("event", {}).get("blocks", []) if block.get("type") == "rich_text" for element in block.get("elements", []) if element.get("type") == "rich_text_section" for el in element.get("elements", []) if el.get("type") == "text"), "") + return message_text return "" @@ -49,7 +47,7 @@ class SlackEvent(dict): files = self.get("event", {}).get("files", []) if files: return files[0].get("url_private", "") - return "" + return None @property diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index e4eafe17..8101e5c5 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -32,13 +32,13 @@ class SlackMessageConverter(adapter.MessageConverter): return content_list @staticmethod - async def target2yiri(message:str,message_id:str,pic_url:str): + async def target2yiri(message:str,message_id:str,pic_url:str,bot:SlackClient): yiri_msg_list = [] yiri_msg_list.append( platform_message.Source(id=message_id,time=datetime.datetime.now()) ) if pic_url is not None: - base64_url = await image.get_slack_image_to_base64(pic_url=pic_url) + base64_url = await image.get_slack_image_to_base64(pic_url=pic_url,bot_token=bot.bot_token) yiri_msg_list.append( platform_message.Image(base64=base64_url) ) @@ -55,9 +55,9 @@ class SlackEventConverter(adapter.EventConverter): return event.source_platform_object @staticmethod - async def target2yiri(event:SlackEvent): + async def target2yiri(event:SlackEvent,bot:SlackClient): yiri_chain = await SlackMessageConverter.target2yiri( - message=event.text,message_id=event.message_id,pic_url=event.pic_url + message=event.text,message_id=event.message_id,pic_url=event.pic_url,bot=bot ) if event.type == 'channel': @@ -138,7 +138,6 @@ class SlackAdapter(adapter.MessagePlatformAdapter): for content in content_list: if slack_event.type == 'channel': - print("fasong1") await self.bot.send_message_to_channle( content['content'],slack_event.channel_id ) @@ -162,7 +161,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = "SlackBot" try: return await callback( - await self.event_converter.target2yiri(event),self + await self.event_converter.target2yiri(event,self.bot),self ) except: traceback.print_exc() diff --git a/pkg/utils/image.py b/pkg/utils/image.py index 2aedbfd7..6e499340 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -215,5 +215,20 @@ async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, st return base64_str, image_format -async def get_slack_image_to_base64(pic_url:str): - pass \ No newline at end of file +async def get_slack_image_to_base64(pic_url:str,bot_token:str): + """ + 将Slack图片转换为base64 + """ + +async def get_slack_image_to_base64(pic_url:str, bot_token:str): + headers = {"Authorization": f"Bearer {bot_token}"} + try: + async with aiohttp.ClientSession() as session: + async with session.get(pic_url, headers=headers) as resp: + image_data = await resp.read() + return base64.b64encode(image_data).decode('utf-8') + except Exception as e: + raise(e) + + + From 70f8ddb1baa17fa43b8d0bc85f2a23144a32123c Mon Sep 17 00:00:00 2001 From: wangcham Date: Sun, 30 Mar 2025 22:56:51 -0400 Subject: [PATCH 18/73] fix: delete useless image function in slack --- pkg/utils/image.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/utils/image.py b/pkg/utils/image.py index 6e499340..8f395c35 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -215,10 +215,6 @@ async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, st return base64_str, image_format -async def get_slack_image_to_base64(pic_url:str,bot_token:str): - """ - 将Slack图片转换为base64 - """ async def get_slack_image_to_base64(pic_url:str, bot_token:str): headers = {"Authorization": f"Bearer {bot_token}"} From 5744eca37aea004157b49898643aeb2f15ef4510 Mon Sep 17 00:00:00 2001 From: wangcham Date: Sun, 30 Mar 2025 23:06:03 -0400 Subject: [PATCH 19/73] fix: bot user id in slack --- pkg/platform/sources/slack.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index 8101e5c5..2e71defc 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -158,7 +158,10 @@ class SlackAdapter(adapter.MessagePlatformAdapter): ], ): async def on_message(event:SlackEvent): - self.bot_account_id = "SlackBot" + if self.bot.bot_user_id: + self.bot_account_id = self.bot.bot_user_id + else: + self.bot_account_id = 'SlackBot' try: return await callback( await self.event_converter.target2yiri(event,self.bot),self From 686be4acbcb9e1f45500840438aa47a35c612855 Mon Sep 17 00:00:00 2001 From: wangcham Date: Mon, 31 Mar 2025 01:10:45 -0400 Subject: [PATCH 20/73] fix: eliminate host config --- libs/slack_api/slackevent.py | 5 +++-- pkg/platform/sources/slack.py | 2 +- templates/platform.json | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/slack_api/slackevent.py b/libs/slack_api/slackevent.py index 63c949e0..bf335ac8 100644 --- a/libs/slack_api/slackevent.py +++ b/libs/slack_api/slackevent.py @@ -16,11 +16,12 @@ class SlackEvent(dict): for el in elements: if el.get("type") == "text": return el.get("text", "") - if self.get("event",{}).get("channel_type") == 'channel': + + if self.get("event",{}).get("channel_type") == 'channel': + message_text = next((el["text"] for block in self.get("event", {}).get("blocks", []) if block.get("type") == "rich_text" for element in block.get("elements", []) if element.get("type") == "rich_text_section" for el in element.get("elements", []) if el.get("type") == "text"), "") return message_text - return "" diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index 2e71defc..f98c2a85 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -181,7 +181,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter): await asyncio.sleep(1) await self.bot.run_task( - host=self.config["host"], + host="0.0.0.0", port=self.config["port"], shutdown_trigger=shutdown_trigger_placeholder, ) diff --git a/templates/platform.json b/templates/platform.json index ddb9f045..a2fbd36e 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -100,7 +100,6 @@ "enable":true, "bot_token":"", "signing_secret":"", - "host":"0.0.0.0", "port":2288 } ], From 8799f86ea4a714f2ea9d6235bb69ea8e95f22b4f Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Mon, 31 Mar 2025 13:48:37 +0800 Subject: [PATCH 21/73] Update pkg/platform/sources/slack.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/platform/sources/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index f98c2a85..b899bbe2 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -110,7 +110,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter): def __init__(self,config:dict,ap:app.Application): self.config = config - self.ap = app.Application + self.ap = ap required_keys = [ "bot_token", "signing_secret", From 5378c6ba35647923186156f9abebd2dd2b03f8fa Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Mon, 31 Mar 2025 14:00:08 +0800 Subject: [PATCH 22/73] chore: provides `TZ=Asia/Shanghai` in docker-compose.yaml as default (#1259) --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 78548de5..6f75e85d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,6 +8,8 @@ services: - ./data:/app/data - ./plugins:/app/plugins restart: on-failure + environment: + - TZ=Asia/Shanghai ports: - 5300:5300 # 供 WebUI 使用 - 2280-2290:2280-2290 # 供消息平台适配器方向连接 From 715da548c8b1bbd20f56fb1821b133a739363a60 Mon Sep 17 00:00:00 2001 From: wangcham Date: Tue, 1 Apr 2025 02:34:16 -0400 Subject: [PATCH 23/73] fix: put the link and content together --- libs/slack_api/api.py | 5 ++++- libs/slack_api/slackevent.py | 40 +++++++++++++++++++++++++++++------ pkg/platform/sources/slack.py | 7 ++---- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py index 5988e3e7..86239ce9 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -32,6 +32,7 @@ class SlackClient(): if self.bot_user_id and bot_user_id == self.bot_user_id: return jsonify({'status': 'ok'}) + # 处理私信 if data and data.get("event", {}).get("channel_type") in ["im"]: event = SlackEvent.from_payload(data) @@ -44,6 +45,8 @@ class SlackClient(): event = SlackEvent.from_payload(data) await self._handle_message(event) return jsonify({'status':'ok'}) + + return jsonify({'status': 'ok'}) except Exception as e: raise(e) @@ -68,7 +71,7 @@ class SlackClient(): return func return decorator - async def send_message_to_channle(self,text:str,channel_id:str): + async def send_message_to_channel(self,text:str,channel_id:str): try: response = await self.client.chat_postMessage( channel=channel_id, diff --git a/libs/slack_api/slackevent.py b/libs/slack_api/slackevent.py index bf335ac8..5a6e9f90 100644 --- a/libs/slack_api/slackevent.py +++ b/libs/slack_api/slackevent.py @@ -11,18 +11,44 @@ class SlackEvent(dict): @property def text(self) -> str: + if self.get("event", {}).get("channel_type") == "im": - elements = self["event"]["blocks"][0]["elements"][0]["elements"] - for el in elements: - if el.get("type") == "text": - return el.get("text", "") + blocks = self.get("event", {}).get("blocks", []) + if not blocks: + return "" + + elements = blocks[0].get("elements", []) + if not elements: + return "" + + elements = elements[0].get("elements", []) + text = "" + + for el in elements: + if el.get("type") == "text": + text += el.get("text", "") + elif el.get("type") == "link": + text += el.get("url", "") + + return text + if self.get("event",{}).get("channel_type") == 'channel': - - message_text = next((el["text"] for block in self.get("event", {}).get("blocks", []) if block.get("type") == "rich_text" for element in block.get("elements", []) if element.get("type") == "rich_text_section" for el in element.get("elements", []) if el.get("type") == "text"), "") + message_text = "" + for block in self.get("event", {}).get("blocks", []): + if block.get("type") == "rich_text": + for element in block.get("elements", []): + if element.get("type") == "rich_text_section": + parts = [] + for el in element.get("elements", []): + if el.get("type") == "text": + parts.append(el["text"]) + elif el.get("type") == "link": + parts.append(el["url"]) + message_text = "".join(parts) + return message_text - return "" @property diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index b899bbe2..9d910a70 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -138,7 +138,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter): for content in content_list: if slack_event.type == 'channel': - await self.bot.send_message_to_channle( + await self.bot.send_message_to_channel( content['content'],slack_event.channel_id ) if slack_event.type == 'im': @@ -158,10 +158,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter): ], ): async def on_message(event:SlackEvent): - if self.bot.bot_user_id: - self.bot_account_id = self.bot.bot_user_id - else: - self.bot_account_id = 'SlackBot' + self.bot_account_id = 'SlackBot' try: return await callback( await self.event_converter.target2yiri(event,self.bot),self From 873a0339d8a9d8aa8452e3842fbfe8a579d477b7 Mon Sep 17 00:00:00 2001 From: wangcham Date: Tue, 1 Apr 2025 03:03:48 -0400 Subject: [PATCH 24/73] feat: add support for sending active message in slack --- pkg/platform/sources/slack.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index 9d910a70..bc4e4d8e 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -147,7 +147,12 @@ class SlackAdapter(adapter.MessagePlatformAdapter): ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): - pass + content_list = await SlackMessageConverter.yiri2target(message) + for content in content_list: + if target_type == 'person': + await self.bot.send_message_to_one(content['content'],target_id) + if target_type == 'group': + await self.bot.send_message_to_channel(content['content'],target_id) def register_listener( From 011a7958959b3e2773dbbc1ae3950626476a2cd4 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Apr 2025 15:32:48 +0800 Subject: [PATCH 25/73] doc(README): add slack --- README.md | 2 +- README_EN.md | 2 +- README_JP.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0b7df23d..f7ef74e6 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ | 钉钉 | ✅ | | | Discord | ✅ | | | Telegram | ✅ | | -| Slack | 🚧 | | +| Slack | ✅ | | | LINE | 🚧 | | | WhatsApp | 🚧 | | diff --git a/README_EN.md b/README_EN.md index 47df5ec1..77338219 100644 --- a/README_EN.md +++ b/README_EN.md @@ -90,7 +90,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do | DingTalk | ✅ | | | Discord | ✅ | | | Telegram | ✅ | | -| Slack | 🚧 | | +| Slack | ✅ | | | LINE | 🚧 | | | WhatsApp | 🚧 | | diff --git a/README_JP.md b/README_JP.md index db4b0cf7..440134a2 100644 --- a/README_JP.md +++ b/README_JP.md @@ -89,7 +89,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール | DingTalk | ✅ | | | Discord | ✅ | | | Telegram | ✅ | | -| Slack | 🚧 | | +| Slack | ✅ | | | LINE | 🚧 | | | WhatsApp | 🚧 | | From 47acb63feba9f302e738117b30f7d94a72828281 Mon Sep 17 00:00:00 2001 From: wangcham Date: Tue, 1 Apr 2025 07:11:48 -0400 Subject: [PATCH 26/73] add support for markdown card in dingtalk & tg --- libs/dingtalk_api/api.py | 8 ++++++-- pkg/platform/sources/dingtalk.py | 3 ++- pkg/platform/sources/telegram.py | 3 +++ templates/platform.json | 6 ++++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/libs/dingtalk_api/api.py b/libs/dingtalk_api/api.py index fa4d0421..789b2e95 100644 --- a/libs/dingtalk_api/api.py +++ b/libs/dingtalk_api/api.py @@ -10,7 +10,7 @@ import traceback class DingTalkClient: - def __init__(self, client_id: str, client_secret: str,robot_name:str,robot_code:str): + def __init__(self, client_id: str, client_secret: str,robot_name:str,robot_code:str,markdown_card:bool): """初始化 WebSocket 连接并自动启动""" self.credential = dingtalk_stream.Credential(client_id, client_secret) self.client = dingtalk_stream.DingTalkStreamClient(self.credential) @@ -26,6 +26,7 @@ class DingTalkClient: self.robot_name = robot_name self.robot_code = robot_code self.access_token_expiry_time = '' + self.markdown_card = markdown_card @@ -128,7 +129,10 @@ class DingTalkClient: async def send_message(self,content:str,incoming_message): - self.EchoTextHandler.reply_text(content,incoming_message) + if self.markdown_card: + self.EchoTextHandler.reply_markdown(title=self.robot_name+'的回答',text=content,incoming_message=incoming_message) + else: + self.EchoTextHandler.reply_text(content,incoming_message) async def get_incoming_message(self): diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index aa768039..94a7d249 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -131,7 +131,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): client_id=config["client_id"], client_secret=config["client_secret"], robot_name = config["robot_name"], - robot_code=config["robot_code"] + robot_code=config["robot_code"], + markdown_card=config["markdown_card"] ) async def reply_message( diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 49822673..b8c4a4b7 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -207,6 +207,9 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): "text": component['text'], } + if self.config['markdown_card'] is True: + args["parse_mode"] = "MarkdownV2" + if quote_origin: args['reply_to_message_id'] = message_source.source_platform_object.message.id diff --git a/templates/platform.json b/templates/platform.json index a2fbd36e..dae9a21a 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -88,12 +88,14 @@ "client_id":"", "client_secret":"", "robot_code":"", - "robot_name":"" + "robot_name":"", + "markdown_card":false }, { "adapter":"telegram", "enable": false, - "token":"" + "token":"", + "markdown_card":false }, { "adapter":"slack", From 4a4ca54c6e27d47e84366790fed1ee0394556a60 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Apr 2025 19:59:45 +0800 Subject: [PATCH 27/73] feat: migration for markdown config --- .../migrations/m038_tg_dingtalk_markdown.py | 26 +++++++++++++++++++ pkg/core/stages/migrate.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 pkg/core/migrations/m038_tg_dingtalk_markdown.py diff --git a/pkg/core/migrations/m038_tg_dingtalk_markdown.py b/pkg/core/migrations/m038_tg_dingtalk_markdown.py new file mode 100644 index 00000000..1123c6b2 --- /dev/null +++ b/pkg/core/migrations/m038_tg_dingtalk_markdown.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from .. import migration + + +@migration.migration_class("tg-dingtalk-markdown", 38) +class TgDingtalkMarkdownMigration(migration.Migration): + """迁移""" + + async def need_migrate(self) -> bool: + """判断当前环境是否需要运行此迁移""" + + for adapter in self.ap.platform_cfg.data['platform-adapters']: + if adapter['adapter'] in ['dingtalk','telegram']: + if 'markdown_card' not in adapter: + return True + return False + + async def run(self): + """执行迁移""" + for adapter in self.ap.platform_cfg.data['platform-adapters']: + if adapter['adapter'] in ['dingtalk','telegram']: + if 'markdown_card' not in adapter: + adapter['markdown_card'] = False + await self.ap.platform_cfg.dump_config() + \ No newline at end of file diff --git a/pkg/core/stages/migrate.py b/pkg/core/stages/migrate.py index fe0dc464..a12129f2 100644 --- a/pkg/core/stages/migrate.py +++ b/pkg/core/stages/migrate.py @@ -12,7 +12,7 @@ from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_conf from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message -from ..migrations import m037_mcp_config +from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown @stage.stage_class("MigrationStage") From dbe5a4139522a4cf26b2dfa21c0333f520a81ca8 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Apr 2025 20:01:20 +0800 Subject: [PATCH 28/73] chore: schema for markdown config --- templates/schema/platform.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/templates/schema/platform.json b/templates/schema/platform.json index 4bc6f111..3819a993 100644 --- a/templates/schema/platform.json +++ b/templates/schema/platform.json @@ -446,6 +446,11 @@ "type": "string", "default": "", "description": "钉钉的robot_name" + }, + "markdown_card": { + "type": "boolean", + "default": false, + "description": "是否使用 Markdown 卡片发送消息" } } }, @@ -466,6 +471,11 @@ "type": "string", "default": "", "description": "Telegram 的 token" + }, + "markdown_card": { + "type": "boolean", + "default": false, + "description": "是否使用 Markdown 卡片发送消息" } } } From 0877046db7acd8fdefdee19dad6abd4bddab2ce4 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Apr 2025 20:03:42 +0800 Subject: [PATCH 29/73] chore: add slack config schema --- templates/schema/platform.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/templates/schema/platform.json b/templates/schema/platform.json index 3819a993..ffc735a0 100644 --- a/templates/schema/platform.json +++ b/templates/schema/platform.json @@ -478,6 +478,36 @@ "description": "是否使用 Markdown 卡片发送消息" } } + }, + { + "title": "Slack 适配器", + "description": "用于接入 Slack", + "properties": { + "adapter": { + "type": "string", + "const": "slack" + }, + "enable": { + "type": "boolean", + "default": false, + "description": "是否启用此适配器" + }, + "bot_token": { + "type": "string", + "default": "", + "description": "Slack 的 bot_token" + }, + "signing_secret": { + "type": "string", + "default": "", + "description": "Slack 的 signing_secret" + }, + "port": { + "type": "integer", + "default": 2288, + "description": "监听的端口" + } + } } ] } From 122cb1188c1fd0bae3fc8faac4b4eeb506a404ce Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Apr 2025 20:37:39 +0800 Subject: [PATCH 30/73] style: standardized component names --- pkg/platform/sources/gewechat.py | 32 +++++----- pkg/platform/types/message.py | 100 +++++++++++++++---------------- 2 files changed, 65 insertions(+), 67 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index acbf92e6..e1be48f1 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -47,16 +47,16 @@ class GewechatMessageConverter(adapter.MessageConverter): if not component.url: pass content_list.append({"type": "image", "image": component.url}) - elif isinstance(component, platform_message.MiniPrograms): - content_list.append({"type": 'MiniPrograms', 'mini_app_id': component.mini_app_id, 'display_name': component.display_name, + elif isinstance(component, platform_message.WeChatMiniPrograms): + content_list.append({"type": 'WeChatMiniPrograms', 'mini_app_id': component.mini_app_id, 'display_name': component.display_name, 'page_path': component.page_path, 'cover_img_url': component.image_url, 'title': component.title, 'user_name': component.user_name}) - elif isinstance(component, platform_message.ForwardMiniPrograms): - content_list.append({"type": 'ForwardMiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) - elif isinstance(component, platform_message.Emoji): - content_list.append({'type': 'emoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size}) - elif isinstance(component, platform_message.Link): - content_list.append({'type': 'Link', 'link_title': component.link_title, 'link_desc': component.link_desc, + elif isinstance(component, platform_message.WeChatForwardMiniPrograms): + content_list.append({"type": 'WeChatForwardMiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url}) + elif isinstance(component, platform_message.WeChatEmoji): + content_list.append({'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size}) + elif isinstance(component, platform_message.WeChatLink): + content_list.append({'type': 'WeChatLink', 'link_title': component.link_title, 'link_desc': component.link_desc, 'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url}) @@ -372,16 +372,16 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): elif msg['type'] == 'image': self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) - elif msg['type'] == 'MiniPrograms': + elif msg['type'] == 'WeChatMiniPrograms': self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id'] , display_name=msg['display_name'], page_path=msg['page_path'] , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) - elif msg['type'] == 'ForwardMiniPrograms': + elif msg['type'] == 'WeChatForwardMiniPrograms': self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) - elif msg['type'] == 'emoji': + elif msg['type'] == 'WeChatEmoji': self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) - elif msg['type'] == 'Link': + elif msg['type'] == 'WeChatLink': self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id ,title=msg['link_title'], desc=msg['link_desc'] , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) @@ -421,16 +421,16 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): elif msg['type'] == 'image': self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) - elif msg['type'] == 'MiniPrograms': + elif msg['type'] == 'WeChatMiniPrograms': self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id'] , display_name=msg['display_name'], page_path=msg['page_path'] , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) - elif msg['type'] == 'ForwardMiniPrograms': + elif msg['type'] == 'WeChatForwardMiniPrograms': self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) - elif msg['type'] == 'emoji': + elif msg['type'] == 'WeChatEmoji': self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) - elif msg['type'] == 'Link': + elif msg['type'] == 'WeChatLink': self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id , title=msg['link_title'], desc=msg['link_desc'] , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index 8b509b17..b6b87596 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -644,57 +644,6 @@ class Unknown(MessageComponent): """文本。""" - - -class MiniPrograms(MessageComponent): - """小程序""" - type: str = 'MiniPrograms' - """小程序id""" - mini_app_id: str - """小程序归属用户id""" - user_name: str - """小程序名称""" - display_name: typing.Optional[str] = '' - """打开地址""" - page_path: typing.Optional[str] = '' - """小程序标题""" - title: typing.Optional[str] = '' - """首页图片""" - image_url: typing.Optional[str] = '' - - -class ForwardMiniPrograms(MessageComponent): - """转发小程序""" - type: str = 'ForwardMiniPrograms' - """xml数据""" - xml_data: str - """首页图片""" - image_url: typing.Optional[str] = None - - -class Emoji(MessageComponent): - """emoji表情""" - type: str = 'EmoJi' - """emojimd5""" - emoji_md5: str - """emoji大小""" - emoji_size: int - - -class Link(MessageComponent): - """发送链接""" - type: str = 'Link' - """标题""" - link_title: str = '' - """链接描述""" - link_desc: str = '' - """链接地址""" - link_url: str = '' - """链接略缩图""" - link_thumb_url: str = '' - - - class Voice(MessageComponent): """语音。""" type: str = "Voice" @@ -862,3 +811,52 @@ class File(MessageComponent): def __str__(self): return f'[文件]{self.name}' +# ================ 个人微信专用组件 ================ + +class WeChatMiniPrograms(MessageComponent): + """小程序。个人微信专用组件。""" + type: str = 'WeChatMiniPrograms' + """小程序id""" + mini_app_id: str + """小程序归属用户id""" + user_name: str + """小程序名称""" + display_name: typing.Optional[str] = '' + """打开地址""" + page_path: typing.Optional[str] = '' + """小程序标题""" + title: typing.Optional[str] = '' + """首页图片""" + image_url: typing.Optional[str] = '' + + +class WeChatForwardMiniPrograms(MessageComponent): + """转发小程序。个人微信专用组件。""" + type: str = 'WeChatForwardMiniPrograms' + """xml数据""" + xml_data: str + """首页图片""" + image_url: typing.Optional[str] = None + + +class WeChatEmoji(MessageComponent): + """emoji表情。个人微信专用组件。""" + type: str = 'WeChatEmoji' + """emojimd5""" + emoji_md5: str + """emoji大小""" + emoji_size: int + + +class WeChatLink(MessageComponent): + """发送链接。个人微信专用组件。""" + type: str = 'WeChatLink' + """标题""" + link_title: str = '' + """链接描述""" + link_desc: str = '' + """链接地址""" + link_url: str = '' + """链接略缩图""" + link_thumb_url: str = '' + From 99cc50b5cbee439d6c488437695732b259d2c093 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Apr 2025 20:42:23 +0800 Subject: [PATCH 31/73] chore: provide default prompt --- templates/provider.json | 2 +- templates/schema/provider.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/provider.json b/templates/provider.json index e34f6d32..6b9ce010 100644 --- a/templates/provider.json +++ b/templates/provider.json @@ -100,7 +100,7 @@ "model": "gpt-4o", "prompt-mode": "normal", "prompt": { - "default": "" + "default": "You are a helpful assistant." }, "runner": "local-agent", "dify-service-api": { diff --git a/templates/schema/provider.json b/templates/schema/provider.json index def2cc12..af36a19b 100644 --- a/templates/schema/provider.json +++ b/templates/schema/provider.json @@ -368,7 +368,7 @@ "type": "string", "title": "默认情景预设", "description": "设置默认情景预设。值为空字符串时,将不使用情景预设(人格)", - "default": "" + "default": "You are a helpful assistant." } }, "patternProperties": { From f11a036c609bce29ef245c660f84baec27df7e3b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Apr 2025 21:13:41 +0800 Subject: [PATCH 32/73] chore: release v3.4.12 --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 5830d883..183a7c44 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.11.2" +semantic_version = "v3.4.12" debug_mode = False From 0e00da6617d6bed993ba9ae1203417a3684d5ccd Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Wed, 2 Apr 2025 12:33:15 +0800 Subject: [PATCH 33/73] Merge pull request #1270 from RockChinQ/fix/telegram-markdown fix: markdown and image problems in tg --- pkg/core/bootutils/deps.py | 1 + pkg/platform/sources/slack.yaml | 7 ------- pkg/platform/sources/telegram.py | 20 +++++++++++--------- requirements.txt | 1 + 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pkg/core/bootutils/deps.py b/pkg/core/bootutils/deps.py index c1db6482..71639d08 100644 --- a/pkg/core/bootutils/deps.py +++ b/pkg/core/bootutils/deps.py @@ -35,6 +35,7 @@ required_deps = { "telegram": "python-telegram-bot", "certifi": "certifi", "mcp": "mcp", + "telegramify_markdown":"telegramify-markdown", } diff --git a/pkg/platform/sources/slack.yaml b/pkg/platform/sources/slack.yaml index ffc924e3..7b16960c 100644 --- a/pkg/platform/sources/slack.yaml +++ b/pkg/platform/sources/slack.yaml @@ -31,13 +31,6 @@ spec: type: int required: true default: 2288 - - name: host - label: - en_US: Host - zh_CN: 监听主机 - type: string - required: true - default: 0.0.0.0 execution: python: path: ./slack.py diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index b8c4a4b7..05e24a44 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -4,7 +4,7 @@ import telegram import telegram.ext from telegram import Update from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters - +import telegramify_markdown import typing import asyncio import traceback @@ -86,9 +86,10 @@ class TelegramMessageConverter(adapter.MessageConverter): 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)) + if message.caption: + message_components.extend(parse_message_text(message.caption)) file = await message.photo[-1].get_file() @@ -201,19 +202,20 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): for component in components: if component['type'] == 'text': - + content = telegramify_markdown.markdownify( + content= component['text'], + ) args = { "chat_id": message_source.source_platform_object.effective_chat.id, - "text": component['text'], + "text": content, } - if self.config['markdown_card'] is True: args["parse_mode"] = "MarkdownV2" + if quote_origin: + args['reply_to_message_id'] = message_source.source_platform_object.message.id - if quote_origin: - args['reply_to_message_id'] = message_source.source_platform_object.message.id + await self.bot.send_message(**args) - await self.bot.send_message(**args) async def is_muted(self, group_id: int) -> bool: return False diff --git a/requirements.txt b/requirements.txt index cd82211c..a185ee24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,6 @@ python-telegram-bot certifi mcp slack_sdk +telegramify-markdown # indirect taskgroup==0.0.0a4 \ No newline at end of file From 51634c1caf2f097f8e54da14fe5a80a4f41acf72 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Wed, 2 Apr 2025 15:23:38 +0800 Subject: [PATCH 34/73] chore: release v3.4.12.1 (#1271) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 183a7c44..332756ac 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.12" +semantic_version = "v3.4.12.1" debug_mode = False From f9d07779a9b8d9468ee52361ad42c36803b9fe59 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Thu, 3 Apr 2025 14:17:21 +0800 Subject: [PATCH 35/73] fix: slack is incorrectly enabled as default (#1274) --- templates/platform.json | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/templates/platform.json b/templates/platform.json index dae9a21a..d3f23b43 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -71,38 +71,38 @@ "token": "" }, { - "adapter":"officialaccount", + "adapter": "officialaccount", "enable": false, "token": "", - "EncodingAESKey":"", - "AppID":"", - "AppSecret":"", - "Mode":"drop", - "LoadingMessage":"AI正在思考中,请发送任意内容获取回复。", + "EncodingAESKey": "", + "AppID": "", + "AppSecret": "", + "Mode": "drop", + "LoadingMessage": "AI正在思考中,请发送任意内容获取回复。", "host": "0.0.0.0", "port": 2287 }, { - "adapter":"dingtalk", + "adapter": "dingtalk", "enable": false, - "client_id":"", - "client_secret":"", - "robot_code":"", - "robot_name":"", - "markdown_card":false + "client_id": "", + "client_secret": "", + "robot_code": "", + "robot_name": "", + "markdown_card": false }, { - "adapter":"telegram", + "adapter": "telegram", "enable": false, - "token":"", - "markdown_card":false + "token": "", + "markdown_card": false }, { - "adapter":"slack", - "enable":true, - "bot_token":"", - "signing_secret":"", - "port":2288 + "adapter": "slack", + "enable": false, + "bot_token": "", + "signing_secret": "", + "port": 2288 } ], "track-function-calls": true, From b09ce8296f02ab80bd6841d894d367bdf9038249 Mon Sep 17 00:00:00 2001 From: yrk <12787191+yrk15994109427@user.noreply.gitee.com> Date: Thu, 3 Apr 2025 16:55:14 +0800 Subject: [PATCH 36/73] Add ModelScope Support --- README.md | 1 + README_EN.md | 1 + README_JP.md | 1 + .../m039_modelscope_cfg_completion.py | 30 +++ pkg/core/stages/migrate.py | 2 +- pkg/provider/modelmgr/modelmgr.py | 2 +- .../modelmgr/requesters/modelscopechatcmpl.py | 207 ++++++++++++++++++ .../requesters/modelscopechatcmpl.yaml | 34 +++ templates/metadata/llm-models.json | 90 ++++++++ templates/provider.json | 8 + 10 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 pkg/core/migrations/m039_modelscope_cfg_completion.py create mode 100644 pkg/provider/modelmgr/requesters/modelscopechatcmpl.py create mode 100644 pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml diff --git a/README.md b/README.md index f7ef74e6..74def45c 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ | [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 | | [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 | | [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 | +| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | | ### TTS diff --git a/README_EN.md b/README_EN.md index 77338219..894ce5fe 100644 --- a/README_EN.md +++ b/README_EN.md @@ -114,6 +114,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do | [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 | | [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol | +| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | | ## 🤝 Community Contribution diff --git a/README_JP.md b/README_JP.md index 440134a2..5589ad79 100644 --- a/README_JP.md +++ b/README_JP.md @@ -113,6 +113,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール | [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プラットフォーム | | [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート | +| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | | ## 🤝 コミュニティ貢献 diff --git a/pkg/core/migrations/m039_modelscope_cfg_completion.py b/pkg/core/migrations/m039_modelscope_cfg_completion.py new file mode 100644 index 00000000..ef1a82fd --- /dev/null +++ b/pkg/core/migrations/m039_modelscope_cfg_completion.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from .. import migration + + +@migration.migration_class("modelscope-config-completion", 4) +class ModelScopeConfigCompletionMigration(migration.Migration): + """OpenAI配置迁移 + """ + + async def need_migrate(self) -> bool: + """判断当前环境是否需要运行此迁移 + """ + return 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester'] \ + or 'modelscope' not in self.ap.provider_cfg.data['keys'] + + async def run(self): + """执行迁移 + """ + if 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester']: + self.ap.provider_cfg.data['requester']['modelscope-chat-completions'] = { + 'base-url': 'https://api.modelscope.cn/v1', + 'args': {}, + 'timeout': 120, + } + + if 'modelscope' not in self.ap.provider_cfg.data['keys']: + self.ap.provider_cfg.data['keys']['modelscope'] = [] + + await self.ap.provider_cfg.dump_config() diff --git a/pkg/core/stages/migrate.py b/pkg/core/stages/migrate.py index a12129f2..f8e6965d 100644 --- a/pkg/core/stages/migrate.py +++ b/pkg/core/stages/migrate.py @@ -12,7 +12,7 @@ from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_conf from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message -from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown +from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown, m039_modelscope_cfg_completion @stage.stage_class("MigrationStage") diff --git a/pkg/provider/modelmgr/modelmgr.py b/pkg/provider/modelmgr/modelmgr.py index e8c231c5..a5ffe6bc 100644 --- a/pkg/provider/modelmgr/modelmgr.py +++ b/pkg/provider/modelmgr/modelmgr.py @@ -6,7 +6,7 @@ from . import entities, requester from ...core import app from ...discover import engine 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, modelscopechatcmpl FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list" diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py new file mode 100644 index 00000000..8f51241e --- /dev/null +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +import asyncio +import typing +import json +import base64 +from typing import AsyncGenerator + +import openai +import openai.types.chat.chat_completion as chat_completion +import openai.types.chat.chat_completion_message_tool_call as chat_completion_message_tool_call +import httpx +import aiohttp +import async_lru + +from .. import entities, errors, requester +from ....core import entities as core_entities, app +from ... import entities as llm_entities +from ...tools import entities as tools_entities +from ....utils import image + + +class ModelScopeChatCompletions(requester.LLMAPIRequester): + """ModelScope 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']['modelscope-chat-completions'] + + async def initialize(self): + + self.client = openai.AsyncClient( + api_key="", + base_url=self.requester_cfg['base-url'], + timeout=self.requester_cfg['timeout'], + http_client=httpx.AsyncClient( + trust_env=True, + timeout=self.requester_cfg['timeout'] + ) + ) + + async def _req( + 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, + chat_completion: chat_completion.ChatCompletion, + ) -> llm_entities.Message: + chatcmpl_message = chat_completion.choices[0].message.dict() + + # 确保 role 字段存在且不为 None + if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: + chatcmpl_message['role'] = 'assistant' + + message = llm_entities.Message(**chatcmpl_message) + + return message + + async def _closure( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: entities.LLMModelInfo, + use_funcs: list[tools_entities.LLMFunction] = None, + ) -> llm_entities.Message: + self.client.api_key = use_model.token_mgr.get_token() + + args = self.requester_cfg['args'].copy() + args["model"] = use_model.name if use_model.model_name is None else use_model.model_name + + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args["tools"] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg["content"], list): + for me in msg["content"]: + if me["type"] == "image_base64": + me["image_url"] = { + "url": me["image_base64"] + } + me["type"] = "image_url" + del me["image_base64"] + + args["messages"] = messages + + # 发送请求 + resp = await self._req(args) + + # 处理请求结果 + message = await self._make_msg(resp) + + return message + + async def call( + self, + query: core_entities.Query, + model: entities.LLMModelInfo, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + ) -> llm_entities.Message: + req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 + for m in messages: + msg_dict = m.dict(exclude_none=True) + content = msg_dict.get("content") + if isinstance(content, list): + # 检查 content 列表中是否每个部分都是文本 + if all(isinstance(part, dict) and part.get("type") == "text" for part in content): + # 将所有文本部分合并为一个字符串 + msg_dict["content"] = "\n".join(part["text"] for part in content) + req_messages.append(msg_dict) + + try: + return await self._closure(query=query, req_messages=req_messages, use_model=model, use_funcs=funcs) + except asyncio.TimeoutError: + raise errors.RequesterError('请求超时') + except openai.BadRequestError as e: + if 'context_length_exceeded' in e.message: + raise errors.RequesterError(f'上文过长,请重置会话: {e.message}') + else: + raise errors.RequesterError(f'请求参数错误: {e.message}') + except openai.AuthenticationError as e: + raise errors.RequesterError(f'无效的 api-key: {e.message}') + except openai.NotFoundError as e: + raise errors.RequesterError(f'请求路径错误: {e.message}') + except openai.RateLimitError as e: + raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') + except openai.APIError as e: + raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml new file mode 100644 index 00000000..eb8c70ec --- /dev/null +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: LLMAPIRequester +metadata: + name: modelscope-chat-completions + label: + en_US: ModelScope + zh_CN: 魔搭社区 +spec: + config: + - name: base-url + label: + en_US: Base URL + zh_CN: 基础 URL + type: string + required: true + default: "https://api-inference.modelscope.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: ./modelscopechatcmpl.py + attr: ModelScopeChatCompletions diff --git a/templates/metadata/llm-models.json b/templates/metadata/llm-models.json index a12c7687..66163064 100644 --- a/templates/metadata/llm-models.json +++ b/templates/metadata/llm-models.json @@ -232,6 +232,96 @@ "token_mgr": "zhipuai", "vision_supported": true, "tool_call_supported": true + }, + { + "name": "Qwen/Qwen2.5-Coder-32B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/Qwen2.5-Coder-14B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/Qwen2.5-Coder-7B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/Qwen2.5-72B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/Qwen2.5-32B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/Qwen2.5-14B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/Qwen2.5-7B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/QwQ-32B-Preview", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "Qwen/QwQ-32B", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "LLM-Research/Llama-3.3-70B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "LLM-Research/Meta-Llama-3.1-405B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "LLM-Research/Meta-Llama-3.1-8B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "LLM-Research/Meta-Llama-3.1-70B-Instruct", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "mistralai/Ministral-8B-Instruct-2410", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true + }, + { + "name": "deepseek-ai/DeepSeek-V3-0324", + "requester": "modelscope-chat-completions", + "token_mgr": "modelscope", + "tool_call_supported": true } ] } \ No newline at end of file diff --git a/templates/provider.json b/templates/provider.json index 6b9ce010..135c086d 100644 --- a/templates/provider.json +++ b/templates/provider.json @@ -31,6 +31,9 @@ ], "volcark": [ "xxxxxxxx" + ], + "modelscope": [ + "xxxxxxxx" ] }, "requester": { @@ -95,6 +98,11 @@ "args": {}, "base-url": "https://ark.cn-beijing.volces.com/api/v3", "timeout": 120 + }, + "modelscope-chat-completions": { + "base-url": "https://api-inference.modelscope.cn/v1", + "args": {}, + "timeout": 120 } }, "model": "gpt-4o", From ea3fff59ac4a3693e053836d8e324e5ee8df78c8 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 3 Apr 2025 20:40:36 +0800 Subject: [PATCH 37/73] chore: remove verbose models from llm-models.json --- templates/metadata/llm-models.json | 90 ------------------------------ 1 file changed, 90 deletions(-) diff --git a/templates/metadata/llm-models.json b/templates/metadata/llm-models.json index 66163064..a12c7687 100644 --- a/templates/metadata/llm-models.json +++ b/templates/metadata/llm-models.json @@ -232,96 +232,6 @@ "token_mgr": "zhipuai", "vision_supported": true, "tool_call_supported": true - }, - { - "name": "Qwen/Qwen2.5-Coder-32B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/Qwen2.5-Coder-14B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/Qwen2.5-Coder-7B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/Qwen2.5-72B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/Qwen2.5-32B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/Qwen2.5-14B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/Qwen2.5-7B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/QwQ-32B-Preview", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "Qwen/QwQ-32B", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "LLM-Research/Llama-3.3-70B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "LLM-Research/Meta-Llama-3.1-405B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "LLM-Research/Meta-Llama-3.1-8B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "LLM-Research/Meta-Llama-3.1-70B-Instruct", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "mistralai/Ministral-8B-Instruct-2410", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true - }, - { - "name": "deepseek-ai/DeepSeek-V3-0324", - "requester": "modelscope-chat-completions", - "token_mgr": "modelscope", - "tool_call_supported": true } ] } \ No newline at end of file From 30b068c6e213b455b92ca490cfb6f6ee279b7f93 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 3 Apr 2025 20:44:41 +0800 Subject: [PATCH 38/73] doc: reorder `modelscope` in README --- README.md | 2 +- README_EN.md | 2 +- README_JP.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74def45c..9e627ce3 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,8 @@ | [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 平台 | +| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 | | [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 | -| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | | ### TTS diff --git a/README_EN.md b/README_EN.md index 894ce5fe..68fd4028 100644 --- a/README_EN.md +++ b/README_EN.md @@ -113,8 +113,8 @@ Directly use the released version to run, see the [Manual Deployment](https://do | [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 | +| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) | | [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol | -| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | | ## 🤝 Community Contribution diff --git a/README_JP.md b/README_JP.md index 5589ad79..6966ce1f 100644 --- a/README_JP.md +++ b/README_JP.md @@ -112,8 +112,8 @@ LangBotはBTPanelにリストされています。BTPanelをインストール | [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プラットフォーム | +| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) | | [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート | -| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | | ## 🤝 コミュニティ貢献 From 8a4967525ada1e4caf34a8356d8e3e9b522f5f71 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 3 Apr 2025 20:52:01 +0800 Subject: [PATCH 39/73] fix(modelscope): bad base-url in migration --- pkg/core/migrations/m039_modelscope_cfg_completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/migrations/m039_modelscope_cfg_completion.py b/pkg/core/migrations/m039_modelscope_cfg_completion.py index ef1a82fd..63e9c6d8 100644 --- a/pkg/core/migrations/m039_modelscope_cfg_completion.py +++ b/pkg/core/migrations/m039_modelscope_cfg_completion.py @@ -19,7 +19,7 @@ class ModelScopeConfigCompletionMigration(migration.Migration): """ if 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester']: self.ap.provider_cfg.data['requester']['modelscope-chat-completions'] = { - 'base-url': 'https://api.modelscope.cn/v1', + 'base-url': 'https://api-inference.modelscope.cn/v1', 'args': {}, 'timeout': 120, } From c5457374a8e633caeabbe4afc5025ab6a2968492 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Wed, 9 Apr 2025 21:58:23 +0800 Subject: [PATCH 40/73] chore: release v3.4.13 (#1284) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 332756ac..5ea1b15d 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.12.1" +semantic_version = "v3.4.13" debug_mode = False From 07e073f52687bb40d31ac87b6a93db0385e36fba Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Fri, 11 Apr 2025 17:52:04 +0800 Subject: [PATCH 41/73] chore: perf issue template (#1289) --- .github/ISSUE_TEMPLATE/bug-report.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 7181f918..546e10be 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -3,22 +3,6 @@ description: 报错或漏洞请使用这个模板创建,不使用此模板创 title: "[Bug]: " labels: ["bug?"] body: - - type: dropdown - attributes: - label: 消息平台适配器 - description: "接入的消息平台类型" - options: - - 其他(或暂未使用) - - Nakuru(go-cqhttp) - - aiocqhttp(使用 OneBot 协议接入的) - - qq-botpy(QQ官方API WebSocket) - - qqofficial(QQ官方API Webhook) - - lark(飞书) - - wecom(企业微信) - - gewechat(个人微信) - - discord - validations: - required: true - type: input attributes: label: 运行环境 From c531cb11af6613b162b6f5529b46fd33b2c3940c Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Sun, 13 Apr 2025 17:47:05 +0800 Subject: [PATCH 42/73] fix: bailian api streaming mode can't be established --- pkg/provider/modelmgr/requesters/bailianchatcmpl.py | 5 +++-- requirements.txt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/bailianchatcmpl.py b/pkg/provider/modelmgr/requesters/bailianchatcmpl.py index 8f6b258c..5504b151 100644 --- a/pkg/provider/modelmgr/requesters/bailianchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/bailianchatcmpl.py @@ -2,12 +2,12 @@ from __future__ import annotations import openai -from . import chatcmpl +from . import chatcmpl, modelscopechatcmpl from .. import requester from ....core import app -class BailianChatCompletions(chatcmpl.OpenAIChatCompletions): +class BailianChatCompletions(modelscopechatcmpl.ModelScopeChatCompletions): """阿里云百炼大模型平台 ChatCompletion API 请求器""" client: openai.AsyncClient @@ -18,3 +18,4 @@ class BailianChatCompletions(chatcmpl.OpenAIChatCompletions): self.ap = ap self.requester_cfg = self.ap.provider_cfg.data['requester']['bailian-chat-completions'] + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a185ee24..d0adc126 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,4 +37,5 @@ mcp slack_sdk telegramify-markdown # indirect -taskgroup==0.0.0a4 \ No newline at end of file +taskgroup==0.0.0a4 +python-socks \ No newline at end of file From 210a8856e268750946394be15ab5229347e3038d Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Sun, 13 Apr 2025 18:48:38 +0800 Subject: [PATCH 43/73] fix: telegram markdown & supergroup bugs (#1293) --- pkg/platform/sources/telegram.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 05e24a44..b463c6b3 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -127,7 +127,7 @@ class TelegramEventConverter(adapter.EventConverter): time=event.message.date.timestamp(), source_platform_object=event ) - elif event.effective_chat.type == 'group': + elif event.effective_chat.type == 'group' or 'supergroup' : return platform_events.GroupMessage( sender=platform_entities.GroupMember( id=event.effective_chat.id, @@ -202,9 +202,12 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): for component in components: if component['type'] == 'text': - content = telegramify_markdown.markdownify( - content= component['text'], - ) + if self.config['markdown_card'] is True: + content = telegramify_markdown.markdownify( + content= component['text'], + ) + else: + content = component['text'] args = { "chat_id": message_source.source_platform_object.effective_chat.id, "text": content, From 42fabd51333cc75c0f1dc3d6b6c82074e4317482 Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Mon, 14 Apr 2025 14:37:34 +0800 Subject: [PATCH 44/73] fix: delete print function in lark (#1295) --- pkg/platform/sources/lark.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index a04ceb4e..5639739c 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -343,7 +343,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): type = context.header.event_type if 'url_verification' == type: - print(data.get("challenge")) # todo 验证verification token return { "challenge": data.get("challenge") From 7c2ceb0aca16fd5ec9a31409d47acef73de55aca Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Mon, 14 Apr 2025 15:05:53 +0800 Subject: [PATCH 45/73] fix: add reasoning content for deepseek-reasoner (#1296) --- pkg/provider/modelmgr/requesters/chatcmpl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index e65d908b..14f83146 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -61,6 +61,12 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: chatcmpl_message['role'] = 'assistant' + reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None + + # deepseek的reasoner模型 + if reasoning_content is not None: + chatcmpl_message['content'] = "\n" + reasoning_content + "\n\n\n"+ chatcmpl_message['content'] + message = llm_entities.Message(**chatcmpl_message) return message From 601b0a896470fbd64143f181b35638d56d3f67de Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Mon, 14 Apr 2025 20:17:11 +0800 Subject: [PATCH 46/73] fix(moonshot): tool_call_id not found error (#1040) (#1298) --- pkg/provider/modelmgr/requesters/moonshotchatcmpl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index d863049b..5389d132 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -42,8 +42,8 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): if 'content' in m and isinstance(m["content"], list): m["content"] = " ".join([c["text"] for c in m["content"]]) - # 删除空的 - messages = [m for m in messages if m["content"].strip() != ""] + # 删除空的,不知道干嘛的,直接删了。 + # messages = [m for m in messages if m["content"].strip() != "" and ('tool_calls' not in m or not m['tool_calls'])] args["messages"] = messages From 13e29a996642753fe995b07f00fe241fa5e06f6b Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Mon, 14 Apr 2025 20:19:18 +0800 Subject: [PATCH 47/73] chore: release v3.4.13.1 (#1299) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 5ea1b15d..31d27337 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.13" +semantic_version = "v3.4.13.1" debug_mode = False From 2782c8cebee898c45b978da01468212ea0c59ac5 Mon Sep 17 00:00:00 2001 From: SkyFutu <119418823+SkyFutu@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:00:02 +0800 Subject: [PATCH 48/73] Fix/windows compatibility (#1303) * Update anthropicmsgs.py * Update anthropicmsgs.py * Update anthropicmsgs.py * Update anthropicmsgs.py * Update anthropicmsgs.py --- pkg/provider/modelmgr/requesters/anthropicmsgs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 6bbe4bf1..b6052633 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -4,6 +4,8 @@ import typing import json import traceback import base64 +import platform +import socket import anthropic import httpx @@ -23,6 +25,12 @@ class AnthropicMessages(requester.LLMAPIRequester): client: anthropic.AsyncAnthropic async def initialize(self): + # 兼容 Windows 缺失 TCP_KEEPINTVL 和 TCP_KEEPCNT 的问题 + if platform.system() == "Windows": + if not hasattr(socket, "TCP_KEEPINTVL"): + socket.TCP_KEEPINTVL = 0 + if not hasattr(socket, "TCP_KEEPCNT"): + socket.TCP_KEEPCNT = 0 httpx_client = anthropic._base_client.AsyncHttpxClientWrapper( base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'].replace(' ', ''), From 6e3514c0b2494fedccd3e24bae7346400ba9bccc Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Wed, 16 Apr 2025 15:02:01 +0800 Subject: [PATCH 49/73] feat: add support for wecom customer service (#1304) --- libs/wecom_customer_service_api/__init__.py | 0 libs/wecom_customer_service_api/api.py | 337 ++++++++++++++++++ .../wecomcsevent.py | 134 +++++++ pkg/platform/sources/wecomcs.py | 223 ++++++++++++ pkg/platform/sources/wecomcs.yaml | 51 +++ templates/platform.json | 9 + 6 files changed, 754 insertions(+) create mode 100644 libs/wecom_customer_service_api/__init__.py create mode 100644 libs/wecom_customer_service_api/api.py create mode 100644 libs/wecom_customer_service_api/wecomcsevent.py create mode 100644 pkg/platform/sources/wecomcs.py create mode 100644 pkg/platform/sources/wecomcs.yaml diff --git a/libs/wecom_customer_service_api/__init__.py b/libs/wecom_customer_service_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/wecom_customer_service_api/api.py b/libs/wecom_customer_service_api/api.py new file mode 100644 index 00000000..279585b3 --- /dev/null +++ b/libs/wecom_customer_service_api/api.py @@ -0,0 +1,337 @@ +from quart import request +from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt +import base64 +import binascii +import httpx +import traceback +from quart import Quart +import xml.etree.ElementTree as ET +from typing import Callable, Dict, Any +from .wecomcsevent import WecomCSEvent +from pkg.platform.types import events as platform_events, message as platform_message +import aiofiles + + +class WecomCSClient(): + def __init__(self,corpid:str,secret:str,token:str,EncodingAESKey:str): + self.corpid = corpid + self.secret = secret + self.access_token_for_contacts ='' + self.token = token + self.aes = EncodingAESKey + self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin' + self.access_token = '' + self.app = Quart(__name__) + self.wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid) + self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']) + self._message_handlers = { + "example":[], + } + + async def get_pic_url(self, media_id: str): + if not await self.check_access_token(): + self.access_token = await self.get_access_token(self.secret) + + url = f"{self.base_url}/media/get?access_token={self.access_token}&media_id={media_id}" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.headers.get("Content-Type", "").startswith("application/json"): + data = response.json() + if data.get('errcode') in [40014, 42001]: + self.access_token = await self.get_access_token(self.secret) + return await self.get_pic_url(media_id) + else: + raise Exception("Failed to get image: " + str(data)) + + # 否则是图片,转成 base64 + image_bytes = response.content + content_type = response.headers.get("Content-Type", "") + base64_str = base64.b64encode(image_bytes).decode("utf-8") + base64_str = f"data:{content_type};base64,{base64_str}" + return base64_str + + + #access——token操作 + async def check_access_token(self): + return bool(self.access_token and self.access_token.strip()) + + async def check_access_token_for_contacts(self): + return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip()) + + async def get_access_token(self,secret): + url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}' + async with httpx.AsyncClient() as client: + response = await client.get(url) + data = response.json() + if 'access_token' in data: + return data['access_token'] + else: + raise Exception(f"未获取access token: {data}") + + async def get_detailed_message_list(self,xml_msg:str): + # 在本方法中解析消息,并且获得消息的具体内容 + root = ET.fromstring(xml_msg) + token = root.find("Token").text + open_kfid = root.find("OpenKfId").text + + # if open_kfid in self.openkfid_list: + # return None + # else: + # self.openkfid_list.append(open_kfid) + + if not await self.check_access_token(): + self.access_token = await self.get_access_token(self.secret) + + url = self.base_url+'/kf/sync_msg?access_token='+ self.access_token + async with httpx.AsyncClient() as client: + params = { + "token": token, + "voice_format": 0, + "open_kfid": open_kfid, + } + response = await client.post(url,json=params) + data = response.json() + if data['errcode'] != 0: + raise Exception("Failed to get message") + if data['errcode'] == 40014 or data['errcode'] == 42001: + self.access_token = await self.get_access_token(self.secret) + return await self.get_detailed_message_list(xml_msg) + last_msg_data = data['msg_list'][-1] + open_kfid = last_msg_data.get("open_kfid") + # 进行获取图片操作 + if last_msg_data.get("msgtype") == "image": + media_id = last_msg_data.get("image").get("media_id") + picurl = await self.get_pic_url(media_id) + last_msg_data["picurl"] = picurl + # await self.change_service_status(userid=external_userid,openkfid=open_kfid,servicer=servicer) + return last_msg_data + + + async def change_service_status(self,userid:str,openkfid:str,servicer:str): + if not await self.check_access_token(): + self.access_token = await self.get_access_token(self.secret) + url = self.base_url+"/kf/service_state/get?access_token="+self.access_token + async with httpx.AsyncClient() as client: + params = { + "open_kfid" : openkfid, + "external_userid" : userid, + "service_state" : 1, + "servicer_userid" : servicer, + } + response = await client.post(url,json=params) + data = response.json() + if data['errcode'] == 40014 or data['errcode'] == 42001: + self.access_token = await self.get_access_token(self.secret) + return await self.change_service_status(userid,openkfid) + if data['errcode'] != 0: + raise Exception("Failed to change service status: "+str(data)) + + + async def send_image(self,user_id:str,agent_id:int,media_id:str): + if not await self.check_access_token(): + self.access_token = await self.get_access_token(self.secret) + url = self.base_url+'/media/upload?access_token='+self.access_token + async with httpx.AsyncClient() as client: + params = { + "touser" : user_id, + "toparty" : "", + "totag":"", + "agentid" : agent_id, + "msgtype" : "image", + "image" : { + "media_id" : media_id, + }, + "safe":0, + "enable_id_trans": 0, + "enable_duplicate_check": 0, + "duplicate_check_interval": 1800 + } + try: + response = await client.post(url,json=params) + data = response.json() + except Exception as e: + raise Exception("Failed to send image: "+str(e)) + + # 企业微信错误码40014和42001,代表accesstoken问题 + if data['errcode'] == 40014 or data['errcode'] == 42001: + self.access_token = await self.get_access_token(self.secret) + return await self.send_image(user_id,agent_id,media_id) + + if data['errcode'] != 0: + raise Exception("Failed to send image: "+str(data)) + + + async def send_text_msg(self, open_kfid: str, external_userid: str, msgid: str,content:str): + if not await self.check_access_token(): + self.access_token = await self.get_access_token(self.secret) + + url = f"https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token={self.access_token}" + + payload = { + "touser": external_userid, + "open_kfid": open_kfid, + "msgid": msgid, + "msgtype": "text", + "text": { + "content": content, + } + } + + async with httpx.AsyncClient() as client: + response = await client.post(url, json=payload) + + data = response.json() + if data.get("errcode") != 0: + raise Exception(f"消息发送失败: {data}") + return data + + + async def handle_callback_request(self): + """ + 处理回调请求,包括 GET 验证和 POST 消息接收。 + """ + try: + + msg_signature = request.args.get("msg_signature") + timestamp = request.args.get("timestamp") + nonce = request.args.get("nonce") + + if request.method == "GET": + echostr = request.args.get("echostr") + ret, reply_echo_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) + if ret != 0: + raise Exception(f"验证失败,错误码: {ret}") + return reply_echo_str + + elif request.method == "POST": + encrypt_msg = await request.data + ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce) + if ret != 0: + raise Exception(f"消息解密失败,错误码: {ret}") + + # 解析消息并处理 + message_data = await self.get_detailed_message_list(xml_msg) + if message_data is not None: + event = WecomCSEvent.from_payload(message_data) + if event: + await self._handle_message(event) + + return "success" + except Exception as e: + traceback.print_exc() + return f"Error processing request: {str(e)}", 400 + + async def run_task(self, host: str, port: int, *args, **kwargs): + """ + 启动 Quart 应用。 + """ + await self.app.run_task(host=host, port=port, *args, **kwargs) + + def on_message(self, msg_type: str): + """ + 注册消息类型处理器。 + """ + def decorator(func: Callable[[WecomCSEvent], 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: WecomCSEvent): + """ + 处理消息事件。 + """ + msg_type = event.type + if msg_type in self._message_handlers: + for handler in self._message_handlers[msg_type]: + await handler(event) + + + @staticmethod + async def get_image_type(image_bytes: bytes) -> str: + """ + 通过图片的magic numbers判断图片类型 + """ + magic_numbers = { + b'\xFF\xD8\xFF': 'jpg', + b'\x89\x50\x4E\x47': 'png', + b'\x47\x49\x46': 'gif', + b'\x42\x4D': 'bmp', + b'\x00\x00\x01\x00': 'ico' + } + + for magic, ext in magic_numbers.items(): + if image_bytes.startswith(magic): + return ext + return 'jpg' # 默认返回jpg + + + async def upload_to_work(self, image: platform_message.Image): + """ + 获取 media_id + """ + if not await self.check_access_token(): + self.access_token = await self.get_access_token(self.secret) + + url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file' + file_bytes = None + file_name = "uploaded_file.txt" + + # 获取文件的二进制数据 + if image.path: + async with aiofiles.open(image.path, 'rb') as f: + file_bytes = await f.read() + file_name = image.path.split('/')[-1] + elif image.url: + file_bytes = await self.download_image_to_bytes(image.url) + file_name = image.url.split('/')[-1] + elif image.base64: + try: + base64_data = image.base64 + if ',' in base64_data: + base64_data = base64_data.split(',', 1)[1] + padding = 4 - (len(base64_data) % 4) if len(base64_data) % 4 else 0 + padded_base64 = base64_data + '=' * padding + file_bytes = base64.b64decode(padded_base64) + except binascii.Error as e: + raise ValueError(f"Invalid base64 string: {str(e)}") + else: + raise ValueError("image对象出错") + + # 设置 multipart/form-data 格式的文件 + boundary = "-------------------------acebdf13572468" + headers = { + 'Content-Type': f'multipart/form-data; boundary={boundary}' + } + body = ( + f"--{boundary}\r\n" + f"Content-Disposition: form-data; name=\"media\"; filename=\"{file_name}\"; filelength={len(file_bytes)}\r\n" + f"Content-Type: application/octet-stream\r\n\r\n" + ).encode('utf-8') + file_bytes + f"\r\n--{boundary}--\r\n".encode('utf-8') + + # 上传文件 + async with httpx.AsyncClient() as client: + response = await client.post(url, headers=headers, content=body) + data = response.json() + if data['errcode'] == 40014 or data['errcode'] == 42001: + self.access_token = await self.get_access_token(self.secret) + media_id = await self.upload_to_work(image) + if data.get('errcode', 0) != 0: + raise Exception("failed to upload file") + + media_id = data.get('media_id') + return media_id + + async def download_image_to_bytes(self,url:str) -> bytes: + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + return response.content + + #进行media_id的获取 + async def get_media_id(self, image: platform_message.Image): + + media_id = await self.upload_to_work(image=image) + return media_id diff --git a/libs/wecom_customer_service_api/wecomcsevent.py b/libs/wecom_customer_service_api/wecomcsevent.py new file mode 100644 index 00000000..8dc0e30d --- /dev/null +++ b/libs/wecom_customer_service_api/wecomcsevent.py @@ -0,0 +1,134 @@ +from typing import Dict, Any, Optional + + +class WecomCSEvent(dict): + """ + 封装从企业微信收到的事件数据对象(字典),提供属性以获取其中的字段。 + + 除 `type` 和 `detail_type` 属性对于任何事件都有效外,其它属性是否存在(若不存在则返回 `None`)依事件类型不同而不同。 + """ + + @staticmethod + def from_payload(payload: Dict[str, Any]) -> Optional["WecomCSEvent"]: + """ + 从企业微信(客服会话)事件数据构造 `WecomEvent` 对象。 + + Args: + payload (Dict[str, Any]): 解密后的企业微信事件数据。 + + Returns: + Optional[WecomEvent]: 如果事件数据合法,则返回 WecomEvent 对象;否则返回 None。 + """ + try: + event = WecomCSEvent(payload) + _ = event.type, + return event + except KeyError: + return None + + @property + def type(self) -> str: + """ + 事件类型,例如 "message"、"event"、"text" 等。 + + Returns: + str: 事件类型。 + """ + return self.get("msgtype", "") + + @property + def user_id(self) -> Optional[str]: + """ + 用户 ID,例如消息的发送者或事件的触发者。 + + Returns: + Optional[str]: 用户 ID。 + """ + return self.get("external_userid") + + @property + def receiver_id(self) -> Optional[str]: + """ + 接收者 ID,例如机器人自身的企业微信 ID。 + + Returns: + Optional[str]: 接收者 ID。 + """ + return self.get("open_kfid","") + + @property + def picurl(self) -> Optional[str]: + """ + 图片 URL,仅在图片消息中存在。 + base64格式 + Returns: + Optional[str]: 图片 URL。 + """ + + return self.get("picurl","") + + @property + def message_id(self) -> Optional[str]: + """ + 消息 ID,仅在消息类型事件中存在。 + + Returns: + Optional[str]: 消息 ID。 + """ + return self.get("msgid") + + @property + def message(self) -> Optional[str]: + """ + 消息内容,仅在消息类型事件中存在。 + + Returns: + Optional[str]: 消息内容。 + """ + if self.get("msgtype") == 'text': + return self.get("text").get("content","") + else: + return None + + + @property + def timestamp(self) -> Optional[int]: + """ + 事件发生的时间戳。 + + Returns: + Optional[int]: 时间戳。 + """ + return self.get("send_time") + + + 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"" diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py new file mode 100644 index 00000000..532d7470 --- /dev/null +++ b/pkg/platform/sources/wecomcs.py @@ -0,0 +1,223 @@ +from __future__ import annotations +import typing +import asyncio +import traceback + +import datetime + +from libs.wecom_customer_service_api.api import WecomCSClient +from pkg.platform.adapter import MessagePlatformAdapter +from pkg.platform.types import events as platform_events, message as platform_message +from libs.wecom_customer_service_api.wecomcsevent import WecomCSEvent +from pkg.core import app +from .. import adapter +from ...core import app +from ..types import message as platform_message +from ..types import events as platform_events +from ..types import entities as platform_entities +from ...command.errors import ParamNotEnoughError +from ...utils import image + +class WecomMessageConverter(adapter.MessageConverter): + + @staticmethod + async def yiri2target( + message_chain: platform_message.MessageChain, bot: WecomCSClient + ): + content_list = [] + + for msg in message_chain: + if type(msg) is platform_message.Plain: + content_list.append({ + "type": "text", + "content": msg.text, + }) + elif type(msg) is platform_message.Image: + content_list.append({ + "type": "image", + "media_id": await bot.get_media_id(msg), + }) + elif type(msg) is platform_message.Forward: + for node in msg.node_list: + content_list.extend((await WecomMessageConverter.yiri2target(node.message_chain, bot))) + else: + content_list.append({ + "type": "text", + "content": str(msg), + }) + + return content_list + + @staticmethod + async def target2yiri(message: str, message_id: int = -1): + yiri_msg_list = [] + yiri_msg_list.append( + platform_message.Source(id=message_id, time=datetime.datetime.now()) + ) + + yiri_msg_list.append(platform_message.Plain(text=message)) + chain = platform_message.MessageChain(yiri_msg_list) + + return chain + + @staticmethod + async def target2yiri_image(picurl: str, message_id: int = -1): + yiri_msg_list = [] + yiri_msg_list.append( + platform_message.Source(id=message_id, time=datetime.datetime.now()) + ) + yiri_msg_list.append(platform_message.Image(base64=picurl)) + chain = platform_message.MessageChain(yiri_msg_list) + + return chain + + +class WecomEventConverter: + + @staticmethod + async def yiri2target( + event: platform_events.Event, bot_account_id: int, bot: WecomCSClient + ) -> WecomCSEvent: + # only for extracting user information + + if type(event) is platform_events.GroupMessage: + pass + + if type(event) is platform_events.FriendMessage: + return event.source_platform_object + + @staticmethod + async def target2yiri(event: WecomCSEvent): + """ + 将 WecomEvent 转换为平台的 FriendMessage 对象。 + + Args: + event (WecomEvent): 企业微信客服事件。 + + Returns: + platform_events.FriendMessage: 转换后的 FriendMessage 对象。 + """ + # 转换消息链 + if event.type == "text": + yiri_chain = await WecomMessageConverter.target2yiri( + event.message, event.message_id + ) + friend = platform_entities.Friend( + id=f"u{event.user_id}", + nickname=str(event.user_id), + remark="", + ) + + return platform_events.FriendMessage( + sender=friend, message_chain=yiri_chain, time=event.timestamp, source_platform_object=event + ) + elif event.type == "image": + friend = platform_entities.Friend( + id=f"u{event.user_id}", + nickname=str(event.user_id), + remark="", + ) + + yiri_chain = await WecomMessageConverter.target2yiri_image( + picurl=event.picurl, message_id=event.message_id + ) + + return platform_events.FriendMessage( + sender=friend, message_chain=yiri_chain, time=event.timestamp, source_platform_object=event + ) + + +class WecomCSAdapter(adapter.MessagePlatformAdapter): + + bot: WecomCSClient + ap: app.Application + bot_account_id: str + message_converter: WecomMessageConverter = WecomMessageConverter() + event_converter: WecomEventConverter = WecomEventConverter() + config: dict + + def __init__(self, config: dict, ap: app.Application): + self.config = config + + self.ap = ap + + required_keys = [ + "corpid", + "secret", + "token", + "EncodingAESKey", + ] + missing_keys = [key for key in required_keys if key not in config] + if missing_keys: + raise ParamNotEnoughError("企业微信客服缺少相关配置项,请查看文档或联系管理员") + + self.bot = WecomCSClient( + corpid=config["corpid"], + secret=config["secret"], + token=config["token"], + EncodingAESKey=config["EncodingAESKey"], + ) + + async def reply_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + + Wecom_event = await WecomEventConverter.yiri2target( + message_source, self.bot_account_id, self.bot + ) + content_list = await WecomMessageConverter.yiri2target(message, self.bot) + + for content in content_list: + if content["type"] == "text": + await self.bot.send_text_msg(open_kfid=Wecom_event.receiver_id,external_userid=Wecom_event.user_id,msgid=Wecom_event.message_id,content=content["content"]) + + 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: WecomCSEvent): + self.bot_account_id = event.receiver_id + try: + return await callback( + await self.event_converter.target2yiri(event), self + ) + except: + traceback.print_exc() + + if event_type == platform_events.FriendMessage: + self.bot.on_message("text")(on_message) + self.bot.on_message("image")(on_message) + elif event_type == platform_events.GroupMessage: + pass + + async def run_async(self): + async def shutdown_trigger_placeholder(): + while True: + await asyncio.sleep(1) + + await self.bot.run_task( + host="0.0.0.0", + port=self.config["port"], + shutdown_trigger=shutdown_trigger_placeholder, + ) + + async def kill(self) -> bool: + return False + + async def unregister_listener( + self, + event_type: type, + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + ): + return super().unregister_listener(event_type, callback) \ No newline at end of file diff --git a/pkg/platform/sources/wecomcs.yaml b/pkg/platform/sources/wecomcs.yaml new file mode 100644 index 00000000..fb93d0b6 --- /dev/null +++ b/pkg/platform/sources/wecomcs.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: wecomcs + label: + en_US: WeComCustomerService + zh_CN: 企业微信客服 + description: + en_US: WeComCSAdapter + zh_CN: 企业微信客服适配器 +spec: + config: + - name: port + label: + en_US: Port + zh_CN: 监听端口 + type: int + required: true + default: 2289 + - 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: "" +execution: + python: + path: ./wecomcs.py + attr: WecomCSAdapter \ No newline at end of file diff --git a/templates/platform.json b/templates/platform.json index d3f23b43..3fa4e3bf 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -103,6 +103,15 @@ "bot_token": "", "signing_secret": "", "port": 2288 + }, + { + "adapter": "wecomcs", + "enable": false, + "port": 2289, + "corpid": "", + "secret": "", + "token": "", + "EncodingAESKey": "" } ], "track-function-calls": true, From 8a9000cc675dda38ca3213dfe41bd50642395ec5 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Wed, 16 Apr 2025 15:06:47 +0800 Subject: [PATCH 50/73] chore: release v3.4.14 (#1307) * chore: release v3.4.14 * doc(README): wecom cs --- README.md | 1 + README_EN.md | 1 + README_JP.md | 1 + pkg/utils/constants.py | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e627ce3..b60c35d6 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ | QQ 个人号 | ✅ | QQ 个人号私聊、群聊 | | QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 | | 企业微信 | ✅ | | +| 企微对外客服 | ✅ | | | 个人微信 | ✅ | 使用 [Gewechat](https://github.com/Devo919/Gewechat) 接入 | | 微信公众号 | ✅ | | | 飞书 | ✅ | | diff --git a/README_EN.md b/README_EN.md index 68fd4028..d7a643ab 100644 --- a/README_EN.md +++ b/README_EN.md @@ -85,6 +85,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do | Personal QQ | ✅ | | | QQ Official API | ✅ | | | WeCom | ✅ | | +| WeComCS | ✅ | | | Personal WeChat | ✅ | Use [Gewechat](https://github.com/Devo919/Gewechat) to access | | Lark | ✅ | | | DingTalk | ✅ | | diff --git a/README_JP.md b/README_JP.md index 6966ce1f..f0644766 100644 --- a/README_JP.md +++ b/README_JP.md @@ -84,6 +84,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール | 個人QQ | ✅ | | | QQ公式API | ✅ | | | WeCom | ✅ | | +| WeComCS | ✅ | | | 個人WeChat | ✅ | [Gewechat](https://github.com/Devo919/Gewechat)を使用して接続 | | Lark | ✅ | | | DingTalk | ✅ | | diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 31d27337..451200b0 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.13.1" +semantic_version = "v3.4.14" debug_mode = False From 92e3546e8a96feefe17327d69881cecda8202cf2 Mon Sep 17 00:00:00 2001 From: WangCham <651122857@qq.com> Date: Thu, 17 Apr 2025 16:18:05 +0800 Subject: [PATCH 51/73] feat: add support for ppio --- .../modelmgr/requesters/ppiochatcmpl.py | 20 +++++++++++ .../modelmgr/requesters/ppiochatcmpl.yaml | 34 +++++++++++++++++++ templates/provider.json | 8 +++++ 3 files changed, 62 insertions(+) create mode 100644 pkg/provider/modelmgr/requesters/ppiochatcmpl.py create mode 100644 pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py new file mode 100644 index 00000000..d0149a80 --- /dev/null +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -0,0 +1,20 @@ + +from __future__ import annotations + +import openai + +from . import chatcmpl, modelscopechatcmpl +from .. import requester +from ....core import app + +class PPIOChatCompletions(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']['ppio-chat-completions'] \ No newline at end of file diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml b/pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml new file mode 100644 index 00000000..555f6416 --- /dev/null +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: LLMAPIRequester +metadata: + name: ppio-chat-completions + label: + en_US: ppio + zh_CN: 派欧云 +spec: + config: + - name: base-url + label: + en_US: Base URL + zh_CN: 基础 URL + type: string + required: true + default: "https://api.ppinfra.com/v3/openai" + - 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: ./ppiochatcmpl.py + attr: PPIOChatCompletions \ No newline at end of file diff --git a/templates/provider.json b/templates/provider.json index 135c086d..75b107f5 100644 --- a/templates/provider.json +++ b/templates/provider.json @@ -34,6 +34,9 @@ ], "modelscope": [ "xxxxxxxx" + ], + "ppio": [ + "xxxxxxxx" ] }, "requester": { @@ -103,6 +106,11 @@ "base-url": "https://api-inference.modelscope.cn/v1", "args": {}, "timeout": 120 + }, + "ppio-chat-completions": { + "base-url": "https://api.ppinfra.com/v3/openai", + "args": {}, + "timeout": 120 } }, "model": "gpt-4o", From 43a6492cab27bba8a3f7f35c65dd182971f8363e Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 17 Apr 2025 16:32:19 +0800 Subject: [PATCH 52/73] chore: migration for ppio config --- .../m039_modelscope_cfg_completion.py | 4 +-- pkg/core/migrations/m040_ppip_config.py | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 pkg/core/migrations/m040_ppip_config.py diff --git a/pkg/core/migrations/m039_modelscope_cfg_completion.py b/pkg/core/migrations/m039_modelscope_cfg_completion.py index 63e9c6d8..8e574911 100644 --- a/pkg/core/migrations/m039_modelscope_cfg_completion.py +++ b/pkg/core/migrations/m039_modelscope_cfg_completion.py @@ -3,9 +3,9 @@ from __future__ import annotations from .. import migration -@migration.migration_class("modelscope-config-completion", 4) +@migration.migration_class("modelscope-config-completion", 39) class ModelScopeConfigCompletionMigration(migration.Migration): - """OpenAI配置迁移 + """ModelScope配置迁移 """ async def need_migrate(self) -> bool: diff --git a/pkg/core/migrations/m040_ppip_config.py b/pkg/core/migrations/m040_ppip_config.py new file mode 100644 index 00000000..cd218d87 --- /dev/null +++ b/pkg/core/migrations/m040_ppip_config.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from .. import migration + + +@migration.migration_class("ppio-config", 40) +class PPIOConfigMigration(migration.Migration): + """PPIO配置迁移 + """ + + async def need_migrate(self) -> bool: + """判断当前环境是否需要运行此迁移 + """ + return 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester'] \ + or 'ppio' not in self.ap.provider_cfg.data['keys'] + + async def run(self): + """执行迁移 + """ + if 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester']: + self.ap.provider_cfg.data['requester']['ppio-chat-completions'] = { + 'base-url': 'https://api.ppinfra.com/v3/openai', + 'args': {}, + 'timeout': 120, + } + + if 'ppio' not in self.ap.provider_cfg.data['keys']: + self.ap.provider_cfg.data['keys']['ppio'] = [] + + await self.ap.provider_cfg.dump_config() From b6f312325ffb0bf0e6e79e2918feef9080fc1b8d Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 17 Apr 2025 16:33:35 +0800 Subject: [PATCH 53/73] chore: fix --- .../migrations/{m040_ppip_config.py => m040_ppio_config.py} | 0 pkg/core/stages/migrate.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/core/migrations/{m040_ppip_config.py => m040_ppio_config.py} (100%) diff --git a/pkg/core/migrations/m040_ppip_config.py b/pkg/core/migrations/m040_ppio_config.py similarity index 100% rename from pkg/core/migrations/m040_ppip_config.py rename to pkg/core/migrations/m040_ppio_config.py diff --git a/pkg/core/stages/migrate.py b/pkg/core/stages/migrate.py index f8e6965d..8d4366bf 100644 --- a/pkg/core/stages/migrate.py +++ b/pkg/core/stages/migrate.py @@ -12,7 +12,7 @@ from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_conf from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message -from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown, m039_modelscope_cfg_completion +from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown, m039_modelscope_cfg_completion, m040_ppio_config @stage.stage_class("MigrationStage") From 4d53b3cb062eccfa659d6875feeb599fbf3c17ba Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Fri, 18 Apr 2025 20:25:50 +0800 Subject: [PATCH 54/73] doc: update README doc: update README --- README.md | 3 ++- README_EN.md | 3 ++- README_JP.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b60c35d6..355eec6e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ RockChinQ%2FLangBot | Trendshift -项目主页 | +项目主页功能介绍部署文档常见问题 | @@ -110,6 +110,7 @@ | [Anthropic](https://www.anthropic.com/) | ✅ | | | [xAI](https://x.ai/) | ✅ | | | [智谱AI](https://open.bigmodel.cn/) | ✅ | | +| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 | | [Dify](https://dify.ai) | ✅ | LLMOps 平台 | | [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 | | [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 | diff --git a/README_EN.md b/README_EN.md index d7a643ab..6a059810 100644 --- a/README_EN.md +++ b/README_EN.md @@ -7,7 +7,7 @@ RockChinQ%2FLangBot | Trendshift -Home | +HomeFeaturesDeploymentFAQ | @@ -108,6 +108,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do | [xAI](https://x.ai/) | ✅ | | | [Zhipu AI](https://open.bigmodel.cn/) | ✅ | | | [Dify](https://dify.ai) | ✅ | LLMOps platform | +| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform | | [Ollama](https://ollama.com/) | ✅ | Local LLM running platform | | [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform | | [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) | diff --git a/README_JP.md b/README_JP.md index f0644766..36eb72d3 100644 --- a/README_JP.md +++ b/README_JP.md @@ -7,7 +7,7 @@ RockChinQ%2FLangBot | Trendshift -ホーム | +ホーム機能デプロイFAQ | @@ -106,6 +106,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール | [Anthropic](https://www.anthropic.com/) | ✅ | | | [xAI](https://x.ai/) | ✅ | | | [Zhipu AI](https://open.bigmodel.cn/) | ✅ | | +| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム | | [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム | | [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム | | [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム | From 92acaf6c2751436e7db53d7725cc71adaf44e793 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sat, 19 Apr 2025 22:30:22 +0800 Subject: [PATCH 55/73] chore: release 3.4.14.1 (#1315) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 451200b0..6a265424 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.14" +semantic_version = "v3.4.14.1" debug_mode = False From 8a6d9d76da27dafa13e40f238c1cdff34089c5c5 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sun, 20 Apr 2025 13:41:02 +0800 Subject: [PATCH 56/73] perf: reduce newline in think tag converting (#1319) --- pkg/provider/modelmgr/requesters/chatcmpl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 14f83146..f83a4909 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -65,7 +65,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): # deepseek的reasoner模型 if reasoning_content is not None: - chatcmpl_message['content'] = "\n" + reasoning_content + "\n\n\n"+ chatcmpl_message['content'] + chatcmpl_message['content'] = "\n" + reasoning_content + "\n\n"+ chatcmpl_message['content'] message = llm_entities.Message(**chatcmpl_message) From 577dc0d175904d4323ccc002e240e34a04198046 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 23 Apr 2025 02:25:58 +0800 Subject: [PATCH 57/73] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E5=A4=84?= =?UTF-8?q?=E7=90=86=E8=AF=AD=E9=9F=B3=E6=B6=88=E6=81=AF=E5=92=8C=E5=A4=84?= =?UTF-8?q?=E7=90=86=E7=BE=A4=E8=81=8A=E5=9B=BE=E7=89=87=E6=B6=88=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=8F=91=E9=80=81=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index cac732a1..ba8f0490 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -58,6 +58,8 @@ class GewechatMessageConverter(adapter.MessageConverter): elif isinstance(component, platform_message.WeChatLink): content_list.append({'type': 'WeChatLink', 'link_title': component.link_title, 'link_desc': component.link_desc, 'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url}) + elif isinstance(component, platform_message.WeChatForwardLink): + content_list.append({'type': 'WeChatLink', 'xml_data': component.xml_data}) elif isinstance(component, platform_message.Voice): @@ -108,6 +110,9 @@ class GewechatMessageConverter(adapter.MessageConverter): elif message["Data"]["MsgType"] == 3: image_xml = message["Data"]["Content"]["string"] + if image_xml.startswith('wxid'): # 此处处理群聊发送图片会有微信id开头 + xml_list = image_xml.split('\n')[2:] + image_xml = '\n'.join(xml_list) if not image_xml: return platform_message.MessageChain([ platform_message.Plain(text="[图片内容为空]") @@ -135,10 +140,15 @@ class GewechatMessageConverter(adapter.MessageConverter): platform_message.Plain(text=f"[图片处理失败]") ]) elif message["Data"]["MsgType"] == 34: - audio_base64 = message["Data"]["ImgBuf"]["buffer"] - return platform_message.MessageChain( - [platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")] - ) + try: + audio_base64 = message["Data"]["ImgBuf"]["buffer"] + return platform_message.MessageChain( + [platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")] + ) + except Exception as e: + return platform_message.MessageChain( + [platform_message.Plain(text="[无法解析群聊语音的消息]")] # 小测了一下,免费版拿不到群聊语音消息的base64,或者用什么办法解析xml里的url? + ) elif message["Data"]["MsgType"] == 49: # 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 try: @@ -389,6 +399,11 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): ,title=msg['link_title'], desc=msg['link_desc'] , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) + elif msg['type'] == 'WeChatForwardMiniPrograms': + self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) + elif msg['type'] == 'voice': + self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['voice_url'],voice_duration=msg['length']) + async def reply_message( @@ -437,6 +452,9 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id , title=msg['link_title'], desc=msg['link_desc'] , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) + elif msg['type'] == 'voice': + self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['voice_url'], + voice_duration=msg['length']) async def is_muted(self, group_id: int) -> bool: pass From 8ca714853a77962d33d4374206db6fb8ee7199a1 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 23 Apr 2025 02:28:39 +0800 Subject: [PATCH 58/73] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E8=BD=AC=E5=8F=91=E9=93=BE=E6=8E=A5=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/types/message.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index b6b87596..857a10f2 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -860,3 +860,10 @@ class WeChatLink(MessageComponent): """链接略缩图""" link_thumb_url: str = '' + +class WeChatForwardLink(MessageComponent): + """转发链接。个人微信专用组件。""" + type: str = 'WeChatLink' + """xml数据""" + xml_data: str + From 5c26ce215b314108611e6a65cd3df69932fd4d41 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 23 Apr 2025 02:36:36 +0800 Subject: [PATCH 59/73] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E8=BD=AC?= =?UTF-8?q?=E5=8F=91=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index ba8f0490..796cabe2 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -59,7 +59,7 @@ class GewechatMessageConverter(adapter.MessageConverter): content_list.append({'type': 'WeChatLink', 'link_title': component.link_title, 'link_desc': component.link_desc, 'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url}) elif isinstance(component, platform_message.WeChatForwardLink): - content_list.append({'type': 'WeChatLink', 'xml_data': component.xml_data}) + content_list.append({'type': 'WeChatForwardLink', 'xml_data': component.xml_data}) elif isinstance(component, platform_message.Voice): @@ -399,8 +399,8 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): ,title=msg['link_title'], desc=msg['link_desc'] , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) - elif msg['type'] == 'WeChatForwardMiniPrograms': - self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) + elif msg['type'] == 'WeChatForwardLink': + self.bot.forward_url(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) elif msg['type'] == 'voice': self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['voice_url'],voice_duration=msg['length']) @@ -452,6 +452,8 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id , title=msg['link_title'], desc=msg['link_desc'] , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) + elif msg['type'] == 'WeChatForwardLink': + self.bot.forward_url(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) elif msg['type'] == 'voice': self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['voice_url'], voice_duration=msg['length']) From 446546b69f58124934914218cfb7caecbc9b3948 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Wed, 23 Apr 2025 16:55:52 +0800 Subject: [PATCH 60/73] fix(dify runner): response message event incorrect when using agent app (#1325) --- .gitignore | 4 +- pkg/provider/runners/difysvapi.py | 93 +++++++++++++++++-------------- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 1c2147a8..71e0ef8e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,5 @@ botpy.log* /poc /libs/wecom_api/test.py /venv -/jp-tyo-churros-05.rockchin.top -test.py \ No newline at end of file +test.py +/web_ui \ No newline at end of file diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index a33a3c0b..ef35e829 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -164,12 +164,14 @@ class DifyServiceAPIRunner(runner.RequestRunner): for image_id in image_ids ] - ignored_events = ["agent_message"] + ignored_events = [] inputs = {} inputs.update(query.variables) + pending_agent_message = '' + async for chunk in self.dify_client.chat_messages( inputs=inputs, query=plain_text, @@ -183,50 +185,55 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk["event"] in ignored_events: continue - if chunk["event"] == "agent_thought": - - if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 - continue - - if chunk['thought'].strip() != '': # 文字回复内容 - msg = llm_entities.Message( - role="assistant", - content=chunk["thought"], - ) - yield msg - - if chunk['tool']: - msg = llm_entities.Message( - role="assistant", - tool_calls=[ - llm_entities.ToolCall( - id=chunk['id'], - type="function", - function=llm_entities.FunctionCall( - name=chunk["tool"], - arguments=json.dumps({}), - ), - ) - ], - ) - yield msg - if chunk['event'] == 'message_file': - - if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': - - base_url = self.dify_client.base_url - - if base_url.endswith('/v1'): - base_url = base_url[:-3] - - image_url = base_url + chunk['url'] + if chunk['event'] == 'agent_message': + pending_agent_message += chunk['answer'] + else: + if pending_agent_message.strip() != '': + pending_agent_message = pending_agent_message.replace('Action:', '') yield llm_entities.Message( role="assistant", - content=[llm_entities.ContentElement.from_image_url(image_url)], + content=self._try_convert_thinking(pending_agent_message), ) - if chunk['event'] == 'error': - raise errors.DifyAPIError("dify 服务错误: " + chunk['message']) + pending_agent_message = '' + + if chunk["event"] == "agent_thought": + + if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 + continue + + if chunk['tool']: + msg = llm_entities.Message( + role="assistant", + tool_calls=[ + llm_entities.ToolCall( + id=chunk['id'], + type="function", + function=llm_entities.FunctionCall( + name=chunk["tool"], + arguments=json.dumps({}), + ), + ) + ], + ) + yield msg + if chunk['event'] == 'message_file': + + if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': + + base_url = self.dify_client.base_url + + if base_url.endswith('/v1'): + base_url = base_url[:-3] + + image_url = base_url + chunk['url'] + + yield llm_entities.Message( + role="assistant", + content=[llm_entities.ContentElement.from_image_url(image_url)], + ) + if chunk['event'] == 'error': + raise errors.DifyAPIError("dify 服务错误: " + chunk['message']) query.session.using_conversation.uuid = chunk["conversation_id"] @@ -303,11 +310,11 @@ class DifyServiceAPIRunner(runner.RequestRunner): msg = llm_entities.Message( role="assistant", - content=chunk["data"]["outputs"][ + content=self._try_convert_thinking(chunk["data"]["outputs"][ self.ap.provider_cfg.data["dify-service-api"]["workflow"][ "output-key" ] - ], + ]), ) yield msg From 8af401eea4f3f8827d42a72c394073f66555fc0d Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Wed, 23 Apr 2025 17:34:00 +0800 Subject: [PATCH 61/73] chore: release v3.4.14.2 (#1326) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 6a265424..631a04d2 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.14.1" +semantic_version = "v3.4.14.2" debug_mode = False From 00cafb1188e5d9995fc75acfcbb9ffd9dab7fb0a Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 24 Apr 2025 00:00:49 +0800 Subject: [PATCH 62/73] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E6=89=8B=E8=AF=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/types/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index 857a10f2..20ca399b 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -863,7 +863,7 @@ class WeChatLink(MessageComponent): class WeChatForwardLink(MessageComponent): """转发链接。个人微信专用组件。""" - type: str = 'WeChatLink' + type: str = 'WeChatForwardLink' """xml数据""" xml_data: str From 112f99d6d9ab33610ced7c2a76ea755dae76a01a Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 24 Apr 2025 21:12:30 +0800 Subject: [PATCH 63/73] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=94=B6=E5=88=B0?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=EF=BC=8C=E5=85=AC=E4=BC=97=E5=8F=B7?= =?UTF-8?q?=E8=BD=AC=E8=B4=A6=E7=AD=89=E6=B6=88=E6=81=AF=E6=97=B6=E5=B0=86?= =?UTF-8?q?=E5=85=B6=E9=80=9A=E8=BF=87unknown=E4=BC=A0=E9=80=92=E5=87=BA?= =?UTF-8?q?=E6=9D=A5=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8Dvoice=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=86=99=E9=94=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 796cabe2..da4a6b99 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -151,8 +151,9 @@ class GewechatMessageConverter(adapter.MessageConverter): ) elif message["Data"]["MsgType"] == 49: # 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 + content = message["Data"]["Content"]["string"] try: - content = message["Data"]["Content"]["string"] + # content = message["Data"]["Content"]["string"] # 有三种可能的消息结构weid开头,私聊直接和直接 if content.startswith('wxid'): xml_list = content.split('\n')[2:] @@ -192,24 +193,29 @@ class GewechatMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(message_list) elif data_type == '51': return platform_message.MessageChain( - [platform_message.Plain(text=f'[视频号消息]')] + [ # platform_message.Plain(text=f'[视频号消息]'), + platform_message.Unknown(text=content)] ) # print(content_data) elif data_type == '2000': return platform_message.MessageChain( - [platform_message.Plain(text=f'[转账消息]')] + [ # platform_message.Plain(text=f'[转账消息]'), + platform_message.Unknown(text=content)] ) elif data_type == '2001': return platform_message.MessageChain( - [platform_message.Plain(text=f'[红包消息]')] + [ # platform_message.Plain(text=f'[红包消息]'), + platform_message.Unknown(text=content)] ) elif data_type == '5': return platform_message.MessageChain( - [platform_message.Plain(text=f'[公众号消息]')] + [ # platform_message.Plain(text=f'[公众号消息]'), + platform_message.Unknown(text=content)] ) elif data_type == '33' or data_type == '36': return platform_message.MessageChain( - [platform_message.Plain(text=f'[小程序消息]')] + [ # platform_message.Plain(text=f'[小程序消息]'), + platform_message.Unknown(text=content)] ) # print(data_type.text) else: @@ -219,8 +225,8 @@ class GewechatMessageConverter(adapter.MessageConverter): content_bytes = content.encode('utf-8') decoded_content = base64.b64decode(content_bytes) return platform_message.MessageChain( - [platform_message.Unknown(content=decoded_content)] - ) + [platform_message.Unknown(text=decoded_content)] + ) # 不对劲,十分有九分不对劲这里这么写不对吧 except Exception as e: return platform_message.MessageChain( [platform_message.Plain(text=content)] @@ -228,7 +234,8 @@ class GewechatMessageConverter(adapter.MessageConverter): except Exception as e: print(f"Error processing type 49 message: {str(e)}") return platform_message.MessageChain( - [platform_message.Plain(text="[无法解析的消息]")] + [ # platform_message.Plain(text="[无法解析的消息]"), + platform_message.Unknown(text=content)] ) class GewechatEventConverter(adapter.EventConverter): @@ -402,7 +409,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): elif msg['type'] == 'WeChatForwardLink': self.bot.forward_url(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) elif msg['type'] == 'voice': - self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['voice_url'],voice_duration=msg['length']) + self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['url'],voice_duration=msg['length']) @@ -455,7 +462,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): elif msg['type'] == 'WeChatForwardLink': self.bot.forward_url(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) elif msg['type'] == 'voice': - self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['voice_url'], + self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['url'], voice_duration=msg['length']) async def is_muted(self, group_id: int) -> bool: From cb7f7b80df05af17a9ab0c7bca0fb2b3fdc4172d Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 24 Apr 2025 22:05:54 +0800 Subject: [PATCH 64/73] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=9C=89=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E5=B0=86=E6=95=B0=E6=8D=AE=E5=BD=93=E4=BD=9Cbase64?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=B9=B6=E9=80=9A=E8=BF=87unknown=E4=B8=ADco?= =?UTF-8?q?ntent(=E4=BD=86=E6=98=AF=E6=B2=A1=E6=9C=89=E5=95=8A)=E4=BC=A0?= =?UTF-8?q?=E9=80=92=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/gewechat.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index da4a6b99..06e1378b 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -219,18 +219,20 @@ class GewechatMessageConverter(adapter.MessageConverter): ) # print(data_type.text) else: - - - try: - content_bytes = content.encode('utf-8') - decoded_content = base64.b64decode(content_bytes) - return platform_message.MessageChain( - [platform_message.Unknown(text=decoded_content)] - ) # 不对劲,十分有九分不对劲这里这么写不对吧 - except Exception as e: - return platform_message.MessageChain( - [platform_message.Plain(text=content)] + return platform_message.MessageChain( + [platform_message.Unknown(text=content)] ) + + # try: + # content_bytes = content.encode('utf-8') + # decoded_content = base64.b64decode(content_bytes) + # return platform_message.MessageChain( + # [platform_message.Unknown(content=decoded_content)] + # ) # unknown中没有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( From efed9f334827280f139a714824addde84baf39e7 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" <1010553892@qq.com> Date: Sat, 26 Apr 2025 21:08:55 +0800 Subject: [PATCH 65/73] Merge pull request #1338 from RockChinQ/RockChinQ-patch-1 Update README_EN.md --- README_EN.md | 1 + README_JP.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README_EN.md b/README_EN.md index 6a059810..d9906ac9 100644 --- a/README_EN.md +++ b/README_EN.md @@ -45,6 +45,7 @@ > > - Before you start deploying in any way, please read the [New User Guide](https://docs.langbot.app/insight/guide.html). > - All documentation is in Chinese, we will provide i18n version in the near future. +> - Read [the auto-generated wiki on DeepWiki](https://deepwiki.com/RockChinQ/LangBot). #### Docker Compose Deployment diff --git a/README_JP.md b/README_JP.md index 36eb72d3..bb7f6208 100644 --- a/README_JP.md +++ b/README_JP.md @@ -44,6 +44,7 @@ > > - どのデプロイ方法を始める前に、必ず[新規ユーザーガイド](https://docs.langbot.app/insight/guide.html)をお読みください。 > - すべてのドキュメントは中国語で提供されています。近い将来、i18nバージョンを提供する予定です。 +> - Read [the auto-generated wiki on DeepWiki](https://deepwiki.com/RockChinQ/LangBot)。 #### Docker Compose デプロイ From ac500266f3c05e4c017f94a21eeb71ba1551f0b2 Mon Sep 17 00:00:00 2001 From: shinelin Date: Sun, 27 Apr 2025 20:48:55 +0800 Subject: [PATCH 66/73] =?UTF-8?q?feat(gewechat):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=86=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84+fix=E7=BE=A4?= =?UTF-8?q?=E8=81=8A=E8=89=BE=E7=89=B9=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=B6=88=E6=81=AF=E7=B1=BB=E5=9E=8B=20(#1336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(gewechat): 优化了代码结构+fix群聊艾特逻辑,新增消息类型 * feat(gewechat): 移除不合理的message定义,优化GewechatMessageConverter * bugfix(gewechat): fix typo * feat(gewechat): 去掉多余日志+公众号消息和文件消息转发+msg_source取空异常fix * bugfix(message):删除image中的xml定义 * bugfix(message): fix typo --- pkg/platform/sources/gewechat.py | 441 ++++++++++++++++++------------- pkg/platform/types/message.py | 33 ++- 2 files changed, 293 insertions(+), 181 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 06e1378b..8c1b7d6f 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -26,7 +26,7 @@ from ..types import events as platform_events from ..types import entities as platform_entities from ...utils import image import xml.etree.ElementTree as ET - +from typing import Optional, List, Tuple class GewechatMessageConverter(adapter.MessageConverter): @@ -60,13 +60,18 @@ class GewechatMessageConverter(adapter.MessageConverter): 'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url}) elif isinstance(component, platform_message.WeChatForwardLink): content_list.append({'type': 'WeChatForwardLink', 'xml_data': component.xml_data}) - - elif isinstance(component, platform_message.Voice): - content_list.append({"type": "voice", "url": component.url, "length": component.length}) + content_list.append({"type": "voice", "url": component.url, "length": component.length}) + elif isinstance(component, platform_message.WeChatForwardImage): + content_list.append({'type': 'WeChatForwardImage', 'xml_data': component.xml_data}) + elif isinstance(component, platform_message.WeChatForwardFile): + content_list.append({'type': 'WeChatForwardFile', 'xml_data': component.xml_data}) + elif isinstance(component, platform_message.WeChatAppMsg): + content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg}) elif isinstance(component, platform_message.Forward): for node in component.node_list: - content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) + if node.message_chain: + content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) return content_list @@ -76,49 +81,38 @@ class GewechatMessageConverter(adapter.MessageConverter): bot_account_id: str ) -> platform_message.MessageChain: - - - if message["Data"]["MsgType"] == 1: - # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 - regex = re.compile(r"^wxid_.*:") - # print(message) - - line_split = message["Data"]["Content"]["string"].split("\n") - - if len(line_split) > 0 and regex.match(line_split[0]): - message["Data"]["Content"]["string"] = "\n".join(line_split[1:]) - - - # 正则表达式模式,匹配'@'后跟任意数量的非空白字符 - pattern = r'@\S+' - at_string = f"@{bot_account_id}" - content_list = [] - if at_string in message["Data"]["Content"]["string"]: + # 预处理 + content_list = [] + ats_bot = False + raw_content = message["Data"]["Content"]["string"] + is_group_message = self.__is_group_message(message) + if is_group_message: + ats_bot = self.__ats_bot(message, bot_account_id) + # 优先处理艾特全体成员, + if "@所有人" in raw_content: ## at全员时候传入atll不当作at自己 + content_list.append(platform_message.AtAll()) + elif ats_bot: content_list.append(platform_message.At(target=bot_account_id)) - content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, '', 1))) - # 更优雅的替换改名后@机器人,仅仅限于单独AT的情况 - elif "PushContent" in message['Data'] and '在群聊中@了你' in message["Data"]["PushContent"]: - if '@所有人' in message["Data"]["Content"]["string"]: # at全员时候传入atll不当作at自己 - content_list.append(platform_message.AtAll()) - else: - content_list.append(platform_message.At(target=bot_account_id)) - content_list.append(platform_message.Plain(re.sub(pattern, '', message["Data"]["Content"]["string"]))) - else: - content_list = [platform_message.Plain(message["Data"]["Content"]["string"])] + raw_content, sender_id = self.__extract_content_and_sender(raw_content) + # 消息类型 + msg_type = message["Data"]["MsgType"] + + # 文本消息 + if msg_type == 1: + # 文本清洗,仅替换群文本中的@文本[空格],的文本 + if is_group_message and ats_bot: + pattern = r'@\S+' + raw_content = re.sub(pattern, '',raw_content) + content_list.append(platform_message.Plain(raw_content)) return platform_message.MessageChain(content_list) - - elif message["Data"]["MsgType"] == 3: - image_xml = message["Data"]["Content"]["string"] - if image_xml.startswith('wxid'): # 此处处理群聊发送图片会有微信id开头 - xml_list = image_xml.split('\n')[2:] - image_xml = '\n'.join(xml_list) + + # 图像 + elif msg_type == 3: + image_xml = raw_content # 已经去除群聊消息前缀 if not image_xml: - return platform_message.MessageChain([ - platform_message.Plain(text="[图片内容为空]") - ]) - - + content_list.append(platform_message.Plain(text="[图片内容为空]")) + return platform_message.MessageChain(content_list) try: base64_str, image_format = await image.get_gewechat_image_base64( gewechat_url=self.config["gewechat_url"], @@ -129,17 +123,20 @@ class GewechatMessageConverter(adapter.MessageConverter): image_type=2, ) - return platform_message.MessageChain([ - platform_message.Image( - base64=f"data:image/{image_format};base64,{base64_str}" - ) - ]) + content_list.append(platform_message.Image( + base64=f"data:image/{image_format};base64,{base64_str}" + )) + # 消息链中加一个WeChatForwardImage的xml用于转发 + content_list.append(platform_message.WeChatForwardImage( + xml_data = image_xml + )) + return platform_message.MessageChain(content_list) except Exception as e: print(f"处理图片消息失败: {str(e)}") - return platform_message.MessageChain([ - platform_message.Plain(text=f"[图片处理失败]") - ]) - elif message["Data"]["MsgType"] == 34: + content_list.append(platform_message.Plain(text=f"[图片处理失败]")) + return platform_message.MessageChain(content_list) + # 语音消息 + elif msg_type == 34: try: audio_base64 = message["Data"]["ImgBuf"]["buffer"] return platform_message.MessageChain( @@ -149,23 +146,20 @@ class GewechatMessageConverter(adapter.MessageConverter): return platform_message.MessageChain( [platform_message.Plain(text="[无法解析群聊语音的消息]")] # 小测了一下,免费版拿不到群聊语音消息的base64,或者用什么办法解析xml里的url? ) - elif message["Data"]["MsgType"] == 49: + finally: + return platform_message.MessageChain(content_list) + elif msg_type == 49: # 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 - content = message["Data"]["Content"]["string"] - try: - # content = message["Data"]["Content"]["string"] - # 有三种可能的消息结构weid开头,私聊直接和直接 - if content.startswith('wxid'): - xml_list = content.split('\n')[2:] + try: + # 下方是移除 bool: + ats_bot = False + try: + to_user_name = message['Wxid'] # 接收方: 所属微信的wxid + raw_content = message["Data"]["Content"]["string"] # 原始消息内容 + # step 1 + ats_bot = ats_bot or (f"@{bot_account_id}" in raw_content) + # step 2 + push_content = message.get('Data', {}).get('PushContent', '') + ats_bot = ats_bot or ('在群聊中@了你' in push_content) + # step 3 + msg_source = message.get('Data', {}).get('MsgSource', '') or '' + if len(msg_source) > 0: + msg_source_data = ET.fromstring(msg_source) + at_user_list = msg_source_data.findtext("atuserlist") or "" + ats_bot = ats_bot or (to_user_name in at_user_list) + except Exception as e: + print(f"__ats_bot got except: {e}") + finally: + return ats_bot + + # 提取一下content前面的sender_id, 和去掉前缀的内容 + def __extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: + try: + # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 + # add: 有些用户的wxid不是上述格式。换成user_name: + regex = re.compile(r"^[a-zA-Z0-9_\-]{5,20}:") + line_split = raw_content.split("\n") + if len(line_split) > 0 and regex.match(line_split[0]): + raw_content = "\n".join(line_split[1:]) + sender_id = line_split[0].strip(":") + return raw_content, sender_id + except Exception as e: + print(f"__extract_content_and_sender got except: {e}") + finally: + return raw_content, None + + # 是否是群消息 + def __is_group_message(self, message: dict)->bool: + from_user_name = message['Data']['FromUserName']['string'] + return from_user_name.endswith("@chatroom") class GewechatEventConverter(adapter.EventConverter): @@ -365,55 +423,118 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): return 'ok' + async def _handle_message( + self, + message: platform_message.MessageChain, + target_id: str + ): + """统一消息处理核心逻辑""" + content_list = await self.message_converter.yiri2target(message) + at_targets = [item["target"] for item in content_list if item["type"] == "at"] + + # 处理@逻辑 + at_targets = at_targets or [] + member_info = [] + if at_targets: + member_info = self.bot.get_chatroom_member_detail( + self.config["app_id"], + target_id, + at_targets[::-1] + )["data"] + + # 处理消息组件 + for msg in content_list: + # 文本消息处理@ + if msg['type'] == 'text' and at_targets: + for member in member_info: + msg['content'] = f'@{member["nickName"]} {msg["content"]}' + + # 统一消息派发 + handler_map = { + 'text': lambda msg: self.bot.post_text( + app_id=self.config['app_id'], + to_wxid=target_id, + content=msg['content'], + ats=",".join(at_targets) + ), + 'image': lambda msg: self.bot.post_image( + app_id=self.config['app_id'], + to_wxid=target_id, + img_url=msg["image"] + ), + 'WeChatForwardMiniPrograms': lambda msg: self.bot.forward_mini_app( + app_id=self.config['app_id'], + to_wxid=target_id, + xml=msg['xml_data'], + cover_img_url=msg.get('image_url') + ), + 'WeChatEmoji': lambda msg: self.bot.post_emoji( + app_id=self.config['app_id'], + to_wxid=target_id, + emoji_md5=msg['emoji_md5'], + emoji_size=msg['emoji_size'] + ), + 'WeChatLink': lambda msg: self.bot.post_link( + app_id=self.config['app_id'], + to_wxid=target_id, + title=msg['link_title'], + desc=msg['link_desc'], + link_url=msg['link_url'], + thumb_url=msg['link_thumb_url'], + ), + 'WeChatMiniPrograms': lambda msg: self.bot.post_mini_app( + app_id=self.config['app_id'], + to_wxid=target_id, + mini_app_id=msg['mini_app_id'], + display_name=msg['display_name'], + page_path=msg['page_path'], + cover_img_url=msg['cover_img_url'], + title=msg['title'], + user_name=msg['user_name'] + ), + 'WeChatForwardLink': lambda msg: self.bot.forward_url( + app_id=self.config['app_id'], + to_wxid=target_id, + xml=msg['xml_data'] + ), + 'WeChatForwardImage': lambda msg: self.bot.forward_image( + app_id=self.config['app_id'], + to_wxid=target_id, + xml=msg['xml_data'] + ), + 'WeChatForwardFile': lambda msg: self.bot.forward_file( + app_id=self.config['app_id'], + to_wxid=target_id, + xml=msg['xml_data'] + ), + 'voice': lambda msg: self.bot.post_voice( + app_id=self.config['app_id'], + to_wxid=target_id, + voice_url=msg['url'], + voice_duration=msg['length'] + ), + 'WeChatAppMsg': lambda msg: self.bot.post_app_msg( + app_id=self.config['app_id'], + to_wxid=target_id, + appmsg=msg['app_msg'] + ), + 'at': lambda msg: None + } + + if handler := handler_map.get(msg['type']): + handler(msg) + else: + self.ap.logger.warning(f"未处理的消息类型: {msg['type']}") + continue + async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain ): - 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: - # at主动发送消息 - if msg['type'] == 'text': - if ats: - member_info = self.bot.get_chatroom_member_detail( - self.config["app_id"], - target_id, - ats[::-1] - )["data"] - - for member in member_info: - msg['content'] = f'@{member["nickName"]} {msg["content"]}' - self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id, content=msg['content'], - ats=",".join(ats)) - - elif msg['type'] == 'image': - - self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) - elif msg['type'] == 'WeChatMiniPrograms': - self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id'] - , display_name=msg['display_name'], page_path=msg['page_path'] - , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) - elif msg['type'] == 'WeChatForwardMiniPrograms': - self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) - elif msg['type'] == 'WeChatEmoji': - self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, - emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) - elif msg['type'] == 'WeChatLink': - self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id - ,title=msg['link_title'], desc=msg['link_desc'] - , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) - - elif msg['type'] == 'WeChatForwardLink': - self.bot.forward_url(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) - elif msg['type'] == 'voice': - self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['url'],voice_duration=msg['length']) - - + """主动发送消息""" + return await self._handle_message(message, target_id) async def reply_message( self, @@ -421,51 +542,10 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False ): - content_list = await self.message_converter.yiri2target(message) - - ats = [item["target"] for item in content_list if item["type"] == "at"] - target_id = message_source.source_platform_object["Data"]["FromUserName"]["string"] - - for msg in content_list: - if msg["type"] == "text": - - if ats: - member_info = self.bot.get_chatroom_member_detail( - self.config["app_id"], - message_source.source_platform_object["Data"]["FromUserName"]["string"], - ats[::-1] - )["data"] - - for member in member_info: - msg['content'] = f'@{member["nickName"]} {msg["content"]}' - - self.bot.post_text( - app_id=self.config["app_id"], - to_wxid=message_source.source_platform_object["Data"]["FromUserName"]["string"], - content=msg["content"], - ats=",".join(ats) - ) - elif msg['type'] == 'image': - - self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"]) - elif msg['type'] == 'WeChatMiniPrograms': - self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id'] - , display_name=msg['display_name'], page_path=msg['page_path'] - , cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name']) - elif msg['type'] == 'WeChatForwardMiniPrograms': - self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url']) - elif msg['type'] == 'WeChatEmoji': - self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id, - emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']) - elif msg['type'] == 'WeChatLink': - self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id - , title=msg['link_title'], desc=msg['link_desc'] - , link_url=msg['link_url'], thumb_url=msg['link_thumb_url']) - elif msg['type'] == 'WeChatForwardLink': - self.bot.forward_url(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data']) - elif msg['type'] == 'voice': - self.bot.post_voice(app_id=self.config['app_id'], to_wxid=target_id, voice_url=msg['url'], - voice_duration=msg['length']) + """回复消息""" + if message_source.source_platform_object: + target_id = message_source.source_platform_object["Data"]["FromUserName"]["string"] + return await self._handle_message(message, target_id) async def is_muted(self, group_id: int) -> bool: pass @@ -519,8 +599,13 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): time.sleep(2) - ret = self.bot.set_callback(self.config["token"], self.config["callback_url"]) - print('设置 Gewechat 回调:', ret) + try: + # gewechat-server容器重启, token会变,但是还会登录成功 + # 换新token也会收不到回调,要重新登陆下。 + ret = self.bot.set_callback(self.config["token"], self.config["callback_url"]) + except Exception as e: + raise Exception(f"设置 Gewechat 回调失败, token失效: {e}") + threading.Thread(target=gewechat_login_process).start() diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index 20ca399b..0396dad9 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -1,16 +1,15 @@ import itertools import logging +import typing from datetime import datetime from enum import Enum from pathlib import Path -import typing import pydantic.v1 as pydantic from . import entities as platform_entities from .base import PlatformBaseModel, PlatformIndexedMetaclass, PlatformIndexedModel - logger = logging.getLogger(__name__) @@ -642,7 +641,8 @@ class Unknown(MessageComponent): """消息组件类型。""" text: str """文本。""" - + def __str__(self): + return f'Unknown Message: {self.text}' class Voice(MessageComponent): """语音。""" @@ -837,6 +837,8 @@ class WeChatForwardMiniPrograms(MessageComponent): xml_data: str """首页图片""" image_url: typing.Optional[str] = None + def __str__(self): + return self.xml_data class WeChatEmoji(MessageComponent): @@ -866,4 +868,29 @@ class WeChatForwardLink(MessageComponent): type: str = 'WeChatForwardLink' """xml数据""" xml_data: str + def __str__(self): + return self.xml_data +class WeChatForwardImage(MessageComponent): + """转发图片。个人微信专用组件。""" + type: str = 'WeChatForwardImage' + """xml数据""" + xml_data: str + def __str__(self): + return self.xml_data + +class WeChatForwardFile(MessageComponent): + """转发文件。个人微信专用组件。""" + type: str = 'WeChatForwardFile' + """xml数据""" + xml_data: str + def __str__(self): + return self.xml_data + +class WeChatAppMsg(MessageComponent): + """通用appmsg发送。个人微信专用组件。""" + type: str = 'WeChatAppMsg' + """xml数据""" + app_msg: str + def __str__(self): + return self.app_msg From 778065f7fbf6c0dcb41241452d0dc15c1a92a70c Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Mon, 28 Apr 2025 15:30:30 +0800 Subject: [PATCH 67/73] fix: image couldn't be sent in lark (#1348) --- pkg/platform/sources/lark.py | 126 +++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 5639739c..2857f5c5 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -1,7 +1,8 @@ from __future__ import annotations import lark_oapi - +from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody, CreateImageResponse +import traceback import typing import asyncio import traceback @@ -60,12 +61,22 @@ class LarkMessageConverter(adapter.MessageConverter): message_chain: platform_message.MessageChain, api_client: lark_oapi.Client ) -> typing.Tuple[list]: message_elements = [] - pending_paragraph = [] - for msg in message_chain: if isinstance(msg, platform_message.Plain): - pending_paragraph.append({"tag": "md", "text": msg.text}) + # Ensure text is valid UTF-8 + try: + text = msg.text.encode('utf-8').decode('utf-8') + pending_paragraph.append({"tag": "md", "text": text}) + except UnicodeError: + # If text is not valid UTF-8, try to decode with other encodings + try: + text = msg.text.encode('latin1').decode('utf-8') + pending_paragraph.append({"tag": "md", "text": text}) + except UnicodeError: + # If still fails, replace invalid characters + text = msg.text.encode('utf-8', errors='replace').decode('utf-8') + pending_paragraph.append({"tag": "md", "text": text}) elif isinstance(msg, platform_message.At): pending_paragraph.append( {"tag": "at", "user_id": msg.target, "style": []} @@ -73,51 +84,84 @@ class LarkMessageConverter(adapter.MessageConverter): elif isinstance(msg, platform_message.AtAll): pending_paragraph.append({"tag": "at", "user_id": "all", "style": []}) elif isinstance(msg, platform_message.Image): - image_bytes = None if msg.base64: - image_bytes = base64.b64decode(msg.base64) + try: + # Remove data URL prefix if present + if msg.base64.startswith('data:'): + msg.base64 = msg.base64.split(',', 1)[1] + image_bytes = base64.b64decode(msg.base64) + except Exception as e: + traceback.print_exc() + continue elif msg.url: - async with aiohttp.ClientSession() as session: - async with session.get(msg.url) as response: - image_bytes = await response.read() + try: + async with aiohttp.ClientSession() as session: + async with session.get(msg.url) as response: + if response.status == 200: + image_bytes = await response.read() + else: + traceback.print_exc() + continue + except Exception as e: + traceback.print_exc() + continue elif msg.path: - with open(msg.path, "rb") as f: - image_bytes = f.read() + try: + with open(msg.path, "rb") as f: + image_bytes = f.read() + except Exception as e: + traceback.print_exc() + continue - request: CreateImageRequest = ( - CreateImageRequest.builder() - .request_body( - CreateImageRequestBody.builder() - .image_type("message") - .image(image_bytes) - .build() - ) - .build() - ) + if image_bytes is None: + continue - response: CreateImageResponse = await api_client.im.v1.image.acreate( - request - ) + try: + # Create a temporary file to store the image bytes + import tempfile + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file.write(image_bytes) + temp_file.flush() + + # Create image request using the temporary file + request = CreateImageRequest.builder()\ + .request_body( + CreateImageRequestBody.builder() + .image_type("message") + .image(open(temp_file.name, "rb")) + .build() + )\ + .build() + + response = await api_client.im.v1.image.acreate(request) + + if not response.success(): + raise Exception( + f"client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" + ) + + image_key = response.data.image_key - if not response.success(): - raise Exception( - f"client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" - ) - - image_key = response.data.image_key - - message_elements.append(pending_paragraph) - message_elements.append( - [ - { - "tag": "img", - "image_key": image_key, - } - ] - ) - pending_paragraph = [] + message_elements.append(pending_paragraph) + message_elements.append( + [ + { + "tag": "img", + "image_key": image_key, + } + ] + ) + pending_paragraph = [] + except Exception as e: + traceback.print_exc() + continue + finally: + # Clean up the temporary file + import os + if 'temp_file' in locals(): + os.unlink(temp_file.name) elif isinstance(msg, platform_message.Forward): for node in msg.node_list: message_elements.extend(await LarkMessageConverter.yiri2target(node.message_chain, api_client)) From 96183eb3e04b71a32bdc338503c28bd8806f5b84 Mon Sep 17 00:00:00 2001 From: Guanchao Wang Date: Tue, 29 Apr 2025 13:04:52 +0800 Subject: [PATCH 68/73] fix: access_token problems in wecomcs (#1355) --- libs/wecom_customer_service_api/api.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/libs/wecom_customer_service_api/api.py b/libs/wecom_customer_service_api/api.py index 279585b3..04e5398b 100644 --- a/libs/wecom_customer_service_api/api.py +++ b/libs/wecom_customer_service_api/api.py @@ -71,6 +71,8 @@ class WecomCSClient(): async def get_detailed_message_list(self,xml_msg:str): # 在本方法中解析消息,并且获得消息的具体内容 + if isinstance(xml_msg, bytes): + xml_msg = xml_msg.decode('utf-8') root = ET.fromstring(xml_msg) token = root.find("Token").text open_kfid = root.find("OpenKfId").text @@ -92,11 +94,12 @@ class WecomCSClient(): } response = await client.post(url,json=params) data = response.json() - if data['errcode'] != 0: - raise Exception("Failed to get message") if data['errcode'] == 40014 or data['errcode'] == 42001: self.access_token = await self.get_access_token(self.secret) return await self.get_detailed_message_list(xml_msg) + if data['errcode'] != 0: + raise Exception("Failed to get message") + last_msg_data = data['msg_list'][-1] open_kfid = last_msg_data.get("open_kfid") # 进行获取图片操作 @@ -182,8 +185,11 @@ class WecomCSClient(): response = await client.post(url, json=payload) data = response.json() - if data.get("errcode") != 0: - raise Exception(f"消息发送失败: {data}") + if data['errcode'] == 40014 or data['errcode'] == 42001: + self.access_token = await self.get_access_token(self.secret) + return await self.send_text_msg(open_kfid,external_userid,msgid,content) + if data['errcode'] != 0: + raise Exception("Failed to send message") return data From 3554702054daf2df22ad9c3af952afd1b73e80cc Mon Sep 17 00:00:00 2001 From: shinelin Date: Tue, 29 Apr 2025 13:18:19 +0800 Subject: [PATCH 69/73] =?UTF-8?q?feat(gewechat):=20=E9=87=8D=E6=9E=84targe?= =?UTF-8?q?t2yiri=E4=BB=A3=E7=A0=81+=E5=BC=95=E7=94=A8=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=B1=95=E5=BC=80=20(#1352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(gewechat): 重构target2yiri代码+引用消息展开 * feat(gewe): 引用消息,图片视频音频是单独的类型 --- pkg/platform/sources/gewechat.py | 470 +++++++++++++++++++------------ 1 file changed, 295 insertions(+), 175 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 8c1b7d6f..6175677e 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import gewechat_client import typing @@ -27,6 +25,7 @@ from ..types import entities as platform_entities from ...utils import image import xml.etree.ElementTree as ET from typing import Optional, List, Tuple +from functools import partial class GewechatMessageConverter(adapter.MessageConverter): @@ -75,210 +74,332 @@ class GewechatMessageConverter(adapter.MessageConverter): return content_list + async def target2yiri( self, message: dict, bot_account_id: str ) -> platform_message.MessageChain: - - # 预处理 - content_list = [] - ats_bot = False - raw_content = message["Data"]["Content"]["string"] - is_group_message = self.__is_group_message(message) + """外部消息转平台消息""" + # 数据预处理 + message_list = [] + ats_bot = False # 是否被@ + content = message["Data"]["Content"]["string"] + content_no_preifx = content # 群消息则去掉前缀 + is_group_message = self._is_group_message(message) if is_group_message: - ats_bot = self.__ats_bot(message, bot_account_id) - # 优先处理艾特全体成员, - if "@所有人" in raw_content: ## at全员时候传入atll不当作at自己 - content_list.append(platform_message.AtAll()) + ats_bot = self._ats_bot(message, bot_account_id) + if "@所有人" in content: + message_list.append(platform_message.AtAll()) elif ats_bot: - content_list.append(platform_message.At(target=bot_account_id)) - raw_content, sender_id = self.__extract_content_and_sender(raw_content) + message_list.append(platform_message.At(target=bot_account_id)) + content_no_preifx, _ = self._extract_content_and_sender(content) - # 消息类型 msg_type = message["Data"]["MsgType"] + + # 映射消息类型到处理器方法 + handler_map = { + 1: self._handler_text, + 3: self._handler_image, + 34: self._handler_voice, + 49: self._handler_compound, # 复合类型 + } + + # 分派处理 + handler = handler_map.get(msg_type, self._handler_default) + handler_result = await handler( + message = message, # 原始的message + content_no_preifx = content_no_preifx, # 处理后的content + ) - # 文本消息 - if msg_type == 1: - # 文本清洗,仅替换群文本中的@文本[空格],的文本 - if is_group_message and ats_bot: - pattern = r'@\S+' - raw_content = re.sub(pattern, '',raw_content) - content_list.append(platform_message.Plain(raw_content)) - return platform_message.MessageChain(content_list) + if handler_result and len(handler_result) > 0: + message_list.extend(handler_result) + + return platform_message.MessageChain(message_list) - # 图像 - elif msg_type == 3: - image_xml = raw_content # 已经去除群聊消息前缀 + async def _handler_text( + self, + message: Optional[dict], + content_no_preifx: str + ) -> platform_message.MessageChain: + """处理文本消息 (msg_type=1)""" + if message and self._is_group_message(message): + pattern = r'@\S+' + content_no_preifx = re.sub(pattern, '', content_no_preifx) + + return platform_message.MessageChain([platform_message.Plain(content_no_preifx)]) + + async def _handler_image( + self, + message: Optional[dict], + content_no_preifx: str + ) -> platform_message.MessageChain: + """处理图像消息 (msg_type=3)""" + try: + image_xml = content_no_preifx if not image_xml: - content_list.append(platform_message.Plain(text="[图片内容为空]")) - return platform_message.MessageChain(content_list) - 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.Unknown("[图片内容为空]")]) + + 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, + ) - content_list.append(platform_message.Image( - base64=f"data:image/{image_format};base64,{base64_str}" - )) - # 消息链中加一个WeChatForwardImage的xml用于转发 - content_list.append(platform_message.WeChatForwardImage( - xml_data = image_xml - )) - return platform_message.MessageChain(content_list) - except Exception as e: - print(f"处理图片消息失败: {str(e)}") - content_list.append(platform_message.Plain(text=f"[图片处理失败]")) - return platform_message.MessageChain(content_list) - # 语音消息 - elif msg_type == 34: - try: - audio_base64 = message["Data"]["ImgBuf"]["buffer"] - return platform_message.MessageChain( - [platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")] - ) - except Exception as e: - return platform_message.MessageChain( - [platform_message.Plain(text="[无法解析群聊语音的消息]")] # 小测了一下,免费版拿不到群聊语音消息的base64,或者用什么办法解析xml里的url? - ) - finally: - return platform_message.MessageChain(content_list) - elif msg_type == 49: - # 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 - try: - # 下方是移除 platform_message.MessageChain: + """处理语音消息 (msg_type=34)""" + message_List = [] + try: + # 从消息中提取语音数据(需根据实际数据结构调整字段名) + audio_base64 = message["Data"]["ImgBuf"]["buffer"] + + # 验证语音数据有效性 + if not audio_base64: + message_List.append(platform_message.Unknown(text="[语音内容为空]")) + return platform_message.MessageChain(message_List) + + # 转换为平台支持的语音格式(如 Silk 格式) + voice_element = platform_message.Voice( + base64=f"data:audio/silk;base64,{audio_base64}" + ) + message_List.append(voice_element) + + except KeyError as e: + print(f"语音数据字段缺失: {str(e)}") + message_List.append(platform_message.Unknown(text="[语音数据解析失败]")) + except Exception as e: + print(f"处理语音消息异常: {str(e)}") + message_List.append(platform_message.Unknown(text="[语音处理失败]")) + + return platform_message.MessageChain(message_List) - # 群特殊处理:引用消息的原发送者是bot or @bot - # 引用判断:quote_id == tousername - if is_group_message and (quote_id == tousername): - if not platform_message.MessageChain(content_list).has(platform_message.At): - content_list.append(platform_message.At(target=bot_account_id)) - - content_list.append(platform_message.Quote( - sender_id=sender_id, - origin=platform_message.MessageChain( - # 这里是文本或者xml, 历史原因用了plain - # TODO: 后面需要重构一下,根据type解析具体的消息类型 - [platform_message.Plain(quote_data)] - ))) - content_list.append(platform_message.Plain(user_data)) # FIXME: 这里还有wxid - return platform_message.MessageChain(content_list) - elif data_type == '51': - return platform_message.MessageChain( - [ # platform_message.Plain(text=f'[视频号消息]'), - platform_message.Unknown(text=raw_content)] - ) - # print(content_data) - elif data_type == '2000': - return platform_message.MessageChain( - [ # platform_message.Plain(text=f'[转账消息]'), - platform_message.Unknown(text=raw_content)] - ) - elif data_type == '2001': - return platform_message.MessageChain( - [ # platform_message.Plain(text=f'[红包消息]'), - platform_message.Unknown(text=raw_content)] - ) - elif data_type == '5': - content_list.append( - # platform_message.Plain(text=f'[公众号消息]'), - platform_message.WeChatForwardLink(xml_data=raw_content) - ) - return platform_message.MessageChain(content_list) - elif data_type == '33' or data_type == '36': - return platform_message.MessageChain( - [ # platform_message.Plain(text=f'[小程序消息]'), - platform_message.Unknown(text=raw_content)] - ) - elif data_type == "6": - # 文件消息 - content_list.append( - # platform_message.Plain(text=f'[文件消息]'), - platform_message.WeChatForwardFile(xml_data=raw_content) - ) - return platform_message.MessageChain(content_list) - # print(data_type.text) + + async def _handler_compound( + self, + message: Optional[dict], + content_no_preifx: str + ) -> platform_message.MessageChain: + """处理复合消息 (msg_type=49),根据子类型分派""" + try: + xml_data = ET.fromstring(content_no_preifx) + appmsg_data = xml_data.find('.//appmsg') + data_type = appmsg_data.findtext('.//type', "") + + # 二次分派处理器 + sub_handler_map = { + '57': self._handler_compound_quote, + '5': self._handler_compound_link, + '6': self._handler_compound_file, + '33': self._handler_compound_mini_program, + '36': self._handler_compound_mini_program, + '2000': partial(self._handler_compound_unsupported, text="[转账消息]"), + '2001': partial(self._handler_compound_unsupported, text="[红包消息]"), + '51': partial(self._handler_compound_unsupported, text="[视频号消息]"), + } + + handler = sub_handler_map.get(data_type, self._handler_compound_unsupported) + return await handler( + message=message, #原始msg + xml_data=xml_data, # xml数据 + ) + except Exception as e: + print(f"解析复合消息失败: {str(e)}") + return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) + + async def _handler_compound_quote( + self, + message: Optional[dict], + xml_data: ET.Element + ) -> platform_message.MessageChain: + """处理引用消息 (data_type=57)""" + # print("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) + appmsg_data = xml_data.find('.//appmsg') + quote_data = "" # 引用原文 + quote_id = None # 引用消息的原发送者 + tousername = None # 接收方: 所属微信的wxid + user_data = "" # 用户消息 + sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member + if appmsg_data: + user_data = appmsg_data.findtext('.//title') or "" + quote_data = appmsg_data.find('.//refermsg').findtext('.//content') + quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') + if message: + tousername = message['Wxid'] + + message_list = [] + # quote_data原始的消息 + if quote_data: + quote_data_message_list = platform_message.MessageChain() + # 文本消息 + try: + if "" not in quote_data: + quote_data_message_list.append(platform_message.Plain(quote_data)) else: - return platform_message.MessageChain( - [platform_message.Unknown(text=raw_content)] - ) - - # try: - # content_bytes = content.encode('utf-8') - # decoded_content = base64.b64decode(content_bytes) - # return platform_message.MessageChain( - # [platform_message.Unknown(content=decoded_content)] - # ) # unknown中没有content - # except Exception as e: - # return platform_message.MessageChain( - # [platform_message.Plain(text=content)] - # ) + # 引用消息展开 + quote_data_xml = ET.fromstring(quote_data) + if quote_data_xml.find("img"): + quote_data_message_list.extend(await self._handler_image(None, quote_data)) + elif quote_data_xml.find("voicemsg"): + quote_data_message_list.extend(await self._handler_voice(None, quote_data)) + elif quote_data_xml.find("videomsg"): + quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理 + else: + # appmsg + quote_data_message_list.extend(await self._handler_compound(None, quote_data)) except Exception as e: - print(f"Error processing type 49 message: {str(e)}") - return platform_message.MessageChain( - [ # platform_message.Plain(text="[无法解析的消息]"), - platform_message.Unknown(text=raw_content)] + print(f"处理引用消息异常 expcetion:{e}") + quote_data_message_list.append(platform_message.Plain(quote_data)) + message_list.append( + platform_message.Quote( + sender_id=sender_id, + origin=quote_data_message_list, ) - finally: - return platform_message.MessageChain(content_list) + ) + if len(user_data) > 0: + pattern = r'^@\S+' + user_data = re.sub(pattern, '', user_data) + message_list.append(platform_message.Plain(user_data)) + # print(f"**_handler_compound_quote message_list len={len(message_list)}") + # for comp in message_list: + # if isinstance(comp, platform_message.Quote): + # print(f"**_handler_compound_quote send_id {comp.sender_id}" ) + # for quote_item in comp.origin: + # print(f"******* _handler_compound_quote item {quote_item.type} + message: {quote_item}" ) + # else: + # print(f"_handler_compound_quote type: {comp.type} + message: {comp}") + return platform_message.MessageChain(message_list) + + async def _handler_compound_file( + self, + message: dict, + xml_data: ET.Element + ) -> platform_message.MessageChain: + """处理文件消息 (data_type=6)""" + xml_data_str = ET.tostring(xml_data, encoding='unicode') + return platform_message.MessageChain([ + platform_message.WeChatForwardFile(xml_data=xml_data_str) + ]) + + async def _handler_compound_link( + self, + message: dict, + xml_data: ET.Element + ) -> platform_message.MessageChain: + """处理链接消息(如公众号文章、外部网页)""" + message_list = [] + try: + # 解析 XML 中的链接参数 + appmsg = xml_data.find('.//appmsg') + if appmsg is None: + return platform_message.MessageChain() + message_list.append( + platform_message.WeChatLink( + link_title = appmsg.findtext('title', ''), + link_desc = appmsg.findtext('des', ''), + link_url = appmsg.findtext('url', ''), + link_thumb_url = appmsg.findtext("thumburl", '') # 这个字段拿不到 + ) + ) + # 转发消息 + xml_data_str = ET.tostring(xml_data, encoding='unicode') + # print(xml_data_str) + message_list.append( + platform_message.WeChatForwardLink( + xml_data=xml_data_str + ) + ) + except Exception as e: + print(f"解析链接消息失败: {str(e)}") + return platform_message.MessageChain(message_list) + + async def _handler_compound_mini_program( + self, + message: dict, + xml_data: ET.Element + ) -> platform_message.MessageChain: + """处理小程序消息(如小程序卡片、服务通知)""" + xml_data_str = ET.tostring(xml_data, encoding='unicode') + return platform_message.MessageChain([ + platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str) + ]) + + async def _handler_default( + self, + message: Optional[dict], + content_no_preifx: str + ) -> platform_message.MessageChain: + """处理未知消息类型""" + if message: + msg_type = message["Data"]["MsgType"] else: - content_list.append(platform_message.Unknown(text=f"[未知消息类型 msg_type:{msg_type}")) - return platform_message.MessageChain(content_list) + msg_type = "" + return platform_message.MessageChain([ + platform_message.Unknown(text=f"[未知消息类型 msg_type:{msg_type}]") + ]) + + def _handler_compound_unsupported( + self, + message: dict, + xml_data: str, + text: Optional[str] = None + ) -> platform_message.MessageChain: + """处理未支持复合消息类型(msg_type=49)子类型""" + if not text: + text = f"[xml_data={xml_data}]" + content_list = [] + content_list.append( + platform_message.Unknown(text=f"[处理未支持复合消息类型[msg_type=49]|{text}")) + + return platform_message.MessageChain(content_list) # 返回是否被艾特 - def __ats_bot(self, message: dict, bot_account_id:str) -> bool: + def _ats_bot(self, message: dict, bot_account_id:str) -> bool: ats_bot = False try: to_user_name = message['Wxid'] # 接收方: 所属微信的wxid raw_content = message["Data"]["Content"]["string"] # 原始消息内容 - # step 1 - ats_bot = ats_bot or (f"@{bot_account_id}" in raw_content) - # step 2 + content_no_prefix, _ = self._extract_content_and_sender(raw_content) + # 直接艾特机器人 + ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix) + # 文本类@bot push_content = message.get('Data', {}).get('PushContent', '') ats_bot = ats_bot or ('在群聊中@了你' in push_content) - # step 3 + # 引用别人时@bot msg_source = message.get('Data', {}).get('MsgSource', '') or '' if len(msg_source) > 0: msg_source_data = ET.fromstring(msg_source) at_user_list = msg_source_data.findtext("atuserlist") or "" ats_bot = ats_bot or (to_user_name in at_user_list) + # 引用bot + if message.get('Data', {}).get('MsgType', 0) == 49: + xml_data = ET.fromstring(content_no_prefix) + appmsg_data = xml_data.find('.//appmsg') + tousername = message['Wxid'] # 接收方: 所属微信的wxid + quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者 + ats_bot = ats_bot or (quote_id == tousername) except Exception as e: - print(f"__ats_bot got except: {e}") + print(f"_ats_bot got except: {e}") finally: return ats_bot - + # 提取一下content前面的sender_id, 和去掉前缀的内容 - def __extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: + def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: try: # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 # add: 有些用户的wxid不是上述格式。换成user_name: @@ -289,12 +410,12 @@ class GewechatMessageConverter(adapter.MessageConverter): sender_id = line_split[0].strip(":") return raw_content, sender_id except Exception as e: - print(f"__extract_content_and_sender got except: {e}") + print(f"_extract_content_and_sender got except: {e}") finally: return raw_content, None # 是否是群消息 - def __is_group_message(self, message: dict)->bool: + def _is_group_message(self, message: dict)->bool: from_user_name = message['Data']['FromUserName']['string'] return from_user_name.endswith("@chatroom") @@ -388,7 +509,6 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): def __init__(self, config: dict, ap: app.Application): self.config = config self.ap = ap - self.quart_app = quart.Quart(__name__) self.message_converter = GewechatMessageConverter(config) @@ -620,4 +740,4 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): ) async def kill(self) -> bool: - pass + pass \ No newline at end of file From 7538973b330760c0a408e5219b6a3fdadd22631b Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Tue, 29 Apr 2025 19:45:19 +0800 Subject: [PATCH 70/73] chore: release v3.4.14.3 (#1358) --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 631a04d2..16886956 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.14.2" +semantic_version = "v3.4.14.3" debug_mode = False From 2a6ca9cb977cd50b5200053c3571490f3aa5c211 Mon Sep 17 00:00:00 2001 From: shinelin Date: Sun, 4 May 2025 16:05:01 +0800 Subject: [PATCH 71/73] =?UTF-8?q?feat(gewechat):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E6=B6=88=E6=81=AF=E8=BD=AC=E5=8F=91+@?= =?UTF-8?q?=E5=9C=A8=E5=BC=95=E7=94=A8=E4=B8=AD=E7=9A=84bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20(#1361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(bugfix): 群消息替换@用户时, 限制下长度 * bugfix(gewechat): 修复@逻辑 * feat(gewechat): 把引用内容暴露出来,插件才可以定制化 * bugfix(gewechat): 空值处理 --- pkg/platform/sources/gewechat.py | 79 ++++++++++++++++++-------------- pkg/platform/types/message.py | 8 ++++ 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 6175677e..28407400 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -66,7 +66,10 @@ class GewechatMessageConverter(adapter.MessageConverter): elif isinstance(component, platform_message.WeChatForwardFile): content_list.append({'type': 'WeChatForwardFile', 'xml_data': component.xml_data}) elif isinstance(component, platform_message.WeChatAppMsg): - content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg}) + content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg}) + # 引用消息转发 + elif isinstance(component, platform_message.WeChatForwardQuote): + content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg}) elif isinstance(component, platform_message.Forward): for node in component.node_list: if node.message_chain: @@ -124,7 +127,7 @@ class GewechatMessageConverter(adapter.MessageConverter): ) -> platform_message.MessageChain: """处理文本消息 (msg_type=1)""" if message and self._is_group_message(message): - pattern = r'@\S+' + pattern = r'@\S{1,20}' content_no_preifx = re.sub(pattern, '', content_no_preifx) return platform_message.MessageChain([platform_message.Plain(content_no_preifx)]) @@ -199,25 +202,28 @@ class GewechatMessageConverter(adapter.MessageConverter): try: xml_data = ET.fromstring(content_no_preifx) appmsg_data = xml_data.find('.//appmsg') - data_type = appmsg_data.findtext('.//type', "") + if appmsg_data: + data_type = appmsg_data.findtext('.//type', "") - # 二次分派处理器 - sub_handler_map = { - '57': self._handler_compound_quote, - '5': self._handler_compound_link, - '6': self._handler_compound_file, - '33': self._handler_compound_mini_program, - '36': self._handler_compound_mini_program, - '2000': partial(self._handler_compound_unsupported, text="[转账消息]"), - '2001': partial(self._handler_compound_unsupported, text="[红包消息]"), - '51': partial(self._handler_compound_unsupported, text="[视频号消息]"), - } - - handler = sub_handler_map.get(data_type, self._handler_compound_unsupported) - return await handler( - message=message, #原始msg - xml_data=xml_data, # xml数据 - ) + # 二次分派处理器 + sub_handler_map = { + '57': self._handler_compound_quote, + '5': self._handler_compound_link, + '6': self._handler_compound_file, + '33': self._handler_compound_mini_program, + '36': self._handler_compound_mini_program, + '2000': partial(self._handler_compound_unsupported, text="[转账消息]"), + '2001': partial(self._handler_compound_unsupported, text="[红包消息]"), + '51': partial(self._handler_compound_unsupported, text="[视频号消息]"), + } + + handler = sub_handler_map.get(data_type, self._handler_compound_unsupported) + return await handler( + message=message, #原始msg + xml_data=xml_data, # xml数据 + ) + else: + return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) except Exception as e: print(f"解析复合消息失败: {str(e)}") return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) @@ -228,6 +234,7 @@ class GewechatMessageConverter(adapter.MessageConverter): xml_data: ET.Element ) -> platform_message.MessageChain: """处理引用消息 (data_type=57)""" + message_list = [] # print("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) appmsg_data = xml_data.find('.//appmsg') quote_data = "" # 引用原文 @@ -236,13 +243,15 @@ class GewechatMessageConverter(adapter.MessageConverter): user_data = "" # 用户消息 sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member if appmsg_data: - user_data = appmsg_data.findtext('.//title') or "" + user_data = appmsg_data.findtext('.//title') or "" quote_data = appmsg_data.find('.//refermsg').findtext('.//content') - quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') + quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') + message_list.append( + platform_message.WeChatForwardQuote( + app_msg=ET.tostring(appmsg_data, encoding='unicode')) + ) if message: tousername = message['Wxid'] - - message_list = [] # quote_data原始的消息 if quote_data: quote_data_message_list = platform_message.MessageChain() @@ -272,17 +281,18 @@ class GewechatMessageConverter(adapter.MessageConverter): ) ) if len(user_data) > 0: - pattern = r'^@\S+' + pattern = r'@\S{1,20}' user_data = re.sub(pattern, '', user_data) message_list.append(platform_message.Plain(user_data)) - # print(f"**_handler_compound_quote message_list len={len(message_list)}") + # for comp in message_list: # if isinstance(comp, platform_message.Quote): - # print(f"**_handler_compound_quote send_id {comp.sender_id}" ) + # print(f"quote_message_chain len={len(message_list)}") + # print(f"quote_message_chain send_id={comp.sender_id}" ) # for quote_item in comp.origin: - # print(f"******* _handler_compound_quote item {quote_item.type} + message: {quote_item}" ) + # print(f"--quote_message_component [msg_type={quote_item.type}][message={quote_item}]" ) # else: - # print(f"_handler_compound_quote type: {comp.type} + message: {comp}") + # print(f"quote_message_chain plain [msg_type={comp.type}][message={comp.text}]") return platform_message.MessageChain(message_list) async def _handler_compound_file( @@ -375,8 +385,8 @@ class GewechatMessageConverter(adapter.MessageConverter): to_user_name = message['Wxid'] # 接收方: 所属微信的wxid raw_content = message["Data"]["Content"]["string"] # 原始消息内容 content_no_prefix, _ = self._extract_content_and_sender(raw_content) - # 直接艾特机器人 - ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix) + # 直接艾特机器人(这个有bug,当被引用的消息里面有@bot,会套娃 + # ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix) # 文本类@bot push_content = message.get('Data', {}).get('PushContent', '') ats_bot = ats_bot or ('在群聊中@了你' in push_content) @@ -390,9 +400,10 @@ class GewechatMessageConverter(adapter.MessageConverter): if message.get('Data', {}).get('MsgType', 0) == 49: xml_data = ET.fromstring(content_no_prefix) appmsg_data = xml_data.find('.//appmsg') - tousername = message['Wxid'] # 接收方: 所属微信的wxid - quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者 - ats_bot = ats_bot or (quote_id == tousername) + tousername = message['Wxid'] + if appmsg_data: # 接收方: 所属微信的wxid + quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者 + ats_bot = ats_bot or (quote_id == tousername) except Exception as e: print(f"_ats_bot got except: {e}") finally: diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index 0396dad9..c0b97671 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -894,3 +894,11 @@ class WeChatAppMsg(MessageComponent): app_msg: str def __str__(self): return self.app_msg + +class WeChatForwardQuote(MessageComponent): + """转发引用消息。个人微信专用组件。""" + type: str = 'WeChatForwardQuote' + """xml数据""" + app_msg: str + def __str__(self): + return self.app_msg From f58d5f184f87cb2c4dcafadce3306a30d75abd51 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 01:28:43 +0000 Subject: [PATCH 72/73] fix: initialize chunk variable before reference in difysvapi.py Co-Authored-By: Junyan Qin --- pkg/provider/runners/difysvapi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index ef35e829..1c2b858e 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -114,6 +114,8 @@ class DifyServiceAPIRunner(runner.RequestRunner): inputs = {} inputs.update(query.variables) + + chunk = None # 初始化chunk变量,防止在没有响应时引用错误 async for chunk in self.dify_client.chat_messages( inputs=inputs, @@ -171,6 +173,8 @@ class DifyServiceAPIRunner(runner.RequestRunner): inputs.update(query.variables) pending_agent_message = '' + + chunk = None # 初始化chunk变量,防止在没有响应时引用错误 async for chunk in self.dify_client.chat_messages( inputs=inputs, From e265f267e10cb017587433e6f18d1f4969013eef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 01:37:04 +0000 Subject: [PATCH 73/73] improve: add explicit error handling for empty API responses Co-Authored-By: Junyan Qin --- pkg/provider/runners/difysvapi.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 1c2b858e..863359f1 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -146,6 +146,9 @@ class DifyServiceAPIRunner(runner.RequestRunner): content=self._try_convert_thinking(basic_mode_pending_chunk), ) basic_mode_pending_chunk = '' + + if chunk is None: + raise errors.DifyAPIError("Dify API 没有返回任何响应,请检查网络连接和API配置") query.session.using_conversation.uuid = chunk["conversation_id"] @@ -238,6 +241,9 @@ class DifyServiceAPIRunner(runner.RequestRunner): ) if chunk['event'] == 'error': raise errors.DifyAPIError("dify 服务错误: " + chunk['message']) + + if chunk is None: + raise errors.DifyAPIError("Dify API 没有返回任何响应,请检查网络连接和API配置") query.session.using_conversation.uuid = chunk["conversation_id"]