# -*- coding: utf-8 -*- """ 企业微信 AI 智能体消息回调控制器 接收企业微信用户发送给 AI 应用的消息,异步处理后主动推送回复 """ import logging import threading from odoo import http, api, SUPERUSER_ID from odoo.http import request, Response from ..utils.wx_biz_msg_crypt import WXBizMsgCrypt, WXBizMsgCryptError, parse_wecom_message _logger = logging.getLogger(__name__) class WecomAiCallback(http.Controller): """企业微信 AI 消息回调""" def _get_ai_app_config(self): """获取 AI 智能体对应的企业微信应用配置""" config = request.env['wecom.apps.config'].sudo().search([ ('wecom_apps_type', '=', 'ai_callback'), ('wecom_apps_category', '=', 'agent'), ], limit=1) if not config or not config.wecom_apps_id: return None return config.wecom_apps_id def _get_crypt_instance(self, wecom_app): """创建加解密实例""" token = wecom_app.callback_token aes_key = wecom_app.encoding_aes_key corpid = wecom_app.corpid if not all([token, aes_key, corpid]): _logger.error("企业微信 AI 回调配置不完整: token=%s, aes_key=%s, corpid=%s", bool(token), bool(aes_key), bool(corpid)) return None return WXBizMsgCrypt(token, aes_key, corpid) @http.route('/wecom/ai/callback', type='http', auth='none', methods=['GET', 'POST'], csrf=False) def ai_callback(self, msg_signature=None, timestamp=None, nonce=None, echostr=None, **kw): """ 企业微信 AI 应用消息回调入口 GET: URL验证(配置接收消息服务器时企业微信发起的验证请求) POST: 接收用户消息 """ # 获取 AI 应用配置 wecom_app = self._get_ai_app_config() if not wecom_app: _logger.error("未找到 AI 回调应用配置(wecom_apps_type=ai_callback, category=agent)") return Response("config error", content_type='text/plain') crypt = self._get_crypt_instance(wecom_app) if not crypt: return Response("config error", content_type='text/plain') # === GET: URL验证 === if request.httprequest.method == 'GET': return self._handle_verify(crypt, msg_signature, timestamp, nonce, echostr) # === POST: 接收消息 === return self._handle_message(crypt, msg_signature, timestamp, nonce) def _handle_verify(self, crypt, msg_signature, timestamp, nonce, echostr): """处理URL验证请求""" if not all([msg_signature, timestamp, nonce, echostr]): _logger.warning("AI回调URL验证缺少参数: sig=%s, ts=%s, nonce=%s, echo=%s", msg_signature, timestamp, nonce, bool(echostr)) return Response("param error", content_type='text/plain') try: echo_plain = crypt.verify_url(msg_signature, timestamp, nonce, echostr) _logger.info("企业微信 AI 回调URL验证成功") return Response(echo_plain, content_type='text/plain') except WXBizMsgCryptError as e: _logger.error("AI回调URL验证失败: %s", e) return Response("verify error", content_type='text/plain') def _handle_message(self, crypt, msg_signature, timestamp, nonce): """处理接收消息请求""" if not all([msg_signature, timestamp, nonce]): _logger.warning("AI回调消息缺少签名参数") return Response("", content_type='text/plain') post_data = request.httprequest.get_data(as_text=True) if not post_data: _logger.warning("AI回调消息体为空") return Response("", content_type='text/plain') # 解密消息 try: xml_plain = crypt.decrypt_msg(msg_signature, timestamp, nonce, post_data) except WXBizMsgCryptError as e: _logger.error("AI回调消息解密失败: %s", e) return "" # 解析消息 msg = parse_wecom_message(xml_plain) if not msg: _logger.error("AI回调消息XML解析失败") return "" _logger.info("收到企业微信AI消息: from=%s, type=%s, content=%s", msg.get('from_user'), msg.get('msg_type'), msg.get('content', '')[:50]) # 只处理文本消息 if msg.get('msg_type') != 'text': _logger.info("AI回调收到非文本消息类型: %s,暂不处理", msg.get('msg_type')) return Response("", content_type='text/plain') wecom_userid = msg.get('from_user') content = msg.get('content', '').strip() if not wecom_userid or not content: return Response("", content_type='text/plain') # 异步处理: 立即返回空响应,后台线程处理AI对话并推送回复 db_name = request.env.cr.dbname threading.Thread( target=self._async_process_ai_message, args=(db_name, wecom_userid, content), daemon=True, ).start() # 立即返回空字符串,表示成功接收 return Response("", content_type='text/plain') def _async_process_ai_message(self, db_name, wecom_userid, content): """ 后台线程: 处理 AI 对话并推送回复 使用独立的数据库游标和环境 """ try: registry = api.Registry(db_name) with registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) env['wecom.ai.chat'].process_ai_message(wecom_userid, content) cr.commit() except Exception as e: _logger.exception("AI消息异步处理失败 [user=%s]: %s", wecom_userid, e)