mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-12 13:50:59 +00:00
feat: add support for wecombot,wxoa,slack and qqo
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 验证是否为回调验证请求
|
||||
|
||||
+30
-5
@@ -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':
|
||||
|
||||
@@ -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' 字段")
|
||||
|
||||
Reference in New Issue
Block a user