增强斗鱼日报的Dota2语境理解与房间画像
1. 为斗鱼日报新增直播间语义画像能力,支持按房间号配置主播职业生涯、相关选手主播、剧情关键词和常见梗解释。\n2. 在斗鱼运行时状态、直播会话和日报 payload 中补充房间上下文,使 LLM 能结合分区标签与圈内背景理解弹幕。\n3. 优化运营日报、弹幕总结、粉丝日报及其 Dify 分支提示词,明确 Dota2/电竞语境下优先按职业生涯梗、人物关系和历史比赛理解内容。
This commit is contained in:
@@ -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 统一维护。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user