ForcePilot/backend/package/yuxi/channel/extensions/bilibili/danmaku/handler.py
Kris 415d66d1c6 feat(bilibili): 新增B站渠道插件,支持直播弹幕和私信交互
该提交实现了完整的B站渠道插件,包含以下核心功能:
1.  支持B站直播弹幕监听与处理,包含弹幕、SC、礼物等多种直播间事件
2.  支持B站私信的轮询接收与发送
3.  内置WBI签名算法,适配B站API鉴权要求
4.  提供账号配对、黑白名单等弹幕私信权限控制
5.  集成速率限制与防风险机制,降低账号封禁风险
6.  完善的配置管理与状态监控能力
2026-05-21 10:40:04 +08:00

357 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import logging
import time
from yuxi.channel.extensions.bilibili.danmaku.types import DanmakuMessage, SuperChatEvent
from yuxi.channel.extensions.bilibili.types import BilibiliAccountConfig
from yuxi.channel.message.models import (
GroupContext,
MessageType,
PeerInfo,
UnifiedMessage,
)
from yuxi.channel.routing.models import PeerKind
logger = logging.getLogger(__name__)
_GUARD_LEVEL_NAME = {1: "总督", 2: "提督", 3: "舰长"}
class DanmakuEventHandler:
def __init__(self, room_id: int, account, runtime, account_id: str = "default"):
self._room_id = room_id
self._account = account
self._runtime = runtime
self._account_id = account_id
self._bot_uid = str(account.dedeuserid)
self._room_users_seen: set[str] = set()
async def handle_danmaku(self, data: dict) -> None:
info = data["info"]
text = info[1]
user_info = info[2]
uid = str(user_info[0])
uname = user_info[1]
msg = DanmakuMessage(
room_id=self._room_id,
uid=uid,
uname=uname,
text=text,
timestamp=time.time(),
fans_medal_name=info[3][1] if len(info[3]) > 1 else "",
fans_medal_level=info[3][0] if len(info[3]) > 0 else 0,
ul_level=info[5][0] if len(info[5]) > 0 else 0,
)
if uid == self._bot_uid:
return
if self._account.require_mention and not self._should_respond(msg):
return
await self._dispatch_to_agent(msg)
async def handle_super_chat(self, data: dict) -> None:
event = SuperChatEvent(
room_id=self._room_id,
uid=str(data["uid"]),
uname=data["user_info"]["uname"],
message=data["message"],
price=data["price"],
start_time=data["start_time"],
end_time=data["end_time"],
timestamp=time.time(),
)
await self._dispatch_to_agent(event)
async def handle_gift(self, data: dict) -> None:
uname = data.get("uname", "")
uid = str(data.get("uid", ""))
num = data.get("num", 0)
gift_name = data.get("giftName", "")
price = data.get("price", 0)
logger.info("直播间 %s 礼物: %s 送出 %sx %s", self._room_id, uname, num, gift_name)
content = f"[礼物通知] {uname} 送出了 {num}x {gift_name}"
if price:
content += f" (总价 {price} 电池)"
await self._dispatch_event("gift", uid, uname, content)
async def handle_guard(self, data: dict) -> None:
username = data.get("username", "")
uid = str(data.get("uid", ""))
guard_level = data.get("guard_level", 0)
guard_name = _GUARD_LEVEL_NAME.get(guard_level, f"舰队Lv{guard_level}")
logger.info("直播间 %s 大航海: %s 开通 %s", self._room_id, username, guard_name)
content = f"[舰队通知] {username} 开通了{guard_name}"
await self._dispatch_event("guard", uid, username, content)
async def handle_interact(self, data: dict) -> None:
msg_type = data.get("msg_type", 0)
if msg_type != 1:
return
uid = str(data.get("uid", ""))
uname = data.get("uname", "")
if uid in self._room_users_seen:
return
self._room_users_seen.add(uid)
content = f"[进房通知] {uname} 进入了直播间"
await self._dispatch_event("enter", uid, uname, content)
async def handle_live(self, data: dict) -> None:
logger.info("直播间 %s 开播", self._room_id)
await self._dispatch_status_event("live", "[直播状态] 开播了")
async def handle_preparing(self, data: dict) -> None:
logger.info("直播间 %s 下播", self._room_id)
await self._dispatch_status_event("preparing", "[直播状态] 下播了")
async def handle_cut_off(self, data: dict) -> None:
logger.warning("直播间 %s 被超管切断", self._room_id)
await self._dispatch_status_event("cut_off", "[直播状态] 直播间被超管切断")
async def handle_warning(self, data: dict) -> None:
logger.warning("直播间 %s 收到管理员警告", self._room_id)
await self._dispatch_status_event("warning", "[直播状态] 收到管理员警告")
async def handle_combo_send(self, data: dict) -> None:
uname = data.get("uname", "")
uid = str(data.get("uid", ""))
combo_num = data.get("combo_num", 0)
gift_name = data.get("gift_name", "")
logger.info("直播间 %s 连击: %s 连送 %s%s", self._room_id, uname, combo_num, gift_name)
content = f"[连击礼物] {uname} 连送 {combo_num}{gift_name}"
await self._dispatch_event("combo_send", uid, uname, content)
async def handle_sc_delete(self, data: dict) -> None:
message_id = data.get("message_id", "")
logger.info("直播间 %s SC 删除: message_id=%s", self._room_id, message_id)
content = f"[SC 删除] 消息 #{message_id} 已被删除"
await self._dispatch_status_event("sc_delete", content)
async def handle_user_toast(self, data: dict) -> None:
uid = str(data.get("uid", ""))
uname = data.get("username", "")
toast_msg = data.get("toast_msg", "")
guard_level = data.get("guard_level", 0)
price = data.get("price", 0)
num = data.get("num", 0)
gift_name = data.get("gift_name", "")
logger.info("直播间 %s Toast: %s - %s", self._room_id, uname, toast_msg)
if guard_level:
guard_name = _GUARD_LEVEL_NAME.get(guard_level, f"舰队Lv{guard_level}")
content = f"[上舰感谢] {uname} 开通{guard_name}"
elif gift_name:
content = f"[礼物感谢] {uname} 赠送 {num}x {gift_name}"
if price:
content += f" (总价 {price} 电池)"
else:
content = f"[系统通知] {toast_msg}"
await self._dispatch_event("toast", uid, uname, content)
async def handle_welcome_guard(self, data: dict) -> None:
uid = str(data.get("uid", ""))
username = data.get("username", "")
guard_level = data.get("guard_level", 0)
guard_name = _GUARD_LEVEL_NAME.get(guard_level, f"舰队Lv{guard_level}")
logger.info("直播间 %s 舰长入场: %s (%s)", self._room_id, username, guard_name)
content = f"[舰长入场] {username} ({guard_name}) 进入了直播间"
await self._dispatch_event("welcome_guard", uid, username, content)
async def handle_entry_effect(self, data: dict) -> None:
uid = str(data.get("uid", ""))
uname = data.get("uname", "")
effect_id = data.get("effect_id", 0)
logger.info("直播间 %s 进场特效: %s (effect_id=%s)", self._room_id, uname, effect_id)
content = f"[进场特效] {uname} 进入了直播间"
await self._dispatch_event("entry_effect", uid, uname, content)
async def handle_super_chat_jpn(self, data: dict) -> None:
event = SuperChatEvent(
room_id=self._room_id,
uid=str(data.get("uid", "")),
uname=data.get("user_info", {}).get("uname", ""),
message=data.get("message", ""),
price=data.get("price", 0),
start_time=data.get("start_time", 0),
end_time=data.get("end_time", 0),
timestamp=time.time(),
)
await self._dispatch_to_agent(event)
async def handle_pk_start(self, data: dict) -> None:
pk_id = data.get("pk_id", "")
logger.info("直播间 %s PK 开始: pk_id=%s", self._room_id, pk_id)
content = "[PK通知] 大乱斗开始了!"
await self._dispatch_status_event("pk_start", content)
async def handle_pk_end(self, data: dict) -> None:
pk_id = data.get("pk_id", "")
logger.info("直播间 %s PK 结束: pk_id=%s", self._room_id, pk_id)
content = "[PK通知] 大乱斗结束了"
await self._dispatch_status_event("pk_end", content)
async def handle_pk_process(self, data: dict) -> None:
logger.debug("直播间 %s PK 进行中", self._room_id)
async def handle_anchor_lot_start(self, data: dict) -> None:
award_name = data.get("award_name", "")
logger.info("直播间 %s 天选时刻开始: %s", self._room_id, award_name)
content = f"[天选时刻] 抽奖开始 - {award_name}"
await self._dispatch_status_event("anchor_lot_start", content)
async def handle_anchor_lot_end(self, data: dict) -> None:
logger.info("直播间 %s 天选时刻结束", self._room_id)
content = "[天选时刻] 抽奖已结束"
await self._dispatch_status_event("anchor_lot_end", content)
async def handle_anchor_lot_award(self, data: dict) -> None:
award_name = data.get("award_name", "")
winners = data.get("award_users", [])
winner_names = [w.get("uname", "") for w in winners]
logger.info("直播间 %s 天选时刻开奖: %s -> %s", self._room_id, award_name, winner_names)
content = f"[天选时刻] 恭喜 {', '.join(winner_names)} 获得 {award_name}"
await self._dispatch_status_event("anchor_lot_award", content)
async def handle_notice_msg(self, data: dict) -> None:
msg_common = data.get("msg_common", "")
business_id = data.get("business_id", "")
msg_type = data.get("msg_type", 0)
logger.info("直播间 %s 系统通知: type=%s business=%s", self._room_id, msg_type, business_id)
content = f"[系统通知] {msg_common}"
await self._dispatch_status_event("notice", content)
async def handle_welcome(self, data: dict) -> None:
uname = data.get("uname", "")
logger.debug("直播间 %s 老爷入场: %s", self._room_id, uname)
async def handle_online_rank(self, data: dict) -> None:
logger.debug("直播间 %s 高能榜更新 (count=...)", self._room_id)
async def handle_watched_change(self, data: dict) -> None:
num = data.get("num", 0)
text_large = data.get("text_large", "")
logger.debug("直播间 %s 观看人数: %s (%s)", self._room_id, num, text_large)
async def handle_like_info(self, data: dict) -> None:
logger.debug("直播间 %s 点赞更新", self._room_id)
async def handle_room_change(self, data: dict) -> None:
title = data.get("title", "")
area_name = data.get("area_name", "")
logger.info("直播间 %s 信息变更: title=%s area=%s", self._room_id, title, area_name)
async def handle_voice_join(self, data: dict) -> None:
logger.debug("直播间 %s 语音连麦状态变更", self._room_id)
async def handle_red_pocket_start(self, data: dict) -> None:
logger.debug("直播间 %s 红包活动开始", self._room_id)
async def handle_red_pocket_winner(self, data: dict) -> None:
logger.debug("直播间 %s 红包中奖名单", self._room_id)
def _should_respond(self, msg: DanmakuMessage) -> bool:
text = msg.text.strip()
keywords = self._account.bot_keywords or []
return any(kw in text for kw in keywords) or len(text) > 0
async def _dispatch_to_agent(self, msg) -> None:
content = msg.text if hasattr(msg, "text") else msg.message
unified = UnifiedMessage(
msg_id=f"danmaku_{msg.room_id}_{msg.uid}_{int(msg.timestamp)}",
channel_type="bilibili",
account_id=self._account_id,
content=content,
sender=PeerInfo(
kind=PeerKind.GROUP,
id=msg.uid,
display_name=msg.uname,
),
group=GroupContext(
id=str(msg.room_id),
route_peer_kind="group",
route_peer_id=str(msg.room_id),
),
message_type=MessageType.TEXT,
timestamp=time.time(),
metadata={"source": "bilibili_danmaku"},
)
queue = getattr(self._runtime, "queue", None)
if queue:
await queue.put(unified)
else:
logger.warning("运行时队列不可用,无法分发弹幕消息")
async def _dispatch_event(self, event_type: str, uid: str, uname: str, content: str) -> None:
unified = UnifiedMessage(
msg_id=f"{event_type}_{self._room_id}_{uid}_{int(time.time())}",
channel_type="bilibili",
account_id=self._account_id,
content=content,
sender=PeerInfo(kind=PeerKind.GROUP, id=uid, display_name=uname),
group=GroupContext(
id=str(self._room_id),
route_peer_kind="group",
route_peer_id=str(self._room_id),
),
message_type=MessageType.EVENT,
timestamp=time.time(),
metadata={"source": "bilibili_danmaku", "event_type": event_type},
)
queue = getattr(self._runtime, "queue", None)
if queue:
await queue.put(unified)
else:
logger.warning("运行时队列不可用,无法分发 %s 事件", event_type)
async def _dispatch_status_event(self, event_type: str, content: str) -> None:
unified = UnifiedMessage(
msg_id=f"live_status_{self._room_id}_{event_type}_{int(time.time())}",
channel_type="bilibili",
account_id=self._account_id,
content=content,
sender=PeerInfo(kind=PeerKind.GROUP, id="system", display_name="B站系统"),
group=GroupContext(
id=str(self._room_id),
route_peer_kind="group",
route_peer_id=str(self._room_id),
),
message_type=MessageType.EVENT,
timestamp=time.time(),
metadata={"source": "bilibili_danmaku", "event_type": event_type},
)
queue = getattr(self._runtime, "queue", None)
if queue:
await queue.put(unified)
else:
logger.warning("运行时队列不可用,无法分发 %s 状态事件", event_type)
def resolve_reply_target(msg: DanmakuMessage, account: BilibiliAccountConfig) -> str | None:
if account.danmaku_reply_enabled:
return f"danmaku:{msg.room_id}"
elif account.dm_enabled:
return f"dm:{msg.uid}"
else:
return None