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