From 6e9fcccd7df0970fa0709065d5fa60719e583645 Mon Sep 17 00:00:00 2001 From: fdc310 <2213070223@qq.com> Date: Sun, 22 Mar 2026 04:59:29 +0800 Subject: [PATCH] feat: migrate gewechat adapter to unified webhook architecture Move gewechat adapter from legacy (self-hosted Quart server) to the unified webhook entry point at /api/bots/{bot_uuid}. This removes the need for a dedicated port and aligns with the modern adapter pattern. Key changes: - Add set_bot_uuid() and handle_unified_webhook() methods - Remove self-hosted Quart server from run_async() - Auto-generate callback URL from callback_base_url + bot_uuid - Update constructor signature to (config, logger) - Rename legacy gewechat.yaml to prevent name conflict Co-Authored-By: Claude Opus 4.6 --- src/langbot/pkg/platform/sources/gewechat.png | Bin 0 -> 25318 bytes src/langbot/pkg/platform/sources/gewechat.py | 626 ++++++++++++++++++ .../pkg/platform/sources/gewechat.yaml | 61 ++ .../{gewechat.yaml => gewechat.yaml.legacy} | 0 4 files changed, 687 insertions(+) create mode 100644 src/langbot/pkg/platform/sources/gewechat.png create mode 100644 src/langbot/pkg/platform/sources/gewechat.py create mode 100644 src/langbot/pkg/platform/sources/gewechat.yaml rename src/langbot/pkg/platform/sources/legacy/{gewechat.yaml => gewechat.yaml.legacy} (100%) diff --git a/src/langbot/pkg/platform/sources/gewechat.png b/src/langbot/pkg/platform/sources/gewechat.png new file mode 100644 index 0000000000000000000000000000000000000000..32b2fa3237f1951881ce80dac3f7e77737bb4aaa GIT binary patch literal 25318 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 bzwMd+G@Z&y^t1*3U$N^H&djh9;}ZK{%Xbt{ literal 0 HcmV?d00001 diff --git a/src/langbot/pkg/platform/sources/gewechat.py b/src/langbot/pkg/platform/sources/gewechat.py new file mode 100644 index 00000000..a99a046b --- /dev/null +++ b/src/langbot/pkg/platform/sources/gewechat.py @@ -0,0 +1,626 @@ +import gewechat_client + +import typing +import asyncio +import traceback +import time +import re +import copy +import threading + +from langbot.pkg.utils import httpclient + +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +from ...utils import image +import xml.etree.ElementTree as ET +from typing import Optional, Tuple +from functools import partial +from ..logger import EventLogger + + +class GewechatMessageConverter(abstract_platform_adapter.AbstractMessageConverter): + def __init__(self, config: dict): + self.config = config + + @staticmethod + async def yiri2target(message_chain: platform_message.MessageChain) -> list[dict]: + content_list = [] + for component in message_chain: + if isinstance(component, platform_message.At): + content_list.append({'type': 'at', 'target': component.target}) + elif isinstance(component, platform_message.Plain): + content_list.append({'type': 'text', 'content': component.text}) + elif isinstance(component, platform_message.Image): + if not component.url: + pass + content_list.append({'type': 'image', 'image': component.url}) + elif isinstance(component, platform_message.Voice): + content_list.append({'type': 'voice', 'url': component.url, 'length': component.length}) + elif isinstance(component, platform_message.Forward): + for node in component.node_list: + content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) + content_list.append({'type': 'image', 'image': component.url}) + 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.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, + } + ) + elif isinstance(component, platform_message.WeChatForwardLink): + content_list.append({'type': 'WeChatForwardLink', 'xml_data': component.xml_data}) + 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.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: + content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) + + return content_list + + async def target2yiri(self, message: dict, bot_account_id: str) -> platform_message.MessageChain: + 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 content: + message_list.append(platform_message.AtAll()) + elif ats_bot: + 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, + content_no_preifx=content_no_preifx, + ) + + if handler_result and len(handler_result) > 0: + message_list.extend(handler_result) + + return platform_message.MessageChain(message_list) + + async def _handler_text(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: + if message and self._is_group_message(message): + pattern = r'@\S{1,20}' + 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: + try: + image_xml = content_no_preifx + if not image_xml: + 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, + ) + + elements = [ + platform_message.Image(base64=f'data:image/{image_format};base64,{base64_str}'), + platform_message.WeChatForwardImage(xml_data=image_xml), + ] + return platform_message.MessageChain(elements) + except Exception as e: + print(f'处理图片失败: {str(e)}') + return platform_message.MessageChain([platform_message.Unknown('[图片处理失败]')]) + + async def _handler_voice(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: + 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) + + 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) + + async def _handler_compound(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: + try: + xml_data = ET.fromstring(content_no_preifx) + appmsg_data = xml_data.find('.//appmsg') + 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, + xml_data=xml_data, + ) + 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)]) + + async def _handler_compound_quote( + self, message: Optional[dict], xml_data: ET.Element + ) -> platform_message.MessageChain: + message_list = [] + appmsg_data = xml_data.find('.//appmsg') + quote_data = '' + user_data = '' + sender_id = xml_data.findtext('.//fromusername') + if appmsg_data: + user_data = appmsg_data.findtext('.//title') or '' + quote_data = appmsg_data.find('.//refermsg').findtext('.//content') + message_list.append( + platform_message.WeChatForwardQuote(app_msg=ET.tostring(appmsg_data, encoding='unicode')) + ) + 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: + 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: + quote_data_message_list.extend(await self._handler_compound(None, quote_data)) + except Exception as e: + 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, + ) + ) + if len(user_data) > 0: + pattern = r'@\S{1,20}' + user_data = re.sub(pattern, '', user_data) + message_list.append(platform_message.Plain(user_data)) + + return platform_message.MessageChain(message_list) + + async def _handler_compound_file(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.WeChatForwardFile(xml_data=xml_data_str)]) + + async def _handler_compound_link(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: + message_list = [] + try: + 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') + 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: + 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: + 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: + ats_bot = False + try: + to_user_name = message['Wxid'] + raw_content = message['Data']['Content']['string'] + content_no_prefix, _ = self._extract_content_and_sender(raw_content) + push_content = message.get('Data', {}).get('PushContent', '') + ats_bot = ats_bot or ('在群聊中@了你' in push_content) + 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) + if message.get('Data', {}).get('MsgType', 0) == 49: + xml_data = ET.fromstring(content_no_prefix) + appmsg_data = xml_data.find('.//appmsg') + tousername = message['Wxid'] + if appmsg_data: + quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') + ats_bot = ats_bot or (quote_id == tousername) + except Exception as e: + print(f'Error in gewechat _ats_bot: {e}') + finally: + return ats_bot + + def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: + try: + 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(abstract_platform_adapter.AbstractEventConverter): + def __init__(self, config: dict): + self.config = config + self.message_converter = GewechatMessageConverter(config) + + @staticmethod + async def yiri2target(event: platform_events.MessageEvent) -> dict: + pass + + async def target2yiri(self, event: dict, bot_account_id: str) -> platform_events.MessageEvent: + if event['Wxid'] == event['Data']['FromUserName']['string']: + return None + if event['Data']['FromUserName']['string'].startswith('gh_') or event['Data']['FromUserName'][ + 'string' + ].startswith('weixin'): + return None + message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id) + + if not message_chain: + return None + + if '@chatroom' in event['Data']['FromUserName']['string']: + sender_wxid = event['Data']['Content']['string'].split(':')[0] + + return platform_events.GroupMessage( + sender=platform_entities.GroupMember( + id=sender_wxid, + member_name=event['Data']['FromUserName']['string'], + permission=platform_entities.Permission.Member, + group=platform_entities.Group( + id=event['Data']['FromUserName']['string'], + name=event['Data']['FromUserName']['string'], + permission=platform_entities.Permission.Member, + ), + special_title='', + ), + message_chain=message_chain, + time=event['Data']['CreateTime'], + source_platform_object=event, + ) + else: + return platform_events.FriendMessage( + sender=platform_entities.Friend( + id=event['Data']['FromUserName']['string'], + nickname=event['Data']['FromUserName']['string'], + remark='', + ), + message_chain=message_chain, + time=event['Data']['CreateTime'], + source_platform_object=event, + ) + + +class GeWeChatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: gewechat_client.GewechatClient = None + bot_uuid: str = None + + bot_account_id: str = '' + + config: dict + + message_converter: GewechatMessageConverter = None + event_converter: GewechatEventConverter = None + + listeners: typing.Dict[ + typing.Type[platform_events.Event], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], + ] = {} + + def __init__(self, config: dict, logger: EventLogger): + self.config = config + + super().__init__( + config=config, + logger=logger, + ) + + self.message_converter = GewechatMessageConverter(config) + self.event_converter = GewechatEventConverter(config) + + def set_bot_uuid(self, bot_uuid: str): + self.bot_uuid = bot_uuid + + async def handle_unified_webhook(self, bot_uuid: str, path: str, request): + data = await request.json + await self.logger.debug(f'Gewechat callback event: {data}') + + if 'data' in data: + data['Data'] = data['data'] + if 'type_name' in data: + data['TypeName'] = data['type_name'] + + if 'testMsg' in data: + return 'ok' + elif 'TypeName' in data and data['TypeName'] == 'AddMsg': + try: + event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id) + except Exception: + await self.logger.error(f'Error in gewechat callback: {traceback.format_exc()}') + return 'ok' + + if event and event.__class__ in self.listeners: + await self.listeners[event.__class__](event, self) + + return 'ok' + + 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: + await self.logger.warning(f'未处理的消息类型: {msg["type"]}') + continue + + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): + return await self._handle_message(message, target_id) + + async def reply_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + 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 + + def register_listener( + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], + ): + self.listeners[event_type] = callback + + def unregister_listener( + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], + ): + pass + + def _build_callback_url(self) -> str: + base_url = self.config.get('callback_base_url', '').rstrip('/') + return f'{base_url}/api/bots/{self.bot_uuid}' + + async def run_async(self): + if not self.config['token']: + session = httpclient.get_session() + async with session.post( + f'{self.config["gewechat_url"]}/v2/api/tools/getTokenId', + json={'app_id': self.config['app_id']}, + ) as response: + if response.status != 200: + raise Exception(f'获取gewechat token失败: {await response.text()}') + self.config['token'] = (await response.json())['data'] + + self.bot = gewechat_client.GewechatClient(f'{self.config["gewechat_url"]}/v2/api', self.config['token']) + + def gewechat_login_process(): + app_id, error_msg = self.bot.login(self.config['app_id']) + if error_msg: + raise Exception(f'Gewechat 登录失败: {error_msg}') + + self.config['app_id'] = app_id + + print(f'Gewechat 登录成功,app_id: {app_id}') + + profile = self.bot.get_profile(self.config['app_id']) + self.bot_account_id = profile['data']['nickName'] + + time.sleep(2) + + try: + callback_url = self._build_callback_url() + self.bot.set_callback(self.config['token'], callback_url) + print(f'Gewechat 回调地址已设置: {callback_url}') + except Exception as e: + raise Exception(f'设置 Gewechat 回调失败,token失效:{e}') + + threading.Thread(target=gewechat_login_process).start() + + # 统一 webhook 模式下,不启动独立的 HTTP 服务 + # 保持适配器运行 + while True: + await asyncio.sleep(1) + + async def kill(self) -> bool: + return False diff --git a/src/langbot/pkg/platform/sources/gewechat.yaml b/src/langbot/pkg/platform/sources/gewechat.yaml new file mode 100644 index 00000000..2d0f664f --- /dev/null +++ b/src/langbot/pkg/platform/sources/gewechat.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: gewechat + label: + en_US: GeWeChat + zh_Hans: GeWeChat(个人微信) + description: + en_US: GeWeChat Adapter (Unified Webhook) + zh_Hans: GeWeChat 适配器(统一 Webhook),请查看文档了解使用方式 + icon: gewechat.png +spec: + config: + - name: gewechat_url + label: + en_US: GeWeChat URL + zh_Hans: GeWeChat URL + description: + en_US: GeWeChat API server address, e.g. http://127.0.0.1:2531 + zh_Hans: GeWeChat API 服务器地址,如 http://127.0.0.1:2531 + type: string + required: true + default: "" + - name: gewechat_file_url + label: + en_US: GeWeChat file download URL + zh_Hans: GeWeChat 文件下载URL + description: + en_US: GeWeChat file download service address + zh_Hans: GeWeChat 文件下载服务地址 + type: string + required: true + default: "" + - name: callback_base_url + label: + en_US: Callback Base URL + zh_Hans: 回调基础URL + description: + en_US: "LangBot public base URL for webhook callbacks, e.g. http://your-server:5300. The full callback URL will be auto-generated as {callback_base_url}/api/bots/{bot_uuid}" + zh_Hans: "LangBot 对外可访问的基础URL,如 http://your-server:5300。完整回调地址将自动生成为 {callback_base_url}/api/bots/{bot_uuid}" + type: string + required: true + default: "" + - name: app_id + label: + en_US: App ID + zh_Hans: 应用ID + type: string + required: true + default: "" + - name: token + label: + en_US: Token + zh_Hans: 令牌 + type: string + required: true + default: "" +execution: + python: + path: ./gewechat.py + attr: GeWeChatAdapter diff --git a/src/langbot/pkg/platform/sources/legacy/gewechat.yaml b/src/langbot/pkg/platform/sources/legacy/gewechat.yaml.legacy similarity index 100% rename from src/langbot/pkg/platform/sources/legacy/gewechat.yaml rename to src/langbot/pkg/platform/sources/legacy/gewechat.yaml.legacy