Files
assist/src/web/blueprints/feishu_bot.py

124 lines
5.2 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""
飞书机器人蓝图
处理来自飞书机器人的事件回调
"""
import logging
import json
import threading
from flask import Blueprint, request, jsonify
from src.integrations.feishu_service import FeishuService
from src.web.service_manager import service_manager
# 初始化日志
logger = logging.getLogger(__name__)
# 创建蓝图
feishu_bot_bp = Blueprint('feishu_bot', __name__, url_prefix='/api/feishu/bot')
# 在模块级别实例化飞书服务,以便复用
# 注意:这假设配置在启动时是固定的。如果配置可热更新,则需要调整。
feishu_service = FeishuService()
def _process_message_in_background(app_context, event_data: dict):
"""
在后台线程中处理消息避免阻塞飞书的回调请求
"""
with app_context:
try:
# 1. 解析事件数据
message_id = event_data['event']['message']['message_id']
chat_id = event_data['event']['message']['chat_id']
# 内容是一个JSON字符串需要再次解析
content_json = json.loads(event_data['event']['message']['content'])
text_content = content_json.get('text', '').strip()
logger.info(f"[Feishu Bot] 后台开始处理消息ID: {message_id}, 内容: '{text_content}'")
# 2. 移除@机器人的部分
# 飞书的@消息格式通常是 "@机器人名 实际内容"
if event_data['event']['message'].get('mentions'):
for mention in event_data['event']['message']['mentions']:
# mention['key']是@内容,例如"@_user_1"
# mention['name']是显示的名字
bot_mention_text = f"@{mention['name']}"
if text_content.startswith(bot_mention_text):
text_content = text_content[len(bot_mention_text):].strip()
break
if not text_content:
logger.warning(f"[Feishu Bot] 移除@后内容为空不处理。消息ID: {message_id}")
return
logger.info(f"[Feishu Bot] 清理后的消息内容: '{text_content}'")
# 3. 调用核心服务获取回复
assistant = service_manager.get_assistant()
# 注意process_message_agent 是一个异步方法,需要处理
# 在同步线程中运行异步方法
import asyncio
try:
loop = asyncio.get_running_loop()
except RuntimeError: # 'RuntimeError: There is no current event loop...'
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 调用对话服务
logger.info(f"[Feishu Bot] 调用Agent服务处理消息...")
response_data = loop.run_until_complete(
assistant.process_message_agent(message=text_content, user_id=f"feishu_{chat_id}")
)
logger.info(f"[Feishu Bot] Agent服务返回结果: {response_data}")
# 4. 提取回复并发送
reply_text = response_data.get("message", "抱歉,我暂时无法回答这个问题。")
if isinstance(reply_text, dict): # 有时候返回的可能是字典
reply_text = reply_text.get('content', str(reply_text))
logger.info(f"[Feishu Bot] 准备发送回复到飞书: '{reply_text}'")
success = feishu_service.reply_to_message(message_id, reply_text)
if success:
logger.info(f"[Feishu Bot] 成功回复消息到飞书。消息ID: {message_id}")
else:
logger.error(f"[Feishu Bot] 回复消息到飞书失败。消息ID: {message_id}")
except Exception as e:
logger.error(f"[Feishu Bot] 后台处理消息时发生严重错误: {e}", exc_info=True)
@feishu_bot_bp.route('/event', methods=['POST'])
def handle_feishu_event():
"""
接收并处理飞书事件回调
"""
# 1. 解析请求
data = request.json
logger.info(f"[Feishu Bot] 收到飞书事件回调:\n{json.dumps(data, indent=2)}")
# 2. 安全校验 (如果配置了)
# 此处可以添加Verification Token的校验逻辑
# headers = request.headers
# ...
# 3. 处理URL验证挑战
if data and data.get("type") == "url_verification":
challenge = data.get("challenge", "")
logger.info(f"[Feishu Bot] 收到URL验证请求返回challenge: {challenge}")
return jsonify({"challenge": challenge})
# 4. 处理事件回调
if data and data.get("header", {}).get("event_type") == "im.message.receive_v1":
# 立即响应飞书,防止超时重试
threading.Thread(
target=_process_message_in_background,
args=(request.environ['werkzeug.request'].environ['flask.app'].app_context(), data)
).start()
logger.info("[Feishu Bot] 已将消息处理任务推送到后台线程并立即响应200 OK")
return jsonify({"status": "processing"})
# 5. 对于其他未知事件,也返回成功,避免飞书重试
logger.warning(f"[Feishu Bot] 收到未知类型的事件: {data.get('header', {}).get('event_type')}")
return jsonify({"status": "ignored"})