Files
gzth/yuthon_wechat/controllers/ai_callback.py
T
2026-05-28 14:06:39 +08:00

145 lines
5.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 -*-
"""
企业微信 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)