Explorar el Código

fix(notification): 修复 RSS 翻译未应用到网页版 + Telegram 格式不匹配问题

sansan hace 1 mes
padre
commit
87e89ba6ff
Se han modificado 3 ficheros con 78 adiciones y 13 borrados
  1. 18 1
      trendradar/__main__.py
  2. 36 1
      trendradar/ai/formatter.py
  3. 24 11
      trendradar/notification/dispatcher.py

+ 18 - 1
trendradar/__main__.py

@@ -867,7 +867,22 @@ class NewsAnalyzer:
                 standalone_data=standalone_data
             )
 
-        # HTML生成(如果启用)
+        # 翻译 RSS 内容(如果启用)— 在 HTML 生成前执行,确保网页版也能展示翻译内容
+        # 注意:仅翻译 rss_items 和 rss_new_items,不翻译 standalone_data(通知前会重新生成)
+        # 热榜翻译在推送时由 dispatch_all 处理 report_data
+        trans_config = self.ctx.config.get("AI_TRANSLATION", {})
+        if trans_config.get("ENABLED", False):
+            dispatcher = self.ctx.create_notification_dispatcher()
+            display_regions = self.ctx.config.get("DISPLAY", {}).get("REGIONS", {})
+            _, rss_items, rss_new_items, _ = \
+                dispatcher.translate_content(
+                    report_data={"stats": [], "new_titles": []},
+                    rss_items=rss_items,
+                    rss_new_items=rss_new_items,
+                    display_regions=display_regions,
+                )
+
+        # HTML生成(如果启用)— 使用翻译后的数据
         html_file = None
         if self.ctx.config["STORAGE"]["FORMATS"]["HTML"]:
             html_file = self.ctx.generate_html(
@@ -960,6 +975,7 @@ class NewsAnalyzer:
             update_info_to_send = self.update_info if cfg["SHOW_VERSION_UPDATE"] else None
 
             # 使用 NotificationDispatcher 发送到所有渠道
+            # RSS/独立展示区数据已在分析流水线中翻译过,跳过重复翻译(仅翻译热榜 report_data)
             dispatcher = self.ctx.create_notification_dispatcher()
             results = dispatcher.dispatch_all(
                 report_data=report_data,
@@ -972,6 +988,7 @@ class NewsAnalyzer:
                 rss_new_items=rss_new_items,
                 ai_analysis=ai_result,
                 standalone_data=standalone_data,
+                skip_translation=True,
             )
 
             if not results:

+ 36 - 1
trendradar/ai/formatter.py

@@ -308,13 +308,48 @@ def render_ai_analysis_plain(result: AIAnalysisResult) -> str:
     return "\n".join(lines)
 
 
+def render_ai_analysis_telegram(result: AIAnalysisResult) -> str:
+    """渲染为 Telegram HTML 格式(配合 parse_mode: HTML)
+
+    Telegram Bot API 的 HTML 模式仅支持有限标签:
+    <b>, <i>, <u>, <s>, <code>, <pre>, <a href="">, <blockquote>
+    换行直接使用 \\n,不支持 <br>, <div>, <h1>-<h6> 等标签。
+    """
+    if not result.success:
+        return f"⚠️ AI 分析失败: {_escape_html(result.error)}"
+
+    lines = ["<b>✨ AI 热点分析</b>", ""]
+
+    if result.core_trends:
+        lines.extend(["<b>核心热点态势</b>", _escape_html(_format_list_content(result.core_trends)), ""])
+
+    if result.sentiment_controversy:
+        lines.extend(["<b>舆论风向争议</b>", _escape_html(_format_list_content(result.sentiment_controversy)), ""])
+
+    if result.signals:
+        lines.extend(["<b>异动与弱信号</b>", _escape_html(_format_list_content(result.signals)), ""])
+
+    if result.rss_insights:
+        lines.extend(["<b>RSS 深度洞察</b>", _escape_html(_format_list_content(result.rss_insights)), ""])
+
+    if result.outlook_strategy:
+        lines.extend(["<b>研判策略建议</b>", _escape_html(_format_list_content(result.outlook_strategy)), ""])
+
+    if result.standalone_summaries:
+        summaries_text = _format_standalone_summaries(result.standalone_summaries)
+        if summaries_text:
+            lines.extend(["<b>独立源点速览</b>", _escape_html(summaries_text)])
+
+    return "\n".join(lines)
+
+
 def get_ai_analysis_renderer(channel: str):
     """根据渠道获取对应的渲染函数"""
     renderers = {
         "feishu": render_ai_analysis_feishu,
         "dingtalk": render_ai_analysis_dingtalk,
         "wework": render_ai_analysis_markdown,
-        "telegram": render_ai_analysis_markdown,
+        "telegram": render_ai_analysis_telegram,
         "email": render_ai_analysis_html_rich,  # 邮件使用丰富样式,配合 HTML 报告的 CSS
         "ntfy": render_ai_analysis_markdown,
         "bark": render_ai_analysis_plain,

+ 24 - 11
trendradar/notification/dispatcher.py

@@ -73,13 +73,14 @@ class NotificationDispatcher:
         self.max_accounts = config.get("MAX_ACCOUNTS_PER_CHANNEL", 3)
         self.translator = translator
 
-    def _translate_content(
+    def translate_content(
         self,
         report_data: Dict,
         rss_items: Optional[List[Dict]] = None,
         rss_new_items: Optional[List[Dict]] = None,
         standalone_data: Optional[Dict] = None,
         display_regions: Optional[Dict] = None,
+        skip_rss: bool = False,
     ) -> tuple:
         """
         翻译推送内容
@@ -90,6 +91,7 @@ class NotificationDispatcher:
             rss_new_items: RSS 新增条目
             standalone_data: 独立展示区数据
             display_regions: 区域显示配置(不展示的区域跳过翻译)
+            skip_rss: 跳过 RSS 和独立展示区翻译(当数据已在上游翻译过时使用)
 
         Returns:
             tuple: (翻译后的 report_data, rss_items, rss_new_items, standalone_data)
@@ -127,14 +129,14 @@ class NotificationDispatcher:
                     title_locations.append(("new_titles", source_idx, title_idx))
 
         # 3. RSS 统计标题(结构与 stats 一致:[{word, count, titles: [{title, ...}]}])
-        if rss_items and scope.get("RSS", True) and display_regions.get("RSS", True):
+        if not skip_rss and rss_items and scope.get("RSS", True) and display_regions.get("RSS", True):
             for stat_idx, stat in enumerate(rss_items):
                 for title_idx, title_data in enumerate(stat.get("titles", [])):
                     titles_to_translate.append(title_data.get("title", ""))
                     title_locations.append(("rss_items", stat_idx, title_idx))
 
         # 4. RSS 新增标题(结构与 stats 一致)
-        if rss_new_items and scope.get("RSS", True) and display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True):
+        if not skip_rss and rss_new_items and scope.get("RSS", True) and display_regions.get("RSS", True) and display_regions.get("NEW_ITEMS", True):
             for stat_idx, stat in enumerate(rss_new_items):
                 for title_idx, title_data in enumerate(stat.get("titles", [])):
                     titles_to_translate.append(title_data.get("title", ""))
@@ -147,11 +149,12 @@ class NotificationDispatcher:
                     titles_to_translate.append(item.get("title", ""))
                     title_locations.append(("standalone_platforms", plat_idx, item_idx))
 
-            # 6. 独立展示区 - RSS 源
-            for feed_idx, feed in enumerate(standalone_data.get("rss_feeds", [])):
-                for item_idx, item in enumerate(feed.get("items", [])):
-                    titles_to_translate.append(item.get("title", ""))
-                    title_locations.append(("standalone_rss", feed_idx, item_idx))
+            # 6. 独立展示区 - RSS 源(跳过已翻译的)
+            if not skip_rss:
+                for feed_idx, feed in enumerate(standalone_data.get("rss_feeds", [])):
+                    for item_idx, item in enumerate(feed.get("items", [])):
+                        titles_to_translate.append(item.get("title", ""))
+                        title_locations.append(("standalone_rss", feed_idx, item_idx))
 
         if not titles_to_translate:
             print("[翻译] 没有需要翻译的内容")
@@ -225,6 +228,7 @@ class NotificationDispatcher:
         rss_new_items: Optional[List[Dict]] = None,
         ai_analysis: Optional[AIAnalysisResult] = None,
         standalone_data: Optional[Dict] = None,
+        skip_translation: bool = False,
     ) -> Dict[str, bool]:
         """
         分发通知到所有已配置的渠道(支持热榜+RSS合并推送+AI分析+独立展示区)
@@ -240,6 +244,7 @@ class NotificationDispatcher:
             rss_new_items: RSS 新增条目列表(用于 RSS 新增区块)
             ai_analysis: AI 分析结果(可选)
             standalone_data: 独立展示区数据(可选)
+            skip_translation: 跳过翻译(当数据已在上游翻译过时使用)
 
         Returns:
             Dict[str, bool]: 每个渠道的发送结果,key 为渠道名,value 为是否成功
@@ -250,9 +255,17 @@ class NotificationDispatcher:
         display_regions = self.config.get("DISPLAY", {}).get("REGIONS", {})
 
         # 执行翻译(如果启用,根据 display_regions 跳过不展示的区域)
-        report_data, rss_items, rss_new_items, standalone_data = self._translate_content(
-            report_data, rss_items, rss_new_items, standalone_data, display_regions
-        )
+        # skip_translation=True 时,RSS 已在上游翻译过,跳过 RSS 重复翻译
+        if not skip_translation:
+            report_data, rss_items, rss_new_items, standalone_data = self.translate_content(
+                report_data, rss_items, rss_new_items, standalone_data, display_regions
+            )
+        else:
+            # RSS 已翻译,仅翻译热榜 report_data 和独立展示区热榜部分
+            report_data, _, _, standalone_data = self.translate_content(
+                report_data, standalone_data=standalone_data, display_regions=display_regions,
+                skip_rss=True,
+            )
 
         # 飞书
         if self.config.get("FEISHU_WEBHOOK_URL"):