This commit is contained in:
sansan 2025-09-22 21:05:20 +08:00
parent b73f16d7cd
commit 89ab87cad2
8 changed files with 371 additions and 30 deletions

View File

@ -53,6 +53,11 @@ jobs:
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
DINGTALK_WEBHOOK_URL: ${{ secrets.DINGTALK_WEBHOOK_URL }} DINGTALK_WEBHOOK_URL: ${{ secrets.DINGTALK_WEBHOOK_URL }}
WEWORK_WEBHOOK_URL: ${{ secrets.WEWORK_WEBHOOK_URL }} WEWORK_WEBHOOK_URL: ${{ secrets.WEWORK_WEBHOOK_URL }}
EMAIL_FROM: ${{ secrets.EMAIL_FROM }}
EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }}
EMAIL_TO: ${{ secrets.EMAIL_TO }}
EMAIL_SMTP_SERVER: ${{ secrets.EMAIL_SMTP_SERVER }}
EMAIL_SMTP_PORT: ${{ secrets.EMAIL_SMTP_PORT }}
GITHUB_ACTIONS: true GITHUB_ACTIONS: true
run: python main.py run: python main.py

View File

@ -53,6 +53,11 @@ notification:
wework_url: "" # 企业微信机器人的 webhook URL wework_url: "" # 企业微信机器人的 webhook URL
telegram_bot_token: "" # Telegram Bot Token telegram_bot_token: "" # Telegram Bot Token
telegram_chat_id: "" # Telegram Chat ID telegram_chat_id: "" # Telegram Chat ID
email_from: "" # 发件人邮箱地址
email_password: "" # 发件人邮箱密码或授权码
email_to: "" # 收件人邮箱地址,多个收件人用逗号分隔
email_smtp_server: "" # SMTP服务器地址可选留空自动识别
email_smtp_port: "" # SMTP端口可选留空自动识别
# 用于让关注度更高的新闻在更前面显示,即用算法重新组合不同平台的热搜排序形成你侧重的热搜,合起来是 1 就行 # 用于让关注度更高的新闻在更前面显示,即用算法重新组合不同平台的热搜排序形成你侧重的热搜,合起来是 1 就行
weight: weight:

View File

@ -4,6 +4,11 @@ TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID= TELEGRAM_CHAT_ID=
DINGTALK_WEBHOOK_URL= DINGTALK_WEBHOOK_URL=
WEWORK_WEBHOOK_URL= WEWORK_WEBHOOK_URL=
EMAIL_FROM=
EMAIL_PASSWORD=
EMAIL_TO=
EMAIL_SMTP_SERVER=
EMAIL_SMTP_PORT=
# 运行配置 # 运行配置
CRON_SCHEDULE=*/30 * * * * # 定时任务表达式,每 30 分钟执行一次(比如 8点8点半9点9点半这种时间规律执行) CRON_SCHEDULE=*/30 * * * * # 定时任务表达式,每 30 分钟执行一次(比如 8点8点半9点9点半这种时间规律执行)

View File

@ -17,6 +17,11 @@ services:
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
- DINGTALK_WEBHOOK_URL=${DINGTALK_WEBHOOK_URL:-} - DINGTALK_WEBHOOK_URL=${DINGTALK_WEBHOOK_URL:-}
- WEWORK_WEBHOOK_URL=${WEWORK_WEBHOOK_URL:-} - WEWORK_WEBHOOK_URL=${WEWORK_WEBHOOK_URL:-}
- EMAIL_FROM=${EMAIL_FROM:-}
- EMAIL_PASSWORD=${EMAIL_PASSWORD:-}
- EMAIL_TO=${EMAIL_TO:-}
- EMAIL_SMTP_SERVER=${EMAIL_SMTP_SERVER:-}
- EMAIL_SMTP_PORT=${EMAIL_SMTP_PORT:-}
- CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *} - CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *}
- RUN_MODE=${RUN_MODE:-cron} - RUN_MODE=${RUN_MODE:-cron}
- IMMEDIATE_RUN=${IMMEDIATE_RUN:-true} - IMMEDIATE_RUN=${IMMEDIATE_RUN:-true}

View File

@ -15,6 +15,11 @@ services:
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
- DINGTALK_WEBHOOK_URL=${DINGTALK_WEBHOOK_URL:-} - DINGTALK_WEBHOOK_URL=${DINGTALK_WEBHOOK_URL:-}
- WEWORK_WEBHOOK_URL=${WEWORK_WEBHOOK_URL:-} - WEWORK_WEBHOOK_URL=${WEWORK_WEBHOOK_URL:-}
- EMAIL_FROM=${EMAIL_FROM:-}
- EMAIL_PASSWORD=${EMAIL_PASSWORD:-}
- EMAIL_TO=${EMAIL_TO:-}
- EMAIL_SMTP_SERVER=${EMAIL_SMTP_SERVER:-}
- EMAIL_SMTP_PORT=${EMAIL_SMTP_PORT:-}
- CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *} - CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *}
- RUN_MODE=${RUN_MODE:-cron} - RUN_MODE=${RUN_MODE:-cron}
- IMMEDIATE_RUN=${IMMEDIATE_RUN:-true} - IMMEDIATE_RUN=${IMMEDIATE_RUN:-true}

260
main.py
View File

@ -6,6 +6,11 @@ import random
import re import re
import time import time
import webbrowser import webbrowser
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import formataddr, formatdate, make_msgid
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Tuple, Optional, Union from typing import Dict, List, Tuple, Optional, Union
@ -15,9 +20,69 @@ import requests
import yaml import yaml
VERSION = "2.2.0" VERSION = "2.3.0"
# === SMTP邮件配置 ===
SMTP_CONFIGS = {
# Gmail
'gmail.com': {
'server': 'smtp.gmail.com',
'port': 587,
'encryption': 'TLS'
},
# QQ邮箱
'qq.com': {
'server': 'smtp.qq.com',
'port': 587,
'encryption': 'TLS'
},
# Outlook
'outlook.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'
},
# 网易邮箱
'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'
},
# 搜狐邮箱
'sohu.com': {
'server': 'smtp.sohu.com',
'port': 587,
'encryption': 'TLS'
}
}
# === 配置管理 === # === 配置管理 ===
def load_config(): def load_config():
"""加载配置文件""" """加载配置文件"""
@ -97,6 +162,23 @@ def load_config():
"TELEGRAM_CHAT_ID", "" "TELEGRAM_CHAT_ID", ""
).strip() or webhooks.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_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_SMTP_SERVER"] = os.environ.get(
"EMAIL_SMTP_SERVER", ""
).strip() or webhooks.get("email_smtp_server", "")
config["EMAIL_SMTP_PORT"] = os.environ.get(
"EMAIL_SMTP_PORT", ""
).strip() or webhooks.get("email_smtp_port", "")
# 输出配置来源信息 # 输出配置来源信息
webhook_sources = [] webhook_sources = []
if config["FEISHU_WEBHOOK_URL"]: if config["FEISHU_WEBHOOK_URL"]:
@ -114,6 +196,9 @@ def load_config():
) )
chat_source = "环境变量" if os.environ.get("TELEGRAM_CHAT_ID") else "配置文件" chat_source = "环境变量" if os.environ.get("TELEGRAM_CHAT_ID") else "配置文件"
webhook_sources.append(f"Telegram({token_source}/{chat_source})") webhook_sources.append(f"Telegram({token_source}/{chat_source})")
if config["EMAIL_FROM"] and config["EMAIL_PASSWORD"] and config["EMAIL_TO"]:
from_source = "环境变量" if os.environ.get("EMAIL_FROM") else "配置文件"
webhook_sources.append(f"邮件({from_source})")
if webhook_sources: if webhook_sources:
print(f"Webhook 配置来源: {', '.join(webhook_sources)}") print(f"Webhook 配置来源: {', '.join(webhook_sources)}")
@ -1464,6 +1549,7 @@ def generate_html_report(
id_to_name: Optional[Dict] = None, id_to_name: Optional[Dict] = None,
mode: str = "daily", mode: str = "daily",
is_daily_summary: bool = False, is_daily_summary: bool = False,
update_info: Optional[Dict] = None,
) -> str: ) -> str:
"""生成HTML报告""" """生成HTML报告"""
if is_daily_summary: if is_daily_summary:
@ -1481,7 +1567,7 @@ def generate_html_report(
report_data = prepare_report_data(stats, failed_ids, new_titles, id_to_name, mode) report_data = prepare_report_data(stats, failed_ids, new_titles, id_to_name, mode)
html_content = render_html_content( html_content = render_html_content(
report_data, total_titles, is_daily_summary, mode report_data, total_titles, is_daily_summary, mode, update_info
) )
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
@ -1500,6 +1586,7 @@ def render_html_content(
total_titles: int, total_titles: int,
is_daily_summary: bool = False, is_daily_summary: bool = False,
mode: str = "daily", mode: str = "daily",
update_info: Optional[Dict] = None,
) -> str: ) -> str:
"""渲染HTML内容""" """渲染HTML内容"""
html = """ html = """
@ -1876,7 +1963,7 @@ def render_html_content(
.footer-content { .footer-content {
font-size: 13px; font-size: 13px;
color: #6b7280; color: #6b7280;
line-height: 1.4; line-height: 1.6;
} }
.footer-link { .footer-link {
@ -2157,7 +2244,16 @@ def render_html_content(
<span class="project-name">TrendRadar</span> 生成 · <span class="project-name">TrendRadar</span> 生成 ·
<a href="https://github.com/sansan0/TrendRadar" target="_blank" class="footer-link"> <a href="https://github.com/sansan0/TrendRadar" target="_blank" class="footer-link">
GitHub 开源项目 GitHub 开源项目
</a> </a>"""
if update_info:
html += f"""
<br>
<span style="color: #ea580c; font-weight: 500;">
发现新版本 {update_info['remote_version']}当前版本 {update_info['current_version']}
</span>"""
html += """
</div> </div>
</div> </div>
</div> </div>
@ -2823,6 +2919,7 @@ def send_to_webhooks(
update_info: Optional[Dict] = None, update_info: Optional[Dict] = None,
proxy_url: Optional[str] = None, proxy_url: Optional[str] = None,
mode: str = "daily", mode: str = "daily",
html_file_path: Optional[str] = None,
) -> Dict[str, bool]: ) -> Dict[str, bool]:
"""发送数据到多个webhook平台""" """发送数据到多个webhook平台"""
results = {} results = {}
@ -2851,6 +2948,11 @@ def send_to_webhooks(
wework_url = CONFIG["WEWORK_WEBHOOK_URL"] wework_url = CONFIG["WEWORK_WEBHOOK_URL"]
telegram_token = CONFIG["TELEGRAM_BOT_TOKEN"] telegram_token = CONFIG["TELEGRAM_BOT_TOKEN"]
telegram_chat_id = CONFIG["TELEGRAM_CHAT_ID"] telegram_chat_id = CONFIG["TELEGRAM_CHAT_ID"]
email_from = CONFIG["EMAIL_FROM"]
email_password = CONFIG["EMAIL_PASSWORD"]
email_to = CONFIG["EMAIL_TO"]
email_smtp_server = CONFIG.get("EMAIL_SMTP_SERVER", "")
email_smtp_port = CONFIG.get("EMAIL_SMTP_PORT", "")
update_info_to_send = update_info if CONFIG["SHOW_VERSION_UPDATE"] else None update_info_to_send = update_info if CONFIG["SHOW_VERSION_UPDATE"] else None
@ -2884,6 +2986,18 @@ def send_to_webhooks(
mode, mode,
) )
# 发送邮件
if email_from and email_password and email_to:
results["email"] = send_to_email(
email_from,
email_password,
email_to,
report_type,
html_file_path,
email_smtp_server,
email_smtp_port,
)
if not results: if not results:
print("未配置任何webhook URL跳过通知发送") print("未配置任何webhook URL跳过通知发送")
@ -3156,6 +3270,137 @@ def send_to_telegram(
print(f"Telegram所有 {len(batches)} 批次发送完成 [{report_type}]") print(f"Telegram所有 {len(batches)} 批次发送完成 [{report_type}]")
return True return True
def send_to_email(
from_email: str,
password: str,
to_email: str,
report_type: str,
html_file_path: str,
custom_smtp_server: Optional[str] = None,
custom_smtp_port: Optional[int] = None,
) -> bool:
"""发送邮件通知"""
try:
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()
if custom_smtp_server and custom_smtp_port:
# 使用自定义 SMTP 配置
smtp_server = custom_smtp_server
smtp_port = int(custom_smtp_port)
use_tls = smtp_port == 587
elif domain in SMTP_CONFIGS:
# 使用预设配置
config = SMTP_CONFIGS[domain]
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')
# 严格按照 RFC 标准设置 From header
sender_name = "TrendRadar"
msg['From'] = formataddr((sender_name, from_email))
# 设置收件人
recipients = [addr.strip() for addr in to_email.split(',')]
if len(recipients) == 1:
msg['To'] = recipients[0]
else:
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')
# 设置其他标准 header
msg['MIME-Version'] = '1.0'
msg['Date'] = formatdate(localtime=True)
msg['Message-ID'] = make_msgid()
# 添加纯文本部分(作为备选)
text_content = f"""
TrendRadar 热点分析报告
========================
报告类型{report_type}
生成时间{now.strftime('%Y-%m-%d %H:%M:%S')}
请使用支持HTML的邮件客户端查看完整报告内容
"""
text_part = MIMEText(text_content, 'plain', 'utf-8')
msg.attach(text_part)
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 模式
server = smtplib.SMTP(smtp_server, smtp_port, timeout=30)
server.set_debuglevel(0) # 设为1可以查看详细调试信息
server.ehlo()
server.starttls()
server.ehlo()
else:
# SSL 模式
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)}")
return False
except smtplib.SMTPRecipientsRefused as e:
print(f"邮件发送失败:收件人地址被拒绝 {e}")
return False
except smtplib.SMTPSenderRefused as e:
print(f"邮件发送失败:发件人地址被拒绝 {e}")
return False
except smtplib.SMTPDataError as e:
print(f"邮件发送失败:邮件数据错误 {e}")
return False
except smtplib.SMTPConnectError as e:
print(f"邮件发送失败:无法连接到 SMTP 服务器 {smtp_server}:{smtp_port}")
print(f"详细错误: {str(e)}")
return False
except Exception as e:
print(f"邮件发送失败 [{report_type}]{e}")
import traceback
traceback.print_exc()
return False
# === 主分析器 === # === 主分析器 ===
class NewsAnalyzer: class NewsAnalyzer:
@ -3374,6 +3619,7 @@ class NewsAnalyzer:
id_to_name=id_to_name, id_to_name=id_to_name,
mode=mode, mode=mode,
is_daily_summary=is_daily_summary, is_daily_summary=is_daily_summary,
update_info=self.update_info if CONFIG["SHOW_VERSION_UPDATE"] else None,
) )
return stats, html_file return stats, html_file
@ -3386,6 +3632,7 @@ class NewsAnalyzer:
failed_ids: Optional[List] = None, failed_ids: Optional[List] = None,
new_titles: Optional[Dict] = None, new_titles: Optional[Dict] = None,
id_to_name: Optional[Dict] = None, id_to_name: Optional[Dict] = None,
html_file_path: Optional[str] = None,
) -> bool: ) -> bool:
"""统一的通知发送逻辑,包含所有判断条件""" """统一的通知发送逻辑,包含所有判断条件"""
has_webhook = self._has_webhook_configured() has_webhook = self._has_webhook_configured()
@ -3404,6 +3651,7 @@ class NewsAnalyzer:
self.update_info, self.update_info,
self.proxy_url, self.proxy_url,
mode=mode, mode=mode,
html_file_path=html_file_path,
) )
return True return True
elif CONFIG["ENABLE_NOTIFICATION"] and not has_webhook: elif CONFIG["ENABLE_NOTIFICATION"] and not has_webhook:
@ -3462,8 +3710,10 @@ class NewsAnalyzer:
stats, stats,
mode_strategy["summary_report_type"], mode_strategy["summary_report_type"],
mode_strategy["summary_mode"], mode_strategy["summary_mode"],
failed_ids=[],
new_titles=new_titles, new_titles=new_titles,
id_to_name=id_to_name, id_to_name=id_to_name,
html_file_path=html_file,
) )
return html_file return html_file
@ -3596,6 +3846,7 @@ class NewsAnalyzer:
failed_ids=failed_ids, failed_ids=failed_ids,
new_titles=historical_new_titles, new_titles=historical_new_titles,
id_to_name=combined_id_to_name, id_to_name=combined_id_to_name,
html_file_path=html_file,
) )
else: else:
print("❌ 严重错误:无法读取刚保存的数据文件") print("❌ 严重错误:无法读取刚保存的数据文件")
@ -3624,6 +3875,7 @@ class NewsAnalyzer:
failed_ids=failed_ids, failed_ids=failed_ids,
new_titles=new_titles, new_titles=new_titles,
id_to_name=id_to_name, id_to_name=id_to_name,
html_file_path=html_file,
) )
# 生成汇总报告(如果需要) # 生成汇总报告(如果需要)

110
readme.md
View File

@ -9,12 +9,13 @@
[![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers) [![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) [![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) [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE)
[![Version](https://img.shields.io/badge/version-v2.2.0-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar) [![Version](https://img.shields.io/badge/version-v2.3.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/) [![企业微信通知](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/) [![Telegram通知](https://img.shields.io/badge/Telegram-通知-00D4AA?style=flat-square)](https://telegram.org/)
[![dingtalk通知](https://img.shields.io/badge/钉钉-通知支持-00D4AA?style=flat-square)](#) [![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/飞书-通知-00D4AA?style=flat-square)](https://www.feishu.cn/)
[![邮件通知](https://img.shields.io/badge/Email-通知-00D4AA?style=flat-square)](mailto:)
[![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-自动化-2088FF?style=flat-square&logo=github-actions&logoColor=white)](https://github.com/sansan0/TrendRadar) [![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) [![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/) [![Docker](https://img.shields.io/badge/Docker-部署-2496ED?style=flat-square&logo=docker&logoColor=white)](https://hub.docker.com/)
@ -29,7 +30,7 @@
- 遇到问题可选择以上 2 种方式获得帮助,[点此跳转到两者的区别](#问题答疑与1元点赞) - 遇到问题可选择以上 2 种方式获得帮助,[点此跳转到两者的区别](#问题答疑与1元点赞)
<details> <details>
<summary>👉 点击查看<strong>致谢名单</strong> (当前 <strong>🔥22🔥</strong> 位)</summary> <summary>👉 点击查看<strong>致谢名单</strong> (当前 <strong>🔥23🔥</strong> 位)</summary>
### 数据支持 ### 数据支持
@ -49,6 +50,7 @@
| 点赞人 | 金额 | 日期 | 备注 | | 点赞人 | 金额 | 日期 | 备注 |
| :-------------------------: | :----: | :----: | :-----------------------: | | :-------------------------: | :----: | :----: | :-----------------------: |
| *🍍 | 10 | 2025.9.21 | |
| E*f | 1 | 2025.9.20 | | | E*f | 1 | 2025.9.20 | |
| *记 | 1 | 2025.9.20 | | | *记 | 1 | 2025.9.20 | |
| z*u | 2 | 2025.9.19 | | | z*u | 2 | 2025.9.19 | |
@ -186,7 +188,11 @@ weight:
### **多渠道实时推送** ### **多渠道实时推送**
支持**企业微信**(微信方案)、**飞书**、**钉钉**、**Telegram**,消息直达手机 支持**企业微信**(+ 微信推送方案)、**飞书**、**钉钉**、**Telegram**、**邮件**,消息直达手机和邮箱
- **邮件推送**:支持 QQ邮箱、Gmail、Outlook、163邮箱等主流邮箱服务
- **智能识别**:自动识别邮箱服务商,也可以手动配置 SMTP 服务器
- **HTML 格式**:精美的 HTML 邮件格式,与网页版效果一致
### **多端适配** ### **多端适配**
- **GitHub Pages**自动生成精美网页报告PC/移动端适配 - **GitHub Pages**自动生成精美网页报告PC/移动端适配
@ -212,7 +218,7 @@ GitHub 一键 Fork 即可使用,无需编程基础。
**典型场景:** 股市投资监控、品牌舆情追踪、行业动态关注、生活资讯获取 **典型场景:** 股市投资监控、品牌舆情追踪、行业动态关注、生活资讯获取
| Github Pages 网页效果(手机端适配) | 飞书推送效果 | | Github Pages 效果(手机端适配、邮箱推送效果) | 飞书推送效果 |
|:---:|:---:| |:---:|:---:|
| ![Github Pages效果](_image/github-pages.png) | ![飞书推送效果](_image/feishu.jpg) | | ![Github Pages效果](_image/github-pages.png) | ![飞书推送效果](_image/feishu.jpg) |
@ -290,6 +296,28 @@ GitHub 一键 Fork 即可使用,无需编程基础。
> >
> 下一次**新功能**,大概会是 ai 分析功能(●'◡'●) > 下一次**新功能**,大概会是 ai 分析功能(●'◡'●)
### 2025/09/22 - v2.3.0
- **新增邮件推送功能**,支持将热点新闻报告发送到邮箱
- **智能 SMTP 识别**:自动识别 Gmail、QQ邮箱、Outlook、网易邮箱等 10+ 种邮箱服务商配置
- **HTML 精美格式**:邮件内容采用与网页版相同的 HTML 格式,排版精美,移动端适配
- **批量发送支持**:支持多个收件人,用逗号分隔即可同时发送给多人
- **自定义 SMTP**:可自定义 SMTP 服务器和端口
**使用说明**
- 适用场景:适合需要邮件归档、团队分享、定时报告的用户
- 注意事项:为防止邮件群发功能被**滥用**,当前的群发是所有收件人都能看到彼此的邮箱地址,适合熟人间交流资讯
**支持的邮箱服务**
- Gmail、QQ邮箱、Outlook/Hotmail、163/126邮箱、新浪邮箱、搜狐邮箱等
**更新提示**
- 此次更新的内容比较多,建议删除原有 fork, 重新 fork 并配置
<details>
<summary><strong>👉 历史更新</strong></summary>
### 2025/09/17 - v2.2.0 ### 2025/09/17 - v2.2.0
- 新增一键保存新闻图片功能,让你轻松分享关注的热点 - 新增一键保存新闻图片功能,让你轻松分享关注的热点
@ -300,9 +328,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。
- 实际效果:系统会自动将当前的新闻报告制作成一张精美图片,保存到你的手机相册或电脑桌面 - 实际效果:系统会自动将当前的新闻报告制作成一张精美图片,保存到你的手机相册或电脑桌面
- 分享便利:你可以直接把这张图片发给朋友、发到朋友圈,或分享到工作群,让别人也能看到你发现的重要资讯 - 分享便利:你可以直接把这张图片发给朋友、发到朋友圈,或分享到工作群,让别人也能看到你发现的重要资讯
<details>
<summary><strong>👉 历史更新</strong></summary>
### 2025/09/13 - v2.1.2 ### 2025/09/13 - v2.1.2
- 解决钉钉的推送容量限制导致的新闻推送失败问题(采用分批推送) - 解决钉钉的推送容量限制导致的新闻推送失败问题(采用分批推送)
@ -581,6 +606,49 @@ frequency_words.txt 文件增加了一个【必须词】功能,使用 + 号
- `TELEGRAM_CHAT_ID`:填入第 2 步获得的 Chat ID - `TELEGRAM_CHAT_ID`:填入第 2 步获得的 Chat ID
</details> </details>
<details>
<summary> <strong>👉 邮件推送</strong>(支持所有主流邮箱)</summary>
<br>
- 注意事项:为防止邮件群发功能被**滥用**,当前的群发是所有收件人都能看到彼此的邮箱地址,适合熟人间交流资讯。
- 仅供参考:请根据实际情况调整,邮箱方面并没有一一验证,是按照 SMTP 的标准配置的
**GitHub Secret 配置:**
- 名称:`EMAIL_FROM` - 发件人邮箱地址
- 名称:`EMAIL_PASSWORD` - 邮箱密码或授权码
- 名称:`EMAIL_TO` - 收件人邮箱地址(多个收件人用英文逗号分隔)
- 名称:`EMAIL_SMTP_SERVER` - SMTP服务器地址可选留空则自动识别
- 名称:`EMAIL_SMTP_PORT` - SMTP端口可选留空则自动识别
**常见邮箱设置:**
#### QQ邮箱
1. 登录 QQ邮箱网页版 → 设置 → 账户
2. 开启 POP3/SMTP 服务
3. 生成授权码16位字母
4. `EMAIL_PASSWORD` 填写授权码,而非 QQ 密码
#### Gmail
1. 开启两步验证
2. 生成应用专用密码
3. `EMAIL_PASSWORD` 填写应用专用密码
#### 163/126邮箱
1. 登录网页版 → 设置 → POP3/SMTP/IMAP
2. 开启 SMTP 服务
3. 设置客户端授权码
4. `EMAIL_PASSWORD` 填写授权码
**高级配置**
如果自动识别失败,可手动配置 SMTP
- `EMAIL_SMTP_SERVER`:如 smtp.gmail.com
- `EMAIL_SMTP_PORT`:如 587TLS或 465SSL
**多收件人设置**
- EMAIL_TO="user1@example.com,user2@example.com,user3@example.com"
</details>
3. **主要配置**: 3. **主要配置**:
- **推送设置:** : 在 [config/config.yaml](config/config.yaml) 中进行,可根据里面的描述文字操作,这里不重复了 - **推送设置:** : 在 [config/config.yaml](config/config.yaml) 中进行,可根据里面的描述文字操作,这里不重复了
@ -765,7 +833,7 @@ docker run -d --name trend-radar \
-e IMMEDIATE_RUN="true" \ -e IMMEDIATE_RUN="true" \
wantcat/trendradar:latest wantcat/trendradar:latest
# 或者启用手机应用推送通知 # 或者启用手机应用推送通知或邮件通知
docker run -d --name trend-radar \ docker run -d --name trend-radar \
-v ./config:/app/config:ro \ -v ./config:/app/config:ro \
-v ./output:/app/output \ -v ./output:/app/output \
@ -774,6 +842,9 @@ docker run -d --name trend-radar \
-e WEWORK_WEBHOOK_URL="你的企业微信webhook" \ -e WEWORK_WEBHOOK_URL="你的企业微信webhook" \
-e TELEGRAM_BOT_TOKEN="你的telegram_bot_token" \ -e TELEGRAM_BOT_TOKEN="你的telegram_bot_token" \
-e TELEGRAM_CHAT_ID="你的telegram_chat_id" \ -e TELEGRAM_CHAT_ID="你的telegram_chat_id" \
-e EMAIL_FROM="你的发件邮箱" \
-e EMAIL_PASSWORD="你的邮箱密码或授权码" \
-e EMAIL_TO="收件人邮箱" \
-e CRON_SCHEDULE="*/30 * * * *" \ -e CRON_SCHEDULE="*/30 * * * *" \
-e RUN_MODE="cron" \ -e RUN_MODE="cron" \
-e IMMEDIATE_RUN="true" \ -e IMMEDIATE_RUN="true" \
@ -946,21 +1017,14 @@ docker exec -it trend-radar ls -la /app/config/
> 心意到就行,收到的**点赞**用于提高开发者开源的积极性。你们的**点赞**已记录于最顶部的【致谢名单】 > 心意到就行,收到的**点赞**用于提高开发者开源的积极性。你们的**点赞**已记录于最顶部的【致谢名单】
<div align="center">
|公众号关注 |微信点赞 | 支付宝点赞 | |公众号关注 |微信点赞 | 支付宝点赞 |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| <img src="_image/weixin.png" width="300" title="硅基茶水间"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="300" title="微信支付"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2Fed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="300" title="支付宝支付"/> | | <img src="_image/weixin.png" width="300" title="硅基茶水间"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="300" title="微信支付"/> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2Fed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="300" title="支付宝支付"/> |
</div> | 答疑方式 | 适用场景 | 响应时间 | 详细程度 | 社区参与度 | 如何提问 |
|:---:|:---:|:---:|:---:|:---:|:---:|
| **GitHub Issues** | 部署配置问题<br/>功能异常 | 1-2天内 | 针对性强 | **公开讨论**<br/>其他用户可参与<br/>问题记录可搜索 | 📋 **提供完整信息**<br/>• 尽量截图<br/>• 错误日志<br/>• 系统环境等等 |
| 答疑方式 | 适用场景 | 响应时间 | 详细程度 | 如何提问 | | **公众号交流** | 快速咨询<br/>使用疑问<br/>功能了解 | 几小时 | 简要指导 | 可以文章下留言<br/>也可以私信交流 | 💡 **抓住问题核心**<br/>• 一句话描述问题<br/>• 说明想要的效果 |
|---------|---------|---------|---------|---------|
| **GitHub Issues** | 部署配置问题<br/>功能异常 | 1-2天内 | 针对性强 | 📋 **提供完整信息**<br/>• 尽量截图<br/>• 错误日志<br/>• 系统环境等等 |
| **公众号留言** | 快速咨询<br/>使用疑问<br/>功能了解 | 几小时 | 简要指导 | 💡 **抓住问题核心**<br/>• 一句话描述问题<br/>• 说明想要的效果 |
### 项目相关 ### 项目相关

View File

@ -1 +1 @@
2.2.0 2.3.0