""" 企业微信通知模块 提供企业微信消息发送功能,支持文本和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)