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:
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding:utf-8 -*-
|
||||
|
||||
""" 对企业微信发送给企业后台的消息加解密示例代码.
|
||||
@copyright: Copyright (c) 1998-2014 Tencent Inc.
|
||||
|
||||
"""
|
||||
# ------------------------------------------------------------------------
|
||||
import logging
|
||||
import base64
|
||||
import random
|
||||
import hashlib
|
||||
import time
|
||||
import struct
|
||||
from Crypto.Cipher import AES
|
||||
import xml.etree.cElementTree as ET
|
||||
import socket
|
||||
|
||||
try:
|
||||
import ierror
|
||||
except ImportError:
|
||||
from . import ierror
|
||||
|
||||
|
||||
"""
|
||||
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
|
||||
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
|
||||
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
|
||||
"""
|
||||
|
||||
|
||||
class FormatException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def throw_exception(message, exception_class=FormatException):
|
||||
"""my define raise exception function"""
|
||||
raise exception_class(message)
|
||||
|
||||
|
||||
class SHA1:
|
||||
"""计算企业微信的消息签名接口"""
|
||||
|
||||
def getSHA1(self, token, timestamp, nonce, encrypt):
|
||||
"""用SHA1算法生成安全签名
|
||||
@param token: 票据
|
||||
@param timestamp: 时间戳
|
||||
@param encrypt: 密文
|
||||
@param nonce: 随机字符串
|
||||
@return: 安全签名
|
||||
"""
|
||||
try:
|
||||
sortlist = [token, timestamp, nonce, encrypt]
|
||||
sortlist.sort()
|
||||
sha = hashlib.sha1()
|
||||
sha.update("".join(sortlist).encode())
|
||||
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
|
||||
except Exception as e:
|
||||
logger = logging.getLogger()
|
||||
logger.error(e)
|
||||
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
|
||||
|
||||
|
||||
class XMLParse:
|
||||
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
|
||||
|
||||
# xml消息模板
|
||||
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
|
||||
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
|
||||
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
|
||||
<TimeStamp>%(timestamp)s</TimeStamp>
|
||||
<Nonce><![CDATA[%(nonce)s]]></Nonce>
|
||||
</xml>"""
|
||||
|
||||
def extract(self, xmltext):
|
||||
"""提取出xml数据包中的加密消息
|
||||
@param xmltext: 待提取的xml字符串
|
||||
@return: 提取出的加密消息字符串
|
||||
"""
|
||||
try:
|
||||
xml_tree = ET.fromstring(xmltext)
|
||||
encrypt = xml_tree.find("Encrypt")
|
||||
return ierror.WXBizMsgCrypt_OK, encrypt.text
|
||||
except Exception as e:
|
||||
logger = logging.getLogger()
|
||||
logger.error(e)
|
||||
return ierror.WXBizMsgCrypt_ParseXml_Error, None
|
||||
|
||||
def generate(self, encrypt, signature, timestamp, nonce):
|
||||
"""生成xml消息
|
||||
@param encrypt: 加密后的消息密文
|
||||
@param signature: 安全签名
|
||||
@param timestamp: 时间戳
|
||||
@param nonce: 随机字符串
|
||||
@return: 生成的xml字符串
|
||||
"""
|
||||
resp_dict = {
|
||||
'msg_encrypt': encrypt,
|
||||
'msg_signaturet': signature,
|
||||
'timestamp': timestamp,
|
||||
'nonce': nonce,
|
||||
}
|
||||
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
|
||||
return resp_xml
|
||||
|
||||
|
||||
class PKCS7Encoder():
|
||||
"""提供基于PKCS7算法的加解密接口"""
|
||||
|
||||
block_size = 32
|
||||
|
||||
def encode(self, text):
|
||||
""" 对需要加密的明文进行填充补位
|
||||
@param text: 需要进行填充补位操作的明文
|
||||
@return: 补齐明文字符串
|
||||
"""
|
||||
text_length = len(text)
|
||||
# 计算需要填充的位数
|
||||
amount_to_pad = self.block_size - (text_length % self.block_size)
|
||||
if amount_to_pad == 0:
|
||||
amount_to_pad = self.block_size
|
||||
# 获得补位所用的字符
|
||||
pad = chr(amount_to_pad)
|
||||
return text + (pad * amount_to_pad).encode()
|
||||
|
||||
def decode(self, decrypted):
|
||||
"""删除解密后明文的补位字符
|
||||
@param decrypted: 解密后的明文
|
||||
@return: 删除补位字符后的明文
|
||||
"""
|
||||
pad = ord(decrypted[-1])
|
||||
if pad < 1 or pad > 32:
|
||||
pad = 0
|
||||
return decrypted[:-pad]
|
||||
|
||||
|
||||
class Prpcrypt(object):
|
||||
"""提供接收和推送给企业微信消息的加解密接口"""
|
||||
|
||||
def __init__(self, key):
|
||||
|
||||
# self.key = base64.b64decode(key+"=")
|
||||
self.key = key
|
||||
# 设置加解密模式为AES的CBC模式
|
||||
self.mode = AES.MODE_CBC
|
||||
|
||||
def encrypt(self, text, receiveid):
|
||||
"""对明文进行加密
|
||||
@param text: 需要加密的明文
|
||||
@return: 加密得到的字符串
|
||||
"""
|
||||
# 16位随机字符串添加到明文开头
|
||||
text = text.encode()
|
||||
text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode()
|
||||
|
||||
# 使用自定义的填充方式对明文进行补位填充
|
||||
pkcs7 = PKCS7Encoder()
|
||||
text = pkcs7.encode(text)
|
||||
# 加密
|
||||
cryptor = AES.new(self.key, self.mode, self.key[:16])
|
||||
try:
|
||||
ciphertext = cryptor.encrypt(text)
|
||||
# 使用BASE64对加密后的字符串进行编码
|
||||
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
|
||||
except Exception as e:
|
||||
logger = logging.getLogger()
|
||||
logger.error(e)
|
||||
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
|
||||
|
||||
def decrypt(self, text, receiveid):
|
||||
"""对解密后的明文进行补位删除
|
||||
@param text: 密文
|
||||
@return: 删除填充补位后的明文
|
||||
"""
|
||||
try:
|
||||
cryptor = AES.new(self.key, self.mode, self.key[:16])
|
||||
# 使用BASE64对密文进行解码,然后AES-CBC解密
|
||||
plain_text = cryptor.decrypt(base64.b64decode(text))
|
||||
except Exception as e:
|
||||
logger = logging.getLogger()
|
||||
logger.error(e)
|
||||
return ierror.WXBizMsgCrypt_DecryptAES_Error, None
|
||||
try:
|
||||
pad = plain_text[-1]
|
||||
# 去掉补位字符串
|
||||
# pkcs7 = PKCS7Encoder()
|
||||
# plain_text = pkcs7.encode(plain_text)
|
||||
# 去除16位随机字符串
|
||||
content = plain_text[16:-pad]
|
||||
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
|
||||
xml_content = content[4: xml_len + 4]
|
||||
from_receiveid = content[xml_len + 4:]
|
||||
except Exception as e:
|
||||
logger = logging.getLogger()
|
||||
logger.error(e)
|
||||
return ierror.WXBizMsgCrypt_IllegalBuffer, None
|
||||
|
||||
if from_receiveid.decode('utf8') != receiveid:
|
||||
return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
|
||||
return 0, xml_content
|
||||
|
||||
def get_random_str(self):
|
||||
""" 随机生成16位字符串
|
||||
@return: 16位字符串
|
||||
"""
|
||||
return str(random.randint(1000000000000000, 9999999999999999)).encode()
|
||||
|
||||
|
||||
class WXBizMsgCrypt(object):
|
||||
# 构造函数
|
||||
def __init__(self, sToken, sEncodingAESKey, sReceiveId):
|
||||
try:
|
||||
self.key = base64.b64decode(sEncodingAESKey + "=")
|
||||
assert len(self.key) == 32
|
||||
except:
|
||||
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
|
||||
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
|
||||
self.m_sToken = sToken
|
||||
self.m_sReceiveId = sReceiveId
|
||||
|
||||
# 验证URL
|
||||
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
|
||||
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
|
||||
# @param sNonce: 随机串,对应URL参数的nonce
|
||||
# @param sEchoStr: 随机串,对应URL参数的echostr
|
||||
# @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
|
||||
# @return:成功0,失败返回对应的错误码
|
||||
|
||||
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
|
||||
sha1 = SHA1()
|
||||
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
if not signature == sMsgSignature:
|
||||
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
|
||||
pc = Prpcrypt(self.key)
|
||||
ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId)
|
||||
return ret, sReplyEchoStr
|
||||
|
||||
def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
|
||||
# 将企业回复用户的消息加密打包
|
||||
# @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
|
||||
# @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
|
||||
# @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
|
||||
# sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
|
||||
# return:成功0,sEncryptMsg,失败返回对应的错误码None
|
||||
pc = Prpcrypt(self.key)
|
||||
ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
|
||||
encrypt = encrypt.decode('utf8')
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
if timestamp is None:
|
||||
timestamp = str(int(time.time()))
|
||||
# 生成安全签名
|
||||
sha1 = SHA1()
|
||||
ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
xmlParse = XMLParse()
|
||||
return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)
|
||||
|
||||
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
|
||||
# 检验消息的真实性,并且获取解密后的明文
|
||||
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
|
||||
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
|
||||
# @param sNonce: 随机串,对应URL参数的nonce
|
||||
# @param sPostData: 密文,对应POST请求的数据
|
||||
# xml_content: 解密后的原文,当return返回0时有效
|
||||
# @return: 成功0,失败返回对应的错误码
|
||||
# 验证安全签名
|
||||
xmlParse = XMLParse()
|
||||
ret, encrypt = xmlParse.extract(sPostData)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
sha1 = SHA1()
|
||||
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
if not signature == sMsgSignature:
|
||||
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
|
||||
pc = Prpcrypt(self.key)
|
||||
ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId)
|
||||
return ret, xml_content
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
企业微信交互模块
|
||||
提供企业微信回调服务器、消息处理、菜单管理等功能
|
||||
"""
|
||||
|
||||
from .callback_server import WeChatCallbackServer
|
||||
from .message_handler import WeChatMessageHandler
|
||||
from .menu_manager import WeChatMenuManager
|
||||
|
||||
__all__ = ['WeChatCallbackServer', 'WeChatMessageHandler', 'WeChatMenuManager']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#########################################################################
|
||||
# Author: jonyqin
|
||||
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
|
||||
# File Name: ierror.py
|
||||
# Description:定义错误码含义
|
||||
#########################################################################
|
||||
WXBizMsgCrypt_OK = 0
|
||||
WXBizMsgCrypt_ValidateSignature_Error = -40001
|
||||
WXBizMsgCrypt_ParseXml_Error = -40002
|
||||
WXBizMsgCrypt_ComputeSignature_Error = -40003
|
||||
WXBizMsgCrypt_IllegalAesKey = -40004
|
||||
WXBizMsgCrypt_ValidateCorpid_Error = -40005
|
||||
WXBizMsgCrypt_EncryptAES_Error = -40006
|
||||
WXBizMsgCrypt_DecryptAES_Error = -40007
|
||||
WXBizMsgCrypt_IllegalBuffer = -40008
|
||||
WXBizMsgCrypt_EncodeBase64_Error = -40009
|
||||
WXBizMsgCrypt_DecodeBase64_Error = -40010
|
||||
WXBizMsgCrypt_GenReturnXml_Error = -40011
|
||||
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
企业微信菜单管理器
|
||||
负责创建和管理企业微信应用菜单
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
try:
|
||||
from ..core.config_manager import get_config
|
||||
from ..core.logger import get_logger
|
||||
from ..notification.wechat import WeChatService
|
||||
except ImportError:
|
||||
try:
|
||||
from core.config_manager import get_config
|
||||
from core.logger import get_logger
|
||||
from notification.wechat import WeChatService
|
||||
except ImportError as e:
|
||||
raise ImportError(f"菜单管理器导入失败: {e}")
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WeChatMenuManager:
|
||||
"""企业微信菜单管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config().wechat_app
|
||||
self.wechat_service = WeChatService()
|
||||
|
||||
# 菜单配置
|
||||
self.menu_data = {
|
||||
"button": [
|
||||
{
|
||||
"name": "操作",
|
||||
"sub_button": [
|
||||
{
|
||||
"type": "click",
|
||||
"name": "立即爬取",
|
||||
"key": "crawl_now"
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"name": "今日总结",
|
||||
"key": "today_summary"
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"name": "自定义爬取",
|
||||
"key": "custom_crawl"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "帮助",
|
||||
"sub_button": [
|
||||
{
|
||||
"type": "click",
|
||||
"name": "使用说明",
|
||||
"key": "help_guide"
|
||||
},
|
||||
{
|
||||
"type": "click",
|
||||
"name": "联系我们",
|
||||
"key": "contact_us"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
logger.info("企业微信菜单管理器初始化完成")
|
||||
|
||||
def create_menu(self) -> bool:
|
||||
"""
|
||||
创建菜单
|
||||
|
||||
Returns:
|
||||
bool: 创建是否成功
|
||||
"""
|
||||
try:
|
||||
logger.info("开始创建企业微信菜单")
|
||||
|
||||
# 获取访问令牌
|
||||
access_token = self.wechat_service._get_access_token()
|
||||
if not access_token:
|
||||
logger.error("获取访问令牌失败,无法创建菜单")
|
||||
return False
|
||||
|
||||
# 构建请求URL
|
||||
if self.config.use_proxy and hasattr(self.config, 'proxy_api_url'):
|
||||
url = f"{self.config.proxy_api_url}/cgi-bin/menu/create"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/menu/create"
|
||||
|
||||
params = {
|
||||
"access_token": access_token,
|
||||
"agentid": self.config.agent_id
|
||||
}
|
||||
|
||||
# 发送创建菜单请求
|
||||
response = requests.post(url, params=params, json=self.menu_data, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info("企业微信菜单创建成功")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"企业微信菜单创建失败: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建菜单异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def delete_menu(self) -> bool:
|
||||
"""
|
||||
删除菜单
|
||||
|
||||
Returns:
|
||||
bool: 删除是否成功
|
||||
"""
|
||||
try:
|
||||
logger.info("开始删除企业微信菜单")
|
||||
|
||||
# 获取访问令牌
|
||||
access_token = self.wechat_service._get_access_token()
|
||||
if not access_token:
|
||||
logger.error("获取访问令牌失败,无法删除菜单")
|
||||
return False
|
||||
|
||||
# 构建请求URL
|
||||
if self.config.use_proxy and hasattr(self.config, 'proxy_api_url'):
|
||||
url = f"{self.config.proxy_api_url}/cgi-bin/menu/delete"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete"
|
||||
|
||||
params = {
|
||||
"access_token": access_token,
|
||||
"agentid": self.config.agent_id
|
||||
}
|
||||
|
||||
# 发送删除菜单请求
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info("企业微信菜单删除成功")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"企业微信菜单删除失败: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除菜单异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_menu(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取当前菜单
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 菜单信息,失败返回None
|
||||
"""
|
||||
try:
|
||||
logger.info("开始获取企业微信菜单")
|
||||
|
||||
# 获取访问令牌
|
||||
access_token = self.wechat_service._get_access_token()
|
||||
if not access_token:
|
||||
logger.error("获取访问令牌失败,无法获取菜单")
|
||||
return None
|
||||
|
||||
# 构建请求URL
|
||||
if self.config.use_proxy and hasattr(self.config, 'proxy_api_url'):
|
||||
url = f"{self.config.proxy_api_url}/cgi-bin/menu/get"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/menu/get"
|
||||
|
||||
params = {
|
||||
"access_token": access_token,
|
||||
"agentid": self.config.agent_id
|
||||
}
|
||||
|
||||
# 发送获取菜单请求
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info("企业微信菜单获取成功")
|
||||
return result
|
||||
else:
|
||||
logger.error(f"企业微信菜单获取失败: {result}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取菜单异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def update_menu(self, menu_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
更新菜单
|
||||
|
||||
Args:
|
||||
menu_data: 新的菜单数据
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
"""
|
||||
try:
|
||||
logger.info("开始更新企业微信菜单")
|
||||
|
||||
# 先删除旧菜单
|
||||
if not self.delete_menu():
|
||||
logger.warning("删除旧菜单失败,继续创建新菜单")
|
||||
|
||||
# 更新菜单配置
|
||||
self.menu_data = menu_data
|
||||
|
||||
# 创建新菜单
|
||||
return self.create_menu()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新菜单异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_menu_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取菜单信息(用于调试)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 菜单信息
|
||||
"""
|
||||
return {
|
||||
"menu_data": self.menu_data,
|
||||
"menu_structure": self._analyze_menu_structure()
|
||||
}
|
||||
|
||||
def _analyze_menu_structure(self) -> Dict[str, Any]:
|
||||
"""分析菜单结构"""
|
||||
try:
|
||||
buttons = self.menu_data.get("button", [])
|
||||
structure = {
|
||||
"total_buttons": len(buttons),
|
||||
"buttons": []
|
||||
}
|
||||
|
||||
for i, button in enumerate(buttons):
|
||||
button_info = {
|
||||
"index": i,
|
||||
"name": button.get("name", ""),
|
||||
"type": button.get("type", "menu"),
|
||||
}
|
||||
|
||||
if "sub_button" in button:
|
||||
button_info["sub_buttons"] = len(button["sub_button"])
|
||||
button_info["sub_button_list"] = [
|
||||
{
|
||||
"name": sub.get("name", ""),
|
||||
"type": sub.get("type", ""),
|
||||
"key": sub.get("key", "")
|
||||
}
|
||||
for sub in button["sub_button"]
|
||||
]
|
||||
else:
|
||||
button_info["key"] = button.get("key", "")
|
||||
|
||||
structure["buttons"].append(button_info)
|
||||
|
||||
return structure
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分析菜单结构异常: {str(e)}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def test_menu_operations(self) -> Dict[str, bool]:
|
||||
"""
|
||||
测试菜单操作
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 测试结果
|
||||
"""
|
||||
results = {
|
||||
"create_menu": False,
|
||||
"get_menu": False,
|
||||
"delete_menu": False
|
||||
}
|
||||
|
||||
try:
|
||||
# 测试获取菜单
|
||||
menu_info = self.get_menu()
|
||||
results["get_menu"] = menu_info is not None
|
||||
|
||||
# 测试创建菜单(如果没有菜单的话)
|
||||
if not menu_info:
|
||||
results["create_menu"] = self.create_menu()
|
||||
else:
|
||||
results["create_menu"] = True # 已经有菜单了
|
||||
|
||||
# 不测试删除,避免影响现有菜单
|
||||
results["delete_menu"] = True
|
||||
|
||||
logger.info(f"菜单操作测试完成: {results}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"菜单操作测试异常: {str(e)}")
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,357 @@
|
||||
"""
|
||||
企业微信消息处理器
|
||||
处理用户消息和事件,实现菜单功能
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from ..core.config_manager import get_config
|
||||
from ..core.logger import get_logger
|
||||
from ..notification.wechat import send_system_notification
|
||||
from ..storage.postgresql import save_all_announcements_by_source_to_storage
|
||||
from ..storage.md_generator import generate_onu_md
|
||||
from ..core.models import Announcement
|
||||
except ImportError:
|
||||
try:
|
||||
from core.config_manager import get_config
|
||||
from core.logger import get_logger
|
||||
from notification.wechat import send_system_notification
|
||||
from storage.postgresql import save_all_announcements_by_source_to_storage
|
||||
from storage.md_generator import generate_onu_md
|
||||
from core.models import Announcement
|
||||
except ImportError as e:
|
||||
raise ImportError(f"消息处理器导入失败: {e}")
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WeChatMessageHandler:
|
||||
"""企业微信消息处理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.monitor_app = None
|
||||
|
||||
# 菜单配置
|
||||
self.menu_config = {
|
||||
"crawl": {
|
||||
"key": "crawl_now",
|
||||
"name": "立即爬取",
|
||||
"description": "立即执行一次公告爬取"
|
||||
},
|
||||
"today_summary": {
|
||||
"key": "today_summary",
|
||||
"name": "今日总结",
|
||||
"description": "查看今日公告统计"
|
||||
},
|
||||
"custom_crawl": {
|
||||
"key": "custom_crawl",
|
||||
"name": "自定义爬取",
|
||||
"description": "输入关键词进行爬取"
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("企业微信消息处理器初始化完成")
|
||||
|
||||
def _get_monitor_app(self):
|
||||
"""获取监控应用实例"""
|
||||
if self.monitor_app is None:
|
||||
# 动态导入避免循环导入
|
||||
try:
|
||||
from ..main import GXGPMonitorApp
|
||||
self.monitor_app = GXGPMonitorApp()
|
||||
# 初始化但不启动服务器
|
||||
if not self.monitor_app.initialize():
|
||||
logger.error("监控应用初始化失败")
|
||||
return None
|
||||
except ImportError:
|
||||
logger.error("无法导入监控应用")
|
||||
return None
|
||||
return self.monitor_app
|
||||
|
||||
def handle_event(self, event: str, event_key: Optional[str], from_user: str) -> Optional[str]:
|
||||
"""处理事件消息"""
|
||||
try:
|
||||
logger.info(f"处理事件: {event}, key: {event_key}, user: {from_user}")
|
||||
|
||||
if event == 'click':
|
||||
# 菜单点击事件
|
||||
if event_key == 'crawl_now':
|
||||
return self._handle_crawl_now(from_user)
|
||||
elif event_key == 'today_summary':
|
||||
return self._handle_today_summary(from_user)
|
||||
elif event_key.startswith('custom_crawl'):
|
||||
return self._handle_custom_crawl(event_key, from_user)
|
||||
else:
|
||||
return self._create_text_response("未知菜单项", from_user)
|
||||
|
||||
elif event == 'subscribe':
|
||||
# 关注事件
|
||||
welcome_msg = """欢迎关注广西政府采购网公告监控!
|
||||
|
||||
我可以帮您:
|
||||
• 自动监控最新采购公告
|
||||
• 筛选您关心的关键词信息
|
||||
• 及时推送重要更新
|
||||
|
||||
点击下方菜单开始使用。"""
|
||||
return self._create_text_response(welcome_msg, from_user)
|
||||
|
||||
elif event == 'unsubscribe':
|
||||
# 取消关注事件
|
||||
logger.info(f"用户 {from_user} 取消关注")
|
||||
return None
|
||||
|
||||
else:
|
||||
logger.info(f"未处理的event类型: {event}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"事件处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def handle_text_message(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理文本消息"""
|
||||
try:
|
||||
logger.info(f"处理文本消息: {content}, user: {from_user}")
|
||||
|
||||
# 移除前后空格
|
||||
content = content.strip()
|
||||
|
||||
if content == "帮助" or content == "help":
|
||||
return self._handle_help(from_user)
|
||||
elif content.startswith("爬取"):
|
||||
return self._handle_manual_crawl(content, from_user)
|
||||
elif content.startswith("总结"):
|
||||
return self._handle_today_summary(from_user)
|
||||
elif content.startswith("关键词"):
|
||||
return self._handle_keyword_search(content, from_user)
|
||||
else:
|
||||
# 默认当作关键词搜索
|
||||
return self._handle_keyword_search(f"关键词 {content}", from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文本消息处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def handle_other_message(self, msg_type: str, from_user: str) -> Optional[str]:
|
||||
"""处理其他类型的消息"""
|
||||
try:
|
||||
logger.info(f"处理其他消息类型: {msg_type}, user: {from_user}")
|
||||
|
||||
if msg_type == 'image':
|
||||
return self._create_text_response("收到图片消息,但我只能处理文本消息", from_user)
|
||||
elif msg_type == 'voice':
|
||||
return self._create_text_response("收到语音消息,但我只能处理文本消息", from_user)
|
||||
else:
|
||||
return self._create_text_response(f"收到{msg_type}消息,暂不支持此类型", from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"其他消息处理异常: {str(e)}")
|
||||
return self._create_text_response("处理失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_crawl_now(self, from_user: str) -> Optional[str]:
|
||||
"""处理立即爬取菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 触发立即爬取")
|
||||
|
||||
# 获取监控应用
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
# 执行爬取
|
||||
result = app.run_crawl()
|
||||
|
||||
if result.get("success"):
|
||||
total = result.get("total_crawled", 0)
|
||||
filtered = result.get("filtered", 0)
|
||||
saved = result.get("saved", 0)
|
||||
|
||||
response = f"""✅ 爬取完成!
|
||||
|
||||
📊 统计信息:
|
||||
• 总共发现: {total} 条公告
|
||||
• 关键词筛选: {filtered} 条
|
||||
• 已保存: {saved} 条
|
||||
|
||||
如有匹配的公告,我会及时推送通知。"""
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
response = f"❌ 爬取失败: {error}"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"立即爬取处理异常: {str(e)}")
|
||||
return self._create_text_response("爬取失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_today_summary(self, from_user: str) -> Optional[str]:
|
||||
"""处理今日总结菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 请求今日总结")
|
||||
|
||||
# 这里可以查询今日的公告统计
|
||||
# 由于数据库查询较为复杂,这里先返回简单的响应
|
||||
response = """📅 今日公告统计
|
||||
|
||||
由于系统正在优化中,今日统计功能暂时不可用。
|
||||
|
||||
您可以:
|
||||
• 点击"立即爬取"获取最新数据
|
||||
• 发送关键词进行搜索
|
||||
• 发送"帮助"查看更多功能"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"今日总结处理异常: {str(e)}")
|
||||
return self._create_text_response("获取统计失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_custom_crawl(self, event_key: str, from_user: str) -> Optional[str]:
|
||||
"""处理自定义爬取菜单"""
|
||||
try:
|
||||
logger.info(f"用户 {from_user} 触发自定义爬取")
|
||||
|
||||
response = """🔍 自定义爬取
|
||||
|
||||
请回复您想要搜索的关键词,我将为您执行爬取并筛选相关公告。
|
||||
|
||||
例如:
|
||||
• 大化
|
||||
• 信息化
|
||||
• 政府采购
|
||||
|
||||
发送关键词开始搜索。"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"自定义爬取处理异常: {str(e)}")
|
||||
return self._create_text_response("操作失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_help(self, from_user: str) -> Optional[str]:
|
||||
"""处理帮助命令"""
|
||||
help_text = """🤖 广西政府采购网公告监控助手
|
||||
|
||||
📋 菜单功能:
|
||||
• 立即爬取 - 执行一次公告爬取
|
||||
• 今日总结 - 查看今日公告统计
|
||||
• 自定义爬取 - 输入关键词搜索
|
||||
|
||||
💬 文本命令:
|
||||
• 发送关键词 - 搜索相关公告
|
||||
• "爬取 [关键词]" - 指定关键词爬取
|
||||
• "总结" - 查看今日统计
|
||||
• "帮助" - 显示此帮助信息
|
||||
|
||||
📢 自动推送:
|
||||
系统会自动监控最新公告,并推送匹配关键词的信息。
|
||||
|
||||
💡 使用提示:
|
||||
• 关键词支持多个,用空格分隔
|
||||
• 公告按时间倒序显示
|
||||
• 点击公告标题可查看详情"""
|
||||
|
||||
return self._create_text_response(help_text, from_user)
|
||||
|
||||
def _handle_manual_crawl(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理手动爬取命令"""
|
||||
try:
|
||||
# 解析关键词
|
||||
parts = content.split()
|
||||
if len(parts) < 2:
|
||||
return self._create_text_response("请指定爬取关键词,例如:爬取 大化", from_user)
|
||||
|
||||
keywords = parts[1:]
|
||||
logger.info(f"用户 {from_user} 手动爬取关键词: {keywords}")
|
||||
|
||||
# 获取监控应用
|
||||
app = self._get_monitor_app()
|
||||
if not app:
|
||||
return self._create_text_response("系统初始化失败,请稍后重试", from_user)
|
||||
|
||||
# 执行爬取
|
||||
result = app.run_crawl(keywords=keywords)
|
||||
|
||||
if result.get("success"):
|
||||
total = result.get("total_crawled", 0)
|
||||
filtered = result.get("filtered", 0)
|
||||
|
||||
if filtered > 0:
|
||||
response = f"""✅ 爬取完成!
|
||||
|
||||
关键词: {' '.join(keywords)}
|
||||
📊 统计信息:
|
||||
• 总共发现: {total} 条公告
|
||||
• 匹配筛选: {filtered} 条
|
||||
|
||||
相关公告已推送,请查收。"""
|
||||
else:
|
||||
response = f"""✅ 爬取完成!
|
||||
|
||||
关键词: {' '.join(keywords)}
|
||||
📊 统计信息:
|
||||
• 总共发现: {total} 条公告
|
||||
• 匹配筛选: 0 条
|
||||
|
||||
没有找到匹配的公告。"""
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
response = f"❌ 爬取失败: {error}"
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"手动爬取处理异常: {str(e)}")
|
||||
return self._create_text_response("爬取失败,请稍后重试", from_user)
|
||||
|
||||
def _handle_keyword_search(self, content: str, from_user: str) -> Optional[str]:
|
||||
"""处理关键词搜索"""
|
||||
try:
|
||||
# 解析关键词
|
||||
parts = content.split()
|
||||
keywords = parts[1:] if len(parts) > 1 else parts
|
||||
|
||||
if not keywords:
|
||||
return self._create_text_response("请提供搜索关键词", from_user)
|
||||
|
||||
logger.info(f"用户 {from_user} 关键词搜索: {keywords}")
|
||||
|
||||
# 这里可以实现关键词搜索逻辑
|
||||
# 目前先返回提示信息
|
||||
response = f"""🔍 关键词搜索
|
||||
|
||||
搜索关键词: {' '.join(keywords)}
|
||||
|
||||
由于系统正在优化中,搜索功能暂时不可用。
|
||||
|
||||
您可以:
|
||||
• 使用"爬取 [关键词]"执行新的爬取
|
||||
• 点击菜单中的"立即爬取"
|
||||
• 发送"帮助"查看更多功能"""
|
||||
|
||||
return self._create_text_response(response, from_user)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关键词搜索处理异常: {str(e)}")
|
||||
return self._create_text_response("搜索失败,请稍后重试", from_user)
|
||||
|
||||
def _create_text_response(self, content: str, to_user: str) -> str:
|
||||
"""创建文本消息响应"""
|
||||
timestamp = str(int(time.time()))
|
||||
|
||||
response_xml = f"""<xml>
|
||||
<ToUserName><![CDATA[{to_user}]]></ToUserName>
|
||||
<FromUserName><![CDATA[{self.config.wechat_app.corp_id}]]></FromUserName>
|
||||
<CreateTime>{timestamp}</CreateTime>
|
||||
<MsgType><![CDATA[text]]></MsgType>
|
||||
<Content><![CDATA[{content}]]></Content>
|
||||
</xml>"""
|
||||
|
||||
return response_xml
|
||||
Reference in New Issue
Block a user