diff --git a/plugins/douyu/report_template.py b/plugins/douyu/report_template.py
index 283eceb..d3597e2 100644
--- a/plugins/douyu/report_template.py
+++ b/plugins/douyu/report_template.py
@@ -462,6 +462,159 @@ def _render_fans_metric_cards(metrics: List[Dict[str, str]]) -> str:
return "".join(blocks)
+def _build_fans_effective_info_lines(payload: Dict[str, Any], limit: int = 6) -> List[str]:
+ """
+ 为粉丝日报补一层“有效信息速览”。
+ 这里优先从本地提纯后的主题证据簇中拿事实,不依赖 LLM 自己归纳,
+ 这样模板本身就能稳定承载更多有效信息。
+ """
+ lines: List[str] = []
+ seen = set()
+
+ def push(text: str) -> None:
+ value = str(text or "").strip()
+ if not value or value in seen:
+ return
+ seen.add(value)
+ lines.append(value)
+
+ for item in (payload.get("topic_evidence_clusters", []) or [])[:6]:
+ label = str(item.get("label") or "").strip()
+ count = int(item.get("count", 0) or 0)
+ time_range = str(item.get("time_range") or "").strip()
+ samples = item.get("samples", []) or []
+ sample_text = ""
+ if samples:
+ first_sample = samples[0]
+ sample_text = str(first_sample.get("content") or "").strip()[:42]
+ if label and sample_text:
+ push(f"{label}在 {time_range or '全场'} 持续被讨论,相关弹幕约 {count} 条,代表内容是「{sample_text}」。")
+ elif label:
+ push(f"{label}是今天的高关注话题之一,相关弹幕约 {count} 条。")
+ if len(lines) >= limit:
+ return lines[:limit]
+
+ hero_mentions = payload.get("compact_scene_material", {}).get("semantic_fact_hints", {}).get("hero_mentions", []) or []
+ if hero_mentions:
+ hero_names = [str(item.get("hero") or "").strip() for item in hero_mentions[:4] if str(item.get("hero") or "").strip()]
+ if hero_names:
+ push(f"英雄讨论主要集中在 {'、'.join(hero_names)}。")
+
+ hot_windows = payload.get("local_stats", {}).get("peak_windows", []) or []
+ if hot_windows:
+ top_window = hot_windows[0]
+ push(
+ f"最热窗口出现在 {str(top_window.get('start_time') or '')[-8:-3]},"
+ f"该时段累计弹幕 {int(top_window.get('message_count', 0) or 0)} 条。"
+ )
+ return lines[:limit]
+
+
+def _render_fans_info_cards(items: List[str]) -> str:
+ blocks = []
+ for item in items[:6]:
+ blocks.append(
+ '
'
+ f'
{_escape(item)}
'
+ '
'
+ )
+ return "".join(blocks)
+
+
+def _render_topic_clusters(topic_clusters: List[Dict[str, Any]]) -> str:
+ blocks = []
+ for item in topic_clusters[:5]:
+ label = str(item.get("label") or "").strip()
+ if not label:
+ continue
+ count = int(item.get("count", 0) or 0)
+ user_count = int(item.get("user_count", 0) or 0)
+ time_range = str(item.get("time_range") or "").strip()
+ samples = item.get("samples", []) or []
+ sample_lines = []
+ for sample in samples[:3]:
+ content = str(sample.get("content") or "").strip()
+ nickname = str(sample.get("nickname") or "").strip() or "观众"
+ hm = str(sample.get("hm") or "").strip()
+ if content:
+ sample_lines.append(f"{hm} {nickname}:{content[:42]}")
+ sample_html = "".join(f'{_escape(line)}' for line in sample_lines)
+ blocks.append(
+ ''
+ f'
{_escape(label)}
'
+ f'
{_escape(time_range or "全场")} · {count} 条讨论 · {user_count} 人参与
'
+ f'
'
+ '
'
+ )
+ return "".join(blocks)
+
+
+def _render_hero_mentions(hero_mentions: List[Dict[str, Any]]) -> str:
+ blocks = []
+ for item in hero_mentions[:4]:
+ hero_name = str(item.get("hero") or "").strip()
+ if not hero_name:
+ continue
+ mention_count = int(item.get("mention_count", 0) or 0)
+ user_count = int(item.get("user_count", 0) or 0)
+ sample_text = ""
+ samples = item.get("samples", []) or []
+ if samples:
+ sample = samples[0]
+ sample_text = str(sample.get("content") or "").strip()[:46]
+ blocks.append(
+ ''
+ f'
{_escape(hero_name)}
'
+ f'
{mention_count} 次提及 / {user_count} 人讨论
'
+ f'
{_escape(sample_text)}
'
+ '
'
+ )
+ return "".join(blocks)
+
+
+def _render_hot_window_cards(hot_windows: List[Dict[str, Any]]) -> str:
+ blocks = []
+ for item in hot_windows[:4]:
+ start_time = str(item.get("start_time") or "")[-8:-3]
+ message_count = int(item.get("message_count", 0) or 0)
+ user_count = int(item.get("user_count", 0) or 0)
+ blocks.append(
+ ''
+ f'
{_escape(start_time)}
'
+ f'
{message_count} 条弹幕 / {user_count} 人参与
'
+ '
'
+ )
+ return "".join(blocks)
+
+
+def _render_repeat_digest(payload: Dict[str, Any]) -> str:
+ local_stats = payload.get("local_stats", {}) or {}
+ repeated_messages = local_stats.get("top_repeated_messages", []) or []
+ emotion_bursts = local_stats.get("top_emotion_bursts", []) or []
+ blocks = []
+ for item in repeated_messages[:4]:
+ text = str(item.get("text") or "").strip()
+ count = int(item.get("count", 0) or 0)
+ if text:
+ blocks.append(
+ ''
+ f'{_escape(text[:28])}'
+ f'{count} 次'
+ '
'
+ )
+ for item in emotion_bursts[:4]:
+ text = str(item.get("text") or "").strip()
+ count = int(item.get("count", 0) or 0)
+ if text:
+ blocks.append(
+ ''
+ f'{_escape(text[:18])}'
+ f'{count} 次'
+ '
'
+ )
+ return "".join(blocks)
+
+
def _render_badges(top_badges: List[Dict[str, Any]]) -> str:
blocks = []
for item in top_badges[:6]:
@@ -783,6 +936,14 @@ def render_fans_daily_report_html(
lead_text = str(sections.get("lead") or "").strip()
if not lead_text:
lead_text = "今天这场直播的弹幕主打一个集体上头,观众一边盯着画面,一边忙着把梗越刷越离谱。"
+ topic_clusters = payload.get("topic_evidence_clusters", []) or []
+ hero_mentions = (
+ payload.get("compact_scene_material", {})
+ .get("semantic_fact_hints", {})
+ .get("hero_mentions", [])
+ or []
+ )
+ local_stats = payload.get("local_stats", {}) or {}
renderer = HtmlTemplateRenderer()
return renderer.render(
@@ -791,8 +952,13 @@ def render_fans_daily_report_html(
"title_name": title_name,
"subtitle": subtitle,
"lead_text": lead_text,
- # 粉丝版刻意弱化“分析感”,下面几块都只展示娱乐化后的结果。
+ # 粉丝版不再只做“乐子文案展示”,而是补进本地提纯后的有效信息区。
"fans_metrics_html": Markup(_render_fans_metric_cards(_build_fans_fun_metrics(payload))),
+ "effective_info_html": Markup(_render_fans_info_cards(_build_fans_effective_info_lines(payload))),
+ "topic_clusters_html": Markup(_render_topic_clusters(topic_clusters)),
+ "hero_mentions_html": Markup(_render_hero_mentions(hero_mentions)),
+ "hot_windows_html": Markup(_render_hot_window_cards(local_stats.get("peak_windows", []) or [])),
+ "repeat_digest_html": Markup(_render_repeat_digest(payload)),
"laugh_points_html": Markup(_render_list(laugh_points, item_class="funny-list")),
"famous_scenes_html": Markup(_render_fans_scene_cards(famous_scenes)),
"meme_rank_html": Markup(_render_rank_cards(meme_rank)),
diff --git a/plugins/douyu/templates/daily_fans_report.html b/plugins/douyu/templates/daily_fans_report.html
index cd16eab..536aa69 100644
--- a/plugins/douyu/templates/daily_fans_report.html
+++ b/plugins/douyu/templates/daily_fans_report.html
@@ -4,95 +4,90 @@