From 913b9a24c41016d1c947b6c3e5c4acb661164447 Mon Sep 17 00:00:00 2001 From: wangcham Date: Thu, 6 Nov 2025 09:21:17 +0000 Subject: [PATCH] feat: add support for wecombot,wxoa,slack and qqo --- libs/official_account_api/api.py | 109 ++++++++++++++++------ libs/qq_official_api/api.py | 42 +++++++-- libs/slack_api/api.py | 35 ++++++- libs/wecom_ai_bot_api/api.py | 71 +++++++++----- pkg/api/http/service/bot.py | 4 +- pkg/platform/sources/officialaccount.py | 51 ++++++++-- pkg/platform/sources/officialaccount.yaml | 17 ---- pkg/platform/sources/qqofficial.py | 50 ++++++++-- pkg/platform/sources/qqofficial.yaml | 7 -- pkg/platform/sources/slack.py | 49 ++++++++-- pkg/platform/sources/slack.yaml | 7 -- pkg/platform/sources/wecombot.py | 51 ++++++++-- pkg/platform/sources/wecombot.yaml | 7 -- 13 files changed, 359 insertions(+), 141 deletions(-) diff --git a/libs/official_account_api/api.py b/libs/official_account_api/api.py index 569196f8..bde683a9 100644 --- a/libs/official_account_api/api.py +++ b/libs/official_account_api/api.py @@ -23,20 +23,25 @@ xml_template = """ class OAClient: - def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None): + def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None, unified_mode: bool = False): self.token = token self.aes = EncodingAESKey self.appid = AppID self.appsecret = Appsecret self.base_url = 'https://api.weixin.qq.com' self.access_token = '' + self.unified_mode = unified_mode self.app = Quart(__name__) - self.app.add_url_rule( - '/callback/command', - 'handle_callback', - self.handle_callback_request, - methods=['GET', 'POST'], - ) + + # 只有在非统一模式下才注册独立路由 + if not self.unified_mode: + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['GET', 'POST'], + ) + self._message_handlers = { 'example': [], } @@ -46,19 +51,39 @@ class OAClient: self.logger = logger async def handle_callback_request(self): + """处理回调请求(独立端口模式,使用全局 request)。""" + return await self._handle_callback_internal(request) + + async def handle_unified_webhook(self, req): + """处理回调请求(统一 webhook 模式,显式传递 request)。 + + Args: + req: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self._handle_callback_internal(req) + + async def _handle_callback_internal(self, req): + """处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。 + + Args: + req: Quart Request 对象 + """ try: # 每隔100毫秒查询是否生成ai回答 start_time = time.time() - signature = request.args.get('signature', '') - timestamp = request.args.get('timestamp', '') - nonce = request.args.get('nonce', '') - echostr = request.args.get('echostr', '') - msg_signature = request.args.get('msg_signature', '') + signature = req.args.get('signature', '') + timestamp = req.args.get('timestamp', '') + nonce = req.args.get('nonce', '') + echostr = req.args.get('echostr', '') + msg_signature = req.args.get('msg_signature', '') if msg_signature is None: await self.logger.error('msg_signature不在请求体中') raise Exception('msg_signature不在请求体中') - if request.method == 'GET': + if req.method == 'GET': # 校验签名 check_str = ''.join(sorted([self.token, timestamp, nonce])) check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest() @@ -68,8 +93,8 @@ class OAClient: else: await self.logger.error('拒绝请求') raise Exception('拒绝请求') - elif request.method == 'POST': - encryt_msg = await request.data + elif req.method == 'POST': + encryt_msg = await req.data wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid) ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce) xml_msg = xml_msg.decode('utf-8') @@ -182,6 +207,7 @@ class OAClientForLongerResponse: Appsecret: str, LoadingMessage: str, logger: None, + unified_mode: bool = False, ): self.token = token self.aes = EncodingAESKey @@ -189,13 +215,18 @@ class OAClientForLongerResponse: self.appsecret = Appsecret self.base_url = 'https://api.weixin.qq.com' self.access_token = '' + self.unified_mode = unified_mode self.app = Quart(__name__) - self.app.add_url_rule( - '/callback/command', - 'handle_callback', - self.handle_callback_request, - methods=['GET', 'POST'], - ) + + # 只有在非统一模式下才注册独立路由 + if not self.unified_mode: + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['GET', 'POST'], + ) + self._message_handlers = { 'example': [], } @@ -206,24 +237,44 @@ class OAClientForLongerResponse: self.logger = logger async def handle_callback_request(self): + """处理回调请求(独立端口模式,使用全局 request)。""" + return await self._handle_callback_internal(request) + + async def handle_unified_webhook(self, req): + """处理回调请求(统一 webhook 模式,显式传递 request)。 + + Args: + req: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self._handle_callback_internal(req) + + async def _handle_callback_internal(self, req): + """处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。 + + Args: + req: Quart Request 对象 + """ try: - signature = request.args.get('signature', '') - timestamp = request.args.get('timestamp', '') - nonce = request.args.get('nonce', '') - echostr = request.args.get('echostr', '') - msg_signature = request.args.get('msg_signature', '') + signature = req.args.get('signature', '') + timestamp = req.args.get('timestamp', '') + nonce = req.args.get('nonce', '') + echostr = req.args.get('echostr', '') + msg_signature = req.args.get('msg_signature', '') if msg_signature is None: await self.logger.error('msg_signature不在请求体中') raise Exception('msg_signature不在请求体中') - if request.method == 'GET': + if req.method == 'GET': check_str = ''.join(sorted([self.token, timestamp, nonce])) check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest() return echostr if check_signature == signature else '拒绝请求' - elif request.method == 'POST': - encryt_msg = await request.data + elif req.method == 'POST': + encryt_msg = await req.data wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid) ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce) xml_msg = xml_msg.decode('utf-8') diff --git a/libs/qq_official_api/api.py b/libs/qq_official_api/api.py index c5728437..085437f1 100644 --- a/libs/qq_official_api/api.py +++ b/libs/qq_official_api/api.py @@ -34,14 +34,19 @@ def handle_validation(body: dict, bot_secret: str): class QQOfficialClient: - def __init__(self, secret: str, token: str, app_id: str, logger: None): + def __init__(self, secret: str, token: str, app_id: str, logger: None, unified_mode: bool = False): + self.unified_mode = unified_mode self.app = Quart(__name__) - self.app.add_url_rule( - '/callback/command', - 'handle_callback', - self.handle_callback_request, - methods=['GET', 'POST'], - ) + + # 只有在非统一模式下才注册独立路由 + if not self.unified_mode: + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['GET', 'POST'], + ) + self.secret = secret self.token = token self.app_id = app_id @@ -82,10 +87,29 @@ class QQOfficialClient: raise Exception(f'获取access_token失败: {e}') async def handle_callback_request(self): - """处理回调请求""" + """处理回调请求(独立端口模式,使用全局 request)""" + return await self._handle_callback_internal(request) + + async def handle_unified_webhook(self, req): + """处理回调请求(统一 webhook 模式,显式传递 request)。 + + Args: + req: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self._handle_callback_internal(req) + + async def _handle_callback_internal(self, req): + """处理回调请求的内部实现。 + + Args: + req: Quart Request 对象 + """ try: # 读取请求数据 - body = await request.get_data() + body = await req.get_data() payload = json.loads(body) # 验证是否为回调验证请求 diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py index 241a42cf..6f869b93 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -8,14 +8,19 @@ import langbot_plugin.api.entities.builtin.platform.events as platform_events class SlackClient: - def __init__(self, bot_token: str, signing_secret: str, logger: None): + def __init__(self, bot_token: str, signing_secret: str, logger: None, unified_mode: bool = False): self.bot_token = bot_token self.signing_secret = signing_secret + self.unified_mode = unified_mode 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'] - ) + + # 只有在非统一模式下才注册独立路由 + if not self.unified_mode: + self.app.add_url_rule( + '/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'] + ) + self._message_handlers = { 'example': [], } @@ -23,8 +28,28 @@ class SlackClient: self.logger = logger async def handle_callback_request(self): + """处理回调请求(独立端口模式,使用全局 request)""" + return await self._handle_callback_internal(request) + + async def handle_unified_webhook(self, req): + """处理回调请求(统一 webhook 模式,显式传递 request)。 + + Args: + req: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self._handle_callback_internal(req) + + async def _handle_callback_internal(self, req): + """处理回调请求的内部实现。 + + Args: + req: Quart Request 对象 + """ try: - body = await request.get_data() + body = await req.get_data() data = json.loads(body) if 'type' in data: if data['type'] == 'url_verification': diff --git a/libs/wecom_ai_bot_api/api.py b/libs/wecom_ai_bot_api/api.py index b20c6ed3..2ac01b89 100644 --- a/libs/wecom_ai_bot_api/api.py +++ b/libs/wecom_ai_bot_api/api.py @@ -200,7 +200,7 @@ class StreamSessionManager: class WecomBotClient: - def __init__(self, Token: str, EnCodingAESKey: str, Corpid: str, logger: EventLogger): + def __init__(self, Token: str, EnCodingAESKey: str, Corpid: str, logger: EventLogger, unified_mode: bool = False): """企业微信智能机器人客户端。 Args: @@ -208,6 +208,7 @@ class WecomBotClient: EnCodingAESKey: 企业微信消息加解密密钥。 Corpid: 企业 ID。 logger: 日志记录器。 + unified_mode: 是否使用统一 webhook 模式(默认 False)。 Example: >>> client = WecomBotClient(Token='token', EnCodingAESKey='aeskey', Corpid='corp', logger=logger) @@ -217,13 +218,18 @@ class WecomBotClient: self.EnCodingAESKey = EnCodingAESKey self.Corpid = Corpid self.ReceiveId = '' + self.unified_mode = unified_mode self.app = Quart(__name__) - self.app.add_url_rule( - '/callback/command', - 'handle_callback', - self.handle_callback_request, - methods=['POST', 'GET'] - ) + + # 只有在非统一模式下才注册独立路由 + if not self.unified_mode: + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['POST', 'GET'] + ) + self._message_handlers = { 'example': [], } @@ -362,7 +368,7 @@ class WecomBotClient: return await self._encrypt_and_reply(payload, nonce) async def handle_callback_request(self): - """企业微信回调入口。 + """企业微信回调入口(独立端口模式,使用全局 request)。 Returns: Quart Response: 根据请求类型返回验证、首包或刷新结果。 @@ -370,15 +376,34 @@ class WecomBotClient: Example: 作为 Quart 路由处理函数直接注册并使用。 """ + return await self._handle_callback_internal(request) + + async def handle_unified_webhook(self, req): + """处理回调请求(统一 webhook 模式,显式传递 request)。 + + Args: + req: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self._handle_callback_internal(req) + + async def _handle_callback_internal(self, req): + """处理回调请求的内部实现,包括 GET 验证和 POST 消息接收。 + + Args: + req: Quart Request 对象 + """ try: self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '') - await self.logger.info(f'{request.method} {request.url} {str(request.args)}') + await self.logger.info(f'{req.method} {req.url} {str(req.args)}') - if request.method == 'GET': - return await self._handle_get_callback() + if req.method == 'GET': + return await self._handle_get_callback(req) - if request.method == 'POST': - return await self._handle_post_callback() + if req.method == 'POST': + return await self._handle_post_callback(req) return Response('', status=405) @@ -386,13 +411,13 @@ class WecomBotClient: await self.logger.error(traceback.format_exc()) return Response('Internal Server Error', status=500) - async def _handle_get_callback(self) -> tuple[Response, int] | Response: + async def _handle_get_callback(self, req) -> tuple[Response, int] | Response: """处理企业微信的 GET 验证请求。""" - msg_signature = unquote(request.args.get('msg_signature', '')) - timestamp = unquote(request.args.get('timestamp', '')) - nonce = unquote(request.args.get('nonce', '')) - echostr = unquote(request.args.get('echostr', '')) + msg_signature = unquote(req.args.get('msg_signature', '')) + timestamp = unquote(req.args.get('timestamp', '')) + nonce = unquote(req.args.get('nonce', '')) + echostr = unquote(req.args.get('echostr', '')) if not all([msg_signature, timestamp, nonce, echostr]): await self.logger.error('请求参数缺失') @@ -405,16 +430,16 @@ class WecomBotClient: return Response(decrypted_str, mimetype='text/plain') - async def _handle_post_callback(self) -> tuple[Response, int] | Response: + async def _handle_post_callback(self, req) -> tuple[Response, int] | Response: """处理企业微信的 POST 回调请求。""" self.stream_sessions.cleanup() - msg_signature = unquote(request.args.get('msg_signature', '')) - timestamp = unquote(request.args.get('timestamp', '')) - nonce = unquote(request.args.get('nonce', '')) + msg_signature = unquote(req.args.get('msg_signature', '')) + timestamp = unquote(req.args.get('timestamp', '')) + nonce = unquote(req.args.get('nonce', '')) - encrypted_json = await request.get_json() + encrypted_json = await req.get_json() encrypted_msg = (encrypted_json or {}).get('encrypt', '') if not encrypted_msg: await self.logger.error("请求体中缺少 'encrypt' 字段") diff --git a/pkg/api/http/service/bot.py b/pkg/api/http/service/bot.py index 8f2f1e5f..497ef4ad 100644 --- a/pkg/api/http/service/bot.py +++ b/pkg/api/http/service/bot.py @@ -59,8 +59,8 @@ class BotService: adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id # 为支持统一 webhook 的适配器生成 webhook URL - # 目前只有 wecom 支持 - if persistence_bot['adapter'] == 'wecom': + # 支持:wecom、wecombot、officialaccount、qqofficial、slack + if persistence_bot['adapter'] in ['wecom', 'wecombot', 'officialaccount', 'qqofficial', 'slack']: api_port = self.ap.instance_config.data['api']['port'] webhook_url = f"/bots/{bot_uuid}" adapter_runtime_values['webhook_url'] = webhook_url diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 1f70f3eb..3d339fa0 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -59,14 +59,16 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd message_converter: OAMessageConverter = OAMessageConverter() event_converter: OAEventConverter = OAEventConverter() bot: typing.Union[OAClient, OAClientForLongerResponse] = pydantic.Field(exclude=True) + bot_uuid: str = None def __init__(self, config: dict, logger: EventLogger): + # 校验必填项 required_keys = ['token', 'EncodingAESKey', 'AppSecret', 'AppID', 'Mode'] missing_keys = [k for k in required_keys if k not in config] if missing_keys: raise Exception(f'OfficialAccount 缺少配置项: {missing_keys}') - + # 创建运行时 bot 对象,始终使用统一 webhook 模式 if config['Mode'] == 'drop': bot = OAClient( token=config['token'], @@ -74,6 +76,7 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd Appsecret=config['AppSecret'], AppID=config['AppID'], logger=logger, + unified_mode=True, ) elif config['Mode'] == 'passive': bot = OAClientForLongerResponse( @@ -83,13 +86,14 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd AppID=config['AppID'], LoadingMessage=config.get('LoadingMessage', ''), logger=logger, + unified_mode=True, ) else: raise KeyError('请设置微信公众号通信模式') bot_account_id = config.get('AppID', '') - + super().__init__( bot=bot, bot_account_id=bot_account_id, @@ -136,16 +140,45 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd elif event_type == platform_events.GroupMessage: pass + def set_bot_uuid(self, bot_uuid: str): + """设置 bot UUID(用于生成 webhook URL)""" + self.bot_uuid = bot_uuid + + async def handle_unified_webhook(self, bot_uuid: str, path: str, request): + """处理统一 webhook 请求。 + + Args: + bot_uuid: Bot 的 UUID + path: 子路径(如果有的话) + request: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self.bot.handle_unified_webhook(request) + async def run_async(self): - async def shutdown_trigger_placeholder(): + # 统一 webhook 模式下,不启动独立的 Quart 应用 + # 保持运行但不启动独立端口 + + # 打印 webhook 回调地址 + if self.bot_uuid and hasattr(self.logger, 'ap'): + try: + api_port = self.logger.ap.instance_config.data['api']['port'] + webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}" + webhook_url_public = f"http://:{api_port}/bots/{self.bot_uuid}" + + await self.logger.info(f"微信公众号 Webhook 回调地址:") + await self.logger.info(f" 本地地址: {webhook_url}") + await self.logger.info(f" 公网地址: {webhook_url_public}") + await self.logger.info(f"请在微信公众号后台配置此回调地址") + except Exception as e: + await self.logger.warning(f"无法生成 webhook URL: {e}") + + async def keep_alive(): while True: await asyncio.sleep(1) - - await self.bot.run_task( - host=self.config['host'], - port=self.config['port'], - shutdown_trigger=shutdown_trigger_placeholder, - ) + await keep_alive() async def kill(self) -> bool: return False diff --git a/pkg/platform/sources/officialaccount.yaml b/pkg/platform/sources/officialaccount.yaml index 1a93c0bc..53345413 100644 --- a/pkg/platform/sources/officialaccount.yaml +++ b/pkg/platform/sources/officialaccount.yaml @@ -53,23 +53,6 @@ spec: type: string required: true default: "AI正在思考中,请发送任意内容获取回复。" - - name: host - label: - en_US: Host - zh_Hans: 监听主机 - description: - en_US: The host that Official Account listens on for Webhook connections. - zh_Hans: 微信公众号监听的主机,除非你知道自己在做什么,否则请写 0.0.0.0 - type: string - required: true - default: 0.0.0.0 - - name: port - label: - en_US: Port - zh_Hans: 监听端口 - type: integer - required: true - default: 2287 execution: python: path: ./officialaccount.py diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index 240b46d0..91989126 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -135,12 +135,17 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter bot: QQOfficialClient config: dict bot_account_id: str + bot_uuid: str = None message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter() event_converter: QQOfficialEventConverter = QQOfficialEventConverter() def __init__(self, config: dict, logger: EventLogger): bot = QQOfficialClient( - app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger + app_id=config['appid'], + secret=config['secret'], + token=config['token'], + logger=logger, + unified_mode=True ) super().__init__( @@ -226,16 +231,45 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter self.bot.on_message('GROUP_AT_MESSAGE_CREATE')(on_message) self.bot.on_message('AT_MESSAGE_CREATE')(on_message) + def set_bot_uuid(self, bot_uuid: str): + """设置 bot UUID(用于生成 webhook URL)""" + self.bot_uuid = bot_uuid + + async def handle_unified_webhook(self, bot_uuid: str, path: str, request): + """处理统一 webhook 请求。 + + Args: + bot_uuid: Bot 的 UUID + path: 子路径(如果有的话) + request: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self.bot.handle_unified_webhook(request) + async def run_async(self): - async def shutdown_trigger_placeholder(): + # 统一 webhook 模式下,不启动独立的 Quart 应用 + # 保持运行但不启动独立端口 + + # 打印 webhook 回调地址 + if self.bot_uuid and hasattr(self.logger, 'ap'): + try: + api_port = self.logger.ap.instance_config.data['api']['port'] + webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}" + webhook_url_public = f"http://:{api_port}/bots/{self.bot_uuid}" + + await self.logger.info(f"QQ 官方机器人 Webhook 回调地址:") + await self.logger.info(f" 本地地址: {webhook_url}") + await self.logger.info(f" 公网地址: {webhook_url_public}") + await self.logger.info(f"请在 QQ 官方机器人后台配置此回调地址") + except Exception as e: + await self.logger.warning(f"无法生成 webhook URL: {e}") + + async def keep_alive(): 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, - ) + await keep_alive() async def kill(self) -> bool: return False diff --git a/pkg/platform/sources/qqofficial.yaml b/pkg/platform/sources/qqofficial.yaml index e3f39ce1..54d800bb 100644 --- a/pkg/platform/sources/qqofficial.yaml +++ b/pkg/platform/sources/qqofficial.yaml @@ -25,13 +25,6 @@ spec: type: string required: true default: "" - - name: port - label: - en_US: Port - zh_Hans: 监听端口 - type: integer - required: true - default: 2284 - name: token label: en_US: Token diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index e08cc8c0..d575ee92 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -86,6 +86,7 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter): class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: SlackClient bot_account_id: str + bot_uuid: str = None message_converter: SlackMessageConverter = SlackMessageConverter() event_converter: SlackEventConverter = SlackEventConverter() config: dict @@ -102,7 +103,10 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员') self.bot = SlackClient( - bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger + bot_token=self.config['bot_token'], + signing_secret=self.config['signing_secret'], + logger=self.logger, + unified_mode=True ) async def reply_message( @@ -148,16 +152,45 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): elif event_type == platform_events.GroupMessage: self.bot.on_message('channel')(on_message) + def set_bot_uuid(self, bot_uuid: str): + """设置 bot UUID(用于生成 webhook URL)""" + self.bot_uuid = bot_uuid + + async def handle_unified_webhook(self, bot_uuid: str, path: str, request): + """处理统一 webhook 请求。 + + Args: + bot_uuid: Bot 的 UUID + path: 子路径(如果有的话) + request: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self.bot.handle_unified_webhook(request) + async def run_async(self): - async def shutdown_trigger_placeholder(): + # 统一 webhook 模式下,不启动独立的 Quart 应用 + # 保持运行但不启动独立端口 + + # 打印 webhook 回调地址 + if self.bot_uuid and hasattr(self.logger, 'ap'): + try: + api_port = self.logger.ap.instance_config.data['api']['port'] + webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}" + webhook_url_public = f"http://:{api_port}/bots/{self.bot_uuid}" + + await self.logger.info(f"Slack 机器人 Webhook 回调地址:") + await self.logger.info(f" 本地地址: {webhook_url}") + await self.logger.info(f" 公网地址: {webhook_url_public}") + await self.logger.info(f"请在 Slack 后台配置此回调地址") + except Exception as e: + await self.logger.warning(f"无法生成 webhook URL: {e}") + + async def keep_alive(): 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, - ) + await keep_alive() async def kill(self) -> bool: return False diff --git a/pkg/platform/sources/slack.yaml b/pkg/platform/sources/slack.yaml index 69d96187..13d303bb 100644 --- a/pkg/platform/sources/slack.yaml +++ b/pkg/platform/sources/slack.yaml @@ -25,13 +25,6 @@ spec: type: string required: true default: "" - - name: port - label: - en_US: Port - zh_Hans: 监听端口 - type: int - required: true - default: 2288 execution: python: path: ./slack.py diff --git a/pkg/platform/sources/wecombot.py b/pkg/platform/sources/wecombot.py index 13dd8e92..d2d363f9 100644 --- a/pkg/platform/sources/wecombot.py +++ b/pkg/platform/sources/wecombot.py @@ -88,19 +88,22 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): message_converter: WecomBotMessageConverter = WecomBotMessageConverter() event_converter: WecomBotEventConverter = WecomBotEventConverter() config: dict + bot_uuid: str = None def __init__(self, config: dict, logger: EventLogger): - required_keys = ['Token', 'EncodingAESKey', 'Corpid', 'BotId', 'port'] + + required_keys = ['Token', 'EncodingAESKey', 'Corpid', 'BotId'] missing_keys = [key for key in required_keys if key not in config] if missing_keys: raise Exception(f'WecomBot 缺少配置项: {missing_keys}') - # 创建运行时 bot 对象 + bot = WecomBotClient( Token=config['Token'], EnCodingAESKey=config['EncodingAESKey'], Corpid=config['Corpid'], logger=logger, + unified_mode=True, ) bot_account_id = config['BotId'] @@ -182,18 +185,46 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): self.bot.on_message('group')(on_message) except Exception: print(traceback.format_exc()) - + + def set_bot_uuid(self, bot_uuid: str): + """设置 bot UUID(用于生成 webhook URL)""" + self.bot_uuid = bot_uuid + + async def handle_unified_webhook(self, bot_uuid: str, path: str, request): + """处理统一 webhook 请求。 + + Args: + bot_uuid: Bot 的 UUID + path: 子路径(如果有的话) + request: Quart Request 对象 + + Returns: + 响应数据 + """ + return await self.bot.handle_unified_webhook(request) async def run_async(self): - async def shutdown_trigger_placeholder(): + # 统一 webhook 模式下,不启动独立的 Quart 应用 + # 保持运行但不启动独立端口 + + # 打印 webhook 回调地址 + if self.bot_uuid and hasattr(self.logger, 'ap'): + try: + api_port = self.logger.ap.instance_config.data['api']['port'] + webhook_url = f"http://127.0.0.1:{api_port}/bots/{self.bot_uuid}" + webhook_url_public = f"http://:{api_port}/bots/{self.bot_uuid}" + + await self.logger.info(f"企业微信机器人 Webhook 回调地址:") + await self.logger.info(f" 本地地址: {webhook_url}") + await self.logger.info(f" 公网地址: {webhook_url_public}") + await self.logger.info(f"请在企业微信后台配置此回调地址") + except Exception as e: + await self.logger.warning(f"无法生成 webhook URL: {e}") + + async def keep_alive(): 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, - ) + await keep_alive() async def kill(self) -> bool: return False diff --git a/pkg/platform/sources/wecombot.yaml b/pkg/platform/sources/wecombot.yaml index 487c2df6..ceb97b7b 100644 --- a/pkg/platform/sources/wecombot.yaml +++ b/pkg/platform/sources/wecombot.yaml @@ -11,13 +11,6 @@ metadata: icon: wecombot.png spec: config: - - name: port - label: - en_US: Port - zh_Hans: 监听端口 - type: integer - required: true - default: 2291 - name: Corpid label: en_US: Corpid