feat: add support for wecombot,wxoa,slack and qqo

This commit is contained in:
wangcham
2025-11-06 09:21:17 +00:00
parent ceb38d91b4
commit 913b9a24c4
13 changed files with 359 additions and 141 deletions

View File

@@ -23,20 +23,25 @@ xml_template = """
class OAClient: 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.token = token
self.aes = EncodingAESKey self.aes = EncodingAESKey
self.appid = AppID self.appid = AppID
self.appsecret = Appsecret self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com' self.base_url = 'https://api.weixin.qq.com'
self.access_token = '' self.access_token = ''
self.unified_mode = unified_mode
self.app = Quart(__name__) self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command', # 只有在非统一模式下才注册独立路由
'handle_callback', if not self.unified_mode:
self.handle_callback_request, self.app.add_url_rule(
methods=['GET', 'POST'], '/callback/command',
) 'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self._message_handlers = { self._message_handlers = {
'example': [], 'example': [],
} }
@@ -46,19 +51,39 @@ class OAClient:
self.logger = logger self.logger = logger
async def handle_callback_request(self): 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: try:
# 每隔100毫秒查询是否生成ai回答 # 每隔100毫秒查询是否生成ai回答
start_time = time.time() start_time = time.time()
signature = request.args.get('signature', '') signature = req.args.get('signature', '')
timestamp = request.args.get('timestamp', '') timestamp = req.args.get('timestamp', '')
nonce = request.args.get('nonce', '') nonce = req.args.get('nonce', '')
echostr = request.args.get('echostr', '') echostr = req.args.get('echostr', '')
msg_signature = request.args.get('msg_signature', '') msg_signature = req.args.get('msg_signature', '')
if msg_signature is None: if msg_signature is None:
await self.logger.error('msg_signature不在请求体中') await self.logger.error('msg_signature不在请求体中')
raise Exception('msg_signature不在请求体中') raise Exception('msg_signature不在请求体中')
if request.method == 'GET': if req.method == 'GET':
# 校验签名 # 校验签名
check_str = ''.join(sorted([self.token, timestamp, nonce])) check_str = ''.join(sorted([self.token, timestamp, nonce]))
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest() check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
@@ -68,8 +93,8 @@ class OAClient:
else: else:
await self.logger.error('拒绝请求') await self.logger.error('拒绝请求')
raise Exception('拒绝请求') raise Exception('拒绝请求')
elif request.method == 'POST': elif req.method == 'POST':
encryt_msg = await request.data encryt_msg = await req.data
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid) wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce) ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
xml_msg = xml_msg.decode('utf-8') xml_msg = xml_msg.decode('utf-8')
@@ -182,6 +207,7 @@ class OAClientForLongerResponse:
Appsecret: str, Appsecret: str,
LoadingMessage: str, LoadingMessage: str,
logger: None, logger: None,
unified_mode: bool = False,
): ):
self.token = token self.token = token
self.aes = EncodingAESKey self.aes = EncodingAESKey
@@ -189,13 +215,18 @@ class OAClientForLongerResponse:
self.appsecret = Appsecret self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com' self.base_url = 'https://api.weixin.qq.com'
self.access_token = '' self.access_token = ''
self.unified_mode = unified_mode
self.app = Quart(__name__) self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command', # 只有在非统一模式下才注册独立路由
'handle_callback', if not self.unified_mode:
self.handle_callback_request, self.app.add_url_rule(
methods=['GET', 'POST'], '/callback/command',
) 'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self._message_handlers = { self._message_handlers = {
'example': [], 'example': [],
} }
@@ -206,24 +237,44 @@ class OAClientForLongerResponse:
self.logger = logger self.logger = logger
async def handle_callback_request(self): 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: try:
signature = request.args.get('signature', '') signature = req.args.get('signature', '')
timestamp = request.args.get('timestamp', '') timestamp = req.args.get('timestamp', '')
nonce = request.args.get('nonce', '') nonce = req.args.get('nonce', '')
echostr = request.args.get('echostr', '') echostr = req.args.get('echostr', '')
msg_signature = request.args.get('msg_signature', '') msg_signature = req.args.get('msg_signature', '')
if msg_signature is None: if msg_signature is None:
await self.logger.error('msg_signature不在请求体中') await self.logger.error('msg_signature不在请求体中')
raise Exception('msg_signature不在请求体中') raise Exception('msg_signature不在请求体中')
if request.method == 'GET': if req.method == 'GET':
check_str = ''.join(sorted([self.token, timestamp, nonce])) check_str = ''.join(sorted([self.token, timestamp, nonce]))
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest() check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
return echostr if check_signature == signature else '拒绝请求' return echostr if check_signature == signature else '拒绝请求'
elif request.method == 'POST': elif req.method == 'POST':
encryt_msg = await request.data encryt_msg = await req.data
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid) wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce) ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
xml_msg = xml_msg.decode('utf-8') xml_msg = xml_msg.decode('utf-8')

View File

@@ -34,14 +34,19 @@ def handle_validation(body: dict, bot_secret: str):
class QQOfficialClient: 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 = Quart(__name__)
self.app.add_url_rule(
'/callback/command', # 只有在非统一模式下才注册独立路由
'handle_callback', if not self.unified_mode:
self.handle_callback_request, self.app.add_url_rule(
methods=['GET', 'POST'], '/callback/command',
) 'handle_callback',
self.handle_callback_request,
methods=['GET', 'POST'],
)
self.secret = secret self.secret = secret
self.token = token self.token = token
self.app_id = app_id self.app_id = app_id
@@ -82,10 +87,29 @@ class QQOfficialClient:
raise Exception(f'获取access_token失败: {e}') raise Exception(f'获取access_token失败: {e}')
async def handle_callback_request(self): 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: try:
# 读取请求数据 # 读取请求数据
body = await request.get_data() body = await req.get_data()
payload = json.loads(body) payload = json.loads(body)
# 验证是否为回调验证请求 # 验证是否为回调验证请求

View File

@@ -8,14 +8,19 @@ import langbot_plugin.api.entities.builtin.platform.events as platform_events
class SlackClient: 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.bot_token = bot_token
self.signing_secret = signing_secret self.signing_secret = signing_secret
self.unified_mode = unified_mode
self.app = Quart(__name__) self.app = Quart(__name__)
self.client = AsyncWebClient(self.bot_token) 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 = { self._message_handlers = {
'example': [], 'example': [],
} }
@@ -23,8 +28,28 @@ class SlackClient:
self.logger = logger self.logger = logger
async def handle_callback_request(self): 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: try:
body = await request.get_data() body = await req.get_data()
data = json.loads(body) data = json.loads(body)
if 'type' in data: if 'type' in data:
if data['type'] == 'url_verification': if data['type'] == 'url_verification':

View File

@@ -200,7 +200,7 @@ class StreamSessionManager:
class WecomBotClient: 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: Args:
@@ -208,6 +208,7 @@ class WecomBotClient:
EnCodingAESKey: 企业微信消息加解密密钥。 EnCodingAESKey: 企业微信消息加解密密钥。
Corpid: 企业 ID。 Corpid: 企业 ID。
logger: 日志记录器。 logger: 日志记录器。
unified_mode: 是否使用统一 webhook 模式(默认 False
Example: Example:
>>> client = WecomBotClient(Token='token', EnCodingAESKey='aeskey', Corpid='corp', logger=logger) >>> client = WecomBotClient(Token='token', EnCodingAESKey='aeskey', Corpid='corp', logger=logger)
@@ -217,13 +218,18 @@ class WecomBotClient:
self.EnCodingAESKey = EnCodingAESKey self.EnCodingAESKey = EnCodingAESKey
self.Corpid = Corpid self.Corpid = Corpid
self.ReceiveId = '' self.ReceiveId = ''
self.unified_mode = unified_mode
self.app = Quart(__name__) self.app = Quart(__name__)
self.app.add_url_rule(
'/callback/command', # 只有在非统一模式下才注册独立路由
'handle_callback', if not self.unified_mode:
self.handle_callback_request, self.app.add_url_rule(
methods=['POST', 'GET'] '/callback/command',
) 'handle_callback',
self.handle_callback_request,
methods=['POST', 'GET']
)
self._message_handlers = { self._message_handlers = {
'example': [], 'example': [],
} }
@@ -362,7 +368,7 @@ class WecomBotClient:
return await self._encrypt_and_reply(payload, nonce) return await self._encrypt_and_reply(payload, nonce)
async def handle_callback_request(self): async def handle_callback_request(self):
"""企业微信回调入口。 """企业微信回调入口(独立端口模式,使用全局 request
Returns: Returns:
Quart Response: 根据请求类型返回验证、首包或刷新结果。 Quart Response: 根据请求类型返回验证、首包或刷新结果。
@@ -370,15 +376,34 @@ class WecomBotClient:
Example: Example:
作为 Quart 路由处理函数直接注册并使用。 作为 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: try:
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '') 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': if req.method == 'GET':
return await self._handle_get_callback() return await self._handle_get_callback(req)
if request.method == 'POST': if req.method == 'POST':
return await self._handle_post_callback() return await self._handle_post_callback(req)
return Response('', status=405) return Response('', status=405)
@@ -386,13 +411,13 @@ class WecomBotClient:
await self.logger.error(traceback.format_exc()) await self.logger.error(traceback.format_exc())
return Response('Internal Server Error', status=500) 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 验证请求。""" """处理企业微信的 GET 验证请求。"""
msg_signature = unquote(request.args.get('msg_signature', '')) msg_signature = unquote(req.args.get('msg_signature', ''))
timestamp = unquote(request.args.get('timestamp', '')) timestamp = unquote(req.args.get('timestamp', ''))
nonce = unquote(request.args.get('nonce', '')) nonce = unquote(req.args.get('nonce', ''))
echostr = unquote(request.args.get('echostr', '')) echostr = unquote(req.args.get('echostr', ''))
if not all([msg_signature, timestamp, nonce, echostr]): if not all([msg_signature, timestamp, nonce, echostr]):
await self.logger.error('请求参数缺失') await self.logger.error('请求参数缺失')
@@ -405,16 +430,16 @@ class WecomBotClient:
return Response(decrypted_str, mimetype='text/plain') 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 回调请求。""" """处理企业微信的 POST 回调请求。"""
self.stream_sessions.cleanup() self.stream_sessions.cleanup()
msg_signature = unquote(request.args.get('msg_signature', '')) msg_signature = unquote(req.args.get('msg_signature', ''))
timestamp = unquote(request.args.get('timestamp', '')) timestamp = unquote(req.args.get('timestamp', ''))
nonce = unquote(request.args.get('nonce', '')) 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', '') encrypted_msg = (encrypted_json or {}).get('encrypt', '')
if not encrypted_msg: if not encrypted_msg:
await self.logger.error("请求体中缺少 'encrypt' 字段") await self.logger.error("请求体中缺少 'encrypt' 字段")

View File

@@ -59,8 +59,8 @@ class BotService:
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
# 为支持统一 webhook 的适配器生成 webhook URL # 为支持统一 webhook 的适配器生成 webhook URL
# 目前只有 wecom 支持 # 支持wecom、wecombot、officialaccount、qqofficial、slack
if persistence_bot['adapter'] == 'wecom': if persistence_bot['adapter'] in ['wecom', 'wecombot', 'officialaccount', 'qqofficial', 'slack']:
api_port = self.ap.instance_config.data['api']['port'] api_port = self.ap.instance_config.data['api']['port']
webhook_url = f"/bots/{bot_uuid}" webhook_url = f"/bots/{bot_uuid}"
adapter_runtime_values['webhook_url'] = webhook_url adapter_runtime_values['webhook_url'] = webhook_url

View File

@@ -59,14 +59,16 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
message_converter: OAMessageConverter = OAMessageConverter() message_converter: OAMessageConverter = OAMessageConverter()
event_converter: OAEventConverter = OAEventConverter() event_converter: OAEventConverter = OAEventConverter()
bot: typing.Union[OAClient, OAClientForLongerResponse] = pydantic.Field(exclude=True) bot: typing.Union[OAClient, OAClientForLongerResponse] = pydantic.Field(exclude=True)
bot_uuid: str = None
def __init__(self, config: dict, logger: EventLogger): def __init__(self, config: dict, logger: EventLogger):
# 校验必填项
required_keys = ['token', 'EncodingAESKey', 'AppSecret', 'AppID', 'Mode'] required_keys = ['token', 'EncodingAESKey', 'AppSecret', 'AppID', 'Mode']
missing_keys = [k for k in required_keys if k not in config] missing_keys = [k for k in required_keys if k not in config]
if missing_keys: if missing_keys:
raise Exception(f'OfficialAccount 缺少配置项: {missing_keys}') raise Exception(f'OfficialAccount 缺少配置项: {missing_keys}')
# 创建运行时 bot 对象,始终使用统一 webhook 模式
if config['Mode'] == 'drop': if config['Mode'] == 'drop':
bot = OAClient( bot = OAClient(
token=config['token'], token=config['token'],
@@ -74,6 +76,7 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
Appsecret=config['AppSecret'], Appsecret=config['AppSecret'],
AppID=config['AppID'], AppID=config['AppID'],
logger=logger, logger=logger,
unified_mode=True,
) )
elif config['Mode'] == 'passive': elif config['Mode'] == 'passive':
bot = OAClientForLongerResponse( bot = OAClientForLongerResponse(
@@ -83,13 +86,14 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
AppID=config['AppID'], AppID=config['AppID'],
LoadingMessage=config.get('LoadingMessage', ''), LoadingMessage=config.get('LoadingMessage', ''),
logger=logger, logger=logger,
unified_mode=True,
) )
else: else:
raise KeyError('请设置微信公众号通信模式') raise KeyError('请设置微信公众号通信模式')
bot_account_id = config.get('AppID', '') bot_account_id = config.get('AppID', '')
super().__init__( super().__init__(
bot=bot, bot=bot,
bot_account_id=bot_account_id, bot_account_id=bot_account_id,
@@ -136,16 +140,45 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd
elif event_type == platform_events.GroupMessage: elif event_type == platform_events.GroupMessage:
pass 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 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://<Your-Public-IP>:{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: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
await keep_alive()
await self.bot.run_task(
host=self.config['host'],
port=self.config['port'],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool: async def kill(self) -> bool:
return False return False

View File

@@ -53,23 +53,6 @@ spec:
type: string type: string
required: true required: true
default: "AI正在思考中请发送任意内容获取回复。" 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: execution:
python: python:
path: ./officialaccount.py path: ./officialaccount.py

View File

@@ -135,12 +135,17 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
bot: QQOfficialClient bot: QQOfficialClient
config: dict config: dict
bot_account_id: str bot_account_id: str
bot_uuid: str = None
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter() message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
event_converter: QQOfficialEventConverter = QQOfficialEventConverter() event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
def __init__(self, config: dict, logger: EventLogger): def __init__(self, config: dict, logger: EventLogger):
bot = QQOfficialClient( 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__( 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('GROUP_AT_MESSAGE_CREATE')(on_message)
self.bot.on_message('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 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://<Your-Public-IP>:{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: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
await keep_alive()
await self.bot.run_task(
host='0.0.0.0',
port=self.config['port'],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool: async def kill(self) -> bool:
return False return False

View File

@@ -25,13 +25,6 @@ spec:
type: string type: string
required: true required: true
default: "" default: ""
- name: port
label:
en_US: Port
zh_Hans: 监听端口
type: integer
required: true
default: 2284
- name: token - name: token
label: label:
en_US: Token en_US: Token

View File

@@ -86,6 +86,7 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter):
class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
bot: SlackClient bot: SlackClient
bot_account_id: str bot_account_id: str
bot_uuid: str = None
message_converter: SlackMessageConverter = SlackMessageConverter() message_converter: SlackMessageConverter = SlackMessageConverter()
event_converter: SlackEventConverter = SlackEventConverter() event_converter: SlackEventConverter = SlackEventConverter()
config: dict config: dict
@@ -102,7 +103,10 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项请查看文档或联系管理员') raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项请查看文档或联系管理员')
self.bot = SlackClient( 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( async def reply_message(
@@ -148,16 +152,45 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
elif event_type == platform_events.GroupMessage: elif event_type == platform_events.GroupMessage:
self.bot.on_message('channel')(on_message) 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 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://<Your-Public-IP>:{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: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
await keep_alive()
await self.bot.run_task(
host='0.0.0.0',
port=self.config['port'],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool: async def kill(self) -> bool:
return False return False

View File

@@ -25,13 +25,6 @@ spec:
type: string type: string
required: true required: true
default: "" default: ""
- name: port
label:
en_US: Port
zh_Hans: 监听端口
type: int
required: true
default: 2288
execution: execution:
python: python:
path: ./slack.py path: ./slack.py

View File

@@ -88,19 +88,22 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
message_converter: WecomBotMessageConverter = WecomBotMessageConverter() message_converter: WecomBotMessageConverter = WecomBotMessageConverter()
event_converter: WecomBotEventConverter = WecomBotEventConverter() event_converter: WecomBotEventConverter = WecomBotEventConverter()
config: dict config: dict
bot_uuid: str = None
def __init__(self, config: dict, logger: EventLogger): 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] missing_keys = [key for key in required_keys if key not in config]
if missing_keys: if missing_keys:
raise Exception(f'WecomBot 缺少配置项: {missing_keys}') raise Exception(f'WecomBot 缺少配置项: {missing_keys}')
# 创建运行时 bot 对象
bot = WecomBotClient( bot = WecomBotClient(
Token=config['Token'], Token=config['Token'],
EnCodingAESKey=config['EncodingAESKey'], EnCodingAESKey=config['EncodingAESKey'],
Corpid=config['Corpid'], Corpid=config['Corpid'],
logger=logger, logger=logger,
unified_mode=True,
) )
bot_account_id = config['BotId'] bot_account_id = config['BotId']
@@ -182,18 +185,46 @@ class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
self.bot.on_message('group')(on_message) self.bot.on_message('group')(on_message)
except Exception: except Exception:
print(traceback.format_exc()) 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 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://<Your-Public-IP>:{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: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
await keep_alive()
await self.bot.run_task(
host='0.0.0.0',
port=self.config['port'],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool: async def kill(self) -> bool:
return False return False

View File

@@ -11,13 +11,6 @@ metadata:
icon: wecombot.png icon: wecombot.png
spec: spec:
config: config:
- name: port
label:
en_US: Port
zh_Hans: 监听端口
type: integer
required: true
default: 2291
- name: Corpid - name: Corpid
label: label:
en_US: Corpid en_US: Corpid