diff --git a/plugins/douyu/report_template.py b/plugins/douyu/report_template.py index 14a5154..15c794b 100644 --- a/plugins/douyu/report_template.py +++ b/plugins/douyu/report_template.py @@ -474,6 +474,95 @@ def _render_fans_metric_cards(metrics: List[Dict[str, str]]) -> str: return "".join(blocks) +def _get_compact_scene_material(payload: Dict[str, Any]) -> Dict[str, Any]: + """ + 兼容两种来源的粉丝日报载荷: + 1. 新版 prompt 材料已经预先组装好的 compact_scene_material; + 2. 正式渲染链路里仍然保留在 llm_compact 下的压缩材料。 + 这样模板层可以向后兼容,不会因为字段尚未完全迁移就出现空版块。 + """ + compact = payload.get("compact_scene_material", {}) or {} + if compact: + return compact + return payload.get("llm_compact", {}) or {} + + +def _get_topic_evidence_clusters(payload: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + 兼容“已展开主题簇”和“仍放在 llm_compact.semantic_fact_hints.topic_clusters”两种结构。 + """ + clusters = payload.get("topic_evidence_clusters", []) or [] + if clusters: + return clusters + compact = _get_compact_scene_material(payload) + fact_hints = compact.get("semantic_fact_hints", {}) or {} + normalized = [] + for item in (fact_hints.get("topic_clusters", []) or [])[:6]: + normalized.append({ + "label": str(item.get("label") or "").strip(), + "count": int(item.get("match_count", item.get("count", 0)) or 0), + "user_count": int(item.get("user_count", 0) or 0), + "time_range": ( + f"{str(item.get('first_hm') or '').strip()}-{str(item.get('last_hm') or '').strip()}" + ).strip("-"), + "keywords": item.get("keywords", []) or [], + "samples": item.get("samples", []) or [], + }) + return normalized + + +def _get_hero_mentions(payload: Dict[str, Any]) -> List[Dict[str, Any]]: + compact = _get_compact_scene_material(payload) + return ( + compact.get("semantic_fact_hints", {}) + .get("hero_mentions", []) + or [] + ) + + +def _build_local_stats(payload: Dict[str, Any]) -> Dict[str, Any]: + """ + 兼容新旧载荷的本地统计字段。 + 如果没有 top-level local_stats,就从 peak_buckets / repeated_messages / burst_terms / content_cues 临时拼一份, + 保证“热点窗口与共识梗”始终有内容可用。 + """ + local_stats = payload.get("local_stats", {}) or {} + if local_stats: + return local_stats + + compact = _get_compact_scene_material(payload) + content_cues = compact.get("content_cues", []) or [] + return { + "message_count": int((payload.get("report_meta", {}) or {}).get("message_count", 0) or 0), + "unique_user_count": int((payload.get("report_meta", {}) or {}).get("unique_user_count", 0) or 0), + "top_emotion_bursts": [ + { + "text": str(item.get("text") or "").strip(), + "count": int(item.get("count", 0) or 0), + } + for item in content_cues + if str(item.get("kind") or "").strip() == "emotion" and str(item.get("text") or "").strip() + ][:8], + "top_repeated_messages": [ + { + "text": str(item.get("text") or "").strip(), + "count": int(item.get("count", 0) or 0), + "user_count": int(item.get("user_count", 0) or 0), + } + for item in (payload.get("repeated_messages", []) or [])[:8] + if str(item.get("text") or "").strip() + ], + "peak_windows": [ + { + "start_time": str(item.get("start_time") or "").strip(), + "message_count": int(item.get("message_count", 0) or 0), + "user_count": int(item.get("user_count", 0) or 0), + } + for item in (payload.get("peak_buckets", []) or [])[:6] + ], + } + + def _build_fans_effective_info_lines(payload: Dict[str, Any], limit: int = 6) -> List[str]: """ 为粉丝日报补一层“有效信息速览”。 @@ -490,7 +579,7 @@ def _build_fans_effective_info_lines(payload: Dict[str, Any], limit: int = 6) -> seen.add(value) lines.append(value) - for item in (payload.get("topic_evidence_clusters", []) or [])[:6]: + for item in _get_topic_evidence_clusters(payload)[: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() @@ -506,13 +595,13 @@ def _build_fans_effective_info_lines(payload: Dict[str, Any], limit: int = 6) -> if len(lines) >= limit: return lines[:limit] - hero_mentions = payload.get("compact_scene_material", {}).get("semantic_fact_hints", {}).get("hero_mentions", []) or [] + hero_mentions = _get_hero_mentions(payload) 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 [] + hot_windows = _build_local_stats(payload).get("peak_windows", []) or [] if hot_windows: top_window = hot_windows[0] push( @@ -537,7 +626,7 @@ def _build_local_topic_focus_lines(payload: Dict[str, Any], limit: int = 4) -> L seen.add(value) lines.append(value) - for item in (payload.get("topic_evidence_clusters", []) or [])[:4]: + for item in _get_topic_evidence_clusters(payload)[:4]: label = str(item.get("label") or "").strip() keywords = [str(keyword).strip() for keyword in (item.get("keywords", []) or [])[:5] if str(keyword).strip()] count = int(item.get("count", 0) or 0) @@ -555,12 +644,7 @@ def _build_local_hero_focus_lines(payload: Dict[str, Any], limit: int = 4) -> Li 为“英雄与对局焦点”准备本地兜底。 这部分直接复用英雄提及聚类,优先强调出现频次和代表发言,方便粉丝快速看懂今天在聊什么英雄。 """ - hero_mentions = ( - payload.get("compact_scene_material", {}) - .get("semantic_fact_hints", {}) - .get("hero_mentions", []) - or [] - ) + hero_mentions = _get_hero_mentions(payload) lines: List[str] = [] seen = set() @@ -687,11 +771,20 @@ def _render_hot_window_cards(hot_windows: List[Dict[str, Any]]) -> str: f'
' '' ) + # 页面层兜底: + # 如果热窗没有任何卡片,也返回一个说明块,避免整个区域看起来像渲染坏了。 + if not blocks: + blocks.append( + '