mirror of
https://gitee.com/houhuan/TrendRadar.git
synced 2025-12-21 14:27:15 +08:00
v2.1.0
This commit is contained in:
parent
45b6ee3b15
commit
6fe73d5748
@ -33,6 +33,16 @@ notification:
|
||||
batch_send_interval: 1 # 批次发送间隔(秒)
|
||||
feishu_message_separator: "━━━━━━━━━━━━━━━━━━━" # feishu 消息分割线
|
||||
|
||||
silent_push:
|
||||
enabled: false # 是否启用静默推送模式,如果 true,则启用
|
||||
# 因为我们白嫖的 github 服务器执行时间不稳定,所以时间范围要根据实际尽可能大一点,留足 2 小时
|
||||
# 如果你想寻求稳定的按时的推送,建议部署在个人的服务器上
|
||||
time_range:
|
||||
start: "20:00" # 推送时间范围开始(北京时间)
|
||||
end: "22:00" # 推送时间范围结束(北京时间)
|
||||
once_per_day: true # 每天在时间范围内只推送一次,如果 false,则时间范围内每次执行都推送一次
|
||||
push_record_retention_days: 7 # 推送记录保留天数
|
||||
|
||||
# 请务必妥善保管好 webhooks,不要公开
|
||||
# 如果你以 fork 的方式将本项目部署在 GitHub 上,请勿在此填写任何 webhooks,而是将 webhooks 填入 GitHub Secret
|
||||
# 不然轻则手机上收到奇怪的广告推送,重则存在一定的安全隐患
|
||||
|
||||
182
main.py
182
main.py
@ -15,7 +15,7 @@ import requests
|
||||
import yaml
|
||||
|
||||
|
||||
VERSION = "2.0.4"
|
||||
VERSION = "2.1.0"
|
||||
|
||||
|
||||
# === 配置管理 ===
|
||||
@ -47,6 +47,27 @@ def load_config():
|
||||
"FEISHU_MESSAGE_SEPARATOR": config_data["notification"][
|
||||
"feishu_message_separator"
|
||||
],
|
||||
"SILENT_PUSH": {
|
||||
"ENABLED": config_data["notification"]
|
||||
.get("silent_push", {})
|
||||
.get("enabled", False),
|
||||
"TIME_RANGE": {
|
||||
"START": config_data["notification"]
|
||||
.get("silent_push", {})
|
||||
.get("time_range", {})
|
||||
.get("start", "08:00"),
|
||||
"END": config_data["notification"]
|
||||
.get("silent_push", {})
|
||||
.get("time_range", {})
|
||||
.get("end", "22:00"),
|
||||
},
|
||||
"ONCE_PER_DAY": config_data["notification"]
|
||||
.get("silent_push", {})
|
||||
.get("once_per_day", True),
|
||||
"RECORD_RETENTION_DAYS": config_data["notification"]
|
||||
.get("silent_push", {})
|
||||
.get("push_record_retention_days", 7),
|
||||
},
|
||||
"WEIGHT_CONFIG": {
|
||||
"RANK_WEIGHT": config_data["weight"]["rank_weight"],
|
||||
"FREQUENCY_WEIGHT": config_data["weight"]["frequency_weight"],
|
||||
@ -216,6 +237,81 @@ def html_escape(text: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
# === 推送记录管理 ===
|
||||
class PushRecordManager:
|
||||
"""推送记录管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.record_dir = Path("output") / ".push_records"
|
||||
self.ensure_record_dir()
|
||||
self.cleanup_old_records()
|
||||
|
||||
def ensure_record_dir(self):
|
||||
"""确保记录目录存在"""
|
||||
self.record_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def get_today_record_file(self) -> Path:
|
||||
"""获取今天的记录文件路径"""
|
||||
today = get_beijing_time().strftime("%Y%m%d")
|
||||
return self.record_dir / f"push_record_{today}.json"
|
||||
|
||||
def cleanup_old_records(self):
|
||||
"""清理过期的推送记录"""
|
||||
retention_days = CONFIG["SILENT_PUSH"]["RECORD_RETENTION_DAYS"]
|
||||
current_time = get_beijing_time()
|
||||
|
||||
for record_file in self.record_dir.glob("push_record_*.json"):
|
||||
try:
|
||||
date_str = record_file.stem.replace("push_record_", "")
|
||||
file_date = datetime.strptime(date_str, "%Y%m%d")
|
||||
file_date = pytz.timezone("Asia/Shanghai").localize(file_date)
|
||||
|
||||
if (current_time - file_date).days > retention_days:
|
||||
record_file.unlink()
|
||||
print(f"清理过期推送记录: {record_file.name}")
|
||||
except Exception as e:
|
||||
print(f"清理记录文件失败 {record_file}: {e}")
|
||||
|
||||
def has_pushed_today(self) -> bool:
|
||||
"""检查今天是否已经推送过"""
|
||||
record_file = self.get_today_record_file()
|
||||
|
||||
if not record_file.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(record_file, "r", encoding="utf-8") as f:
|
||||
record = json.load(f)
|
||||
return record.get("pushed", False)
|
||||
except Exception as e:
|
||||
print(f"读取推送记录失败: {e}")
|
||||
return False
|
||||
|
||||
def record_push(self, report_type: str):
|
||||
"""记录推送"""
|
||||
record_file = self.get_today_record_file()
|
||||
now = get_beijing_time()
|
||||
|
||||
record = {
|
||||
"pushed": True,
|
||||
"push_time": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"report_type": report_type,
|
||||
}
|
||||
|
||||
try:
|
||||
with open(record_file, "w", encoding="utf-8") as f:
|
||||
json.dump(record, f, ensure_ascii=False, indent=2)
|
||||
print(f"推送记录已保存: {report_type} at {now.strftime('%H:%M:%S')}")
|
||||
except Exception as e:
|
||||
print(f"保存推送记录失败: {e}")
|
||||
|
||||
def is_in_time_range(self, start_time: str, end_time: str) -> bool:
|
||||
"""检查当前时间是否在指定时间范围内"""
|
||||
now = get_beijing_time()
|
||||
current_time = now.strftime("%H:%M")
|
||||
return start_time <= current_time <= end_time
|
||||
|
||||
|
||||
# === 数据获取 ===
|
||||
class DataFetcher:
|
||||
"""数据获取器"""
|
||||
@ -1778,18 +1874,18 @@ def render_html_content(
|
||||
<div class="info-item">
|
||||
<span class="info-label">新闻总数</span>
|
||||
<span class="info-value">"""
|
||||
|
||||
|
||||
html += f"{total_titles} 条"
|
||||
|
||||
|
||||
# 计算筛选后的热点新闻数量
|
||||
hot_news_count = sum(len(stat["titles"]) for stat in report_data["stats"])
|
||||
|
||||
|
||||
html += """</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">热点新闻</span>
|
||||
<span class="info-value">"""
|
||||
|
||||
|
||||
html += f"{hot_news_count} 条"
|
||||
|
||||
html += """</span>
|
||||
@ -1799,7 +1895,7 @@ def render_html_content(
|
||||
<span class="info-value">"""
|
||||
|
||||
now = get_beijing_time()
|
||||
html += now.strftime('%m-%d %H:%M')
|
||||
html += now.strftime("%m-%d %H:%M")
|
||||
|
||||
html += """</span>
|
||||
</div>
|
||||
@ -1823,10 +1919,10 @@ def render_html_content(
|
||||
# 处理主要统计数据
|
||||
if report_data["stats"]:
|
||||
total_count = len(report_data["stats"])
|
||||
|
||||
|
||||
for i, stat in enumerate(report_data["stats"], 1):
|
||||
count = stat["count"]
|
||||
|
||||
|
||||
# 确定热度等级
|
||||
if count >= 10:
|
||||
count_class = "hot"
|
||||
@ -1836,7 +1932,7 @@ def render_html_content(
|
||||
count_class = ""
|
||||
|
||||
escaped_word = html_escape(stat["word"])
|
||||
|
||||
|
||||
html += f"""
|
||||
<div class="word-group">
|
||||
<div class="word-header">
|
||||
@ -1851,62 +1947,68 @@ def render_html_content(
|
||||
for j, title_data in enumerate(stat["titles"], 1):
|
||||
is_new = title_data.get("is_new", False)
|
||||
new_class = "new" if is_new else ""
|
||||
|
||||
|
||||
html += f"""
|
||||
<div class="news-item {new_class}">
|
||||
<div class="news-number">{j}</div>
|
||||
<div class="news-content">
|
||||
<div class="news-header">
|
||||
<span class="source-name">{html_escape(title_data["source_name"])}</span>"""
|
||||
|
||||
|
||||
# 处理排名显示
|
||||
ranks = title_data.get("ranks", [])
|
||||
if ranks:
|
||||
min_rank = min(ranks)
|
||||
max_rank = max(ranks)
|
||||
rank_threshold = title_data.get("rank_threshold", 10)
|
||||
|
||||
|
||||
# 确定排名等级
|
||||
if min_rank <= 3:
|
||||
rank_class = "top"
|
||||
elif min_rank <= rank_threshold:
|
||||
rank_class = "high"
|
||||
rank_class = "high"
|
||||
else:
|
||||
rank_class = ""
|
||||
|
||||
|
||||
if min_rank == max_rank:
|
||||
rank_text = str(min_rank)
|
||||
else:
|
||||
rank_text = f"{min_rank}-{max_rank}"
|
||||
|
||||
|
||||
html += f'<span class="rank-num {rank_class}">{rank_text}</span>'
|
||||
|
||||
|
||||
# 处理时间显示
|
||||
time_display = title_data.get("time_display", "")
|
||||
if time_display:
|
||||
# 简化时间显示格式,将波浪线替换为~
|
||||
simplified_time = time_display.replace(" ~ ", "~").replace("[", "").replace("]", "")
|
||||
html += f'<span class="time-info">{html_escape(simplified_time)}</span>'
|
||||
|
||||
simplified_time = (
|
||||
time_display.replace(" ~ ", "~")
|
||||
.replace("[", "")
|
||||
.replace("]", "")
|
||||
)
|
||||
html += (
|
||||
f'<span class="time-info">{html_escape(simplified_time)}</span>'
|
||||
)
|
||||
|
||||
# 处理出现次数
|
||||
count_info = title_data.get("count", 1)
|
||||
if count_info > 1:
|
||||
html += f'<span class="count-info">{count_info}次</span>'
|
||||
|
||||
|
||||
html += """
|
||||
</div>
|
||||
<div class="news-title">"""
|
||||
|
||||
|
||||
# 处理标题和链接
|
||||
escaped_title = html_escape(title_data["title"])
|
||||
link_url = title_data.get("mobile_url") or title_data.get("url", "")
|
||||
|
||||
|
||||
if link_url:
|
||||
escaped_url = html_escape(link_url)
|
||||
html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
|
||||
else:
|
||||
html += escaped_title
|
||||
|
||||
|
||||
html += """
|
||||
</div>
|
||||
</div>
|
||||
@ -1924,7 +2026,7 @@ def render_html_content(
|
||||
for source_data in report_data["new_titles"]:
|
||||
escaped_source = html_escape(source_data["source_name"])
|
||||
titles_count = len(source_data["titles"])
|
||||
|
||||
|
||||
html += f"""
|
||||
<div class="new-source-group">
|
||||
<div class="new-source-title">{escaped_source} · {titles_count}条</div>"""
|
||||
@ -1932,7 +2034,7 @@ def render_html_content(
|
||||
# 为新增新闻也添加序号
|
||||
for idx, title_data in enumerate(source_data["titles"], 1):
|
||||
ranks = title_data.get("ranks", [])
|
||||
|
||||
|
||||
# 处理新增新闻的排名显示
|
||||
rank_class = ""
|
||||
if ranks:
|
||||
@ -1941,7 +2043,7 @@ def render_html_content(
|
||||
rank_class = "top"
|
||||
elif min_rank <= title_data.get("rank_threshold", 10):
|
||||
rank_class = "high"
|
||||
|
||||
|
||||
if len(ranks) == 1:
|
||||
rank_text = str(ranks[0])
|
||||
else:
|
||||
@ -1955,17 +2057,17 @@ def render_html_content(
|
||||
<div class="new-item-rank {rank_class}">{rank_text}</div>
|
||||
<div class="new-item-content">
|
||||
<div class="new-item-title">"""
|
||||
|
||||
|
||||
# 处理新增新闻的链接
|
||||
escaped_title = html_escape(title_data["title"])
|
||||
link_url = title_data.get("mobile_url") or title_data.get("url", "")
|
||||
|
||||
|
||||
if link_url:
|
||||
escaped_url = html_escape(link_url)
|
||||
html += f'<a href="{escaped_url}" target="_blank" class="news-link">{escaped_title}</a>'
|
||||
else:
|
||||
html += escaped_title
|
||||
|
||||
|
||||
html += """
|
||||
</div>
|
||||
</div>
|
||||
@ -2511,6 +2613,23 @@ def send_to_webhooks(
|
||||
"""发送数据到多个webhook平台"""
|
||||
results = {}
|
||||
|
||||
if CONFIG["SILENT_PUSH"]["ENABLED"]:
|
||||
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} 内,跳过推送")
|
||||
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"]
|
||||
@ -2554,6 +2673,11 @@ def send_to_webhooks(
|
||||
if not results:
|
||||
print("未配置任何webhook URL,跳过通知发送")
|
||||
|
||||
# 如果成功发送了任何通知,且启用了每天只推一次,则记录推送
|
||||
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
|
||||
|
||||
|
||||
|
||||
53
readme.md
53
readme.md
@ -7,7 +7,7 @@
|
||||
[](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/)
|
||||
@ -25,7 +25,7 @@
|
||||
> 遇到问题提 issues,或【硅基茶水间】公众号留言
|
||||
|
||||
<details>
|
||||
<summary>👉 点击查看<strong>致谢名单 (当前 12 个)</strong></summary>
|
||||
<summary>👉 点击查看致谢名单 (当前 <strong>13</strong> 个)</summary>
|
||||
|
||||
### 数据支持
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
|
||||
| 点赞人 | 金额 | 日期 | 备注 |
|
||||
| :-------------------------: | :----: | :----: | :-----------------------: |
|
||||
| *下 | 1 | 2025.8.30 | |
|
||||
| 2*D | 88 | 2025.8.13 下午 | |
|
||||
| 2*D | 1 | 2025.8.13 上午 | |
|
||||
| S*o | 1 | 2025.8.05 | 支持一下 |
|
||||
@ -56,10 +57,11 @@
|
||||
| **龙 | 10 | 2025.7.29 | 支持一下 |
|
||||
|
||||
<details>
|
||||
<summary><strong>👉 "手机推送通知系列" 挖坑</strong></summary>
|
||||
<summary><strong>👉 "手机推送通知系列" 挖坑(暂时鸽)</strong></summary>
|
||||
<br>
|
||||
|
||||
截图中只支持一个渠道,大家有什么好的建议和想法可以公众号留言,完善好后开源
|
||||
这个暂时没有人来和我讨论,我先鸽为敬嘿嘿
|
||||
|
||||
<img src="_image/next.jpg" width="300" title="github"/>
|
||||
|
||||
@ -89,12 +91,22 @@
|
||||
|
||||
### **智能推送策略**
|
||||
|
||||
三种推送模式:
|
||||
**三种推送模式**:
|
||||
|
||||
- **📈 投资者/交易员** → 选择 `incremental`,及时获取新增资讯
|
||||
- **📰 自媒体人/内容创作者** → 选择 `current`,掌握实时热点趋势
|
||||
- **📋 企业管理者/普通用户** → 选择 `daily`,定时获取完整日报
|
||||
|
||||
|
||||
**静默推送模式**:
|
||||
|
||||
支持时间窗口控制,避免非工作时间的消息打扰:
|
||||
|
||||
- **时间范围控制**:设定推送时间窗口(如 9:00-18:00),仅在指定时间内推送
|
||||
- **适用场景**:
|
||||
- 时间内每次执行都推送
|
||||
- 时间范围内只推送一次
|
||||
|
||||
### **精准内容筛选**
|
||||
|
||||
设置个人关键词(如:AI、比亚迪、教育政策),只推送相关热点,过滤无关信息
|
||||
@ -179,8 +191,34 @@ GitHub 一键 Fork 即可使用,无需编程基础。
|
||||
</details>
|
||||
|
||||
>**升级说明:**
|
||||
- **注意**:请通过以下方式更新项目,不要通过 Sync fork 等方式更新
|
||||
- **小版本更新**:直接在 GitHub 网页编辑器中,用本项目的 `main.py` 代码替换你 fork 仓库中的对应文件
|
||||
- **大版本升级**:从 v1.x 升级到 v2.0 建议删除现有 fork 后重新 fork,这样更省力且避免配置冲突
|
||||
- **或者**:根据更新日志的特别说明升级
|
||||
|
||||
### 2025/08/30 - v2.1.0
|
||||
|
||||
> 感谢各位朋友的支持与厚爱,特别感谢:
|
||||
>
|
||||
> **fork 并为项目点 star** 的观众们,你们的认可是我前进的动力
|
||||
>
|
||||
> **关注公众号并积极互动** 的读者们,你们的留言和点赞让内容更有温度
|
||||
>
|
||||
> **给予资金点赞支持** 的朋友们,你们的慷慨让项目得以持续发展
|
||||
>
|
||||
> 下一次**新功能**,大概会是 ai 分析功能(大概(●'◡'●)
|
||||
|
||||
**核心改进**:
|
||||
- **推送逻辑优化**:从"每次执行都推送"改为"时间窗口内可控推送"
|
||||
- **时间窗口控制**:可设定推送时间范围,避免非工作时间打扰
|
||||
- **推送频率可选**:时间段内支持单次推送或多次推送
|
||||
|
||||
**更新提示**:
|
||||
- 本功能默认关闭,需手动开启
|
||||
- 同时更新 main.py 和 config.yaml
|
||||
|
||||
<details>
|
||||
<summary><strong>👉 历史更新</strong></summary>
|
||||
|
||||
### 2025/08/27 - v2.0.4
|
||||
|
||||
@ -189,9 +227,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。
|
||||
- 如果你以 fork 的方式将本项目部署在 GitHub 上,请将 webhooks 填入 GitHub Secret,而非 config.yaml
|
||||
- 如果你已经暴露了 webhooks 或将其填入了 config.yaml,建议删除后重新生成
|
||||
|
||||
<details>
|
||||
<summary><strong>👉 历史更新</strong></summary>
|
||||
|
||||
### 2025/08/06 - v2.0.3
|
||||
|
||||
- 优化 github page 的网页版效果,方便移动端使用
|
||||
@ -612,7 +647,8 @@ frequency_words.txt 文件增加了一个【必须词】功能,使用 + 号
|
||||
- 运行结果将自动保存在仓库的`output`目录中
|
||||
- 同时通过配置的机器人发送通知到你的群组
|
||||
|
||||
|
||||
<details>
|
||||
<summary><strong>👉 自定义监控平台</strong></summary>
|
||||
|
||||
### 🔧 自定义监控平台
|
||||
|
||||
@ -630,6 +666,7 @@ platforms:
|
||||
name: "华尔街见闻"
|
||||
# 添加更多平台...
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>👉 Docker 部署</strong></summary>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user