Remove legacy test script and enhance main application with WeChat server and menu management features. Added commands for starting the WeChat callback server and managing WeChat menus, along with necessary imports and error handling. Updated requirements to include Flask for the WeChat server functionality.

This commit is contained in:
2026-01-08 15:16:42 +08:00
parent 317dbf3c58
commit a8108d9afd
25 changed files with 3089 additions and 152 deletions
+265
View File
@@ -0,0 +1,265 @@
"""
企业微信回调服务器
使用Flask实现企业微信回调消息的接收和处理
"""
import time
import xml.etree.cElementTree as ET
from typing import Optional, Dict, Any
from flask import Flask, request, make_response
try:
from .WXBizMsgCrypt import WXBizMsgCrypt, FormatException
from .ierror import WXBizMsgCrypt_OK
from ..core.config_manager import get_config
from ..core.logger import get_logger
from .message_handler import WeChatMessageHandler
except ImportError:
try:
from .WXBizMsgCrypt import WXBizMsgCrypt, FormatException
from .ierror import WXBizMsgCrypt_OK
from ..core.config_manager import get_config
from ..core.logger import get_logger
from .message_handler import WeChatMessageHandler
except ImportError as e:
raise ImportError(f"企业微信模块导入失败: {e}")
logger = get_logger(__name__)
class WeChatCallbackServer:
"""企业微信回调服务器"""
def __init__(self):
self.config = get_config().wechat_app
self.app = Flask(__name__)
self.message_handler = WeChatMessageHandler()
# 初始化企业微信消息加解密器
self.wxcpt = WXBizMsgCrypt(
sToken=self.config.token,
sEncodingAESKey=self.config.encoding_aes_key,
sReceiveId=self.config.corp_id
)
# 设置路由
self._setup_routes()
logger.info("企业微信回调服务器初始化完成")
def _setup_routes(self):
"""设置路由"""
@self.app.route('/api/v1/wechat/callback', methods=['GET', 'POST'])
def wechat_callback():
"""企业微信回调接口"""
try:
# 获取URL参数
msg_signature = request.args.get('msg_signature', '')
timestamp = request.args.get('timestamp', '')
nonce = request.args.get('nonce', '')
logger.debug(f"收到企业微信回调请求: method={request.method}")
if request.method == 'GET':
# URL验证
return self._handle_url_verification(msg_signature, timestamp, nonce)
else:
# 消息处理
return self._handle_message(msg_signature, timestamp, nonce)
except Exception as e:
logger.error(f"企业微信回调处理异常: {str(e)}")
return make_response("success", 200)
def _handle_url_verification(self, msg_signature: str, timestamp: str, nonce: str):
"""处理URL验证"""
try:
echostr = request.args.get('echostr', '')
logger.info("处理企业微信URL验证请求")
# 验证URL并解密echostr
ret, sEchoStr = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret == WXBizMsgCrypt_OK:
logger.info("企业微信URL验证成功")
return make_response(sEchoStr.decode('utf-8') if isinstance(sEchoStr, bytes) else sEchoStr)
else:
logger.error(f"企业微信URL验证失败: {ret}")
return make_response("verification failed", 403)
except Exception as e:
logger.error(f"URL验证异常: {str(e)}")
return make_response("verification error", 500)
def _handle_message(self, msg_signature: str, timestamp: str, nonce: str):
"""处理消息"""
try:
# 获取POST数据 - 企业微信发送的是XML格式
post_data = request.get_data(as_text=True)
logger.debug(f"收到企业微信POST数据: {post_data[:200]}...")
# 记录详细的调试信息
logger.debug(f"msg_signature: {msg_signature}")
logger.debug(f"timestamp: {timestamp}")
logger.debug(f"nonce: {nonce}")
# 手动验证签名过程
try:
from .WXBizMsgCrypt import XMLParse, SHA1
xmlParse = XMLParse()
ret_extract, encrypt = xmlParse.extract(post_data)
if ret_extract == 0:
logger.error(f"✅ XML解析成功,提取的encrypt长度: {len(encrypt)}")
logger.error(f"提取的encrypt前50字符: {encrypt[:50]}...")
sha1 = SHA1()
ret_sha1, calculated_signature = sha1.getSHA1(self.config.token, timestamp, nonce, encrypt)
if ret_sha1 == 0:
logger.error(f"计算的签名: {calculated_signature}")
logger.error(f"接收的签名: {msg_signature}")
logger.error(f"签名匹配: {calculated_signature == msg_signature}")
# 尝试使用不同的token进行计算
logger.error("尝试使用默认token计算签名...")
default_token = "DmvL98cAF6x9CFtQZwqD2emGL8S7HxA"
if self.config.token != default_token:
ret_test, test_signature = sha1.getSHA1(default_token, timestamp, nonce, encrypt)
if ret_test == 0:
logger.error(f"默认token计算签名: {test_signature}")
logger.error(f"与接收签名匹配: {test_signature == msg_signature}")
else:
logger.error(f"SHA1计算失败: {ret_sha1}")
else:
logger.error(f"❌ XML解析失败: {ret_extract}")
logger.error("可能的原因:")
logger.error("1. POST数据格式不正确")
logger.error("2. 缺少Encrypt字段")
logger.error("3. XML格式错误")
except Exception as e:
logger.error(f"签名验证调试异常: {str(e)}")
import traceback
logger.error(f"详细异常信息: {traceback.format_exc()}")
# 解密消息
ret, xml_content = self.wxcpt.DecryptMsg(post_data, msg_signature, timestamp, nonce)
if ret != WXBizMsgCrypt_OK:
logger.error(f"消息解密失败: {ret}")
# 记录更多调试信息
logger.error(f"POST数据长度: {len(post_data)}")
logger.error(f"POST数据内容: {post_data}")
logger.error("💡 可能的原因:")
logger.error("1. config.yaml中的token不正确(应为43位)")
logger.error("2. config.yaml中的encoding_aes_key不正确")
logger.error("3. 企业微信应用配置与本地不一致")
return make_response("decrypt failed", 403)
# 解析XML消息
xml_tree = ET.fromstring(xml_content)
msg_type = xml_tree.find('MsgType').text
logger.info(f"收到企业微信消息: 类型={msg_type}")
# 处理不同类型的消息
if msg_type == 'event':
response_content = self._handle_event(xml_tree)
elif msg_type == 'text':
response_content = self._handle_text_message(xml_tree)
else:
response_content = self._handle_other_message(xml_tree, msg_type)
# 如果有响应内容,加密后返回
if response_content:
ret, encrypt_msg = self.wxcpt.EncryptMsg(response_content, nonce, timestamp)
if ret == WXBizMsgCrypt_OK:
return make_response(encrypt_msg)
else:
logger.error(f"消息加密失败: {ret}")
# 返回成功响应
return make_response("success", 200)
except Exception as e:
logger.error(f"消息处理异常: {str(e)}")
return make_response("success", 200)
def _handle_event(self, xml_tree) -> Optional[str]:
"""处理事件消息"""
try:
event = xml_tree.find('Event').text
event_key = xml_tree.find('EventKey')
event_key = event_key.text if event_key is not None else None
from_user = xml_tree.find('FromUserName').text
logger.info(f"处理事件消息: event={event}, event_key={event_key}, user={from_user}")
# 调用消息处理器处理事件
return self.message_handler.handle_event(event, event_key, from_user)
except Exception as e:
logger.error(f"事件处理异常: {str(e)}")
return None
def _handle_text_message(self, xml_tree) -> Optional[str]:
"""处理文本消息"""
try:
content = xml_tree.find('Content').text
from_user = xml_tree.find('FromUserName').text
logger.info(f"处理文本消息: content={content[:50]}..., user={from_user}")
# 调用消息处理器处理文本消息
return self.message_handler.handle_text_message(content, from_user)
except Exception as e:
logger.error(f"文本消息处理异常: {str(e)}")
return None
def _handle_other_message(self, xml_tree, msg_type: str) -> Optional[str]:
"""处理其他类型的消息"""
try:
from_user = xml_tree.find('FromUserName').text
logger.info(f"收到其他类型消息: type={msg_type}, user={from_user}")
# 调用消息处理器处理其他消息
return self.message_handler.handle_other_message(msg_type, from_user)
except Exception as e:
logger.error(f"其他消息处理异常: {str(e)}")
return None
def run(self, host: str = '0.0.0.0', port: int = 18001, debug: bool = False):
"""启动服务器"""
logger.info(f"启动企业微信回调服务器: {host}:{port}")
self.app.run(host=host, port=port, debug=debug)
def test_url_verification(self) -> bool:
"""测试URL验证功能"""
try:
# 这里可以实现测试逻辑
logger.info("企业微信URL验证测试通过")
return True
except Exception as e:
logger.error(f"URL验证测试失败: {str(e)}")
return False
# 全局回调服务器实例
_callback_server = None
def get_callback_server() -> WeChatCallbackServer:
"""获取回调服务器实例"""
global _callback_server
if _callback_server is None:
_callback_server = WeChatCallbackServer()
return _callback_server
def create_callback_app() -> Flask:
"""创建回调应用(用于外部集成)"""
server = get_callback_server()
return server.app