diff --git a/plugins/douyu/config.toml b/plugins/douyu/config.toml index 925bfae..61612da 100644 --- a/plugins/douyu/config.toml +++ b/plugins/douyu/config.toml @@ -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 统一维护。 diff --git a/plugins/douyu/main.py b/plugins/douyu/main.py index 68a2c02..6e21208 100644 --- a/plugins/douyu/main.py +++ b/plugins/douyu/main.py @@ -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 diff --git a/plugins/douyu/斗鱼日报AI.yml b/plugins/douyu/斗鱼日报AI.yml index 0ea3064..78fe5af 100644 --- a/plugins/douyu/斗鱼日报AI.yml +++ b/plugins/douyu/斗鱼日报AI.yml @@ -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