diff --git a/config/config.yaml b/config/config.yaml index aa4dbc9..83f62aa 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -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 # 不然轻则手机上收到奇怪的广告推送,重则存在一定的安全隐患 diff --git a/main.py b/main.py index 30f00bc..6e5c434 100644 --- a/main.py +++ b/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(
新闻总数 """ - + html += f"{total_titles} 条" - + # 计算筛选后的热点新闻数量 hot_news_count = sum(len(stat["titles"]) for stat in report_data["stats"]) - + html += """
热点新闻 """ - + html += f"{hot_news_count} 条" html += """ @@ -1799,7 +1895,7 @@ def render_html_content( """ now = get_beijing_time() - html += now.strftime('%m-%d %H:%M') + html += now.strftime("%m-%d %H:%M") html += """
@@ -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"""
@@ -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"""
{j}
{html_escape(title_data["source_name"])}""" - + # 处理排名显示 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'{rank_text}' - + # 处理时间显示 time_display = title_data.get("time_display", "") if time_display: # 简化时间显示格式,将波浪线替换为~ - simplified_time = time_display.replace(" ~ ", "~").replace("[", "").replace("]", "") - html += f'{html_escape(simplified_time)}' - + simplified_time = ( + time_display.replace(" ~ ", "~") + .replace("[", "") + .replace("]", "") + ) + html += ( + f'{html_escape(simplified_time)}' + ) + # 处理出现次数 count_info = title_data.get("count", 1) if count_info > 1: html += f'{count_info}次' - + html += """
""" - + # 处理标题和链接 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'{escaped_title}' else: html += escaped_title - + html += """
@@ -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"""
{escaped_source} · {titles_count}条
""" @@ -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(
{rank_text}
""" - + # 处理新增新闻的链接 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'{escaped_title}' else: html += escaped_title - + html += """
@@ -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 diff --git a/readme.md b/readme.md index 7c34f1a..5f9ad9c 100644 --- a/readme.md +++ b/readme.md @@ -7,7 +7,7 @@ [![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.0.4-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar) +[![Version](https://img.shields.io/badge/version-v2.1.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/) @@ -25,7 +25,7 @@ > 遇到问题提 issues,或【硅基茶水间】公众号留言
-👉 点击查看致谢名单 (当前 12 个) +👉 点击查看致谢名单 (当前 13 个) ### 数据支持 @@ -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 | 支持一下 |
-👉 "手机推送通知系列" 挖坑 +👉 "手机推送通知系列" 挖坑(暂时鸽)
截图中只支持一个渠道,大家有什么好的建议和想法可以公众号留言,完善好后开源 +这个暂时没有人来和我讨论,我先鸽为敬嘿嘿 @@ -89,12 +91,22 @@ ### **智能推送策略** -三种推送模式: +**三种推送模式**: - **📈 投资者/交易员** → 选择 `incremental`,及时获取新增资讯 - **📰 自媒体人/内容创作者** → 选择 `current`,掌握实时热点趋势 - **📋 企业管理者/普通用户** → 选择 `daily`,定时获取完整日报 + +**静默推送模式**: + +支持时间窗口控制,避免非工作时间的消息打扰: + +- **时间范围控制**:设定推送时间窗口(如 9:00-18:00),仅在指定时间内推送 +- **适用场景**: + - 时间内每次执行都推送 + - 时间范围内只推送一次 + ### **精准内容筛选** 设置个人关键词(如:AI、比亚迪、教育政策),只推送相关热点,过滤无关信息 @@ -179,8 +191,34 @@ GitHub 一键 Fork 即可使用,无需编程基础。
>**升级说明:** +- **注意**:请通过以下方式更新项目,不要通过 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 + +
+👉 历史更新 ### 2025/08/27 - v2.0.4 @@ -189,9 +227,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。 - 如果你以 fork 的方式将本项目部署在 GitHub 上,请将 webhooks 填入 GitHub Secret,而非 config.yaml - 如果你已经暴露了 webhooks 或将其填入了 config.yaml,建议删除后重新生成 -
-👉 历史更新 - ### 2025/08/06 - v2.0.3 - 优化 github page 的网页版效果,方便移动端使用 @@ -612,7 +647,8 @@ frequency_words.txt 文件增加了一个【必须词】功能,使用 + 号 - 运行结果将自动保存在仓库的`output`目录中 - 同时通过配置的机器人发送通知到你的群组 - +
+👉 自定义监控平台 ### 🔧 自定义监控平台 @@ -630,6 +666,7 @@ platforms: name: "华尔街见闻" # 添加更多平台... ``` +
👉 Docker 部署 diff --git a/version b/version index 26e3379..50aea0e 100644 --- a/version +++ b/version @@ -1 +1 @@ -2.0.4 \ No newline at end of file +2.1.0 \ No newline at end of file