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,555 @@
|
||||
"""
|
||||
企业微信通知模块
|
||||
提供企业微信消息发送功能,支持文本和Markdown格式
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
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 ..core.models import Announcement
|
||||
from ..core.reliability import retry_on_exception, RetryConfig, safe_execute
|
||||
from ..storage.md_generator import AnnouncementMarkdownFormatter
|
||||
except ImportError:
|
||||
from core.config_manager import get_config
|
||||
from core.logger import get_logger
|
||||
from core.models import Announcement
|
||||
from core.reliability import retry_on_exception, RetryConfig, safe_execute
|
||||
from storage.md_generator import AnnouncementMarkdownFormatter
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class WeChatService:
|
||||
"""企业微信服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = get_config().wechat_app
|
||||
self._access_token = None
|
||||
self._token_expires_at = 0
|
||||
|
||||
logger.info("企业微信服务初始化完成")
|
||||
|
||||
def _get_access_token(self) -> Optional[str]:
|
||||
"""
|
||||
获取访问令牌
|
||||
|
||||
Returns:
|
||||
Optional[str]: 访问令牌
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# 检查令牌是否仍然有效
|
||||
if self._access_token and current_time < self._token_expires_at:
|
||||
return self._access_token
|
||||
|
||||
try:
|
||||
# 构建请求URL
|
||||
if self.config.use_proxy and hasattr(self.config, 'proxy_api_url'):
|
||||
url = f"{self.config.proxy_api_url}/cgi-bin/gettoken"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
|
||||
params = {
|
||||
"corpid": self.config.corp_id,
|
||||
"corpsecret": self.config.secret
|
||||
}
|
||||
|
||||
logger.debug("正在获取企业微信访问令牌")
|
||||
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
self._access_token = result.get("access_token")
|
||||
# 提前5分钟过期
|
||||
expires_in = result.get("expires_in", 7200) - 300
|
||||
self._token_expires_at = current_time + expires_in
|
||||
|
||||
logger.info("成功获取企业微信访问令牌")
|
||||
return self._access_token
|
||||
else:
|
||||
logger.error(f"获取访问令牌失败: {result}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取访问令牌异常: {str(e)}")
|
||||
return None
|
||||
|
||||
@retry_on_exception(RetryConfig(max_retries=3))
|
||||
def send_text_message(self, content: str,
|
||||
to_user: str = "@all",
|
||||
to_party: str = "",
|
||||
to_tag: str = "") -> bool:
|
||||
"""
|
||||
发送文本消息
|
||||
|
||||
Args:
|
||||
content: 消息内容
|
||||
to_user: 接收者用户ID,多个用|分隔,@all表示全体
|
||||
to_party: 接收者部门ID,多个用|分隔
|
||||
to_tag: 接收者标签ID,多个用|分隔
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
try:
|
||||
access_token = self._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/message/send"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/message/send"
|
||||
|
||||
params = {"access_token": access_token}
|
||||
|
||||
data = {
|
||||
"touser": to_user,
|
||||
"toparty": to_party,
|
||||
"totag": to_tag,
|
||||
"msgtype": "text",
|
||||
"agentid": self.config.agent_id,
|
||||
"text": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(f"发送文本消息: {content[:100]}...")
|
||||
|
||||
response = requests.post(url, params=params, json=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
|
||||
|
||||
@retry_on_exception(RetryConfig(max_retries=3))
|
||||
def send_markdown_message(self, content: str,
|
||||
to_user: str = "@all",
|
||||
to_party: str = "",
|
||||
to_tag: str = "") -> bool:
|
||||
"""
|
||||
发送Markdown消息
|
||||
|
||||
Args:
|
||||
content: Markdown格式的消息内容
|
||||
to_user: 接收者用户ID
|
||||
to_party: 接收者部门ID
|
||||
to_tag: 接收者标签ID
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
try:
|
||||
access_token = self._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/message/send"
|
||||
else:
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/message/send"
|
||||
|
||||
params = {"access_token": access_token}
|
||||
|
||||
data = {
|
||||
"touser": to_user,
|
||||
"toparty": to_party,
|
||||
"totag": to_tag,
|
||||
"msgtype": "markdown",
|
||||
"agentid": self.config.agent_id,
|
||||
"markdown": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("发送Markdown消息")
|
||||
|
||||
response = requests.post(url, params=params, json=data, timeout=30)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info("Markdown消息发送成功")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Markdown消息发送失败: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送Markdown消息异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def send_announcement_notification(self, announcements: List[Announcement],
|
||||
max_count: int = 20) -> bool:
|
||||
"""
|
||||
发送公告通知
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
max_count: 最大显示数量
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
if not announcements:
|
||||
logger.info("没有新公告,跳过通知")
|
||||
return True
|
||||
|
||||
try:
|
||||
# 生成通知内容
|
||||
notification_content = self._generate_announcement_notification(announcements, max_count)
|
||||
|
||||
# 发送Markdown消息
|
||||
return self.send_markdown_message(notification_content)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送公告通知失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def _generate_announcement_notification(self, announcements: List[Announcement],
|
||||
max_count: int) -> str:
|
||||
"""
|
||||
生成公告通知内容
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
max_count: 最大显示数量
|
||||
|
||||
Returns:
|
||||
str: Markdown格式的通知内容
|
||||
"""
|
||||
# 按日期分组
|
||||
today_announcements = []
|
||||
other_announcements = []
|
||||
|
||||
today = datetime.now().date()
|
||||
|
||||
for announcement in announcements:
|
||||
if announcement.publish_date and announcement.publish_date.date() == today:
|
||||
today_announcements.append(announcement)
|
||||
else:
|
||||
other_announcements.append(announcement)
|
||||
|
||||
lines = []
|
||||
|
||||
# 标题
|
||||
total_count = len(announcements)
|
||||
lines.append(f"# 🔔 广西政府采购网公告更新")
|
||||
lines.append("")
|
||||
lines.append(f"**发现 {total_count} 条新公告**")
|
||||
lines.append("")
|
||||
|
||||
# 今日公告
|
||||
if today_announcements:
|
||||
lines.append(f"## 📅 今日公告 ({len(today_announcements)}条)")
|
||||
lines.append("")
|
||||
display_today = today_announcements[:max_count//2]
|
||||
for announcement in display_today:
|
||||
title = announcement.title
|
||||
if len(title) > 40:
|
||||
title = title[:40] + "..."
|
||||
publish_time = announcement.publish_date.strftime("%H:%M") if announcement.publish_date else "N/A"
|
||||
lines.append(f"• [{title}]({announcement.content_url}) - {publish_time}")
|
||||
|
||||
if len(today_announcements) > len(display_today):
|
||||
lines.append(f"• ... 还有 {len(today_announcements) - len(display_today)} 条今日公告")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# 其他公告
|
||||
if other_announcements:
|
||||
lines.append(f"## 📄 其他公告 ({len(other_announcements)}条)")
|
||||
lines.append("")
|
||||
remaining_slots = max_count - len(today_announcements) if today_announcements else max_count
|
||||
display_other = other_announcements[:remaining_slots]
|
||||
|
||||
for announcement in display_other:
|
||||
title = announcement.title
|
||||
if len(title) > 40:
|
||||
title = title[:40] + "..."
|
||||
publish_date = announcement.publish_date.strftime("%m-%d") if announcement.publish_date else "N/A"
|
||||
lines.append(f"• [{title}]({announcement.content_url}) - {publish_date}")
|
||||
|
||||
if len(other_announcements) > len(display_other):
|
||||
lines.append(f"• ... 还有 {len(other_announcements) - len(display_other)} 条公告")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# 统计信息
|
||||
source_stats = {}
|
||||
for announcement in announcements:
|
||||
source = announcement.source_name
|
||||
source_stats[source] = source_stats.get(source, 0) + 1
|
||||
|
||||
lines.append("## 📊 统计信息")
|
||||
lines.append("")
|
||||
for source, count in sorted(source_stats.items()):
|
||||
lines.append(f"• {source}: {count}条")
|
||||
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append(f"*更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
|
||||
lines.append("*点击公告标题查看详情*")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_system_notification(self, title: str, content: str,
|
||||
message_type: str = "text") -> bool:
|
||||
"""
|
||||
发送系统通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
message_type: 消息类型 (text/markdown)
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
try:
|
||||
if message_type == "markdown":
|
||||
full_content = f"# {title}\n\n{content}"
|
||||
return self.send_markdown_message(full_content)
|
||||
else:
|
||||
full_content = f"{title}\n\n{content}"
|
||||
return self.send_text_message(full_content)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送系统通知失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def send_error_notification(self, error_message: str, error_details: Optional[str] = None) -> bool:
|
||||
"""
|
||||
发送错误通知
|
||||
|
||||
Args:
|
||||
error_message: 错误消息
|
||||
error_details: 错误详情
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
content = f"## ❌ 系统错误\n\n**错误信息**: {error_message}"
|
||||
|
||||
if error_details:
|
||||
content += f"\n\n**错误详情**:\n```\n{error_details}\n```"
|
||||
|
||||
content += f"\n\n*发生时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*"
|
||||
|
||||
return self.send_markdown_message(content)
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""
|
||||
测试连接
|
||||
|
||||
Returns:
|
||||
bool: 连接是否正常
|
||||
"""
|
||||
try:
|
||||
token = self._get_access_token()
|
||||
return token is not None
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信连接测试失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_service_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取服务状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 服务状态信息
|
||||
"""
|
||||
return {
|
||||
"service": "wechat",
|
||||
"enabled": self.config.enabled,
|
||||
"corp_id": self.config.corp_id[:10] + "..." if self.config.corp_id else None,
|
||||
"agent_id": self.config.agent_id,
|
||||
"has_token": self._access_token is not None,
|
||||
"token_expires_at": datetime.fromtimestamp(self._token_expires_at).isoformat() if self._token_expires_at > 0 else None,
|
||||
"use_proxy": self.config.use_proxy,
|
||||
"connection_test": self.test_connection() if self.config.enabled else False
|
||||
}
|
||||
|
||||
|
||||
class NotificationManager:
|
||||
"""通知管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.wechat = WeChatService()
|
||||
self._services = {
|
||||
"wechat": self.wechat
|
||||
}
|
||||
|
||||
def send_announcement_notification(self, announcements: List[Announcement]) -> Dict[str, bool]:
|
||||
"""
|
||||
发送公告通知
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 各服务发送结果
|
||||
"""
|
||||
results = {}
|
||||
|
||||
# 企业微信通知
|
||||
if self.wechat.config.enabled:
|
||||
try:
|
||||
results["wechat"] = self.wechat.send_announcement_notification(announcements)
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信通知失败: {str(e)}")
|
||||
results["wechat"] = False
|
||||
else:
|
||||
results["wechat"] = None # 未启用
|
||||
|
||||
return results
|
||||
|
||||
def send_system_notification(self, title: str, content: str) -> Dict[str, bool]:
|
||||
"""
|
||||
发送系统通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 发送结果
|
||||
"""
|
||||
results = {}
|
||||
|
||||
if self.wechat.config.enabled:
|
||||
try:
|
||||
results["wechat"] = self.wechat.send_system_notification(title, content, "markdown")
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信系统通知失败: {str(e)}")
|
||||
results["wechat"] = False
|
||||
else:
|
||||
results["wechat"] = None
|
||||
|
||||
return results
|
||||
|
||||
def send_error_notification(self, error_message: str, error_details: Optional[str] = None) -> Dict[str, bool]:
|
||||
"""
|
||||
发送错误通知
|
||||
|
||||
Args:
|
||||
error_message: 错误消息
|
||||
error_details: 错误详情
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 发送结果
|
||||
"""
|
||||
results = {}
|
||||
|
||||
if self.wechat.config.enabled:
|
||||
try:
|
||||
results["wechat"] = self.wechat.send_error_notification(error_message, error_details)
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信错误通知失败: {str(e)}")
|
||||
results["wechat"] = False
|
||||
else:
|
||||
results["wechat"] = None
|
||||
|
||||
return results
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取通知服务状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 服务状态
|
||||
"""
|
||||
return {
|
||||
"services": {
|
||||
name: service.get_service_status() for name, service in self._services.items()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 全局通知管理器实例
|
||||
_notification_manager = None
|
||||
|
||||
|
||||
def get_notification_manager() -> NotificationManager:
|
||||
"""
|
||||
获取通知管理器实例
|
||||
|
||||
Returns:
|
||||
NotificationManager: 通知管理器实例
|
||||
"""
|
||||
global _notification_manager
|
||||
if _notification_manager is None:
|
||||
_notification_manager = NotificationManager()
|
||||
return _notification_manager
|
||||
|
||||
|
||||
def send_announcements_notification(announcements: List[Announcement]) -> bool:
|
||||
"""
|
||||
发送公告通知
|
||||
|
||||
Args:
|
||||
announcements: 公告列表
|
||||
|
||||
Returns:
|
||||
bool: 是否至少有一个服务发送成功
|
||||
"""
|
||||
manager = get_notification_manager()
|
||||
results = manager.send_announcement_notification(announcements)
|
||||
|
||||
# 检查是否有服务发送成功
|
||||
return any(result for result in results.values() if result is True)
|
||||
|
||||
|
||||
def send_system_notification(title: str, content: str) -> bool:
|
||||
"""
|
||||
发送系统通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
bool: 是否至少有一个服务发送成功
|
||||
"""
|
||||
manager = get_notification_manager()
|
||||
results = manager.send_system_notification(title, content)
|
||||
|
||||
return any(result for result in results.values() if result is True)
|
||||
|
||||
|
||||
def send_error_alert(error_message: str, error_details: Optional[str] = None) -> bool:
|
||||
"""
|
||||
发送错误警报
|
||||
|
||||
Args:
|
||||
error_message: 错误消息
|
||||
error_details: 错误详情
|
||||
|
||||
Returns:
|
||||
bool: 是否至少有一个服务发送成功
|
||||
"""
|
||||
manager = get_notification_manager()
|
||||
results = manager.send_error_notification(error_message, error_details)
|
||||
|
||||
return any(result for result in results.values() if result is True)
|
||||
Reference in New Issue
Block a user