增强斗鱼日报的Dota2语境理解与房间画像

1. 为斗鱼日报新增直播间语义画像能力,支持按房间号配置主播职业生涯、相关选手主播、剧情关键词和常见梗解释。\n2. 在斗鱼运行时状态、直播会话和日报 payload 中补充房间上下文,使 LLM 能结合分区标签与圈内背景理解弹幕。\n3. 优化运营日报、弹幕总结、粉丝日报及其 Dify 分支提示词,明确 Dota2/电竞语境下优先按职业生涯梗、人物关系和历史比赛理解内容。
This commit is contained in:
liuwei
2026-04-27 12:50:18 +08:00
parent ea2c01532e
commit 889ce5acdd
3 changed files with 289 additions and 16 deletions

View File

@@ -28,6 +28,27 @@ daily_report_max_length = 1800
daily_report_send_image = true
audience_stats_sample_interval_seconds = 0
# 直播间语义画像(可选):
# 1. 用于给 LLM 补充“这是哪个圈子、主播职业生涯是什么、常提到哪些相关人物”的背景;
# 2. 尤其适合 Dota2 这类重人物关系、重老梗、重职业生涯语境的直播间;
# 3. 不会替代真实弹幕,只会帮助日报在解释梗时更贴近粉丝语境。
#
# 示例:
# [Douyu.room_context_profiles."7718843"]
# domain = "Dota2"
# identity_summary = "Dota2 圈内主播,弹幕常围绕职业时期、老队友、圈内主播互动展开。"
# career_background = "前 Dota2 职业选手,观众经常会把当天操作和职业时期的比赛记忆、经典名场面联系起来。"
# related_people = ["YYF", "Zhou", "Maybe", "fy", "xiao8", "Ame"]
# storyline_keywords = ["TI", "Major", "退役", "老队友", "教练", "转播", "解说", "复盘"]
# meme_explanations = [
# "如果弹幕提到老比赛、老队友、转会、退役节点,优先按主播职业生涯梗理解。",
# "如果弹幕点名 Dota2 选手或主播,优先理解为圈内人物关系梗,而不是普通泛娱乐玩笑。"
# ]
# style_hints = [
# "总结时可以保留刀圈黑话和选手简称,但不要硬解释成科普文。",
# "粉丝日报要更像老观众在接梗,避免写成普通直播间段子。"
# ]
[Douyu.report_api]
# 切换到“场景路由”模式:日报插件只关心 douyu.daily_report
# 具体绑定哪个后端由根目录 config.yaml 的 llm.scenes 统一维护。

View File

@@ -532,6 +532,11 @@ class DouyuPlugin(MessagePluginInterface):
self._status_check_retry_count = 3
self._status_check_retry_delay_seconds = 1
self._daily_report_llm_client: Optional[UnifiedLLMClient] = None
# 直播间语义画像:
# 1. 允许按房间号补充“主播职业生涯、圈内关系、常见梗来源”等背景;
# 2. 这些信息不会直接替代真实弹幕,只用于帮助 LLM 更准确理解圈内黑话;
# 3. 当前主要用于 Dota2 这类强语境直播间,但结构保持通用。
self._room_context_profiles: Dict[str, Dict[str, Any]] = {}
self._danmu_recorders: Dict[str, DouyuDanmuRecorder] = {}
# 直播状态/鱼吧轮询继续保留在轻量 async_job 中,保障现网行为稳定。
async_job.every_minutes(self._check_interval)(self._scheduled_unified_check_job)
@@ -560,6 +565,191 @@ class DouyuPlugin(MessagePluginInterface):
except Exception:
return False, day_text
@staticmethod
def _normalize_text_list(values: Any) -> List[str]:
"""
将配置或接口返回的“字符串列表”统一规整成干净的 list[str]。
这样做的原因:
1. TOML 里有些字段可能写成单字符串,有些写成字符串数组;
2. 后续 prompt 拼装只关心“有序文本集合”,不希望每处都重复判空和类型判断。
"""
if values is None:
return []
if isinstance(values, str):
value = values.strip()
return [value] if value else []
if not isinstance(values, (list, tuple, set)):
return []
result: List[str] = []
for item in values:
value = str(item or "").strip()
if value:
result.append(value)
return result
def _extract_room_runtime_context(self, room_info: Dict[str, Any]) -> Dict[str, Any]:
"""
从斗鱼房间接口里尽量多抽取“语义上下文”。
注意:
1. 斗鱼字段在不同房间或接口版本里可能并不完全一致,所以这里做多 key 兜底;
2. 就算某些字段拿不到,也保留空结构,避免后续 prompt 拼装分支过多。
"""
if not isinstance(room_info, dict):
room_info = {}
def pick(*keys: str) -> str:
for key in keys:
value = str(room_info.get(key) or "").strip()
if value:
return value
return ""
tags = self._normalize_text_list(
room_info.get("tag")
or room_info.get("tags")
or room_info.get("room_tags")
or room_info.get("show_details")
)
return {
"primary_category": pick("cate1Name", "cate_name", "game_name", "gameCateName"),
"secondary_category": pick("cate2Name", "second_lvl_name", "secondCateName", "sub_cate_name"),
"game_name": pick("game_name", "gameCateName", "cate2Name", "second_lvl_name"),
"tags": tags,
}
def _match_room_context_profile(self, room_id: str) -> Dict[str, Any]:
"""
从配置中读取指定房间号的人设/圈内背景。
配置优先按 room_id 精确匹配,避免不同主播之间串用职业生涯信息。
"""
if not isinstance(self._room_context_profiles, dict):
return {}
profile = self._room_context_profiles.get(str(room_id)) or {}
return dict(profile) if isinstance(profile, dict) else {}
def _build_room_semantic_context(
self,
room_id: str,
nickname: str,
room_name: str,
sessions: List[Dict[str, Any]],
) -> Dict[str, Any]:
"""
构建直播间语义上下文。
核心思想:
1. 先用实时房间信息判断“这是不是 Dota2/电竞强语境房间”;
2. 再叠加人工配置的主播职业生涯、圈内人物、常见梗来源;
3. 最终给 LLM 一份“理解背景”,但不替代真实弹幕证据。
"""
latest_session = sessions[-1] if sessions else {}
latest_runtime_context = dict(latest_session.get("room_context") or {})
latest_status_context = {}
if self.redis_manager:
latest_status = self.redis_manager.get_room_status(room_id) or {}
latest_status_context = dict(latest_status.get("room_context") or {})
merged_runtime_context = {
"primary_category": str(
latest_runtime_context.get("primary_category")
or latest_status_context.get("primary_category")
or ""
).strip(),
"secondary_category": str(
latest_runtime_context.get("secondary_category")
or latest_status_context.get("secondary_category")
or ""
).strip(),
"game_name": str(
latest_runtime_context.get("game_name")
or latest_status_context.get("game_name")
or ""
).strip(),
"tags": self._normalize_text_list(
latest_runtime_context.get("tags") or latest_status_context.get("tags") or []
),
}
profile = self._match_room_context_profile(room_id)
category_text = " ".join([
merged_runtime_context.get("primary_category", ""),
merged_runtime_context.get("secondary_category", ""),
merged_runtime_context.get("game_name", ""),
room_name,
nickname,
" ".join(merged_runtime_context.get("tags", [])),
" ".join(self._normalize_text_list(profile.get("domain_keywords"))),
]).lower()
inferred_domains: List[str] = []
if any(keyword in category_text for keyword in ["dota", "dota2", "刀塔", "ti", "major"]):
inferred_domains.append("Dota2")
if any(keyword in category_text for keyword in ["电竞", "esports", "职业", "选手"]):
inferred_domains.append("电竞直播")
# 如果配置明确写了 domain则放在最前面作为最强语义锚点。
configured_domain = str(profile.get("domain") or "").strip()
if configured_domain:
inferred_domains = [configured_domain] + [item for item in inferred_domains if item != configured_domain]
return {
"domain": configured_domain,
"inferred_domains": inferred_domains,
"runtime_context": merged_runtime_context,
"career_background": str(profile.get("career_background") or "").strip(),
"identity_summary": str(profile.get("identity_summary") or "").strip(),
"related_people": self._normalize_text_list(profile.get("related_people")),
"storyline_keywords": self._normalize_text_list(profile.get("storyline_keywords")),
"meme_explanations": self._normalize_text_list(profile.get("meme_explanations")),
"style_hints": self._normalize_text_list(profile.get("style_hints")),
}
def _build_room_context_prompt_block(self, payload: Dict[str, Any]) -> str:
"""
将直播间语义上下文整理成一段可以直接喂给 LLM 的提示块。
目标不是要求模型“背设定”,而是提醒它:
1. 先按 Dota2 / 电竞圈语境理解黑话和人物;
2. 看到选手、主播、职业生涯梗时,优先往房间背景上靠;
3. 仍然必须以当天真实弹幕和统计材料为主,不得凭空补剧情。
"""
room_context = payload.get("room_context", {}) or {}
runtime_context = room_context.get("runtime_context", {}) or {}
parts: List[str] = []
domains = [str(item or "").strip() for item in room_context.get("inferred_domains", []) or [] if str(item or "").strip()]
if domains:
parts.append(f"- 直播间领域语境:{', '.join(domains)}。若出现圈内黑话、人物简称、老梗,优先按这个语境理解。")
if runtime_context.get("game_name") or runtime_context.get("secondary_category") or runtime_context.get("primary_category"):
parts.append(
"- 房间分区信息:"
f"{runtime_context.get('primary_category') or '未知大类'} / "
f"{runtime_context.get('secondary_category') or runtime_context.get('game_name') or '未知小类'}"
)
if runtime_context.get("tags"):
parts.append(f"- 房间标签:{''.join(self._normalize_text_list(runtime_context.get('tags'))[:8])}")
if room_context.get("identity_summary"):
parts.append(f"- 主播身份提示:{room_context.get('identity_summary')}")
if room_context.get("career_background"):
parts.append(f"- 职业生涯背景:{room_context.get('career_background')}")
related_people = self._normalize_text_list(room_context.get("related_people"))
if related_people:
parts.append(f"- 重点相关人物:{''.join(related_people[:12])}。弹幕提到这些人时,优先考虑圈内关联。")
storyline_keywords = self._normalize_text_list(room_context.get("storyline_keywords"))
if storyline_keywords:
parts.append(f"- 常见剧情关键词:{''.join(storyline_keywords[:12])}")
meme_explanations = self._normalize_text_list(room_context.get("meme_explanations"))
if meme_explanations:
parts.append("- 常见梗解释:")
for item in meme_explanations[:6]:
parts.append(f" * {item}")
style_hints = self._normalize_text_list(room_context.get("style_hints"))
if style_hints:
parts.append(f"- 风格提示:{''.join(style_hints[:6])}")
if not parts:
return ""
return "【直播间语义上下文】\n" + "\n".join(parts) + "\n\n"
async def _fetch_json_with_retries(self, session: aiohttp.ClientSession, url: str,
headers: Dict[str, str], context: str,
params: Optional[Dict[str, Any]] = None) -> Any:
@@ -712,6 +902,10 @@ class DouyuPlugin(MessagePluginInterface):
self._audience_stats_sample_interval_seconds = int(
cfg.get("audience_stats_sample_interval_seconds", self._audience_stats_sample_interval_seconds)
)
raw_room_context_profiles = cfg.get("room_context_profiles", {}) or {}
self._room_context_profiles = (
raw_room_context_profiles if isinstance(raw_room_context_profiles, dict) else {}
)
report_api_cfg = cfg.get("report_api", {}) or {}
self._daily_report_include_structured_inputs = bool(
@@ -944,6 +1138,7 @@ class DouyuPlugin(MessagePluginInterface):
show_status = room_info.get("show_status")
nickname = room_info.get("nickname", "")
room_name = room_info.get("room_name", "")
room_context = self._extract_room_runtime_context(room_info)
avatar = room_info.get("avatar", {}) or {}
thumb_url = str(avatar.get("small", "") or "").strip().strip("`").strip()
video_loop_raw = room_info.get("videoLoop", 0)
@@ -958,23 +1153,25 @@ class DouyuPlugin(MessagePluginInterface):
"is_live": curr_live,
"nickname": nickname,
"room_name": room_name,
"is_loop": True if video_loop == 1 else False
"is_loop": True if video_loop == 1 else False,
# 保存最近一次探测到的房间上下文,供日报生成阶段辅助理解圈内梗。
"room_context": room_context,
}
self.redis_manager.set_room_status(room_id, status_obj)
if prev_live is None and curr_live is False:
continue
if prev_live is None and curr_live is True:
await self._notify_groups_live(room_id, nickname, room_name, thumb_url)
await self._notify_groups_live(room_id, nickname, room_name, thumb_url, room_context)
continue
if prev_live is False and curr_live is True:
await self._notify_groups_live(room_id, nickname, room_name, thumb_url)
await self._notify_groups_live(room_id, nickname, room_name, thumb_url, room_context)
continue
if prev_live is True and curr_live is False:
await self._notify_groups_offline(room_id, nickname, room_name, video_loop == 1)
await self._notify_groups_offline(room_id, nickname, room_name, video_loop == 1, room_context)
continue
if prev_live is True and curr_live is True and room_id not in self._danmu_recorders:
try:
room_session = self._open_or_resume_session(room_id, nickname, room_name)
room_session = self._open_or_resume_session(room_id, nickname, room_name, room_context)
if room_session:
logger.info(
f"检测到持续直播状态,续接斗鱼直播会话({room_id}): "
@@ -996,7 +1193,14 @@ class DouyuPlugin(MessagePluginInterface):
except Exception as e:
logger.exception(f"斗鱼定时任务异常: {self._format_exception(e)}")
async def _notify_groups_live(self, room_id: str, nickname: str, room_name: str, thumb_url: str):
async def _notify_groups_live(
self,
room_id: str,
nickname: str,
room_name: str,
thumb_url: str,
room_context: Optional[Dict[str, Any]] = None,
):
groups = self.redis_manager.groups_for_room(room_id)
text = f"🚀 斗鱼开播通知 \n🎤 {nickname} 正在直播中!\n 📌 房间标题:{room_name} \n 👉 点击观看https://www.douyu.com/{room_id}"
xml_content = DOUYU_MESSAGE_XML.format(title=room_name, liver=nickname, roomid=room_id, thumburl=thumb_url)
@@ -1013,7 +1217,7 @@ class DouyuPlugin(MessagePluginInterface):
logger.error(f"发送斗鱼开播提醒失败: {e}")
continue
try:
session = self._open_or_resume_session(room_id, nickname, room_name)
session = self._open_or_resume_session(room_id, nickname, room_name, room_context or {})
if session:
logger.info(
f"斗鱼直播会话开启/续接: room={room_id}, session={session.get('session_id')}, "
@@ -1024,7 +1228,14 @@ class DouyuPlugin(MessagePluginInterface):
except Exception as e:
logger.error(f"启动斗鱼弹幕记录失败({room_id}): {e}")
async def _notify_groups_offline(self, room_id: str, nickname: str, room_name: str, is_loop: bool = False):
async def _notify_groups_offline(
self,
room_id: str,
nickname: str,
room_name: str,
is_loop: bool = False,
room_context: Optional[Dict[str, Any]] = None,
):
groups = self.redis_manager.groups_for_room(room_id)
text = f"🔔 斗鱼提醒:{nickname} 下播啦~\n 🏷️ {room_name}"
if is_loop:
@@ -1037,7 +1248,7 @@ class DouyuPlugin(MessagePluginInterface):
logger.error(f"发送斗鱼下播提醒失败: {e}")
continue
try:
session = self._close_active_session(room_id, nickname, room_name)
session = self._close_active_session(room_id, nickname, room_name, room_context or {})
if session:
logger.info(
f"斗鱼直播会话关闭片段: room={room_id}, session={session.get('session_id')}, "
@@ -1235,7 +1446,13 @@ class DouyuPlugin(MessagePluginInterface):
gap_seconds = (now_dt - end_dt).total_seconds()
return 0 <= gap_seconds <= self._merge_gap_hours * 3600
def _open_or_resume_session(self, room_id: str, nickname: str, room_name: str) -> Optional[Dict[str, Any]]:
def _open_or_resume_session(
self,
room_id: str,
nickname: str,
room_name: str,
room_context: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
if not self.redis_manager:
return None
now_dt = datetime.now()
@@ -1257,6 +1474,7 @@ class DouyuPlugin(MessagePluginInterface):
"anchor_day": anchor_day,
"nickname": nickname,
"room_name": room_name,
"room_context": dict(room_context or {}),
"segments": [{"start_time": now_str, "end_time": ""}],
"audience_points": [],
"is_live": True,
@@ -1267,6 +1485,8 @@ class DouyuPlugin(MessagePluginInterface):
session["nickname"] = nickname or session.get("nickname", "")
session["room_name"] = room_name or session.get("room_name", "")
if room_context:
session["room_context"] = dict(room_context)
session["audience_points"] = self._normalize_audience_points(list(session.get("audience_points", []) or []))
session["is_live"] = True
session["updated_at"] = now_str
@@ -1274,7 +1494,13 @@ class DouyuPlugin(MessagePluginInterface):
self.redis_manager.save_room_session(room_id, session)
return session
def _close_active_session(self, room_id: str, nickname: str, room_name: str) -> Optional[Dict[str, Any]]:
def _close_active_session(
self,
room_id: str,
nickname: str,
room_name: str,
room_context: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
if not self.redis_manager:
return None
session = self.redis_manager.get_latest_room_session(room_id)
@@ -1288,6 +1514,8 @@ class DouyuPlugin(MessagePluginInterface):
session["nickname"] = nickname or session.get("nickname", "")
session["room_name"] = room_name or session.get("room_name", "")
if room_context:
session["room_context"] = dict(room_context)
session["is_live"] = False
session["updated_at"] = now_str
session["last_offline_at"] = now_str
@@ -1649,6 +1877,7 @@ class DouyuPlugin(MessagePluginInterface):
artifact_dir = os.path.join("temp", "douyu_materials")
os.makedirs(artifact_dir, exist_ok=True)
audience_trend = self._build_audience_trend(sessions)
room_context = self._build_room_semantic_context(room_id, nickname, room_name, sessions)
payload = {
"report_meta": {
"room_id": room_id,
@@ -1690,6 +1919,11 @@ class DouyuPlugin(MessagePluginInterface):
}
for item in session_payloads
],
# 直播间语义上下文:
# 1. 给 LLM 一个“这是什么圈子”的先验;
# 2. 主要用于 Dota2 这类重人物关系、重职业生涯梗的直播间;
# 3. 不替代真实弹幕,只帮助模型更准确解释黑话和典故。
"room_context": room_context,
"audience_trend": audience_trend,
"merged_templates": merged_templates[:24],
"repeated_messages": repeated_messages[:24],
@@ -1706,9 +1940,11 @@ class DouyuPlugin(MessagePluginInterface):
def _build_daily_report_prompt(self, payload: Dict[str, Any]) -> Tuple[str, str]:
meta = payload.get("report_meta", {}) or {}
room_context_prompt = self._build_room_context_prompt_block(payload)
system_prompt = (
"你是斗鱼直播日报助手。请基于给定的结构化弹幕材料,输出一份适合发群的中文日报。"
"要求简洁、自然、信息密度高,不要编造,不要使用代码块。"
"如果材料显示这是 Dota2 / 电竞语境,请优先按该圈层理解弹幕中的人物、黑话、历史梗和职业生涯梗。"
)
user_prompt = (
"请输出一份斗鱼每日报告,格式要求:\n"
@@ -1718,15 +1954,18 @@ class DouyuPlugin(MessagePluginInterface):
"4. 单独列出高频梗/复读内容(不超过 5 条)。\n"
"5. 单独列出 2-3 个热点时段。\n"
"6. 整体控制在 600 字以内。\n\n"
f"{room_context_prompt}"
f"材料如下:\n{json.dumps(payload, ensure_ascii=False, indent=2)}"
)
return system_prompt, user_prompt
def _build_danmu_summary_prompt(self, payload: Dict[str, Any]) -> Tuple[str, str]:
meta = payload.get("report_meta", {}) or {}
room_context_prompt = self._build_room_context_prompt_block(payload)
system_prompt = (
"你是直播弹幕总结助手。请只根据给定材料,总结这场直播的弹幕内容与氛围。"
"不要输出运营数据,不要编造,不要写空话套话。"
"如果材料表明这是 Dota2 / 电竞直播间,请优先把梗理解为圈内人物、职业经历、赛事记忆和主播关系梗。"
)
user_prompt = (
"请输出一段适合放在日报图片上半部分的弹幕总结,要求:\n"
@@ -1738,6 +1977,7 @@ class DouyuPlugin(MessagePluginInterface):
"6. 不要写“根据数据”“建议”“策略”等词。\n\n"
f"主播:{meta.get('nickname') or meta.get('room_name') or meta.get('room_id')}\n"
f"日期:{meta.get('anchor_day', '')}\n"
f"{room_context_prompt}"
f"材料:\n{json.dumps(payload, ensure_ascii=False, indent=2)}"
)
return system_prompt, user_prompt
@@ -1750,10 +1990,12 @@ class DouyuPlugin(MessagePluginInterface):
3. 允许轻微恶搞和夸张,但不能编造未出现的事件,也不能攻击主播或观众。
"""
meta = payload.get("report_meta", {}) or {}
room_context_prompt = self._build_room_context_prompt_block(payload)
system_prompt = (
"你是斗鱼直播间的粉丝向整活日报编辑。"
"请只根据提供的真实弹幕材料,输出一份开心、欢乐、带一点恶搞气质的中文总结。"
"语气要像群友在复盘名场面,不要写成运营分析,不要编造剧情,不要使用代码块。"
"如果这是 Dota2 / 电竞语境直播间,请优先按刀圈/电竞圈人物关系、职业生涯、老比赛和主播互动梗去理解笑点。"
)
user_prompt = (
"请输出一份适合给粉丝看的《斗鱼弹幕乐子日报》,严格按下面结构输出:\n"
@@ -1765,6 +2007,7 @@ class DouyuPlugin(MessagePluginInterface):
"6. 可以夸张一点、调皮一点,但不要低俗,不要攻击主播,不要使用“建议、策略、转化、数据表现”等运营词。\n\n"
f"主播:{meta.get('nickname') or meta.get('room_name') or meta.get('room_id')}\n"
f"日期:{meta.get('anchor_day', '')}\n"
f"{room_context_prompt}"
f"材料:\n{json.dumps(payload, ensure_ascii=False, indent=2)}"
)
return system_prompt, user_prompt

View File

@@ -413,7 +413,9 @@ workflow:
4. 如果 system_prompt 非空,优先遵循其中的格式要求。
5. 严格控制在 max_length 以内
5. 如果材料或提示词表明这是 Dota2 / 电竞语境直播间,请优先按该圈层的人物关系、赛事记忆、职业生涯梗来理解弹幕
6. 严格控制在 max_length 以内。
'
- id: daily_user_1
@@ -505,9 +507,11 @@ workflow:
4. 不要写运营策略、建议、转化或复盘结论。
5. 如果 system_prompt 非空,优先遵循其中的格式要求
5. 如果材料或提示词表明这是 Dota2 / 电竞语境直播间,请优先按圈内选手、主播、职业经历和老梗来理解弹幕
6. 严格控制在 max_length 以内
6. 如果 system_prompt 非空,优先遵循其中的格式要求
7. 严格控制在 max_length 以内。
'
- id: danmu_user_1
@@ -602,9 +606,11 @@ workflow:
5. 尽量保留现场弹幕感、口头语和接梗氛围。
6. 如果 system_prompt 非空,优先遵循其中的格式要求
6. 如果材料或提示词表明这是 Dota2 / 电竞语境直播间,请优先按刀圈人物关系、主播职业生涯、老比赛和圈内名场面去理解笑点
7. 严格控制在 max_length 以内
7. 如果 system_prompt 非空,优先遵循其中的格式要求
8. 严格控制在 max_length 以内。
'
- id: fans_user_1
@@ -685,6 +691,7 @@ workflow:
当前是回退链路,请稳定输出运营版完整日报正文。
只根据输入材料生成中文纯文本,不编造,不要使用代码块,不要超过 max_length。
如果材料表明这是 Dota2 / 电竞语境直播间,请优先按圈内人物关系和职业生涯梗理解弹幕。
'
- id: daily_user_2
@@ -759,6 +766,7 @@ workflow:
当前是回退链路,请稳定输出图片上半部分使用的弹幕总结。
只根据输入材料生成中文纯文本,不编造,不要使用代码块,不要写运营分析,不要超过 max_length。
如果材料表明这是 Dota2 / 电竞语境直播间,请优先按圈内选手、主播和历史梗理解弹幕。
'
- id: danmu_user_2
@@ -833,6 +841,7 @@ workflow:
当前是回退链路,请稳定输出单独给粉丝看的欢乐恶搞日报。
只根据输入材料生成中文纯文本,不编造,不要使用代码块,不要写运营分析,不要超过 max_length。
如果材料表明这是 Dota2 / 电竞语境直播间,请优先按刀圈人物关系、职业生涯和老梗去理解笑点。
'
- id: fans_user_2