sansan 8 månader sedan
förälder
incheckning
6fe73d5748
4 ändrade filer med 209 tillägg och 38 borttagningar
  1. 10 0
      config/config.yaml
  2. 153 29
      main.py
  3. 45 8
      readme.md
  4. 1 1
      version

+ 10 - 0
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
   # 不然轻则手机上收到奇怪的广告推送,重则存在一定的安全隐患

+ 153 - 29
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
 
 

+ 45 - 8
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,或【硅基茶水间】公众号留言
 
 <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>

+ 1 - 1
version

@@ -1 +1 @@
-2.0.4
+2.1.0