ForcePilot/backend/package/yuxi/channel/extensions/bilibili/gateway.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

153 lines
5.1 KiB
Python

from __future__ import annotations
import asyncio
import logging
from yuxi.channel.extensions.bilibili.anti_risk import AntiRisk
from yuxi.channel.extensions.bilibili.client import BilibiliClientManager
from yuxi.channel.extensions.bilibili.config import BilibiliConfigAdapter
from yuxi.channel.extensions.bilibili.types import BilibiliAccountConfig
logger = logging.getLogger(__name__)
class BilibiliGateway:
def __init__(self):
self._config_adapter = BilibiliConfigAdapter()
self._client_manager: BilibiliClientManager | None = None
self._anti_risk: AntiRisk | None = None
self._danmaku_tasks: dict[int, asyncio.Task] = {}
self._dm_poll_task: asyncio.Task | None = None
self._running = False
self._abort_signal: asyncio.Event | None = None
self._queue: asyncio.Queue | None = None
@property
def credential(self):
if self._client_manager is None:
return None
return self._client_manager.credential
@property
def anti_risk(self) -> AntiRisk | None:
return self._anti_risk
async def start(self, ctx) -> object:
account, account_id = await self._resolve_account(ctx)
if not self._config_adapter.is_configured(account):
logger.warning("B站账户 %s 未配置,跳过启动", account_id)
return {"running": False, "reason": "not-configured"}
self._abort_signal = asyncio.Event()
self._queue = asyncio.Queue(maxsize=2000)
self._running = True
acct_config = self._config_adapter.get_account_config(ctx.config or {}, account_id)
self._client_manager = BilibiliClientManager(acct_config, ctx.logger or logger)
await self._client_manager.setup()
if not await self._client_manager.verify_credential():
raise RuntimeError("B站 Cookie 验证失败,可能已过期")
anti_risk_level = self._config_adapter.get_anti_risk_level(ctx.config or {})
self._anti_risk = AntiRisk(
level=anti_risk_level,
multiplier=acct_config.rate_limit_multiplier,
)
for room_id in acct_config.room_ids:
task = asyncio.create_task(
self._run_danmaku_monitor(room_id, acct_config, ctx, account_id),
name=f"bilibili-danmaku:{room_id}",
)
self._danmaku_tasks[room_id] = task
if acct_config.dm_enabled:
self._dm_poll_task = asyncio.create_task(
self._run_dm_poller(acct_config, ctx, account_id),
name="bilibili-dm-poller",
)
ctx.cancel_event_monitor = self._abort_signal
logger.info(
"B站网关已启动 account=%s room_count=%s dm=%s",
account_id,
len(acct_config.room_ids),
acct_config.dm_enabled,
)
return {"running": True, "account_id": account_id, "queue": self._queue}
async def stop(self, ctx) -> None:
self._running = False
if self._abort_signal:
self._abort_signal.set()
for room_id, task in self._danmaku_tasks.items():
task.cancel()
if self._dm_poll_task:
self._dm_poll_task.cancel()
for room_id, task in self._danmaku_tasks.items():
try:
await asyncio.wait_for(task, timeout=5.0)
except (TimeoutError, asyncio.CancelledError):
pass
if self._dm_poll_task:
try:
await asyncio.wait_for(self._dm_poll_task, timeout=5.0)
except (TimeoutError, asyncio.CancelledError):
pass
self._danmaku_tasks.clear()
self._dm_poll_task = None
if self._abort_signal:
self._abort_signal = None
self._queue = None
logger.info("B站网关已停止")
async def _run_danmaku_monitor(
self,
room_id: int,
account: BilibiliAccountConfig,
ctx,
account_id: str,
) -> None:
from yuxi.channel.extensions.bilibili.danmaku.monitor import DanmakuMonitor
monitor = DanmakuMonitor(
credential=self._client_manager.credential,
room_id=room_id,
account=account,
anti_risk=self._anti_risk,
runtime=ctx,
abort_signal=self._abort_signal,
account_id=account_id,
)
await monitor.run()
async def _run_dm_poller(self, account: BilibiliAccountConfig, ctx, account_id: str) -> None:
from yuxi.channel.extensions.bilibili.private_msg.poller import DMPoller
poller = DMPoller(
credential=self._client_manager.credential,
account=account,
anti_risk=self._anti_risk,
runtime=ctx,
abort_signal=self._abort_signal,
account_id=account_id,
)
await poller.run()
async def _resolve_account(self, ctx) -> tuple[dict, str]:
config = getattr(ctx, "config", {}) or {}
account_id = getattr(ctx, "account_id", "default")
self._config_adapter.list_account_ids(config)
account = await self._config_adapter.resolve_account(account_id)
return account, account_id