Files
assist/src/web/blueprints/feishu_bot.py
Jeason 28e90d2182 fix: 飞书群绑定租户完善
- tenants.js 去掉已删除的 appid/appsecret 元素引用
- showEditTenantModal 改为从 API 加载完整租户数据(不再传参拼接)
- saveTenant 保留已有的非 feishu 配置,只更新 chat_groups
- 租户列表显示绑定群数量或'未绑定飞书群'
- 飞书 bot/longconn 复用已有会话时同步更新 tenant_id(群重新绑定后立即生效)
- 删除租户后同步刷新租户选择器
2026-04-02 15:25:50 +08:00

228 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
from src.core.cache_manager import cache_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共享问题
from src.web.blueprints.tenants import resolve_tenant_by_chat_id, get_tenant_feishu_config
try:
# 1. 解析事件数据
event = event_data.get('event', {})
message = event.get('message', {})
message_id = message.get('message_id')
chat_id = message.get('chat_id')
chat_type = message.get('chat_type', 'unknown')
if not message_id or not chat_id:
logger.error(f"[Feishu Bot] 事件数据缺少必要字段: {event_data}")
return
# 解析租户:根据 chat_id 查找绑定的租户
tenant_id = resolve_tenant_by_chat_id(chat_id)
logger.info(f"[Feishu Bot] 群 {chat_id} 对应租户: {tenant_id}")
# 获取租户级飞书凭证(如果配置了)
tenant_feishu_cfg = get_tenant_feishu_config(tenant_id)
feishu_service = FeishuService(
app_id=tenant_feishu_cfg.get('app_id'),
app_secret=tenant_feishu_cfg.get('app_secret')
)
if not message_id or not chat_id:
logger.error(f"[Feishu Bot] 事件数据缺少必要字段: {event_data}")
return
# 记录会话类型
chat_type_desc = '群聊(group)' if chat_type == 'group' else '私聊(p2p)' if chat_type == 'p2p' else chat_type
logger.info(f"[Feishu Bot] 收到 {chat_type_desc} 消息, ChatID: {chat_id}")
# 消息去重检查
if cache_manager.check_and_set_message_processed(message_id):
logger.warning(f"[Feishu Bot] 🔁 消息 {message_id} 已被处理过(可能是长连接已处理),跳过")
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')
# 更新会话的 tenant_id群可能重新绑定了租户
if session_id in chat_manager.active_sessions:
chat_manager.active_sessions[session_id]['tenant_id'] = tenant_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, tenant_id=tenant_id)
logger.info(f"[Feishu Bot] 为用户 {sender_id} 在群聊 {chat_id} 创建新会话: {session_id} (租户: {tenant_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"})