diff --git a/pkg/core/migrations/m025_gewechat_config.py b/pkg/core/migrations/m025_gewechat_config.py index 3ed108c0..65b5c1d5 100644 --- a/pkg/core/migrations/m025_gewechat_config.py +++ b/pkg/core/migrations/m025_gewechat_config.py @@ -23,6 +23,7 @@ class GewechatConfigMigration(migration.Migration): "adapter": "gewechat", "enable": False, "gewechat_url": "http://your-gewechat-server:2531", + "gewechat_file_url": "http://your-gewechat-server:2532", "port": 2286, "callback_url": "http://your-callback-url:2286/gewechat/callback", "app_id": "", diff --git a/pkg/core/migrations/m034_gewechat_file_url_config.py b/pkg/core/migrations/m034_gewechat_file_url_config.py new file mode 100644 index 00000000..44bbd65e --- /dev/null +++ b/pkg/core/migrations/m034_gewechat_file_url_config.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from urllib.parse import urlparse + +from .. import migration + + +@migration.migration_class("gewechat-file-url-config", 34) +class GewechatFileUrlConfigMigration(migration.Migration): + """迁移""" + + async def need_migrate(self) -> bool: + """判断当前环境是否需要运行此迁移""" + + for adapter in self.ap.platform_cfg.data['platform-adapters']: + if adapter['adapter'] == 'gewechat': + if 'gewechat_file_url' not in adapter: + return True + return False + + async def run(self): + """执行迁移""" + for adapter in self.ap.platform_cfg.data['platform-adapters']: + if adapter['adapter'] == 'gewechat': + if 'gewechat_file_url' not in adapter: + parsed_url = urlparse(adapter['gewechat_url']) + adapter['gewechat_file_url'] = f"{parsed_url.scheme}://{parsed_url.hostname}:2532" + + await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/stages/migrate.py b/pkg/core/stages/migrate.py index d317596e..ce6e41a5 100644 --- a/pkg/core/stages/migrate.py +++ b/pkg/core/stages/migrate.py @@ -11,7 +11,7 @@ from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_ from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config from ..migrations import 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 +from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config @stage.stage_class("MigrationStage") class MigrationStage(stage.BootingStage): diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index 5ff5a467..c7478635 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -28,7 +28,10 @@ from ...utils import image class GewechatMessageConverter(adapter.MessageConverter): - + + def __init__(self, config: dict): + self.config = config + @staticmethod async def yiri2target( message_chain: platform_message.MessageChain @@ -48,12 +51,12 @@ class GewechatMessageConverter(adapter.MessageConverter): return content_list - @staticmethod async def target2yiri( + self, message: dict, bot_account_id: str ) -> platform_message.MessageChain: - + if message["Data"]["MsgType"] == 1: # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 regex = re.compile(r"^wxid_.*:") @@ -74,25 +77,72 @@ class GewechatMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(content_list) elif message["Data"]["MsgType"] == 3: - image_base64 = message["Data"]["ImgBuf"]["buffer"] - return platform_message.MessageChain( - [platform_message.Image(base64=f"data:image/jpeg;base64,{image_base64}")] - ) + image_xml = message["Data"]["Content"]["string"] + if not image_xml: + return platform_message.MessageChain([ + platform_message.Plain(text="[图片内容为空]") + ]) + + try: + base64_str, image_format = await image.get_gewechat_image_base64( + gewechat_url=self.config["gewechat_url"], + gewechat_file_url=self.config["gewechat_file_url"] + app_id=self.config["app_id"], + xml_content=image_xml, + token=self.config["token"], + image_type=2, + ) + + return platform_message.MessageChain([ + platform_message.Image( + base64=f"data:image/{image_format};base64,{base64_str}" + ) + ]) + except Exception as e: + print(f"处理图片消息失败: {str(e)}") + return platform_message.MessageChain([ + platform_message.Plain(text="[图片处理失败]") + ]) + + elif message["Data"]["MsgType"] == 49: + # 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 + try: + content = message["Data"]["Content"]["string"] + + try: + content_bytes = content.encode('utf-8') + decoded_content = base64.b64decode(content_bytes) + return platform_message.MessageChain( + [platform_message.Unknown(content=decoded_content)] + ) + except Exception as e: + return platform_message.MessageChain( + [platform_message.Plain(text=content)] + ) + except Exception as e: + print(f"Error processing type 49 message: {str(e)}") + return platform_message.MessageChain( + [platform_message.Plain(text="[无法解析的消息]")] + ) class GewechatEventConverter(adapter.EventConverter): - + + def __init__(self, config: dict): + self.config = config + self.message_converter = GewechatMessageConverter(config) + @staticmethod async def yiri2target( event: platform_events.MessageEvent ) -> dict: pass - @staticmethod async def target2yiri( + self, event: dict, bot_account_id: str ) -> platform_events.MessageEvent: - message_chain = await GewechatMessageConverter.target2yiri(copy.deepcopy(event), bot_account_id) + message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id) if not message_chain: return None @@ -120,7 +170,7 @@ class GewechatEventConverter(adapter.EventConverter): time=event["Data"]["CreateTime"], source_platform_object=event, ) - elif 'wxid_' in event["Data"]["FromUserName"]["string"]: + else: return platform_events.FriendMessage( sender=platform_entities.Friend( id=event["Data"]["FromUserName"]["string"], @@ -134,7 +184,9 @@ class GewechatEventConverter(adapter.EventConverter): class GeWeChatAdapter(adapter.MessagePlatformAdapter): - + + name: str = "gewechat" # 定义适配器名称 + bot: gewechat_client.GewechatClient quart_app: quart.Quart @@ -144,8 +196,8 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): ap: app.Application - message_converter: GewechatMessageConverter = GewechatMessageConverter() - event_converter: GewechatEventConverter = GewechatEventConverter() + message_converter: GewechatMessageConverter + event_converter: GewechatEventConverter listeners: typing.Dict[ typing.Type[platform_events.Event], @@ -158,6 +210,9 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self.quart_app = quart.Quart(__name__) + self.message_converter = GewechatMessageConverter(config) + self.event_converter = GewechatEventConverter(config) + @self.quart_app.route('/gewechat/callback', methods=['POST']) async def gewechat_callback(): data = await quart.request.json diff --git a/pkg/platform/sources/gewechat.yaml b/pkg/platform/sources/gewechat.yaml index 6a6aec28..01967ffc 100644 --- a/pkg/platform/sources/gewechat.yaml +++ b/pkg/platform/sources/gewechat.yaml @@ -17,6 +17,13 @@ spec: type: string required: true default: "" + - name: gewechat_file_url + label: + en_US: GeWeChat file download URL + zh_CN: GeWeChat 文件下载URL + type: string + required: true + default: "" - name: port label: en_US: Port diff --git a/pkg/utils/image.py b/pkg/utils/image.py index 7a60df2a..16077ace 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -8,6 +8,106 @@ import aiohttp import PIL.Image import httpx +import os +import aiofiles +import pathlib +import asyncio +from urllib.parse import urlparse + + +async def get_gewechat_image_base64( + gewechat_url: str, + gewechat_file_url: str, + app_id: str, + xml_content: str, + token: str, + image_type: int = 2, +) -> typing.Tuple[str, str]: + """从gewechat服务器获取图片并转换为base64格式 + + Args: + gewechat_url (str): gewechat服务器地址(用于获取图片URL) + gewechat_file_url (str): gewechat文件下载服务地址 + app_id (str): gewechat应用ID + xml_content (str): 图片的XML内容 + token (str): Gewechat API Token + image_type (int, optional): 图片类型. Defaults to 2. + + Returns: + typing.Tuple[str, str]: (base64编码, 图片格式) + + Raises: + aiohttp.ClientTimeout: 请求超时(15秒)或连接超时(2秒) + Exception: 其他错误 + """ + headers = { + 'X-GEWE-TOKEN': token, + 'Content-Type': 'application/json' + } + + # 设置超时 + timeout = aiohttp.ClientTimeout( + total=15.0, # 总超时时间15秒 + connect=2.0, # 连接超时2秒 + sock_connect=2.0, # socket连接超时2秒 + sock_read=15.0 # socket读取超时15秒 + ) + + try: + async with aiohttp.ClientSession(timeout=timeout) as session: + # 获取图片下载链接 + try: + async with session.post( + f"{gewechat_url}/v2/api/message/downloadImage", + headers=headers, + json={ + "appId": app_id, + "type": image_type, + "xml": xml_content + } + ) as response: + if response.status != 200: + raise Exception(f"获取gewechat图片下载失败: {await response.text()}") + + resp_data = await response.json() + if resp_data.get("ret") != 200: + raise Exception(f"获取gewechat图片下载链接失败: {resp_data}") + + file_url = resp_data['data']['fileUrl'] + except asyncio.TimeoutError: + raise Exception("获取图片下载链接超时") + except aiohttp.ClientError as e: + raise Exception(f"获取图片下载链接网络错误: {str(e)}") + + # 解析原始URL并替换端口 + base_url = gewechat_file_url + download_url = f"{base_url}/download/{file_url}" + + # 下载图片 + try: + async with session.get(download_url) as img_response: + if img_response.status != 200: + raise Exception(f"下载图片失败: {await img_response.text()}, URL: {download_url}") + + image_data = await img_response.read() + + content_type = img_response.headers.get('Content-Type', '') + if content_type: + image_format = content_type.split('/')[-1] + else: + image_format = file_url.split('.')[-1] + + base64_str = base64.b64encode(image_data).decode('utf-8') + + return base64_str, image_format + except asyncio.TimeoutError: + raise Exception(f"下载图片超时, URL: {download_url}") + except aiohttp.ClientError as e: + raise Exception(f"下载图片网络错误: {str(e)}, URL: {download_url}") + except Exception as e: + raise Exception(f"获取图片失败: {str(e)}") from e + + async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]: """ 下载企业微信图片并转换为 base64 diff --git a/templates/platform.json b/templates/platform.json index 8e8e73ef..a0fb8ce2 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -64,6 +64,7 @@ "adapter": "gewechat", "enable": false, "gewechat_url": "http://your-gewechat-server:2531", + "gewechat_file_url": "http://your-gewechat-server:2532", "port": 2286, "callback_url": "http://your-callback-url:2286/gewechat/callback", "app_id": "", diff --git a/templates/schema/platform.json b/templates/schema/platform.json index c33907c1..925330a7 100644 --- a/templates/schema/platform.json +++ b/templates/schema/platform.json @@ -325,6 +325,11 @@ "default": "", "description": "gewechat 的 url" }, + "gewechat_file_url": { + "type": "string", + "default": "", + "description": "gewechat 文件下载URL" + }, "port": { "type": "integer", "default": 2286,