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

177 lines
6.1 KiB
Python

from __future__ import annotations
import logging
from yuxi.channel.extensions.bilibili.types import (
BilibiliAccountConfig,
BilibiliConfig,
is_account_configured,
)
logger = logging.getLogger(__name__)
_MERGE_FIELDS = (
"sessdata",
"bili_jct",
"dedeuserid",
"buvid3",
"buvid4",
"bili_ticket",
"require_mention",
"rate_limit_multiplier",
)
class BilibiliConfigAdapter:
def __init__(self):
self._config: dict = {}
@property
def _bl_cfg(self) -> dict:
return self._config.get("channels", {}).get("bilibili", {})
def list_account_ids(self, config: dict) -> list[str]:
self._config = config
accounts = self._bl_cfg.get("accounts", {})
if accounts:
return list(accounts.keys())
return ["default"]
async def resolve_account(self, account_id: str) -> dict:
return self._build_account(account_id)
def is_configured(self, account: dict) -> bool:
acct = self._dict_to_account_config(account)
return is_account_configured(acct)
def is_enabled(self, account: dict) -> bool:
return account.get("enabled", True)
def describe_account(self, account: dict) -> dict:
acct = self._dict_to_account_config(account)
return {
"account_id": acct.dedeuserid or "",
"uid": acct.dedeuserid,
"room_count": len(acct.room_ids),
"dm_enabled": acct.dm_enabled,
"configured": is_account_configured(acct),
}
def default_account_id(self) -> str:
return self._bl_cfg.get("default_account", "default")
def get_account_config(self, config: dict, account_id: str = "default") -> BilibiliAccountConfig:
cfg = self._parse_config(config)
account = cfg.accounts.get(account_id)
if account is None:
return BilibiliAccountConfig()
if account_id == "default":
merged = account.model_copy()
for field_name in _MERGE_FIELDS:
base_val = getattr(cfg, field_name, None)
is_empty = False
if isinstance(base_val, str) and not base_val:
is_empty = True
elif isinstance(base_val, list) and not base_val:
is_empty = True
if base_val is None or is_empty:
continue
setattr(merged, field_name, base_val)
return merged
return account
def get_anti_risk_level(self, config: dict) -> str:
cfg = self._parse_config(config)
return cfg.anti_risk_level
def config_schema(self) -> dict:
return {
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"title": "B站 渠道配置",
"properties": {
"sessdata": {
"type": "string",
"title": "SESSDATA",
"description": "B站 Cookie SESSDATA",
"x-ui-password": True,
},
"bili_jct": {
"type": "string",
"title": "bili_jct (CSRF Token)",
"description": "B站 CSRF Token",
"x-ui-password": True,
},
"dedeuserid": {
"type": "string",
"title": "DedeUserID",
"description": "B站用户 UID",
},
"room_ids": {
"type": "array",
"items": {"type": "integer"},
"title": "直播间 ID 列表",
},
"dm_policy": {
"type": "string",
"title": "DM 策略",
"enum": ["pairing", "allowlist", "open", "disabled"],
"default": "pairing",
},
"comment_enabled": {
"type": "boolean",
"title": "评论功能",
"default": False,
"x-ui-hidden": True,
},
"comment_oids": {
"type": "array",
"items": {"type": "integer"},
"title": "评论 OID 列表",
"x-ui-hidden": True,
},
"require_mention": {
"type": "boolean",
"title": "弹幕提及要求",
"default": True,
},
},
}
def _parse_config(self, config_dict: dict | None) -> BilibiliConfig:
if config_dict is None:
return BilibiliConfig()
channel_cfg = config_dict.get("channels", {}).get("bilibili", {})
if isinstance(channel_cfg, dict):
accounts_raw = channel_cfg.get("accounts", {})
accounts = {}
for aid, acct in accounts_raw.items():
if isinstance(acct, BilibiliAccountConfig):
accounts[aid] = acct
elif isinstance(acct, dict):
accounts[aid] = BilibiliAccountConfig(**acct)
return BilibiliConfig(
enabled=channel_cfg.get("enabled", True),
name=channel_cfg.get("name"),
default_account=channel_cfg.get("default_account", "default"),
accounts=accounts,
anti_risk_level=channel_cfg.get("anti_risk_level", "moderate"),
)
if isinstance(channel_cfg, BilibiliConfig):
return channel_cfg
return BilibiliConfig()
def _build_account(self, account_id: str) -> dict:
return self._get_account_or_default(account_id).model_dump()
def _get_account_or_default(self, account_id: str) -> BilibiliAccountConfig:
cfg = self._parse_config(self._config)
return cfg.accounts.get(account_id) or BilibiliAccountConfig()
@staticmethod
def _dict_to_account_config(account: dict) -> BilibiliAccountConfig:
if isinstance(account, BilibiliAccountConfig):
return account
return BilibiliAccountConfig(**account) if account else BilibiliAccountConfig()