This commit is contained in:
sansan 2025-10-02 19:56:32 +08:00
parent fc927933c7
commit 8914e15d71
6 changed files with 464 additions and 118 deletions

View File

@ -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

View File

@ -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}

View File

@ -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}

355
main.py
View File

@ -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"
@ -163,15 +133,15 @@ def load_config():
).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"]:
@ -200,6 +181,10 @@ def load_config():
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"
@ -2534,6 +2541,8 @@ 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)
@ -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"
@ -2931,7 +2975,9 @@ def send_to_notifications(
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"]:
@ -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,7 +3064,11 @@ 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)
@ -3075,7 +3141,7 @@ def send_to_dingtalk(
"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,
@ -3289,7 +3355,7 @@ def send_to_email(
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 配置
@ -3299,37 +3365,37 @@ 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"""
@ -3340,10 +3406,10 @@ 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}...")
@ -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"]),
]
)

147
readme.md
View File

@ -11,13 +11,15 @@
[![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members)
[![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
[![Version](https://img.shields.io/badge/version-v2.3.2-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
[![Version](https://img.shields.io/badge/version-v2.4.0-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar)
[![企业微信通知](https://img.shields.io/badge/企业微信-通知-00D4AA?style=flat-square)](https://work.weixin.qq.com/)
[![Telegram通知](https://img.shields.io/badge/Telegram-通知-00D4AA?style=flat-square)](https://telegram.org/)
[![dingtalk通知](https://img.shields.io/badge/钉钉-通知-00D4AA?style=flat-square)](#)
[![飞书通知](https://img.shields.io/badge/飞书-通知-00D4AA?style=flat-square)](https://www.feishu.cn/)
[![邮件通知](https://img.shields.io/badge/Email-通知-00D4AA?style=flat-square)](mailto:)
[![邮件通知](https://img.shields.io/badge/Email-通知-00D4AA?style=flat-square)](#)
[![ntfy通知](https://img.shields.io/badge/ntfy-通知-00D4AA?style=flat-square)](https://github.com/binwiederhier/ntfy)
[![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-自动化-2088FF?style=flat-square&logo=github-actions&logoColor=white)](https://github.com/sansan0/TrendRadar)
[![GitHub Pages](https://img.shields.io/badge/GitHub_Pages-部署-4285F4?style=flat-square&logo=github&logoColor=white)](https://sansan0.github.io/TrendRadar)
[![Docker](https://img.shields.io/badge/Docker-部署-2496ED?style=flat-square&logo=docker&logoColor=white)](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 开发**

View File

@ -1 +1 @@
2.3.2
2.4.0