237 lines
7.8 KiB
Python
237 lines
7.8 KiB
Python
"""
|
|
钉钉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)
|
|
|