""" 钉钉Webhook推送工具 支持推送Markdown格式消息到钉钉群 """ import requests import json from typing import Optional, Dict, Any from loguru import logger class DingTalkWebhook: """钉钉Webhook推送工具""" def __init__(self, webhook_url: str): """ 初始化钉钉Webhook Args: webhook_url: 钉钉机器人Webhook地址 """ self.webhook_url = webhook_url self.logger = logger.bind(module="DingTalkWebhook") def send_text(self, content: str, at_mobiles: list = None, at_all: bool = False) -> bool: """ 发送文本消息 Args: content: 消息内容 at_mobiles: 要@的手机号列表 at_all: 是否@所有人 Returns: 是否发送成功 """ data = { "msgtype": "text", "text": { "content": content } } if at_mobiles or at_all: data["at"] = {} if at_mobiles: data["at"]["atMobiles"] = at_mobiles if at_all: data["at"]["isAtAll"] = True return self._send(data) def send_markdown(self, title: str, text: str, at_mobiles: list = None, at_all: bool = False) -> bool: """ 发送Markdown消息 Args: title: 消息标题 text: Markdown内容(钉钉支持的格式) at_mobiles: 要@的手机号列表 at_all: 是否@所有人 Returns: 是否发送成功 """ # 钉钉markdown消息有长度限制,需要截断 max_length = 5000 if len(text) > max_length: text = text[:max_length - 100] + "\n\n...(内容已截断,完整内容请查看附件)" self.logger.warning(f"Markdown内容过长,已截断至{max_length}字符") data = { "msgtype": "markdown", "markdown": { "title": title, "text": text } } if at_mobiles or at_all: data["at"] = {} if at_mobiles: data["at"]["atMobiles"] = at_mobiles if at_all: data["at"]["isAtAll"] = True return self._send(data) def send_markdown_from_file(self, title: str, markdown_file: str, max_length: int = 5000, at_mobiles: list = None, at_all: bool = False) -> bool: """ 从Markdown文件发送消息 Args: title: 消息标题 markdown_file: Markdown文件路径 max_length: 最大长度限制(默认5000字符) at_mobiles: 要@的手机号列表 at_all: 是否@所有人 Returns: 是否发送成功 """ try: with open(markdown_file, 'r', encoding='utf-8') as f: content = f.read() # 转换为钉钉markdown格式(简化一些不支持的语法) text = self._convert_to_dingtalk_markdown(content, max_length) return self.send_markdown(title, text, at_mobiles, at_all) except Exception as e: self.logger.error(f"读取Markdown文件失败: {str(e)}", exc_info=True) return False def _convert_to_dingtalk_markdown(self, content: str, max_length: int = 5000) -> str: """ 将标准Markdown转换为钉钉支持的格式 钉钉Markdown支持的语法: - 标题:# ## ### - 加粗:**text** - 链接:[text](url) - 列表:- 或 1. - 引用:> - 代码:`code` - 换行:两个换行符 不支持: - 表格(需要转换为文本) - HTML标签 - 复杂嵌套 """ # 如果内容太长,截断并添加提示 if len(content) > max_length: content = content[:max_length - 200] + "\n\n---\n\n**提示**: 内容已截断,完整内容请查看报告文件。" # 钉钉markdown基本兼容标准markdown,但需要清理一些不支持的语法 # 保留基本格式即可 text = content return text def _send(self, data: Dict[str, Any]) -> bool: """ 发送消息到钉钉 Args: data: 消息数据 Returns: 是否发送成功 """ try: headers = { 'Content-Type': 'application/json' } response = requests.post( self.webhook_url, headers=headers, data=json.dumps(data), timeout=10 ) response.raise_for_status() result = response.json() if result.get('errcode') == 0: self.logger.info("消息发送成功") return True else: self.logger.error(f"消息发送失败: {result.get('errmsg', '未知错误')}") return False except requests.exceptions.RequestException as e: self.logger.error(f"发送消息请求失败: {str(e)}", exc_info=True) return False except Exception as e: self.logger.error(f"发送消息失败: {str(e)}", exc_info=True) return False def send_report(self, title: str, markdown_content: str, markdown_file: str = None) -> bool: """ 发送报告消息(优化版本,自动处理长内容) Args: title: 消息标题 markdown_content: Markdown内容 markdown_file: Markdown文件路径(可选,用于提示) Returns: 是否发送成功 """ # 钉钉markdown有长度限制,需要截断或分段 max_length = 4500 # 留一些余量 if len(markdown_content) <= max_length: # 内容不长,直接发送 text = markdown_content if markdown_file: text += f"\n\n---\n\n**完整报告**: 已保存到 `{markdown_file}`" return self.send_markdown(title, text) else: # 内容太长,发送摘要 # 提取关键部分(标题、统计、前几条新闻) lines = markdown_content.split('\n') summary_lines = [] news_count = 0 max_news_items = 5 for line in lines: summary_lines.append(line) # 计算已添加的新闻条目数 if line.startswith('### ') and news_count < max_news_items: news_count += 1 # 添加接下来的几行(摘要、链接等) continue elif news_count >= max_news_items and line.startswith('### '): # 达到最大条目数,停止添加 break summary = '\n'.join(summary_lines) # 如果还有更多内容,添加提示 if len(markdown_content) > len(summary): remaining_count = markdown_content.count('### ') - news_count summary += f"\n\n---\n\n**提示**: 报告内容较长,已显示前{news_count}条新闻。" if remaining_count > 0: summary += f" 还有{remaining_count}条新闻未显示。" if markdown_file: summary += f"\n\n**完整报告**: 已保存到 `{markdown_file}`" return self.send_markdown(title, summary)