# -*- coding: utf-8 -*- """ 飞书机器人蓝图 处理来自飞书机器人的事件回调 """ import logging import json import threading from flask import Blueprint, request, jsonify, current_app 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') def _process_message_in_background(app, event_data: dict): """ 在后台线程中处理消息,避免阻塞飞书的回调请求。 Args: app: Flask应用实例 event_data: 飞书事件数据 """ with app.app_context(): # 每个线程创建独立的飞书服务实例,避免token共享问题 feishu_service = FeishuService() try: # 1. 解析事件数据 event = event_data.get('event', {}) message = event.get('message', {}) message_id = message.get('message_id') chat_id = message.get('chat_id') if not message_id or not chat_id: logger.error(f"[Feishu Bot] 事件数据缺少必要字段: {event_data}") return # 内容是一个JSON字符串,需要再次解析 try: content_json = json.loads(message.get('content', '{}')) text_content = content_json.get('text', '').strip() except json.JSONDecodeError as e: logger.error(f"[Feishu Bot] 解析消息内容失败: {e}") return logger.info(f"[Feishu Bot] 后台开始处理消息ID: {message_id}, 内容: '{text_content}'") # 2. 移除@机器人的部分 # 飞书的@消息格式通常是 "@机器人名 实际内容" mentions = message.get('mentions', []) if mentions: for mention in mentions: # mention['key']是@内容,例如"@_user_1" # mention['name']是显示的名字 mention_name = mention.get('name', '') if mention_name: # 尝试多种@格式 for prefix in [f"@{mention_name}", f"@{mention_name} "]: if text_content.startswith(prefix): text_content = text_content[len(prefix):].strip() break if not text_content: logger.warning(f"[Feishu Bot] 移除@后内容为空,不处理。消息ID: {message_id}") # 仍然回复一个提示 feishu_service.reply_to_message(message_id, "您好!请问有什么可以帮助您的吗?") return logger.info(f"[Feishu Bot] 清理后的消息内容: '{text_content}'") # 3. 获取或创建该飞书用户的会话(支持群聊隔离) chat_manager = service_manager.get_chat_manager() # 获取发送者ID(从event中提取) sender_id = event.get('sender', {}).get('sender_id', {}).get('user_id', 'unknown') # 群聊隔离:每个用户在每个群都有独立会话 # 格式:feishu_群聊ID_用户ID user_id = f"feishu_{chat_id}_{sender_id}" logger.info(f"[Feishu Bot] 会话用户标识: {user_id}") # 检查是否已有活跃会话 active_sessions = chat_manager.get_active_sessions() session_id = None for session in active_sessions: if session.get('user_id') == user_id: session_id = session.get('session_id') logger.info(f"[Feishu Bot] 找到已有会话: {session_id}") break # 如果没有会话,创建新会话 if not session_id: session_id = chat_manager.create_session(user_id=user_id, work_order_id=None) logger.info(f"[Feishu Bot] 为用户 {sender_id} 在群聊 {chat_id} 创建新会话: {session_id}") # 4. 调用实时对话接口处理消息 logger.info(f"[Feishu Bot] 调用实时对话接口处理消息...") response_data = chat_manager.process_message( session_id=session_id, user_message=text_content, ip_address=None, invocation_method="feishu_bot" ) logger.info(f"[Feishu Bot] 实时对话接口返回结果: {response_data}") # 5. 提取回复并发送 if response_data.get("success"): reply_text = response_data.get("response") or response_data.get("content", "抱歉,我暂时无法回答这个问题。") else: error_msg = response_data.get('error', '未知错误') reply_text = f"抱歉,处理您的问题时遇到了一些问题。请稍后重试或联系客服。\n错误信息: {error_msg}" logger.error(f"[Feishu Bot] 处理消息失败: {error_msg}") # 确保回复是字符串 if isinstance(reply_text, dict): reply_text = reply_text.get('content', str(reply_text)) if not isinstance(reply_text, str): reply_text = str(reply_text) logger.info(f"[Feishu Bot] 准备发送回复到飞书 (长度: {len(reply_text)})") logger.debug(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 KeyError as e: logger.error(f"[Feishu Bot] 事件数据格式错误,缺少字段: {e}", exc_info=True) except Exception as e: logger.error(f"[Feishu Bot] 后台处理消息时发生严重错误: {e}", exc_info=True) # 尝试发送错误提示给用户 try: if 'message_id' in locals(): feishu_service.reply_to_message(message_id, "抱歉,系统遇到了一些问题,请稍后重试。") except: pass @feishu_bot_bp.route('/event', methods=['POST']) def handle_feishu_event(): """ 接收并处理飞书事件回调 """ # 1. 解析请求 data = request.json if not data: logger.warning("[Feishu Bot] 收到空的请求数据") return jsonify({"status": "error", "message": "empty request"}), 400 logger.info(f"[Feishu Bot] 收到飞书事件回调:\n{json.dumps(data, indent=2, ensure_ascii=False)}") # 2. 安全校验 (如果配置了) # 可以在这里添加Verification Token的校验逻辑 # from src.config.unified_config import get_config # config = get_config() # if config.feishu.verification_token: # token = request.headers.get('X-Lark-Request-Token') # if token != config.feishu.verification_token: # logger.warning("[Feishu Bot] Token验证失败") # return jsonify({"status": "error", "message": "invalid token"}), 403 # 3. 处理URL验证挑战 if data.get("type") == "url_verification": challenge = data.get("challenge", "") logger.info(f"[Feishu Bot] 收到URL验证请求,返回challenge: {challenge}") return jsonify({"challenge": challenge}) # 4. 处理事件回调 event_type = data.get("header", {}).get("event_type") if event_type == "im.message.receive_v1": # 获取当前Flask应用实例 app = current_app._get_current_object() # 立即在后台线程中处理,避免阻塞飞书回调 threading.Thread( target=_process_message_in_background, args=(app, data), daemon=True # 设置为守护线程 ).start() logger.info("[Feishu Bot] 已将消息处理任务推送到后台线程,并立即响应200 OK") return jsonify({"status": "processing"}) # 5. 对于其他未知事件,也返回成功,避免飞书重试 logger.warning(f"[Feishu Bot] 收到未知类型的事件: {event_type}") return jsonify({"status": "ignored"})