From 8b935175bdbf26acc8dc9c45f37bc5c6aae414b9 Mon Sep 17 00:00:00 2001 From: wangcham Date: Fri, 5 Sep 2025 08:08:49 +0000 Subject: [PATCH 1/4] feat:wecom ai bot --- libs/wecom_ai_bot_api/WXBizMsgCrypt3.py | 278 +++++++++++++++++++++++ libs/wecom_ai_bot_api/api.py | 287 ++++++++++++++++++++++++ libs/wecom_ai_bot_api/ierror.py | 20 ++ libs/wecom_ai_bot_api/wecombotevent.py | 60 +++++ pkg/platform/sources/wecombot.png | Bin 0 -> 12126 bytes pkg/platform/sources/wecombot.py | 158 +++++++++++++ pkg/platform/sources/wecombot.yaml | 62 +++++ 7 files changed, 865 insertions(+) create mode 100644 libs/wecom_ai_bot_api/WXBizMsgCrypt3.py create mode 100644 libs/wecom_ai_bot_api/api.py create mode 100644 libs/wecom_ai_bot_api/ierror.py create mode 100644 libs/wecom_ai_bot_api/wecombotevent.py create mode 100644 pkg/platform/sources/wecombot.png create mode 100644 pkg/platform/sources/wecombot.py create mode 100644 pkg/platform/sources/wecombot.yaml diff --git a/libs/wecom_ai_bot_api/WXBizMsgCrypt3.py b/libs/wecom_ai_bot_api/WXBizMsgCrypt3.py new file mode 100644 index 00000000..96e9367f --- /dev/null +++ b/libs/wecom_ai_bot_api/WXBizMsgCrypt3.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# -*- encoding:utf-8 -*- + +"""对企业微信发送给企业后台的消息加解密示例代码. +@copyright: Copyright (c) 1998-2014 Tencent Inc. + +""" + +# ------------------------------------------------------------------------ +import logging +import base64 +import random +import hashlib +import time +import struct +from Crypto.Cipher import AES +import xml.etree.cElementTree as ET +import socket +from libs.wecom_ai_bot_api import ierror + + +""" +Crypto.Cipher包已不再维护,开发者可以通过以下命令下载安装最新版的加解密工具包 + pip install pycryptodome +""" + + +class FormatException(Exception): + pass + + +def throw_exception(message, exception_class=FormatException): + """my define raise exception function""" + raise exception_class(message) + + +class SHA1: + """计算企业微信的消息签名接口""" + + def getSHA1(self, token, timestamp, nonce, encrypt): + """用SHA1算法生成安全签名 + @param token: 票据 + @param timestamp: 时间戳 + @param encrypt: 密文 + @param nonce: 随机字符串 + @return: 安全签名 + """ + try: + sortlist = [token, timestamp, nonce, encrypt] + sortlist.sort() + sha = hashlib.sha1() + sha.update(''.join(sortlist).encode()) + return ierror.WXBizMsgCrypt_OK, sha.hexdigest() + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_ComputeSignature_Error, None + + +class XMLParse: + """提供提取消息格式中的密文及生成回复消息格式的接口""" + + # xml消息模板 + AES_TEXT_RESPONSE_TEMPLATE = """ + + +%(timestamp)s + +""" + + def extract(self, xmltext): + """提取出xml数据包中的加密消息 + @param xmltext: 待提取的xml字符串 + @return: 提取出的加密消息字符串 + """ + try: + xml_tree = ET.fromstring(xmltext) + encrypt = xml_tree.find('Encrypt') + return ierror.WXBizMsgCrypt_OK, encrypt.text + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_ParseXml_Error, None + + def generate(self, encrypt, signature, timestamp, nonce): + """生成xml消息 + @param encrypt: 加密后的消息密文 + @param signature: 安全签名 + @param timestamp: 时间戳 + @param nonce: 随机字符串 + @return: 生成的xml字符串 + """ + resp_dict = { + 'msg_encrypt': encrypt, + 'msg_signaturet': signature, + 'timestamp': timestamp, + 'nonce': nonce, + } + resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict + return resp_xml + + +class PKCS7Encoder: + """提供基于PKCS7算法的加解密接口""" + + block_size = 32 + + def encode(self, text): + """对需要加密的明文进行填充补位 + @param text: 需要进行填充补位操作的明文 + @return: 补齐明文字符串 + """ + text_length = len(text) + # 计算需要填充的位数 + amount_to_pad = self.block_size - (text_length % self.block_size) + if amount_to_pad == 0: + amount_to_pad = self.block_size + # 获得补位所用的字符 + pad = chr(amount_to_pad) + return text + (pad * amount_to_pad).encode() + + def decode(self, decrypted): + """删除解密后明文的补位字符 + @param decrypted: 解密后的明文 + @return: 删除补位字符后的明文 + """ + pad = ord(decrypted[-1]) + if pad < 1 or pad > 32: + pad = 0 + return decrypted[:-pad] + + +class Prpcrypt(object): + """提供接收和推送给企业微信消息的加解密接口""" + + def __init__(self, key): + # self.key = base64.b64decode(key+"=") + self.key = key + # 设置加解密模式为AES的CBC模式 + self.mode = AES.MODE_CBC + + def encrypt(self, text, receiveid): + """对明文进行加密 + @param text: 需要加密的明文 + @return: 加密得到的字符串 + """ + # 16位随机字符串添加到明文开头 + text = text.encode() + text = self.get_random_str() + struct.pack('I', socket.htonl(len(text))) + text + receiveid.encode() + + # 使用自定义的填充方式对明文进行补位填充 + pkcs7 = PKCS7Encoder() + text = pkcs7.encode(text) + # 加密 + cryptor = AES.new(self.key, self.mode, self.key[:16]) + try: + ciphertext = cryptor.encrypt(text) + # 使用BASE64对加密后的字符串进行编码 + return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_EncryptAES_Error, None + + def decrypt(self, text, receiveid): + """对解密后的明文进行补位删除 + @param text: 密文 + @return: 删除填充补位后的明文 + """ + try: + cryptor = AES.new(self.key, self.mode, self.key[:16]) + # 使用BASE64对密文进行解码,然后AES-CBC解密 + plain_text = cryptor.decrypt(base64.b64decode(text)) + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_DecryptAES_Error, None + try: + pad = plain_text[-1] + # 去掉补位字符串 + # pkcs7 = PKCS7Encoder() + # plain_text = pkcs7.encode(plain_text) + # 去除16位随机字符串 + content = plain_text[16:-pad] + xml_len = socket.ntohl(struct.unpack('I', content[:4])[0]) + xml_content = content[4 : xml_len + 4] + from_receiveid = content[xml_len + 4 :] + except Exception as e: + logger = logging.getLogger() + logger.error(e) + return ierror.WXBizMsgCrypt_IllegalBuffer, None + + if from_receiveid.decode('utf8') != receiveid: + return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None + return 0, xml_content + + def get_random_str(self): + """随机生成16位字符串 + @return: 16位字符串 + """ + return str(random.randint(1000000000000000, 9999999999999999)).encode() + + +class WXBizMsgCrypt(object): + # 构造函数 + def __init__(self, sToken, sEncodingAESKey, sReceiveId): + try: + self.key = base64.b64decode(sEncodingAESKey + '=') + assert len(self.key) == 32 + except Exception: + throw_exception('[error]: EncodingAESKey unvalid !', FormatException) + # return ierror.WXBizMsgCrypt_IllegalAesKey,None + self.m_sToken = sToken + self.m_sReceiveId = sReceiveId + + # 验证URL + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sEchoStr: 随机串,对应URL参数的echostr + # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 + # @return:成功0,失败返回对应的错误码 + + def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId) + return ret, sReplyEchoStr + + def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): + # 将企业回复用户的消息加密打包 + # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 + # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 + # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce + # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, + # return:成功0,sEncryptMsg,失败返回对应的错误码None + pc = Prpcrypt(self.key) + ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) + encrypt = encrypt.decode('utf8') + if ret != 0: + return ret, None + if timestamp is None: + timestamp = str(int(time.time())) + # 生成安全签名 + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) + if ret != 0: + return ret, None + xmlParse = XMLParse() + return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) + + def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): + # 检验消息的真实性,并且获取解密后的明文 + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sPostData: 密文,对应POST请求的数据 + # xml_content: 解密后的原文,当return返回0时有效 + # @return: 成功0,失败返回对应的错误码 + # 验证安全签名 + xmlParse = XMLParse() + ret, encrypt = xmlParse.extract(sPostData) + if ret != 0: + return ret, None + sha1 = SHA1() + ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) + if ret != 0: + return ret, None + if not signature == sMsgSignature: + return ierror.WXBizMsgCrypt_ValidateSignature_Error, None + pc = Prpcrypt(self.key) + ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId) + return ret, xml_content diff --git a/libs/wecom_ai_bot_api/api.py b/libs/wecom_ai_bot_api/api.py new file mode 100644 index 00000000..e6abc4ac --- /dev/null +++ b/libs/wecom_ai_bot_api/api.py @@ -0,0 +1,287 @@ +import json +import time +import uuid +import xml.etree.ElementTree as ET +from urllib.parse import unquote +import hashlib +import traceback + +import httpx +from libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt +from quart import Quart, request, Response, jsonify +from pkg.platform.types import message as platform_message +import asyncio +from libs.wecom_ai_bot_api import wecombotevent +from typing import Callable +import base64 +from Crypto.Cipher import AES + + +class WecomBotClient: + def __init__(self,Token:str,EnCodingAESKey:str,Corpid:str,logger:None): + self.Token=Token + self.EnCodingAESKey=EnCodingAESKey + self.Corpid=Corpid + self.ReceiveId = '' + self.app = Quart(__name__) + self.app.add_url_rule( + '/callback/command', + 'handle_callback', + self.handle_callback_request, + methods=['POST','GET'] + ) + self._message_handlers = { + 'example': [], + } + self.user_stream_map = {} + self.logger = logger + self.generated_content = {} + self.msg_id_map = {} + + async def sha1_signature(token: str, timestamp: str, nonce: str, encrypt: str) -> str: + raw = "".join(sorted([token, timestamp, nonce, encrypt])) + return hashlib.sha1(raw.encode("utf-8")).hexdigest() + + async def handle_callback_request(self): + try: + self.wxcpt=WXBizMsgCrypt(self.Token,self.EnCodingAESKey,'') + + if request.method == "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", "")) + + if not all([msg_signature, timestamp, nonce, echostr]): + await self.logger.error("请求参数缺失") + return Response("缺少参数", status=400) + + ret, decrypted_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) + if ret != 0: + + await self.logger.error("验证URL失败") + return Response("验证失败", status=403) + + return Response(decrypted_str, mimetype="text/plain") + + elif request.method == "POST": + msg_signature = unquote(request.args.get("msg_signature", "")) + timestamp = unquote(request.args.get("timestamp", "")) + nonce = unquote(request.args.get("nonce", "")) + + try: + timeout = 3.2 + interval = 0.1 + start_time = time.monotonic() + encrypted_json = await request.get_json() + encrypted_msg = encrypted_json.get("encrypt", "") + if not encrypted_msg: + await self.logger.error("请求体中缺少 'encrypt' 字段") + + xml_post_data = f"" + ret, decrypted_xml = self.wxcpt.DecryptMsg(xml_post_data, msg_signature, timestamp, nonce) + if ret != 0: + await self.logger.error("解密失败") + + + msg_json = json.loads(decrypted_xml) + + from_user_id = msg_json.get("from", {}).get("userid") + chatid = msg_json.get("chatid", "") + + message_data = await self.get_message(msg_json) + + + + if message_data: + try: + event = wecombotevent.WecomBotEvent(message_data) + if event: + await self._handle_message(event) + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + + start_time = time.time() + try: + if msg_json.get('chattype','') == 'single': + if from_user_id in self.user_stream_map: + stream_id = self.user_stream_map[from_user_id] + else: + stream_id =str(uuid.uuid4()) + self.user_stream_map[from_user_id] = stream_id + + + else: + + if chatid in self.user_stream_map: + stream_id = self.user_stream_map[chatid] + else: + stream_id = str(uuid.uuid4()) + self.user_stream_map[chatid] = stream_id + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + while True: + content = self.generated_content.pop(msg_json['msgid'],None) + if content: + reply_plain = { + "msgtype": "stream", + "stream": { + "id": stream_id, + "finish": True, + "content": content + } + } + reply_plain_str = json.dumps(reply_plain, ensure_ascii=False) + + reply_timestamp = str(int(time.time())) + ret, encrypt_text = self.wxcpt.EncryptMsg(reply_plain_str, nonce, reply_timestamp) + if ret != 0: + await self.logger.error("加密失败") + + + root = ET.fromstring(encrypt_text) + encrypt = root.find("Encrypt").text + resp = { + "encrypt": encrypt, + } + return jsonify(resp), 200 + + if time.time() - start_time > timeout: + break + + await asyncio.sleep(interval) + + if self.msg_id_map.get(message_data['msgid'], 1) == 3: + print('请求失效:暂不支持智能机器人超过10秒的请求,如有需求,请联系 LangBot 团队。') + return '' + + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + + except Exception as e: + await self.logger.error(traceback.format_exc()) + print(traceback.format_exc()) + + + async def get_message(self,msg_json): + message_data = {} + + if msg_json.get('chattype','') == 'single': + message_data['type'] = 'single' + elif msg_json.get('chattype','') == 'group': + message_data['type'] = 'group' + + if msg_json.get('msgtype') == 'text': + message_data['content'] = msg_json.get('text',{}).get('content') + elif msg_json.get('msgtype') == 'image': + picurl = msg_json.get('image', {}).get('url','') + base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey) + message_data['picurl'] = base64 + elif msg_json.get('msgtype') == 'mixed': + items = msg_json.get('mixed', {}).get('msg_item', []) + texts = [] + picurl = None + for item in items: + if item.get('msgtype') == 'text': + texts.append(item.get('text', {}).get('content', '')) + elif item.get('msgtype') == 'image' and picurl is None: + picurl = item.get('image', {}).get('url') + + if texts: + message_data['content'] = "".join(texts) # 拼接所有 text + if picurl: + base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey) + message_data['picurl'] = base64 # 只保留第一个 image + + message_data['userid'] = msg_json.get('from', {}).get('userid', '') + message_data['msgid'] = msg_json.get('msgid', '') + + if msg_json.get('aibotid'): + message_data['aibotid'] = msg_json.get('aibotid', '') + + return message_data + + async def _handle_message(self, event: wecombotevent.WecomBotEvent): + """ + 处理消息事件。 + """ + try: + message_id = event.message_id + if message_id in self.msg_id_map.keys(): + self.msg_id_map[message_id] += 1 + return + self.msg_id_map[message_id] = 1 + msg_type = event.type + if msg_type in self._message_handlers: + for handler in self._message_handlers[msg_type]: + await handler(event) + except Exception: + print(traceback.format_exc()) + + async def set_message(self, msg_id: str, content: str): + self.generated_content[msg_id] = content + + def on_message(self, msg_type: str): + def decorator(func: Callable[[wecombotevent.WecomBotEvent], 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 download_url_to_base64(self, download_url, encoding_aes_key): + async with httpx.AsyncClient() as client: + response = await client.get(download_url) + if response.status_code != 200: + await self.logger.error(f'failed to get file: {response.text}') + return None + + encrypted_bytes = response.content + + + aes_key = base64.b64decode(encoding_aes_key + "=") # base64 补齐 + iv = aes_key[:16] + + + cipher = AES.new(aes_key, AES.MODE_CBC, iv) + decrypted = cipher.decrypt(encrypted_bytes) + + + pad_len = decrypted[-1] + decrypted = decrypted[:-pad_len] + + + if decrypted.startswith(b"\xff\xd8"): # JPEG + mime_type = "image/jpeg" + elif decrypted.startswith(b"\x89PNG"): # PNG + mime_type = "image/png" + elif decrypted.startswith((b"GIF87a", b"GIF89a")): # GIF + mime_type = "image/gif" + elif decrypted.startswith(b"BM"): # BMP + mime_type = "image/bmp" + elif decrypted.startswith(b"II*\x00") or decrypted.startswith(b"MM\x00*"): # TIFF + mime_type = "image/tiff" + else: + mime_type = "application/octet-stream" + + # 转 base64 + base64_str = base64.b64encode(decrypted).decode("utf-8") + return f"data:{mime_type};base64,{base64_str}" + + + 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/wecom_ai_bot_api/ierror.py b/libs/wecom_ai_bot_api/ierror.py new file mode 100644 index 00000000..6c7ca122 --- /dev/null +++ b/libs/wecom_ai_bot_api/ierror.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +######################################################################### +# Author: jonyqin +# Created Time: Thu 11 Sep 2014 01:53:58 PM CST +# File Name: ierror.py +# Description:定义错误码含义 +######################################################################### +WXBizMsgCrypt_OK = 0 +WXBizMsgCrypt_ValidateSignature_Error = -40001 +WXBizMsgCrypt_ParseXml_Error = -40002 +WXBizMsgCrypt_ComputeSignature_Error = -40003 +WXBizMsgCrypt_IllegalAesKey = -40004 +WXBizMsgCrypt_ValidateCorpid_Error = -40005 +WXBizMsgCrypt_EncryptAES_Error = -40006 +WXBizMsgCrypt_DecryptAES_Error = -40007 +WXBizMsgCrypt_IllegalBuffer = -40008 +WXBizMsgCrypt_EncodeBase64_Error = -40009 +WXBizMsgCrypt_DecodeBase64_Error = -40010 +WXBizMsgCrypt_GenReturnXml_Error = -40011 diff --git a/libs/wecom_ai_bot_api/wecombotevent.py b/libs/wecom_ai_bot_api/wecombotevent.py new file mode 100644 index 00000000..f2edeac7 --- /dev/null +++ b/libs/wecom_ai_bot_api/wecombotevent.py @@ -0,0 +1,60 @@ +from typing import Dict, Any, Optional + + +class WecomBotEvent(dict): + @staticmethod + def from_payload(payload: Dict[str, Any]) -> Optional['WecomBotEvent']: + try: + event = WecomBotEvent(payload) + return event + except KeyError: + return None + + @property + def type(self) -> str: + """ + 事件类型 + """ + return self.get('type', '') + + @property + def userid(self) -> str: + """ + 用户id + """ + return self.get('from', {}).get('userid', '') + + @property + def content(self) -> str: + """ + 内容 + """ + return self.get('content', '') + + @property + def picurl(self) -> str: + """ + 图片url + """ + return self.get('picurl', '') + + @property + def chatid(self) -> str: + """ + 群组id + """ + return self.get('chatid', {}) + + @property + def message_id(self) -> str: + """ + 消息id + """ + return self.get('msgid', '') + + @property + def ai_bot_id(self) -> str: + """ + AI Bot ID + """ + return self.get('aibotid', '') diff --git a/pkg/platform/sources/wecombot.png b/pkg/platform/sources/wecombot.png new file mode 100644 index 0000000000000000000000000000000000000000..0734efaf05e347c03008402a91cb9ad367166e8f GIT binary patch literal 12126 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 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/wecombot.yaml b/pkg/platform/sources/wecombot.yaml new file mode 100644 index 00000000..766bd904 --- /dev/null +++ b/pkg/platform/sources/wecombot.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: wecombot + label: + en_US: WeComBot + zh_Hans: 企业微信智能机器人 + description: + en_US: WeComBot Adapter + zh_Hans: 企业微信智能机器人适配器,请查看文档了解使用方式 + icon: wecombot.png +spec: + config: + - name: host + label: + en_US: Host + zh_Hans: 监听主机 + description: + en_US: Webhook host, unless you know what you're doing, please write 0.0.0.0 + zh_Hans: Webhook 监听主机,除非你知道自己在做什么,否则请写 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: 2291 + - name: Corpid + label: + en_US: Corpid + zh_Hans: 企业ID + type: string + required: true + default: "" + - name: Token + label: + en_US: Token + zh_Hans: 令牌 + type: string + required: true + default: "" + - name: EncodingAESKey + label: + en_US: EncodingAESKey + zh_Hans: 消息加解密密钥 + type: string + required: true + default: "" + - name: BotId + label: + en_US: BotId + zh_Hans: 机器人ID + type: string + required: true + default: "" +execution: + python: + path: ./wecombot.py + attr: WecomBotAdapter \ No newline at end of file From 72ec4b77d6c9605ada6f06ad9d1d62acb3e43a24 Mon Sep 17 00:00:00 2001 From: wangcham Date: Sat, 6 Sep 2025 05:41:02 +0000 Subject: [PATCH 2/4] feat: fix bot id --- libs/wecom_ai_bot_api/api.py | 4 ++-- pkg/platform/sources/wecombot.py | 6 +++--- pkg/platform/sources/wecombot.yaml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/wecom_ai_bot_api/api.py b/libs/wecom_ai_bot_api/api.py index e6abc4ac..a30b960b 100644 --- a/libs/wecom_ai_bot_api/api.py +++ b/libs/wecom_ai_bot_api/api.py @@ -71,7 +71,7 @@ class WecomBotClient: nonce = unquote(request.args.get("nonce", "")) try: - timeout = 3.2 + timeout = 3 interval = 0.1 start_time = time.monotonic() encrypted_json = await request.get_json() @@ -155,7 +155,7 @@ class WecomBotClient: await asyncio.sleep(interval) if self.msg_id_map.get(message_data['msgid'], 1) == 3: - print('请求失效:暂不支持智能机器人超过10秒的请求,如有需求,请联系 LangBot 团队。') + print('请求失效:暂不支持智能机器人超过7秒的请求,如有需求,请联系 LangBot 团队。') return '' except Exception as e: diff --git a/pkg/platform/sources/wecombot.py b/pkg/platform/sources/wecombot.py index b2380120..e8e8592c 100644 --- a/pkg/platform/sources/wecombot.py +++ b/pkg/platform/sources/wecombot.py @@ -98,14 +98,14 @@ class WecomBotAdapter(adapter.MessagePlatformAdapter): missing_keys = [key for key in required_keys if key not in config] if missing_keys: raise ParamNotEnoughError('缺少相关配置项,请查看文档或联系管理员') - self.bot_account_id = self.config['BotId'] self.bot = WecomBotClient( Token=self.config['Token'], EnCodingAESKey=self.config['EncodingAESKey'], Corpid=self.config['Corpid'], - logger=self.logger + logger=self.logger, ) - + self.bot_account_id = self.config['BotId'] + async def reply_message(self, message_source:platform_events.MessageEvent, message:platform_message.MessageChain,quote_origin: bool = False): content = await self.message_converter.yiri2target(message) diff --git a/pkg/platform/sources/wecombot.yaml b/pkg/platform/sources/wecombot.yaml index 766bd904..960a0417 100644 --- a/pkg/platform/sources/wecombot.yaml +++ b/pkg/platform/sources/wecombot.yaml @@ -54,7 +54,7 @@ spec: en_US: BotId zh_Hans: 机器人ID type: string - required: true + required: false default: "" execution: python: From 2c6f127f471c8ccfc163b2a9debf544bba45b2d8 Mon Sep 17 00:00:00 2001 From: wangcham Date: Fri, 12 Sep 2025 05:53:31 +0000 Subject: [PATCH 3/4] feat: delete host config --- pkg/platform/sources/wecombot.py | 2 +- pkg/platform/sources/wecombot.yaml | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/pkg/platform/sources/wecombot.py b/pkg/platform/sources/wecombot.py index e8e8592c..6138174d 100644 --- a/pkg/platform/sources/wecombot.py +++ b/pkg/platform/sources/wecombot.py @@ -140,7 +140,7 @@ class WecomBotAdapter(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/pkg/platform/sources/wecombot.yaml b/pkg/platform/sources/wecombot.yaml index 960a0417..487c2df6 100644 --- a/pkg/platform/sources/wecombot.yaml +++ b/pkg/platform/sources/wecombot.yaml @@ -11,16 +11,6 @@ metadata: icon: wecombot.png spec: config: - - name: host - label: - en_US: Host - zh_Hans: 监听主机 - description: - en_US: Webhook host, unless you know what you're doing, please write 0.0.0.0 - zh_Hans: Webhook 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0 - type: string - required: true - default: "0.0.0.0" - name: port label: en_US: Port From d70196e7995e8489ccc37494a9002b5b6c9d4c61 Mon Sep 17 00:00:00 2001 From: WangCham <651122857@qq.com> Date: Sun, 14 Sep 2025 16:40:34 +0800 Subject: [PATCH 4/4] feat: modify for new plugin system --- libs/wecom_ai_bot_api/api.py | 11 ++++-- pkg/platform/sources/wecombot.py | 67 ++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/libs/wecom_ai_bot_api/api.py b/libs/wecom_ai_bot_api/api.py index a30b960b..6b5dc573 100644 --- a/libs/wecom_ai_bot_api/api.py +++ b/libs/wecom_ai_bot_api/api.py @@ -9,16 +9,18 @@ import traceback import httpx from libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt from quart import Quart, request, Response, jsonify -from pkg.platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import asyncio from libs.wecom_ai_bot_api import wecombotevent from typing import Callable import base64 from Crypto.Cipher import AES +from pkg.platform.logger import EventLogger + class WecomBotClient: - def __init__(self,Token:str,EnCodingAESKey:str,Corpid:str,logger:None): + def __init__(self,Token:str,EnCodingAESKey:str,Corpid:str,logger:EventLogger): self.Token=Token self.EnCodingAESKey=EnCodingAESKey self.Corpid=Corpid @@ -139,7 +141,8 @@ class WecomBotClient: reply_timestamp = str(int(time.time())) ret, encrypt_text = self.wxcpt.EncryptMsg(reply_plain_str, nonce, reply_timestamp) if ret != 0: - await self.logger.error("加密失败") + + await self.logger.error("加密失败"+str(ret)) root = ET.fromstring(encrypt_text) @@ -155,7 +158,7 @@ class WecomBotClient: await asyncio.sleep(interval) if self.msg_id_map.get(message_data['msgid'], 1) == 3: - print('请求失效:暂不支持智能机器人超过7秒的请求,如有需求,请联系 LangBot 团队。') + await self.logger.error('请求失效:暂不支持智能机器人超过7秒的请求,如有需求,请联系 LangBot 团队。') return '' except Exception as e: diff --git a/pkg/platform/sources/wecombot.py b/pkg/platform/sources/wecombot.py index 6138174d..9487b637 100644 --- a/pkg/platform/sources/wecombot.py +++ b/pkg/platform/sources/wecombot.py @@ -4,17 +4,17 @@ import asyncio import traceback import datetime -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +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 +import pydantic +from ..logger import EventLogger from libs.wecom_ai_bot_api.wecombotevent import WecomBotEvent from libs.wecom_ai_bot_api.api import WecomBotClient -from .. import adapter from ...core import app -from ..types import entities as platform_entities -from ...command.errors import ParamNotEnoughError -from ..logger import EventLogger -class WecomBotMessageConverter(adapter.MessageConverter): +class WecomBotMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content = '' @@ -36,7 +36,7 @@ class WecomBotMessageConverter(adapter.MessageConverter): return chain -class WecomBotEventConverter(adapter.EventConverter): +class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event:platform_events.MessageEvent): @@ -82,29 +82,35 @@ class WecomBotEventConverter(adapter.EventConverter): except Exception: print(traceback.format_exc()) -class WecomBotAdapter(adapter.MessagePlatformAdapter): - bot : WecomBotClient - app: app.Application - message_converter = WecomBotMessageConverter() - event_converter = WecomBotEventConverter() - config:dict - bot_account_id:str +class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: WecomBotClient + bot_account_id: str + message_converter: WecomBotMessageConverter = WecomBotMessageConverter() + event_converter: WecomBotEventConverter = WecomBotEventConverter() + config: dict - def __init__(self, config:dict, ap:app.Application, logger:EventLogger): - self.config = config - self.app = ap - self.logger = logger - required_keys = ['Token', 'EncodingAESKey', 'Corpid'] + def __init__(self, config: dict, logger: EventLogger): + required_keys = ['Token', 'EncodingAESKey', 'Corpid', 'BotId', 'port'] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError('缺少相关配置项,请查看文档或联系管理员') - self.bot = WecomBotClient( - Token=self.config['Token'], - EnCodingAESKey=self.config['EncodingAESKey'], - Corpid=self.config['Corpid'], - logger=self.logger, + raise Exception(f'WecomBot 缺少配置项: {missing_keys}') + + # 创建运行时 bot 对象 + bot = WecomBotClient( + Token=config['Token'], + EnCodingAESKey=config['EncodingAESKey'], + Corpid=config['Corpid'], + logger=logger, ) - self.bot_account_id = self.config['BotId'] + bot_account_id = config['BotId'] + + super().__init__( + config=config, + logger=logger, + bot=bot, + bot_account_id=bot_account_id, + ) + async def reply_message(self, message_source:platform_events.MessageEvent, message:platform_message.MessageChain,quote_origin: bool = False): @@ -117,7 +123,7 @@ class WecomBotAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ): async def on_message(event: WecomBotEvent): try: @@ -151,8 +157,11 @@ class WecomBotAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) + + async def is_muted(self, group_id: int) -> bool: + pass