mirror of
https://gitee.com/houhuan/TrendRadar.git
synced 2025-12-21 12:47:16 +08:00
v2.4.0
This commit is contained in:
parent
fc927933c7
commit
8914e15d71
@ -1,15 +1,21 @@
|
||||
# Webhook 配置
|
||||
# 推送配置
|
||||
FEISHU_WEBHOOK_URL=
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_CHAT_ID=
|
||||
DINGTALK_WEBHOOK_URL=
|
||||
WEWORK_WEBHOOK_URL=
|
||||
|
||||
EMAIL_FROM=
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_TO=
|
||||
EMAIL_SMTP_SERVER=
|
||||
EMAIL_SMTP_PORT=
|
||||
|
||||
# ntfy 推送配置
|
||||
NTFY_SERVER_URL=https://ntfy.sh # 默认使用公共服务,可改为自托管地址
|
||||
NTFY_TOPIC= # ntfy主题名称
|
||||
NTFY_TOKEN= # 可选:访问令牌(用于私有主题)
|
||||
|
||||
# 运行配置
|
||||
CRON_SCHEDULE=*/30 * * * * # 定时任务表达式,每 30 分钟执行一次(比如 8点,8点半,9点,9点半这种时间规律执行)
|
||||
RUN_MODE=cron # 运行模式:cron/once
|
||||
|
||||
@ -22,6 +22,9 @@ services:
|
||||
- EMAIL_TO=${EMAIL_TO:-}
|
||||
- EMAIL_SMTP_SERVER=${EMAIL_SMTP_SERVER:-}
|
||||
- EMAIL_SMTP_PORT=${EMAIL_SMTP_PORT:-}
|
||||
- NTFY_SERVER_URL=${NTFY_SERVER_URL:-https://ntfy.sh}
|
||||
- NTFY_TOPIC=${NTFY_TOPIC:-}
|
||||
- NTFY_TOKEN=${NTFY_TOKEN:-}
|
||||
- CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *}
|
||||
- RUN_MODE=${RUN_MODE:-cron}
|
||||
- IMMEDIATE_RUN=${IMMEDIATE_RUN:-true}
|
||||
|
||||
@ -20,6 +20,9 @@ services:
|
||||
- EMAIL_TO=${EMAIL_TO:-}
|
||||
- EMAIL_SMTP_SERVER=${EMAIL_SMTP_SERVER:-}
|
||||
- EMAIL_SMTP_PORT=${EMAIL_SMTP_PORT:-}
|
||||
- NTFY_SERVER_URL=${NTFY_SERVER_URL:-https://ntfy.sh}
|
||||
- NTFY_TOPIC=${NTFY_TOPIC:-}
|
||||
- NTFY_TOKEN=${NTFY_TOKEN:-}
|
||||
- CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *}
|
||||
- RUN_MODE=${RUN_MODE:-cron}
|
||||
- IMMEDIATE_RUN=${IMMEDIATE_RUN:-true}
|
||||
|
||||
419
main.py
419
main.py
@ -26,63 +26,31 @@ VERSION = "2.3.2"
|
||||
# === SMTP邮件配置 ===
|
||||
SMTP_CONFIGS = {
|
||||
# Gmail
|
||||
'gmail.com': {
|
||||
'server': 'smtp.gmail.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
},
|
||||
|
||||
"gmail.com": {"server": "smtp.gmail.com", "port": 587, "encryption": "TLS"},
|
||||
# QQ邮箱
|
||||
'qq.com': {
|
||||
'server': 'smtp.qq.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
},
|
||||
|
||||
"qq.com": {"server": "smtp.qq.com", "port": 587, "encryption": "TLS"},
|
||||
# Outlook
|
||||
'outlook.com': {
|
||||
'server': 'smtp-mail.outlook.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
"outlook.com": {
|
||||
"server": "smtp-mail.outlook.com",
|
||||
"port": 587,
|
||||
"encryption": "TLS",
|
||||
},
|
||||
'hotmail.com': {
|
||||
'server': 'smtp-mail.outlook.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
"hotmail.com": {
|
||||
"server": "smtp-mail.outlook.com",
|
||||
"port": 587,
|
||||
"encryption": "TLS",
|
||||
},
|
||||
'live.com': {
|
||||
'server': 'smtp-mail.outlook.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
},
|
||||
|
||||
"live.com": {"server": "smtp-mail.outlook.com", "port": 587, "encryption": "TLS"},
|
||||
# 网易邮箱
|
||||
'163.com': {
|
||||
'server': 'smtp.163.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
},
|
||||
'126.com': {
|
||||
'server': 'smtp.126.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
},
|
||||
|
||||
"163.com": {"server": "smtp.163.com", "port": 587, "encryption": "TLS"},
|
||||
"126.com": {"server": "smtp.126.com", "port": 587, "encryption": "TLS"},
|
||||
# 新浪邮箱
|
||||
'sina.com': {
|
||||
'server': 'smtp.sina.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
},
|
||||
|
||||
"sina.com": {"server": "smtp.sina.com", "port": 587, "encryption": "TLS"},
|
||||
# 搜狐邮箱
|
||||
'sohu.com': {
|
||||
'server': 'smtp.sohu.com',
|
||||
'port': 587,
|
||||
'encryption': 'TLS'
|
||||
}
|
||||
"sohu.com": {"server": "smtp.sohu.com", "port": 587, "encryption": "TLS"},
|
||||
}
|
||||
|
||||
|
||||
# === 配置管理 ===
|
||||
def load_config():
|
||||
"""加载配置文件"""
|
||||
@ -108,7 +76,9 @@ def load_config():
|
||||
"ENABLE_CRAWLER": config_data["crawler"]["enable_crawler"],
|
||||
"ENABLE_NOTIFICATION": config_data["notification"]["enable_notification"],
|
||||
"MESSAGE_BATCH_SIZE": config_data["notification"]["message_batch_size"],
|
||||
"DINGTALK_BATCH_SIZE": config_data["notification"].get("dingtalk_batch_size", 20000),
|
||||
"DINGTALK_BATCH_SIZE": config_data["notification"].get(
|
||||
"dingtalk_batch_size", 20000
|
||||
),
|
||||
"BATCH_SEND_INTERVAL": config_data["notification"]["batch_send_interval"],
|
||||
"FEISHU_MESSAGE_SEPARATOR": config_data["notification"][
|
||||
"feishu_message_separator"
|
||||
@ -161,17 +131,17 @@ def load_config():
|
||||
config["TELEGRAM_CHAT_ID"] = os.environ.get(
|
||||
"TELEGRAM_CHAT_ID", ""
|
||||
).strip() or webhooks.get("telegram_chat_id", "")
|
||||
|
||||
|
||||
# 邮件配置
|
||||
config["EMAIL_FROM"] = os.environ.get(
|
||||
"EMAIL_FROM", ""
|
||||
).strip() or webhooks.get("email_from", "")
|
||||
config["EMAIL_FROM"] = os.environ.get("EMAIL_FROM", "").strip() or webhooks.get(
|
||||
"email_from", ""
|
||||
)
|
||||
config["EMAIL_PASSWORD"] = os.environ.get(
|
||||
"EMAIL_PASSWORD", ""
|
||||
).strip() or webhooks.get("email_password", "")
|
||||
config["EMAIL_TO"] = os.environ.get(
|
||||
"EMAIL_TO", ""
|
||||
).strip() or webhooks.get("email_to", "")
|
||||
config["EMAIL_TO"] = os.environ.get("EMAIL_TO", "").strip() or webhooks.get(
|
||||
"email_to", ""
|
||||
)
|
||||
config["EMAIL_SMTP_SERVER"] = os.environ.get(
|
||||
"EMAIL_SMTP_SERVER", ""
|
||||
).strip() or webhooks.get("email_smtp_server", "")
|
||||
@ -179,6 +149,17 @@ def load_config():
|
||||
"EMAIL_SMTP_PORT", ""
|
||||
).strip() or webhooks.get("email_smtp_port", "")
|
||||
|
||||
# ntfy配置
|
||||
config["NTFY_SERVER_URL"] = os.environ.get(
|
||||
"NTFY_SERVER_URL", "https://ntfy.sh"
|
||||
).strip() or webhooks.get("ntfy_server_url", "https://ntfy.sh")
|
||||
config["NTFY_TOPIC"] = os.environ.get("NTFY_TOPIC", "").strip() or webhooks.get(
|
||||
"ntfy_topic", ""
|
||||
)
|
||||
config["NTFY_TOKEN"] = os.environ.get("NTFY_TOKEN", "").strip() or webhooks.get(
|
||||
"ntfy_token", ""
|
||||
)
|
||||
|
||||
# 输出配置来源信息
|
||||
notification_sources = []
|
||||
if config["FEISHU_WEBHOOK_URL"]:
|
||||
@ -199,7 +180,11 @@ def load_config():
|
||||
if config["EMAIL_FROM"] and config["EMAIL_PASSWORD"] and config["EMAIL_TO"]:
|
||||
from_source = "环境变量" if os.environ.get("EMAIL_FROM") else "配置文件"
|
||||
notification_sources.append(f"邮件({from_source})")
|
||||
|
||||
|
||||
if config["NTFY_SERVER_URL"] and config["NTFY_TOPIC"]:
|
||||
server_source = "环境变量" if os.environ.get("NTFY_SERVER_URL") else "配置文件"
|
||||
notification_sources.append(f"ntfy({server_source})")
|
||||
|
||||
if notification_sources:
|
||||
print(f"通知渠道配置来源: {', '.join(notification_sources)}")
|
||||
else:
|
||||
@ -1506,6 +1491,28 @@ def format_title_for_platform(
|
||||
|
||||
return result
|
||||
|
||||
elif platform == "ntfy":
|
||||
if link_url:
|
||||
formatted_title = f"[{cleaned_title}]({link_url})"
|
||||
else:
|
||||
formatted_title = cleaned_title
|
||||
|
||||
title_prefix = "🆕 " if title_data.get("is_new") else ""
|
||||
|
||||
if show_source:
|
||||
result = f"[{title_data['source_name']}] {title_prefix}{formatted_title}"
|
||||
else:
|
||||
result = f"{title_prefix}{formatted_title}"
|
||||
|
||||
if rank_display:
|
||||
result += f" {rank_display}"
|
||||
if title_data["time_display"]:
|
||||
result += f" `- {title_data['time_display']}`"
|
||||
if title_data["count"] > 1:
|
||||
result += f" `({title_data['count']}次)`"
|
||||
|
||||
return result
|
||||
|
||||
elif platform == "html":
|
||||
rank_display = format_rank_display(
|
||||
title_data["ranks"], title_data["rank_threshold"], "html"
|
||||
@ -2245,7 +2252,7 @@ def render_html_content(
|
||||
<a href="https://github.com/sansan0/TrendRadar" target="_blank" class="footer-link">
|
||||
GitHub 开源项目
|
||||
</a>"""
|
||||
|
||||
|
||||
if update_info:
|
||||
html += f"""
|
||||
<br>
|
||||
@ -2534,9 +2541,11 @@ def split_content_into_batches(
|
||||
if max_bytes is None:
|
||||
if format_type == "dingtalk":
|
||||
max_bytes = CONFIG.get("DINGTALK_BATCH_SIZE", 20000)
|
||||
elif format_type == "ntfy":
|
||||
max_bytes = 3800
|
||||
else:
|
||||
max_bytes = CONFIG.get("MESSAGE_BATCH_SIZE", 4000)
|
||||
|
||||
|
||||
batches = []
|
||||
|
||||
total_titles = sum(
|
||||
@ -2549,6 +2558,8 @@ def split_content_into_batches(
|
||||
base_header = f"**总新闻数:** {total_titles}\n\n\n\n"
|
||||
elif format_type == "telegram":
|
||||
base_header = f"总新闻数: {total_titles}\n\n"
|
||||
elif format_type == "ntfy":
|
||||
base_header = f"**总新闻数:** {total_titles}\n\n"
|
||||
elif format_type == "dingtalk":
|
||||
base_header = f"**总新闻数:** {total_titles}\n\n"
|
||||
base_header += f"**时间:** {now.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
@ -2564,6 +2575,10 @@ def split_content_into_batches(
|
||||
base_footer = f"\n\n更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
if update_info:
|
||||
base_footer += f"\nTrendRadar 发现新版本 {update_info['remote_version']},当前 {update_info['current_version']}"
|
||||
elif format_type == "ntfy":
|
||||
base_footer = f"\n\n> 更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
if update_info:
|
||||
base_footer += f"\n> TrendRadar 发现新版本 **{update_info['remote_version']}**,当前 **{update_info['current_version']}**"
|
||||
elif format_type == "dingtalk":
|
||||
base_footer = f"\n\n> 更新时间:{now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
if update_info:
|
||||
@ -2575,6 +2590,8 @@ def split_content_into_batches(
|
||||
stats_header = f"📊 **热点词汇统计**\n\n"
|
||||
elif format_type == "telegram":
|
||||
stats_header = f"📊 热点词汇统计\n\n"
|
||||
elif format_type == "ntfy":
|
||||
stats_header = f"📊 **热点词汇统计**\n\n"
|
||||
elif format_type == "dingtalk":
|
||||
stats_header = f"📊 **热点词汇统计**\n\n"
|
||||
|
||||
@ -2641,6 +2658,17 @@ def split_content_into_batches(
|
||||
word_header = f"📈 {sequence_display} {word} : {count} 条\n\n"
|
||||
else:
|
||||
word_header = f"📌 {sequence_display} {word} : {count} 条\n\n"
|
||||
elif format_type == "ntfy":
|
||||
if count >= 10:
|
||||
word_header = (
|
||||
f"🔥 {sequence_display} **{word}** : **{count}** 条\n\n"
|
||||
)
|
||||
elif count >= 5:
|
||||
word_header = (
|
||||
f"📈 {sequence_display} **{word}** : **{count}** 条\n\n"
|
||||
)
|
||||
else:
|
||||
word_header = f"📌 {sequence_display} **{word}** : {count} 条\n\n"
|
||||
elif format_type == "dingtalk":
|
||||
if count >= 10:
|
||||
word_header = (
|
||||
@ -2665,6 +2693,10 @@ def split_content_into_batches(
|
||||
formatted_title = format_title_for_platform(
|
||||
"telegram", first_title_data, show_source=True
|
||||
)
|
||||
elif format_type == "ntfy":
|
||||
formatted_title = format_title_for_platform(
|
||||
"ntfy", first_title_data, show_source=True
|
||||
)
|
||||
elif format_type == "dingtalk":
|
||||
formatted_title = format_title_for_platform(
|
||||
"dingtalk", first_title_data, show_source=True
|
||||
@ -2706,6 +2738,10 @@ def split_content_into_batches(
|
||||
formatted_title = format_title_for_platform(
|
||||
"telegram", title_data, show_source=True
|
||||
)
|
||||
elif format_type == "ntfy":
|
||||
formatted_title = format_title_for_platform(
|
||||
"ntfy", title_data, show_source=True
|
||||
)
|
||||
elif format_type == "dingtalk":
|
||||
formatted_title = format_title_for_platform(
|
||||
"dingtalk", title_data, show_source=True
|
||||
@ -2737,6 +2773,8 @@ def split_content_into_batches(
|
||||
separator = f"\n\n\n\n"
|
||||
elif format_type == "telegram":
|
||||
separator = f"\n\n"
|
||||
elif format_type == "ntfy":
|
||||
separator = f"\n\n"
|
||||
elif format_type == "dingtalk":
|
||||
separator = f"\n---\n\n"
|
||||
|
||||
@ -2756,6 +2794,8 @@ def split_content_into_batches(
|
||||
new_header = (
|
||||
f"\n\n🆕 本次新增热点新闻 (共 {report_data['total_new_count']} 条)\n\n"
|
||||
)
|
||||
elif format_type == "ntfy":
|
||||
new_header = f"\n\n🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
|
||||
elif format_type == "dingtalk":
|
||||
new_header = f"\n---\n\n🆕 **本次新增热点新闻** (共 {report_data['total_new_count']} 条)\n\n"
|
||||
|
||||
@ -2779,6 +2819,8 @@ def split_content_into_batches(
|
||||
source_header = f"**{source_data['source_name']}** ({len(source_data['titles'])} 条):\n\n"
|
||||
elif format_type == "telegram":
|
||||
source_header = f"{source_data['source_name']} ({len(source_data['titles'])} 条):\n\n"
|
||||
elif format_type == "ntfy":
|
||||
source_header = f"**{source_data['source_name']}** ({len(source_data['titles'])} 条):\n\n"
|
||||
elif format_type == "dingtalk":
|
||||
source_header = f"**{source_data['source_name']}** ({len(source_data['titles'])} 条):\n\n"
|
||||
|
||||
@ -2868,6 +2910,8 @@ def split_content_into_batches(
|
||||
failed_header = f"\n\n\n\n⚠️ **数据获取失败的平台:**\n\n"
|
||||
elif format_type == "telegram":
|
||||
failed_header = f"\n\n⚠️ 数据获取失败的平台:\n\n"
|
||||
elif format_type == "ntfy":
|
||||
failed_header = f"\n\n⚠️ **数据获取失败的平台:**\n\n"
|
||||
elif format_type == "dingtalk":
|
||||
failed_header = f"\n---\n\n⚠️ **数据获取失败的平台:**\n\n"
|
||||
|
||||
@ -2889,7 +2933,7 @@ def split_content_into_batches(
|
||||
failed_line = f" • **{id_value}**\n"
|
||||
else:
|
||||
failed_line = f" • {id_value}\n"
|
||||
|
||||
|
||||
test_content = current_batch + failed_line
|
||||
if (
|
||||
len(test_content.encode("utf-8")) + len(base_footer.encode("utf-8"))
|
||||
@ -2928,19 +2972,21 @@ def send_to_notifications(
|
||||
push_manager = PushRecordManager()
|
||||
time_range_start = CONFIG["SILENT_PUSH"]["TIME_RANGE"]["START"]
|
||||
time_range_end = CONFIG["SILENT_PUSH"]["TIME_RANGE"]["END"]
|
||||
|
||||
|
||||
if not push_manager.is_in_time_range(time_range_start, time_range_end):
|
||||
now = get_beijing_time()
|
||||
print(f"静默模式:当前时间 {now.strftime('%H:%M')} 不在推送时间范围 {time_range_start}-{time_range_end} 内,跳过推送")
|
||||
print(
|
||||
f"静默模式:当前时间 {now.strftime('%H:%M')} 不在推送时间范围 {time_range_start}-{time_range_end} 内,跳过推送"
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
if CONFIG["SILENT_PUSH"]["ONCE_PER_DAY"]:
|
||||
if push_manager.has_pushed_today():
|
||||
print(f"静默模式:今天已推送过,跳过本次推送")
|
||||
return results
|
||||
else:
|
||||
print(f"静默模式:今天首次推送")
|
||||
|
||||
|
||||
report_data = prepare_report_data(stats, failed_ids, new_titles, id_to_name, mode)
|
||||
|
||||
feishu_url = CONFIG["FEISHU_WEBHOOK_URL"]
|
||||
@ -2953,6 +2999,9 @@ def send_to_notifications(
|
||||
email_to = CONFIG["EMAIL_TO"]
|
||||
email_smtp_server = CONFIG.get("EMAIL_SMTP_SERVER", "")
|
||||
email_smtp_port = CONFIG.get("EMAIL_SMTP_PORT", "")
|
||||
ntfy_server_url = CONFIG["NTFY_SERVER_URL"]
|
||||
ntfy_topic = CONFIG["NTFY_TOPIC"]
|
||||
ntfy_token = CONFIG.get("NTFY_TOKEN", "")
|
||||
|
||||
update_info_to_send = update_info if CONFIG["SHOW_VERSION_UPDATE"] else None
|
||||
|
||||
@ -2986,6 +3035,19 @@ def send_to_notifications(
|
||||
mode,
|
||||
)
|
||||
|
||||
# 发送到 ntfy
|
||||
if ntfy_server_url and ntfy_topic:
|
||||
results["ntfy"] = send_to_ntfy(
|
||||
ntfy_server_url,
|
||||
ntfy_topic,
|
||||
ntfy_token,
|
||||
report_data,
|
||||
report_type,
|
||||
update_info_to_send,
|
||||
proxy_url,
|
||||
mode,
|
||||
)
|
||||
|
||||
# 发送邮件
|
||||
if email_from and email_password and email_to:
|
||||
results["email"] = send_to_email(
|
||||
@ -3002,10 +3064,14 @@ def send_to_notifications(
|
||||
print("未配置任何通知渠道,跳过通知发送")
|
||||
|
||||
# 如果成功发送了任何通知,且启用了每天只推一次,则记录推送
|
||||
if CONFIG["SILENT_PUSH"]["ENABLED"] and CONFIG["SILENT_PUSH"]["ONCE_PER_DAY"] and any(results.values()):
|
||||
if (
|
||||
CONFIG["SILENT_PUSH"]["ENABLED"]
|
||||
and CONFIG["SILENT_PUSH"]["ONCE_PER_DAY"]
|
||||
and any(results.values())
|
||||
):
|
||||
push_manager = PushRecordManager()
|
||||
push_manager.record_push(report_type)
|
||||
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@ -3071,11 +3137,11 @@ def send_to_dingtalk(
|
||||
|
||||
# 获取分批内容,使用钉钉专用的批次大小
|
||||
batches = split_content_into_batches(
|
||||
report_data,
|
||||
"dingtalk",
|
||||
update_info,
|
||||
report_data,
|
||||
"dingtalk",
|
||||
update_info,
|
||||
max_bytes=CONFIG.get("DINGTALK_BATCH_SIZE", 20000),
|
||||
mode=mode
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
print(f"钉钉消息分为 {len(batches)} 批次发送 [{report_type}]")
|
||||
@ -3093,8 +3159,7 @@ def send_to_dingtalk(
|
||||
# 将批次标识插入到适当位置(在标题之后)
|
||||
if "📊 **热点词汇统计**" in batch_content:
|
||||
batch_content = batch_content.replace(
|
||||
"📊 **热点词汇统计**\n\n",
|
||||
f"📊 **热点词汇统计** {batch_header}\n\n"
|
||||
"📊 **热点词汇统计**\n\n", f"📊 **热点词汇统计** {batch_header}\n\n"
|
||||
)
|
||||
else:
|
||||
# 如果没有统计标题,直接在开头添加
|
||||
@ -3270,6 +3335,7 @@ def send_to_telegram(
|
||||
print(f"Telegram所有 {len(batches)} 批次发送完成 [{report_type}]")
|
||||
return True
|
||||
|
||||
|
||||
def send_to_email(
|
||||
from_email: str,
|
||||
password: str,
|
||||
@ -3284,13 +3350,13 @@ def send_to_email(
|
||||
if not html_file_path or not Path(html_file_path).exists():
|
||||
print(f"错误:HTML文件不存在或未提供: {html_file_path}")
|
||||
return False
|
||||
|
||||
|
||||
print(f"使用HTML文件: {html_file_path}")
|
||||
with open(html_file_path, "r", encoding="utf-8") as f:
|
||||
html_content = f.read()
|
||||
|
||||
domain = from_email.split('@')[-1].lower()
|
||||
|
||||
|
||||
domain = from_email.split("@")[-1].lower()
|
||||
|
||||
if custom_smtp_server and custom_smtp_port:
|
||||
# 使用自定义 SMTP 配置
|
||||
smtp_server = custom_smtp_server
|
||||
@ -3299,38 +3365,38 @@ def send_to_email(
|
||||
elif domain in SMTP_CONFIGS:
|
||||
# 使用预设配置
|
||||
config = SMTP_CONFIGS[domain]
|
||||
smtp_server = config['server']
|
||||
smtp_port = config['port']
|
||||
use_tls = config['encryption'] == 'TLS'
|
||||
smtp_server = config["server"]
|
||||
smtp_port = config["port"]
|
||||
use_tls = config["encryption"] == "TLS"
|
||||
else:
|
||||
print(f"未识别的邮箱服务商: {domain},使用通用 SMTP 配置")
|
||||
smtp_server = f"smtp.{domain}"
|
||||
smtp_port = 587
|
||||
use_tls = True
|
||||
|
||||
msg = MIMEMultipart('alternative')
|
||||
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
|
||||
# 严格按照 RFC 标准设置 From header
|
||||
sender_name = "TrendRadar"
|
||||
msg['From'] = formataddr((sender_name, from_email))
|
||||
|
||||
msg["From"] = formataddr((sender_name, from_email))
|
||||
|
||||
# 设置收件人
|
||||
recipients = [addr.strip() for addr in to_email.split(',')]
|
||||
recipients = [addr.strip() for addr in to_email.split(",")]
|
||||
if len(recipients) == 1:
|
||||
msg['To'] = recipients[0]
|
||||
msg["To"] = recipients[0]
|
||||
else:
|
||||
msg['To'] = ', '.join(recipients)
|
||||
|
||||
msg["To"] = ", ".join(recipients)
|
||||
|
||||
# 设置邮件主题
|
||||
now = get_beijing_time()
|
||||
subject = f"TrendRadar 热点分析报告 - {report_type} - {now.strftime('%m月%d日 %H:%M')}"
|
||||
msg['Subject'] = Header(subject, 'utf-8')
|
||||
|
||||
msg["Subject"] = Header(subject, "utf-8")
|
||||
|
||||
# 设置其他标准 header
|
||||
msg['MIME-Version'] = '1.0'
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
msg['Message-ID'] = make_msgid()
|
||||
|
||||
msg["MIME-Version"] = "1.0"
|
||||
msg["Date"] = formatdate(localtime=True)
|
||||
msg["Message-ID"] = make_msgid()
|
||||
|
||||
# 添加纯文本部分(作为备选)
|
||||
text_content = f"""
|
||||
TrendRadar 热点分析报告
|
||||
@ -3340,16 +3406,16 @@ TrendRadar 热点分析报告
|
||||
|
||||
请使用支持HTML的邮件客户端查看完整报告内容。
|
||||
"""
|
||||
text_part = MIMEText(text_content, 'plain', 'utf-8')
|
||||
text_part = MIMEText(text_content, "plain", "utf-8")
|
||||
msg.attach(text_part)
|
||||
|
||||
html_part = MIMEText(html_content, 'html', 'utf-8')
|
||||
|
||||
html_part = MIMEText(html_content, "html", "utf-8")
|
||||
msg.attach(html_part)
|
||||
|
||||
|
||||
print(f"正在发送邮件到 {to_email}...")
|
||||
print(f"SMTP 服务器: {smtp_server}:{smtp_port}")
|
||||
print(f"发件人: {from_email}")
|
||||
|
||||
|
||||
try:
|
||||
if use_tls:
|
||||
# TLS 模式
|
||||
@ -3363,21 +3429,21 @@ TrendRadar 热点分析报告
|
||||
server = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=30)
|
||||
server.set_debuglevel(0)
|
||||
server.ehlo()
|
||||
|
||||
|
||||
# 登录
|
||||
server.login(from_email, password)
|
||||
|
||||
|
||||
# 发送邮件
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
|
||||
|
||||
print(f"邮件发送成功 [{report_type}] -> {to_email}")
|
||||
return True
|
||||
|
||||
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
print(f"邮件发送失败:服务器意外断开连接,请检查网络或稍后重试")
|
||||
return False
|
||||
|
||||
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
print(f"邮件发送失败:认证错误,请检查邮箱和密码/授权码")
|
||||
print(f"详细错误: {str(e)}")
|
||||
@ -3398,10 +3464,142 @@ TrendRadar 热点分析报告
|
||||
except Exception as e:
|
||||
print(f"邮件发送失败 [{report_type}]:{e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def send_to_ntfy(
|
||||
server_url: str,
|
||||
topic: str,
|
||||
token: Optional[str],
|
||||
report_data: Dict,
|
||||
report_type: str,
|
||||
update_info: Optional[Dict] = None,
|
||||
proxy_url: Optional[str] = None,
|
||||
mode: str = "daily",
|
||||
) -> bool:
|
||||
"""发送到ntfy(支持分批发送,严格遵守4KB限制)"""
|
||||
headers = {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Markdown": "yes",
|
||||
"Title": f"TrendRadar 热点分析报告 - {report_type}",
|
||||
"Priority": "default",
|
||||
"Tags": "newspaper,📰",
|
||||
}
|
||||
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
# 构建完整URL,确保格式正确
|
||||
base_url = server_url.rstrip("/")
|
||||
if not base_url.startswith(("http://", "https://")):
|
||||
base_url = f"https://{base_url}"
|
||||
url = f"{base_url}/{topic}"
|
||||
|
||||
proxies = None
|
||||
if proxy_url:
|
||||
proxies = {"http": proxy_url, "https": proxy_url}
|
||||
|
||||
# 获取分批内容,使用ntfy专用的4KB限制
|
||||
batches = split_content_into_batches(
|
||||
report_data, "ntfy", update_info, max_bytes=3800, mode=mode
|
||||
)
|
||||
|
||||
print(f"ntfy消息分为 {len(batches)} 批次发送 [{report_type}]")
|
||||
|
||||
# 逐批发送
|
||||
success_count = 0
|
||||
for i, batch_content in enumerate(batches, 1):
|
||||
batch_size = len(batch_content.encode("utf-8"))
|
||||
print(
|
||||
f"发送ntfy第 {i}/{len(batches)} 批次,大小:{batch_size} 字节 [{report_type}]"
|
||||
)
|
||||
|
||||
# 检查消息大小,确保不超过4KB
|
||||
if batch_size > 4096:
|
||||
print(f"警告:ntfy第 {i} 批次消息过大({batch_size} 字节),可能被拒绝")
|
||||
|
||||
# 添加批次标识
|
||||
current_headers = headers.copy()
|
||||
if len(batches) > 1:
|
||||
batch_header = f"**[第 {i}/{len(batches)} 批次]**\n\n"
|
||||
batch_content = batch_header + batch_content
|
||||
current_headers["Title"] = (
|
||||
f"TrendRadar 热点分析报告 - {report_type} ({i}/{len(batches)})"
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=current_headers,
|
||||
data=batch_content.encode("utf-8"),
|
||||
proxies=proxies,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"ntfy第 {i}/{len(batches)} 批次发送成功 [{report_type}]")
|
||||
success_count += 1
|
||||
if i < len(batches):
|
||||
# 公共服务器建议 2-3 秒,自托管可以更短
|
||||
interval = 2 if "ntfy.sh" in server_url else 1
|
||||
time.sleep(interval)
|
||||
elif response.status_code == 429:
|
||||
print(
|
||||
f"ntfy第 {i}/{len(batches)} 批次速率限制 [{report_type}],等待后重试"
|
||||
)
|
||||
time.sleep(10) # 等待10秒后重试
|
||||
# 重试一次
|
||||
retry_response = requests.post(
|
||||
url,
|
||||
headers=current_headers,
|
||||
data=batch_content.encode("utf-8"),
|
||||
proxies=proxies,
|
||||
timeout=30,
|
||||
)
|
||||
if retry_response.status_code == 200:
|
||||
print(f"ntfy第 {i}/{len(batches)} 批次重试成功 [{report_type}]")
|
||||
success_count += 1
|
||||
else:
|
||||
print(
|
||||
f"ntfy第 {i}/{len(batches)} 批次重试失败,状态码:{retry_response.status_code}"
|
||||
)
|
||||
elif response.status_code == 413:
|
||||
print(
|
||||
f"ntfy第 {i}/{len(batches)} 批次消息过大被拒绝 [{report_type}],消息大小:{batch_size} 字节"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"ntfy第 {i}/{len(batches)} 批次发送失败 [{report_type}],状态码:{response.status_code}"
|
||||
)
|
||||
try:
|
||||
error_detail = response.text[:200] # 只显示前200字符的错误信息
|
||||
print(f"错误详情:{error_detail}")
|
||||
except:
|
||||
pass
|
||||
|
||||
except requests.exceptions.ConnectTimeout:
|
||||
print(f"ntfy第 {i}/{len(batches)} 批次连接超时 [{report_type}]")
|
||||
except requests.exceptions.ReadTimeout:
|
||||
print(f"ntfy第 {i}/{len(batches)} 批次读取超时 [{report_type}]")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"ntfy第 {i}/{len(batches)} 批次连接错误 [{report_type}]:{e}")
|
||||
except Exception as e:
|
||||
print(f"ntfy第 {i}/{len(batches)} 批次发送异常 [{report_type}]:{e}")
|
||||
|
||||
# 判断整体发送是否成功
|
||||
if success_count == len(batches):
|
||||
print(f"ntfy所有 {len(batches)} 批次发送完成 [{report_type}]")
|
||||
return True
|
||||
elif success_count > 0:
|
||||
print(f"ntfy部分发送成功:{success_count}/{len(batches)} 批次 [{report_type}]")
|
||||
return True # 部分成功也视为成功
|
||||
else:
|
||||
print(f"ntfy发送完全失败 [{report_type}]")
|
||||
return False
|
||||
|
||||
|
||||
# === 主分析器 ===
|
||||
class NewsAnalyzer:
|
||||
"""新闻分析器"""
|
||||
@ -3508,7 +3706,12 @@ class NewsAnalyzer:
|
||||
CONFIG["DINGTALK_WEBHOOK_URL"],
|
||||
CONFIG["WEWORK_WEBHOOK_URL"],
|
||||
(CONFIG["TELEGRAM_BOT_TOKEN"] and CONFIG["TELEGRAM_CHAT_ID"]),
|
||||
(CONFIG["EMAIL_FROM"] and CONFIG["EMAIL_PASSWORD"] and CONFIG["EMAIL_TO"]),
|
||||
(
|
||||
CONFIG["EMAIL_FROM"]
|
||||
and CONFIG["EMAIL_PASSWORD"]
|
||||
and CONFIG["EMAIL_TO"]
|
||||
),
|
||||
(CONFIG["NTFY_SERVER_URL"] and CONFIG["NTFY_TOPIC"]),
|
||||
]
|
||||
)
|
||||
|
||||
@ -3652,7 +3855,7 @@ class NewsAnalyzer:
|
||||
self.update_info,
|
||||
self.proxy_url,
|
||||
mode=mode,
|
||||
html_file_path=html_file_path,
|
||||
html_file_path=html_file_path,
|
||||
)
|
||||
return True
|
||||
elif CONFIG["ENABLE_NOTIFICATION"] and not has_notification:
|
||||
@ -3705,7 +3908,7 @@ class NewsAnalyzer:
|
||||
)
|
||||
|
||||
print(f"{summary_type}报告已生成: {html_file}")
|
||||
|
||||
|
||||
# 发送通知
|
||||
self._send_notification_if_needed(
|
||||
stats,
|
||||
@ -3714,7 +3917,7 @@ class NewsAnalyzer:
|
||||
failed_ids=[],
|
||||
new_titles=new_titles,
|
||||
id_to_name=id_to_name,
|
||||
html_file_path=html_file,
|
||||
html_file_path=html_file,
|
||||
)
|
||||
|
||||
return html_file
|
||||
|
||||
147
readme.md
147
readme.md
@ -11,13 +11,15 @@
|
||||
[](https://github.com/sansan0/TrendRadar/stargazers)
|
||||
[](https://github.com/sansan0/TrendRadar/network/members)
|
||||
[](LICENSE)
|
||||
[](https://github.com/sansan0/TrendRadar)
|
||||
[](https://github.com/sansan0/TrendRadar)
|
||||
|
||||
[](https://work.weixin.qq.com/)
|
||||
[](https://telegram.org/)
|
||||
[](#)
|
||||
[](https://www.feishu.cn/)
|
||||
[](mailto:)
|
||||
[](#)
|
||||
[](https://github.com/binwiederhier/ntfy)
|
||||
|
||||
[](https://github.com/sansan0/TrendRadar)
|
||||
[](https://sansan0.github.io/TrendRadar)
|
||||
[](https://hub.docker.com/)
|
||||
@ -34,7 +36,7 @@
|
||||
- **给予资金点赞支持** 的朋友们,你们的慷慨已化身为键盘旁的零食饮料,陪伴着项目的每一次迭代
|
||||
|
||||
<details>
|
||||
<summary>👉 点击查看<strong>致谢名单</strong> (当前 <strong>🔥24🔥</strong> 位)</summary>
|
||||
<summary>👉 点击查看<strong>致谢名单</strong> (当前 <strong>🔥25🔥</strong> 位)</summary>
|
||||
|
||||
### 数据支持
|
||||
|
||||
@ -54,6 +56,7 @@
|
||||
|
||||
| 点赞人 | 金额 | 日期 | 备注 |
|
||||
| :-------------------------: | :----: | :----: | :-----------------------: |
|
||||
| **培 | 5.2 | 2025.10.2 | github-yzyf1312:开源万岁 |
|
||||
| *椿 | 3 | 2025.9.23 | 加油,很不错 |
|
||||
| *🍍 | 10 | 2025.9.21 | |
|
||||
| E*f | 1 | 2025.9.20 | |
|
||||
@ -421,7 +424,7 @@ weight:
|
||||
|
||||
### **多渠道实时推送**
|
||||
|
||||
支持**企业微信**(+ 微信推送方案)、**飞书**、**钉钉**、**Telegram**、**邮件**,消息直达手机和邮箱
|
||||
支持**企业微信**(+ 微信推送方案)、**飞书**、**钉钉**、**Telegram**、**邮件**、**ntfy**,消息直达手机和邮箱
|
||||
|
||||
### **多端适配**
|
||||
- **GitHub Pages**:自动生成精美网页报告,PC/移动端适配
|
||||
@ -459,6 +462,25 @@ GitHub 一键 Fork 即可使用,无需编程基础。
|
||||
- **小版本更新**:从 v2.x 升级到 v2.y, 用本项目的 `main.py` 代码替换你 fork 仓库中的对应文件
|
||||
- **大版本升级**:从 v1.x 升级到 v2.y, 建议删除现有 fork 后重新 fork,这样更省力且避免配置冲突
|
||||
|
||||
### 2025/10/2 - v2.4.0
|
||||
|
||||
**新增 ntfy 推送通知**
|
||||
|
||||
- **核心功能**:
|
||||
- 支持 ntfy.sh 公共服务和自托管服务器
|
||||
|
||||
- **使用场景**:
|
||||
- 适合追求隐私的用户(支持自托管)
|
||||
- 跨平台推送(iOS、Android、Desktop、Web)
|
||||
- 无需注册账号(公共服务器)
|
||||
- 开源免费(MIT 协议)
|
||||
|
||||
- **更新提示**:
|
||||
- 建议使用【大版本更新】
|
||||
|
||||
<details>
|
||||
<summary><strong>👉 历史更新</strong></summary>
|
||||
|
||||
### 2025/09/26 - v2.3.2
|
||||
|
||||
- 修正了邮件通知配置检查被遗漏的问题([#88](https://github.com/sansan0/TrendRadar/issues/88))
|
||||
@ -466,9 +488,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。
|
||||
**修复说明**:
|
||||
- 解决了即使正确配置邮件通知,系统仍提示"未配置任何webhook"的问题
|
||||
|
||||
<details>
|
||||
<summary><strong>👉 历史更新</strong></summary>
|
||||
|
||||
### 2025/09/22 - v2.3.1
|
||||
|
||||
- **新增邮件推送功能**,支持将热点新闻报告发送到邮箱
|
||||
@ -816,6 +835,117 @@ frequency_words.txt 文件增加了一个【必须词】功能,使用 + 号
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <strong>👉 ntfy 推送</strong>(开源免费,支持自托管)</summary>
|
||||
<br>
|
||||
|
||||
**两种使用方式:**
|
||||
|
||||
### 方式一:免费使用(推荐新手) 🆓
|
||||
|
||||
**特点**:
|
||||
- ✅ 无需注册账号,立即使用
|
||||
- ✅ 每天 250 条消息(足够 90% 用户)
|
||||
- ✅ Topic 名称即"密码"(需选择不易猜测的名称)
|
||||
- ⚠️ 消息未加密,不适合敏感信息, 但适合我们这个项目的不敏感信息
|
||||
|
||||
**快速开始:**
|
||||
|
||||
1. **下载 ntfy 应用**:
|
||||
- Android:[Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) / [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/)
|
||||
- iOS:[App Store](https://apps.apple.com/us/app/ntfy/id1625396347)
|
||||
- 桌面:访问 [ntfy.sh](https://ntfy.sh)
|
||||
|
||||
2. **订阅主题**(选择一个难猜的名称):
|
||||
```
|
||||
建议格式:trendradar-{你的名字缩写}-{随机数字}
|
||||
|
||||
✅ 好例子:trendradar-zs-8492
|
||||
❌ 坏例子:news、alerts(太容易被猜到)
|
||||
```
|
||||
|
||||
3. **配置 GitHub Secret**:
|
||||
- `NTFY_TOPIC`:填写你刚才订阅的主题名称
|
||||
- `NTFY_SERVER_URL`:留空(默认使用 ntfy.sh)
|
||||
- `NTFY_TOKEN`:留空
|
||||
|
||||
4. **测试**:
|
||||
```bash
|
||||
curl -d "测试消息" ntfy.sh/你的主题名称
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方式二:自托管(完全隐私控制) 🔒
|
||||
|
||||
**适合人群**:有服务器、追求完全隐私、技术能力强
|
||||
|
||||
**优势**:
|
||||
- ✅ 完全开源(Apache 2.0 + GPLv2)
|
||||
- ✅ 数据完全自主控制
|
||||
- ✅ 无任何限制
|
||||
- ✅ 零费用
|
||||
|
||||
**Docker 一键部署**:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name ntfy \
|
||||
-p 80:80 \
|
||||
-v /var/cache/ntfy:/var/cache/ntfy \
|
||||
binwiederhier/ntfy \
|
||||
serve --cache-file /var/cache/ntfy/cache.db
|
||||
```
|
||||
|
||||
**配置 TrendRadar**:
|
||||
```yaml
|
||||
NTFY_SERVER_URL: https://ntfy.yourdomain.com
|
||||
NTFY_TOPIC: trendradar-alerts # 自托管可用简单名称
|
||||
NTFY_TOKEN: tk_your_token # 可选:启用访问控制
|
||||
```
|
||||
|
||||
**在应用中订阅**:
|
||||
- 点击"Use another server"
|
||||
- 输入你的服务器地址
|
||||
- 输入主题名称
|
||||
- (可选)输入登录凭据
|
||||
|
||||
---
|
||||
|
||||
**常见问题:**
|
||||
|
||||
<details>
|
||||
<summary><strong>Q1: 免费版够用吗?</strong></summary>
|
||||
|
||||
每天 250 条消息对大多数用户足够。按 30 分钟抓取一次计算,每天约 48 次推送,完全够用。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Q2: Topic 名称真的安全吗?</strong></summary>
|
||||
|
||||
如果你选择随机的、足够长的名称(如 `trendradar-zs-8492-news`),暴力破解几乎不可能:
|
||||
- ntfy 有严格的速率限制(1 秒 1 次请求)
|
||||
- 64 个字符选择(A-Z, a-z, 0-9, _, -)
|
||||
- 10 位随机字符串有 64^10 种可能性(需要数年才能破解)
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**推荐选择:**
|
||||
|
||||
| 用户类型 | 推荐方案 | 理由 |
|
||||
|---------|---------|------|
|
||||
| 普通用户 | 方式一(免费) | 简单快速,够用 |
|
||||
| 技术用户 | 方式二(自托管) | 完全控制,无限制 |
|
||||
| 高频用户 | 方式三(付费) | 这个自己去官网看吧 |
|
||||
|
||||
**相关链接:**
|
||||
- [ntfy 官方文档](https://docs.ntfy.sh/)
|
||||
- [自托管教程](https://docs.ntfy.sh/install/)
|
||||
- [GitHub 仓库](https://github.com/binwiederhier/ntfy)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
3. **配置说明:**:
|
||||
|
||||
- **推送设置**:在 [config/config.yaml](config/config.yaml) 中配置推送模式和通知选项
|
||||
@ -1020,10 +1150,11 @@ docker exec -it trend-radar ls -la /app/config/
|
||||
|
||||
### 项目相关
|
||||
|
||||
> **3 篇文章**:
|
||||
> **4 篇文章**:
|
||||
|
||||
- [可在该文章下方留言,方便项目作者用手机答疑](https://mp.weixin.qq.com/s/KYEPfTPVzZNWFclZh4am_g)
|
||||
- [2个月破 1000 star,我的GitHub项目推广实战经验](https://mp.weixin.qq.com/s/jzn0vLiQFX408opcfpPPxQ)
|
||||
- [github fork 运行本项目的注意事项 ](https://mp.weixin.qq.com/s/C8evK-U7onG1sTTdwdW2zg)
|
||||
- [基于本项目,如何开展公众号或者新闻资讯类文章写作](https://mp.weixin.qq.com/s/8ghyfDAtQZjLrnWTQabYOQ)
|
||||
|
||||
>**AI 开发**:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user